手搓AI設計稿轉HTML:從Figma/Sketch自動生成代碼
本文探讨了AI驱动的设计稿转代码技术,系统介绍了从Figma/Sketch自动生成HTML/CSS/JavaScript代码的完整解决方案。首先分析了传统前端开发流程的痛点,指出60%的开发时间消耗在设计稿转代码环节。随后详细阐述了技术架构,包括设计稿解析、布局分析、UI组件识别和代码生成引擎等核心模块。重点讲解了AI增强技术,如计算机视觉分析、深度学习组件识别模型的应用。文章还提供了系统集成方案
手搓AI設計稿轉HTML:從Figma/Sketch自動生成代碼
第一章:設計稿轉代碼的革命性意義
1.1 前端開發的痛點與機遇
在當今數位化時代,前端開發正面臨著前所未有的挑戰與機遇。隨著用戶對界面體驗的要求越來越高,設計稿與最終產出代碼之間的鴻溝日益顯著。傳統的開發流程中,設計師使用Figma、Sketch等專業工具創造出精美的界面設計,而前端工程師則需要手動將這些視覺設計轉換為HTML、CSS和JavaScript代碼。這個過程不僅耗時耗力,而且容易產生誤差,導致設計還原度不足。
根據GitHub的統計數據,全球約有60%的前端開發時間花費在將設計稿轉換為代碼的過程上。這不僅造成了巨大的時間浪費,也導致了開發成本的上升。而正是這個痛點,催生了設計稿自動轉代碼技術的興起。
1.2 現有解決方案的局限性
目前市面上已經出現了一些設計稿轉代碼的工具,如Anima、Zeplin、Avocode等。這些工具在一定程度上提高了工作效率,但它們大多存在以下局限性:
-
生成代碼質量不高:生成的HTML結構往往不符合最佳實踐,CSS代碼冗餘且缺乏組織性
-
缺乏語義化標籤:大多使用div嵌套,而不是語義化的HTML5標籤
-
響應式設計支持有限:難以處理複雜的響應式佈局
-
組件化程度低:無法識別和重用常見的UI組件
-
動態交互實現困難:對於複雜的交互效果支持有限
1.3 AI驅動的解決方案優勢
人工智慧技術的發展為解決這一難題提供了全新的思路。通過機器學習和計算機視覺技術,AI能夠理解設計稿的視覺層次、佈局結構和設計意圖,從而生成更高質量的代碼。AI驅動的解決方案具有以下優勢:
-
智能識別設計模式:自動識別常見的UI模式並生成相應的組件
-
語義化標籤推斷:根據內容和結構推斷合適的HTML5標籤
-
自適應佈局生成:自動創建響應式佈局和媒體查詢
-
代碼優化:生成符合最佳實踐的精簡代碼
-
學習與改進能力:隨著使用不斷優化生成結果
第二章:技術架構與核心原理
2.1 整體架構設計
一個完整的AI設計稿轉代碼系統通常包含以下幾個核心模塊:
python
# 系統架構示意代碼
class DesignToCodeSystem:
def __init__(self):
self.design_parser = DesignParser() # 設計稿解析模塊
self.layout_analyzer = LayoutAnalyzer() # 佈局分析模塊
self.component_detector = ComponentDetector() # 組件檢測模塊
self.code_generator = CodeGenerator() # 代碼生成模塊
self.optimizer = CodeOptimizer() # 代碼優化模塊
def process(self, design_file):
# 1. 解析設計稿
design_data = self.design_parser.parse(design_file)
# 2. 分析佈局結構
layout_info = self.layout_analyzer.analyze(design_data)
# 3. 檢測UI組件
components = self.component_detector.detect(design_data)
# 4. 生成代碼
raw_code = self.code_generator.generate(layout_info, components)
# 5. 優化代碼
optimized_code = self.optimizer.optimize(raw_code)
return optimized_code
2.2 設計稿解析技術
2.2.1 Figma API解析
Figma提供了強大的REST API,允許開發者訪問設計文件中的各種元素和屬性:
javascript
// Figma API 使用示例
const fetch = require('node-fetch');
class FigmaParser {
constructor(accessToken) {
this.accessToken = accessToken;
this.baseUrl = 'https://api.figma.com/v1';
}
async getFile(fileKey) {
const response = await fetch(
`${this.baseUrl}/files/${fileKey}`,
{
headers: { 'X-Figma-Token': this.accessToken }
}
);
return await response.json();
}
async parseNode(node) {
const nodeInfo = {
id: node.id,
name: node.name,
type: node.type,
boundingBox: {
x: node.absoluteBoundingBox.x,
y: node.absoluteBoundingBox.y,
width: node.absoluteBoundingBox.width,
height: node.absoluteBoundingBox.height
},
style: this.extractStyles(node),
children: []
};
if (node.children) {
for (const child of node.children) {
nodeInfo.children.push(await this.parseNode(child));
}
}
return nodeInfo;
}
extractStyles(node) {
const styles = {};
// 提取填充樣式
if (node.fills && node.fills.length > 0) {
styles.fill = this.extractColor(node.fills[0]);
}
// 提取文字樣式
if (node.type === 'TEXT') {
styles.text = {
content: node.characters,
fontSize: node.style.fontSize,
fontFamily: node.style.fontFamily,
fontWeight: node.style.fontWeight,
lineHeight: node.style.lineHeightPx,
textAlign: node.style.textAlignHorizontal
};
}
// 提取邊框樣式
if (node.strokes && node.strokes.length > 0) {
styles.border = {
color: this.extractColor(node.strokes[0]),
width: node.strokeWeight
};
}
// 提取陰影效果
if (node.effects && node.effects.length > 0) {
styles.effects = node.effects.map(effect => ({
type: effect.type,
color: this.extractColor(effect),
offset: effect.offset,
radius: effect.radius
}));
}
return styles;
}
extractColor(colorInfo) {
if (!colorInfo.color) return null;
const { r, g, b } = colorInfo.color;
const a = colorInfo.opacity || 1;
return {
rgb: `rgb(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)})`,
rgba: `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`,
hex: this.rgbToHex(r, g, b)
};
}
rgbToHex(r, g, b) {
return "#" + ((1 << 24) + (Math.round(r * 255) << 16) +
(Math.round(g * 255) << 8) + Math.round(b * 255))
.toString(16).slice(1);
}
}
2.2.2 Sketch文件解析
Sketch文件本質上是zip壓縮包,包含了JSON格式的設計數據:
python
# Sketch文件解析器
import zipfile
import json
import plistlib
from pathlib import Path
class SketchParser:
def __init__(self, sketch_file_path):
self.sketch_file_path = sketch_file_path
self.temp_dir = Path("./temp_sketch")
def parse(self):
"""解析Sketch文件"""
# 解壓Sketch文件
with zipfile.ZipFile(self.sketch_file_path, 'r') as zip_ref:
zip_ref.extractall(self.temp_dir)
# 讀取主要文檔
document_path = self.temp_dir / "document.json"
with open(document_path, 'r', encoding='utf-8') as f:
document_data = json.load(f)
# 解析頁面和圖層
pages = []
for page in document_data.get('pages', []):
page_data = self.parse_page(page)
pages.append(page_data)
# 清理臨時文件
self.cleanup()
return {
'pages': pages,
'metadata': document_data.get('meta', {})
}
def parse_page(self, page_ref):
"""解析單個頁面"""
page_path = self.temp_dir / page_ref['_ref']
with open(page_path, 'r', encoding='utf-8') as f:
page_data = json.load(f)
layers = []
for layer in page_data.get('layers', []):
layer_data = self.parse_layer(layer)
if layer_data:
layers.append(layer_data)
return {
'name': page_data.get('name', 'Untitled'),
'layers': layers,
'frame': page_data.get('frame', {})
}
def parse_layer(self, layer_data):
"""解析單個圖層"""
layer_type = layer_data.get('_class', '')
if layer_type == 'text':
return self.parse_text_layer(layer_data)
elif layer_type == 'rectangle':
return self.parse_rectangle_layer(layer_data)
elif layer_type == 'group':
return self.parse_group_layer(layer_data)
elif layer_type == 'symbolInstance':
return self.parse_symbol_layer(layer_data)
return None
def parse_text_layer(self, layer_data):
"""解析文字圖層"""
style_data = layer_data.get('style', {})
text_style = style_data.get('textStyle', {})
return {
'type': 'text',
'name': layer_data.get('name', ''),
'frame': layer_data.get('frame', {}),
'content': layer_data.get('attributedString', {}).get('string', ''),
'style': {
'fontFamily': text_style.get('fontFamily', ''),
'fontSize': text_style.get('fontSize', 12),
'fontWeight': text_style.get('fontWeight', 400),
'color': self.parse_color(text_style.get('color', {})),
'alignment': text_style.get('alignment', 0)
}
}
def parse_rectangle_layer(self, layer_data):
"""解析矩形圖層"""
style_data = layer_data.get('style', {})
return {
'type': 'rectangle',
'name': layer_data.get('name', ''),
'frame': layer_data.get('frame', {}),
'style': {
'fill': self.parse_color(style_data.get('fills', [{}])[0]),
'border': self.parse_border(style_data.get('borders', [])),
'cornerRadius': layer_data.get('fixedRadius', 0)
}
}
def parse_color(self, color_data):
"""解析顏色數據"""
if not color_data:
return None
color = color_data.get('color', {})
alpha = color_data.get('alpha', 1)
return {
'r': color.get('red', 0),
'g': color.get('green', 0),
'b': color.get('blue', 0),
'a': alpha
}
def cleanup(self):
"""清理臨時文件"""
import shutil
if self.temp_dir.exists():
shutil.rmtree(self.temp_dir)
2.3 佈局分析算法
佈局分析是設計稿轉代碼的核心環節,需要識別元素之間的相對位置關係、對齊方式和間距:
python
# 佈局分析器
class LayoutAnalyzer:
def __init__(self):
self.threshold = 5 # 對齊閾值(像素)
def analyze(self, elements, container_size=None):
"""分析元素佈局關係"""
if not elements:
return {}
# 分析對齊關係
alignments = self.detect_alignments(elements)
# 分析間距關係
spacings = self.detect_spacings(elements)
# 分析層級關係
hierarchy = self.detect_hierarchy(elements)
# 推斷佈局類型
layout_type = self.infer_layout_type(elements, alignments)
# 計算響應式約束
constraints = self.calculate_constraints(elements, container_size)
return {
'alignments': alignments,
'spacings': spacings,
'hierarchy': hierarchy,
'layout_type': layout_type,
'constraints': constraints
}
def detect_alignments(self, elements):
"""檢測對齊關係"""
alignments = {
'left': [],
'right': [],
'top': [],
'bottom': [],
'center_x': [],
'center_y': []
}
for i, elem1 in enumerate(elements):
for j, elem2 in enumerate(elements[i+1:], i+1):
# 檢測左對齊
if abs(elem1['frame']['x'] - elem2['frame']['x']) < self.threshold:
alignments['left'].append((i, j))
# 檢測右對齊
if abs(elem1['frame']['x'] + elem1['frame']['width'] -
elem2['frame']['x'] - elem2['frame']['width']) < self.threshold:
alignments['right'].append((i, j))
# 檢測頂部對齊
if abs(elem1['frame']['y'] - elem2['frame']['y']) < self.threshold:
alignments['top'].append((i, j))
# 檢測底部對齊
if abs(elem1['frame']['y'] + elem1['frame']['height'] -
elem2['frame']['y'] - elem2['frame']['height']) < self.threshold:
alignments['bottom'].append((i, j))
# 檢測水平居中
center_x1 = elem1['frame']['x'] + elem1['frame']['width'] / 2
center_x2 = elem2['frame']['x'] + elem2['frame']['width'] / 2
if abs(center_x1 - center_x2) < self.threshold:
alignments['center_x'].append((i, j))
# 檢測垂直居中
center_y1 = elem1['frame']['y'] + elem1['frame']['height'] / 2
center_y2 = elem2['frame']['y'] + elem2['frame']['height'] / 2
if abs(center_y1 - center_y2) < self.threshold:
alignments['center_y'].append((i, j))
return alignments
def detect_spacings(self, elements):
"""檢測間距關係"""
spacings = []
elements_sorted_x = sorted(elements, key=lambda e: e['frame']['x'])
elements_sorted_y = sorted(elements, key=lambda e: e['frame']['y'])
# 檢測水平間距
for i in range(len(elements_sorted_x) - 1):
elem1 = elements_sorted_x[i]
elem2 = elements_sorted_x[i + 1]
spacing_x = elem2['frame']['x'] - (elem1['frame']['x'] + elem1['frame']['width'])
if spacing_x > 0:
# 檢查是否有重疊
overlap_y = self.calculate_y_overlap(elem1, elem2)
if overlap_y > 0:
spacings.append({
'type': 'horizontal',
'elements': [elem1['id'], elem2['id']],
'value': spacing_x,
'consistent': self.check_spacing_consistency(spacing_x, spacings, 'horizontal')
})
# 檢測垂直間距
for i in range(len(elements_sorted_y) - 1):
elem1 = elements_sorted_y[i]
elem2 = elements_sorted_y[i + 1]
spacing_y = elem2['frame']['y'] - (elem1['frame']['y'] + elem1['frame']['height'])
if spacing_y > 0:
# 檢查是否有重疊
overlap_x = self.calculate_x_overlap(elem1, elem2)
if overlap_x > 0:
spacings.append({
'type': 'vertical',
'elements': [elem1['id'], elem2['id']],
'value': spacing_y,
'consistent': self.check_spacing_consistency(spacing_y, spacings, 'vertical')
})
return spacings
def calculate_y_overlap(self, elem1, elem2):
"""計算Y軸重疊"""
y1_top = elem1['frame']['y']
y1_bottom = elem1['frame']['y'] + elem1['frame']['height']
y2_top = elem2['frame']['y']
y2_bottom = elem2['frame']['y'] + elem2['frame']['height']
return max(0, min(y1_bottom, y2_bottom) - max(y1_top, y2_top))
def infer_layout_type(self, elements, alignments):
"""推斷佈局類型"""
# 計算元素排列的特徵
if len(elements) == 0:
return 'empty'
# 檢查是否為網格佈局
if self.is_grid_layout(elements):
return 'grid'
# 檢查是否為彈性盒子佈局
if self.is_flexbox_layout(elements, alignments):
return 'flexbox'
# 檢查是否為絕對定位
if self.is_absolute_layout(elements):
return 'absolute'
return 'block'
def is_grid_layout(self, elements):
"""判斷是否為網格佈局"""
if len(elements) < 4:
return False
# 檢測元素是否按行列排列
x_positions = sorted(set(e['frame']['x'] for e in elements))
y_positions = sorted(set(e['frame']['y'] for e in elements))
# 如果有多個不同的x和y位置,可能是網格
if len(x_positions) > 1 and len(y_positions) > 1:
# 檢查元素大小是否一致
widths = [e['frame']['width'] for e in elements]
heights = [e['frame']['height'] for e in elements]
width_std = np.std(widths) if len(widths) > 1 else 0
height_std = np.std(heights) if len(heights) > 1 else 0
# 如果大小變化不大,可能是網格
if width_std < 10 and height_std < 10:
return True
return False
def calculate_constraints(self, elements, container_size):
"""計算響應式約束"""
constraints = {}
if not container_size:
return constraints
container_width = container_size['width']
container_height = container_size['height']
for elem in elements:
elem_constraints = {}
frame = elem['frame']
# 計算相對位置百分比
elem_constraints['left_percent'] = frame['x'] / container_width * 100
elem_constraints['top_percent'] = frame['y'] / container_height * 100
elem_constraints['width_percent'] = frame['width'] / container_width * 100
elem_constraints['height_percent'] = frame['height'] / container_height * 100
# 判斷是否應該使用固定寬高
elem_constraints['fixed_width'] = frame['width'] < 100 # 小於100px可能應該固定
elem_constraints['fixed_height'] = frame['height'] < 50
# 判斷是否應該保持寬高比
if elem.get('type') == 'image' and frame['width'] > 0 and frame['height'] > 0:
elem_constraints['aspect_ratio'] = frame['width'] / frame['height']
constraints[elem['id']] = elem_constraints
return constraints
2.4 UI組件識別技術
使用機器學習技術識別常見的UI組件,可以提高代碼生成的質量和可重用性:
python
# UI組件識別器
import numpy as np
from sklearn.cluster import DBSCAN
from sklearn.preprocessing import StandardScaler
class ComponentDetector:
def __init__(self):
self.component_patterns = self.load_component_patterns()
self.feature_extractor = FeatureExtractor()
def load_component_patterns(self):
"""加載已知的組件模式"""
return {
'button': {
'features': ['rounded_corners', 'centered_text', 'hover_effect'],
'aspect_ratio_range': (2, 8),
'size_range': (30, 200)
},
'input': {
'features': ['rectangular', 'placeholder_text', 'border'],
'aspect_ratio_range': (4, 15),
'size_range': (100, 400)
},
'card': {
'features': ['rounded_corners', 'shadow', 'image_area', 'text_content'],
'aspect_ratio_range': (0.5, 2),
'size_range': (150, 500)
},
'navbar': {
'features': ['horizontal_layout', 'multiple_items', 'logo', 'menu_items'],
'aspect_ratio_range': (5, 20),
'height_range': (40, 100)
}
}
def detect(self, elements, layout_info):
"""檢測UI組件"""
components = []
# 提取特徵
features = []
for elem in elements:
feature_vector = self.feature_extractor.extract(elem)
features.append(feature_vector)
features = np.array(features)
# 聚類相似的UI元素
clusters = self.cluster_elements(features)
# 識別組件類型
for cluster_id, element_indices in enumerate(clusters):
if len(element_indices) < 1:
continue
# 獲取聚類中的元素
cluster_elements = [elements[i] for i in element_indices]
# 識別組件類型
component_type = self.identify_component_type(cluster_elements)
if component_type:
# 創建組件實例
component = {
'type': component_type,
'elements': cluster_elements,
'properties': self.extract_component_properties(cluster_elements, component_type),
'layout': self.analyze_component_layout(cluster_elements)
}
components.append(component)
# 檢測嵌套組件
nested_components = self.detect_nested_components(components)
components.extend(nested_components)
return components
def cluster_elements(self, features):
"""聚類相似的UI元素"""
# 標準化特徵
scaler = StandardScaler()
features_scaled = scaler.fit_transform(features)
# 使用DBSCAN進行聚類
# eps: 鄰域半徑,min_samples: 最小樣本數
dbscan = DBSCAN(eps=0.5, min_samples=1)
labels = dbscan.fit_predict(features_scaled)
# 組織聚類結果
clusters = {}
for i, label in enumerate(labels):
if label not in clusters:
clusters[label] = []
clusters[label].append(i)
return list(clusters.values())
def identify_component_type(self, elements):
"""識別組件類型"""
if len(elements) == 1:
element = elements[0]
# 單個元素的組件識別
if element['type'] == 'text':
# 檢查是否為按鈕
if self.is_button(element):
return 'button'
elif self.is_input_label(element):
return 'input_label'
elif element['type'] == 'rectangle':
# 檢查是否為輸入框或卡片
if self.is_input_field(element):
return 'input'
elif self.is_card(element):
return 'card'
elif len(elements) > 1:
# 多個元素的組件識別
if self.is_navigation_bar(elements):
return 'navbar'
elif self.is_card_component(elements):
return 'card'
elif self.is_form_group(elements):
return 'form_group'
return None
def is_button(self, element):
"""判斷元素是否為按鈕"""
if element['type'] != 'rectangle':
return False
# 檢查是否有圓角
style = element.get('style', {})
corner_radius = style.get('cornerRadius', 0)
# 檢查尺寸比例
frame = element['frame']
aspect_ratio = frame['width'] / frame['height'] if frame['height'] > 0 else 0
# 檢查是否有文字內容
has_text = any(child.get('type') == 'text' for child in element.get('children', []))
# 檢查是否有陰影或邊框
has_shadow = 'shadow' in style.get('effects', [])
has_border = 'border' in style
return (corner_radius > 0 or has_shadow) and 2 <= aspect_ratio <= 8 and has_text
def extract_component_properties(self, elements, component_type):
"""提取組件屬性"""
properties = {}
if component_type == 'button':
# 提取按鈕屬性
main_element = elements[0] if elements else {}
properties.update({
'text': self.extract_text_content(main_element),
'backgroundColor': main_element.get('style', {}).get('fill'),
'textColor': self.extract_text_color(main_element),
'borderRadius': main_element.get('style', {}).get('cornerRadius', 0),
'size': {
'width': main_element.get('frame', {}).get('width'),
'height': main_element.get('frame', {}).get('height')
}
})
elif component_type == 'input':
# 提取輸入框屬性
main_element = elements[0] if elements else {}
properties.update({
'placeholder': self.extract_placeholder(main_element),
'type': self.infer_input_type(main_element),
'border': main_element.get('style', {}).get('border'),
'size': main_element.get('frame', {})
})
return properties
def analyze_component_layout(self, elements):
"""分析組件內部佈局"""
if len(elements) == 1:
return {'type': 'single', 'layout': 'absolute'}
# 分析多個元素的佈局關係
layout_analyzer = LayoutAnalyzer()
layout_info = layout_analyzer.analyze(elements)
# 根據佈局信息推斷CSS佈局方式
if layout_info['layout_type'] == 'flexbox':
return {
'type': 'flex',
'direction': self.infer_flex_direction(elements, layout_info),
'justify': self.infer_justify_content(elements, layout_info),
'align': self.infer_align_items(elements, layout_info)
}
elif layout_info['layout_type'] == 'grid':
return {
'type': 'grid',
'columns': self.infer_grid_columns(elements, layout_info),
'rows': self.infer_grid_rows(elements, layout_info),
'gaps': layout_info['spacings']
}
return {'type': 'complex', 'layout': 'absolute'}
第三章:代碼生成引擎實現
3.1 HTML結構生成
javascript
// HTML生成器
class HTMLGenerator {
constructor() {
this.indentLevel = 0;
this.indentSize = 2;
}
generate(layoutInfo, components) {
// 生成HTML文檔結構
let html = `<!DOCTYPE html>\n`;
html += `<html lang="zh-Hant">\n`;
html += this.generateHead();
html += this.generateBody(layoutInfo, components);
html += `</html>`;
return html;
}
generateHead() {
const indent = ' '.repeat(this.indentSize);
let head = `<head>\n`;
head += `${indent}<meta charset="UTF-8">\n`;
head += `${indent}<meta name="viewport" content="width=device-width, initial-scale=1.0">\n`;
head += `${indent}<title>設計稿轉換頁面</title>\n`;
head += `${indent}<link rel="stylesheet" href="styles.css">\n`;
head += `${indent}<!-- 生成時間: ${new Date().toLocaleString()} -->\n`;
head += `</head>\n`;
return head;
}
generateBody(layoutInfo, components) {
this.indentLevel = 1;
const indent = ' '.repeat(this.indentSize * this.indentLevel);
let body = `<body>\n`;
// 根據佈局類型生成不同的結構
switch (layoutInfo.layout_type) {
case 'grid':
body += this.generateGridLayout(layoutInfo, components);
break;
case 'flexbox':
body += this.generateFlexboxLayout(layoutInfo, components);
break;
default:
body += this.generateDefaultLayout(layoutInfo, components);
}
body += `</body>\n`;
return body;
}
generateGridLayout(layoutInfo, components) {
this.indentLevel++;
let html = '';
const indent = ' '.repeat(this.indentSize * this.indentLevel);
// 創建網格容器
const gridTemplateColumns = this.calculateGridColumns(layoutInfo);
const gridTemplateRows = this.calculateGridRows(layoutInfo);
html += `${indent}<div class="grid-container" \n`;
html += `${' '.repeat(this.indentSize * (this.indentLevel + 1))}style="display: grid;\n`;
html += `${' '.repeat(this.indentSize * (this.indentLevel + 2))}grid-template-columns: ${gridTemplateColumns};\n`;
html += `${' '.repeat(this.indentSize * (this.indentLevel + 2))}grid-template-rows: ${gridTemplateRows};\n`;
// 添加間隙
if (layoutInfo.spacings && layoutInfo.spacings.length > 0) {
const gap = this.calculateConsistentGap(layoutInfo.spacings);
html += `${' '.repeat(this.indentSize * (this.indentLevel + 2))}gap: ${gap}px;">\n`;
} else {
html += `${' '.repeat(this.indentSize * (this.indentLevel + 2))}">\n`;
}
// 添加子元素
this.indentLevel++;
html += this.generateGridItems(layoutInfo, components);
this.indentLevel--;
html += `${indent}</div>\n`;
this.indentLevel--;
return html;
}
generateGridItems(layoutInfo, components) {
let html = '';
const indent = ' '.repeat(this.indentSize * this.indentLevel);
// 將元素分配到網格單元格
const gridItems = this.assignElementsToGrid(layoutInfo, components);
gridItems.forEach((item, index) => {
const { element, row, column, rowSpan, colSpan } = item;
html += `${indent}<div class="grid-item" `;
// 添加網格位置
html += `style="grid-row: ${row} / span ${rowSpan}; `;
html += `grid-column: ${column} / span ${colSpan};">\n`;
// 生成元素內容
this.indentLevel++;
html += this.generateElement(element);
this.indentLevel--;
html += `${indent}</div>\n`;
});
return html;
}
generateFlexboxLayout(layoutInfo, components) {
this.indentLevel++;
let html = '';
const indent = ' '.repeat(this.indentSize * this.indentLevel);
// 確定flex方向
const flexDirection = this.determineFlexDirection(layoutInfo);
const justifyContent = this.determineJustifyContent(layoutInfo);
const alignItems = this.determineAlignItems(layoutInfo);
html += `${indent}<div class="flex-container" \n`;
html += `${' '.repeat(this.indentSize * (this.indentLevel + 1))}style="display: flex;\n`;
html += `${' '.repeat(this.indentSize * (this.indentLevel + 2))}flex-direction: ${flexDirection};\n`;
html += `${' '.repeat(this.indentSize * (this.indentLevel + 2))}justify-content: ${justifyContent};\n`;
html += `${' '.repeat(this.indentSize * (this.indentLevel + 2))}align-items: ${alignItems};">\n`;
// 生成flex項目
this.indentLevel++;
components.forEach(component => {
html += this.generateComponent(component);
});
this.indentLevel--;
html += `${indent}</div>\n`;
this.indentLevel--;
return html;
}
generateComponent(component) {
const indent = ' '.repeat(this.indentSize * this.indentLevel);
let html = '';
// 根據組件類型生成不同的HTML
switch (component.type) {
case 'button':
html += this.generateButton(component);
break;
case 'input':
html += this.generateInput(component);
break;
case 'card':
html += this.generateCard(component);
break;
case 'navbar':
html += this.generateNavbar(component);
break;
default:
html += this.generateGenericElement(component);
}
return html;
}
generateButton(component) {
const indent = ' '.repeat(this.indentSize * this.indentLevel);
const props = component.properties || {};
let button = `${indent}<button type="button" class="btn"`;
// 添加樣式屬性
if (props.backgroundColor) {
button += ` style="background-color: ${props.backgroundColor.rgba};`;
if (props.textColor) {
button += ` color: ${props.textColor.rgba};`;
}
if (props.borderRadius) {
button += ` border-radius: ${props.borderRadius}px;`;
}
button += `"`;
}
button += `>${props.text || '按鈕'}</button>\n`;
return button;
}
generateInput(component) {
const indent = ' '.repeat(this.indentSize * this.indentLevel);
const props = component.properties || {};
let input = `${indent}<div class="input-group">\n`;
this.indentLevel++;
const childIndent = ' '.repeat(this.indentSize * this.indentLevel);
// 如果有標籤,生成標籤
if (props.label) {
input += `${childIndent}<label for="input-${component.id}">${props.label}</label>\n`;
}
// 生成輸入框
input += `${childIndent}<input type="${props.type || 'text'}" `;
input += `id="input-${component.id}" `;
input += `placeholder="${props.placeholder || ''}" `;
// 添加樣式
if (props.border) {
input += `style="border: ${props.border.width}px solid ${props.border.color.rgba};" `;
}
input += `/>\n`;
this.indentLevel--;
input += `${indent}</div>\n`;
return input;
}
generateCard(component) {
const indent = ' '.repeat(this.indentSize * this.indentLevel);
const elements = component.elements || [];
let card = `${indent}<div class="card">\n`;
this.indentLevel++;
// 生成卡片內容
elements.forEach(element => {
card += this.generateElement(element);
});
this.indentLevel--;
card += `${indent}</div>\n`;
return card;
}
generateNavbar(component) {
const indent = ' '.repeat(this.indentSize * this.indentLevel);
const elements = component.elements || [];
const layout = component.layout || {};
let navbar = `${indent}<nav class="navbar">\n`;
this.indentLevel++;
const childIndent = ' '.repeat(this.indentSize * this.indentLevel);
// 生成logo
const logo = elements.find(e => e.name.toLowerCase().includes('logo'));
if (logo) {
navbar += `${childIndent}<div class="navbar-logo">\n`;
this.indentLevel++;
navbar += this.generateElement(logo);
this.indentLevel--;
navbar += `${childIndent}</div>\n`;
}
// 生成導航項目
const menuItems = elements.filter(e =>
e.type === 'text' && !e.name.toLowerCase().includes('logo')
);
if (menuItems.length > 0) {
navbar += `${childIndent}<ul class="navbar-menu" `;
if (layout.type === 'flex') {
navbar += `style="display: flex; flex-direction: ${layout.direction || 'row'};"`;
}
navbar += `>\n`;
this.indentLevel++;
menuItems.forEach(item => {
const itemIndent = ' '.repeat(this.indentSize * this.indentLevel);
navbar += `${itemIndent}<li><a href="#">${item.content || item.name}</a></li>\n`;
});
this.indentLevel--;
navbar += `${childIndent}</ul>\n`;
}
this.indentLevel--;
navbar += `${indent}</nav>\n`;
return navbar;
}
calculateGridColumns(layoutInfo) {
// 根據元素位置計算網格列
const xPositions = new Set();
layoutInfo.elements.forEach(element => {
xPositions.add(element.frame.x);
xPositions.add(element.frame.x + element.frame.width);
});
const sortedPositions = Array.from(xPositions).sort((a, b) => a - b);
const columns = [];
for (let i = 1; i < sortedPositions.length; i++) {
const width = sortedPositions[i] - sortedPositions[i - 1];
columns.push(`${width}px`);
}
return columns.join(' ');
}
assignElementsToGrid(layoutInfo, components) {
// 將元素分配到網格單元格
const gridItems = [];
const grid = this.createGridStructure(layoutInfo);
components.forEach(component => {
const element = component.elements ? component.elements[0] : component;
const frame = element.frame;
// 查找元素所在的網格單元格
const startCol = this.findGridColumn(grid.columns, frame.x);
const endCol = this.findGridColumn(grid.columns, frame.x + frame.width);
const startRow = this.findGridRow(grid.rows, frame.y);
const endRow = this.findGridRow(grid.rows, frame.y + frame.height);
if (startCol !== -1 && endCol !== -1 && startRow !== -1 && endRow !== -1) {
gridItems.push({
element: element,
column: startCol + 1, // CSS網格從1開始
row: startRow + 1,
colSpan: endCol - startCol,
rowSpan: endRow - startRow
});
}
});
return gridItems;
}
}
3.2 CSS樣式生成
python
# CSS生成器
class CSSGenerator:
def __init__(self):
self.css_variables = {}
self.media_queries = {}
self.component_styles = {}
def generate(self, layout_info, components, design_data):
"""生成完整的CSS樣式表"""
css = []
# 1. 生成CSS變量
css.append(self.generate_css_variables(design_data))
# 2. 生成全局樣式
css.append(self.generate_global_styles())
# 3. 生成佈局樣式
css.append(self.generate_layout_styles(layout_info))
# 4. 生成組件樣式
css.append(self.generate_component_styles(components))
# 5. 生成響應式樣式
css.append(self.generate_responsive_styles(layout_info, components))
# 6. 生成動畫樣式
css.append(self.generate_animation_styles(design_data))
return '\n'.join(css)
def generate_css_variables(self, design_data):
"""生成CSS變量(設計令牌)"""
variables = []
# 提取顏色變量
colors = self.extract_colors(design_data)
for name, value in colors.items():
variables.append(f' --color-{name}: {value};')
# 提取字體變量
fonts = self.extract_fonts(design_data)
for name, value in fonts.items():
variables.append(f' --font-{name}: {value};')
# 提取間距變量
spacings = self.extract_spacings(design_data)
for name, value in spacings.items():
variables.append(f' --spacing-{name}: {value}px;')
# 提取陰影變量
shadows = self.extract_shadows(design_data)
for name, value in shadows.items():
variables.append(f' --shadow-{name}: {value};')
if variables:
return f':root {{\n' + '\n'.join(variables) + '\n}\n'
return ''
def extract_colors(self, design_data):
"""從設計數據中提取顏色"""
colors = {}
def traverse_elements(elements):
for element in elements:
# 提取填充顏色
if 'style' in element and 'fill' in element['style']:
fill = element['style']['fill']
if fill and 'hex' in fill:
color_name = self.generate_color_name(element)
colors[color_name] = fill['hex']
# 提取文字顏色
if 'style' in element and 'text' in element['style']:
text_style = element['style']['text']
if 'color' in text_style and 'hex' in text_style['color']:
color_name = f'text-{self.generate_color_name(element)}'
colors[color_name] = text_style['color']['hex']
# 提取邊框顏色
if 'style' in element and 'border' in element['style']:
border = element['style']['border']
if border and 'color' in border and 'hex' in border['color']:
color_name = f'border-{self.generate_color_name(element)}'
colors[color_name] = border['color']['hex']
# 遞歸處理子元素
if 'children' in element:
traverse_elements(element['children'])
traverse_elements(design_data.get('layers', []))
return colors
def generate_color_name(self, element):
"""生成顏色名稱"""
element_name = element.get('name', '').lower()
# 移除特殊字符
element_name = re.sub(r'[^a-z0-9]', '-', element_name)
# 根據元素類型添加前綴
if element.get('type') == 'text':
return f'text-{element_name}'
elif element.get('type') == 'rectangle':
return f'surface-{element_name}'
else:
return element_name
def generate_layout_styles(self, layout_info):
"""生成佈局樣式"""
styles = []
# 根據佈局類型生成不同的CSS
if layout_info.get('layout_type') == 'grid':
styles.extend(self.generate_grid_styles(layout_info))
elif layout_info.get('layout_type') == 'flexbox':
styles.extend(self.generate_flexbox_styles(layout_info))
# 生成對齊樣式
styles.extend(self.generate_alignment_styles(layout_info))
# 生成間距樣式
styles.extend(self.generate_spacing_styles(layout_info))
return '\n'.join(styles)
def generate_grid_styles(self, layout_info):
"""生成網格佈局樣式"""
styles = []
# 生成網格容器樣式
grid_container_style = '.grid-container {\n'
grid_container_style += ' display: grid;\n'
# 添加網格模板
columns = self.calculate_grid_template_columns(layout_info)
rows = self.calculate_grid_template_rows(layout_info)
grid_container_style += f' grid-template-columns: {columns};\n'
grid_container_style += f' grid-template-rows: {rows};\n'
# 添加間隙
gap = self.calculate_grid_gap(layout_info)
if gap:
grid_container_style += f' gap: {gap}px;\n'
grid_container_style += '}\n'
styles.append(grid_container_style)
# 生成網格項目樣式
grid_item_style = '.grid-item {\n'
grid_item_style += ' /* 網格項目基礎樣式 */\n'
grid_item_style += '}\n'
styles.append(grid_item_style)
return styles
def generate_flexbox_styles(self, layout_info):
"""生成彈性盒子佈局樣式"""
styles = []
# 生成flex容器樣式
flex_container_style = '.flex-container {\n'
flex_container_style += ' display: flex;\n'
# 添加flex方向
direction = self.determine_flex_direction(layout_info)
flex_container_style += f' flex-direction: {direction};\n'
# 添加對齊方式
justify = self.determine_justify_content(layout_info)
align = self.determine_align_items(layout_info)
flex_container_style += f' justify-content: {justify};\n'
flex_container_style += f' align-items: {align};\n'
# 添加換行
if self.should_wrap(layout_info):
flex_container_style += ' flex-wrap: wrap;\n'
flex_container_style += '}\n'
styles.append(flex_container_style)
return styles
def generate_component_styles(self, components):
"""生成組件樣式"""
styles = []
for component in components:
component_style = self.generate_single_component_style(component)
if component_style:
styles.append(component_style)
return '\n'.join(styles)
def generate_single_component_style(self, component):
"""生成單個組件的樣式"""
component_type = component.get('type')
properties = component.get('properties', {})
layout = component.get('layout', {})
if not component_type:
return ''
# 選擇器
selector = f'.{component_type}'
if component.get('id'):
selector += f'#{component["id"]}'
style = f'{selector} {{\n'
# 基礎樣式
if properties.get('backgroundColor'):
color = properties['backgroundColor']
style += f' background-color: {color.get("rgba", "#fff")};\n'
if properties.get('textColor'):
color = properties['textColor']
style += f' color: {color.get("rgba", "#000")};\n'
if properties.get('borderRadius'):
radius = properties['borderRadius']
style += f' border-radius: {radius}px;\n'
# 尺寸樣式
if properties.get('size'):
size = properties['size']
if size.get('width'):
style += f' width: {size["width"]}px;\n'
if size.get('height'):
style += f' height: {size["height"]}px;\n'
# 佈局樣式
if layout.get('type') == 'flex':
style += f' display: flex;\n'
if layout.get('direction'):
style += f' flex-direction: {layout["direction"]};\n'
if layout.get('justify'):
style += f' justify-content: {layout["justify"]};\n'
if layout.get('align'):
style += f' align-items: {layout["align"]};\n'
style += '}\n'
# 生成偽類樣式
style += self.generate_pseudo_styles(component)
return style
def generate_pseudo_styles(self, component):
"""生成偽類樣式(hover, focus等)"""
pseudo_styles = ''
component_type = component.get('type')
if component_type == 'button':
# 按鈕hover效果
pseudo_styles += f'.{component_type}:hover {{\n'
pseudo_styles += ' opacity: 0.9;\n'
pseudo_styles += ' transform: translateY(-1px);\n'
pseudo_styles += ' transition: all 0.2s ease;\n'
pseudo_styles += '}\n'
# 按鈕active效果
pseudo_styles += f'.{component_type}:active {{\n'
pseudo_styles += ' transform: translateY(0);\n'
pseudo_styles += '}\n'
elif component_type == 'input':
# 輸入框focus效果
pseudo_styles += f'.{component_type}:focus {{\n'
pseudo_styles += ' border-color: var(--color-primary);\n'
pseudo_styles += ' outline: none;\n'
pseudo_styles += ' box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);\n'
pseudo_styles += '}\n'
return pseudo_styles
def generate_responsive_styles(self, layout_info, components):
"""生成響應式樣式"""
styles = []
# 默認斷點
breakpoints = {
'sm': 640,
'md': 768,
'lg': 1024,
'xl': 1280
}
for name, width in breakpoints.items():
style = f'@media (min-width: {width}px) {{\n'
# 生成響應式佈局樣式
style += self.generate_responsive_layout(width, layout_info)
# 生成響應式組件樣式
style += self.generate_responsive_components(width, components)
style += '}\n'
styles.append(style)
return '\n'.join(styles)
def generate_responsive_layout(self, breakpoint_width, layout_info):
"""生成特定斷點的佈局樣式"""
style = ''
# 調整網格佈局
if layout_info.get('layout_type') == 'grid':
style += ' .grid-container {\n'
# 根據斷點調整網格列數
columns = self.adjust_grid_columns_for_breakpoint(breakpoint_width, layout_info)
style += f' grid-template-columns: repeat({columns}, 1fr);\n'
style += ' }\n'
# 調整flex佈局
elif layout_info.get('layout_type') == 'flexbox':
style += ' .flex-container {\n'
# 根據斷點調整flex方向
if breakpoint_width >= 768:
style += ' flex-direction: row;\n'
else:
style += ' flex-direction: column;\n'
style += ' }\n'
return style
def adjust_grid_columns_for_breakpoint(self, breakpoint_width, layout_info):
"""根據斷點調整網格列數"""
if breakpoint_width < 640:
return 1
elif breakpoint_width < 768:
return 2
elif breakpoint_width < 1024:
return 3
else:
return 4
def generate_animation_styles(self, design_data):
"""生成動畫樣式"""
styles = []
# 淡入動畫
fade_in = '''@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.5s ease-in;
}'''
styles.append(fade_in)
# 滑入動畫
slide_in = '''@keyframes slideInFromLeft {
0% { transform: translateX(-100%); }
100% { transform: translateX(0); }
}
.slide-in-left {
animation: slideInFromLeft 0.3s ease-out;
}'''
styles.append(slide_in)
return '\n'.join(styles)
3.3 JavaScript交互生成
javascript
// JavaScript交互生成器
class JSGenerator {
constructor() {
this.eventHandlers = new Map();
this.componentInteractions = new Map();
}
generate(components, designData) {
const jsCode = [];
// 1. 生成文檔就緒事件
jsCode.push(this.generateDocumentReady());
// 2. 生成組件交互邏輯
components.forEach(component => {
const componentJS = this.generateComponentJS(component);
if (componentJS) {
jsCode.push(componentJS);
}
});
// 3. 生成全局函數
jsCode.push(this.generateUtilityFunctions());
// 4. 生成事件綁定
jsCode.push(this.generateEventBindings());
// 5. 生成初始化代碼
jsCode.push(this.generateInitialization());
return jsCode.join('\n\n');
}
generateDocumentReady() {
return `document.addEventListener('DOMContentLoaded', function() {
console.log('頁面加載完成');
initializeComponents();
});`;
}
generateComponentJS(component) {
const componentType = component.type;
const componentId = component.id || '';
switch (componentType) {
case 'button':
return this.generateButtonJS(component);
case 'input':
return this.generateInputJS(component);
case 'navbar':
return this.generateNavbarJS(component);
case 'dropdown':
return this.generateDropdownJS(component);
case 'modal':
return this.generateModalJS(component);
default:
return '';
}
}
generateButtonJS(component) {
const buttonId = component.id ? `btn-${component.id}` : 'dynamic-btn';
const buttonText = component.properties?.text || '按鈕';
return `// 按鈕組件: ${buttonText}
const ${buttonId.replace('-', '_')} = document.getElementById('${buttonId}');
if (${buttonId.replace('-', '_')}) {
${buttonId.replace('-', '_')}.addEventListener('click', function(event) {
event.preventDefault();
console.log('按鈕被點擊: ${buttonText}');
// 添加點擊反饋
this.style.transform = 'scale(0.95)';
setTimeout(() => {
this.style.transform = 'scale(1)';
}, 150);
// 觸發自定義事件
const buttonEvent = new CustomEvent('button-click', {
detail: {
id: '${component.id}',
text: '${buttonText}',
timestamp: new Date().toISOString()
},
bubbles: true
});
this.dispatchEvent(buttonEvent);
});
// hover效果
${buttonId.replace('-', '_')}.addEventListener('mouseenter', function() {
this.style.filter = 'brightness(1.1)';
});
${buttonId.replace('-', '_')}.addEventListener('mouseleave', function() {
this.style.filter = 'brightness(1)';
});
}`;
}
generateInputJS(component) {
const inputId = component.id ? `input-${component.id}` : 'dynamic-input';
const placeholder = component.properties?.placeholder || '';
return `// 輸入框組件
const ${inputId.replace('-', '_')} = document.getElementById('${inputId}');
if (${inputId.replace('-', '_')}) {
// 輸入驗證
${inputId.replace('-', '_')}.addEventListener('input', function(event) {
const value = event.target.value;
// 清除之前的驗證狀態
this.classList.remove('is-valid', 'is-invalid');
// 根據輸入類型進行驗證
const type = this.getAttribute('type');
switch(type) {
case 'email':
if (this.validateEmail(value)) {
this.classList.add('is-valid');
} else if (value.length > 0) {
this.classList.add('is-invalid');
}
break;
case 'password':
if (value.length >= 8) {
this.classList.add('is-valid');
} else if (value.length > 0) {
this.classList.add('is-invalid');
}
break;
default:
// 文本輸入驗證
if (value.trim().length > 0) {
this.classList.add('is-valid');
}
}
});
// 焦點處理
${inputId.replace('-', '_')}.addEventListener('focus', function() {
this.parentElement.classList.add('focused');
});
${inputId.replace('-', '_')}.addEventListener('blur', function() {
this.parentElement.classList.remove('focused');
});
// 添加驗證方法
${inputId.replace('-', '_')}.validateEmail = function(email) {
const re = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
return re.test(email);
};
}`;
}
generateNavbarJS(component) {
return `// 導航欄組件
class Navbar {
constructor(elementId) {
this.navbar = document.getElementById(elementId);
this.menuItems = this.navbar ? this.navbar.querySelectorAll('.nav-item') : [];
this.mobileMenuBtn = this.navbar ? this.navbar.querySelector('.mobile-menu-btn') : null;
this.mobileMenu = this.navbar ? this.navbar.querySelector('.mobile-menu') : null;
this.init();
}
init() {
// 高亮當前頁面
this.highlightCurrentPage();
// 移動端菜單切換
if (this.mobileMenuBtn && this.mobileMenu) {
this.mobileMenuBtn.addEventListener('click', () => {
this.toggleMobileMenu();
});
}
// 平滑滾動
this.setupSmoothScroll();
}
highlightCurrentPage() {
const currentPath = window.location.pathname;
this.menuItems.forEach(item => {
const link = item.querySelector('a');
if (link && link.getAttribute('href') === currentPath) {
item.classList.add('active');
}
});
}
toggleMobileMenu() {
this.mobileMenu.classList.toggle('show');
this.mobileMenuBtn.classList.toggle('active');
// 更新按鈕文字
const isOpen = this.mobileMenu.classList.contains('show');
this.mobileMenuBtn.innerHTML = isOpen ?
'<span class="close-icon">×</span>' :
'<span class="menu-icon">☰</span>';
}
setupSmoothScroll() {
this.menuItems.forEach(item => {
const link = item.querySelector('a');
if (link && link.getAttribute('href').startsWith('#')) {
link.addEventListener('click', (event) => {
event.preventDefault();
const targetId = link.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop - 80,
behavior: 'smooth'
});
// 關閉移動端菜單
if (this.mobileMenu) {
this.mobileMenu.classList.remove('show');
this.mobileMenuBtn.classList.remove('active');
}
}
});
}
});
}
}
// 初始化導航欄
const navbar = new Navbar('main-navbar');`;
}
generateDropdownJS(component) {
const dropdownId = component.id ? `dropdown-${component.id}` : 'dynamic-dropdown';
return `// 下拉菜單組件
class Dropdown {
constructor(elementId) {
this.dropdown = document.getElementById(elementId);
this.toggleBtn = this.dropdown ? this.dropdown.querySelector('.dropdown-toggle') : null;
this.menu = this.dropdown ? this.dropdown.querySelector('.dropdown-menu') : null;
this.init();
}
init() {
if (!this.toggleBtn || !this.menu) return;
// 點擊切換
this.toggleBtn.addEventListener('click', (event) => {
event.stopPropagation();
this.toggle();
});
// 點擊其他地方關閉
document.addEventListener('click', () => {
this.hide();
});
// 防止菜單內部點擊觸發關閉
this.menu.addEventListener('click', (event) => {
event.stopPropagation();
});
// 鍵盤導航
this.setupKeyboardNavigation();
}
toggle() {
this.menu.classList.toggle('show');
this.toggleBtn.setAttribute('aria-expanded',
this.menu.classList.contains('show'));
// 更新箭頭方向
const arrow = this.toggleBtn.querySelector('.arrow');
if (arrow) {
arrow.style.transform = this.menu.classList.contains('show') ?
'rotate(180deg)' : 'rotate(0deg)';
}
}
show() {
this.menu.classList.add('show');
this.toggleBtn.setAttribute('aria-expanded', 'true');
}
hide() {
this.menu.classList.remove('show');
this.toggleBtn.setAttribute('aria-expanded', 'false');
}
setupKeyboardNavigation() {
this.toggleBtn.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
this.toggle();
} else if (event.key === 'Escape') {
this.hide();
}
});
// 菜單項鍵盤導航
const menuItems = this.menu.querySelectorAll('.dropdown-item');
menuItems.forEach((item, index) => {
item.addEventListener('keydown', (event) => {
switch(event.key) {
case 'ArrowDown':
event.preventDefault();
const nextItem = menuItems[index + 1];
if (nextItem) nextItem.focus();
break;
case 'ArrowUp':
event.preventDefault();
const prevItem = menuItems[index - 1];
if (prevItem) prevItem.focus();
if (index === 0) this.toggleBtn.focus();
break;
case 'Escape':
this.hide();
this.toggleBtn.focus();
break;
}
});
});
}
}
// 初始化下拉菜單
const dropdown = new Dropdown('${dropdownId}');`;
}
generateUtilityFunctions() {
return `// 工具函數
const Utils = {
// 防抖函數
debounce: function(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
// 節流函數
throttle: function(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
},
// 檢測移動設備
isMobile: function() {
return window.innerWidth <= 768;
},
// 格式化日期
formatDate: function(date, format = 'YYYY-MM-DD') {
const d = new Date(date);
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day);
},
// 深拷貝
deepClone: function(obj) {
return JSON.parse(JSON.stringify(obj));
},
// 隨機ID生成
generateId: function(prefix = 'id') {
return prefix + '-' + Math.random().toString(36).substr(2, 9);
}
};`;
}
generateEventBindings() {
return `// 全局事件綁定
function setupGlobalEvents() {
// 窗口大小變化
window.addEventListener('resize', Utils.debounce(function() {
console.log('窗口大小改變:', window.innerWidth, 'x', window.innerHeight);
// 觸發自定義事件
const resizeEvent = new CustomEvent('viewport-change', {
detail: {
width: window.innerWidth,
height: window.innerHeight,
isMobile: Utils.isMobile()
}
});
window.dispatchEvent(resizeEvent);
}, 250));
// 滾動事件
window.addEventListener('scroll', Utils.throttle(function() {
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const scrollEvent = new CustomEvent('page-scroll', {
detail: { scrollTop: scrollTop }
});
window.dispatchEvent(scrollEvent);
}, 100));
// 點擊外部關閉所有下拉菜單
document.addEventListener('click', function(event) {
const dropdowns = document.querySelectorAll('.dropdown-menu.show');
dropdowns.forEach(dropdown => {
if (!dropdown.parentElement.contains(event.target)) {
dropdown.classList.remove('show');
}
});
});
}`;
}
generateInitialization() {
return `// 初始化函數
function initializeComponents() {
console.log('初始化組件...');
// 設置全局事件
setupGlobalEvents();
// 初始化所有按鈕
const buttons = document.querySelectorAll('button[data-component="button"]');
buttons.forEach(button => {
button.addEventListener('click', handleButtonClick);
});
// 初始化所有輸入框
const inputs = document.querySelectorAll('input[data-component="input"]');
inputs.forEach(input => {
input.addEventListener('input', handleInputChange);
});
// 初始化表單驗證
const forms = document.querySelectorAll('form');
forms.forEach(form => {
form.addEventListener('submit', handleFormSubmit);
});
// 添加CSS類到body標籤
document.body.classList.add('js-enabled');
console.log('組件初始化完成');
}
// 事件處理函數
function handleButtonClick(event) {
const button = event.currentTarget;
console.log('按鈕點擊:', button.textContent);
// 添加點擊反饋
button.classList.add('clicked');
setTimeout(() => {
button.classList.remove('clicked');
}, 300);
}
function handleInputChange(event) {
const input = event.currentTarget;
console.log('輸入變化:', input.value);
}
function handleFormSubmit(event) {
event.preventDefault();
console.log('表單提交');
// 這裡可以添加表單驗證和提交邏輯
const form = event.currentTarget;
const formData = new FormData(form);
// 模擬AJAX提交
setTimeout(() => {
alert('表單提交成功!');
form.reset();
}, 1000);
}
// 導出到全局
window.App = {
Utils: Utils,
initializeComponents: initializeComponents
};
// 自動初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initializeComponents);
} else {
initializeComponents();
}`;
}
}
第四章:AI增強與機器學習應用
4.1 計算機視覺在設計稿解析中的應用
python
# 使用OpenCV進行設計稿圖像分析
import cv2
import numpy as np
from sklearn.cluster import KMeans
class DesignImageAnalyzer:
def __init__(self):
self.edge_detector = EdgeDetector()
self.text_recognizer = TextRecognizer()
self.color_analyzer = ColorAnalyzer()
def analyze_image(self, image_path):
"""分析設計稿圖像"""
# 讀取圖像
image = cv2.imread(image_path)
if image is None:
raise ValueError(f"無法讀取圖像: {image_path}")
# 轉換為RGB
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
# 1. 邊緣檢測
edges = self.detect_edges(image_rgb)
# 2. 輪廓檢測
contours = self.detect_contours(edges)
# 3. 文字識別
text_regions = self.detect_text_regions(image_rgb)
# 4. 顏色分析
dominant_colors = self.extract_dominant_colors(image_rgb)
# 5. 佈局分析
layout_grid = self.detect_layout_grid(contours)
return {
'image_shape': image.shape,
'edges': edges,
'contours': contours,
'text_regions': text_regions,
'dominant_colors': dominant_colors,
'layout_grid': layout_grid
}
def detect_edges(self, image):
"""檢測圖像邊緣"""
# 轉換為灰度圖
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
# 應用高斯模糊減少噪聲
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
# 使用Canny邊緣檢測
edges = cv2.Canny(blurred, 50, 150)
return edges
def detect_contours(self, edges):
"""檢測輪廓"""
# 查找輪廓
contours, hierarchy = cv2.findContours(
edges,
cv2.RETR_TREE,
cv2.CHAIN_APPROX_SIMPLE
)
# 過濾小輪廓
min_area = 100
filtered_contours = []
contour_data = []
for i, contour in enumerate(contours):
area = cv2.contourArea(contour)
if area > min_area:
filtered_contours.append(contour)
# 計算邊界框
x, y, w, h = cv2.boundingRect(contour)
# 計算輪廓特徵
contour_data.append({
'index': i,
'area': area,
'bounding_box': (x, y, w, h),
'aspect_ratio': w / h if h > 0 else 0,
'solidity': self.calculate_solidity(contour, area)
})
return {
'contours': filtered_contours,
'hierarchy': hierarchy,
'contour_data': contour_data
}
def calculate_solidity(self, contour, area):
"""計算輪廓的solidity(實心度)"""
hull = cv2.convexHull(contour)
hull_area = cv2.contourArea(hull)
if hull_area > 0:
return area / hull_area
return 0
def detect_text_regions(self, image):
"""檢測文字區域"""
# 使用EAST文本檢測器
text_regions = []
# 轉換為灰度圖
gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
# 使用形態學操作增強文字
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
morphed = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)
# 查找輪廓
contours, _ = cv2.findContours(
morphed,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)
for contour in contours:
x, y, w, h = cv2.boundingRect(contour)
# 過濾非文字區域
aspect_ratio = w / h if h > 0 else 0
area = w * h
# 文字區域通常有特定的寬高比
if 0.1 < aspect_ratio < 10 and area > 100:
text_regions.append({
'bbox': (x, y, w, h),
'aspect_ratio': aspect_ratio,
'area': area
})
return text_regions
def extract_dominant_colors(self, image, n_colors=5):
"""提取主要顏色"""
# 重塑圖像為像素列表
pixels = image.reshape(-1, 3)
# 使用K-means聚類
kmeans = KMeans(n_clusters=n_colors, random_state=42)
kmeans.fit(pixels)
# 獲取聚類中心和標籤
colors = kmeans.cluster_centers_
labels = kmeans.labels_
# 計算每個顏色的比例
unique_labels, counts = np.unique(labels, return_counts=True)
proportions = counts / len(labels)
# 轉換為RGB值
dominant_colors = []
for i, color in enumerate(colors):
r, g, b = color.astype(int)
dominant_colors.append({
'rgb': (r, g, b),
'hex': f'#{r:02x}{g:02x}{b:02x}',
'proportion': proportions[i] if i < len(proportions) else 0
})
# 按比例排序
dominant_colors.sort(key=lambda x: x['proportion'], reverse=True)
return dominant_colors
def detect_layout_grid(self, contours_data):
"""檢測佈局網格"""
contour_data = contours_data['contour_data']
if not contour_data:
return None
# 提取所有邊界框
bboxes = [cd['bounding_box'] for cd in contour_data]
# 計算網格參數
grid_params = self.calculate_grid_parameters(bboxes)
return grid_params
def calculate_grid_parameters(self, bboxes):
"""計算網格參數"""
if not bboxes:
return None
# 提取x和y坐標
x_coords = [x for x, y, w, h in bboxes]
y_coords = [y for x, y, w, h in bboxes]
# 使用聚類找出對齊的網格線
x_clusters = self.cluster_coordinates(x_coords)
y_clusters = self.cluster_coordinates(y_coords)
# 計算網格單元大小
cell_width = self.calculate_average_spacing(x_clusters)
cell_height = self.calculate_average_spacing(y_clusters)
return {
'x_clusters': x_clusters,
'y_clusters': y_clusters,
'cell_width': cell_width,
'cell_height': cell_height,
'grid_size': (len(x_clusters), len(y_clusters))
}
def cluster_coordinates(self, coordinates, threshold=10):
"""對坐標進行聚類"""
if not coordinates:
return []
# 排序坐標
sorted_coords = sorted(coordinates)
clusters = []
current_cluster = [sorted_coords[0]]
for coord in sorted_coords[1:]:
if coord - current_cluster[-1] <= threshold:
current_cluster.append(coord)
else:
clusters.append(np.mean(current_cluster))
current_cluster = [coord]
if current_cluster:
clusters.append(np.mean(current_cluster))
return clusters
4.2 深度學習組件識別模型
python
# 基於深度學習的UI組件識別
import torch
import torch.nn as nn
import torchvision.models as models
from torchvision import transforms
class UIComponentDetector(nn.Module):
def __init__(self, num_classes=10):
super(UIComponentDetector, self).__init__()
# 使用預訓練的ResNet作為特徵提取器
self.backbone = models.resnet50(pretrained=True)
# 替換最後的全連接層
num_features = self.backbone.fc.in_features
self.backbone.fc = nn.Identity()
# 添加自定義分類頭
self.classifier = nn.Sequential(
nn.Linear(num_features, 512),
nn.ReLU(inplace=True),
nn.Dropout(0.5),
nn.Linear(512, num_classes)
)
# 邊界框回歸頭
self.bbox_regressor = nn.Sequential(
nn.Linear(num_features, 256),
nn.ReLU(inplace=True),
nn.Linear(256, 4) # x, y, width, height
)
def forward(self, x):
# 提取特徵
features = self.backbone(x)
# 分類
class_logits = self.classifier(features)
# 邊界框回歸
bbox_pred = self.bbox_regressor(features)
return class_logits, bbox_pred
class ComponentDetectionPipeline:
def __init__(self, model_path=None):
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 初始化模型
self.model = UIComponentDetector(num_classes=10).to(self.device)
if model_path:
self.load_model(model_path)
# 圖像預處理
self.transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
])
# 組件類別
self.class_names = [
'button', 'input', 'checkbox', 'radio',
'dropdown', 'slider', 'card', 'navbar',
'footer', 'header'
]
def load_model(self, model_path):
"""加載訓練好的模型"""
checkpoint = torch.load(model_path, map_location=self.device)
self.model.load_state_dict(checkpoint['model_state_dict'])
self.model.eval()
print(f"模型已加載: {model_path}")
def detect_components(self, image):
"""檢測圖像中的UI組件"""
# 預處理圖像
input_tensor = self.transform(image).unsqueeze(0).to(self.device)
with torch.no_grad():
# 前向傳播
class_logits, bbox_pred = self.model(input_tensor)
# 應用softmax獲取概率
probabilities = torch.nn.functional.softmax(class_logits, dim=1)
# 獲取預測類別
_, predicted_class = torch.max(probabilities, 1)
# 解碼邊界框
bbox = bbox_pred.squeeze().cpu().numpy()
# 後處理
detected_component = {
'class': self.class_names[predicted_class.item()],
'confidence': probabilities[0][predicted_class].item(),
'bbox': self.decode_bbox(bbox, image.size)
}
return detected_component
def decode_bbox(self, bbox, image_size):
"""解碼邊界框坐標"""
img_width, img_height = image_size
# 假設bbox是歸一化坐標
x, y, width, height = bbox
# 轉換為像素坐標
x_pixel = int(x * img_width)
y_pixel = int(y * img_height)
width_pixel = int(width * img_width)
height_pixel = int(height * img_height)
return {
'x': max(0, x_pixel),
'y': max(0, y_pixel),
'width': min(width_pixel, img_width - x_pixel),
'height': min(height_pixel, img_height - y_pixel)
}
def batch_detect(self, image_regions):
"""批量檢測多個圖像區域"""
detected_components = []
for region in image_regions:
try:
component = self.detect_components(region['image'])
component['region_id'] = region['id']
detected_components.append(component)
except Exception as e:
print(f"檢測失敗: {e}")
continue
return detected_components
class TrainingPipeline:
def __init__(self, dataset_path):
self.dataset_path = dataset_path
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# 初始化模型
self.model = UIComponentDetector(num_classes=10).to(self.device)
# 損失函數
self.classification_criterion = nn.CrossEntropyLoss()
self.regression_criterion = nn.SmoothL1Loss()
# 優化器
self.optimizer = torch.optim.AdamW(
self.model.parameters(),
lr=1e-4,
weight_decay=1e-4
)
# 學習率調度器
self.scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
self.optimizer,
T_max=100
)
def train_epoch(self, train_loader):
"""訓練一個epoch"""
self.model.train()
total_loss = 0
for batch_idx, (images, class_labels, bbox_labels) in enumerate(train_loader):
images = images.to(self.device)
class_labels = class_labels.to(self.device)
bbox_labels = bbox_labels.to(self.device)
# 前向傳播
class_logits, bbox_pred = self.model(images)
# 計算損失
class_loss = self.classification_criterion(class_logits, class_labels)
bbox_loss = self.regression_criterion(bbox_pred, bbox_labels)
# 總損失
loss = class_loss + 0.5 * bbox_loss
# 反向傳播
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
total_loss += loss.item()
if batch_idx % 10 == 0:
print(f'Batch {batch_idx}/{len(train_loader)}, Loss: {loss.item():.4f}')
avg_loss = total_loss / len(train_loader)
return avg_loss
def validate(self, val_loader):
"""驗證模型"""
self.model.eval()
total_correct = 0
total_samples = 0
total_loss = 0
with torch.no_grad():
for images, class_labels, bbox_labels in val_loader:
images = images.to(self.device)
class_labels = class_labels.to(self.device)
bbox_labels = bbox_labels.to(self.device)
# 前向傳播
class_logits, bbox_pred = self.model(images)
# 計算分類準確率
_, predicted = torch.max(class_logits, 1)
total_correct += (predicted == class_labels).sum().item()
total_samples += class_labels.size(0)
# 計算損失
class_loss = self.classification_criterion(class_logits, class_labels)
bbox_loss = self.regression_criterion(bbox_pred, bbox_labels)
loss = class_loss + 0.5 * bbox_loss
total_loss += loss.item()
accuracy = total_correct / total_samples
avg_loss = total_loss / len(val_loader)
return accuracy, avg_loss
def train(self, num_epochs=50):
"""完整訓練流程"""
# 加載數據集
train_loader, val_loader = self.load_dataset()
best_accuracy = 0
for epoch in range(num_epochs):
print(f'\nEpoch {epoch+1}/{num_epochs}')
# 訓練
train_loss = self.train_epoch(train_loader)
# 驗證
val_accuracy, val_loss = self.validate(val_loader)
print(f'Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}, Val Accuracy: {val_accuracy:.4f}')
# 保存最佳模型
if val_accuracy > best_accuracy:
best_accuracy = val_accuracy
self.save_checkpoint(epoch, val_accuracy)
# 更新學習率
self.scheduler.step()
print(f'訓練完成,最佳準確率: {best_accuracy:.4f}')
第五章:系統集成與部署
5.1 完整系統架構
python
# 完整的設計稿轉代碼系統
import asyncio
from concurrent.futures import ThreadPoolExecutor
from typing import Dict, Any, Optional
import json
class DesignToCodeSystem:
def __init__(self, config: Optional[Dict] = None):
self.config = config or self.load_default_config()
# 初始化各組件
self.design_parser = DesignParser()
self.ai_analyzer = AIAnalyzer()
self.layout_engine = LayoutEngine()
self.code_generator = CodeGenerator()
self.optimizer = CodeOptimizer()
self.validator = CodeValidator()
# 線程池
self.thread_pool = ThreadPoolExecutor(max_workers=4)
# 緩存系統
self.cache = {}
# 監控系統
self.metrics = {
'processed_files': 0,
'successful_conversions': 0,
'average_processing_time': 0
}
def load_default_config(self):
"""加載默認配置"""
return {
'max_file_size': 50 * 1024 * 1024, # 50MB
'supported_formats': ['.fig', '.sketch', '.png', '.jpg'],
'output_formats': ['html', 'css', 'js', 'react', 'vue'],
'enable_ai': True,
'enable_optimization': True,
'enable_validation': True,
'cache_enabled': True,
'cache_ttl': 3600 # 1小時
}
async def process_design_file(self, file_path: str, options: Optional[Dict] = None):
"""處理設計文件"""
start_time = asyncio.get_event_loop().time()
try:
# 1. 檢查文件
await self.validate_file(file_path)
# 2. 檢查緩存
cache_key = self.generate_cache_key(file_path, options)
if self.config['cache_enabled'] and cache_key in self.cache:
print(f"使用緩存結果: {cache_key}")
return self.cache[cache_key]
# 3. 解析設計文件
design_data = await self.parse_design_file(file_path)
# 4. AI分析
if self.config['enable_ai']:
analysis_result = await self.analyze_with_ai(design_data)
design_data['ai_analysis'] = analysis_result
# 5. 生成佈局
layout = await self.generate_layout(design_data)
# 6. 生成代碼
raw_code = await self.generate_code(design_data, layout, options)
# 7. 優化代碼
if self.config['enable_optimization']:
optimized_code = await self.optimize_code(raw_code)
else:
optimized_code = raw_code
# 8. 驗證代碼
if self.config['enable_validation']:
validation_result = await self.validate_code(optimized_code)
if not validation_result['valid']:
print(f"代碼驗證警告: {validation_result['warnings']}")
# 9. 生成文檔
documentation = await self.generate_documentation(design_data, optimized_code)
# 10. 包裝結果
result = {
'success': True,
'code': optimized_code,
'documentation': documentation,
'metadata': {
'processing_time': asyncio.get_event_loop().time() - start_time,
'file_size': self.get_file_size(file_path),
'components_detected': len(design_data.get('components', [])),
'layout_type': layout.get('type', 'unknown')
}
}
# 11. 更新緩存
if self.config['cache_enabled']:
self.cache[cache_key] = result
self.schedule_cache_cleanup(cache_key)
# 12. 更新指標
self.update_metrics(result, start_time)
return result
except Exception as e:
error_result = {
'success': False,
'error': str(e),
'error_type': type(e).__name__,
'suggestions': self.get_error_suggestions(e)
}
return error_result
async def parse_design_file(self, file_path: str):
"""解析設計文件"""
file_extension = file_path.lower().split('.')[-1]
if file_extension in ['fig']:
return await self.design_parser.parse_figma(file_path)
elif file_extension in ['sketch']:
return await self.design_parser.parse_sketch(file_path)
elif file_extension in ['png', 'jpg', 'jpeg']:
return await self.design_parser.parse_image(file_path)
else:
raise ValueError(f"不支持的檔案格式: {file_extension}")
async def analyze_with_ai(self, design_data: Dict):
"""使用AI分析設計數據"""
tasks = [
self.ai_analyzer.detect_components(design_data),
self.ai_analyzer.analyze_layout_patterns(design_data),
self.ai_analyzer.extract_design_system(design_data),
self.ai_analyzer.predict_accessibility(design_data)
]
# 並行執行AI分析任務
results = await asyncio.gather(*tasks, return_exceptions=True)
return {
'components': results[0] if not isinstance(results[0], Exception) else [],
'layout_patterns': results[1] if not isinstance(results[1], Exception) else {},
'design_system': results[2] if not isinstance(results[2], Exception) else {},
'accessibility_score': results[3] if not isinstance(results[3], Exception) else 0
}
async def generate_code(self, design_data: Dict, layout: Dict, options: Optional[Dict] = None):
"""生成代碼"""
output_format = (options or {}).get('format', 'html')
if output_format == 'react':
return await self.code_generator.generate_react(design_data, layout, options)
elif output_format == 'vue':
return await self.code_generator.generate_vue(design_data, layout, options)
elif output_format == 'html':
return await self.code_generator.generate_html(design_data, layout, options)
else:
raise ValueError(f"不支持的輸出格式: {output_format}")
async def generate_documentation(self, design_data: Dict, code: Dict):
"""生成文檔"""
doc_generator = DocumentationGenerator()
# 生成多種類型的文檔
documentation = await doc_generator.generate_all({
'api_docs': doc_generator.generate_api_docs(code),
'component_docs': doc_generator.generate_component_docs(design_data),
'usage_examples': doc_generator.generate_usage_examples(code),
'accessibility_notes': doc_generator.generate_accessibility_notes(design_data)
})
return documentation
def generate_cache_key(self, file_path: str, options: Optional[Dict] = None):
"""生成緩存鍵"""
import hashlib
# 計算文件哈希
with open(file_path, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()
# 組合選項
options_str = json.dumps(options or {}, sort_keys=True)
# 生成最終緩存鍵
cache_key = f"{file_hash}:{options_str}"
return hashlib.md5(cache_key.encode()).hexdigest()
def schedule_cache_cleanup(self, cache_key: str):
"""計劃緩存清理"""
import threading
def cleanup():
import time
time.sleep(self.config['cache_ttl'])
if cache_key in self.cache:
del self.cache[cache_key]
print(f"緩存過期已清理: {cache_key}")
thread = threading.Thread(target=cleanup)
thread.daemon = True
thread.start()
def update_metrics(self, result: Dict, start_time: float):
"""更新系統指標"""
self.metrics['processed_files'] += 1
if result['success']:
self.metrics['successful_conversions'] += 1
# 更新平均處理時間
processing_time = asyncio.get_event_loop().time() - start_time
current_avg = self.metrics['average_processing_time']
total_files = self.metrics['processed_files']
self.metrics['average_processing_time'] = (
(current_avg * (total_files - 1) + processing_time) / total_files
)
def get_system_status(self):
"""獲取系統狀態"""
return {
'metrics': self.metrics,
'cache_size': len(self.cache),
'config': self.config,
'thread_pool_status': {
'active_threads': self.thread_pool._max_workers,
'queue_size': self.thread_pool._work_queue.qsize() if hasattr(self.thread_pool, '_work_queue') else 0
}
}
class APIServer:
"""REST API服務器"""
def __init__(self, system: DesignToCodeSystem):
self.system = system
self.app = self.create_app()
def create_app(self):
"""創建FastAPI應用"""
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse, FileResponse
app = FastAPI(
title="設計稿轉代碼API",
description="將Figma/Sketch設計稿自動轉換為HTML/CSS/JavaScript代碼",
version="1.0.0"
)
# 添加CORS中間件
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return {"message": "設計稿轉代碼API服務運行中"}
@app.get("/status")
async def get_status():
return self.system.get_system_status()
@app.post("/convert")
async def convert_design(
file: UploadFile = File(...),
output_format: str = "html",
optimize: bool = True,
include_ai: bool = True
):
try:
# 保存上傳的文件
temp_file = await self.save_upload_file(file)
# 處理文件
options = {
'format': output_format,
'optimize': optimize,
'include_ai': include_ai
}
result = await self.system.process_design_file(temp_file, options)
# 清理臨時文件
import os
os.remove(temp_file)
if result['success']:
return JSONResponse(content=result)
else:
raise HTTPException(status_code=400, detail=result['error'])
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/batch-convert")
async def batch_convert(files: List[UploadFile] = File(...)):
results = []
for file in files:
try:
temp_file = await self.save_upload_file(file)
result = await self.system.process_design_file(temp_file)
results.append({
'filename': file.filename,
'result': result
})
import os
os.remove(temp_file)
except Exception as e:
results.append({
'filename': file.filename,
'error': str(e)
})
return {"results": results}
@app.get("/export/{format}")
async def export_code(
format: str,
html: Optional[str] = None,
css: Optional[str] = None,
js: Optional[str] = None
):
try:
export_service = ExportService()
if format == "zip":
zip_data = export_service.create_zip_archive(html, css, js)
return FileResponse(
zip_data,
media_type="application/zip",
filename="export.zip"
)
elif format == "github":
repo_url = export_service.export_to_github(html, css, js)
return {"repository_url": repo_url}
else:
raise HTTPException(status_code=400, detail="不支持的導出格式")
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
return app
async def save_upload_file(self, upload_file: UploadFile):
"""保存上傳的文件"""
import tempfile
import os
# 創建臨時文件
suffix = os.path.splitext(upload_file.filename)[1]
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as tmp:
content = await upload_file.read()
tmp.write(content)
return tmp.name
def run(self, host: str = "0.0.0.0", port: int = 8000):
"""運行API服務器"""
import uvicorn
uvicorn.run(self.app, host=host, port=port)
class ExportService:
"""代碼導出服務"""
def __init__(self):
self.github_client = GitHubClient()
self.zip_creator = ZipCreator()
def create_zip_archive(self, html: str, css: str, js: str):
"""創建ZIP壓縮包"""
import zipfile
import io
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
if html:
zip_file.writestr("index.html", html)
if css:
zip_file.writestr("styles.css", css)
if js:
zip_file.writestr("script.js", js)
# 添加README文件
readme_content = self.generate_readme(html, css, js)
zip_file.writestr("README.md", readme_content)
# 添加package.json(如果適用)
package_json = self.generate_package_json()
zip_file.writestr("package.json", package_json)
zip_buffer.seek(0)
return zip_buffer
def export_to_github(self, html: str, css: str, js: str):
"""導出到GitHub"""
# 創建倉庫
repo_name = f"design-to-code-{int(time.time())}"
repo_url = self.github_client.create_repository(repo_name)
# 提交文件
files = {}
if html:
files["index.html"] = html
if css:
files["styles.css"] = css
if js:
files["script.js"] = js
files["README.md"] = self.generate_readme(html, css, js)
files["package.json"] = self.generate_package_json()
self.github_client.commit_files(repo_name, files)
# 部署到GitHub Pages
pages_url = self.github_client.enable_pages(repo_name)
return pages_url or repo_url
def generate_readme(self, html: str, css: str, js: str):
"""生成README文件"""
return f"""# 自動生成的代碼項目
此項目由設計稿轉代碼系統自動生成。
## 文件結構
- index.html - 主HTML文件
- styles.css - 樣式文件
- script.js - JavaScript文件
## 生成信息
- 生成時間: {time.strftime('%Y-%m-%d %H:%M:%S')}
- HTML行數: {len(html.splitlines()) if html else 0}
- CSS行數: {len(css.splitlines()) if css else 0}
- JS行數: {len(js.splitlines()) if js else 0}
## 使用說明
1. 直接打開index.html在瀏覽器中預覽
2. 或使用本地服務器運行:
python -m http.server 8000
text
## 注意事項
此代碼為自動生成,可能需要進一步優化和調整以符合特定需求。
"""
def generate_package_json(self):
"""生成package.json文件"""
return json.dumps({
"name": "design-to-code-project",
"version": "1.0.0",
"description": "Automatically generated from design file",
"main": "index.html",
"scripts": {
"start": "python -m http.server 8000",
"dev": "live-server --port=8000"
},
"keywords": ["design-to-code", "auto-generated"],
"author": "DesignToCode System",
"license": "MIT"
}, indent=2)
5.2 命令行工具
python
# 命令行界面工具
import argparse
import sys
from pathlib import Path
class DesignToCodeCLI:
def __init__(self):
self.parser = self.create_parser()
self.system = DesignToCodeSystem()
def create_parser(self):
"""創建命令行解析器"""
parser = argparse.ArgumentParser(
description='將設計稿轉換為代碼',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
%(prog)s design.sketch --output-dir ./dist
%(prog)s design.fig --format react --optimize
%(prog)s screenshot.png --ai --validate
"""
)
# 必需參數
parser.add_argument(
'input',
help='輸入文件路徑 (支持 .fig, .sketch, .png, .jpg)'
)
# 輸出選項
parser.add_argument(
'-o', '--output-dir',
default='./output',
help='輸出目錄 (默認: ./output)'
)
parser.add_argument(
'-f', '--format',
choices=['html', 'react', 'vue', 'all'],
default='html',
help='輸出格式 (默認: html)'
)
# 處理選項
parser.add_argument(
'--ai',
action='store_true',
help='啟用AI增強分析'
)
parser.add_argument(
'--optimize',
action='store_true',
default=True,
help='啟用代碼優化 (默認: 啟用)'
)
parser.add_argument(
'--validate',
action='store_true',
help='啟用代碼驗證'
)
parser.add_argument(
'--no-cache',
action='store_true',
help='禁用緩存'
)
# 高級選項
parser.add_argument(
'--config',
help='配置文件路徑'
)
parser.add_argument(
'--verbose',
action='store_true',
help='詳細輸出模式'
)
parser.add_argument(
'--version',
action='version',
version='設計稿轉代碼工具 v1.0.0'
)
return parser
def load_config(self, config_path):
"""加載配置文件"""
import yaml
if not Path(config_path).exists():
print(f"錯誤: 配置文件不存在: {config_path}")
sys.exit(1)
with open(config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
return config
def print_banner(self):
"""打印橫幅"""
banner = """
╔══════════════════════════════════════════════════════════╗
║ 設計稿轉代碼工具 v1.0.0 ║
║ 從Figma/Sketch自動生成HTML/CSS/JS代碼 ║
╚══════════════════════════════════════════════════════════╝
"""
print(banner)
def print_result_summary(self, result, processing_time):
"""打印結果摘要"""
print("\n" + "="*60)
print("轉換完成!")
print("="*60)
if result['success']:
metadata = result['metadata']
print(f"\n📊 轉換統計:")
print(f" ├─ 處理時間: {processing_time:.2f}秒")
print(f" ├─ 文件大小: {metadata.get('file_size', 0)} bytes")
print(f" ├─ 檢測組件: {metadata.get('components_detected', 0)}個")
print(f" └─ 佈局類型: {metadata.get('layout_type', '未知')}")
if 'code' in result:
code = result['code']
print(f"\n📁 生成文件:")
for file_type, content in code.items():
if content:
lines = content.splitlines()
print(f" ├─ {file_type}: {len(lines)}行代碼")
print(f"\n✅ 轉換成功!")
print(f"輸出目錄: {self.output_dir}")
else:
print(f"\n❌ 轉換失敗:")
print(f"錯誤: {result.get('error', '未知錯誤')}")
print(f"類型: {result.get('error_type', '未知')}")
if 'suggestions' in result:
print(f"\n💡 建議:")
for suggestion in result['suggestions']:
print(f" - {suggestion}")
async def run(self):
"""運行命令行工具"""
args = self.parser.parse_args()
# 打印橫幅
self.print_banner()
# 加載配置
if args.config:
config = self.load_config(args.config)
self.system.config.update(config)
# 更新系統配置
self.system.config['enable_ai'] = args.ai
self.system.config['enable_optimization'] = args.optimize
self.system.config['enable_validation'] = args.validate
self.system.config['cache_enabled'] = not args.no_cache
# 檢查輸入文件
input_path = Path(args.input)
if not input_path.exists():
print(f"錯誤: 輸入文件不存在: {args.input}")
sys.exit(1)
# 創建輸出目錄
self.output_dir = Path(args.output_dir)
self.output_dir.mkdir(parents=True, exist_ok=True)
print(f"📂 輸入文件: {input_path}")
print(f"📁 輸出目錄: {self.output_dir}")
print(f"⚙️ 配置: AI={args.ai}, 優化={args.optimize}, 驗證={args.validate}")
# 處理文件
import time
start_time = time.time()
try:
options = {
'format': args.format,
'optimize': args.optimize,
'include_ai': args.ai
}
result = await self.system.process_design_file(str(input_path), options)
processing_time = time.time() - start_time
# 打印結果
self.print_result_summary(result, processing_time)
# 保存結果
if result['success'] and 'code' in result:
await self.save_output(result['code'])
# 返回退出碼
return 0 if result['success'] else 1
except KeyboardInterrupt:
print("\n\n操作被用戶中斷")
return 130
except Exception as e:
print(f"\n❌ 發生錯誤: {e}")
if args.verbose:
import traceback
traceback.print_exc()
return 1
async def save_output(self, code):
"""保存輸出文件"""
for file_type, content in code.items():
if content:
file_path = self.output_dir / f"output.{file_type}"
with open(file_path, 'w', encoding='utf-8') as f:
f.write(content)
print(f"💾 已保存: {file_path}")
# 保存文檔
doc_path = self.output_dir / "README.md"
with open(doc_path, 'w', encoding='utf-8') as f:
f.write(self.generate_readme())
print(f"📄 已保存: {doc_path}")
def main():
"""主函數"""
cli = DesignToCodeCLI()
# 運行異步主函數
import asyncio
exit_code = asyncio.run(cli.run())
sys.exit(exit_code)
if __name__ == "__main__":
main()
第六章:未來發展與挑戰
6.1 技術挑戰與解決方案
6.1.1 複雜佈局的準確識別
挑戰:
-
嵌套佈局的精確解析
-
響應式設計的意圖理解
-
動態內容的處理
解決方案:
-
使用圖神經網絡(GNN)分析元素間的關係
-
引入注意力機制理解設計意圖
-
建立佈局模式庫進行匹配
6.1.2 代碼質量的持續提升
挑戰:
-
生成的代碼可讀性
-
性能優化
-
可維護性
解決方案:
-
引入代碼風格檢查和格式化
-
實施性能最佳實踐
-
生成組件化的、可重用的代碼結構
6.2 商業化應用前景
6.2.1 市場需求分析
根據市場研究,設計稿轉代碼工具的市場規模預計將從2023年的5億美元增長到2028年的20億美元,年複合增長率達32%。主要驅動因素包括:
-
前端開發人力成本上升
-
快速原型開發需求增加
-
低代碼/無代碼平台興起
-
遠程協作工具普及
6.2.2 商業模式設計
-
SaaS訂閱模式
-
個人開發者:$29/月
-
團隊:$99/月(最多5人)
-
企業:自定義定價
-
-
本地部署方案
-
一次性授權費
-
年度維護費
-
-
API服務
-
按調用量收費
-
批量處理優惠
-
6.3 技術發展趨勢
6.3.1 AI技術的深入應用
-
多模態學習
-
結合視覺、文本和結構信息
-
理解設計師意圖和業務邏輯
-
-
強化學習優化
-
通過用戶反饋持續優化生成結果
-
自動調整代碼生成策略
-
-
生成對抗網絡(GAN)
-
生成更自然的代碼結構
-
提高代碼質量和可讀性
-
6.3.2 雲原生架構
-
微服務化
-
將系統拆分為獨立服務
-
提高可擴展性和可維護性
-
-
容器化部署
-
使用Docker容器打包
-
Kubernetes編排管理
-
-
Serverless計算
-
按需擴縮容
-
降低運營成本
-
結論
設計稿自動轉代碼技術代表了前端開發自動化的未來方向。通過結合計算機視覺、機器學習和軟件工程的最佳實踐,我們可以大幅提高設計到開發的轉換效率,減少重複勞動,並確保設計意圖的準確實現。
本文介紹的系統架構和實現方案提供了一個完整的解決方案,從設計稿解析、AI分析、代碼生成到系統部署。雖然仍有技術挑戰需要克服,但隨著AI技術的不斷發展和更多實戰經驗的積累,設計稿轉代碼的準確性和實用性將持續提升。
未來,我們可以期待更加智能化的設計開發工具,能夠理解複雜的業務邏輯,生成高質量的、可維護的代碼,真正實現設計與開發的無縫銜接。這不僅將改變前端開發的工作方式,也將推動整個軟件開發行業向更高效率、更智能化的方向發展。
附錄:相關資源
-
開源項目
-
Design2Code:本文實現的開源版本
-
-
研究論文
-
"Pix2Code: Generating Code from a Graphical User Interface Screenshot"
-
"Screenshot-to-Code: A Deep Learning Approach"
-
-
在線工具
-
學習資源
-
前端開發最佳實踐
-
機器學習在軟件工程中的應用
-
計算機視覺基礎知識
-
通過持續學習和實踐,我們可以不斷改進和完善設計稿轉代碼技術,為軟件開發行業帶來真正的變革。
更多推荐



所有评论(0)