feat(sample): 添加环境设置功能支持本地联调
- 新增 EnvironmentRepository 处理环境状态持久化 - 添加 EnvironmentScreen 提供环境切换界面 - 集成环境设置到主界面和登录界面导航 - 实现外网和本地联调模式切换逻辑 - 添加环境配置文档说明
这个提交包含在:
父节点
5a0378d579
当前提交
c318a318a3
@ -92,6 +92,8 @@ SampleEnvironmentConfig.useLocalhost("10.0.2.2")
|
|||||||
|
|
||||||
切换后,HTTP API 会立即走新端点;如果 IM 已登录,SDK 会自动重新连接。
|
切换后,HTTP API 会立即走新端点;如果 IM 已登录,SDK 会自动重新连接。
|
||||||
|
|
||||||
|
sample-app 里也提供了一个“环境设置”页面,可以直接在外网和本地联调之间切换。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## sdk-core
|
## sdk-core
|
||||||
|
|||||||
@ -3,14 +3,12 @@ package com.xuqm.sdk.sample
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import com.xuqm.sdk.XuqmSDK
|
import com.xuqm.sdk.XuqmSDK
|
||||||
import com.xuqm.sdk.core.LogLevel
|
import com.xuqm.sdk.core.LogLevel
|
||||||
import com.xuqm.sdk.sample.config.SampleEnvironmentConfig
|
|
||||||
import com.xuqm.sdk.sample.di.AppDependencies
|
import com.xuqm.sdk.sample.di.AppDependencies
|
||||||
|
|
||||||
class XuqmSampleApp : Application() {
|
class XuqmSampleApp : Application() {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
SampleEnvironmentConfig.useExternal()
|
|
||||||
AppDependencies.init(this)
|
AppDependencies.init(this)
|
||||||
XuqmSDK.initialize(
|
XuqmSDK.initialize(
|
||||||
context = this,
|
context = this,
|
||||||
|
|||||||
@ -0,0 +1,59 @@
|
|||||||
|
package com.xuqm.sdk.sample.data.repo
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.xuqm.sdk.sample.config.SampleEnvironmentConfig
|
||||||
|
|
||||||
|
enum class EnvironmentMode {
|
||||||
|
EXTERNAL,
|
||||||
|
LOCALHOST,
|
||||||
|
}
|
||||||
|
|
||||||
|
data class EnvironmentState(
|
||||||
|
val mode: EnvironmentMode = EnvironmentMode.EXTERNAL,
|
||||||
|
val host: String = "10.0.2.2",
|
||||||
|
)
|
||||||
|
|
||||||
|
class EnvironmentRepository(context: Context) {
|
||||||
|
|
||||||
|
private val prefs = context.getSharedPreferences("xuqm_demo_env", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
|
fun current(): EnvironmentState = load().also { apply(it) }
|
||||||
|
|
||||||
|
fun setExternal() {
|
||||||
|
save(EnvironmentState(mode = EnvironmentMode.EXTERNAL, host = load().host))
|
||||||
|
SampleEnvironmentConfig.useExternal()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLocalhost(host: String) {
|
||||||
|
val normalizedHost = host.trim().ifBlank { "10.0.2.2" }
|
||||||
|
save(EnvironmentState(mode = EnvironmentMode.LOCALHOST, host = normalizedHost))
|
||||||
|
SampleEnvironmentConfig.useLocalhost(normalizedHost)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load(): EnvironmentState {
|
||||||
|
val mode = runCatching {
|
||||||
|
EnvironmentMode.valueOf(prefs.getString(KEY_MODE, EnvironmentMode.EXTERNAL.name)!!)
|
||||||
|
}.getOrDefault(EnvironmentMode.EXTERNAL)
|
||||||
|
val host = prefs.getString(KEY_HOST, "10.0.2.2").orEmpty().ifBlank { "10.0.2.2" }
|
||||||
|
return EnvironmentState(mode = mode, host = host)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun save(state: EnvironmentState) {
|
||||||
|
prefs.edit()
|
||||||
|
.putString(KEY_MODE, state.mode.name)
|
||||||
|
.putString(KEY_HOST, state.host)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun apply(state: EnvironmentState) {
|
||||||
|
when (state.mode) {
|
||||||
|
EnvironmentMode.EXTERNAL -> SampleEnvironmentConfig.useExternal()
|
||||||
|
EnvironmentMode.LOCALHOST -> SampleEnvironmentConfig.useLocalhost(state.host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val KEY_MODE = "mode"
|
||||||
|
const val KEY_HOST = "host"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,15 +1,19 @@
|
|||||||
package com.xuqm.sdk.sample.di
|
package com.xuqm.sdk.sample.di
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.xuqm.sdk.sample.data.api.DemoApi
|
|
||||||
import com.xuqm.sdk.sample.data.repo.AuthRepository
|
import com.xuqm.sdk.sample.data.repo.AuthRepository
|
||||||
|
import com.xuqm.sdk.sample.data.repo.EnvironmentRepository
|
||||||
|
|
||||||
object AppDependencies {
|
object AppDependencies {
|
||||||
|
|
||||||
lateinit var authRepository: AuthRepository
|
lateinit var authRepository: AuthRepository
|
||||||
private set
|
private set
|
||||||
|
lateinit var environmentRepository: EnvironmentRepository
|
||||||
|
private set
|
||||||
|
|
||||||
fun init(context: Context) {
|
fun init(context: Context) {
|
||||||
|
environmentRepository = EnvironmentRepository(context)
|
||||||
|
environmentRepository.current()
|
||||||
authRepository = AuthRepository(context)
|
authRepository = AuthRepository(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import com.xuqm.sdk.sample.data.repo.AuthRepository
|
|||||||
import com.xuqm.sdk.sample.ui.auth.LoginScreen
|
import com.xuqm.sdk.sample.ui.auth.LoginScreen
|
||||||
import com.xuqm.sdk.sample.ui.auth.RegisterScreen
|
import com.xuqm.sdk.sample.ui.auth.RegisterScreen
|
||||||
import com.xuqm.sdk.sample.ui.chat.ChatScreen
|
import com.xuqm.sdk.sample.ui.chat.ChatScreen
|
||||||
|
import com.xuqm.sdk.sample.ui.environment.EnvironmentScreen
|
||||||
import com.xuqm.sdk.sample.ui.group.GroupSettingsScreen
|
import com.xuqm.sdk.sample.ui.group.GroupSettingsScreen
|
||||||
import com.xuqm.sdk.sample.ui.main.MainScreen
|
import com.xuqm.sdk.sample.ui.main.MainScreen
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
@ -33,6 +34,7 @@ fun AppNavGraph(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onNavigateToRegister = { navController.navigate("register") },
|
onNavigateToRegister = { navController.navigate("register") },
|
||||||
|
onOpenEnvironment = { navController.navigate("environment") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable("register") {
|
composable("register") {
|
||||||
@ -56,6 +58,7 @@ fun AppNavGraph(
|
|||||||
onGroupSettings = { groupId ->
|
onGroupSettings = { groupId ->
|
||||||
navController.navigate("group_settings/$groupId")
|
navController.navigate("group_settings/$groupId")
|
||||||
},
|
},
|
||||||
|
onOpenEnvironment = { navController.navigate("environment") },
|
||||||
onLogout = {
|
onLogout = {
|
||||||
navController.navigate("auth") {
|
navController.navigate("auth") {
|
||||||
popUpTo("main") { inclusive = true }
|
popUpTo("main") { inclusive = true }
|
||||||
@ -88,5 +91,11 @@ fun AppNavGraph(
|
|||||||
onNavigateBack = { navController.popBackStack() },
|
onNavigateBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable("environment") {
|
||||||
|
EnvironmentScreen(
|
||||||
|
onNavigateBack = { navController.popBackStack() },
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,6 +36,7 @@ import com.xuqm.sdk.sample.di.AppDependencies
|
|||||||
fun LoginScreen(
|
fun LoginScreen(
|
||||||
onLoginSuccess: () -> Unit,
|
onLoginSuccess: () -> Unit,
|
||||||
onNavigateToRegister: () -> Unit,
|
onNavigateToRegister: () -> Unit,
|
||||||
|
onOpenEnvironment: () -> Unit,
|
||||||
viewModel: LoginViewModel = viewModel(
|
viewModel: LoginViewModel = viewModel(
|
||||||
factory = viewModelFactory {
|
factory = viewModelFactory {
|
||||||
initializer { LoginViewModel(AppDependencies.authRepository) }
|
initializer { LoginViewModel(AppDependencies.authRepository) }
|
||||||
@ -111,5 +112,9 @@ fun LoginScreen(
|
|||||||
TextButton(onClick = onNavigateToRegister) {
|
TextButton(onClick = onNavigateToRegister) {
|
||||||
Text("没有账号?注册")
|
Text("没有账号?注册")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TextButton(onClick = onOpenEnvironment) {
|
||||||
|
Text("环境设置")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,225 @@
|
|||||||
|
package com.xuqm.sdk.sample.ui.environment
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.lifecycle.viewmodel.initializer
|
||||||
|
import androidx.lifecycle.viewmodel.viewModelFactory
|
||||||
|
import com.xuqm.sdk.sample.data.repo.EnvironmentMode
|
||||||
|
import com.xuqm.sdk.sample.data.repo.EnvironmentRepository
|
||||||
|
import com.xuqm.sdk.sample.data.repo.EnvironmentState
|
||||||
|
import com.xuqm.sdk.sample.di.AppDependencies
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
data class EnvironmentUiState(
|
||||||
|
val mode: EnvironmentMode = EnvironmentMode.EXTERNAL,
|
||||||
|
val host: String = "10.0.2.2",
|
||||||
|
val message: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
class EnvironmentViewModel(
|
||||||
|
private val environmentRepository: EnvironmentRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val initial = environmentRepository.current()
|
||||||
|
private val _state = MutableStateFlow(initial.toUiState())
|
||||||
|
val state: StateFlow<EnvironmentUiState> = _state
|
||||||
|
|
||||||
|
fun selectExternal() {
|
||||||
|
_state.value = _state.value.copy(mode = EnvironmentMode.EXTERNAL, message = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectLocalhost() {
|
||||||
|
_state.value = _state.value.copy(mode = EnvironmentMode.LOCALHOST, message = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHost(host: String) {
|
||||||
|
_state.value = _state.value.copy(host = host, message = null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun apply() {
|
||||||
|
val current = _state.value
|
||||||
|
when (current.mode) {
|
||||||
|
EnvironmentMode.EXTERNAL -> environmentRepository.setExternal()
|
||||||
|
EnvironmentMode.LOCALHOST -> environmentRepository.setLocalhost(current.host)
|
||||||
|
}
|
||||||
|
_state.value = current.copy(message = "已切换并保存")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun EnvironmentState.toUiState(): EnvironmentUiState =
|
||||||
|
EnvironmentUiState(mode = mode, host = host)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun EnvironmentScreen(
|
||||||
|
onNavigateBack: () -> Unit,
|
||||||
|
viewModel: EnvironmentViewModel = viewModel(
|
||||||
|
factory = viewModelFactory {
|
||||||
|
initializer { EnvironmentViewModel(AppDependencies.environmentRepository) }
|
||||||
|
}
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
var hostInput by remember(state.host) { mutableStateOf(state.host) }
|
||||||
|
|
||||||
|
LaunchedEffect(state.host) {
|
||||||
|
hostInput = state.host
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = { Text("环境设置") },
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onNavigateBack) {
|
||||||
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.padding(24.dp)
|
||||||
|
.imePadding(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Settings,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "切换 demo 与 SDK 的联调端点",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = if (state.mode == EnvironmentMode.EXTERNAL) {
|
||||||
|
"当前使用外网服务:dev.xuqinmin.com"
|
||||||
|
} else {
|
||||||
|
"当前使用本地服务:http://${state.host}:8081"
|
||||||
|
},
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
|
||||||
|
EnvironmentModeRow(
|
||||||
|
title = "外网服务",
|
||||||
|
selected = state.mode == EnvironmentMode.EXTERNAL,
|
||||||
|
description = "走线上 / 开发服务器,适合正常联调。",
|
||||||
|
onClick = viewModel::selectExternal,
|
||||||
|
)
|
||||||
|
|
||||||
|
EnvironmentModeRow(
|
||||||
|
title = "本地联调",
|
||||||
|
selected = state.mode == EnvironmentMode.LOCALHOST,
|
||||||
|
description = "走本机或局域网服务,适合本地开发。",
|
||||||
|
onClick = viewModel::selectLocalhost,
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = hostInput,
|
||||||
|
onValueChange = {
|
||||||
|
hostInput = it
|
||||||
|
viewModel.updateHost(it)
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
label = { Text("本地 Host") },
|
||||||
|
placeholder = { Text("10.0.2.2 或你的电脑局域网 IP") },
|
||||||
|
singleLine = true,
|
||||||
|
enabled = state.mode == EnvironmentMode.LOCALHOST,
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "切换后会立即重建 HTTP 客户端;如果 IM 已登录,会自动重连。",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
|
||||||
|
Button(
|
||||||
|
onClick = viewModel::apply,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Text("保存并应用")
|
||||||
|
}
|
||||||
|
|
||||||
|
state.message?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun EnvironmentModeRow(
|
||||||
|
title: String,
|
||||||
|
selected: Boolean,
|
||||||
|
description: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.Surface(
|
||||||
|
onClick = onClick,
|
||||||
|
tonalElevation = if (selected) 2.dp else 0.dp,
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
androidx.compose.foundation.layout.Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
RadioButton(selected = selected, onClick = onClick)
|
||||||
|
Text(title, style = MaterialTheme.typography.titleSmall)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = description,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,11 +8,13 @@ import androidx.compose.material.icons.filled.ChatBubble
|
|||||||
import androidx.compose.material.icons.filled.Group
|
import androidx.compose.material.icons.filled.Group
|
||||||
import androidx.compose.material.icons.filled.People
|
import androidx.compose.material.icons.filled.People
|
||||||
import androidx.compose.material.icons.filled.Person
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.SystemUpdate
|
import androidx.compose.material.icons.filled.SystemUpdate
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
@ -44,13 +46,21 @@ private val tabs = listOf(
|
|||||||
fun MainScreen(
|
fun MainScreen(
|
||||||
onOpenChat: (targetId: String, chatType: String, targetName: String) -> Unit,
|
onOpenChat: (targetId: String, chatType: String, targetName: String) -> Unit,
|
||||||
onGroupSettings: (groupId: String) -> Unit,
|
onGroupSettings: (groupId: String) -> Unit,
|
||||||
|
onOpenEnvironment: () -> Unit,
|
||||||
onLogout: () -> Unit,
|
onLogout: () -> Unit,
|
||||||
) {
|
) {
|
||||||
var selectedTab by remember { mutableIntStateOf(0) }
|
var selectedTab by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(title = { Text(tabs[selectedTab].label) })
|
TopAppBar(
|
||||||
|
title = { Text(tabs[selectedTab].label) },
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onOpenEnvironment) {
|
||||||
|
Icon(Icons.Default.Settings, contentDescription = "环境设置")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
},
|
},
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
NavigationBar {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户