feat: Android login/register add mandatory agreement checkbox

- LoginScreen: add agreement checkbox, disabled until checked
- RegisterScreen: add agreement checkbox, disabled until checked
- RegisterViewModel: pass agreed_terms/agreed_terms_version to API
- RegisterRequest DTO: add agreedTerms/agreedTermsVersion fields
- Also include 3 template scripts missed in previous commit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-01 22:06:16 +08:00
parent 41ffd31923
commit 3dcf769660
8 changed files with 99 additions and 12 deletions

View File

@@ -266,7 +266,9 @@ data class RegisterRequest(
val username: String,
val password: String,
val email: String = "",
@SerializedName("display_name") val displayName: String? = null
@SerializedName("display_name") val displayName: String? = null,
@SerializedName("agreed_terms") val agreedTerms: Boolean = false,
@SerializedName("agreed_terms_version") val agreedTermsVersion: String? = null
)
data class RegisterResponse(

View File

@@ -217,7 +217,42 @@ fun LoginScreen(
)
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(16.dp))
// Agreement checkbox
val context = LocalContext.current
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = uiState.agreedTerms,
onCheckedChange = viewModel::onAgreedTermsChanged,
enabled = !uiState.isLoading
)
Text("我已阅读并同意", style = MaterialTheme.typography.bodySmall)
TextButton(
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://tiangong.ai/privacy"))
context.startActivity(intent)
},
contentPadding = PaddingValues(horizontal = 2.dp)
) {
Text("《隐私政策》", style = MaterialTheme.typography.bodySmall)
}
Text("", style = MaterialTheme.typography.bodySmall)
TextButton(
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://tiangong.ai/terms"))
context.startActivity(intent)
},
contentPadding = PaddingValues(horizontal = 2.dp)
) {
Text("《用户协议》", style = MaterialTheme.typography.bodySmall)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Login button
Button(
@@ -228,7 +263,7 @@ fun LoginScreen(
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
enabled = !uiState.isLoading
enabled = !uiState.isLoading && uiState.agreedTerms
) {
if (uiState.isLoading) {
CircularProgressIndicator(
@@ -251,7 +286,6 @@ fun LoginScreen(
Spacer(modifier = Modifier.height(24.dp))
// Privacy policy & user agreement
val context = LocalContext.current
PrivacyPolicyRow(
onPrivacyPolicy = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://tiangong.ai/privacy"))

View File

@@ -16,6 +16,7 @@ data class LoginUiState(
val username: String = "",
val password: String = "",
val serverUrl: String = "",
val agreedTerms: Boolean = false,
val isLoading: Boolean = false,
val error: String? = null,
val isLoggedIn: Boolean = false
@@ -50,6 +51,10 @@ class LoginViewModel @Inject constructor(
_uiState.value = _uiState.value.copy(serverUrl = url, error = null)
}
fun onAgreedTermsChanged(value: Boolean) {
_uiState.value = _uiState.value.copy(agreedTerms = value, error = null)
}
fun saveServerUrl(url: String) {
val trimmed = url.trim().trimEnd('/')
if (trimmed.isBlank()) {

View File

@@ -10,6 +10,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.*
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -178,13 +179,47 @@ fun RegisterScreen(
Spacer(modifier = Modifier.height(8.dp))
// Agreement checkbox
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = uiState.agreedTerms,
onCheckedChange = viewModel::onAgreedTermsChanged,
enabled = !uiState.isLoading
)
Text(
text = "我已阅读并同意",
style = MaterialTheme.typography.bodySmall
)
TextButton(
onClick = { /* TODO: open privacy policy in-app */ },
contentPadding = PaddingValues(horizontal = 2.dp)
) {
Text("《隐私政策》", style = MaterialTheme.typography.bodySmall)
}
Text(
text = "",
style = MaterialTheme.typography.bodySmall
)
TextButton(
onClick = { /* TODO: open user agreement in-app */ },
contentPadding = PaddingValues(horizontal = 2.dp)
) {
Text("《用户协议》", style = MaterialTheme.typography.bodySmall)
}
}
Spacer(modifier = Modifier.height(8.dp))
// Register button
Button(
onClick = {
focusManager.clearFocus()
viewModel.register()
},
enabled = !uiState.isLoading,
enabled = !uiState.isLoading && uiState.agreedTerms,
modifier = Modifier.fillMaxWidth().height(50.dp)
) {
if (uiState.isLoading) {

View File

@@ -18,6 +18,7 @@ data class RegisterUiState(
val confirmPassword: String = "",
val email: String = "",
val displayName: String = "",
val agreedTerms: Boolean = false,
val isUsernameValid: Boolean = true,
val isPasswordValid: Boolean = true,
@@ -96,6 +97,10 @@ class RegisterViewModel @Inject constructor(
_uiState.update { it.copy(displayName = value) }
}
fun onAgreedTermsChanged(value: Boolean) {
_uiState.update { it.copy(agreedTerms = value) }
}
fun clearError() {
_uiState.update { it.copy(errorMessage = null) }
}
@@ -137,6 +142,10 @@ class RegisterViewModel @Inject constructor(
}
if (!usernameOk || !passwordOk || !confirmOk || !emailOk) return
if (!state.agreedTerms) {
_uiState.update { it.copy(errorMessage = "请先阅读并同意用户协议和隐私政策") }
return
}
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
@@ -145,7 +154,9 @@ class RegisterViewModel @Inject constructor(
username = state.username.trim(),
password = state.password,
email = state.email.trim(),
displayName = state.displayName.trim().ifBlank { null }
displayName = state.displayName.trim().ifBlank { null },
agreedTerms = true,
agreedTermsVersion = "1.0"
)
val result = authRepository.register(request)
result.fold(

View File

@@ -17,8 +17,8 @@ import time
import requests
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
USER = os.getenv("PLATFORM_USERNAME", "admin")
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
USER = os.getenv("PLATFORM_USERNAME")
PWD = os.getenv("PLATFORM_PASSWORD")
TEMPLATE_ID = "template_customer_service"
DEFAULT_NAME = os.getenv("TARGET_NAME") or f"场景模板_客服_{int(time.time())}"

View File

@@ -14,8 +14,8 @@ import time
import requests
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
USER = os.getenv("PLATFORM_USERNAME", "admin")
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
USER = os.getenv("PLATFORM_USERNAME")
PWD = os.getenv("PLATFORM_PASSWORD")
TEMPLATE_ID = "template_dev_codegen"
DEFAULT_NAME = os.getenv("TARGET_NAME") or f"场景模板_研发_{int(time.time())}"

View File

@@ -14,8 +14,8 @@ import time
import requests
BASE = os.getenv("PLATFORM_BASE_URL", "http://127.0.0.1:8037").rstrip("/")
USER = os.getenv("PLATFORM_USERNAME", "admin")
PWD = os.getenv("PLATFORM_PASSWORD", "123456")
USER = os.getenv("PLATFORM_USERNAME")
PWD = os.getenv("PLATFORM_PASSWORD")
TEMPLATE_ID = "template_ops_log_analysis"
DEFAULT_NAME = os.getenv("TARGET_NAME") or f"场景模板_运维_{int(time.time())}"