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