移植完成,网络通信有问题
This commit is contained in:
+19
-4
@@ -8,7 +8,6 @@ plugins {
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
}
|
||||
|
||||
// 自动版本号管理
|
||||
val versionPropsFile = file("version.properties")
|
||||
fun getVersionCode(): Int {
|
||||
val props = Properties()
|
||||
@@ -45,7 +44,6 @@ android {
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// 作者信息
|
||||
buildConfigField("String", "AUTHOR", "\"无闻风\"")
|
||||
buildConfigField("String", "BUILD_DATE", "\"${SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date())}\"")
|
||||
}
|
||||
@@ -90,13 +88,30 @@ dependencies {
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.compose.material3)
|
||||
implementation(libs.androidx.compose.material.icons.extended)
|
||||
implementation(libs.androidx.compose.ui)
|
||||
implementation(libs.androidx.compose.ui.graphics)
|
||||
implementation(libs.androidx.compose.ui.tooling.preview)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
implementation(libs.androidx.datastore.preferences)
|
||||
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.retrofit.converter.gson)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.okhttp.logging.interceptor)
|
||||
implementation(libs.gson)
|
||||
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
implementation(libs.camerax.camera2)
|
||||
implementation(libs.camerax.lifecycle)
|
||||
implementation(libs.camerax.view)
|
||||
implementation(libs.mlkit.barcode.scanning)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
// 打印SDK
|
||||
implementation(files("libs/lcprintsdk-release.aar"))
|
||||
|
||||
testImplementation(libs.junit)
|
||||
@@ -106,4 +121,4 @@ dependencies {
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
debugImplementation(libs.androidx.compose.ui.test.manifest)
|
||||
debugImplementation(libs.androidx.compose.ui.tooling)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,19 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.FLASHLIGHT" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
|
||||
<application
|
||||
android:name=".OpsApplication"
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
@@ -10,7 +22,9 @@
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Ops_android">
|
||||
android:theme="@style/Theme.Ops_android"
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -19,10 +33,9 @@
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
</manifest>
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.example.ops_android
|
||||
|
||||
import android.content.Context
|
||||
import com.example.ops_android.data.cache.UserCache
|
||||
import com.example.ops_android.data.local.SessionManager
|
||||
import com.example.ops_android.data.remote.ApiServiceFactory
|
||||
import com.example.ops_android.data.remote.api.*
|
||||
import com.example.ops_android.data.repository.*
|
||||
import com.example.ops_android.printer.PrinterManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class AppContainer(context: Context) {
|
||||
val sessionManager = SessionManager(context)
|
||||
private val apiServiceFactory = ApiServiceFactory(sessionManager)
|
||||
|
||||
val userApi by lazy { apiServiceFactory.create<UserApi>() }
|
||||
val usersApi by lazy { apiServiceFactory.create<UsersApi>() }
|
||||
val purchaseApi by lazy { apiServiceFactory.create<PurchaseApi>() }
|
||||
val workOrderApi by lazy { apiServiceFactory.create<WorkOrderApi>() }
|
||||
val warehouseApi by lazy { apiServiceFactory.create<WarehouseApi>() }
|
||||
val customerApi by lazy { apiServiceFactory.create<CustomerApi>() }
|
||||
val scheduleApi by lazy { apiServiceFactory.create<ScheduleApi>() }
|
||||
|
||||
val authRepository by lazy { AuthRepository(userApi, sessionManager) }
|
||||
val orderRepository by lazy { OrderRepository(purchaseApi) }
|
||||
val workOrderRepository by lazy { WorkOrderRepository(workOrderApi) }
|
||||
val warehouseRepository by lazy { WarehouseRepository(warehouseApi) }
|
||||
val customerRepository by lazy { CustomerRepository(customerApi) }
|
||||
val scheduleRepository by lazy { ScheduleRepository(scheduleApi) }
|
||||
val userCache by lazy { UserCache(usersApi) }
|
||||
|
||||
var printerManager: PrinterManager? = null
|
||||
|
||||
init {
|
||||
runBlocking { sessionManager.init() }
|
||||
}
|
||||
}
|
||||
@@ -4,32 +4,33 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.example.ops_android.printer.PrinterManager
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import com.example.ops_android.ui.navigation.OPSNavGraph
|
||||
import com.example.ops_android.ui.theme.Ops_androidTheme
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var printerManager: PrinterManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
printerManager = PrinterManager(this)
|
||||
|
||||
val appContainer = (application as OpsApplication).appContainer
|
||||
val printerManager = PrinterManager(this)
|
||||
appContainer.printerManager = printerManager
|
||||
val isLoggedIn = runBlocking { appContainer.sessionManager.isLoggedIn() }
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
Ops_androidTheme {
|
||||
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
|
||||
PrinterTestScreen(
|
||||
printerManager = printerManager,
|
||||
modifier = Modifier.padding(innerPadding)
|
||||
)
|
||||
CompositionLocalProvider(LocalAppContainer provides appContainer) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
OPSNavGraph(isLoggedIn = isLoggedIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,412 +38,11 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
printerManager.bindService()
|
||||
(application as OpsApplication).appContainer.printerManager?.bindService()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
printerManager.unbindService()
|
||||
(application as OpsApplication).appContainer.printerManager?.unbindService()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PrinterTestScreen(
|
||||
printerManager: PrinterManager,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
var statusMessage by remember { mutableStateOf("未连接打印服务") }
|
||||
var isConnected by remember { mutableStateOf(false) }
|
||||
|
||||
DisposableEffect(printerManager) {
|
||||
printerManager.setConnectionCallback { connected, message ->
|
||||
isConnected = connected
|
||||
statusMessage = message
|
||||
}
|
||||
onDispose { }
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "PDA 打印机测试",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) - ${BuildConfig.AUTHOR}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
// ── 状态卡片 ──────────────────────────────────────
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (isConnected)
|
||||
MaterialTheme.colorScheme.primaryContainer
|
||||
else
|
||||
MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = statusMessage,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// 连接控制
|
||||
// ══════════════════════════════════════════════════
|
||||
Text("连接控制", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = { printerManager.bindService() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("绑定服务") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.initDevice().fold(
|
||||
onSuccess = { statusMessage = "打印机初始化成功" },
|
||||
onFailure = { statusMessage = "初始化失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("初始化") }
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.closeDevice().fold(
|
||||
onSuccess = { statusMessage = "打印机已关闭" },
|
||||
onFailure = { statusMessage = "关闭失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("关闭打印机") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.unbindService()
|
||||
isConnected = false
|
||||
statusMessage = "服务已解绑"
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("解绑服务") }
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// 快速打印
|
||||
// ══════════════════════════════════════════════════
|
||||
Text("快速打印", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.quickText("Hello Printer!\n测试打印\n").fold(
|
||||
onSuccess = { statusMessage = "快速打印成功" },
|
||||
onFailure = { statusMessage = "打印失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("快速打印测试") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.buildJob()
|
||||
.text("工单号: WO-001\n操作员: 张三\n")
|
||||
.build()
|
||||
.fold(
|
||||
onSuccess = { statusMessage = "PrintJob 打印成功" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("PrintJob 构建器测试") }
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// 单步打印(手动组合)
|
||||
// ══════════════════════════════════════════════════
|
||||
Text("单步打印", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.ensureOpen().fold(
|
||||
onSuccess = {
|
||||
printerManager.addText("手动 text\n").fold(
|
||||
onSuccess = { statusMessage = "文字已加入缓冲区" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("添加文字") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.ensureOpen().fold(
|
||||
onSuccess = {
|
||||
printerManager.addQR("https://example.com").fold(
|
||||
onSuccess = { statusMessage = "QR 已加入缓冲区" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("添加QR") }
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.ensureOpen().fold(
|
||||
onSuccess = {
|
||||
printerManager.addBarcode("1234567890").fold(
|
||||
onSuccess = { statusMessage = "条码已加入缓冲区" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("添加条码") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.ensureOpen().fold(
|
||||
onSuccess = {
|
||||
printerManager.addLineFeed(3).fold(
|
||||
onSuccess = { statusMessage = "空行已加入缓冲区" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("添加空行") }
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.ensureOpen().fold(
|
||||
onSuccess = {
|
||||
printerManager.commit().fold(
|
||||
onSuccess = { statusMessage = "已提交打印" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("提交打印 (start)") }
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// 样式设置
|
||||
// ══════════════════════════════════════════════════
|
||||
Text("样式设置", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.ensureOpen().fold(
|
||||
onSuccess = { statusMessage = "设备已打开,可设置样式" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
printerManager.setFontSize(32).fold(
|
||||
onSuccess = { statusMessage = "大字体已设置" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("大字体") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.ensureOpen().fold(
|
||||
onSuccess = { statusMessage = "设备已打开" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
printerManager.setBold(true).fold(
|
||||
onSuccess = { statusMessage = "粗体已设置" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("粗体") }
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.setUnderline(true).fold(
|
||||
onSuccess = { statusMessage = "下划线已设置" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("下划线") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.setDensity(5).fold(
|
||||
onSuccess = { statusMessage = "浓度已设置" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("浓度") }
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// 标签打印
|
||||
// ══════════════════════════════════════════════════
|
||||
Text("标签打印(黑标对齐)", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.quickLabel("标签打印测试\n工单号: WO-001\n").fold(
|
||||
onSuccess = { statusMessage = "标签打印成功" },
|
||||
onFailure = { statusMessage = "标签打印失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("快速标签打印") }
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.enableMarkDetection(true).fold(
|
||||
onSuccess = { statusMessage = "已开启黑标检测" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("开启黑标") }
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.enableMarkDetection(false).fold(
|
||||
onSuccess = { statusMessage = "已关闭黑标检测" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f)
|
||||
) { Text("关闭黑标") }
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
printerManager.ensureOpen().fold(
|
||||
onSuccess = {
|
||||
printerManager.enableMarkDetection(true).fold(
|
||||
onSuccess = {
|
||||
printerManager.gotoNextMark().fold(
|
||||
onSuccess = { statusMessage = "已移动到下一标签" },
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
onFailure = { statusMessage = "失败: ${it.message}" }
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { Text("下一标签") }
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// ══════════════════════════════════════════════════
|
||||
// 综合测试
|
||||
// ══════════════════════════════════════════════════
|
||||
Text("综合测试", style = MaterialTheme.typography.titleMedium)
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
runComprehensiveTest(printerManager) { msg -> statusMessage = msg }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) { Text("运行完整测试") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun runComprehensiveTest(
|
||||
printerManager: PrinterManager,
|
||||
onStatus: (String) -> Unit
|
||||
) {
|
||||
onStatus("开始综合测试...")
|
||||
|
||||
printerManager.initDevice().onFailure {
|
||||
onStatus("初始化失败: ${it.message}")
|
||||
return
|
||||
}
|
||||
|
||||
printerManager.setFontSize(32)
|
||||
printerManager.setBold(true)
|
||||
printerManager.addText("=== 打印测试 ===\n")
|
||||
|
||||
printerManager.setFontSize(24)
|
||||
printerManager.setBold(false)
|
||||
printerManager.addText("普通文本测试\n")
|
||||
|
||||
printerManager.setBold(true)
|
||||
printerManager.addText("粗体文本测试\n")
|
||||
printerManager.setBold(false)
|
||||
|
||||
printerManager.setUnderline(true)
|
||||
printerManager.addText("下划线文本测试\n")
|
||||
printerManager.setUnderline(false)
|
||||
|
||||
printerManager.addLineFeed(1)
|
||||
|
||||
printerManager.addText("二维码测试:\n")
|
||||
printerManager.addQR("https://example.com")
|
||||
printerManager.addLineFeed(1)
|
||||
|
||||
printerManager.addText("条形码测试:\n")
|
||||
printerManager.addBarcode("123456789012")
|
||||
printerManager.addLineFeed(3)
|
||||
|
||||
printerManager.addText("--------------------------------\n")
|
||||
printerManager.addText("测试完成\n")
|
||||
printerManager.addLineFeed(2)
|
||||
|
||||
printerManager.commit().fold(
|
||||
onSuccess = {
|
||||
onStatus("综合测试完成,开始打印")
|
||||
Thread.sleep(1500)
|
||||
printerManager.closeDevice()
|
||||
},
|
||||
onFailure = { onStatus("提交打印失败: ${it.message}") }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.example.ops_android
|
||||
|
||||
import android.app.Application
|
||||
|
||||
class OpsApplication : Application() {
|
||||
lateinit var appContainer: AppContainer
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
appContainer = AppContainer(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.example.ops_android.data.cache
|
||||
|
||||
import com.example.ops_android.data.model.UserDetail
|
||||
import com.example.ops_android.data.remote.api.UsersApi
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
class UserCache(
|
||||
private val usersApi: UsersApi
|
||||
) {
|
||||
private val cache = ConcurrentHashMap<Int, UserDetail>()
|
||||
private val inflightRequests = ConcurrentHashMap<Int, CompletableDeferred<UserDetail?>>()
|
||||
|
||||
suspend fun fetchUserInfo(userID: Int): UserDetail? {
|
||||
if (userID == 0) return null
|
||||
|
||||
cache[userID]?.let { return it }
|
||||
|
||||
inflightRequests[userID]?.let { return it.await() }
|
||||
|
||||
val deferred = CompletableDeferred<UserDetail?>()
|
||||
inflightRequests[userID] = deferred
|
||||
|
||||
try {
|
||||
val response = usersApi.getUserInfoFromUserID(userID)
|
||||
val info = if (response.errCode == 0 && response.data?.userinfo != null) {
|
||||
response.data.userinfo.also { cache[userID] = it }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
deferred.complete(info)
|
||||
return info
|
||||
} catch (e: Exception) {
|
||||
deferred.complete(null)
|
||||
return null
|
||||
} finally {
|
||||
inflightRequests.remove(userID)
|
||||
}
|
||||
}
|
||||
|
||||
fun getUsername(userID: Int): String {
|
||||
if (userID == 0) return ""
|
||||
val user = cache[userID]
|
||||
return user?.Username ?: "用户$userID"
|
||||
}
|
||||
|
||||
fun getCachedUser(userID: Int): UserDetail? = cache[userID]
|
||||
|
||||
fun clear() {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.example.ops_android.data.local
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.example.ops_android.data.model.Cookie
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "ops_settings")
|
||||
|
||||
class SessionManager(private val context: Context) {
|
||||
|
||||
companion object {
|
||||
private val KEY_API_BASE_URL = stringPreferencesKey("api_base_url")
|
||||
private val KEY_COOKIE_VALUE = stringPreferencesKey("cookie_value")
|
||||
private val KEY_COOKIE_NAME = stringPreferencesKey("cookie_name")
|
||||
private val KEY_COOKIE_EXPIRES_AT = stringPreferencesKey("cookie_expires_at")
|
||||
private val KEY_COOKIE_REMEMBER = booleanPreferencesKey("cookie_remember")
|
||||
}
|
||||
|
||||
@Volatile
|
||||
var cachedBaseUrl: String = ""
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
var cachedCookieValue: String = ""
|
||||
private set
|
||||
|
||||
fun clearCookieCache() {
|
||||
cachedCookieValue = ""
|
||||
}
|
||||
|
||||
val apiBaseUrl: Flow<String> = context.dataStore.data.map { prefs -> prefs[KEY_API_BASE_URL] ?: "" }
|
||||
val cookieValue: Flow<String> = context.dataStore.data.map { prefs -> prefs[KEY_COOKIE_VALUE] ?: "" }
|
||||
|
||||
suspend fun init() {
|
||||
cachedBaseUrl = context.dataStore.data.first()[KEY_API_BASE_URL] ?: ""
|
||||
val value = context.dataStore.data.first()[KEY_COOKIE_VALUE]
|
||||
cachedCookieValue = value ?: ""
|
||||
}
|
||||
|
||||
suspend fun getApiBaseUrl(): String = cachedBaseUrl.ifEmpty {
|
||||
context.dataStore.data.first()[KEY_API_BASE_URL]?.also { cachedBaseUrl = it } ?: ""
|
||||
}
|
||||
|
||||
suspend fun setApiBaseUrl(url: String) {
|
||||
cachedBaseUrl = url
|
||||
context.dataStore.edit { it[KEY_API_BASE_URL] = url }
|
||||
}
|
||||
|
||||
suspend fun getCookieValue(): String = cachedCookieValue
|
||||
|
||||
suspend fun saveCookie(cookie: Cookie) {
|
||||
cachedCookieValue = cookie.value ?: ""
|
||||
context.dataStore.edit {
|
||||
it[KEY_COOKIE_VALUE] = cookie.value ?: ""
|
||||
it[KEY_COOKIE_NAME] = cookie.name ?: ""
|
||||
it[KEY_COOKIE_EXPIRES_AT] = cookie.expiresAt ?: ""
|
||||
it[KEY_COOKIE_REMEMBER] = cookie.remember
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun clearCookie() {
|
||||
cachedCookieValue = ""
|
||||
context.dataStore.edit {
|
||||
it.remove(KEY_COOKIE_VALUE)
|
||||
it.remove(KEY_COOKIE_NAME)
|
||||
it.remove(KEY_COOKIE_EXPIRES_AT)
|
||||
it.remove(KEY_COOKIE_REMEMBER)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun isCookieExpired(): Boolean {
|
||||
val expiresAt = context.dataStore.data.first()[KEY_COOKIE_EXPIRES_AT] ?: return true
|
||||
return try {
|
||||
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US).parse(expiresAt)?.before(Date()) ?: true
|
||||
} catch (e: Exception) { true }
|
||||
}
|
||||
|
||||
suspend fun isLoggedIn(): Boolean {
|
||||
if (cachedCookieValue.isEmpty()) return false
|
||||
return !isCookieExpired()
|
||||
}
|
||||
|
||||
suspend fun logout() = clearCookie()
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class Container(
|
||||
val id: Int = 0,
|
||||
val name: String? = null,
|
||||
val parent_id: Int = 0,
|
||||
val barcode: String? = null,
|
||||
val remark: String? = null,
|
||||
val created_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Cookie(
|
||||
@SerializedName("Name") val name: String?,
|
||||
@SerializedName("Value") val value: String?,
|
||||
@SerializedName("ExpiresAt") val expiresAt: String?,
|
||||
@SerializedName("Remember") val remember: Boolean = false
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class Customer(
|
||||
val id: Int = 0,
|
||||
val name: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val address: String? = null,
|
||||
val remark: String? = null,
|
||||
val created_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class Item(
|
||||
val id: Int = 0,
|
||||
val name: String? = null,
|
||||
val serial_number: String? = null,
|
||||
val spec: String? = null,
|
||||
val container_id: Int = 0,
|
||||
val barcode: String? = null,
|
||||
val remark: String? = null,
|
||||
val photos: List<String>? = null,
|
||||
val move_history: List<MoveHistory>? = null,
|
||||
val created_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
|
||||
data class MoveHistory(
|
||||
val from_container: String? = null,
|
||||
val to_container: String? = null,
|
||||
val moved_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class OrderCounts(
|
||||
val pending: Int = 0,
|
||||
val ordered: Int = 0,
|
||||
val arrived: Int = 0,
|
||||
val received: Int = 0,
|
||||
val lost: Int = 0,
|
||||
val returned: Int = 0,
|
||||
val total: Int = 0
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class PurchaseOrder(
|
||||
val id: Int = 0,
|
||||
val title: String? = null,
|
||||
val status: String? = null,
|
||||
val remark: String? = null,
|
||||
val link: String? = null,
|
||||
val style_tags: List<String>? = null,
|
||||
val costs: List<Cost>? = null,
|
||||
val photos: List<String>? = null,
|
||||
val work_order_ids: List<Int>? = null,
|
||||
val status_logs: List<StatusLog>? = null,
|
||||
val created_at: String? = null,
|
||||
val updated_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
|
||||
data class Cost(
|
||||
val amount: Double = 0.0,
|
||||
val currency: String = "CNY",
|
||||
val remark: String? = null
|
||||
)
|
||||
|
||||
data class StatusLog(
|
||||
val status: String? = null,
|
||||
val comment: String? = null,
|
||||
val photos: List<String>? = null,
|
||||
val created_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class ScheduleEvent(
|
||||
val id: Int = 0,
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
val start_time: String? = null,
|
||||
val end_time: String? = null,
|
||||
val color: String? = null,
|
||||
val created_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class Stats(
|
||||
val total_containers: Int = 0,
|
||||
val total_items: Int = 0
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class UserDetail(
|
||||
val Username: String? = null,
|
||||
val FirstName: String? = null,
|
||||
val AvatarPath: String? = null,
|
||||
val Role: String? = null
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class UserInfo(
|
||||
val ID: Int? = null,
|
||||
val Name: String? = null
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class WorkOrder(
|
||||
val id: Int = 0,
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
val status: String? = null,
|
||||
val item_ids: List<Int>? = null,
|
||||
val purchase_order_ids: List<Int>? = null,
|
||||
val customer_id: Int? = null,
|
||||
val commits: List<Commit>? = null,
|
||||
val created_at: String? = null,
|
||||
val updated_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
|
||||
data class Commit(
|
||||
val id: Int = 0,
|
||||
val status: String? = null,
|
||||
val comment: String? = null,
|
||||
val photos: List<String>? = null,
|
||||
val created_at: String? = null,
|
||||
val user_id: Int? = null
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.example.ops_android.data.model
|
||||
|
||||
data class WorkOrderCounts(
|
||||
val pending: Int = 0,
|
||||
val checked: Int = 0,
|
||||
val parts_ordered: Int = 0,
|
||||
val repaired: Int = 0,
|
||||
val returned: Int = 0,
|
||||
val unrepairable: Int = 0,
|
||||
val total: Int = 0
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.ops_android.data.remote
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class ApiResponse<T>(
|
||||
@SerializedName("return") val data: T? = null
|
||||
) {
|
||||
@SerializedName("err_code")
|
||||
private val _errCode: Any? = null
|
||||
|
||||
val errCode: Int
|
||||
get() = when (_errCode) {
|
||||
is Number -> (_errCode as Number).toInt()
|
||||
is String -> (_errCode as String).toIntOrNull() ?: -1
|
||||
else -> -1
|
||||
}
|
||||
|
||||
val isSuccess: Boolean get() = errCode == 0
|
||||
}
|
||||
|
||||
data class AuthRequest(
|
||||
@SerializedName("data") val data: Any?,
|
||||
@SerializedName("userCookieValue") val userCookieValue: String?
|
||||
)
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.example.ops_android.data.remote
|
||||
|
||||
import com.example.ops_android.data.local.SessionManager
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ApiServiceFactory(
|
||||
private val sessionManager: SessionManager
|
||||
) {
|
||||
val gson: Gson = GsonBuilder()
|
||||
.setLenient()
|
||||
.create()
|
||||
|
||||
fun <T> create(serviceClass: Class<T>): T {
|
||||
return Retrofit.Builder()
|
||||
.baseUrl("http://localhost/")
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build()
|
||||
.create(serviceClass)
|
||||
}
|
||||
|
||||
inline fun <reified T> create(): T = create(T::class.java)
|
||||
|
||||
private val okHttpClient: OkHttpClient by lazy {
|
||||
val loggingInterceptor = HttpLoggingInterceptor().apply {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
}
|
||||
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(BaseUrlInterceptor(sessionManager))
|
||||
.addInterceptor(CookieInterceptor(sessionManager))
|
||||
.addInterceptor(loggingInterceptor)
|
||||
.connectTimeout(30, TimeUnit.SECONDS)
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.writeTimeout(60, TimeUnit.SECONDS)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.example.ops_android.data.remote
|
||||
|
||||
import com.example.ops_android.data.local.SessionManager
|
||||
import com.google.gson.JsonParser
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
|
||||
class AuthInterceptor(
|
||||
private val sessionManager: SessionManager
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val response = chain.proceed(request)
|
||||
|
||||
if (!response.isSuccessful || request.method != "POST") {
|
||||
return response
|
||||
}
|
||||
|
||||
val contentType = response.header("Content-Type") ?: ""
|
||||
if (!contentType.contains("json")) {
|
||||
return response
|
||||
}
|
||||
|
||||
val body = response.body ?: return response
|
||||
val bodyString = body.string()
|
||||
|
||||
try {
|
||||
val errCode = JsonParser.parseString(bodyString).asJsonObject.get("err_code")?.asInt ?: 0
|
||||
if (errCode == -44) {
|
||||
sessionManager.clearCookieCache()
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
|
||||
val newBody = bodyString.toResponseBody("application/json".toMediaTypeOrNull())
|
||||
return response.newBuilder().body(newBody).build()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.example.ops_android.data.remote
|
||||
|
||||
import com.example.ops_android.data.local.SessionManager
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
class BaseUrlInterceptor(
|
||||
private val sessionManager: SessionManager
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
var request = chain.request()
|
||||
val baseUrl = sessionManager.cachedBaseUrl
|
||||
|
||||
if (baseUrl.isNotEmpty()) {
|
||||
val newUrl = request.url.toString().replace("http://localhost", baseUrl)
|
||||
request = request.newBuilder().url(newUrl).build()
|
||||
}
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.example.ops_android.data.remote
|
||||
|
||||
import com.example.ops_android.data.local.SessionManager
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
|
||||
class CookieInterceptor(
|
||||
private val sessionManager: SessionManager
|
||||
) : Interceptor {
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
if (originalRequest.method != "POST") {
|
||||
return chain.proceed(originalRequest)
|
||||
}
|
||||
|
||||
val cookieValue = sessionManager.cachedCookieValue
|
||||
|
||||
val gson = Gson()
|
||||
|
||||
val oldMap = if (originalRequest.body != null) {
|
||||
val buffer = okio.Buffer()
|
||||
originalRequest.body!!.writeTo(buffer)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
gson.fromJson(buffer.readUtf8(), Map::class.java) as? Map<String, Any> ?: emptyMap()
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val newBody = mapOf(
|
||||
"data" to (oldMap as Any),
|
||||
"userCookieValue" to cookieValue
|
||||
)
|
||||
|
||||
val newBodyJson = gson.toJson(newBody)
|
||||
val contentType = originalRequest.body?.contentType()
|
||||
val newRequestBody = newBodyJson.toRequestBody(contentType)
|
||||
|
||||
return chain.proceed(
|
||||
originalRequest.newBuilder()
|
||||
.method(originalRequest.method, newRequestBody)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.example.ops_android.data.remote.api
|
||||
|
||||
import com.example.ops_android.data.model.Customer
|
||||
import com.example.ops_android.data.remote.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
data class CustomerListResponse(
|
||||
val customers: List<Customer>?
|
||||
)
|
||||
|
||||
data class CustomerGetRequest(
|
||||
val id: Int
|
||||
)
|
||||
|
||||
data class CustomerResponse(
|
||||
val customer: Customer?
|
||||
)
|
||||
|
||||
interface CustomerApi {
|
||||
|
||||
@POST("/customer/list")
|
||||
suspend fun list(@Body request: Map<String, Any> = emptyMap()): ApiResponse<CustomerListResponse>
|
||||
|
||||
@POST("/customer/get")
|
||||
suspend fun get(@Body request: CustomerGetRequest): ApiResponse<CustomerResponse>
|
||||
|
||||
@POST("/customer/add")
|
||||
suspend fun add(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/customer/update")
|
||||
suspend fun update(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/customer/delete")
|
||||
suspend fun delete(@Body request: Map<String, Int>): ApiResponse<Any>
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.example.ops_android.data.remote.api
|
||||
|
||||
import com.example.ops_android.data.model.OrderCounts
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.data.remote.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
data class GetOrdersRequest(
|
||||
val status: String? = null,
|
||||
val search: String? = null,
|
||||
val offset: Int = 0,
|
||||
val limit: Int = 20
|
||||
)
|
||||
|
||||
data class OrdersListResponse(
|
||||
val orders: List<PurchaseOrder>?,
|
||||
val total: Int = 0
|
||||
)
|
||||
|
||||
data class GetOrderRequest(
|
||||
val id: Int
|
||||
)
|
||||
|
||||
data class UpdateOrderStatusRequest(
|
||||
val id: Int,
|
||||
val status: String,
|
||||
val comment: String? = null,
|
||||
val photos: List<String>? = null
|
||||
)
|
||||
|
||||
data class OrderResponse(
|
||||
val order: PurchaseOrder?
|
||||
)
|
||||
|
||||
interface PurchaseApi {
|
||||
|
||||
@POST("/purchase/getorders")
|
||||
suspend fun getOrders(@Body request: GetOrdersRequest): ApiResponse<OrdersListResponse>
|
||||
|
||||
@POST("/purchase/getorder")
|
||||
suspend fun getOrder(@Body request: GetOrderRequest): ApiResponse<OrderResponse>
|
||||
|
||||
@POST("/purchase/getordercount")
|
||||
suspend fun getOrderCount(@Body request: Map<String, Any> = emptyMap()): ApiResponse<OrderCounts>
|
||||
|
||||
@POST("/purchase/updatestatus")
|
||||
suspend fun updateOrderStatus(@Body request: UpdateOrderStatusRequest): ApiResponse<Any>
|
||||
|
||||
@POST("/purchase/addorder")
|
||||
suspend fun addOrder(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/purchase/updateorder")
|
||||
suspend fun updateOrder(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.example.ops_android.data.remote.api
|
||||
|
||||
import com.example.ops_android.data.model.ScheduleEvent
|
||||
import com.example.ops_android.data.remote.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
data class GetEventsRequest(
|
||||
val start_date: String? = null,
|
||||
val end_date: String? = null
|
||||
)
|
||||
|
||||
data class EventsResponse(
|
||||
val events: List<ScheduleEvent>?
|
||||
)
|
||||
|
||||
interface ScheduleApi {
|
||||
|
||||
@POST("/schedule/getevents")
|
||||
suspend fun getEvents(@Body request: GetEventsRequest): ApiResponse<EventsResponse>
|
||||
|
||||
@POST("/schedule/addevent")
|
||||
suspend fun addEvent(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/schedule/editevent")
|
||||
suspend fun editEvent(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/schedule/deleevent")
|
||||
suspend fun deleteEvent(@Body request: Map<String, Int>): ApiResponse<Any>
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.ops_android.data.remote.api
|
||||
|
||||
import com.example.ops_android.data.model.Cookie
|
||||
import com.example.ops_android.data.model.UserDetail
|
||||
import com.example.ops_android.data.model.UserInfo
|
||||
import com.example.ops_android.data.remote.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String,
|
||||
val remember: Boolean = true
|
||||
)
|
||||
|
||||
data class LoginResponse(
|
||||
val cookie: Cookie?
|
||||
)
|
||||
|
||||
data class RegisterRequest(
|
||||
val username: String,
|
||||
val useremail: String,
|
||||
val userpass: String
|
||||
)
|
||||
|
||||
data class ChangePasswordRequest(
|
||||
val oldpass: String,
|
||||
val newpass: String
|
||||
)
|
||||
|
||||
data class ChangeEmailRequest(
|
||||
val newemail: String
|
||||
)
|
||||
|
||||
data class UpdateInfoRequest(
|
||||
val username: String? = null,
|
||||
val remark: String? = null,
|
||||
val birthday: String? = null
|
||||
)
|
||||
|
||||
data class UserInfoResponse(
|
||||
val user: UserInfo?,
|
||||
val userInfo: UserDetail?
|
||||
)
|
||||
|
||||
interface UserApi {
|
||||
|
||||
@POST("/users/login")
|
||||
suspend fun login(@Body request: LoginRequest): ApiResponse<LoginResponse>
|
||||
|
||||
@POST("/users/register")
|
||||
suspend fun register(@Body request: RegisterRequest): ApiResponse<Any>
|
||||
|
||||
@POST("/users/getinfo")
|
||||
suspend fun getUserInfo(@Body request: Map<String, Any> = emptyMap()): ApiResponse<UserInfoResponse>
|
||||
|
||||
@POST("/users/changePassword")
|
||||
suspend fun changePassword(@Body request: ChangePasswordRequest): ApiResponse<Any>
|
||||
|
||||
@POST("/users/changeEmail")
|
||||
suspend fun changeEmail(@Body request: ChangeEmailRequest): ApiResponse<Any>
|
||||
|
||||
@POST("/users/updateInfo")
|
||||
suspend fun updateInfo(@Body request: UpdateInfoRequest): ApiResponse<Any>
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.example.ops_android.data.remote.api
|
||||
|
||||
import com.example.ops_android.data.model.UserDetail
|
||||
import com.example.ops_android.data.remote.ApiResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
|
||||
data class UserInfoByIdResponse(
|
||||
val userinfo: UserDetail?
|
||||
)
|
||||
|
||||
interface UsersApi {
|
||||
|
||||
@GET("/users/getuserinfo/{userID}")
|
||||
suspend fun getUserInfoFromUserID(@Path("userID") userID: Int): ApiResponse<UserInfoByIdResponse>
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.example.ops_android.data.remote.api
|
||||
|
||||
import com.example.ops_android.data.model.Container
|
||||
import com.example.ops_android.data.model.Item
|
||||
import com.example.ops_android.data.model.Stats
|
||||
import com.example.ops_android.data.remote.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
data class ListContainerRequest(
|
||||
val parent_id: Int = 0
|
||||
)
|
||||
|
||||
data class ListContainerResponse(
|
||||
val containers: List<Container>?
|
||||
)
|
||||
|
||||
data class GetContainerRequest(
|
||||
val id: Int
|
||||
)
|
||||
|
||||
data class ContainerResponse(
|
||||
val container: Container?
|
||||
)
|
||||
|
||||
data class ListItemRequest(
|
||||
val container_id: Int = 0
|
||||
)
|
||||
|
||||
data class ListItemResponse(
|
||||
val items: List<Item>?
|
||||
)
|
||||
|
||||
data class GetItemRequest(
|
||||
val id: Int
|
||||
)
|
||||
|
||||
data class ItemResponse(
|
||||
val item: Item?
|
||||
)
|
||||
|
||||
data class MoveItemRequest(
|
||||
val item_id: Int,
|
||||
val new_container: Int
|
||||
)
|
||||
|
||||
interface WarehouseApi {
|
||||
|
||||
@POST("/warehouse/list_container")
|
||||
suspend fun listContainer(@Body request: ListContainerRequest): ApiResponse<ListContainerResponse>
|
||||
|
||||
@POST("/warehouse/get_container")
|
||||
suspend fun getContainer(@Body request: GetContainerRequest): ApiResponse<ContainerResponse>
|
||||
|
||||
@POST("/warehouse/add_container")
|
||||
suspend fun addContainer(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/warehouse/update_container")
|
||||
suspend fun updateContainer(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/warehouse/delete_container")
|
||||
suspend fun deleteContainer(@Body request: Map<String, Int>): ApiResponse<Any>
|
||||
|
||||
@POST("/warehouse/list_item")
|
||||
suspend fun listItem(@Body request: ListItemRequest): ApiResponse<ListItemResponse>
|
||||
|
||||
@POST("/warehouse/get_item")
|
||||
suspend fun getItem(@Body request: GetItemRequest): ApiResponse<ItemResponse>
|
||||
|
||||
@POST("/warehouse/add_item")
|
||||
suspend fun addItem(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/warehouse/update_item")
|
||||
suspend fun updateItem(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/warehouse/delete_item")
|
||||
suspend fun deleteItem(@Body request: Map<String, Int>): ApiResponse<Any>
|
||||
|
||||
@POST("/warehouse/move_item")
|
||||
suspend fun moveItem(@Body request: MoveItemRequest): ApiResponse<Any>
|
||||
|
||||
@POST("/warehouse/get_stats")
|
||||
suspend fun getStats(@Body request: Map<String, Any> = emptyMap()): ApiResponse<Stats>
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.example.ops_android.data.remote.api
|
||||
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.data.model.WorkOrder
|
||||
import com.example.ops_android.data.model.WorkOrderCounts
|
||||
import com.example.ops_android.data.remote.ApiResponse
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
data class WorkOrderListRequest(
|
||||
val status: String? = null,
|
||||
val search: String? = null,
|
||||
val offset: Int = 0,
|
||||
val limit: Int = 20
|
||||
)
|
||||
|
||||
data class WorkOrderListResponse(
|
||||
val work_orders: List<WorkOrder>?,
|
||||
val total: Int = 0
|
||||
)
|
||||
|
||||
data class WorkOrderGetRequest(
|
||||
val id: Int
|
||||
)
|
||||
|
||||
data class WorkOrderResponse(
|
||||
val work_order: WorkOrder?
|
||||
)
|
||||
|
||||
data class WorkOrderCommitRequest(
|
||||
val work_order_id: Int,
|
||||
val status: String? = null,
|
||||
val comment: String? = null,
|
||||
val photos: List<String>? = null
|
||||
)
|
||||
|
||||
data class SearchPurchaseOrdersRequest(
|
||||
val search: String,
|
||||
val limit: Int = 10
|
||||
)
|
||||
|
||||
data class SearchPurchaseOrdersResponse(
|
||||
val purchase_orders: List<PurchaseOrder>?
|
||||
)
|
||||
|
||||
interface WorkOrderApi {
|
||||
|
||||
@POST("/work_order/list")
|
||||
suspend fun list(@Body request: WorkOrderListRequest): ApiResponse<WorkOrderListResponse>
|
||||
|
||||
@POST("/work_order/get")
|
||||
suspend fun get(@Body request: WorkOrderGetRequest): ApiResponse<WorkOrderResponse>
|
||||
|
||||
@POST("/work_order/count")
|
||||
suspend fun getCount(@Body request: Map<String, Any> = emptyMap()): ApiResponse<WorkOrderCounts>
|
||||
|
||||
@POST("/work_order/add")
|
||||
suspend fun add(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/work_order/update")
|
||||
suspend fun update(@Body request: Map<String, Any>): ApiResponse<Any>
|
||||
|
||||
@POST("/work_order/delete")
|
||||
suspend fun delete(@Body request: Map<String, Int>): ApiResponse<Any>
|
||||
|
||||
@POST("/work_order/commit")
|
||||
suspend fun commit(@Body request: WorkOrderCommitRequest): ApiResponse<Any>
|
||||
|
||||
@POST("/work_order/search_purchase_orders")
|
||||
suspend fun searchPurchaseOrders(@Body request: SearchPurchaseOrdersRequest): ApiResponse<SearchPurchaseOrdersResponse>
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.example.ops_android.data.repository
|
||||
|
||||
import com.example.ops_android.data.local.SessionManager
|
||||
import com.example.ops_android.data.model.Cookie
|
||||
import com.example.ops_android.data.model.UserDetail
|
||||
import com.example.ops_android.data.remote.api.*
|
||||
|
||||
class AuthRepository(
|
||||
private val userApi: UserApi,
|
||||
private val sessionManager: SessionManager
|
||||
) {
|
||||
suspend fun login(username: String, password: String, remember: Boolean = true): Result<Cookie> {
|
||||
return try {
|
||||
val response = userApi.login(LoginRequest(username, password, remember))
|
||||
if (response.errCode == 0 && response.data?.cookie != null) {
|
||||
val cookie = response.data.cookie
|
||||
sessionManager.saveCookie(cookie)
|
||||
Result.success(cookie)
|
||||
} else {
|
||||
Result.failure(Exception("登录失败 (err=${response.errCode}, cookie=${response.data?.cookie})"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun register(username: String, email: String, password: String): Result<Unit> {
|
||||
return try {
|
||||
val response = userApi.register(RegisterRequest(username, email, password))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("注册失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUserInfo(): Result<Pair<com.example.ops_android.data.model.UserInfo?, UserDetail?>> {
|
||||
return try {
|
||||
val response = userApi.getUserInfo()
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(Pair(response.data.user, response.data.userInfo))
|
||||
} else {
|
||||
Result.failure(Exception("获取用户信息失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun changePassword(oldPass: String, newPass: String): Result<Unit> {
|
||||
return try {
|
||||
val response = userApi.changePassword(ChangePasswordRequest(oldPass, newPass))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("修改密码失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun changeEmail(newEmail: String): Result<Unit> {
|
||||
return try {
|
||||
val response = userApi.changeEmail(ChangeEmailRequest(newEmail))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("修改邮箱失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateInfo(data: UpdateInfoRequest): Result<Unit> {
|
||||
return try {
|
||||
val response = userApi.updateInfo(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("更新信息失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout() {
|
||||
sessionManager.logout()
|
||||
}
|
||||
|
||||
suspend fun isLoggedIn(): Boolean = sessionManager.isLoggedIn()
|
||||
|
||||
suspend fun restoreSession(): Boolean {
|
||||
if (!sessionManager.isLoggedIn()) return false
|
||||
val result = getUserInfo()
|
||||
return result.isSuccess
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.ops_android.data.repository
|
||||
|
||||
import com.example.ops_android.data.model.Customer
|
||||
import com.example.ops_android.data.remote.api.CustomerApi
|
||||
import com.example.ops_android.data.remote.api.CustomerGetRequest
|
||||
|
||||
class CustomerRepository(
|
||||
private val customerApi: CustomerApi
|
||||
) {
|
||||
suspend fun list(): Result<List<Customer>> {
|
||||
return try {
|
||||
val response = customerApi.list()
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(response.data.customers ?: emptyList())
|
||||
} else {
|
||||
Result.failure(Exception("获取客户列表失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun get(id: Int): Result<Customer> {
|
||||
return try {
|
||||
val response = customerApi.get(CustomerGetRequest(id))
|
||||
if (response.errCode == 0 && response.data?.customer != null) {
|
||||
Result.success(response.data.customer)
|
||||
} else {
|
||||
Result.failure(Exception("获取客户详情失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun add(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = customerApi.add(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("创建客户失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun update(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = customerApi.update(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("更新客户失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(id: Int): Result<Unit> {
|
||||
return try {
|
||||
val response = customerApi.delete(mapOf("id" to id))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("删除客户失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.example.ops_android.data.repository
|
||||
|
||||
import com.example.ops_android.data.remote.api.PurchaseApi
|
||||
import com.example.ops_android.data.remote.api.GetOrdersRequest
|
||||
import com.example.ops_android.data.remote.api.GetOrderRequest
|
||||
import com.example.ops_android.data.remote.api.UpdateOrderStatusRequest
|
||||
import com.example.ops_android.data.model.OrderCounts
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
|
||||
class OrderRepository(
|
||||
private val purchaseApi: PurchaseApi
|
||||
) {
|
||||
suspend fun getOrders(status: String? = null, search: String? = null, offset: Int = 0, limit: Int = 20): Result<Pair<List<PurchaseOrder>, Int>> {
|
||||
return try {
|
||||
val response = purchaseApi.getOrders(GetOrdersRequest(status, search, offset, limit))
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(Pair(response.data.orders ?: emptyList(), response.data.total))
|
||||
} else {
|
||||
Result.failure(Exception("获取订单列表失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getOrder(id: Int): Result<PurchaseOrder> {
|
||||
return try {
|
||||
val response = purchaseApi.getOrder(GetOrderRequest(id))
|
||||
if (response.errCode == 0 && response.data?.order != null) {
|
||||
Result.success(response.data.order)
|
||||
} else {
|
||||
Result.failure(Exception("获取订单详情失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getOrderCount(): Result<OrderCounts> {
|
||||
return try {
|
||||
val response = purchaseApi.getOrderCount()
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(response.data)
|
||||
} else {
|
||||
Result.failure(Exception("获取订单统计失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateOrderStatus(id: Int, status: String, comment: String? = null, photos: List<String>? = null): Result<Unit> {
|
||||
return try {
|
||||
val response = purchaseApi.updateOrderStatus(UpdateOrderStatusRequest(id, status, comment, photos))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("更新订单状态失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addOrder(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = purchaseApi.addOrder(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("创建订单失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateOrder(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = purchaseApi.updateOrder(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("更新订单失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.example.ops_android.data.repository
|
||||
|
||||
import com.example.ops_android.data.model.ScheduleEvent
|
||||
import com.example.ops_android.data.remote.api.ScheduleApi
|
||||
import com.example.ops_android.data.remote.api.GetEventsRequest
|
||||
|
||||
class ScheduleRepository(
|
||||
private val scheduleApi: ScheduleApi
|
||||
) {
|
||||
suspend fun getEvents(startDate: String? = null, endDate: String? = null): Result<List<ScheduleEvent>> {
|
||||
return try {
|
||||
val response = scheduleApi.getEvents(GetEventsRequest(startDate, endDate))
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(response.data.events ?: emptyList())
|
||||
} else {
|
||||
Result.failure(Exception("获取日程失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addEvent(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = scheduleApi.addEvent(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("创建日程失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun editEvent(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = scheduleApi.editEvent(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("编辑日程失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteEvent(id: Int): Result<Unit> {
|
||||
return try {
|
||||
val response = scheduleApi.deleteEvent(mapOf("id" to id))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("删除日程失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.example.ops_android.data.repository
|
||||
|
||||
import com.example.ops_android.data.model.Container
|
||||
import com.example.ops_android.data.model.Item
|
||||
import com.example.ops_android.data.model.Stats
|
||||
import com.example.ops_android.data.remote.api.WarehouseApi
|
||||
import com.example.ops_android.data.remote.api.ListContainerRequest
|
||||
import com.example.ops_android.data.remote.api.GetContainerRequest
|
||||
import com.example.ops_android.data.remote.api.ListItemRequest
|
||||
import com.example.ops_android.data.remote.api.GetItemRequest
|
||||
import com.example.ops_android.data.remote.api.MoveItemRequest
|
||||
|
||||
class WarehouseRepository(
|
||||
private val warehouseApi: WarehouseApi
|
||||
) {
|
||||
suspend fun listContainer(parentId: Int = 0): Result<List<Container>> {
|
||||
return try {
|
||||
val response = warehouseApi.listContainer(ListContainerRequest(parentId))
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(response.data.containers ?: emptyList())
|
||||
} else {
|
||||
Result.failure(Exception("获取容器列表失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContainer(id: Int): Result<Container> {
|
||||
return try {
|
||||
val response = warehouseApi.getContainer(GetContainerRequest(id))
|
||||
if (response.errCode == 0 && response.data?.container != null) {
|
||||
Result.success(response.data.container)
|
||||
} else {
|
||||
Result.failure(Exception("获取容器详情失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addContainer(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = warehouseApi.addContainer(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("创建容器失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateContainer(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = warehouseApi.updateContainer(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("更新容器失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteContainer(id: Int): Result<Unit> {
|
||||
return try {
|
||||
val response = warehouseApi.deleteContainer(mapOf("id" to id))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("删除容器失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun listItem(containerId: Int = 0): Result<List<Item>> {
|
||||
return try {
|
||||
val response = warehouseApi.listItem(ListItemRequest(containerId))
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(response.data.items ?: emptyList())
|
||||
} else {
|
||||
Result.failure(Exception("获取物品列表失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getItem(id: Int): Result<Item> {
|
||||
return try {
|
||||
val response = warehouseApi.getItem(GetItemRequest(id))
|
||||
if (response.errCode == 0 && response.data?.item != null) {
|
||||
Result.success(response.data.item)
|
||||
} else {
|
||||
Result.failure(Exception("获取物品详情失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun addItem(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = warehouseApi.addItem(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("创建物品失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateItem(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = warehouseApi.updateItem(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("更新物品失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteItem(id: Int): Result<Unit> {
|
||||
return try {
|
||||
val response = warehouseApi.deleteItem(mapOf("id" to id))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("删除物品失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun moveItem(itemId: Int, newContainer: Int): Result<Unit> {
|
||||
return try {
|
||||
val response = warehouseApi.moveItem(MoveItemRequest(itemId, newContainer))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("移动物品失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getStats(): Result<Stats> {
|
||||
return try {
|
||||
val response = warehouseApi.getStats()
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(response.data)
|
||||
} else {
|
||||
Result.failure(Exception("获取仓库统计失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.example.ops_android.data.repository
|
||||
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.data.model.WorkOrder
|
||||
import com.example.ops_android.data.model.WorkOrderCounts
|
||||
import com.example.ops_android.data.remote.api.WorkOrderApi
|
||||
import com.example.ops_android.data.remote.api.WorkOrderListRequest
|
||||
import com.example.ops_android.data.remote.api.WorkOrderGetRequest
|
||||
import com.example.ops_android.data.remote.api.WorkOrderCommitRequest
|
||||
import com.example.ops_android.data.remote.api.SearchPurchaseOrdersRequest
|
||||
|
||||
class WorkOrderRepository(
|
||||
private val workOrderApi: WorkOrderApi
|
||||
) {
|
||||
suspend fun list(status: String? = null, search: String? = null, offset: Int = 0, limit: Int = 20): Result<Pair<List<WorkOrder>, Int>> {
|
||||
return try {
|
||||
val response = workOrderApi.list(WorkOrderListRequest(status, search, offset, limit))
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(Pair(response.data.work_orders ?: emptyList(), response.data.total))
|
||||
} else {
|
||||
Result.failure(Exception("获取工单列表失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun get(id: Int): Result<WorkOrder> {
|
||||
return try {
|
||||
val response = workOrderApi.get(WorkOrderGetRequest(id))
|
||||
if (response.errCode == 0 && response.data?.work_order != null) {
|
||||
Result.success(response.data.work_order)
|
||||
} else {
|
||||
Result.failure(Exception("获取工单详情失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCount(): Result<WorkOrderCounts> {
|
||||
return try {
|
||||
val response = workOrderApi.getCount()
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(response.data)
|
||||
} else {
|
||||
Result.failure(Exception("获取工单统计失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun add(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = workOrderApi.add(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("创建工单失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun update(data: Map<String, Any>): Result<Unit> {
|
||||
return try {
|
||||
val response = workOrderApi.update(data)
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("更新工单失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun delete(id: Int): Result<Unit> {
|
||||
return try {
|
||||
val response = workOrderApi.delete(mapOf("id" to id))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("删除工单失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun commit(workOrderId: Int, status: String?, comment: String?, photos: List<String>?): Result<Unit> {
|
||||
return try {
|
||||
val response = workOrderApi.commit(WorkOrderCommitRequest(workOrderId, status, comment, photos))
|
||||
if (response.errCode == 0) Result.success(Unit)
|
||||
else Result.failure(Exception("提交流程失败"))
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun searchPurchaseOrders(query: String, limit: Int = 10): Result<List<PurchaseOrder>> {
|
||||
return try {
|
||||
val response = workOrderApi.searchPurchaseOrders(SearchPurchaseOrdersRequest(query, limit))
|
||||
if (response.errCode == 0 && response.data != null) {
|
||||
Result.success(response.data.purchase_orders ?: emptyList())
|
||||
} else {
|
||||
Result.failure(Exception("搜索采购订单失败"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.example.ops_android.printer
|
||||
|
||||
import com.example.ops_android.data.model.Item
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.data.model.WorkOrder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object PrintHelper {
|
||||
|
||||
suspend fun printOrderLabel(
|
||||
printerManager: PrinterManager,
|
||||
order: PurchaseOrder
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
printerManager.initDevice().onFailure { return@withContext Result.failure(it) }
|
||||
printerManager.setFontSize(24)
|
||||
printerManager.setBold(true)
|
||||
printerManager.addText("采购单: ${order.title ?: "-"}\n")
|
||||
printerManager.setFontSize(20)
|
||||
printerManager.setBold(false)
|
||||
if (order.id > 0) {
|
||||
printerManager.addBarcode("PO-${order.id}").fold(
|
||||
onSuccess = {},
|
||||
onFailure = {}
|
||||
)
|
||||
}
|
||||
printerManager.addLineFeed(2)
|
||||
printerManager.commit().fold(
|
||||
onSuccess = {
|
||||
Thread.sleep(1000)
|
||||
printerManager.closeDevice()
|
||||
Result.success(Unit)
|
||||
},
|
||||
onFailure = { Result.failure(it) }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun printWorkOrderLabel(
|
||||
printerManager: PrinterManager,
|
||||
workOrder: WorkOrder
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
printerManager.initDevice().onFailure { return@withContext Result.failure(it) }
|
||||
printerManager.setFontSize(24)
|
||||
printerManager.setBold(true)
|
||||
printerManager.addText("工单: ${workOrder.title ?: "-"}\n")
|
||||
printerManager.setFontSize(20)
|
||||
printerManager.setBold(false)
|
||||
if (workOrder.id > 0) {
|
||||
printerManager.addBarcode("WO-${workOrder.id}").fold(
|
||||
onSuccess = {},
|
||||
onFailure = {}
|
||||
)
|
||||
}
|
||||
printerManager.addLineFeed(2)
|
||||
printerManager.commit().fold(
|
||||
onSuccess = {
|
||||
Thread.sleep(1000)
|
||||
printerManager.closeDevice()
|
||||
Result.success(Unit)
|
||||
},
|
||||
onFailure = { Result.failure(it) }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun printItemLabel(
|
||||
printerManager: PrinterManager,
|
||||
item: Item
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
printerManager.initDevice().onFailure { return@withContext Result.failure(it) }
|
||||
printerManager.setFontSize(24)
|
||||
printerManager.setBold(true)
|
||||
printerManager.addText("${item.name ?: "-"}\n")
|
||||
printerManager.setFontSize(18)
|
||||
printerManager.setBold(false)
|
||||
item.serial_number?.let { printerManager.addText("编号: $it\n") }
|
||||
item.spec?.let { printerManager.addText("规格: $it\n") }
|
||||
if (item.id > 0) {
|
||||
printerManager.addBarcode("ITEM-${item.id}").fold(
|
||||
onSuccess = {},
|
||||
onFailure = {}
|
||||
)
|
||||
}
|
||||
printerManager.addLineFeed(2)
|
||||
printerManager.commit().fold(
|
||||
onSuccess = {
|
||||
Thread.sleep(1000)
|
||||
printerManager.closeDevice()
|
||||
Result.success(Unit)
|
||||
},
|
||||
onFailure = { Result.failure(it) }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun printContainerLabel(
|
||||
printerManager: PrinterManager,
|
||||
containerName: String,
|
||||
barcode: String? = null
|
||||
): Result<Unit> = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
printerManager.initDevice().onFailure { return@withContext Result.failure(it) }
|
||||
printerManager.setFontSize(24)
|
||||
printerManager.setBold(true)
|
||||
printerManager.addText("容器: $containerName\n")
|
||||
if (barcode != null) {
|
||||
printerManager.setFontSize(18)
|
||||
printerManager.setBold(false)
|
||||
printerManager.addBarcode(barcode).fold(
|
||||
onSuccess = {},
|
||||
onFailure = {}
|
||||
)
|
||||
}
|
||||
printerManager.addLineFeed(2)
|
||||
printerManager.commit().fold(
|
||||
onSuccess = {
|
||||
Thread.sleep(1000)
|
||||
printerManager.closeDevice()
|
||||
Result.success(Unit)
|
||||
},
|
||||
onFailure = { Result.failure(it) }
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.example.ops_android.ui
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import com.example.ops_android.AppContainer
|
||||
|
||||
val LocalAppContainer = compositionLocalOf<AppContainer> {
|
||||
error("AppContainer not provided")
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.example.ops_android.ui.auth
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Visibility
|
||||
import androidx.compose.material.icons.filled.VisibilityOff
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
|
||||
@Composable
|
||||
fun LoginScreen(
|
||||
onLoginSuccess: () -> Unit,
|
||||
onGoToSettings: () -> Unit,
|
||||
viewModel: LoginViewModel = viewModel(factory = LoginViewModel.Factory(LocalAppContainer.current.authRepository))
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var passwordVisible by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(uiState.loginSuccess) {
|
||||
if (uiState.loginSuccess) {
|
||||
onLoginSuccess()
|
||||
viewModel.resetLoginSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color(0xFF667EEA),
|
||||
Color(0xFF764BA2)
|
||||
)
|
||||
)
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = "OPS",
|
||||
fontSize = 48.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "运营管理系统",
|
||||
fontSize = 16.sp,
|
||||
color = Color.White.copy(alpha = 0.8f),
|
||||
modifier = Modifier.padding(bottom = 48.dp)
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 32.dp),
|
||||
shape = RoundedCornerShape(16.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = Color.White)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.username,
|
||||
onValueChange = viewModel::onUsernameChange,
|
||||
label = { Text("用户名") },
|
||||
leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color(0xFF667EEA),
|
||||
focusedLabelColor = Color(0xFF667EEA)
|
||||
)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.password,
|
||||
onValueChange = viewModel::onPasswordChange,
|
||||
label = { Text("密码") },
|
||||
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { passwordVisible = !passwordVisible }) {
|
||||
Icon(
|
||||
if (passwordVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Password,
|
||||
imeAction = ImeAction.Done
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = OutlinedTextFieldDefaults.colors(
|
||||
focusedBorderColor = Color(0xFF667EEA),
|
||||
focusedLabelColor = Color(0xFF667EEA)
|
||||
)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = uiState.rememberMe,
|
||||
onCheckedChange = viewModel::onRememberMeChange,
|
||||
colors = CheckboxDefaults.colors(
|
||||
checkedColor = Color(0xFF667EEA)
|
||||
)
|
||||
)
|
||||
Text(
|
||||
text = "记住登录",
|
||||
fontSize = 14.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
|
||||
uiState.errorMessage?.let { error ->
|
||||
Text(
|
||||
text = error,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.login() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(50.dp),
|
||||
enabled = !uiState.isLoading,
|
||||
shape = RoundedCornerShape(25.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = Color(0xFF667EEA)
|
||||
)
|
||||
) {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(24.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text("登 录", fontSize = 16.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(
|
||||
onClick = onGoToSettings,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(bottom = 32.dp)
|
||||
) {
|
||||
Text("设置服务器地址", color = Color.White.copy(alpha = 0.7f))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.example.ops_android.ui.auth
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.repository.AuthRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class LoginUiState(
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val rememberMe: Boolean = true,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val loginSuccess: Boolean = false,
|
||||
val needSettings: Boolean = false
|
||||
)
|
||||
|
||||
class LoginViewModel(
|
||||
private val authRepository: AuthRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(LoginUiState())
|
||||
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun onUsernameChange(value: String) {
|
||||
_uiState.value = _uiState.value.copy(username = value, errorMessage = null)
|
||||
}
|
||||
|
||||
fun onPasswordChange(value: String) {
|
||||
_uiState.value = _uiState.value.copy(password = value, errorMessage = null)
|
||||
}
|
||||
|
||||
fun onRememberMeChange(value: Boolean) {
|
||||
_uiState.value = _uiState.value.copy(rememberMe = value)
|
||||
}
|
||||
|
||||
fun login() {
|
||||
val state = _uiState.value
|
||||
if (state.username.isBlank() || state.password.isBlank()) {
|
||||
_uiState.value = state.copy(errorMessage = "请输入用户名和密码")
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
val result = authRepository.login(state.username, state.password, state.rememberMe)
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, loginSuccess = true)
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = e.message ?: "登录失败"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun checkNeedSettings(): Boolean {
|
||||
val result = kotlinx.coroutines.runBlocking {
|
||||
authRepository.isLoggedIn()
|
||||
}
|
||||
return !result
|
||||
}
|
||||
|
||||
fun resetLoginSuccess() {
|
||||
_uiState.value = _uiState.value.copy(loginSuccess = false)
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val authRepository: AuthRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return LoginViewModel(authRepository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.example.ops_android.ui.customer
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.model.Customer
|
||||
import com.example.ops_android.data.repository.CustomerRepository
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class CustomerListUiState(
|
||||
val customers: List<Customer> = emptyList(), val isLoading: Boolean = false, val errorMessage: String? = null,
|
||||
val showForm: Boolean = false, val editingCustomer: Customer? = null,
|
||||
val formName: String = "", val formPhone: String = "", val formEmail: String = "", val formAddress: String = "", val formRemark: String = "",
|
||||
val isSubmitting: Boolean = false
|
||||
)
|
||||
|
||||
class CustomerListViewModel(private val repo: CustomerRepository) : ViewModel() {
|
||||
private val _s = MutableStateFlow(CustomerListUiState()); val s: StateFlow<CustomerListUiState> = _s.asStateFlow()
|
||||
init { load() }
|
||||
private fun load() { viewModelScope.launch { _s.value = _s.value.copy(isLoading = true); repo.list().fold(onSuccess = { _s.value = _s.value.copy(customers = it, isLoading = false) }, onFailure = { _s.value = _s.value.copy(isLoading = false, errorMessage = it.message) }) } }
|
||||
|
||||
fun showAddForm() { _s.value = _s.value.copy(showForm = true, editingCustomer = null, formName = "", formPhone = "", formEmail = "", formAddress = "", formRemark = "") }
|
||||
fun showEditForm(c: Customer) { _s.value = _s.value.copy(showForm = true, editingCustomer = c, formName = c.name ?: "", formPhone = c.phone ?: "", formEmail = c.email ?: "", formAddress = c.address ?: "", formRemark = c.remark ?: "") }
|
||||
fun dismissForm() { _s.value = _s.value.copy(showForm = false) }
|
||||
fun onFormName(v: String) { _s.value = _s.value.copy(formName = v) }
|
||||
fun onFormPhone(v: String) { _s.value = _s.value.copy(formPhone = v) }
|
||||
fun onFormEmail(v: String) { _s.value = _s.value.copy(formEmail = v) }
|
||||
fun onFormAddress(v: String) { _s.value = _s.value.copy(formAddress = v) }
|
||||
fun onFormRemark(v: String) { _s.value = _s.value.copy(formRemark = v) }
|
||||
|
||||
fun submitForm() {
|
||||
val st = _s.value
|
||||
if (st.formName.isBlank()) { _s.value = st.copy(errorMessage = "名称不能为空"); return }
|
||||
val data = mapOf("name" to st.formName, "phone" to st.formPhone, "email" to st.formEmail, "address" to st.formAddress, "remark" to st.formRemark)
|
||||
viewModelScope.launch {
|
||||
_s.value = _s.value.copy(isSubmitting = true)
|
||||
val r = if (st.editingCustomer != null) repo.update(data + ("id" to st.editingCustomer.id)) else repo.add(data)
|
||||
r.fold(onSuccess = { _s.value = _s.value.copy(isSubmitting = false, showForm = false); load() }, onFailure = { _s.value = _s.value.copy(isSubmitting = false, errorMessage = it.message) })
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCustomer(id: Int) { viewModelScope.launch { repo.delete(id).fold(onSuccess = { load() }, onFailure = { _s.value = _s.value.copy(errorMessage = it.message) }) } }
|
||||
|
||||
class Factory(private val repo: CustomerRepository) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(c: Class<T>): T = CustomerListViewModel(repo) as T }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CustomerListScreen(onBack: () -> Unit) {
|
||||
val vm: CustomerListViewModel = viewModel(factory = CustomerListViewModel.Factory(LocalAppContainer.current.customerRepository))
|
||||
val s by vm.s.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopAppBar(title = { Text("客户管理") }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White)) },
|
||||
floatingActionButton = { FloatingActionButton(onClick = { vm.showAddForm() }, containerColor = Color(0xFF667EEA)) { Icon(Icons.Default.Add, "添加", tint = Color.White) } }
|
||||
) { padding ->
|
||||
if (s.isLoading) { Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) { CircularProgressIndicator() }; return@Scaffold }
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize().padding(padding), contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
items(s.customers, key = { it.id }) { c ->
|
||||
Card(Modifier.fillMaxWidth().clickable { vm.showEditForm(c) }) {
|
||||
Column(Modifier.padding(12.dp)) {
|
||||
Text(c.name ?: "未命名", fontWeight = FontWeight.Medium)
|
||||
c.phone?.takeIf { it.isNotBlank() }?.let { Text("电话: $it", fontSize = 13.sp, color = Color.Gray) }
|
||||
c.email?.takeIf { it.isNotBlank() }?.let { Text("邮箱: $it", fontSize = 13.sp, color = Color.Gray) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s.showForm) AlertDialog(
|
||||
onDismissRequest = { vm.dismissForm() },
|
||||
title = { Text(if (s.editingCustomer != null) "编辑客户" else "添加客户") },
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(s.formName, vm::onFormName, label = { Text("名称 *") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(s.formPhone, vm::onFormPhone, label = { Text("电话") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(s.formEmail, vm::onFormEmail, label = { Text("邮箱") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(s.formAddress, vm::onFormAddress, label = { Text("地址") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(s.formRemark, vm::onFormRemark, label = { Text("备注") }, minLines = 2, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (s.editingCustomer != null) {
|
||||
TextButton(onClick = { vm.deleteCustomer(s.editingCustomer!!.id) }) { Text("删除", color = Color.Red) }
|
||||
}
|
||||
Button(onClick = { vm.submitForm() }, enabled = !s.isSubmitting) { Text("保存") }
|
||||
}
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { vm.dismissForm() }) { Text("取消") } }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package com.example.ops_android.ui.home
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
onSearchClick: () -> Unit = {},
|
||||
onScheduleClick: () -> Unit = {},
|
||||
viewModel: HomeViewModel = viewModel(factory = HomeViewModel.Factory(
|
||||
LocalAppContainer.current.orderRepository,
|
||||
LocalAppContainer.current.workOrderRepository,
|
||||
LocalAppContainer.current.scheduleRepository
|
||||
))
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("OPS 运营管理") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFF667EEA),
|
||||
titleContentColor = Color.White
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
if (uiState.isLoading) {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiState.errorMessage?.let { error ->
|
||||
item {
|
||||
Card(colors = CardDefaults.cardColors(containerColor = Color(0xFFFFEBEE))) {
|
||||
Text(
|
||||
text = error,
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = Color(0xFFC62828)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 今日日程 ──────────────────────────
|
||||
item {
|
||||
Text("今日日程", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
if (uiState.todayEvents.isEmpty()) {
|
||||
item {
|
||||
Card(colors = CardDefaults.cardColors(containerColor = Color(0xFFF5F5F5))) {
|
||||
Text(
|
||||
"暂无日程安排",
|
||||
modifier = Modifier.padding(16.dp),
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(uiState.todayEvents) { event ->
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Default.Event, null, tint = Color(0xFF667EEA))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Column {
|
||||
Text(event.title ?: "", fontWeight = FontWeight.Medium)
|
||||
Text(
|
||||
"${event.start_time ?: ""} ~ ${event.end_time ?: ""}",
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 采购订单统计 ──────────────────────────
|
||||
item {
|
||||
Text("采购订单", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
uiState.orderCounts?.let { counts ->
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
StatCard("待处理", counts.pending, Color(0xFFFF9800), modifier = Modifier.weight(1f))
|
||||
StatCard("已订购", counts.ordered, Color(0xFF2196F3), modifier = Modifier.weight(1f))
|
||||
StatCard("已到货", counts.arrived, Color(0xFF4CAF50), modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
StatCard("已收货", counts.received, Color(0xFF4CAF50), modifier = Modifier.weight(1f))
|
||||
StatCard("丢失", counts.lost, Color(0xFFF44336), modifier = Modifier.weight(1f))
|
||||
StatCard("退货", counts.returned, Color(0xFF9C27B0), modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 工单统计 ──────────────────────────
|
||||
item {
|
||||
Text("工单", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
uiState.workOrderCounts?.let { counts ->
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
StatCard("待处理", counts.pending, Color(0xFFFF9800), modifier = Modifier.weight(1f))
|
||||
StatCard("已检查", counts.checked, Color(0xFF2196F3), modifier = Modifier.weight(1f))
|
||||
StatCard("待配件", counts.parts_ordered, Color(0xFF9C27B0), modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
StatCard("已修复", counts.repaired, Color(0xFF4CAF50), modifier = Modifier.weight(1f))
|
||||
StatCard("已返还", counts.returned, Color(0xFF4CAF50), modifier = Modifier.weight(1f))
|
||||
StatCard("不可修", counts.unrepairable, Color(0xFFF44336), modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 快捷入口 ──────────────────────────
|
||||
item {
|
||||
Text("快捷入口", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
QuickLinkCard(Icons.Default.Search, "搜索", Color(0xFF4CAF50), Modifier.weight(1f), onSearchClick)
|
||||
QuickLinkCard(Icons.Default.Event, "日程", Color(0xFF9C27B0), Modifier.weight(1f), onScheduleClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatCard(label: String, count: Int, color: Color, modifier: Modifier = Modifier) {
|
||||
Card(
|
||||
modifier = modifier,
|
||||
colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f)),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = "$count",
|
||||
fontSize = 24.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = color
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
fontSize = 12.sp,
|
||||
color = Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QuickLinkCard(icon: ImageVector, label: String, color: Color, modifier: Modifier = Modifier, onClick: () -> Unit = {}) {
|
||||
Card(
|
||||
modifier = modifier.clickable { onClick() },
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = color.copy(alpha = 0.1f))
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(icon, null, tint = color, modifier = Modifier.size(32.dp))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text(label, fontSize = 14.sp, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.example.ops_android.ui.home
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.model.OrderCounts
|
||||
import com.example.ops_android.data.model.ScheduleEvent
|
||||
import com.example.ops_android.data.model.WorkOrderCounts
|
||||
import com.example.ops_android.data.repository.OrderRepository
|
||||
import com.example.ops_android.data.repository.ScheduleRepository
|
||||
import com.example.ops_android.data.repository.WorkOrderRepository
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
data class HomeUiState(
|
||||
val todayEvents: List<ScheduleEvent> = emptyList(),
|
||||
val orderCounts: OrderCounts? = null,
|
||||
val workOrderCounts: WorkOrderCounts? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class HomeViewModel(
|
||||
private val orderRepository: OrderRepository,
|
||||
private val workOrderRepository: WorkOrderRepository,
|
||||
private val scheduleRepository: ScheduleRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(HomeUiState())
|
||||
val uiState: StateFlow<HomeUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var autoRefreshJob: Job? = null
|
||||
|
||||
init {
|
||||
loadData()
|
||||
startAutoRefresh()
|
||||
}
|
||||
|
||||
fun loadData() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)
|
||||
try {
|
||||
val today = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date())
|
||||
val eventsResult = scheduleRepository.getEvents(today, today)
|
||||
val orderCounts = orderRepository.getOrderCount()
|
||||
val workOrderCounts = workOrderRepository.getCount()
|
||||
|
||||
_uiState.value = _uiState.value.copy(
|
||||
todayEvents = eventsResult.getOrNull() ?: emptyList(),
|
||||
orderCounts = orderCounts.getOrNull(),
|
||||
workOrderCounts = workOrderCounts.getOrNull(),
|
||||
isLoading = false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = e.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startAutoRefresh() {
|
||||
autoRefreshJob = viewModelScope.launch {
|
||||
while (isActive) {
|
||||
delay(5000)
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
autoRefreshJob?.cancel()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val orderRepository: OrderRepository,
|
||||
private val workOrderRepository: WorkOrderRepository,
|
||||
private val scheduleRepository: ScheduleRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return HomeViewModel(orderRepository, workOrderRepository, scheduleRepository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.example.ops_android.ui.navigation
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Inventory
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.ShoppingCart
|
||||
import androidx.compose.material.icons.filled.Warehouse
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import androidx.navigation.navArgument
|
||||
import com.example.ops_android.ui.auth.LoginScreen
|
||||
import com.example.ops_android.ui.customer.CustomerListScreen
|
||||
import com.example.ops_android.ui.home.HomeScreen
|
||||
import com.example.ops_android.ui.order.OrderDetailScreen
|
||||
import com.example.ops_android.ui.order.OrderFormScreen
|
||||
import com.example.ops_android.ui.order.OrderListScreen
|
||||
import com.example.ops_android.ui.profile.ProfileScreen
|
||||
import com.example.ops_android.ui.schedule.ScheduleScreen
|
||||
import com.example.ops_android.ui.search.SearchScreen
|
||||
import com.example.ops_android.ui.settings.SettingsScreen
|
||||
import com.example.ops_android.ui.warehouse.ItemDetailScreen
|
||||
import com.example.ops_android.ui.warehouse.ItemFormScreen
|
||||
import com.example.ops_android.ui.warehouse.WarehouseScreen
|
||||
import com.example.ops_android.ui.workorder.WorkOrderDetailScreen
|
||||
import com.example.ops_android.ui.workorder.WorkOrderFormScreen
|
||||
import com.example.ops_android.ui.workorder.WorkOrderListScreen
|
||||
|
||||
object Routes {
|
||||
const val LOGIN = "login"
|
||||
const val MAIN = "main"
|
||||
const val SETTINGS = "settings"
|
||||
const val SEARCH = "search"
|
||||
const val SCHEDULE = "schedule"
|
||||
const val ORDER_DETAIL = "order_detail/{orderId}"
|
||||
const val ORDER_FORM = "order_form?editId={editId}"
|
||||
const val WORK_ORDER_DETAIL = "work_order_detail/{id}"
|
||||
const val WORK_ORDER_FORM = "work_order_form?editId={editId}"
|
||||
const val ITEM_DETAIL = "item_detail/{id}"
|
||||
const val ITEM_FORM = "item_form/{containerId}?editId={editId}"
|
||||
const val CUSTOMER_LIST = "customer_list"
|
||||
|
||||
fun orderDetail(orderId: Int) = "order_detail/$orderId"
|
||||
fun orderForm(editId: Int? = null) = if (editId != null) "order_form?editId=$editId" else "order_form"
|
||||
fun workOrderDetail(id: Int) = "work_order_detail/$id"
|
||||
fun workOrderForm(editId: Int? = null) = if (editId != null) "work_order_form?editId=$editId" else "work_order_form"
|
||||
fun itemDetail(id: Int) = "item_detail/$id"
|
||||
fun itemForm(containerId: Int, editId: Int? = null) = if (editId != null) "item_form/$containerId?editId=$editId" else "item_form/$containerId"
|
||||
}
|
||||
|
||||
object BottomTabs {
|
||||
const val HOME = "home"
|
||||
const val ORDERS = "orders"
|
||||
const val WORK_ORDERS = "work_orders"
|
||||
const val WAREHOUSE = "warehouse"
|
||||
const val PROFILE = "profile"
|
||||
}
|
||||
|
||||
data class BottomNavItem(val route: String, val icon: ImageVector, val label: String)
|
||||
|
||||
val bottomNavItems = listOf(
|
||||
BottomNavItem(BottomTabs.HOME, Icons.Default.Home, "首页"),
|
||||
BottomNavItem(BottomTabs.ORDERS, Icons.Default.ShoppingCart, "订单"),
|
||||
BottomNavItem(BottomTabs.WORK_ORDERS, Icons.Default.Inventory, "工单"),
|
||||
BottomNavItem(BottomTabs.WAREHOUSE, Icons.Default.Warehouse, "仓库"),
|
||||
BottomNavItem(BottomTabs.PROFILE, Icons.Default.Person, "我的")
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun OPSNavGraph(isLoggedIn: Boolean) {
|
||||
val rootNavController = rememberNavController()
|
||||
|
||||
NavHost(navController = rootNavController, startDestination = if (isLoggedIn) Routes.MAIN else Routes.LOGIN) {
|
||||
composable(Routes.LOGIN) {
|
||||
LoginScreen(
|
||||
onLoginSuccess = { rootNavController.navigate(Routes.MAIN) { popUpTo(Routes.LOGIN) { inclusive = true } } },
|
||||
onGoToSettings = { rootNavController.navigate(Routes.SETTINGS) }
|
||||
)
|
||||
}
|
||||
composable(Routes.MAIN) {
|
||||
MainScreen(
|
||||
onLogout = { rootNavController.navigate(Routes.LOGIN) { popUpTo(Routes.MAIN) { inclusive = true } } },
|
||||
onNavigate = { route -> rootNavController.navigate(route) }
|
||||
)
|
||||
}
|
||||
composable(Routes.SETTINGS) {
|
||||
SettingsScreen(onBack = { rootNavController.popBackStack() })
|
||||
}
|
||||
composable(Routes.SEARCH) {
|
||||
SearchScreen(
|
||||
onOrderClick = { id -> rootNavController.navigate(Routes.orderDetail(id)) },
|
||||
onWorkOrderClick = { id -> rootNavController.navigate(Routes.workOrderDetail(id)) },
|
||||
onItemClick = { id -> rootNavController.navigate(Routes.itemDetail(id)) },
|
||||
onContainerClick = { id -> rootNavController.navigate(Routes.MAIN) },
|
||||
onBack = { rootNavController.popBackStack() }
|
||||
)
|
||||
}
|
||||
composable(Routes.SCHEDULE) {
|
||||
ScheduleScreen(onBack = { rootNavController.popBackStack() })
|
||||
}
|
||||
composable(Routes.ORDER_DETAIL, arguments = listOf(navArgument("orderId") { type = NavType.IntType })) { backStackEntry ->
|
||||
val orderId = backStackEntry.arguments?.getInt("orderId") ?: return@composable
|
||||
OrderDetailScreen(orderId = orderId, onBack = { rootNavController.popBackStack() })
|
||||
}
|
||||
composable(Routes.ORDER_FORM, arguments = listOf(navArgument("editId") { type = NavType.IntType; defaultValue = -1 })) { backStackEntry ->
|
||||
val editId = backStackEntry.arguments?.getInt("editId")?.takeIf { it > 0 }
|
||||
OrderFormScreen(editOrderId = editId, onBack = { rootNavController.popBackStack() }, onSaved = { rootNavController.popBackStack() })
|
||||
}
|
||||
composable(Routes.WORK_ORDER_DETAIL, arguments = listOf(navArgument("id") { type = NavType.IntType })) { backStackEntry ->
|
||||
val id = backStackEntry.arguments?.getInt("id") ?: return@composable
|
||||
WorkOrderDetailScreen(id = id, onBack = { rootNavController.popBackStack() })
|
||||
}
|
||||
composable(Routes.WORK_ORDER_FORM, arguments = listOf(navArgument("editId") { type = NavType.IntType; defaultValue = -1 })) { backStackEntry ->
|
||||
val editId = backStackEntry.arguments?.getInt("editId")?.takeIf { it > 0 }
|
||||
WorkOrderFormScreen(editId = editId, onBack = { rootNavController.popBackStack() }, onSaved = { rootNavController.popBackStack() })
|
||||
}
|
||||
composable(Routes.ITEM_DETAIL, arguments = listOf(navArgument("id") { type = NavType.IntType })) { backStackEntry ->
|
||||
val id = backStackEntry.arguments?.getInt("id") ?: return@composable
|
||||
ItemDetailScreen(id = id, onBack = { rootNavController.popBackStack() })
|
||||
}
|
||||
composable(Routes.ITEM_FORM, arguments = listOf(navArgument("containerId") { type = NavType.IntType }, navArgument("editId") { type = NavType.IntType; defaultValue = -1 })) { backStackEntry ->
|
||||
val containerId = backStackEntry.arguments?.getInt("containerId") ?: 0
|
||||
val editId = backStackEntry.arguments?.getInt("editId")?.takeIf { it > 0 }
|
||||
ItemFormScreen(containerId = containerId, editId = editId, onBack = { rootNavController.popBackStack() }, onSaved = { rootNavController.popBackStack() })
|
||||
}
|
||||
composable(Routes.CUSTOMER_LIST) {
|
||||
CustomerListScreen(onBack = { rootNavController.popBackStack() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MainScreen(onLogout: () -> Unit, onNavigate: (String) -> Unit) {
|
||||
val tabNavController = rememberNavController()
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val navBackStackEntry by tabNavController.currentBackStackEntryAsState()
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
bottomNavItems.forEach { item ->
|
||||
NavigationBarItem(
|
||||
icon = { Icon(item.icon, contentDescription = item.label) },
|
||||
label = { Text(item.label) },
|
||||
selected = currentDestination?.hierarchy?.any { it.route == item.route } == true,
|
||||
onClick = {
|
||||
tabNavController.navigate(item.route) {
|
||||
popUpTo(tabNavController.graph.findStartDestination().id) { saveState = true }
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { innerPadding ->
|
||||
NavHost(navController = tabNavController, startDestination = BottomTabs.HOME, modifier = Modifier.padding(innerPadding)) {
|
||||
composable(BottomTabs.HOME) {
|
||||
HomeScreen(
|
||||
onSearchClick = { onNavigate(Routes.SEARCH) },
|
||||
onScheduleClick = { onNavigate(Routes.SCHEDULE) }
|
||||
)
|
||||
}
|
||||
composable(BottomTabs.ORDERS) {
|
||||
OrderListScreen(
|
||||
onOrderClick = { id -> onNavigate(Routes.orderDetail(id)) },
|
||||
onAddClick = { onNavigate(Routes.orderForm()) }
|
||||
)
|
||||
}
|
||||
composable(BottomTabs.WORK_ORDERS) {
|
||||
WorkOrderListScreen(
|
||||
onWorkOrderClick = { id -> onNavigate(Routes.workOrderDetail(id)) },
|
||||
onAddClick = { onNavigate(Routes.workOrderForm()) }
|
||||
)
|
||||
}
|
||||
composable(BottomTabs.WAREHOUSE) {
|
||||
WarehouseScreen(
|
||||
onItemClick = { id -> onNavigate(Routes.itemDetail(id)) }
|
||||
)
|
||||
}
|
||||
composable(BottomTabs.PROFILE) {
|
||||
ProfileScreen(
|
||||
onLogout = onLogout,
|
||||
onGoToSettings = { onNavigate(Routes.SETTINGS) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package com.example.ops_android.ui.order
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Print
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
val STATUS_FLOW = listOf("pending", "ordered", "arrived", "received")
|
||||
val STATUS_FLOW_LABELS = mapOf(
|
||||
"pending" to "待处理",
|
||||
"ordered" to "已订购",
|
||||
"arrived" to "已到货",
|
||||
"received" to "已收货",
|
||||
"lost" to "丢失",
|
||||
"returned" to "退货"
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OrderDetailScreen(
|
||||
orderId: Int,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val repository = LocalAppContainer.current.orderRepository
|
||||
val viewModel: OrderDetailViewModel = viewModel(
|
||||
key = "order_$orderId",
|
||||
factory = OrderDetailViewModel.Factory(repository, orderId)
|
||||
)
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
val order = uiState.order
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("订单详情") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
val scope = rememberCoroutineScope()
|
||||
order?.let { o ->
|
||||
val printerManager = LocalAppContainer.current.printerManager
|
||||
if (printerManager != null) {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
com.example.ops_android.printer.PrintHelper.printOrderLabel(printerManager, o)
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Default.Print, "打印", tint = Color.White)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFF667EEA),
|
||||
titleContentColor = Color.White
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
if (uiState.isLoading) {
|
||||
Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
if (order == null) {
|
||||
Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
|
||||
Text(uiState.errorMessage ?: "加载失败")
|
||||
}
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
item {
|
||||
Text("基本信息", fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
DetailRow("标题", order.title ?: "-")
|
||||
DetailRow("状态", STATUS_FLOW_LABELS[order.status] ?: order.status ?: "-")
|
||||
DetailRow("备注", order.remark ?: "-")
|
||||
DetailRow("链接", order.link ?: "-")
|
||||
DetailRow("创建时间", order.created_at ?: "-")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
order.style_tags?.let { tags ->
|
||||
if (tags.isNotEmpty()) {
|
||||
item {
|
||||
Text("款式标签", fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
tags.forEach { Text("- $it", fontSize = 14.sp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
order.costs?.let { costs ->
|
||||
if (costs.isNotEmpty()) {
|
||||
item {
|
||||
Text("费用明细", fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(16.dp)) {
|
||||
costs.forEach { cost ->
|
||||
Row(
|
||||
Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text("${cost.amount} ${cost.currency}")
|
||||
Text(cost.remark ?: "", color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status timeline
|
||||
order.status_logs?.let { logs ->
|
||||
if (logs.isNotEmpty()) {
|
||||
item {
|
||||
Text("状态时间线", fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
}
|
||||
items(logs) { log ->
|
||||
TimelineItem(log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status change buttons
|
||||
item {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("状态变更", fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
STATUS_FLOW.forEach { status ->
|
||||
if (status != order.status) {
|
||||
FilterChip(
|
||||
selected = false,
|
||||
onClick = { viewModel.showStatusDialog(status) },
|
||||
label = { Text(STATUS_FLOW_LABELS[status] ?: status, fontSize = 12.sp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
FilterChip(
|
||||
selected = false,
|
||||
onClick = { viewModel.showStatusDialog("lost") },
|
||||
label = { Text("丢失", fontSize = 12.sp, color = Color(0xFFF44336)) }
|
||||
)
|
||||
FilterChip(
|
||||
selected = false,
|
||||
onClick = { viewModel.showStatusDialog("returned") },
|
||||
label = { Text("退货", fontSize = 12.sp, color = Color(0xFF9C27B0)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status change dialog
|
||||
if (uiState.showStatusDialog) {
|
||||
val statusLabel = STATUS_FLOW_LABELS[uiState.selectedStatus] ?: uiState.selectedStatus
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.dismissStatusDialog() },
|
||||
title = { Text("变更状态为「$statusLabel」") },
|
||||
text = {
|
||||
Column {
|
||||
Text("请输入备注(可选):")
|
||||
Spacer(Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = uiState.statusComment,
|
||||
onValueChange = viewModel::onStatusCommentChange,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 2,
|
||||
placeholder = { Text("备注信息") }
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = { viewModel.submitStatusChange() },
|
||||
enabled = !uiState.isSubmitting
|
||||
) { Text("确认") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { viewModel.dismissStatusDialog() }) { Text("取消") }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailRow(label: String, value: String) {
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
|
||||
Text(label, color = Color.Gray, modifier = Modifier.width(80.dp))
|
||||
Text(value)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TimelineItem(log: com.example.ops_android.data.model.StatusLog) {
|
||||
Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp)) {
|
||||
Column(Modifier.padding(12.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
Text(
|
||||
STATUS_FLOW_LABELS[log.status] ?: log.status ?: "",
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF667EEA)
|
||||
)
|
||||
Text(log.created_at ?: "", fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
log.comment?.let {
|
||||
if (it.isNotBlank()) {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(it, fontSize = 14.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.example.ops_android.ui.order
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.model.Cost
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.data.model.StatusLog
|
||||
import com.example.ops_android.data.repository.OrderRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class OrderDetailUiState(
|
||||
val order: PurchaseOrder? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val showStatusDialog: Boolean = false,
|
||||
val selectedStatus: String = "",
|
||||
val statusComment: String = "",
|
||||
val isSubmitting: Boolean = false
|
||||
)
|
||||
|
||||
class OrderDetailViewModel(
|
||||
private val orderRepository: OrderRepository,
|
||||
private val orderId: Int
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(OrderDetailUiState())
|
||||
val uiState: StateFlow<OrderDetailUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadOrder()
|
||||
}
|
||||
|
||||
fun loadOrder() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
val result = orderRepository.getOrder(orderId)
|
||||
result.fold(
|
||||
onSuccess = { order ->
|
||||
_uiState.value = _uiState.value.copy(order = order, isLoading = false)
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.value = _uiState.value.copy(isLoading = false, errorMessage = e.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showStatusDialog(status: String) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
showStatusDialog = true,
|
||||
selectedStatus = status,
|
||||
statusComment = ""
|
||||
)
|
||||
}
|
||||
|
||||
fun dismissStatusDialog() {
|
||||
_uiState.value = _uiState.value.copy(showStatusDialog = false)
|
||||
}
|
||||
|
||||
fun onStatusCommentChange(comment: String) {
|
||||
_uiState.value = _uiState.value.copy(statusComment = comment)
|
||||
}
|
||||
|
||||
fun submitStatusChange() {
|
||||
viewModelScope.launch {
|
||||
val state = _uiState.value
|
||||
_uiState.value = state.copy(isSubmitting = true)
|
||||
val result = orderRepository.updateOrderStatus(
|
||||
id = orderId,
|
||||
status = state.selectedStatus,
|
||||
comment = state.statusComment.ifBlank { null }
|
||||
)
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
_uiState.value = _uiState.value.copy(showStatusDialog = false, isSubmitting = false)
|
||||
loadOrder()
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.value = _uiState.value.copy(isSubmitting = false, errorMessage = e.message)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val orderRepository: OrderRepository,
|
||||
private val orderId: Int
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return OrderDetailViewModel(orderRepository, orderId) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.example.ops_android.ui.order
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.repository.OrderRepository
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class OrderFormUiState(
|
||||
val title: String = "",
|
||||
val remark: String = "",
|
||||
val link: String = "",
|
||||
val styleTags: String = "",
|
||||
val costs: List<Pair<String, String>> = listOf("" to ""),
|
||||
val isSubmitting: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val success: Boolean = false
|
||||
)
|
||||
|
||||
class OrderFormViewModel(
|
||||
private val orderRepository: OrderRepository,
|
||||
private val editOrderId: Int? = null
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(OrderFormUiState())
|
||||
val uiState: StateFlow<OrderFormUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun onTitleChange(v: String) { _uiState.value = _uiState.value.copy(title = v) }
|
||||
fun onRemarkChange(v: String) { _uiState.value = _uiState.value.copy(remark = v) }
|
||||
fun onLinkChange(v: String) { _uiState.value = _uiState.value.copy(link = v) }
|
||||
fun onStyleTagsChange(v: String) { _uiState.value = _uiState.value.copy(styleTags = v) }
|
||||
|
||||
fun addCost() {
|
||||
_uiState.value = _uiState.value.copy(costs = _uiState.value.costs + ("" to ""))
|
||||
}
|
||||
|
||||
fun updateCostAmount(index: Int, value: String) {
|
||||
val costs = _uiState.value.costs.toMutableList()
|
||||
costs[index] = costs[index].copy(first = value)
|
||||
_uiState.value = _uiState.value.copy(costs = costs)
|
||||
}
|
||||
|
||||
fun updateCostCurrency(index: Int, value: String) {
|
||||
val costs = _uiState.value.costs.toMutableList()
|
||||
costs[index] = costs[index].copy(second = value)
|
||||
_uiState.value = _uiState.value.copy(costs = costs)
|
||||
}
|
||||
|
||||
fun removeCost(index: Int) {
|
||||
val costs = _uiState.value.costs.toMutableList()
|
||||
if (costs.size > 1) {
|
||||
costs.removeAt(index)
|
||||
_uiState.value = _uiState.value.copy(costs = costs)
|
||||
}
|
||||
}
|
||||
|
||||
fun submit() {
|
||||
val state = _uiState.value
|
||||
if (state.title.isBlank()) {
|
||||
_uiState.value = state.copy(errorMessage = "标题不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
val costList = state.costs
|
||||
.filter { it.first.isNotBlank() }
|
||||
.map { mapOf("amount" to it.first.toDoubleOrNull(), "currency" to it.second.ifBlank { "CNY" }) }
|
||||
|
||||
val tags = state.styleTags
|
||||
.split(",", ",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
|
||||
val data = mapOf(
|
||||
"title" to state.title,
|
||||
"remark" to state.remark,
|
||||
"link" to state.link,
|
||||
"style_tags" to tags,
|
||||
"costs" to costList
|
||||
)
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isSubmitting = true, errorMessage = null)
|
||||
val result = if (editOrderId != null) {
|
||||
orderRepository.updateOrder(data + ("id" to editOrderId))
|
||||
} else {
|
||||
orderRepository.addOrder(data)
|
||||
}
|
||||
result.fold(
|
||||
onSuccess = { _uiState.value = _uiState.value.copy(isSubmitting = false, success = true) },
|
||||
onFailure = { e -> _uiState.value = _uiState.value.copy(isSubmitting = false, errorMessage = e.message) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val orderRepository: OrderRepository,
|
||||
private val editOrderId: Int? = null
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return OrderFormViewModel(orderRepository, editOrderId) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OrderFormScreen(
|
||||
editOrderId: Int? = null,
|
||||
onBack: () -> Unit,
|
||||
onSaved: () -> Unit
|
||||
) {
|
||||
val viewModel: OrderFormViewModel = viewModel(
|
||||
factory = OrderFormViewModel.Factory(LocalAppContainer.current.orderRepository, editOrderId)
|
||||
)
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(uiState.success) {
|
||||
if (uiState.success) onSaved()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(if (editOrderId != null) "编辑订单" else "新建订单") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(padding).padding(16.dp).verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
OutlinedTextField(value = uiState.title, onValueChange = viewModel::onTitleChange, label = { Text("标题 *") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
OutlinedTextField(value = uiState.remark, onValueChange = viewModel::onRemarkChange, label = { Text("备注") }, modifier = Modifier.fillMaxWidth(), minLines = 2)
|
||||
OutlinedTextField(value = uiState.link, onValueChange = viewModel::onLinkChange, label = { Text("链接") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
OutlinedTextField(value = uiState.styleTags, onValueChange = viewModel::onStyleTagsChange, label = { Text("款式标签(逗号分隔)") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
|
||||
Text("费用明细", fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
|
||||
uiState.costs.forEachIndexed { index, cost ->
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
OutlinedTextField(
|
||||
value = cost.first, onValueChange = { viewModel.updateCostAmount(index, it) },
|
||||
label = { Text("金额") }, modifier = Modifier.weight(1f), singleLine = true
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = cost.second, onValueChange = { viewModel.updateCostCurrency(index, it) },
|
||||
label = { Text("币种") }, modifier = Modifier.weight(0.5f), singleLine = true,
|
||||
placeholder = { Text("CNY") }
|
||||
)
|
||||
if (uiState.costs.size > 1) {
|
||||
IconButton(onClick = { viewModel.removeCost(index) }) {
|
||||
Text("✕", color = Color.Red)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextButton(onClick = { viewModel.addCost() }) { Text("+ 添加费用") }
|
||||
|
||||
uiState.errorMessage?.let {
|
||||
Text(it, color = Color.Red, fontSize = 14.sp)
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.submit() },
|
||||
modifier = Modifier.fillMaxWidth().height(48.dp),
|
||||
enabled = !uiState.isSubmitting
|
||||
) {
|
||||
if (uiState.isSubmitting) CircularProgressIndicator(Modifier.size(20.dp), color = Color.White, strokeWidth = 2.dp)
|
||||
else Text("提交")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.example.ops_android.ui.order
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun OrderListScreen(
|
||||
onOrderClick: (Int) -> Unit,
|
||||
onAddClick: () -> Unit,
|
||||
viewModel: OrderListViewModel = viewModel(factory = OrderListViewModel.Factory(LocalAppContainer.current.orderRepository))
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("采购订单") },
|
||||
actions = {
|
||||
IconButton(onClick = onAddClick) {
|
||||
Icon(Icons.Default.Add, "新建", tint = Color.White)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFF667EEA),
|
||||
titleContentColor = Color.White
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(
|
||||
onClick = onAddClick,
|
||||
containerColor = Color(0xFF667EEA)
|
||||
) {
|
||||
Icon(Icons.Default.Add, "新建", tint = Color.White)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
// Status filter tabs
|
||||
ScrollableTabRow(
|
||||
selectedTabIndex = ORDER_STATUSES.indexOfFirst { it.first == uiState.selectedStatus }.coerceAtLeast(0),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
edgePadding = 8.dp
|
||||
) {
|
||||
ORDER_STATUSES.forEachIndexed { index, (status, label) ->
|
||||
Tab(
|
||||
selected = uiState.selectedStatus == status,
|
||||
onClick = { viewModel.selectStatus(status) },
|
||||
text = {
|
||||
Text(
|
||||
label,
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (uiState.selectedStatus == status) FontWeight.Bold else FontWeight.Normal
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
uiState.isLoading -> {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
uiState.errorMessage != null -> {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(uiState.errorMessage!!, color = Color.Red)
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Button(onClick = { viewModel.refresh() }) { Text("重试") }
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(uiState.orders, key = { it.id }) { order ->
|
||||
OrderListItem(
|
||||
order = order,
|
||||
onClick = { onOrderClick(order.id) }
|
||||
)
|
||||
}
|
||||
|
||||
if (uiState.isLoadingMore) {
|
||||
item {
|
||||
Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator(modifier = Modifier.size(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// infinite scroll trigger
|
||||
item {
|
||||
LaunchedEffect(Unit) { viewModel.loadMore() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun OrderListItem(order: PurchaseOrder, onClick: () -> Unit) {
|
||||
val statusColors = mapOf(
|
||||
"pending" to Color(0xFFFF9800),
|
||||
"ordered" to Color(0xFF2196F3),
|
||||
"arrived" to Color(0xFF03A9F4),
|
||||
"received" to Color(0xFF4CAF50),
|
||||
"lost" to Color(0xFFF44336),
|
||||
"returned" to Color(0xFF9C27B0)
|
||||
)
|
||||
val statusLabels = mapOf(
|
||||
"pending" to "待处理",
|
||||
"ordered" to "已订购",
|
||||
"arrived" to "已到货",
|
||||
"received" to "已收货",
|
||||
"lost" to "丢失",
|
||||
"returned" to "退货"
|
||||
)
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(order.title ?: "无标题", fontWeight = FontWeight.Medium, fontSize = 15.sp)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(order.created_at ?: "", fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = statusColors[order.status]?.copy(alpha = 0.15f) ?: Color.Gray.copy(alpha = 0.15f)
|
||||
) {
|
||||
Text(
|
||||
statusLabels[order.status] ?: order.status ?: "",
|
||||
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
fontSize = 12.sp,
|
||||
color = statusColors[order.status] ?: Color.Gray
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package com.example.ops_android.ui.order
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.data.repository.OrderRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
val ORDER_STATUSES = listOf(
|
||||
"" to "全部",
|
||||
"pending" to "待处理",
|
||||
"ordered" to "已订购",
|
||||
"arrived" to "已到货",
|
||||
"received" to "已收货",
|
||||
"lost" to "丢失",
|
||||
"returned" to "退货"
|
||||
)
|
||||
|
||||
data class OrderListUiState(
|
||||
val orders: List<PurchaseOrder> = emptyList(),
|
||||
val selectedStatus: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val isRefreshing: Boolean = false,
|
||||
val isLoadingMore: Boolean = false,
|
||||
val hasMore: Boolean = true,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class OrderListViewModel(
|
||||
private val orderRepository: OrderRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(OrderListUiState())
|
||||
val uiState: StateFlow<OrderListUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val pageSize = 20
|
||||
private var currentOffset = 0
|
||||
|
||||
init {
|
||||
loadOrders()
|
||||
}
|
||||
|
||||
fun selectStatus(status: String) {
|
||||
_uiState.value = _uiState.value.copy(selectedStatus = status)
|
||||
refresh()
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isRefreshing = true, orders = emptyList())
|
||||
currentOffset = 0
|
||||
val state = _uiState.value
|
||||
val result = orderRepository.getOrders(
|
||||
status = state.selectedStatus.ifEmpty { null },
|
||||
offset = 0,
|
||||
limit = pageSize
|
||||
)
|
||||
result.fold(
|
||||
onSuccess = { (orders, total) ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
orders = orders,
|
||||
isRefreshing = false,
|
||||
isLoading = false,
|
||||
hasMore = orders.size >= pageSize,
|
||||
errorMessage = null
|
||||
)
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isRefreshing = false,
|
||||
isLoading = false,
|
||||
errorMessage = e.message
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMore() {
|
||||
val state = _uiState.value
|
||||
if (state.isLoadingMore || !state.hasMore) return
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoadingMore = true)
|
||||
currentOffset += pageSize
|
||||
val result = orderRepository.getOrders(
|
||||
status = state.selectedStatus.ifEmpty { null },
|
||||
offset = currentOffset,
|
||||
limit = pageSize
|
||||
)
|
||||
result.fold(
|
||||
onSuccess = { (orders, total) ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
orders = _uiState.value.orders + orders,
|
||||
isLoadingMore = false,
|
||||
hasMore = orders.size >= pageSize
|
||||
)
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoadingMore = false,
|
||||
errorMessage = e.message
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadOrders() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
val result = orderRepository.getOrders(limit = pageSize)
|
||||
result.fold(
|
||||
onSuccess = { (orders, total) ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
orders = orders,
|
||||
isLoading = false,
|
||||
hasMore = orders.size >= pageSize
|
||||
)
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = e.message
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val orderRepository: OrderRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return OrderListViewModel(orderRepository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package com.example.ops_android.ui.profile
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material.icons.automirrored.filled.Logout
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ProfileScreen(
|
||||
onLogout: () -> Unit,
|
||||
onGoToSettings: () -> Unit,
|
||||
viewModel: ProfileViewModel = viewModel(factory = ProfileViewModel.Factory(
|
||||
LocalAppContainer.current.authRepository,
|
||||
LocalAppContainer.current.sessionManager
|
||||
))
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var showLogoutDialog by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(uiState.isLoggedOut) {
|
||||
if (uiState.isLoggedOut) {
|
||||
onLogout()
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("我的") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFF667EEA),
|
||||
titleContentColor = Color.White
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(200.dp)
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(Color(0xFF667EEA), Color(0xFF764BA2))
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(80.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color.White.copy(alpha = 0.3f)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Text(
|
||||
text = viewModel.username.ifEmpty { "未登录" },
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
ProfileInfoRow("用户名", viewModel.username)
|
||||
ProfileInfoRow("姓名", uiState.userInfo?.FirstName ?: "-")
|
||||
ProfileInfoRow("角色", uiState.userInfo?.Role ?: "-")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
NavigationDrawerItem(
|
||||
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
|
||||
label = { Text("设置") },
|
||||
selected = false,
|
||||
onClick = onGoToSettings,
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
NavigationDrawerItem(
|
||||
icon = { Icon(Icons.AutoMirrored.Filled.Logout, contentDescription = null, tint = Color(0xFFF44336)) },
|
||||
label = { Text("退出登录", color = Color(0xFFF44336)) },
|
||||
selected = false,
|
||||
onClick = { showLogoutDialog = true },
|
||||
modifier = Modifier.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (showLogoutDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showLogoutDialog = false },
|
||||
title = { Text("退出登录") },
|
||||
text = { Text("确定要退出登录吗?") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
showLogoutDialog = false
|
||||
viewModel.logout()
|
||||
}) {
|
||||
Text("确定", color = Color(0xFFF44336))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showLogoutDialog = false }) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProfileInfoRow(label: String, value: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = label, color = Color.Gray)
|
||||
Text(text = value, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.example.ops_android.ui.profile
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.local.SessionManager
|
||||
import com.example.ops_android.data.model.UserDetail
|
||||
import com.example.ops_android.data.model.UserInfo
|
||||
import com.example.ops_android.data.repository.AuthRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ProfileUiState(
|
||||
val user: UserInfo? = null,
|
||||
val userInfo: UserDetail? = null,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val isLoggedOut: Boolean = false
|
||||
)
|
||||
|
||||
class ProfileViewModel(
|
||||
private val authRepository: AuthRepository,
|
||||
private val sessionManager: SessionManager
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(ProfileUiState())
|
||||
val uiState: StateFlow<ProfileUiState> = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadProfile()
|
||||
}
|
||||
|
||||
fun loadProfile() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
val result = authRepository.getUserInfo()
|
||||
result.fold(
|
||||
onSuccess = { (user, userInfo) ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
user = user,
|
||||
userInfo = userInfo,
|
||||
isLoading = false,
|
||||
errorMessage = null
|
||||
)
|
||||
},
|
||||
onFailure = { e ->
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
errorMessage = e.message ?: "加载失败"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
authRepository.logout()
|
||||
_uiState.value = _uiState.value.copy(isLoggedOut = true)
|
||||
}
|
||||
}
|
||||
|
||||
val username: String
|
||||
get() {
|
||||
val info = _uiState.value.userInfo
|
||||
val user = _uiState.value.user
|
||||
return info?.Username ?: user?.Name ?: ""
|
||||
}
|
||||
|
||||
val avatarPath: String?
|
||||
get() = _uiState.value.userInfo?.AvatarPath
|
||||
|
||||
class Factory(
|
||||
private val authRepository: AuthRepository,
|
||||
private val sessionManager: SessionManager
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return ProfileViewModel(authRepository, sessionManager) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
package com.example.ops_android.ui.schedule
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowLeft
|
||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ScheduleScreen(
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val vm: ScheduleViewModel = viewModel(factory = ScheduleViewModel.Factory(LocalAppContainer.current.scheduleRepository))
|
||||
val s by vm.uiState.collectAsState()
|
||||
val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("日程管理") },
|
||||
navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) } },
|
||||
actions = { IconButton(onClick = { vm.showAddForm(dateFormat.format(Date())) }) { Icon(Icons.Default.Add, "添加", tint = Color.White) } },
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
LazyColumn(Modifier.fillMaxSize().padding(padding)) {
|
||||
// Month header
|
||||
item {
|
||||
Card(Modifier.fillMaxWidth().padding(12.dp)) {
|
||||
Column(Modifier.padding(8.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
|
||||
IconButton(onClick = { vm.prevMonth() }) { Icon(Icons.AutoMirrored.Filled.KeyboardArrowLeft, "上月") }
|
||||
Text("${s.currentYear}年 ${s.currentMonth + 1}月", fontWeight = FontWeight.Bold, fontSize = 18.sp)
|
||||
IconButton(onClick = { vm.nextMonth() }) { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, "下月") }
|
||||
}
|
||||
|
||||
// Weekday headers
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
listOf("日", "一", "二", "三", "四", "五", "六").forEach { day ->
|
||||
Text(day, Modifier.weight(1f), textAlign = TextAlign.Center, fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(4.dp))
|
||||
|
||||
// Calendar grid
|
||||
val cal = Calendar.getInstance().apply { set(s.currentYear, s.currentMonth, 1) }
|
||||
val firstDayOfWeek = cal.get(Calendar.DAY_OF_WEEK) - 1
|
||||
val daysInMonth = cal.getActualMaximum(Calendar.DAY_OF_MONTH)
|
||||
val today = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date())
|
||||
|
||||
var dayCount = 0
|
||||
val weeks = mutableListOf<List<Int?>>()
|
||||
var currentWeek = mutableListOf<Int?>()
|
||||
for (i in 0 until firstDayOfWeek) { currentWeek.add(null); dayCount++ }
|
||||
for (day in 1..daysInMonth) { currentWeek.add(day); dayCount++; if (dayCount % 7 == 0) { weeks.add(currentWeek.toList()); currentWeek = mutableListOf() } }
|
||||
if (currentWeek.isNotEmpty()) { while (currentWeek.size < 7) currentWeek.add(null); weeks.add(currentWeek.toList()) }
|
||||
|
||||
weeks.forEach { week ->
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
week.forEach { day ->
|
||||
val cellModifier = Modifier.weight(1f).aspectRatio(1f).padding(2.dp)
|
||||
if (day != null) {
|
||||
val dateStr = String.format("%04d-%02d-%02d", s.currentYear, s.currentMonth + 1, day)
|
||||
val hasEvent = s.events.any { it.start_time?.startsWith(dateStr) == true }
|
||||
val isToday = dateStr == today
|
||||
|
||||
Box(
|
||||
cellModifier.clip(CircleShape)
|
||||
.then(if (isToday) Modifier.background(Color(0xFF667EEA)) else Modifier)
|
||||
.clickable { vm.showAddForm(dateStr) },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(day.toString(), fontSize = 13.sp, color = if (isToday) Color.White else Color.Black)
|
||||
if (hasEvent) Box(Modifier.size(4.dp).clip(CircleShape).background(if (isToday) Color.White else Color(0xFF667EEA)))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Spacer(cellModifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Events list
|
||||
item { Text("日程列表", fontWeight = FontWeight.Bold, fontSize = 16.sp, modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) }
|
||||
|
||||
if (s.events.isEmpty()) {
|
||||
item { Box(Modifier.fillMaxWidth().padding(32.dp), contentAlignment = Alignment.Center) { Text("暂无日程", color = Color.Gray) } }
|
||||
} else {
|
||||
items(s.events.sortedBy { it.start_time }) { event ->
|
||||
Card(Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp).clickable { vm.showEditForm(event) }) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(event.title ?: "", fontWeight = FontWeight.Medium)
|
||||
val timeStr = listOfNotNull(event.start_time?.substring(11)?.take(5), event.end_time?.substring(11)?.take(5)).joinToString(" ~ ")
|
||||
if (timeStr.isNotBlank()) Text(timeStr, fontSize = 12.sp, color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Form dialog
|
||||
if (s.showForm) AlertDialog(
|
||||
onDismissRequest = { vm.dismissForm() },
|
||||
title = { Text(if (s.editingEvent != null) "编辑日程" else "添加日程") },
|
||||
text = {
|
||||
Column(Modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(s.formTitle, vm::onFormTitle, label = { Text("标题 *") }, singleLine = true, modifier = Modifier.fillMaxWidth())
|
||||
OutlinedTextField(s.formDescription, vm::onFormDesc, label = { Text("描述") }, minLines = 2, modifier = Modifier.fillMaxWidth())
|
||||
Text("日期: ${s.formDate}", fontSize = 14.sp, color = Color.Gray)
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
OutlinedTextField(s.formStartTime, vm::onFormStartTime, label = { Text("开始") }, placeholder = { Text("HH:mm") }, singleLine = true, modifier = Modifier.weight(1f))
|
||||
OutlinedTextField(s.formEndTime, vm::onFormEndTime, label = { Text("结束") }, placeholder = { Text("HH:mm") }, singleLine = true, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (s.editingEvent != null) {
|
||||
TextButton(onClick = { vm.deleteEvent(s.editingEvent!!.id); vm.dismissForm() }) { Text("删除", color = Color.Red) }
|
||||
}
|
||||
Button(onClick = { vm.submitForm() }, enabled = !s.isSubmitting) { Text("保存") }
|
||||
}
|
||||
},
|
||||
dismissButton = { TextButton(onClick = { vm.dismissForm() }) { Text("取消") } }
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.example.ops_android.ui.schedule
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.model.ScheduleEvent
|
||||
import com.example.ops_android.data.repository.ScheduleRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
data class ScheduleUiState(
|
||||
val events: List<ScheduleEvent> = emptyList(),
|
||||
val currentYear: Int = Calendar.getInstance().get(Calendar.YEAR),
|
||||
val currentMonth: Int = Calendar.getInstance().get(Calendar.MONTH),
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
val showForm: Boolean = false,
|
||||
val editingEvent: ScheduleEvent? = null,
|
||||
val formTitle: String = "",
|
||||
val formDescription: String = "",
|
||||
val formDate: String = SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date()),
|
||||
val formStartTime: String = "",
|
||||
val formEndTime: String = "",
|
||||
val isSubmitting: Boolean = false
|
||||
)
|
||||
|
||||
class ScheduleViewModel(
|
||||
private val repository: ScheduleRepository
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(ScheduleUiState())
|
||||
val uiState: StateFlow<ScheduleUiState> = _uiState.asStateFlow()
|
||||
private val dateFormat = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
|
||||
init { loadEvents() }
|
||||
|
||||
fun loadEvents() {
|
||||
val state = _uiState.value
|
||||
val cal = Calendar.getInstance()
|
||||
cal.set(state.currentYear, state.currentMonth, 1)
|
||||
val start = dateFormat.format(cal.time)
|
||||
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH))
|
||||
val end = dateFormat.format(cal.time)
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
repository.getEvents(start, end).fold(
|
||||
onSuccess = { _uiState.value = _uiState.value.copy(events = it, isLoading = false) },
|
||||
onFailure = { _uiState.value = _uiState.value.copy(isLoading = false, errorMessage = it.message) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun prevMonth() {
|
||||
val state = _uiState.value
|
||||
_uiState.value = state.copy(
|
||||
currentMonth = if (state.currentMonth == 0) { _uiState.value = _uiState.value.copy(currentYear = state.currentYear - 1); 11 } else state.currentMonth - 1
|
||||
)
|
||||
loadEvents()
|
||||
}
|
||||
|
||||
fun nextMonth() {
|
||||
val state = _uiState.value
|
||||
_uiState.value = state.copy(
|
||||
currentMonth = if (state.currentMonth == 11) { _uiState.value = _uiState.value.copy(currentYear = state.currentYear + 1); 0 } else state.currentMonth + 1
|
||||
)
|
||||
loadEvents()
|
||||
}
|
||||
|
||||
fun showAddForm(date: String) {
|
||||
_uiState.value = _uiState.value.copy(showForm = true, editingEvent = null, formTitle = "", formDescription = "", formDate = date, formStartTime = "", formEndTime = "")
|
||||
}
|
||||
|
||||
fun showEditForm(event: ScheduleEvent) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
showForm = true, editingEvent = event,
|
||||
formTitle = event.title ?: "", formDescription = event.description ?: "",
|
||||
formDate = event.start_time?.take(10) ?: "", formStartTime = event.start_time?.substring(11)?.take(5) ?: "",
|
||||
formEndTime = event.end_time?.substring(11)?.take(5) ?: ""
|
||||
)
|
||||
}
|
||||
|
||||
fun dismissForm() { _uiState.value = _uiState.value.copy(showForm = false) }
|
||||
fun onFormTitle(v: String) { _uiState.value = _uiState.value.copy(formTitle = v) }
|
||||
fun onFormDesc(v: String) { _uiState.value = _uiState.value.copy(formDescription = v) }
|
||||
fun onFormStartTime(v: String) { _uiState.value = _uiState.value.copy(formStartTime = v) }
|
||||
fun onFormEndTime(v: String) { _uiState.value = _uiState.value.copy(formEndTime = v) }
|
||||
|
||||
fun submitForm() {
|
||||
val st = _uiState.value
|
||||
if (st.formTitle.isBlank()) { _uiState.value = st.copy(errorMessage = "标题不能为空"); return }
|
||||
val startTime = if (st.formStartTime.isNotBlank()) "${st.formDate}T${st.formStartTime}:00" else st.formDate
|
||||
val endTime = if (st.formEndTime.isNotBlank()) "${st.formDate}T${st.formEndTime}:00" else st.formDate
|
||||
val data = mapOf("title" to st.formTitle, "description" to st.formDescription, "start_time" to startTime, "end_time" to endTime)
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isSubmitting = true)
|
||||
val r = if (st.editingEvent != null) repository.editEvent(data + ("id" to st.editingEvent.id)) else repository.addEvent(data)
|
||||
r.fold(onSuccess = { _uiState.value = _uiState.value.copy(showForm = false, isSubmitting = false); loadEvents() }, onFailure = { _uiState.value = _uiState.value.copy(isSubmitting = false, errorMessage = it.message) })
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteEvent(id: Int) {
|
||||
viewModelScope.launch { repository.deleteEvent(id).fold(onSuccess = { loadEvents() }, onFailure = { _uiState.value = _uiState.value.copy(errorMessage = it.message) }) }
|
||||
}
|
||||
|
||||
class Factory(private val repo: ScheduleRepository) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(c: Class<T>): T = ScheduleViewModel(repo) as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package com.example.ops_android.ui.search
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Size
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.QrCodeScanner
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.model.Container
|
||||
import com.example.ops_android.data.model.Item
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.data.model.WorkOrder
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanner
|
||||
import com.google.mlkit.vision.barcode.BarcodeScanning
|
||||
import com.google.mlkit.vision.barcode.common.Barcode
|
||||
import com.google.mlkit.vision.common.InputImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SearchScreen(
|
||||
onOrderClick: (Int) -> Unit,
|
||||
onWorkOrderClick: (Int) -> Unit,
|
||||
onItemClick: (Int) -> Unit,
|
||||
onContainerClick: (Int) -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val appContainer = LocalAppContainer.current
|
||||
val viewModel: SearchViewModel = viewModel(
|
||||
factory = SearchViewModel.Factory(appContainer.orderRepository, appContainer.workOrderRepository, appContainer.warehouseRepository)
|
||||
)
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
var showScanner by remember { mutableStateOf(false) }
|
||||
val context = LocalContext.current
|
||||
var hasCameraPermission by remember { mutableStateOf(false) }
|
||||
|
||||
val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
hasCameraPermission = granted
|
||||
if (granted) showScanner = true
|
||||
}
|
||||
|
||||
fun checkCameraAndScan() {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
hasCameraPermission = true
|
||||
showScanner = true
|
||||
} else {
|
||||
permissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("搜索") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White)
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFF667EEA),
|
||||
titleContentColor = Color.White
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
if (showScanner) {
|
||||
BarcodeScannerView(
|
||||
onBarcodeScanned = { barcode ->
|
||||
viewModel.onBarcodeScanned(barcode)
|
||||
showScanner = false
|
||||
},
|
||||
onClose = { showScanner = false }
|
||||
)
|
||||
return@Scaffold
|
||||
}
|
||||
|
||||
Column(modifier = Modifier.fillMaxSize().padding(padding)) {
|
||||
// Search bar
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = uiState.query,
|
||||
onValueChange = viewModel::onQueryChange,
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("输入关键词搜索...", fontSize = 14.sp) },
|
||||
singleLine = true,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { viewModel.search() }) {
|
||||
Icon(Icons.Default.Search, "搜索")
|
||||
}
|
||||
},
|
||||
shape = RoundedCornerShape(24.dp)
|
||||
)
|
||||
IconButton(onClick = { checkCameraAndScan() }) {
|
||||
Icon(Icons.Default.QrCodeScanner, "扫码", tint = Color(0xFF667EEA))
|
||||
}
|
||||
}
|
||||
|
||||
// Prefix hints
|
||||
Text(
|
||||
"前缀快捷搜索: po:采购单 wo:工单 item:物品 warehouse:容器",
|
||||
fontSize = 11.sp,
|
||||
color = Color.Gray,
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
// Category tabs
|
||||
ScrollableTabRow(
|
||||
selectedTabIndex = SearchCategory.entries.indexOf(uiState.category),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
edgePadding = 8.dp
|
||||
) {
|
||||
SearchCategory.entries.forEach { cat ->
|
||||
Tab(
|
||||
selected = uiState.category == cat,
|
||||
onClick = { viewModel.selectCategory(cat); viewModel.clearResults() },
|
||||
text = { Text(cat.label, fontSize = 13.sp) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.isSearching) {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
return@Column
|
||||
}
|
||||
|
||||
// Results
|
||||
LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
val hasResults = uiState.orders.isNotEmpty() || uiState.workOrders.isNotEmpty() || uiState.items.isNotEmpty() || uiState.containers.isNotEmpty()
|
||||
|
||||
if (!hasResults && uiState.query.isNotBlank()) {
|
||||
item {
|
||||
Box(Modifier.fillMaxWidth().padding(32.dp), contentAlignment = Alignment.Center) {
|
||||
Text("未找到匹配结果", color = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (uiState.orders.isNotEmpty() && (uiState.category == SearchCategory.ALL || uiState.category == SearchCategory.ORDER)) {
|
||||
item { CategoryHeader("采购订单") }
|
||||
items(uiState.orders.take(5)) { order -> SearchOrderItem(order) { onOrderClick(order.id) } }
|
||||
}
|
||||
|
||||
if (uiState.workOrders.isNotEmpty() && (uiState.category == SearchCategory.ALL || uiState.category == SearchCategory.WORK_ORDER)) {
|
||||
item { CategoryHeader("工单") }
|
||||
items(uiState.workOrders.take(5)) { wo -> SearchWorkOrderItem(wo) { onWorkOrderClick(wo.id) } }
|
||||
}
|
||||
|
||||
if (uiState.items.isNotEmpty() && (uiState.category == SearchCategory.ALL || uiState.category == SearchCategory.ITEM)) {
|
||||
item { CategoryHeader("物品") }
|
||||
items(uiState.items.take(5)) { item -> SearchItemItem(item) { onItemClick(item.id) } }
|
||||
}
|
||||
|
||||
if (uiState.containers.isNotEmpty() && (uiState.category == SearchCategory.ALL || uiState.category == SearchCategory.CONTAINER)) {
|
||||
item { CategoryHeader("容器") }
|
||||
items(uiState.containers.take(5)) { container -> SearchContainerItem(container) { onContainerClick(container.id) } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CategoryHeader(title: String) {
|
||||
Text(title, fontWeight = FontWeight.Bold, fontSize = 14.sp, color = Color(0xFF667EEA), modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchOrderItem(order: PurchaseOrder, onClick: () -> Unit) {
|
||||
Card(Modifier.fillMaxWidth().clickable { onClick() }) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(order.title ?: "无标题", fontWeight = FontWeight.Medium)
|
||||
order.remark?.takeIf { it.isNotBlank() }?.let { Text(it, fontSize = 12.sp, color = Color.Gray, maxLines = 1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchWorkOrderItem(wo: WorkOrder, onClick: () -> Unit) {
|
||||
Card(Modifier.fillMaxWidth().clickable { onClick() }) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(wo.title ?: "无标题", fontWeight = FontWeight.Medium)
|
||||
wo.description?.takeIf { it.isNotBlank() }?.let { Text(it, fontSize = 12.sp, color = Color.Gray, maxLines = 1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchItemItem(item: Item, onClick: () -> Unit) {
|
||||
Card(Modifier.fillMaxWidth().clickable { onClick() }) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(item.name ?: "未命名", fontWeight = FontWeight.Medium)
|
||||
item.serial_number?.let { Text("编号: $it", fontSize = 12.sp, color = Color.Gray) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchContainerItem(container: Container, onClick: () -> Unit) {
|
||||
Card(Modifier.fillMaxWidth().clickable { onClick() }) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(container.name ?: "未命名", fontWeight = FontWeight.Medium)
|
||||
container.barcode?.let { Text("条码: $it", fontSize = 12.sp, color = Color.Gray) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BarcodeScannerView(
|
||||
onBarcodeScanned: (String) -> Unit,
|
||||
onClose: () -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
var hasScanned by remember { mutableStateOf(false) }
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().background(Color.Black)) {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
PreviewView(ctx).also { previewView ->
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
val preview = androidx.camera.core.Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
|
||||
val scanner = BarcodeScanning.getClient()
|
||||
val imageAnalysis = ImageAnalysis.Builder()
|
||||
.setTargetResolution(Size(1280, 720))
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
imageAnalysis.setAnalyzer(ContextCompat.getMainExecutor(ctx)) { imageProxy ->
|
||||
if (!hasScanned) {
|
||||
processImageProxy(scanner, imageProxy) { barcode ->
|
||||
hasScanned = true
|
||||
onBarcodeScanned(barcode)
|
||||
}
|
||||
} else {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview, imageAnalysis)
|
||||
} catch (e: Exception) { }
|
||||
}, ContextCompat.getMainExecutor(ctx))
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
Button(
|
||||
onClick = onClose,
|
||||
modifier = Modifier.align(Alignment.TopEnd).padding(16.dp)
|
||||
) {
|
||||
Text("关闭")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processImageProxy(scanner: BarcodeScanner, imageProxy: ImageProxy, onResult: (String) -> Unit) {
|
||||
val mediaImage = imageProxy.image
|
||||
if (mediaImage != null) {
|
||||
val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
|
||||
scanner.process(image)
|
||||
.addOnSuccessListener { barcodes ->
|
||||
for (barcode in barcodes) {
|
||||
barcode.rawValue?.let { onResult(it) }
|
||||
}
|
||||
}
|
||||
.addOnCompleteListener { imageProxy.close() }
|
||||
} else {
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.example.ops_android.ui.search
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.model.Container
|
||||
import com.example.ops_android.data.model.Item
|
||||
import com.example.ops_android.data.model.PurchaseOrder
|
||||
import com.example.ops_android.data.model.WorkOrder
|
||||
import com.example.ops_android.data.repository.CustomerRepository
|
||||
import com.example.ops_android.data.repository.OrderRepository
|
||||
import com.example.ops_android.data.repository.WarehouseRepository
|
||||
import com.example.ops_android.data.repository.WorkOrderRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
enum class SearchCategory(val label: String) {
|
||||
ALL("全部"),
|
||||
ORDER("订单"),
|
||||
WORK_ORDER("工单"),
|
||||
ITEM("物品"),
|
||||
CONTAINER("容器")
|
||||
}
|
||||
|
||||
data class SearchUiState(
|
||||
val query: String = "",
|
||||
val category: SearchCategory = SearchCategory.ALL,
|
||||
val orders: List<PurchaseOrder> = emptyList(),
|
||||
val workOrders: List<WorkOrder> = emptyList(),
|
||||
val items: List<Item> = emptyList(),
|
||||
val containers: List<Container> = emptyList(),
|
||||
val isSearching: Boolean = false,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class SearchViewModel(
|
||||
private val orderRepository: OrderRepository,
|
||||
private val workOrderRepository: WorkOrderRepository,
|
||||
private val warehouseRepository: WarehouseRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SearchUiState())
|
||||
val uiState: StateFlow<SearchUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun onQueryChange(value: String) {
|
||||
_uiState.value = _uiState.value.copy(query = value)
|
||||
}
|
||||
|
||||
fun selectCategory(category: SearchCategory) {
|
||||
_uiState.value = _uiState.value.copy(category = category)
|
||||
}
|
||||
|
||||
fun onBarcodeScanned(barcode: String) {
|
||||
_uiState.value = _uiState.value.copy(query = barcode)
|
||||
search()
|
||||
}
|
||||
|
||||
fun search() {
|
||||
val query = _uiState.value.query.trim()
|
||||
if (query.isEmpty()) return
|
||||
|
||||
// Parse prefix shortcuts
|
||||
val (actualCategory, actualQuery) = parsePrefix(query)
|
||||
_uiState.value = _uiState.value.copy(category = actualCategory)
|
||||
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isSearching = true, errorMessage = null)
|
||||
|
||||
when (actualCategory) {
|
||||
SearchCategory.ALL -> {
|
||||
launch { orderRepository.getOrders(search = actualQuery, limit = 10).fold(
|
||||
onSuccess = { (list, _) -> _uiState.value = _uiState.value.copy(orders = list) },
|
||||
onFailure = { }
|
||||
)}
|
||||
launch { workOrderRepository.list(search = actualQuery, limit = 10).fold(
|
||||
onSuccess = { (list, _) -> _uiState.value = _uiState.value.copy(workOrders = list) },
|
||||
onFailure = { }
|
||||
)}
|
||||
launch { warehouseRepository.listItem().fold(
|
||||
onSuccess = { _uiState.value = _uiState.value.copy(items = it.filter { item -> item.name?.contains(actualQuery, true) == true || item.serial_number?.contains(actualQuery, true) == true || item.barcode?.contains(actualQuery, true) == true }.take(10)) },
|
||||
onFailure = { }
|
||||
)}
|
||||
launch { warehouseRepository.listContainer().fold(
|
||||
onSuccess = { _uiState.value = _uiState.value.copy(containers = it.filter { c -> c.name?.contains(actualQuery, true) == true || c.barcode?.contains(actualQuery, true) == true }.take(10)) },
|
||||
onFailure = { }
|
||||
)}
|
||||
}
|
||||
SearchCategory.ORDER -> {
|
||||
orderRepository.getOrders(search = actualQuery, limit = 20).fold(
|
||||
onSuccess = { (list, _) -> _uiState.value = _uiState.value.copy(orders = list) },
|
||||
onFailure = { _uiState.value = _uiState.value.copy(errorMessage = it.message) }
|
||||
)
|
||||
}
|
||||
SearchCategory.WORK_ORDER -> {
|
||||
workOrderRepository.list(search = actualQuery, limit = 20).fold(
|
||||
onSuccess = { (list, _) -> _uiState.value = _uiState.value.copy(workOrders = list) },
|
||||
onFailure = { _uiState.value = _uiState.value.copy(errorMessage = it.message) }
|
||||
)
|
||||
}
|
||||
SearchCategory.ITEM -> {
|
||||
warehouseRepository.listItem().fold(
|
||||
onSuccess = { items ->
|
||||
_uiState.value = _uiState.value.copy(items = items.filter { it.name?.contains(actualQuery, true) == true || it.serial_number?.contains(actualQuery, true) == true || it.barcode?.contains(actualQuery, true) == true })
|
||||
},
|
||||
onFailure = { _uiState.value = _uiState.value.copy(errorMessage = it.message) }
|
||||
)
|
||||
}
|
||||
SearchCategory.CONTAINER -> {
|
||||
warehouseRepository.listContainer().fold(
|
||||
onSuccess = { containers ->
|
||||
_uiState.value = _uiState.value.copy(containers = containers.filter { it.name?.contains(actualQuery, true) == true || it.barcode?.contains(actualQuery, true) == true })
|
||||
},
|
||||
onFailure = { _uiState.value = _uiState.value.copy(errorMessage = it.message) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
_uiState.value = _uiState.value.copy(isSearching = false)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearResults() {
|
||||
_uiState.value = SearchUiState(query = _uiState.value.query, category = _uiState.value.category)
|
||||
}
|
||||
|
||||
private fun parsePrefix(query: String): Pair<SearchCategory, String> {
|
||||
val prefixes = mapOf(
|
||||
"po:" to SearchCategory.ORDER,
|
||||
"wo:" to SearchCategory.WORK_ORDER,
|
||||
"item:" to SearchCategory.ITEM,
|
||||
"warehouse:" to SearchCategory.CONTAINER
|
||||
)
|
||||
for ((prefix, category) in prefixes) {
|
||||
if (query.startsWith(prefix, ignoreCase = true)) {
|
||||
return category to query.removeRange(0, prefix.length).trim()
|
||||
}
|
||||
}
|
||||
return _uiState.value.category to query
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val orderRepository: OrderRepository,
|
||||
private val workOrderRepository: WorkOrderRepository,
|
||||
private val warehouseRepository: WarehouseRepository
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return SearchViewModel(orderRepository, workOrderRepository, warehouseRepository) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package com.example.ops_android.ui.settings
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.BuildConfig
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen(
|
||||
onBack: () -> Unit,
|
||||
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory(LocalAppContainer.current.sessionManager))
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("设置") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "返回")
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = Color(0xFF667EEA),
|
||||
titleContentColor = Color.White,
|
||||
navigationIconContentColor = Color.White
|
||||
)
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "服务器配置",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = uiState.apiBaseUrl,
|
||||
onValueChange = viewModel::onUrlChange,
|
||||
label = { Text("API 地址") },
|
||||
placeholder = { Text("例如: http://192.168.1.100:8080/api") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Button(
|
||||
onClick = { viewModel.saveUrl() },
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
Text("保存")
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = { viewModel.testConnection() },
|
||||
modifier = Modifier.weight(1f),
|
||||
enabled = !uiState.isTesting,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = if (uiState.testSuccess) Color(0xFF4CAF50)
|
||||
else if (uiState.testResult != null && !uiState.testSuccess) Color(0xFFF44336)
|
||||
else MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
) {
|
||||
if (uiState.isTesting) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(20.dp),
|
||||
color = Color.White,
|
||||
strokeWidth = 2.dp
|
||||
)
|
||||
} else {
|
||||
Text("测试连接")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uiState.message?.let {
|
||||
Text(
|
||||
text = it,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
|
||||
uiState.testResult?.let {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (uiState.testSuccess) Color(0xFFE8F5E9) else Color(0xFFFFEBEE)
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = it,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
color = if (uiState.testSuccess) Color(0xFF2E7D32) else Color(0xFFC62828),
|
||||
fontSize = 14.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
|
||||
|
||||
Text(
|
||||
text = "关于",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Card {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
InfoRow("应用名称", "OPS 运营管理系统")
|
||||
InfoRow("版本", "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||
InfoRow("作者", BuildConfig.AUTHOR)
|
||||
InfoRow("构建日期", BuildConfig.BUILD_DATE)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InfoRow(label: String, value: String) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Text(text = label, color = Color.Gray)
|
||||
Text(text = value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.example.ops_android.ui.settings
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.local.SessionManager
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.net.ConnectException
|
||||
import java.net.SocketTimeoutException
|
||||
import java.net.UnknownHostException
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.net.ssl.SSLException
|
||||
|
||||
data class SettingsUiState(
|
||||
val apiBaseUrl: String = "",
|
||||
val isTesting: Boolean = false,
|
||||
val testResult: String? = null,
|
||||
val testSuccess: Boolean = false,
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
class SettingsViewModel(
|
||||
private val sessionManager: SessionManager
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SettingsUiState())
|
||||
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
|
||||
|
||||
private val client = OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.followRedirects(true)
|
||||
.build()
|
||||
|
||||
init {
|
||||
loadCurrentUrl()
|
||||
}
|
||||
|
||||
private fun loadCurrentUrl() {
|
||||
viewModelScope.launch {
|
||||
val url = sessionManager.getApiBaseUrl()
|
||||
_uiState.value = _uiState.value.copy(apiBaseUrl = url)
|
||||
}
|
||||
}
|
||||
|
||||
fun onUrlChange(value: String) {
|
||||
_uiState.value = _uiState.value.copy(apiBaseUrl = value, message = null)
|
||||
}
|
||||
|
||||
fun saveUrl() {
|
||||
viewModelScope.launch {
|
||||
val url = _uiState.value.apiBaseUrl.trim()
|
||||
if (url.isBlank()) {
|
||||
_uiState.value = _uiState.value.copy(message = "请输入有效的API地址")
|
||||
return@launch
|
||||
}
|
||||
sessionManager.setApiBaseUrl(url)
|
||||
_uiState.value = _uiState.value.copy(message = "API地址已保存")
|
||||
}
|
||||
}
|
||||
|
||||
fun testConnection() {
|
||||
viewModelScope.launch {
|
||||
val url = _uiState.value.apiBaseUrl.trim()
|
||||
if (url.isBlank()) {
|
||||
_uiState.value = _uiState.value.copy(message = "请先输入API地址")
|
||||
return@launch
|
||||
}
|
||||
|
||||
_uiState.value = _uiState.value.copy(isTesting = true, testResult = null, message = null)
|
||||
|
||||
try {
|
||||
val code = withContext(Dispatchers.IO) {
|
||||
val request = Request.Builder().url(url).header("Accept", "application/json").build()
|
||||
val response = client.newCall(request).execute()
|
||||
response.use { it.code }
|
||||
}
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isTesting = false,
|
||||
testResult = if (code in 200..299) "连接成功 ($code)" else "服务器响应异常: HTTP $code",
|
||||
testSuccess = code in 200..299
|
||||
)
|
||||
} catch (e: UnknownHostException) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isTesting = false,
|
||||
testResult = "连接失败: 无法解析服务器地址",
|
||||
testSuccess = false
|
||||
)
|
||||
} catch (e: SocketTimeoutException) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isTesting = false,
|
||||
testResult = "连接超时,请检查网络和服务器地址",
|
||||
testSuccess = false
|
||||
)
|
||||
} catch (e: ConnectException) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isTesting = false,
|
||||
testResult = "连接被拒绝: ${e.message ?: "服务器不可达"}",
|
||||
testSuccess = false
|
||||
)
|
||||
} catch (e: SSLException) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isTesting = false,
|
||||
testResult = "SSL证书错误,HTTP地址请使用http://",
|
||||
testSuccess = false
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isTesting = false,
|
||||
testResult = "连接失败: ${e.javaClass.simpleName} - ${e.message ?: "未知错误"}",
|
||||
testSuccess = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(
|
||||
private val sessionManager: SessionManager
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return SettingsViewModel(sessionManager) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.example.ops_android.ui.warehouse
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.model.Item
|
||||
import com.example.ops_android.data.repository.WarehouseRepository
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ItemDetailUiState(val item: Item? = null, val isLoading: Boolean = false, val errorMessage: String? = null)
|
||||
|
||||
class ItemDetailViewModel(repo: WarehouseRepository, private val id: Int) : ViewModel() {
|
||||
private val _s = MutableStateFlow(ItemDetailUiState()); val s: StateFlow<ItemDetailUiState> = _s.asStateFlow()
|
||||
private val repository = repo
|
||||
init { load() }
|
||||
private fun load() { viewModelScope.launch { _s.value = _s.value.copy(isLoading = true); repository.getItem(id).fold(onSuccess = { _s.value = _s.value.copy(item = it, isLoading = false) }, onFailure = { _s.value = _s.value.copy(isLoading = false, errorMessage = it.message) }) } }
|
||||
class Factory(private val repo: WarehouseRepository, private val id: Int) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(c: Class<T>): T = ItemDetailViewModel(repo, id) as T }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ItemDetailScreen(id: Int, onBack: () -> Unit) {
|
||||
val vm: ItemDetailViewModel = viewModel(key = "item_$id", factory = ItemDetailViewModel.Factory(LocalAppContainer.current.warehouseRepository, id))
|
||||
val s by vm.s.collectAsState(); val item = s.item
|
||||
|
||||
Scaffold(topBar = { TopAppBar(title = { Text("物品详情") }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White)) }) { padding ->
|
||||
if (s.isLoading) { Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) { CircularProgressIndicator() }; return@Scaffold }
|
||||
if (item == null) { Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) { Text(s.errorMessage ?: "加载失败") }; return@Scaffold }
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize().padding(padding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
item { Text("基本信息", fontWeight = FontWeight.Bold, fontSize = 18.sp); Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp)) { DetailRow("名称", item.name ?: "-"); DetailRow("编号", item.serial_number ?: "-"); DetailRow("规格", item.spec ?: "-"); DetailRow("条码", item.barcode ?: "-"); DetailRow("备注", item.remark ?: "-"); DetailRow("创建时间", item.created_at ?: "-") } } }
|
||||
|
||||
item.move_history?.let { history ->
|
||||
if (history.isNotEmpty()) {
|
||||
item { Text("移动记录", fontWeight = FontWeight.Bold, fontSize = 16.sp) }
|
||||
items(history) { h ->
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(12.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text("${h.from_container ?: "-"} → ${h.to_container ?: "-"}", fontWeight = FontWeight.Medium); Text(h.moved_at ?: "", fontSize = 12.sp, color = Color.Gray) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailRow(label: String, value: String) {
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Text(label, color = Color.Gray, modifier = Modifier.width(80.dp)); Text(value) }
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.example.ops_android.ui.warehouse
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.repository.WarehouseRepository
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class ItemFormUiState(
|
||||
val name: String = "", val serialNumber: String = "", val spec: String = "", val remark: String = "",
|
||||
val isSubmitting: Boolean = false, val errorMessage: String? = null, val success: Boolean = false
|
||||
)
|
||||
|
||||
class ItemFormViewModel(private val repo: WarehouseRepository, private val containerId: Int, private val editId: Int? = null) : ViewModel() {
|
||||
private val _s = MutableStateFlow(ItemFormUiState()); val s: StateFlow<ItemFormUiState> = _s.asStateFlow()
|
||||
fun onName(v: String) { _s.value = _s.value.copy(name = v) }
|
||||
fun onSerial(v: String) { _s.value = _s.value.copy(serialNumber = v) }
|
||||
fun onSpec(v: String) { _s.value = _s.value.copy(spec = v) }
|
||||
fun onRemark(v: String) { _s.value = _s.value.copy(remark = v) }
|
||||
fun submit() {
|
||||
if (_s.value.name.isBlank()) { _s.value = _s.value.copy(errorMessage = "名称不能为空"); return }
|
||||
val data = mapOf("name" to _s.value.name, "serial_number" to _s.value.serialNumber, "spec" to _s.value.spec, "remark" to _s.value.remark, "container_id" to containerId)
|
||||
viewModelScope.launch {
|
||||
_s.value = _s.value.copy(isSubmitting = true)
|
||||
val r = if (editId != null) repo.updateItem(data + ("id" to editId)) else repo.addItem(data)
|
||||
r.fold(onSuccess = { _s.value = _s.value.copy(isSubmitting = false, success = true) }, onFailure = { _s.value = _s.value.copy(isSubmitting = false, errorMessage = it.message) })
|
||||
}
|
||||
}
|
||||
class Factory(private val repo: WarehouseRepository, private val containerId: Int, private val editId: Int? = null) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(c: Class<T>): T = ItemFormViewModel(repo, containerId, editId) as T }
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ItemFormScreen(containerId: Int = 0, editId: Int? = null, onBack: () -> Unit, onSaved: () -> Unit) {
|
||||
val vm: ItemFormViewModel = viewModel(factory = ItemFormViewModel.Factory(LocalAppContainer.current.warehouseRepository, containerId, editId))
|
||||
val s by vm.s.collectAsState()
|
||||
LaunchedEffect(s.success) { if (s.success) onSaved() }
|
||||
|
||||
Scaffold(topBar = { TopAppBar(title = { Text(if (editId != null) "编辑物品" else "添加物品") }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White)) }) { padding ->
|
||||
Column(Modifier.fillMaxSize().padding(padding).padding(16.dp).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(s.name, vm::onName, label = { Text("名称 *") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
OutlinedTextField(s.serialNumber, vm::onSerial, label = { Text("编号") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
OutlinedTextField(s.spec, vm::onSpec, label = { Text("规格") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
OutlinedTextField(s.remark, vm::onRemark, label = { Text("备注") }, modifier = Modifier.fillMaxWidth(), minLines = 2)
|
||||
s.errorMessage?.let { Text(it, color = Color.Red, fontSize = 14.sp) }
|
||||
Button(onClick = { vm.submit() }, Modifier.fillMaxWidth().height(48.dp), enabled = !s.isSubmitting) { Text("提交") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.example.ops_android.ui.warehouse
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Folder
|
||||
import androidx.compose.material.icons.filled.Inventory
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.model.Container
|
||||
import com.example.ops_android.data.model.Item
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WarehouseScreen(
|
||||
onItemClick: (Int) -> Unit,
|
||||
viewModel: WarehouseViewModel = viewModel(factory = WarehouseViewModel.Factory(LocalAppContainer.current.warehouseRepository))
|
||||
) {
|
||||
val s by viewModel.s.collectAsState()
|
||||
var showAddDialog by remember { mutableStateOf(false) }
|
||||
var newContainerName by remember { mutableStateOf("") }
|
||||
var tabIndex by remember { mutableStateOf(0) }
|
||||
|
||||
Scaffold(
|
||||
topBar = { TopAppBar(title = { Text("仓库") }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White)) },
|
||||
floatingActionButton = { FloatingActionButton(onClick = { showAddDialog = true }, containerColor = Color(0xFF667EEA)) { Icon(Icons.Default.Add, "新建容器", tint = Color.White) } }
|
||||
) { padding ->
|
||||
Column(Modifier.padding(padding)) {
|
||||
// Breadcrumb
|
||||
ScrollableTabRow(selectedTabIndex = s.breadcrumb.size - 1) {
|
||||
s.breadcrumb.forEachIndexed { idx, (id, name) ->
|
||||
Tab(selected = idx == s.breadcrumb.size - 1, onClick = { viewModel.navigateToBreadcrumb(idx) }, text = { Text(name, fontSize = 12.sp) })
|
||||
}
|
||||
}
|
||||
|
||||
// Tabs: All / Items Only
|
||||
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FilterChip(selected = !s.showItemsOnly, onClick = { viewModel.toggleItemsOnly() }, label = { Text("全部") })
|
||||
FilterChip(selected = s.showItemsOnly, onClick = { viewModel.toggleItemsOnly() }, label = { Text("仅物品") })
|
||||
}
|
||||
|
||||
if (s.isLoading) Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
|
||||
else LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (!s.showItemsOnly) {
|
||||
items(s.containers) { container ->
|
||||
ContainerItem(container) { viewModel.navigateTo(container.id, container.name ?: "未命名") }
|
||||
}
|
||||
}
|
||||
items(s.items) { item ->
|
||||
ItemRow(item) { onItemClick(item.id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showAddDialog) AlertDialog(
|
||||
onDismissRequest = { showAddDialog = false },
|
||||
title = { Text("新建容器") },
|
||||
text = { OutlinedTextField(newContainerName, { newContainerName = it }, label = { Text("容器名称") }, singleLine = true) },
|
||||
confirmButton = { Button(onClick = { if (newContainerName.isNotBlank()) { viewModel.addContainer(newContainerName); showAddDialog = false; newContainerName = "" } }) { Text("创建") } },
|
||||
dismissButton = { TextButton(onClick = { showAddDialog = false }) { Text("取消") } }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContainerItem(container: Container, onClick: () -> Unit) {
|
||||
Card(Modifier.fillMaxWidth().clickable { onClick() }) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Folder, null, tint = Color(0xFFFF9800))
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(container.name ?: "未命名", modifier = Modifier.weight(1f), fontWeight = FontWeight.Medium)
|
||||
Icon(Icons.Default.ChevronRight, null, tint = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ItemRow(item: Item, onClick: () -> Unit) {
|
||||
Card(Modifier.fillMaxWidth().clickable { onClick() }) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(Icons.Default.Inventory, null, tint = Color(0xFF2196F3))
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(item.name ?: "未命名", fontWeight = FontWeight.Medium)
|
||||
item.serial_number?.let { Text(it, fontSize = 12.sp, color = Color.Gray) }
|
||||
}
|
||||
Icon(Icons.Default.ChevronRight, null, tint = Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.example.ops_android.ui.warehouse
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.model.Container
|
||||
import com.example.ops_android.data.model.Item
|
||||
import com.example.ops_android.data.repository.WarehouseRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class WarehouseUiState(
|
||||
val containers: List<Container> = emptyList(),
|
||||
val items: List<Item> = emptyList(),
|
||||
val breadcrumb: List<Pair<Int, String>> = listOf(0 to "根目录"),
|
||||
val showItemsOnly: Boolean = false,
|
||||
val isLoading: Boolean = false,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class WarehouseViewModel(
|
||||
private val repo: WarehouseRepository
|
||||
) : ViewModel() {
|
||||
private val _s = MutableStateFlow(WarehouseUiState()); val s: StateFlow<WarehouseUiState> = _s.asStateFlow()
|
||||
|
||||
init { loadContainers(0) }
|
||||
|
||||
fun loadContainers(parentId: Int) {
|
||||
viewModelScope.launch {
|
||||
_s.value = _s.value.copy(isLoading = true)
|
||||
repo.listContainer(parentId).fold(
|
||||
onSuccess = { _s.value = _s.value.copy(containers = it, isLoading = false) },
|
||||
onFailure = { _s.value = _s.value.copy(isLoading = false, errorMessage = it.message) }
|
||||
)
|
||||
repo.listItem(parentId).fold(
|
||||
onSuccess = { _s.value = _s.value.copy(items = it) },
|
||||
onFailure = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun navigateTo(containerId: Int, containerName: String) {
|
||||
val breadcrumb = _s.value.breadcrumb.toMutableList()
|
||||
breadcrumb.add(containerId to containerName)
|
||||
_s.value = _s.value.copy(breadcrumb = breadcrumb)
|
||||
loadContainers(containerId)
|
||||
}
|
||||
|
||||
fun navigateToBreadcrumb(index: Int) {
|
||||
val entry = _s.value.breadcrumb[index]
|
||||
_s.value = _s.value.copy(breadcrumb = _s.value.breadcrumb.take(index + 1))
|
||||
loadContainers(entry.first)
|
||||
}
|
||||
|
||||
fun toggleItemsOnly() { _s.value = _s.value.copy(showItemsOnly = !_s.value.showItemsOnly) }
|
||||
|
||||
fun refresh() { loadContainers(_s.value.breadcrumb.last().first) }
|
||||
|
||||
fun addContainer(name: String) {
|
||||
val parentId = _s.value.breadcrumb.last().first
|
||||
viewModelScope.launch {
|
||||
repo.addContainer(mapOf("name" to name, "parent_id" to parentId)).fold(
|
||||
onSuccess = { refresh() },
|
||||
onFailure = { _s.value = _s.value.copy(errorMessage = it.message) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(private val repo: WarehouseRepository) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(c: Class<T>): T = WarehouseViewModel(repo) as T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.example.ops_android.ui.workorder
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Print
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.model.Commit
|
||||
import com.example.ops_android.data.model.WorkOrder
|
||||
import com.example.ops_android.data.repository.WorkOrderRepository
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class WorkOrderDetailUiState(
|
||||
val workOrder: WorkOrder? = null, val isLoading: Boolean = false, val errorMessage: String? = null,
|
||||
val showCommitDialog: Boolean = false, val commitStatus: String = "", val commitComment: String = "", val isSubmitting: Boolean = false
|
||||
)
|
||||
|
||||
class WorkOrderDetailViewModel(repo: WorkOrderRepository, private val id: Int) : ViewModel() {
|
||||
private val _s = MutableStateFlow(WorkOrderDetailUiState()); val s: StateFlow<WorkOrderDetailUiState> = _s.asStateFlow()
|
||||
private val repository = repo
|
||||
|
||||
init { load() }
|
||||
fun load() { viewModelScope.launch { _s.value = _s.value.copy(isLoading = true); repository.get(id).fold(onSuccess = { _s.value = _s.value.copy(workOrder = it, isLoading = false) }, onFailure = { _s.value = _s.value.copy(isLoading = false, errorMessage = it.message) }) } }
|
||||
fun showCommitDialog(status: String) { _s.value = _s.value.copy(showCommitDialog = true, commitStatus = status, commitComment = "") }
|
||||
fun dismissCommitDialog() { _s.value = _s.value.copy(showCommitDialog = false) }
|
||||
fun onCommentChange(v: String) { _s.value = _s.value.copy(commitComment = v) }
|
||||
fun submitCommit() { viewModelScope.launch {
|
||||
val st = _s.value; _s.value = st.copy(isSubmitting = true)
|
||||
repository.commit(id, st.commitStatus.ifBlank { null }, st.commitComment.ifBlank { null }, null)
|
||||
.fold(onSuccess = { _s.value = _s.value.copy(showCommitDialog = false, isSubmitting = false); load() }, onFailure = { _s.value = _s.value.copy(isSubmitting = false, errorMessage = it.message) })
|
||||
} }
|
||||
|
||||
class Factory(private val repo: WorkOrderRepository, private val id: Int) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(c: Class<T>): T = WorkOrderDetailViewModel(repo, id) as T
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WorkOrderDetailScreen(id: Int, onBack: () -> Unit) {
|
||||
val vm: WorkOrderDetailViewModel = viewModel(key = "wo_$id", factory = WorkOrderDetailViewModel.Factory(LocalAppContainer.current.workOrderRepository, id))
|
||||
val s by vm.s.collectAsState(); val wo = s.workOrder
|
||||
val sl = mapOf("pending" to "待处理", "checked" to "已检查", "parts_ordered" to "待配件", "repaired" to "已修复", "returned" to "已返还", "unrepairable" to "不可修")
|
||||
|
||||
Scaffold(topBar = {
|
||||
TopAppBar(title = { Text("工单详情") },
|
||||
navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) } },
|
||||
actions = {
|
||||
val scope = rememberCoroutineScope()
|
||||
wo?.let { wo_ ->
|
||||
val printerManager = com.example.ops_android.ui.LocalAppContainer.current.printerManager
|
||||
if (printerManager != null) {
|
||||
IconButton(onClick = {
|
||||
scope.launch { com.example.ops_android.printer.PrintHelper.printWorkOrderLabel(printerManager, wo_) }
|
||||
}) { Icon(Icons.Default.Print, "打印", tint = Color.White) }
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White))
|
||||
}) { padding ->
|
||||
if (s.isLoading) { Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) { CircularProgressIndicator() }; return@Scaffold }
|
||||
if (wo == null) { Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) { Text(s.errorMessage ?: "加载失败") }; return@Scaffold }
|
||||
|
||||
LazyColumn(Modifier.fillMaxSize().padding(padding).padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
item { Text("基本信息", fontWeight = FontWeight.Bold, fontSize = 18.sp); Card(Modifier.fillMaxWidth()) { Column(Modifier.padding(16.dp)) { DetailRow("标题", wo.title ?: "-"); DetailRow("状态", sl[wo.status] ?: wo.status ?: "-"); DetailRow("描述", wo.description ?: "-"); DetailRow("创建时间", wo.created_at ?: "-") } } }
|
||||
|
||||
wo.commits?.let { commits ->
|
||||
if (commits.isNotEmpty()) {
|
||||
item { Text("提交记录", fontWeight = FontWeight.Bold, fontSize = 16.sp) }
|
||||
items(commits) { c: Commit ->
|
||||
Card(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(12.dp)) {
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { Text(sl[c.status] ?: c.status ?: "", fontWeight = FontWeight.Medium, color = Color(0xFF667EEA)); Text(c.created_at ?: "", fontSize = 12.sp, color = Color.Gray) }
|
||||
c.comment?.takeIf { it.isNotBlank() }?.let { Spacer(Modifier.height(4.dp)); Text(it, fontSize = 14.sp) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item { Spacer(Modifier.height(8.dp)); Text("流转状态", fontWeight = FontWeight.Bold, fontSize = 16.sp)
|
||||
Row(Modifier.fillMaxWidth().wrapContentHeight(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
listOf("checked", "parts_ordered", "repaired", "returned", "unrepairable").forEach { st ->
|
||||
if (st != wo.status) FilterChip(selected = false, onClick = { vm.showCommitDialog(st) }, label = { Text(sl[st] ?: st, fontSize = 12.sp) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s.showCommitDialog) AlertDialog(onDismissRequest = { vm.dismissCommitDialog() }, title = { Text("流转为「${sl[s.commitStatus] ?: s.commitStatus}」") }, text = { Column { Text("备注(可选):"); Spacer(Modifier.height(8.dp)); OutlinedTextField(value = s.commitComment, onValueChange = vm::onCommentChange, Modifier.fillMaxWidth(), minLines = 2, placeholder = { Text("备注") }) } }, confirmButton = { Button(onClick = { vm.submitCommit() }, enabled = !s.isSubmitting) { Text("确认") } }, dismissButton = { TextButton(onClick = { vm.dismissCommitDialog() }) { Text("取消") } })
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailRow(label: String, value: String) {
|
||||
Row(Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Text(label, color = Color.Gray, modifier = Modifier.width(80.dp)); Text(value) }
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.example.ops_android.ui.workorder
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.repository.WorkOrderRepository
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
data class WorkOrderFormUiState(
|
||||
val title: String = "", val description: String = "", val isSubmitting: Boolean = false, val errorMessage: String? = null, val success: Boolean = false
|
||||
)
|
||||
|
||||
class WorkOrderFormViewModel(private val repo: WorkOrderRepository, private val editId: Int? = null) : ViewModel() {
|
||||
private val _s = MutableStateFlow(WorkOrderFormUiState()); val s: StateFlow<WorkOrderFormUiState> = _s.asStateFlow()
|
||||
fun onTitle(v: String) { _s.value = _s.value.copy(title = v) }
|
||||
fun onDesc(v: String) { _s.value = _s.value.copy(description = v) }
|
||||
fun submit() {
|
||||
if (_s.value.title.isBlank()) { _s.value = _s.value.copy(errorMessage = "标题不能为空"); return }
|
||||
val data = mapOf("title" to _s.value.title, "description" to _s.value.description)
|
||||
viewModelScope.launch {
|
||||
_s.value = _s.value.copy(isSubmitting = true, errorMessage = null)
|
||||
val r = if (editId != null) repo.update(data + ("id" to editId)) else repo.add(data)
|
||||
r.fold(onSuccess = { _s.value = _s.value.copy(isSubmitting = false, success = true) }, onFailure = { _s.value = _s.value.copy(isSubmitting = false, errorMessage = it.message) })
|
||||
}
|
||||
}
|
||||
class Factory(private val repo: WorkOrderRepository, private val editId: Int? = null) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(c: Class<T>): T = WorkOrderFormViewModel(repo, editId) as T
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WorkOrderFormScreen(editId: Int? = null, onBack: () -> Unit, onSaved: () -> Unit) {
|
||||
val vm: WorkOrderFormViewModel = viewModel(factory = WorkOrderFormViewModel.Factory(LocalAppContainer.current.workOrderRepository, editId))
|
||||
val s by vm.s.collectAsState()
|
||||
LaunchedEffect(s.success) { if (s.success) onSaved() }
|
||||
|
||||
Scaffold(topBar = { TopAppBar(title = { Text(if (editId != null) "编辑工单" else "新建工单") }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null, tint = Color.White) } }, colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White)) }) { padding ->
|
||||
Column(Modifier.fillMaxSize().padding(padding).padding(16.dp).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
OutlinedTextField(s.title, vm::onTitle, label = { Text("标题 *") }, modifier = Modifier.fillMaxWidth(), singleLine = true)
|
||||
OutlinedTextField(s.description, vm::onDesc, label = { Text("描述") }, modifier = Modifier.fillMaxWidth(), minLines = 3)
|
||||
s.errorMessage?.let { Text(it, color = Color.Red, fontSize = 14.sp) }
|
||||
Button(onClick = { vm.submit() }, Modifier.fillMaxWidth().height(48.dp), enabled = !s.isSubmitting) {
|
||||
if (s.isSubmitting) CircularProgressIndicator(Modifier.size(20.dp), color = Color.White, strokeWidth = 2.dp) else Text("提交")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.example.ops_android.ui.workorder
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.example.ops_android.data.model.WorkOrder
|
||||
import com.example.ops_android.ui.LocalAppContainer
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun WorkOrderListScreen(
|
||||
onWorkOrderClick: (Int) -> Unit,
|
||||
onAddClick: () -> Unit,
|
||||
viewModel: WorkOrderListViewModel = viewModel(factory = WorkOrderListViewModel.Factory(LocalAppContainer.current.workOrderRepository))
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
val statusColors = mapOf("pending" to Color(0xFFFF9800), "checked" to Color(0xFF2196F3), "parts_ordered" to Color(0xFF9C27B0), "repaired" to Color(0xFF4CAF50), "returned" to Color(0xFF4CAF50), "unrepairable" to Color(0xFFF44336))
|
||||
val statusLabels = mapOf("pending" to "待处理", "checked" to "已检查", "parts_ordered" to "待配件", "repaired" to "已修复", "returned" to "已返还", "unrepairable" to "不可修")
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(title = { Text("工单") }, actions = { IconButton(onClick = onAddClick) { Icon(Icons.Default.Add, "新建", tint = Color.White) } },
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color(0xFF667EEA), titleContentColor = Color.White))
|
||||
},
|
||||
floatingActionButton = { FloatingActionButton(onClick = onAddClick, containerColor = Color(0xFF667EEA)) { Icon(Icons.Default.Add, "新建", tint = Color.White) } }
|
||||
) { padding ->
|
||||
Column(Modifier.padding(padding)) {
|
||||
ScrollableTabRow(selectedTabIndex = WO_STATUSES.indexOfFirst { it.first == uiState.selectedStatus }.coerceAtLeast(0), edgePadding = 8.dp) {
|
||||
WO_STATUSES.forEachIndexed { i, (s, l) ->
|
||||
Tab(selected = uiState.selectedStatus == s, onClick = { viewModel.selectStatus(s) }, text = { Text(l, fontSize = 12.sp, fontWeight = if (uiState.selectedStatus == s) FontWeight.Bold else FontWeight.Normal) })
|
||||
}
|
||||
}
|
||||
if (uiState.isLoading) Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator() }
|
||||
else LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
items(uiState.workOrders, key = { it.id }) { wo ->
|
||||
Card(Modifier.fillMaxWidth().clickable { onWorkOrderClick(wo.id) }) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(wo.title ?: "无标题", fontWeight = FontWeight.Medium)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
Text(wo.description ?: "", fontSize = 12.sp, color = Color.Gray, maxLines = 1)
|
||||
}
|
||||
Surface(shape = MaterialTheme.shapes.small, color = statusColors[wo.status]?.copy(alpha = 0.15f) ?: Color.Gray) {
|
||||
Text(statusLabels[wo.status] ?: wo.status ?: "", modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), fontSize = 11.sp, color = statusColors[wo.status] ?: Color.Gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (uiState.isLoadingMore) item { Box(Modifier.fillMaxWidth().padding(16.dp), contentAlignment = Alignment.Center) { CircularProgressIndicator(Modifier.size(24.dp)) } }
|
||||
item { LaunchedEffect(Unit) { viewModel.loadMore() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.example.ops_android.ui.workorder
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.example.ops_android.data.model.WorkOrder
|
||||
import com.example.ops_android.data.repository.WorkOrderRepository
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
val WO_STATUSES = listOf(
|
||||
"" to "全部",
|
||||
"pending" to "待处理",
|
||||
"checked" to "已检查",
|
||||
"parts_ordered" to "待配件",
|
||||
"repaired" to "已修复",
|
||||
"returned" to "已返还",
|
||||
"unrepairable" to "不可修"
|
||||
)
|
||||
|
||||
data class WorkOrderListUiState(
|
||||
val workOrders: List<WorkOrder> = emptyList(),
|
||||
val selectedStatus: String = "",
|
||||
val isLoading: Boolean = false,
|
||||
val isLoadingMore: Boolean = false,
|
||||
val hasMore: Boolean = true,
|
||||
val errorMessage: String? = null
|
||||
)
|
||||
|
||||
class WorkOrderListViewModel(
|
||||
private val repository: WorkOrderRepository
|
||||
) : ViewModel() {
|
||||
private val _uiState = MutableStateFlow(WorkOrderListUiState())
|
||||
val uiState: StateFlow<WorkOrderListUiState> = _uiState.asStateFlow()
|
||||
private val pageSize = 20
|
||||
private var offset = 0
|
||||
|
||||
init { load() }
|
||||
|
||||
fun selectStatus(s: String) { _uiState.value = _uiState.value.copy(selectedStatus = s); refresh() }
|
||||
|
||||
fun refresh() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true, workOrders = emptyList())
|
||||
offset = 0
|
||||
val r = repository.list(status = _uiState.value.selectedStatus.ifEmpty { null })
|
||||
r.fold(
|
||||
onSuccess = { (list, _) -> _uiState.value = _uiState.value.copy(workOrders = list, isLoading = false, hasMore = list.size >= pageSize) },
|
||||
onFailure = { e -> _uiState.value = _uiState.value.copy(isLoading = false, errorMessage = e.message) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun loadMore() {
|
||||
if (_uiState.value.isLoadingMore || !_uiState.value.hasMore) return
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoadingMore = true)
|
||||
offset += pageSize
|
||||
val r = repository.list(status = _uiState.value.selectedStatus.ifEmpty { null }, offset = offset, limit = pageSize)
|
||||
r.fold(
|
||||
onSuccess = { (list, _) -> _uiState.value = _uiState.value.copy(workOrders = _uiState.value.workOrders + list, isLoadingMore = false, hasMore = list.size >= pageSize) },
|
||||
onFailure = { _uiState.value = _uiState.value.copy(isLoadingMore = false) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun load() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
repository.list().fold(
|
||||
onSuccess = { (list, _) -> _uiState.value = _uiState.value.copy(workOrders = list, isLoading = false, hasMore = list.size >= pageSize) },
|
||||
onFailure = { e -> _uiState.value = _uiState.value.copy(isLoading = false, errorMessage = e.message) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class Factory(private val repo: WorkOrderRepository) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T = WorkOrderListViewModel(repo) as T
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
#Wed Jun 24 20:47:38 CST 2026
|
||||
VERSION_CODE=23
|
||||
#Wed Jun 24 22:02:35 CST 2026
|
||||
VERSION_CODE=45
|
||||
VERSION_NAME=1.2
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.compose) apply false
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,4 +16,4 @@ org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
||||
# has changed. Additionally, Gradle applies performance optimizations to task execution.
|
||||
org.gradle.configuration-cache=true
|
||||
# Kotlin code style for this project: "official" or "obsolete":
|
||||
kotlin.code.style=official
|
||||
kotlin.code.style=official
|
||||
|
||||
@@ -8,6 +8,15 @@ lifecycleRuntimeKtx = "2.6.1"
|
||||
activityCompose = "1.8.0"
|
||||
kotlin = "2.2.10"
|
||||
composeBom = "2026.02.01"
|
||||
retrofit = "2.9.0"
|
||||
okhttp = "4.12.0"
|
||||
gson = "2.10.1"
|
||||
navigationCompose = "2.7.7"
|
||||
coil = "2.5.0"
|
||||
datastore = "1.0.0"
|
||||
camerax = "1.3.0"
|
||||
mlkitBarcode = "17.2.0"
|
||||
coroutines = "1.7.3"
|
||||
|
||||
[libraries]
|
||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||
@@ -24,8 +33,25 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u
|
||||
androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
|
||||
androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
|
||||
androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
|
||||
androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
|
||||
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||
|
||||
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
|
||||
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
|
||||
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
|
||||
okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" }
|
||||
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
|
||||
|
||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||
|
||||
camerax-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
|
||||
camerax-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
|
||||
camerax-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
|
||||
mlkit-barcode-scanning = { group = "com.google.mlkit", name = "barcode-scanning", version.ref = "mlkitBarcode" }
|
||||
|
||||
kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user