task(0a00ccbd-6f98-4730-adf2-6f1e864823de): Mount M2 project workbench home and modernize M1 layouts
- FeatureMount: HOME tab now mounts ProjectWorkspaceFragment; delete placeholder HomeFragment - app/build.gradle.kts + libs.versions.toml: add androidx recyclerview and swiperefreshlayout for project list and pull-to-refresh - NetworkModule: register ProjectApi (M2 2.5.1.1 GetProjectList) via provideProjectApi - SessionRepository: add SessionState.Restoring as initial state so cold start renders neutral loading instead of prompting login - MineFragment/layout: consumer-grade flow with horizontal identity card (whole card acts as login CTA when signed out), config warning card, grouped settings rows; drop back button and manual refresh entry - MineViewModel: remove manual refresh(); session restore is now automatic on cold start - LoginFragment/layout: MaterialToolbar back, segmented MaterialButtonToggleGroup for account/SMS mode, TextInputLayout floating-label inputs, linear progress below button - ConfigFragment/ChangePasswordFragment layouts: unify back navigation to Toolbar, switch plain EditTexts to TextInputLayout, replace ProgressBar with LinearProgressIndicator - strings.xml: add M2 home workbench copy and mine/login labels; remove unused action_back, mine_login, mine_refresh, mine_config_ready, mine_logout_confirm
This commit is contained in:
@@ -57,6 +57,8 @@ dependencies {
|
||||
implementation(libs.androidx.fragment.ktx)
|
||||
implementation(libs.androidx.splashscreen)
|
||||
implementation(libs.google.material)
|
||||
implementation(libs.androidx.recyclerview)
|
||||
implementation(libs.androidx.swiperefreshlayout)
|
||||
|
||||
implementation(libs.androidx.lifecycle.runtime)
|
||||
implementation(libs.androidx.lifecycle.viewmodel)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.stec.cmd
|
||||
|
||||
import com.stec.cmd.feature.home.ProjectWorkspaceFragment
|
||||
import com.stec.cmd.feature.mine.MineFragment
|
||||
|
||||
/**
|
||||
@@ -30,7 +31,7 @@ object FeatureMount {
|
||||
|
||||
/** 挂载注册表:S1+ 替换 fragmentClass 即完成业务页挂载。 */
|
||||
val mounts: List<MountItem> = listOf(
|
||||
MountItem(Tab.HOME, HomeFragment::class.java, "M2 项目工作台"),
|
||||
MountItem(Tab.HOME, ProjectWorkspaceFragment::class.java, "M2 项目工作台"),
|
||||
MountItem(Tab.TASK, TaskFragment::class.java, "M3 任务管理"),
|
||||
MountItem(Tab.COLLECT, CollectFragment::class.java, "M4+M5 数据采集"),
|
||||
MountItem(Tab.STATS, StatsFragment::class.java, "M6 统计与上传"),
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
package com.stec.cmd
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
||||
/** 首页占位(M2 项目工作台,S1 挂载项目列表)。 */
|
||||
@AndroidEntryPoint
|
||||
class HomeFragment : PlaceholderFragment()
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.stec.cmd.feature.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.stec.cmd.config.ConfigRepository
|
||||
import com.stec.cmd.core.common.UiState
|
||||
import com.stec.cmd.core.network.ApiError
|
||||
import com.stec.cmd.core.network.api.ProjectItem
|
||||
import com.stec.cmd.feature.mine.userMessage
|
||||
import com.stec.cmd.session.ProjectRepository
|
||||
import com.stec.cmd.session.ProjectSession
|
||||
import com.stec.cmd.session.SessionRepository
|
||||
import com.stec.cmd.session.SessionState
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 首页项目工作台(M2-01/02/03):
|
||||
* 会话 / 配置 / 项目列表 / 选中项目四流组合渲染。
|
||||
*
|
||||
* 自动加载门控:SignedIn 且三要素就绪且列表尚为 [UiState.Idle] 时发起加载;
|
||||
* 会话离开 SignedIn 复位为 Idle(登出后重新登录可重新加载)。
|
||||
*/
|
||||
@HiltViewModel
|
||||
class HomeViewModel @Inject constructor(
|
||||
private val projectRepository: ProjectRepository,
|
||||
private val projectSession: ProjectSession,
|
||||
sessionRepository: SessionRepository,
|
||||
private val configRepository: ConfigRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
/** 会话状态流(Restoring/SignedOut/SignedIn 门控首页状态机)。 */
|
||||
val sessionState = sessionRepository.state
|
||||
|
||||
/** 连接配置快照流(三要素缺失时警示引导)。 */
|
||||
val configSnapshot = configRepository.snapshotStateFlow
|
||||
|
||||
/** 当前选中项目(M2-03,M3/M5/M6 共享订阅)。 */
|
||||
val selectedProject = projectSession.selectedProject
|
||||
|
||||
private val _projects = MutableStateFlow<UiState<List<ProjectItem>>>(UiState.Idle)
|
||||
|
||||
/** 项目列表加载态。 */
|
||||
val projects: StateFlow<UiState<List<ProjectItem>>> = _projects.asStateFlow()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
sessionState.collect { state ->
|
||||
when (state) {
|
||||
is SessionState.SignedIn ->
|
||||
if (configRepository.snapshotStateFlow.value.isReady &&
|
||||
_projects.value is UiState.Idle
|
||||
) {
|
||||
load()
|
||||
}
|
||||
else -> _projects.value = UiState.Idle
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launch {
|
||||
configRepository.snapshotStateFlow.collect { snapshot ->
|
||||
if (snapshot.isReady &&
|
||||
sessionState.value is SessionState.SignedIn &&
|
||||
_projects.value is UiState.Idle
|
||||
) {
|
||||
load()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 拉取项目列表;成功且尚无选中时自动选中首项(给 M3/M6 可用默认值)。 */
|
||||
fun load() {
|
||||
viewModelScope.launch {
|
||||
_projects.value = UiState.Loading
|
||||
_projects.value = try {
|
||||
val list = projectRepository.projects()
|
||||
if (selectedProject.value == null) {
|
||||
list.firstOrNull()?.let(projectSession::select)
|
||||
}
|
||||
UiState.Success(list)
|
||||
} catch (e: ApiError) {
|
||||
UiState.Error(e.userMessage())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 下拉刷新 / 错误重试。 */
|
||||
fun refresh() = load()
|
||||
|
||||
/** M2-03 项目切换:写入共享选中态,订阅方(M3/M6)随之刷新。 */
|
||||
fun select(project: ProjectItem) = projectSession.select(project)
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.stec.cmd.feature.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.stec.cmd.R
|
||||
import com.stec.cmd.core.network.api.ProjectItem
|
||||
import com.stec.cmd.databinding.ItemProjectSheetBinding
|
||||
import com.stec.cmd.databinding.SheetProjectSwitcherBinding
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* 项目切换单选底部弹层(M2-03,消费级单选范式)。
|
||||
*
|
||||
* 项目列表经 JSON 字符串参数传入(ProjectItem 非 Parcelable,避免依赖序列化之外的传递机制);
|
||||
* 点选结果经父 Fragment 实现 [OnProjectPicked] 回调后写 [com.stec.cmd.session.ProjectSession]。
|
||||
*/
|
||||
class ProjectSwitcherSheet : BottomSheetDialogFragment() {
|
||||
|
||||
/** 切换结果回调,由宿主 Fragment 实现。 */
|
||||
interface OnProjectPicked {
|
||||
fun onProjectPicked(project: ProjectItem)
|
||||
}
|
||||
|
||||
private var _binding: SheetProjectSwitcherBinding? = null
|
||||
private val binding get() = checkNotNull(_binding)
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
coerceInputValues = true
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = SheetProjectSwitcherBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.recyclerProjects.layoutManager = LinearLayoutManager(requireContext())
|
||||
binding.recyclerProjects.adapter = ProjectAdapter(
|
||||
items = decodeProjects(),
|
||||
selectedId = arguments?.getString(ARG_SELECTED_ID),
|
||||
onPicked = { picked ->
|
||||
(parentFragment as? OnProjectPicked)?.onProjectPicked(picked)
|
||||
dismissAllowingStateLoss()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** 参数 JSON → 项目列表;解析失败退化为空列表(弹层仅展示标题)。 */
|
||||
private fun decodeProjects(): List<ProjectItem> =
|
||||
arguments?.getString(ARG_PROJECTS_JSON)?.let { raw ->
|
||||
runCatching {
|
||||
json.decodeFromString(ListSerializer(ProjectItem.serializer()), raw)
|
||||
}.getOrDefault(emptyList())
|
||||
}.orEmpty()
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARG_PROJECTS_JSON = "projects_json"
|
||||
private const val ARG_SELECTED_ID = "selected_id"
|
||||
|
||||
fun newInstance(projectsJson: String, selectedId: String?): ProjectSwitcherSheet =
|
||||
ProjectSwitcherSheet().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_PROJECTS_JSON, projectsJson)
|
||||
putString(ARG_SELECTED_ID, selectedId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 单选列表适配器:当前选中项打勾,行整体可点。 */
|
||||
private class ProjectAdapter(
|
||||
private val items: List<ProjectItem>,
|
||||
private val selectedId: String?,
|
||||
private val onPicked: (ProjectItem) -> Unit,
|
||||
) : RecyclerView.Adapter<ProjectAdapter.ViewHolder>() {
|
||||
|
||||
class ViewHolder(val binding: ItemProjectSheetBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
|
||||
ViewHolder(
|
||||
ItemProjectSheetBinding.inflate(LayoutInflater.from(parent.context), parent, false),
|
||||
)
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val item = items[position]
|
||||
with(holder.binding) {
|
||||
tvName.text = item.SimpleName?.takeIf { it.isNotBlank() }
|
||||
?: root.context.getString(R.string.home_unnamed_project)
|
||||
tvMeta.text = listOfNotNull(
|
||||
item.ProjectCode?.takeIf { it.isNotBlank() },
|
||||
item.ProjectScale,
|
||||
item.ProjectPhase,
|
||||
).joinToString(SEPARATOR)
|
||||
radioSelected.isChecked = !item.ID.isNullOrBlank() && item.ID == selectedId
|
||||
root.setOnClickListener { onPicked(item) }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val SEPARATOR = " · "
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.stec.cmd.feature.home
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.commit
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.stec.cmd.R
|
||||
import com.stec.cmd.core.common.UiState
|
||||
import com.stec.cmd.core.network.api.ProjectItem
|
||||
import com.stec.cmd.databinding.FragmentHomeBinding
|
||||
import com.stec.cmd.feature.mine.ConfigFragment
|
||||
import com.stec.cmd.feature.mine.LoginFragment
|
||||
import com.stec.cmd.session.SessionState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* 首页项目工作台(M2-01 当前用户项目列表 / M2-02 项目信息展示 / M2-03 项目切换)。
|
||||
*
|
||||
* 渲染状态机:Restoring → 中性加载;SignedOut → 登录 CTA;
|
||||
* SignedIn → 配置门控(缺失警示卡)→ 项目列表加载/空/错误/内容四态。
|
||||
* 子页打开沿用「add + hide + addToBackStack」壳层模式(底栏随返回栈自动隐藏)。
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ProjectWorkspaceFragment : Fragment(), ProjectSwitcherSheet.OnProjectPicked {
|
||||
|
||||
private var _binding: FragmentHomeBinding? = null
|
||||
private val binding get() = checkNotNull(_binding)
|
||||
|
||||
private val viewModel: HomeViewModel by viewModels()
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?,
|
||||
): View {
|
||||
_binding = FragmentHomeBinding.inflate(inflater, container, false)
|
||||
return binding.root
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.swipeRefresh.setColorSchemeResources(R.color.brand_primary)
|
||||
binding.swipeRefresh.setOnRefreshListener { viewModel.refresh() }
|
||||
binding.btnGoLogin.setOnClickListener { openOnTop(LoginFragment(), TAG_LOGIN) }
|
||||
binding.cardConfigWarning.setOnClickListener { openOnTop(ConfigFragment(), TAG_CONFIG) }
|
||||
binding.btnSwitchProject.setOnClickListener { showSwitcher() }
|
||||
binding.btnRetry.setOnClickListener { viewModel.refresh() }
|
||||
binding.btnEmptyRefresh.setOnClickListener { viewModel.refresh() }
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch { viewModel.sessionState.collect { render() } }
|
||||
launch { viewModel.configSnapshot.collect { render() } }
|
||||
launch { viewModel.projects.collect { render() } }
|
||||
launch { viewModel.selectedProject.collect { render() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 四流任一变化即整体重渲(读 ViewModel 当前值,避免组合流样板)。 */
|
||||
private fun render() {
|
||||
val session = viewModel.sessionState.value
|
||||
val config = viewModel.configSnapshot.value
|
||||
val projects = viewModel.projects.value
|
||||
val selected = viewModel.selectedProject.value
|
||||
|
||||
binding.swipeRefresh.isRefreshing =
|
||||
session is SessionState.SignedIn && config.isReady && projects is UiState.Loading
|
||||
// 下拉刷新仅登录且配置就绪时可用,防止覆盖层下穿透触发无效请求
|
||||
binding.swipeRefresh.isVisible = session is SessionState.SignedIn && config.isReady
|
||||
binding.progress.isVisible = false
|
||||
binding.stateSignedOut.isVisible = false
|
||||
binding.stateError.isVisible = false
|
||||
binding.stateEmpty.isVisible = false
|
||||
|
||||
when (session) {
|
||||
// 冷启动恢复中:中性加载,不引导登录
|
||||
SessionState.Restoring -> binding.progress.isVisible = true
|
||||
SessionState.SignedOut -> binding.stateSignedOut.isVisible = true
|
||||
is SessionState.SignedIn -> {
|
||||
binding.cardConfigWarning.isVisible = !config.isReady
|
||||
if (!config.isReady) return
|
||||
|
||||
val contentReady = projects is UiState.Success && projects.data.isNotEmpty()
|
||||
binding.tvSectionCurrent.isVisible = contentReady
|
||||
binding.cardProject.isVisible = contentReady && selected != null
|
||||
binding.tvProjectHint.isVisible = contentReady && selected == null
|
||||
binding.btnSwitchProject.isVisible = contentReady
|
||||
binding.tvProjectCount.isVisible = contentReady
|
||||
|
||||
when (projects) {
|
||||
is UiState.Loading -> binding.progress.isVisible = true
|
||||
is UiState.Error -> {
|
||||
binding.stateError.isVisible = true
|
||||
binding.tvErrorMessage.text = projects.message
|
||||
}
|
||||
is UiState.Success -> {
|
||||
if (projects.data.isEmpty()) {
|
||||
binding.stateEmpty.isVisible = true
|
||||
} else {
|
||||
selected?.let(::renderProject)
|
||||
binding.tvProjectCount.text =
|
||||
getString(R.string.home_project_count_fmt, projects.data.size)
|
||||
}
|
||||
}
|
||||
UiState.Idle -> Unit // 会话/配置流将触发自动加载
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** M2-02 当前项目卡:名称大字 + 编号 + 等级/阶段 Chip。 */
|
||||
private fun renderProject(project: ProjectItem) {
|
||||
binding.tvProjectName.text = project.SimpleName?.takeIf { it.isNotBlank() }
|
||||
?: getString(R.string.home_unnamed_project)
|
||||
binding.tvProjectCode.text = project.ProjectCode?.takeIf { it.isNotBlank() } ?: "-"
|
||||
binding.chipScale.text = project.ProjectScale ?: "-"
|
||||
binding.chipPhase.text = project.ProjectPhase ?: "-"
|
||||
}
|
||||
|
||||
/** M2-03 打开切换弹层;列表经 JSON 传参(ProjectItem 非 Parcelable)。 */
|
||||
private fun showSwitcher() {
|
||||
val projects = (viewModel.projects.value as? UiState.Success)?.data.orEmpty()
|
||||
if (projects.isEmpty()) return
|
||||
val projectsJson = json.encodeToString(ListSerializer(ProjectItem.serializer()), projects)
|
||||
ProjectSwitcherSheet.newInstance(projectsJson, viewModel.selectedProject.value?.ID)
|
||||
.show(childFragmentManager, TAG_SWITCHER)
|
||||
}
|
||||
|
||||
override fun onProjectPicked(project: ProjectItem) {
|
||||
viewModel.select(project)
|
||||
}
|
||||
|
||||
private fun openOnTop(fragment: Fragment, tag: String) {
|
||||
parentFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
add(R.id.fragment_container, fragment, tag)
|
||||
hide(this@ProjectWorkspaceFragment)
|
||||
addToBackStack(null)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
super.onDestroyView()
|
||||
_binding = null
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG_LOGIN = "login"
|
||||
const val TAG_CONFIG = "config"
|
||||
const val TAG_SWITCHER = "project_switcher"
|
||||
}
|
||||
}
|
||||
@@ -36,7 +36,7 @@ class ChangePasswordFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.btnBack.setOnClickListener { parentFragmentManager.popBackStack() }
|
||||
binding.btnBack.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
binding.btnSubmit.setOnClickListener {
|
||||
viewModel.submit(
|
||||
oldPassword = binding.etOldPassword.text?.toString().orEmpty(),
|
||||
@@ -54,7 +54,8 @@ class ChangePasswordFragment : Fragment() {
|
||||
is UiState.Error -> binding.tvMessage.text = state.message
|
||||
is UiState.Success -> {
|
||||
binding.tvMessage.text = getString(R.string.change_password_success)
|
||||
binding.btnBack.callOnClick()
|
||||
// Toolbar 导航点击不经 performClick,直接回退
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
else -> Unit
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class ConfigFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.btnBack.setOnClickListener { parentFragmentManager.popBackStack() }
|
||||
binding.btnBack.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
binding.btnSave.setOnClickListener {
|
||||
viewModel.save(
|
||||
baseUrl = binding.etBaseUrl.text?.toString().orEmpty(),
|
||||
|
||||
@@ -17,7 +17,10 @@ import com.stec.cmd.databinding.FragmentLoginBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/** 登录页(M1-01 账号密码 / M1-02 获取验证码 / M1-03 验证码登录)。 */
|
||||
/**
|
||||
* 登录页(M1-01 账号密码 / M1-02 获取验证码 / M1-03 验证码登录)。
|
||||
* 消费级动线:Toolbar 返回 + 分段切换登录方式 + 浮动标签输入 + 按钮下线性进度。
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class LoginFragment : Fragment() {
|
||||
|
||||
@@ -36,11 +39,16 @@ class LoginFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.btnBack.setOnClickListener { parentFragmentManager.popBackStack() }
|
||||
binding.toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
binding.btnSendCode.setOnClickListener { viewModel.sendCode() }
|
||||
binding.btnLogin.setOnClickListener { viewModel.login() }
|
||||
binding.rbAccount.setOnClickListener { viewModel.updateMode(LoginMode.ACCOUNT) }
|
||||
binding.rbSms.setOnClickListener { viewModel.updateMode(LoginMode.SMS) }
|
||||
binding.toggleMode.addOnButtonCheckedListener { _, checkedId, isChecked ->
|
||||
if (isChecked) {
|
||||
viewModel.updateMode(
|
||||
if (checkedId == R.id.btn_mode_account) LoginMode.ACCOUNT else LoginMode.SMS,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
binding.etLoginName.doAfterTextChanged { viewModel.updateLoginName(it?.toString().orEmpty()) }
|
||||
binding.etPassword.doAfterTextChanged { viewModel.updatePassword(it?.toString().orEmpty()) }
|
||||
@@ -55,14 +63,14 @@ class LoginFragment : Fragment() {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
launch {
|
||||
viewModel.form.collect { form ->
|
||||
val accountMode = form.mode == LoginMode.ACCOUNT
|
||||
if (binding.rbAccount.isChecked != accountMode) {
|
||||
binding.radioGroup.check(
|
||||
if (accountMode) binding.rbAccount.id else binding.rbSms.id,
|
||||
)
|
||||
val targetButton =
|
||||
if (form.mode == LoginMode.ACCOUNT) R.id.btn_mode_account
|
||||
else R.id.btn_mode_sms
|
||||
if (binding.toggleMode.checkedButtonId != targetButton) {
|
||||
binding.toggleMode.check(targetButton)
|
||||
}
|
||||
binding.groupAccount.isVisible = accountMode
|
||||
binding.groupSms.isVisible = !accountMode
|
||||
binding.groupAccount.isVisible = form.mode == LoginMode.ACCOUNT
|
||||
binding.groupSms.isVisible = form.mode == LoginMode.SMS
|
||||
}
|
||||
}
|
||||
launch {
|
||||
@@ -85,7 +93,7 @@ class LoginFragment : Fragment() {
|
||||
)
|
||||
}
|
||||
is UiState.Success -> {
|
||||
// SessionRepository 已持久化 Token;返回「我的」
|
||||
// SessionRepository 已持久化 Token;返回「我的」/首页
|
||||
parentFragmentManager.popBackStack()
|
||||
}
|
||||
else -> Unit
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.stec.cmd.R
|
||||
import com.stec.cmd.config.ConfigRepository
|
||||
import com.stec.cmd.databinding.FragmentMineBinding
|
||||
import com.stec.cmd.session.SessionState
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
@@ -19,7 +20,8 @@ import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* 「我的」业务页(M1-05 用户信息 / M1-06 配置入口 / M1-04 改密入口 / M1-07 退出登录)。
|
||||
* 未配置三要素 → 引导配置页;未登录 → 引导登录页。
|
||||
* 消费级动线:横向身份卡(未登录整卡即登录入口)+ 连接配置警示卡 + 设置列表分组卡;
|
||||
* 冷启动 Restoring 渲染中性加载,不误导去登录。
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class MineFragment : Fragment() {
|
||||
@@ -39,12 +41,14 @@ class MineFragment : Fragment() {
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.btnBack.setOnClickListener { parentFragmentManager.popBackStack() }
|
||||
binding.btnLoginOrLogout.setOnClickListener { onPrimaryAction() }
|
||||
binding.btnChangePassword.setOnClickListener { openOnTop(ChangePasswordFragment(), TAG_CHANGE_PASSWORD) }
|
||||
binding.btnOpenConfig.setOnClickListener { openOnTop(ConfigFragment(), TAG_CONFIG) }
|
||||
binding.btnGoConfig.setOnClickListener { openOnTop(ConfigFragment(), TAG_CONFIG) }
|
||||
binding.btnRefresh.setOnClickListener { viewModel.refresh() }
|
||||
binding.cardUser.setOnClickListener { onUserCardClick() }
|
||||
binding.btnLoginCta.setOnClickListener { openOnTop(LoginFragment(), TAG_LOGIN) }
|
||||
binding.rowChangePassword.setOnClickListener {
|
||||
openOnTop(ChangePasswordFragment(), TAG_CHANGE_PASSWORD)
|
||||
}
|
||||
binding.rowOpenConfig.setOnClickListener { openOnTop(ConfigFragment(), TAG_CONFIG) }
|
||||
binding.cardConfigWarning.setOnClickListener { openOnTop(ConfigFragment(), TAG_CONFIG) }
|
||||
binding.btnLogout.setOnClickListener { viewModel.logout() }
|
||||
|
||||
viewLifecycleOwner.lifecycleScope.launch {
|
||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
@@ -56,38 +60,55 @@ class MineFragment : Fragment() {
|
||||
|
||||
private fun renderSession(state: SessionState) {
|
||||
when (state) {
|
||||
is SessionState.SignedIn -> {
|
||||
binding.cardUser.isVisible = true
|
||||
binding.tvName.text = state.user.Name ?: getString(R.string.mine_unknown_user)
|
||||
binding.tvDept.text = getString(R.string.mine_dept_fmt, state.user.DeptName ?: "-")
|
||||
binding.tvAccount.text = getString(R.string.mine_account_fmt, state.user.LoginName ?: "-")
|
||||
binding.tvPhone.text = getString(R.string.mine_phone_fmt, state.user.MobilePhone ?: "-")
|
||||
binding.btnLoginOrLogout.setText(R.string.mine_logout)
|
||||
binding.btnChangePassword.isEnabled = true
|
||||
is SessionState.Restoring -> {
|
||||
binding.tvAvatar.text = ""
|
||||
binding.tvName.setText(R.string.mine_restoring)
|
||||
binding.tvDept.text = ""
|
||||
binding.tvAccount.text = ""
|
||||
binding.tvPhone.text = ""
|
||||
binding.btnLoginCta.isVisible = false
|
||||
binding.btnLogout.isVisible = false
|
||||
binding.rowChangePassword.isEnabled = false
|
||||
binding.cardUser.isClickable = false
|
||||
}
|
||||
is SessionState.SignedOut -> {
|
||||
binding.cardUser.isVisible = true
|
||||
binding.tvAvatar.setText(R.string.mine_avatar_guest)
|
||||
binding.tvName.setText(R.string.mine_not_logged_in)
|
||||
binding.tvDept.text = getString(R.string.mine_dept_fmt, "-")
|
||||
binding.tvAccount.text = getString(R.string.mine_account_fmt, "-")
|
||||
binding.tvPhone.text = getString(R.string.mine_phone_fmt, "-")
|
||||
binding.btnLoginOrLogout.setText(R.string.mine_login)
|
||||
binding.btnChangePassword.isEnabled = false
|
||||
binding.tvDept.setText(R.string.mine_not_logged_hint)
|
||||
binding.tvAccount.text = ""
|
||||
binding.tvPhone.text = ""
|
||||
binding.btnLoginCta.isVisible = true
|
||||
binding.btnLogout.isVisible = false
|
||||
binding.rowChangePassword.isEnabled = false
|
||||
binding.cardUser.isClickable = true
|
||||
}
|
||||
is SessionState.SignedIn -> {
|
||||
binding.tvAvatar.text =
|
||||
state.user.Name?.trim()?.firstOrNull()?.toString() ?: "?"
|
||||
binding.tvName.text = state.user.Name ?: getString(R.string.mine_unknown_user)
|
||||
binding.tvDept.text = getString(R.string.mine_dept_fmt, state.user.DeptName ?: "-")
|
||||
binding.tvAccount.text =
|
||||
getString(R.string.mine_account_fmt, state.user.LoginName ?: "-")
|
||||
binding.tvPhone.text =
|
||||
getString(R.string.mine_phone_fmt, state.user.MobilePhone ?: "-")
|
||||
binding.btnLoginCta.isVisible = false
|
||||
binding.btnLogout.isVisible = true
|
||||
binding.rowChangePassword.isEnabled = true
|
||||
binding.cardUser.isClickable = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderConfig(snapshot: com.stec.cmd.config.ConfigRepository.Snapshot) {
|
||||
binding.tvConfigStatus.setText(
|
||||
if (snapshot.isReady) R.string.mine_config_ready else R.string.mine_config_missing,
|
||||
)
|
||||
binding.btnGoConfig.isVisible = !snapshot.isReady
|
||||
private fun renderConfig(snapshot: ConfigRepository.Snapshot) {
|
||||
binding.cardConfigWarning.isVisible = !snapshot.isReady
|
||||
binding.tvConfigSubtitle.text = snapshot.baseUrl
|
||||
.ifBlank { getString(R.string.mine_row_config_unset) }
|
||||
}
|
||||
|
||||
private fun onPrimaryAction() {
|
||||
when (viewModel.sessionState.value) {
|
||||
is SessionState.SignedIn -> viewModel.logout()
|
||||
is SessionState.SignedOut -> openOnTop(LoginFragment(), TAG_LOGIN)
|
||||
/** 身份卡复用为登录入口(仅未登录态可点)。 */
|
||||
private fun onUserCardClick() {
|
||||
if (viewModel.sessionState.value is SessionState.SignedOut) {
|
||||
openOnTop(LoginFragment(), TAG_LOGIN)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* 「我的」页(M1-05/06/07):登录态 + 连接配置状态双流渲染。
|
||||
* 会话恢复已由 SessionRepository 冷启动自动化,无手动刷新入口。
|
||||
*/
|
||||
@HiltViewModel
|
||||
class MineViewModel @Inject constructor(
|
||||
@@ -20,14 +21,9 @@ class MineViewModel @Inject constructor(
|
||||
/** 会话状态流。 */
|
||||
val sessionState = sessionRepository.state
|
||||
|
||||
/** 连接配置快照流(未配置时引导配置页)。 */
|
||||
/** 连接配置快照流(未配置时警示引导配置页)。 */
|
||||
val configSnapshot = configRepository.snapshotStateFlow
|
||||
|
||||
/** 手动刷新:按存量 Token 重新拉取用户信息。 */
|
||||
fun refresh() {
|
||||
viewModelScope.launch { sessionRepository.restore() }
|
||||
}
|
||||
|
||||
/** M1-07 退出登录。 */
|
||||
fun logout() {
|
||||
viewModelScope.launch { sessionRepository.logout() }
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.stec.cmd.session
|
||||
|
||||
import com.stec.cmd.core.network.ApiCaller
|
||||
import com.stec.cmd.core.network.ApiError
|
||||
import com.stec.cmd.core.network.api.ProjectApi
|
||||
import com.stec.cmd.core.network.api.ProjectItem
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* 项目仓储(M2-01,2.5.1.1):拉取当前用户有权限的「进行中」项目列表。
|
||||
*
|
||||
* 契约宽容:data 形态以 [projectListFromData] 归一提取——
|
||||
* 兼容文档口径的 `{"ProjectList":[…]}` 对象、平铺数组、以及二次 JSON 编码字符串
|
||||
* (与 SessionRepository 对登录/用户信息数据的宽容模式一致,任务 ca3394be)。
|
||||
*/
|
||||
@Singleton
|
||||
class ProjectRepository @Inject constructor(
|
||||
private val projectApi: ProjectApi,
|
||||
private val apiCaller: ApiCaller,
|
||||
private val json: Json,
|
||||
) {
|
||||
|
||||
/**
|
||||
* 当前用户项目列表;[projectId] 传值时仅返回该项目的单条列表数据(2.5.1.1 接口1)。
|
||||
* 任何失败路径以 [ApiError] 子类抛出。
|
||||
*/
|
||||
suspend fun projects(projectId: String? = null): List<ProjectItem> =
|
||||
projectListFromData(apiCaller.call { projectApi.getProjectList(projectId) })
|
||||
|
||||
/**
|
||||
* data → 项目列表归一提取:
|
||||
* - JsonObject:优先取 `ProjectList` 键(文档口径),无该键则按整对象直接解码;
|
||||
* - JsonArray:文档口径的平铺数组;
|
||||
* - JsonPrimitive:二次 JSON 编码的列表字符串。
|
||||
* 结构无法识别或字段不符时以 [ApiError.EmptyBodyError] 抛出。
|
||||
*/
|
||||
private fun projectListFromData(data: JsonElement): List<ProjectItem> {
|
||||
val payload = (data as? JsonObject)?.get(KEY_PROJECT_LIST) ?: data
|
||||
val serializer = ListSerializer(ProjectItem.serializer())
|
||||
return try {
|
||||
when (payload) {
|
||||
is JsonArray -> json.decodeFromJsonElement(serializer, payload)
|
||||
is JsonPrimitive -> json.decodeFromString(serializer, payload.content)
|
||||
else -> throw ApiError.EmptyBodyError("项目列表数据结构异常")
|
||||
}
|
||||
} catch (e: SerializationException) {
|
||||
throw ApiError.EmptyBodyError("项目列表解析失败:${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
/** 文档 2.5.1.1:data 内列表键名(确切大小写)。 */
|
||||
const val KEY_PROJECT_LIST = "ProjectList"
|
||||
}
|
||||
}
|
||||
51
app/src/main/kotlin/com/stec/cmd/session/ProjectSession.kt
Normal file
51
app/src/main/kotlin/com/stec/cmd/session/ProjectSession.kt
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.stec.cmd.session
|
||||
|
||||
import com.stec.cmd.core.network.api.ProjectItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* 项目选中态会话(M2-03):跨页面共享的「当前项目」单例流。
|
||||
*
|
||||
* - 首页(M2)写入选中态;M3 任务 / M5 采集 / M6 统计直接订阅
|
||||
* [selectedProject] 即获得「切换后任务/统计跟随刷新」;
|
||||
* - 构造器订阅 [SessionRepository.state],登出(SignedOut)自动清空,
|
||||
* M1 登出代码零改动;Restoring/SignedIn 不动选中态。
|
||||
*/
|
||||
@Singleton
|
||||
class ProjectSession @Inject constructor(
|
||||
sessionRepository: SessionRepository,
|
||||
) {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val _selectedProject = MutableStateFlow<ProjectItem?>(null)
|
||||
|
||||
/** 当前选中项目;未选择或已登出时为 null。 */
|
||||
val selectedProject: StateFlow<ProjectItem?> = _selectedProject.asStateFlow()
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
sessionRepository.state.collect { state ->
|
||||
if (state is SessionState.SignedOut) clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 选中项目(M2-03 项目切换)。 */
|
||||
fun select(project: ProjectItem) {
|
||||
_selectedProject.value = project
|
||||
}
|
||||
|
||||
/** 清空选中态(登出联动,勿在业务页直接调用)。 */
|
||||
fun clear() {
|
||||
_selectedProject.value = null
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,9 @@ import javax.inject.Singleton
|
||||
/** 会话状态:UI 只读。 */
|
||||
sealed class SessionState {
|
||||
|
||||
/** 冷启动恢复中(restore 未落定),UI 显示中性加载,勿引导登录。 */
|
||||
data object Restoring : SessionState()
|
||||
|
||||
/** 未登录(含 Token 失效被清)。 */
|
||||
data object SignedOut : SessionState()
|
||||
|
||||
@@ -53,7 +56,7 @@ class SessionRepository @Inject constructor(
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
private val _state = MutableStateFlow<SessionState>(SessionState.SignedOut)
|
||||
private val _state = MutableStateFlow<SessionState>(SessionState.Restoring)
|
||||
|
||||
/** 登录态流:Mine 等页面订阅渲染。 */
|
||||
val state: StateFlow<SessionState> = _state.asStateFlow()
|
||||
|
||||
6
app/src/main/res/drawable/bg_avatar_circle.xml
Normal file
6
app/src/main/res/drawable/bg_avatar_circle.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 「我的」身份卡圆形头像底(姓名首字叠于其上) -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/brand_primary" />
|
||||
</shape>
|
||||
11
app/src/main/res/drawable/ic_back.xml
Normal file
11
app/src/main/res/drawable/ic_back.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 返回箭头(Toolbar navigationIcon 共用;着色经 app:navigationIconTint 控制) -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z" />
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_chevron_right.xml
Normal file
11
app/src/main/res/drawable/ic_chevron_right.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 右箭头(设置列表行 / 切换项目入口) -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6z" />
|
||||
</vector>
|
||||
@@ -1,85 +1,101 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 修改登录密码页(M1-04,2.3.1):成功后本地登出,需用新密码重登 -->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:fillViewport="true">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/btn_back"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:navigationIconTint="?attr/colorOnSurface"
|
||||
app:title="@string/change_password_title"
|
||||
app:titleTextColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_back"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_back" />
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/change_password_title"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_old_password"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:hint="@string/change_password_old_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_new_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/change_password_new_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/change_password_old_hint">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_confirm_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/change_password_confirm_hint"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_old_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_submit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/change_password_submit" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/change_password_new_hint">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_new_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/change_password_confirm_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_confirm_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_submit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:text="@string/change_password_submit" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,93 +1,109 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 服务器与密钥配置页(M1-06):仅存本机 DataStore,不入代码库(M7-04) -->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:fillViewport="true">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/btn_back"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:navigationIconTint="?attr/colorOnSurface"
|
||||
app:title="@string/config_title"
|
||||
app:titleTextColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_back"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_back" />
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/config_title"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_base_url"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:hint="@string/config_base_url_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1" />
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_secret_key"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/config_secret_key_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/config_base_url_hint">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_system_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/config_system_code_hint"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_base_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="textUri"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_save"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/action_save" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/config_secret_key_hint">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_secret_key"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="14sp" />
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/config_system_code_hint">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:alpha="0.6"
|
||||
android:text="@string/config_note"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_system_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_save"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:text="@string/action_save" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:alpha="0.6"
|
||||
android:text="@string/config_note"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
297
app/src/main/res/layout/fragment_home.xml
Normal file
297
app/src/main/res/layout/fragment_home.xml
Normal file
@@ -0,0 +1,297 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 首页项目工作台(M2-01 项目列表 / M2-02 项目卡 / M2-03 切换入口) -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background">
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
android:id="@+id/swipe_refresh"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true"
|
||||
android:overScrollMode="never">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<!-- 连接配置警示卡(仅三要素缺失时显示) -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_config_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="?attr/colorErrorContainer"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_config_warning_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/home_config_missing_title"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_config_warning_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:alpha="0.8"
|
||||
android:text="@string/home_config_missing_hint"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:text="@string/mine_go_config"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_section_current"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:text="@string/home_section_current"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 当前项目卡(M2-02) -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_project"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_project_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_project_code"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:alpha="0.65"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_scale"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/chip_phase"
|
||||
style="@style/Widget.Material3.Chip.Assist"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_project_hint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:alpha="0.7"
|
||||
android:text="@string/home_no_project_selected_hint"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_switch_project"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/home_switch_project"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_chevron_right"
|
||||
app:iconGravity="textEnd"
|
||||
app:iconTint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_project_count"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:alpha="0.6"
|
||||
android:textSize="13sp"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- 未登录空态 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/state_signed_out"
|
||||
android:clickable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/home_signed_out_title"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:alpha="0.7"
|
||||
android:gravity="center"
|
||||
android:text="@string/home_signed_out_hint"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_go_login"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/home_go_login" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 错误态 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/state_error"
|
||||
android:clickable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_error_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_retry"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/home_retry" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 空列表态 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/state_empty"
|
||||
android:clickable="true"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="32dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/home_empty"
|
||||
android:alpha="0.7"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_empty_refresh"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/home_empty_refresh" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -1,160 +1,197 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 登录页(M1-01 账号密码 / M1-02 获取验证码 / M1-03 验证码登录) -->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<!-- 登录页(M1-01 账号密码 / M1-02 获取验证码 / M1-03 验证码登录)
|
||||
消费级动线:Toolbar 返回 + 品牌头部 + 分段切换 + 浮动标签输入 + 按钮下线性进度 -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/brand_background"
|
||||
android:fillViewport="true">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:navigationIcon="@drawable/ic_back"
|
||||
app:navigationIconTint="?attr/colorOnSurface"
|
||||
app:title="@string/login_title"
|
||||
app:titleTextColor="?attr/colorOnSurface" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_back"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_back" />
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fillViewport="true">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/login_title"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:alpha="0.7"
|
||||
android:text="@string/login_subtitle"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<RadioGroup
|
||||
android:id="@+id/radio_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_account"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:checked="true"
|
||||
android:text="@string/login_mode_account" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/rb_sms"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/login_mode_sms" />
|
||||
</RadioGroup>
|
||||
|
||||
<!-- 账号密码模式 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/group_account"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_login_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_name_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/login_password_hint"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 短信验证码模式 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/group_sms"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
android:padding="24dp">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_phone"
|
||||
<!-- 品牌头部 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/login_welcome"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="26sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:alpha="0.7"
|
||||
android:text="@string/login_subtitle"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<!-- 登录方式分段切换 -->
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:id="@+id/toggle_mode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_phone_hint"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="phone"
|
||||
android:maxLength="11"
|
||||
android:maxLines="1" />
|
||||
android:layout_marginTop="24dp"
|
||||
app:selectionRequired="true"
|
||||
app:singleSelection="true">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/et_sms_code"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_mode_account"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/login_sms_code_hint"
|
||||
android:inputType="number"
|
||||
android:maxLength="6"
|
||||
android:maxLines="1" />
|
||||
android:text="@string/login_mode_account" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_send_code"
|
||||
android:id="@+id/btn_mode_sms"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:text="@string/login_send_code" />
|
||||
android:layout_weight="1"
|
||||
android:text="@string/login_mode_sms" />
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<!-- 账号密码模式 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/group_account"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_name_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_login_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="text"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:hint="@string/login_password_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionDone"
|
||||
android:inputType="textPassword"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- 短信验证码模式 -->
|
||||
<LinearLayout
|
||||
android:id="@+id/group_sms"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/login_phone_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_phone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:imeOptions="actionNext"
|
||||
android:inputType="phone"
|
||||
android:maxLength="11"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:hint="@string/login_sms_code_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/et_sms_code"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="number"
|
||||
android:maxLength="6"
|
||||
android:maxLines="1" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_send_code"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:text="@string/login_send_code" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_login"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="28dp"
|
||||
android:text="@string/login_submit" />
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_login"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/login_submit" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="16dp"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:textColor="?attr/colorError"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 「我的」业务页(M1-05 用户信息 / M1-06 配置入口 / M1-04 改密入口 / M1-07 退出登录) -->
|
||||
<!-- 「我的」业务页(M1-05 用户信息 / M1-06 配置入口 / M1-04 改密入口 / M1-07 退出登录)
|
||||
消费级动线:横向身份卡 + 连接配置警示卡 + 设置列表分组卡 + 登录 CTA / 退出登录 -->
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
@@ -11,120 +12,278 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="24dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_back"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/action_back" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/mine_title"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
android:paddingBottom="24dp">
|
||||
|
||||
<!-- 横向身份卡:未登录时整卡即登录入口 -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_user"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="1dp">
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/mine_not_logged_in"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="18sp"
|
||||
android:id="@+id/tv_avatar"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:background="@drawable/bg_avatar_circle"
|
||||
android:gravity="center"
|
||||
android:textColor="@color/brand_on_primary"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_dept"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:alpha="0.8"
|
||||
android:textSize="14sp" />
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_account"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:alpha="0.8"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/mine_not_logged_in"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_phone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:alpha="0.8"
|
||||
android:textSize="14sp" />
|
||||
<TextView
|
||||
android:id="@+id/tv_dept"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:alpha="0.75"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_account"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:alpha="0.6"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_phone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:alpha="0.6"
|
||||
android:textSize="13sp" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_config_status"
|
||||
<!-- 连接配置警示卡(仅三要素缺失时显示) -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_config_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:alpha="0.7"
|
||||
android:textSize="13sp" />
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:visibility="gone"
|
||||
app:cardBackgroundColor="?attr/colorErrorContainer"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_go_config"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/mine_config_missing"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:alpha="0.8"
|
||||
android:text="@string/home_config_missing_hint"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:text="@string/mine_go_config"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold" />
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 设置列表分组卡 -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/mine_go_config"
|
||||
android:layout_marginHorizontal="24dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:alpha="0.6"
|
||||
android:text="@string/mine_settings_section"
|
||||
android:textSize="13sp" />
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
app:cardBackgroundColor="@color/brand_surface"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:cardElevation="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/row_change_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="14dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/mine_change_password"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="1dp"
|
||||
android:alpha="0.6"
|
||||
android:text="@string/mine_row_change_password_subtitle"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/mine_change_password"
|
||||
android:src="@drawable/ic_chevron_right"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:alpha="0.15"
|
||||
android:background="?attr/colorOnSurface" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/row_open_config"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="14dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/mine_open_config"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="15sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_config_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="1dp"
|
||||
android:alpha="0.6"
|
||||
android:text="@string/mine_row_config_unset"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/mine_open_config"
|
||||
android:src="@drawable/ic_chevron_right"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- 主 CTA:未登录显示立即登录;已登录显示红字退出登录 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_login_cta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/mine_login_now"
|
||||
android:visibility="gone" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_login_or_logout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:text="@string/mine_login" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_change_password"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/mine_change_password" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_open_config"
|
||||
style="?attr/materialButtonOutlinedStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/mine_open_config" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_refresh"
|
||||
android:id="@+id/btn_logout"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/mine_refresh" />
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/mine_logout"
|
||||
android:textColor="?attr/colorError"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
44
app/src/main/res/layout/item_project_sheet.xml
Normal file
44
app/src/main/res/layout/item_project_sheet.xml
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 项目切换单选行(M2-03):当前选中项打勾,行整体可点 -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingVertical="14dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="15sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_meta"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:alpha="0.6"
|
||||
android:textSize="12sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/radio_selected"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:clickable="false"
|
||||
android:focusable="false" />
|
||||
</LinearLayout>
|
||||
25
app/src/main/res/layout/sheet_project_switcher.xml
Normal file
25
app/src/main/res/layout/sheet_project_switcher.xml
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 项目切换底部弹层(M2-03) -->
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="20dp"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:text="@string/sheet_switch_title"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_projects"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:overScrollMode="never" />
|
||||
</LinearLayout>
|
||||
@@ -10,29 +10,48 @@
|
||||
|
||||
<string name="placeholder_hint">占位页 —— 业务功能按 S1–S4 计划经 FeatureMount 挂载</string>
|
||||
|
||||
<!-- M2 首页项目工作台(M2-01/02/03) -->
|
||||
<string name="home_section_current">当前项目</string>
|
||||
<string name="home_unnamed_project">未命名项目</string>
|
||||
<string name="home_no_project_selected_hint">尚未选择项目,点击「切换项目」选择当前工作的项目</string>
|
||||
<string name="home_switch_project">切换项目</string>
|
||||
<string name="home_project_count_fmt">共 %1$d 个进行中项目</string>
|
||||
<string name="home_signed_out_title">登录后查看您的项目</string>
|
||||
<string name="home_signed_out_hint">登录后即可同步当前账号参与的项目与任务</string>
|
||||
<string name="home_go_login">立即登录</string>
|
||||
<string name="home_config_missing_title">尚未配置服务器与密钥</string>
|
||||
<string name="home_config_missing_hint">配置后才能加载项目数据</string>
|
||||
<string name="home_retry">重试</string>
|
||||
<string name="home_empty">当前账号暂无进行中的项目</string>
|
||||
<string name="home_empty_refresh">刷新</string>
|
||||
<string name="sheet_switch_title">切换项目</string>
|
||||
|
||||
<!-- 通用 -->
|
||||
<string name="action_back">返回</string>
|
||||
<string name="action_save">保存</string>
|
||||
|
||||
<!-- M1 我的(M1-05/06/07) -->
|
||||
<!-- M1 我的(M1-04/05/06/07) -->
|
||||
<string name="mine_title">我的</string>
|
||||
<string name="mine_restoring">正在恢复会话…</string>
|
||||
<string name="mine_avatar_guest">未</string>
|
||||
<string name="mine_not_logged_in">未登录</string>
|
||||
<string name="mine_not_logged_hint">登录后同步项目与任务</string>
|
||||
<string name="mine_unknown_user">未知用户</string>
|
||||
<string name="mine_dept_fmt">部门:%1$s</string>
|
||||
<string name="mine_account_fmt">账号:%1$s</string>
|
||||
<string name="mine_phone_fmt">手机号:%1$s</string>
|
||||
<string name="mine_login">登录</string>
|
||||
<string name="mine_login_now">立即登录</string>
|
||||
<string name="mine_logout">退出登录</string>
|
||||
<string name="mine_settings_section">设置</string>
|
||||
<string name="mine_change_password">修改登录密码</string>
|
||||
<string name="mine_row_change_password_subtitle">定期修改,保护账号安全</string>
|
||||
<string name="mine_open_config">服务器与密钥配置</string>
|
||||
<string name="mine_refresh">刷新用户信息</string>
|
||||
<string name="mine_config_ready">服务器与密钥已配置</string>
|
||||
<string name="mine_row_config_unset">未配置</string>
|
||||
<string name="mine_config_missing">尚未配置服务器地址与密钥,无法登录</string>
|
||||
<string name="mine_go_config">去配置</string>
|
||||
<string name="mine_logout_confirm">确定退出当前账号?</string>
|
||||
|
||||
<!-- M1 登录(M1-01/02/03) -->
|
||||
<string name="login_title">登录</string>
|
||||
<string name="login_welcome">欢迎回来</string>
|
||||
<string name="login_subtitle">上海城建勘测院 · 监测数据不落地平台</string>
|
||||
<string name="login_mode_account">账号密码</string>
|
||||
<string name="login_mode_sms">短信验证码</string>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.stec.cmd.core.network
|
||||
|
||||
import com.stec.cmd.core.network.api.AuthApi
|
||||
import com.stec.cmd.core.network.api.ProjectApi
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -72,6 +73,11 @@ object NetworkModule {
|
||||
@Singleton
|
||||
fun provideAuthApi(retrofit: Retrofit): AuthApi = retrofit.create(AuthApi::class.java)
|
||||
|
||||
/** 项目接口(M2,2.5.1.1)。 */
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideProjectApi(retrofit: Retrofit): ProjectApi = retrofit.create(ProjectApi::class.java)
|
||||
|
||||
/** 仅满足 Retrofit 构造约束;真实地址请求期经 DynamicBaseUrlInterceptor 改写。 */
|
||||
private const val PLACEHOLDER_BASE_URL = "https://placeholder.stec.invalid/"
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.stec.cmd.core.network.api
|
||||
|
||||
import com.stec.cmd.core.network.ApiEnvelope
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Query
|
||||
|
||||
/**
|
||||
* 项目接口(接口管理规范 V4.5 · 测试环境 https://jcd.stec.p-q.co):
|
||||
* - 2.5.1.1 获取当前用户项目列表(M2-01/02/03)
|
||||
*
|
||||
* 三要素请求头(SecretKey/SystemCode/Token)由 AuthInterceptor 统一注入。
|
||||
* 服务端仅返回当前用户有权限的、项目阶段为「进行中」的项目。
|
||||
*
|
||||
* 契约宽容:文档约定 data 为 {"ProjectList":[…]};与登录接口(任务 ca3394be)
|
||||
* 同理保留形态偏差可能,故以 [JsonElement] 宽容承载,
|
||||
* 由 app 层 ProjectRepository.projectListFromData 归一提取。
|
||||
*/
|
||||
interface ProjectApi {
|
||||
|
||||
/**
|
||||
* 2.5.1.1 当前用户项目列表:[projectId] 传 null 时 Retrofit 省略参数,
|
||||
* 返回全部有权限项目;传值时仅返回该 ID 的单条列表数据。
|
||||
*/
|
||||
@GET(PATH_GET_PROJECT_LIST)
|
||||
suspend fun getProjectList(
|
||||
@Query(QUERY_PROJECT_ID) projectId: String? = null,
|
||||
): ApiEnvelope<JsonElement>
|
||||
|
||||
companion object {
|
||||
// ---- 接口管理规范 V4.5 · 章节 2.1 所有 API 目录 ----
|
||||
const val PATH_GET_PROJECT_LIST = "OutWebApi/api/GetProjectList"
|
||||
|
||||
/** 2.5.1.1 查询参数名(非必填,空则返回全部)。 */
|
||||
const val QUERY_PROJECT_ID = "ProjectID"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.stec.cmd.core.network.api
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* 项目 DTO(接口管理规范 V4.5 · 2.5.1.1 / 2.5.1.6)。
|
||||
*
|
||||
* 字段名与平台契约一致(PascalCase);文档未给类型,全 `String?` 容错,
|
||||
* 响应侧配合 Json.ignoreUnknownKeys 容忍字段演进。
|
||||
*/
|
||||
@Serializable
|
||||
data class ProjectItem(
|
||||
/** 项目 ID。 */
|
||||
val ID: String? = null,
|
||||
/** 项目编号。 */
|
||||
val ProjectCode: String? = null,
|
||||
/** 项目简称。 */
|
||||
val SimpleName: String? = null,
|
||||
/** 项目等级:总院重点 | 部门重点 | 一般项目。 */
|
||||
val ProjectScale: String? = null,
|
||||
/** 项目阶段:未开始 | 进行中 | 已结束(2.5.1.1 服务端仅返回「进行中」)。 */
|
||||
val ProjectPhase: String? = null,
|
||||
)
|
||||
@@ -31,6 +31,8 @@ activity = "1.9.3"
|
||||
fragment = "1.8.5"
|
||||
navigation = "2.8.4"
|
||||
material = "1.12.0"
|
||||
recyclerview = "1.3.2"
|
||||
swiperefreshlayout = "1.1.0"
|
||||
splashscreen = "1.0.1"
|
||||
|
||||
# 测试
|
||||
@@ -67,6 +69,10 @@ androidx-security-crypto = { group = "androidx.security", name = "security-crypt
|
||||
# Material
|
||||
google-material = { group = "com.google.android.material", name = "material", version.ref = "material" }
|
||||
|
||||
# 列表与下拉刷新(M2 首页项目切换弹层 / 下拉刷新)
|
||||
androidx-recyclerview = { group = "androidx.recyclerview", name = "recyclerview", version.ref = "recyclerview" }
|
||||
androidx-swiperefreshlayout = { group = "androidx.swiperefreshlayout", name = "swiperefreshlayout", version.ref = "swiperefreshlayout" }
|
||||
|
||||
# Hilt
|
||||
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
|
||||
hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" }
|
||||
|
||||
Reference in New Issue
Block a user