initial commit: bricks-mp project
This commit is contained in:
commit
b9c585699c
28
DESIGN.md
Normal file
28
DESIGN.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# Bricks Multiplatform - 设计文档
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
JSON 描述 → BricksParser → BricksWidget 树 → BricksRenderer → Compose UI
|
||||||
|
|
||||||
|
## 组件映射 (Phase 1)
|
||||||
|
| Bricks Widget | Compose |
|
||||||
|
|--------------|---------|
|
||||||
|
| Text | Text() |
|
||||||
|
| Title1-6 | Text() + fontSize/fontWeight |
|
||||||
|
| HBox/FHBox | Row() + Arrangement |
|
||||||
|
| VBox/FVBox | Column() + Arrangement |
|
||||||
|
| Filler/HFiller/VFiller | Spacer() + weight |
|
||||||
|
| ResponsiveBox | BoxWithConstraints() |
|
||||||
|
| KeyinText | OutlinedTextField() |
|
||||||
|
| Input | TextField() + type |
|
||||||
|
| Running | CircularProgressIndicator() |
|
||||||
|
|
||||||
|
## 事件系统
|
||||||
|
actiontype: urlwidget/method/script/registerfunction/event → ActionDispatcher
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
Kotlin 2.1, Compose Multiplatform 1.7.3, Ktor 3.0, kotlinx.serialization, Coroutines
|
||||||
|
|
||||||
|
## 平台支持
|
||||||
|
- Android (minSdk 24)
|
||||||
|
- iOS (X64/Arm64/SimulatorArm64)
|
||||||
|
- Desktop (Windows/Linux/macOS)
|
||||||
1
build.gradle.kts
Normal file
1
build.gradle.kts
Normal file
@ -0,0 +1 @@
|
|||||||
|
// Top-level build file
|
||||||
101
desktopApp/src/main/kotlin/com/bricks/Main.kt
Normal file
101
desktopApp/src/main/kotlin/com/bricks/Main.kt
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package com.bricks
|
||||||
|
|
||||||
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import com.bricks.mp.core.BricksContext
|
||||||
|
import com.bricks.mp.core.BricksParser
|
||||||
|
import com.bricks.mp.core.BricksApp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
Window(
|
||||||
|
onCloseRequest = ::exitApplication,
|
||||||
|
title = "Bricks MP - Desktop",
|
||||||
|
state = rememberWindowState(width = 1200.dp, height = 800.dp)
|
||||||
|
) {
|
||||||
|
BricksDesktopApp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun BricksDesktopApp() {
|
||||||
|
val context = remember { BricksContext() }
|
||||||
|
var jsonInput by remember { mutableStateOf(SAMPLE_JSON) }
|
||||||
|
var rootWidget by remember { mutableStateOf<com.bricks.mp.core.BricksWidget?>(null) }
|
||||||
|
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
// JSON 输入区
|
||||||
|
Row(modifier = Modifier.fillMaxWidth().weight(0.3f)) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = jsonInput,
|
||||||
|
onValueChange = { jsonInput = it },
|
||||||
|
modifier = Modifier.fillMaxSize().padding(8.dp),
|
||||||
|
label = { Text("Bricks JSON") },
|
||||||
|
textStyle = androidx.compose.ui.text.TextStyle(fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析按钮
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center) {
|
||||||
|
Button(onClick = {
|
||||||
|
try {
|
||||||
|
rootWidget = BricksParser.parse(jsonInput)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("Parse error: ${e.message}")
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text("Parse & Render")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染区
|
||||||
|
Surface(modifier = Modifier.fillMaxWidth().weight(0.6f)) {
|
||||||
|
if (rootWidget != null) {
|
||||||
|
BricksApp(rootWidget!!)
|
||||||
|
} else {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
Text("Enter Bricks JSON and click Parse")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val SAMPLE_JSON = """
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"text": "Hello Bricks MP"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Title1",
|
||||||
|
"options": {
|
||||||
|
"text": "Welcome to Bricks"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "Cross-platform JSON-driven UI"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "KeyinText",
|
||||||
|
"options": {
|
||||||
|
"placeholder": "Type here...",
|
||||||
|
"label": "Input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".trimIndent()
|
||||||
24
gradle/libs.versions.toml
Normal file
24
gradle/libs.versions.toml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
[versions]
|
||||||
|
compose = "1.7.3"
|
||||||
|
compose-plugin = "1.7.3"
|
||||||
|
kotlin = "2.1.0"
|
||||||
|
ktor = "3.0.3"
|
||||||
|
serialization = "1.7.3"
|
||||||
|
coroutines = "1.9.0"
|
||||||
|
coil = "3.0.4"
|
||||||
|
|
||||||
|
[libraries]
|
||||||
|
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
|
||||||
|
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
|
||||||
|
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
|
||||||
|
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
|
||||||
|
ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
|
||||||
|
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
|
||||||
|
coil-network = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
|
||||||
|
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||||
|
jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" }
|
||||||
|
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
|
||||||
|
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
7
settings.gradle.kts
Normal file
7
settings.gradle.kts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
rootProject.name = "bricks-mp"
|
||||||
|
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
|
||||||
|
pluginManagement {
|
||||||
|
repositories { google(); mavenCentral(); gradlePluginPortal() }
|
||||||
|
}
|
||||||
|
plugins { id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" }
|
||||||
|
include(":shared", ":desktopApp")
|
||||||
61
shared/build.gradle.kts
Normal file
61
shared/build.gradle.kts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
|
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
alias(libs.plugins.kotlin.multiplatform)
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
|
alias(libs.plugins.jetbrains.compose)
|
||||||
|
alias(libs.plugins.serialization)
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
androidTarget()
|
||||||
|
jvm("desktop")
|
||||||
|
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
|
||||||
|
iosTarget.binaries.framework {
|
||||||
|
baseName = "BricksShared"
|
||||||
|
isStatic = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalKotlinGradlePluginApi::class)
|
||||||
|
compilerOptions { freeCompilerArgs.add("-Xexpect-actual-classes") }
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
commonMain.dependencies {
|
||||||
|
implementation(compose.runtime)
|
||||||
|
implementation(compose.foundation)
|
||||||
|
implementation(compose.material3)
|
||||||
|
implementation(compose.ui)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.ktor.client.core)
|
||||||
|
implementation(libs.kotlinx.coroutines.core)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
implementation(libs.coil.network)
|
||||||
|
}
|
||||||
|
desktopMain.dependencies {
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(libs.ktor.client.okhttp)
|
||||||
|
}
|
||||||
|
iosMain.dependencies {
|
||||||
|
implementation(libs.ktor.client.darwin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.bricks.mp"
|
||||||
|
compileSdk = 35
|
||||||
|
defaultConfig { minSdk = 24 }
|
||||||
|
}
|
||||||
|
|
||||||
|
compose.desktop {
|
||||||
|
application {
|
||||||
|
mainClass = "com.bricks.MainKt"
|
||||||
|
nativeDistributions {
|
||||||
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
|
packageName = "bricks-mp"
|
||||||
|
packageVersion = "0.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,78 @@
|
|||||||
|
package com.bricks.mp.actions
|
||||||
|
|
||||||
|
import com.bricks.mp.core.BricksBind
|
||||||
|
import com.bricks.mp.core.BricksContext
|
||||||
|
import com.bricks.mp.core.BricksHttp
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 事件分发器 - 处理 actiontype: urlwidget/method/script/registerfunction/event
|
||||||
|
*/
|
||||||
|
class ActionDispatcher(
|
||||||
|
private val context: BricksContext,
|
||||||
|
private val http: BricksHttp,
|
||||||
|
private val scope: CoroutineScope
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val registeredFunctions = mutableMapOf<String, () -> Unit>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 注册回调函数 (registerfunction)
|
||||||
|
*/
|
||||||
|
fun registerFunction(name: String, func: () -> Unit) {
|
||||||
|
registeredFunctions[name] = func
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分发事件
|
||||||
|
*/
|
||||||
|
fun dispatch(bind: BricksBind) {
|
||||||
|
when (bind.actiontype) {
|
||||||
|
"urlwidget" -> handleUrlWidget(bind)
|
||||||
|
"method" -> handleMethod(bind)
|
||||||
|
"script" -> handleScript(bind)
|
||||||
|
"registerfunction" -> handleRegisterFunction(bind)
|
||||||
|
"event" -> handleEvent(bind)
|
||||||
|
else -> println("[Bricks] Unknown actiontype: ${bind.actiontype}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleUrlWidget(bind: BricksBind) {
|
||||||
|
scope.launch {
|
||||||
|
val url = bind.url ?: return@launch
|
||||||
|
val fullUrl = context.entireUrl(url)
|
||||||
|
try {
|
||||||
|
val result = http.getJson(fullUrl)
|
||||||
|
// 加载新的 widget 并更新 UI
|
||||||
|
println("[Bricks] urlwidget loaded: $fullUrl")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("[Bricks] urlwidget error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleMethod(bind: BricksBind) {
|
||||||
|
// 调用客户端方法
|
||||||
|
println("[Bricks] method called: ${bind.methodname}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleScript(bind: BricksBind) {
|
||||||
|
scope.launch {
|
||||||
|
// 服务端脚本调用
|
||||||
|
val script = bind.script ?: return@launch
|
||||||
|
println("[Bricks] script: $script")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleRegisterFunction(bind: BricksBind) {
|
||||||
|
val name = bind.target ?: return
|
||||||
|
registeredFunctions[name]?.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleEvent(bind: BricksBind) {
|
||||||
|
println("[Bricks] event: ${bind.event} -> ${bind.actiontype}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.bricks.mp.core
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局上下文 - 管理应用状态、session、变量
|
||||||
|
*/
|
||||||
|
class BricksContext {
|
||||||
|
|
||||||
|
private val _appState = MutableStateFlow<Map<String, Any>>(emptyMap())
|
||||||
|
val appState: StateFlow<Map<String, Any>> = _appState.asStateFlow()
|
||||||
|
|
||||||
|
private val _sessionData = MutableStateFlow<Map<String, Any>>(emptyMap())
|
||||||
|
val sessionData: StateFlow<Map<String, Any>> = _sessionData.asStateFlow()
|
||||||
|
|
||||||
|
var baseUrl: String = ""
|
||||||
|
var authToken: String = ""
|
||||||
|
|
||||||
|
fun setAppState(key: String, value: Any) {
|
||||||
|
val current = _appState.value.toMutableMap()
|
||||||
|
current[key] = value
|
||||||
|
_appState.value = current
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAppState(key: String): Any? = _appState.value[key]
|
||||||
|
|
||||||
|
fun setSessionData(key: String, value: Any) {
|
||||||
|
val current = _sessionData.value.toMutableMap()
|
||||||
|
current[key] = value
|
||||||
|
_sessionData.value = current
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 entire_url - 拼接 baseUrl
|
||||||
|
*/
|
||||||
|
fun entireUrl(path: String): String {
|
||||||
|
val cleanPath = path.trimStart('/')
|
||||||
|
return if (baseUrl.endsWith("/")) "$baseUrl$cleanPath" else "$baseUrl/$cleanPath"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
package com.bricks.mp.core
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP 客户端 - 对应 JS 版 HttpJson/HttpText
|
||||||
|
*/
|
||||||
|
class BricksHttp(private val context: BricksContext) {
|
||||||
|
|
||||||
|
private val client = HttpClient()
|
||||||
|
|
||||||
|
suspend fun getJson(url: String, params: Map<String, String> = emptyMap()): JsonObject {
|
||||||
|
val response = client.get(url) {
|
||||||
|
url { params.forEach { (k, v) -> parameters.append(k, v) } }
|
||||||
|
if (context.authToken.isNotEmpty()) {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer ${context.authToken}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Json.parseToJsonElement(response.bodyAsText()).jsonObject
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun postJson(url: String, body: JsonObject): JsonObject {
|
||||||
|
val response = client.post(url) {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(body.toString())
|
||||||
|
if (context.authToken.isNotEmpty()) {
|
||||||
|
header(HttpHeaders.Authorization, "Bearer ${context.authToken}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Json.parseToJsonElement(response.bodyAsText()).jsonObject
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getText(url: String, params: Map<String, String> = emptyMap()): String {
|
||||||
|
val response = client.get(url) {
|
||||||
|
url { params.forEach { (k, v) -> parameters.append(k, v) } }
|
||||||
|
}
|
||||||
|
return response.bodyAsText()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() {
|
||||||
|
client.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.bricks.mp.core
|
||||||
|
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 解析器 - 将 bricks JSON 解析为 BricksWidget 树
|
||||||
|
*/
|
||||||
|
object BricksParser {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 JSON 字符串解析 Widget 树
|
||||||
|
*/
|
||||||
|
fun parse(jsonString: String): BricksWidget {
|
||||||
|
val json = Json { ignoreUnknownKeys = true; coerceInputValues = true }
|
||||||
|
val element = json.parseToJsonElement(jsonString)
|
||||||
|
return parseElement(element)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 JsonElement 递归解析
|
||||||
|
*/
|
||||||
|
private fun parseElement(element: JsonElement): BricksWidget {
|
||||||
|
val obj = element.jsonObject
|
||||||
|
val widgettype = obj["widgettype"]?.jsonPrimitive?.content ?: "Text"
|
||||||
|
|
||||||
|
val options = obj["options"]?.jsonObject?.mapValues { it.value } ?: emptyMap()
|
||||||
|
|
||||||
|
val subwidgets = obj["subwidgets"]?.jsonArray?.map { parseElement(it) } ?: emptyList()
|
||||||
|
|
||||||
|
val binds = obj["binds"]?.jsonArray?.map { bindEl ->
|
||||||
|
val b = bindEl.jsonObject
|
||||||
|
BricksBind(
|
||||||
|
event = b["event"]?.jsonPrimitive?.content,
|
||||||
|
actiontype = b["actiontype"]?.jsonPrimitive?.content,
|
||||||
|
target = b["target"]?.jsonPrimitive?.content,
|
||||||
|
methodname = b["methodname"]?.jsonPrimitive?.content,
|
||||||
|
script = b["script"]?.jsonPrimitive?.content,
|
||||||
|
url = b["url"]?.jsonPrimitive?.content,
|
||||||
|
data = (b["data"]?.jsonObject?.mapValues { it.value } ?: emptyMap())
|
||||||
|
)
|
||||||
|
} ?: emptyList()
|
||||||
|
|
||||||
|
return BricksWidget(widgettype, options, subwidgets, binds)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板变量替换 - 将 {{var}} 替换为实际值
|
||||||
|
*/
|
||||||
|
fun resolveTemplate(template: String, data: Map<String, String>): String {
|
||||||
|
var result = template
|
||||||
|
for ((key, value) in data) {
|
||||||
|
result = result.replace("{{$key}}", value)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package com.bricks.mp.core
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.bricks.mp.core.BricksWidget
|
||||||
|
import com.bricks.mp.widgets.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归渲染引擎 - 将 BricksWidget 树渲染为 Compose UI
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RenderWidget(widget: BricksWidget) {
|
||||||
|
when (widget.widgettype) {
|
||||||
|
// 文本
|
||||||
|
"Text" -> RenderTextWidget(widget)
|
||||||
|
"Title1", "Title2", "Title3", "Title4", "Title5", "Title6" -> RenderTitleWidget(widget)
|
||||||
|
|
||||||
|
// 布局
|
||||||
|
"HBox", "FHBox", "VBox", "FVBox", "Filler", "HFiller", "VFiller", "ResponsiveBox" -> RenderLayoutWidget(widget)
|
||||||
|
|
||||||
|
// 输入
|
||||||
|
"KeyinText" -> RenderKeyinTextWidget(widget)
|
||||||
|
"Input" -> RenderInputWidget(widget)
|
||||||
|
"Tooltip" -> RenderTooltipWidget(widget)
|
||||||
|
|
||||||
|
// TODO: Phase 2 组件
|
||||||
|
"Image", "Icon", "StatedIcon", "BlankIcon" -> RenderPlaceholder(widget, "Image/Icon")
|
||||||
|
"Menu", "Popup", "PopupWindow", "Modal", "ModalForm" -> RenderPlaceholder(widget, "Menu/Dialog")
|
||||||
|
"VScrollPanel", "HScrollPanel" -> RenderPlaceholder(widget, "Scroll")
|
||||||
|
"Splitter" -> RenderPlaceholder(widget, "Splitter")
|
||||||
|
"Running" -> RenderRunningWidget(widget)
|
||||||
|
|
||||||
|
// TODO: Phase 3 组件
|
||||||
|
"Html", "MarkdownViewer", "LlmOut" -> RenderPlaceholder(widget, widget.widgettype)
|
||||||
|
|
||||||
|
// 默认: 渲染子组件
|
||||||
|
else -> {
|
||||||
|
widget.subwidgets.forEach { child -> RenderWidget(child) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderPlaceholder(widget: BricksWidget, name: String) {
|
||||||
|
androidx.compose.material3.Text(
|
||||||
|
text = "[${widget.widgettype}: $name - TODO]",
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
widget.subwidgets.forEach { child -> RenderWidget(child) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderRunningWidget(widget: BricksWidget) {
|
||||||
|
androidx.compose.material3.CircularProgressIndicator(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染整个 Widget 树
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun BricksApp(rootWidget: BricksWidget) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
RenderWidget(rootWidget)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
package com.bricks.mp.core
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bricks Widget 数据模型 - 与 JS 版 bricks 完全一致的 JSON 结构
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class BricksWidget(
|
||||||
|
val widgettype: String,
|
||||||
|
val options: Map<String, JsonElement> = emptyMap(),
|
||||||
|
val subwidgets: List<BricksWidget> = emptyList(),
|
||||||
|
val binds: List<BricksBind> = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BricksBind(
|
||||||
|
val event: String? = null,
|
||||||
|
val actiontype: String? = null,
|
||||||
|
val target: String? = null,
|
||||||
|
val data: Map<String, JsonElement> = emptyMap(),
|
||||||
|
val methodname: String? = null,
|
||||||
|
val script: String? = null,
|
||||||
|
val url: String? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 options 中的常用字段
|
||||||
|
*/
|
||||||
|
object WidgetOptions {
|
||||||
|
fun getString(options: Map<String, JsonElement>, key: String, default: String = ""): String {
|
||||||
|
val el = options[key]
|
||||||
|
return if (el is JsonPrimitive) el.contentOrNull ?: default else default
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getInt(options: Map<String, JsonElement>, key: String, default: Int = 0): Int {
|
||||||
|
val el = options[key]
|
||||||
|
return if (el is JsonPrimitive) el.intOrNull ?: default else default
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBoolean(options: Map<String, JsonElement>, key: String, default: Boolean = false): Boolean {
|
||||||
|
val el = options[key]
|
||||||
|
return if (el is JsonPrimitive) el.booleanOrNull ?: default else default
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getList(options: Map<String, JsonElement>, key: String): List<String> {
|
||||||
|
val el = options[key]
|
||||||
|
return if (el is JsonArray) el.mapNotNull { (it as? JsonPrimitive)?.contentOrNull } else emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.bricks.mp.utils
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具函数 - 对应 JS 版 bricks 的 extend/obj_fmtstr 等
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对象深拷贝合并 (对应 bricks.extend)
|
||||||
|
*/
|
||||||
|
fun <K, V> Map<K, V>.extend(other: Map<K, V>): Map<K, V> = this + other
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模板字符串替换 (对应 bricks.obj_fmtstr)
|
||||||
|
* 支持 ${var} 和 {{var}} 两种格式
|
||||||
|
*/
|
||||||
|
fun String.fmt(vars: Map<String, Any?>): String {
|
||||||
|
var result = this
|
||||||
|
for ((key, value) in vars) {
|
||||||
|
result = result.replace("\${{$key}}", value?.toString() ?: "")
|
||||||
|
.replace("$${key}$", value?.toString() ?: "")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 widget ID (从 options 中提取)
|
||||||
|
*/
|
||||||
|
fun getWidgetId(options: Map<String, Any?>): String =
|
||||||
|
(options["id"] as? String) ?: ""
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
package com.bricks.mp.widgets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bricks.mp.core.BricksWidget
|
||||||
|
import com.bricks.mp.core.WidgetOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入组件
|
||||||
|
* KeyinText -> TextField, Input -> 动态类型
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RenderKeyinTextWidget(widget: BricksWidget) {
|
||||||
|
var text by remember { mutableStateOf(WidgetOptions.getString(widget.options, "value", "")) }
|
||||||
|
val placeholder = WidgetOptions.getString(widget.options, "placeholder", "")
|
||||||
|
val label = WidgetOptions.getString(widget.options, "label", "")
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = { text = it },
|
||||||
|
placeholder = { if (placeholder.isNotEmpty()) Text(placeholder) },
|
||||||
|
label = { if (label.isNotEmpty()) Text(label) },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderInputWidget(widget: BricksWidget) {
|
||||||
|
var text by remember { mutableStateOf(WidgetOptions.getString(widget.options, "value", "")) }
|
||||||
|
val inputType = WidgetOptions.getString(widget.options, "type", "text")
|
||||||
|
|
||||||
|
when (inputType) {
|
||||||
|
"password" -> {
|
||||||
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
OutlinedTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = { text = it },
|
||||||
|
visualTransformation = if (!visible) androidx.compose.ui.text.input.PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
"number" -> {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = { if (it.all { c -> c.isDigit() || c == '.' }) text = it },
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = text,
|
||||||
|
onValueChange = { text = it },
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderTooltipWidget(widget: BricksWidget) {
|
||||||
|
// Simplified tooltip - in real app would use TooltipBox
|
||||||
|
val text = WidgetOptions.getString(widget.options, "text", "")
|
||||||
|
Text(text, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.bricks.mp.widgets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bricks.mp.core.BricksWidget
|
||||||
|
import com.bricks.mp.core.WidgetOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 布局组件
|
||||||
|
* HBox -> Row, VBox -> Column, Filler -> Spacer
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RenderLayoutWidget(widget: BricksWidget) {
|
||||||
|
when (widget.widgettype) {
|
||||||
|
"HBox", "FHBox" -> {
|
||||||
|
val align = when (WidgetOptions.getString(widget.options, "align", "start")) {
|
||||||
|
"center" -> Alignment.CenterVertically
|
||||||
|
"end" -> Alignment.Bottom
|
||||||
|
else -> Alignment.Top
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = WidgetOptions.getInt(widget.options, "padding", 0).dp),
|
||||||
|
verticalAlignment = align,
|
||||||
|
horizontalArrangement = when (widget.widgettype) {
|
||||||
|
"FHBox" -> Arrangement.SpaceBetween
|
||||||
|
else -> Arrangement.Start
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
widget.subwidgets.forEach { child -> RenderWidget(child) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"VBox", "FVBox" -> {
|
||||||
|
val align = when (WidgetOptions.getString(widget.options, "align", "start")) {
|
||||||
|
"center" -> Alignment.CenterHorizontally
|
||||||
|
"end" -> Alignment.End
|
||||||
|
else -> Alignment.Start
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = WidgetOptions.getInt(widget.options, "padding", 0).dp),
|
||||||
|
horizontalAlignment = align,
|
||||||
|
verticalArrangement = when (widget.widgettype) {
|
||||||
|
"FVBox" -> Arrangement.SpaceBetween
|
||||||
|
else -> Arrangement.Top
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
widget.subwidgets.forEach { child -> RenderWidget(child) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Filler", "HFiller" -> {
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
}
|
||||||
|
"VFiller" -> {
|
||||||
|
Spacer(modifier = Modifier.height(WidgetOptions.getInt(widget.options, "height", 16).dp))
|
||||||
|
}
|
||||||
|
"ResponsiveBox" -> {
|
||||||
|
BoxWithConstraints {
|
||||||
|
if (maxWidth > 600.dp) {
|
||||||
|
Row { widget.subwidgets.forEach { RenderWidget(it) } }
|
||||||
|
} else {
|
||||||
|
Column { widget.subwidgets.forEach { RenderWidget(it) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package com.bricks.mp.widgets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.bricks.mp.core.BricksWidget
|
||||||
|
import com.bricks.mp.core.WidgetOptions
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文本组件渲染
|
||||||
|
* 映射: Text -> Text, Title1-6 -> Text with different sizes
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RenderTextWidget(widget: BricksWidget) {
|
||||||
|
val text = WidgetOptions.getString(widget.options, "text", "")
|
||||||
|
val i18n = WidgetOptions.getBoolean(widget.options, "i18n", false)
|
||||||
|
val otext = WidgetOptions.getString(widget.options, "otext", text)
|
||||||
|
|
||||||
|
val displayText = if (i18n && otext.isNotEmpty()) otext else text
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = displayText,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RenderTitleWidget(widget: BricksWidget) {
|
||||||
|
val text = WidgetOptions.getString(widget.options, "text", "")
|
||||||
|
val (fontSize, fontWeight) = when (widget.widgettype) {
|
||||||
|
"Title1" -> 32.sp to FontWeight.Bold
|
||||||
|
"Title2" -> 28.sp to FontWeight.Bold
|
||||||
|
"Title3" -> 24.sp to FontWeight.SemiBold
|
||||||
|
"Title4" -> 20.sp to FontWeight.SemiBold
|
||||||
|
"Title5" -> 18.sp to FontWeight.Medium
|
||||||
|
"Title6" -> 16.sp to FontWeight.Medium
|
||||||
|
else -> 20.sp to FontWeight.SemiBold
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 6.dp),
|
||||||
|
fontSize = fontSize,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
style = MaterialTheme.typography.headlineLarge.copy(fontSize = fontSize, fontWeight = fontWeight)
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user