diff --git a/.gitignore b/.gitignore index 9baf0e4..554a47a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ node_modules/ dist/ coverage/ .pnp.* +*.tsbuildinfo *.iml *.log AndroidLibs/.gradle-home/ diff --git a/AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/composesdk/components/AccordionGroup.kt b/AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/composesdk/components/AccordionGroup.kt deleted file mode 100644 index 2bf7623..0000000 --- a/AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/composesdk/components/AccordionGroup.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.xuqm.sdk.compose.components - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.ExpandLess -import androidx.compose.material.icons.rounded.ExpandMore -import androidx.compose.material3.Card -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun AccordionGroup( - title: String, - modifier: Modifier = Modifier, - initiallyExpanded: Boolean = false, - content: @Composable () -> Unit, -) { - var expanded by remember { mutableStateOf(initiallyExpanded) } - - Card(modifier = modifier.fillMaxWidth()) { - Column { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { expanded = !expanded } - .padding(horizontal = 16.dp, vertical = 14.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text(title, style = MaterialTheme.typography.titleMedium) - Icon( - imageVector = if (expanded) Icons.Rounded.ExpandLess else Icons.Rounded.ExpandMore, - contentDescription = null, - ) - } - AnimatedVisibility(expanded) { - Column(modifier = Modifier.padding(16.dp)) { - content() - } - } - } - } -} diff --git a/AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/composesdk/components/FeatureCard.kt b/AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/composesdk/components/FeatureCard.kt deleted file mode 100644 index 23c45c1..0000000 --- a/AndroidLibs/commonsdk-compose/src/main/java/com/xuqm/composesdk/components/FeatureCard.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.xuqm.sdk.compose.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Card -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp - -@Composable -fun FeatureCard( - title: String, - description: String, - modifier: Modifier = Modifier, -) { - Card(modifier = modifier.fillMaxWidth()) { - Column( - modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text(title, style = MaterialTheme.typography.titleMedium) - Text(description, style = MaterialTheme.typography.bodyMedium) - } - } -} diff --git a/AndroidLibs/plugins/plugin-ui/src/main/java/com/xuqm/plugin/ui/PluginUiActivity.kt b/AndroidLibs/plugins/plugin-ui/src/main/java/com/xuqm/plugin/ui/PluginUiActivity.kt index f7a1062..475dede 100644 --- a/AndroidLibs/plugins/plugin-ui/src/main/java/com/xuqm/plugin/ui/PluginUiActivity.kt +++ b/AndroidLibs/plugins/plugin-ui/src/main/java/com/xuqm/plugin/ui/PluginUiActivity.kt @@ -12,6 +12,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -24,7 +26,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.xuqm.sdk.CoreSDK -import com.xuqm.sdk.compose.components.FeatureCard import com.xuqm.szyx.SzyxSDK import com.xuqm.szyx.auth.LoginSession import com.xuqm.szyx.auth.UserSessionManager @@ -97,14 +98,14 @@ private fun PluginUiScreen( } item { - FeatureCard( + PluginInfoCard( title = "插件独立运行", description = "plugin-ui 是独立 APK,可单独安装运行,也可由宿主通过包名直接拉起。", ) } item { - FeatureCard( + PluginInfoCard( title = "共享缓存", description = "通过 commonsdk-core 的 SharedCacheProvider 与宿主共享并更新用户会话。", ) @@ -112,3 +113,23 @@ private fun PluginUiScreen( } } } + +@Composable +private fun PluginInfoCard( + title: String, + description: String, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(title, style = MaterialTheme.typography.titleMedium) + Text(description, style = MaterialTheme.typography.bodyMedium) + } + } +} diff --git a/AndroidLibs/sample-app/src/main/java/com/xuqm/sample/MainActivity.kt b/AndroidLibs/sample-app/src/main/java/com/xuqm/sample/MainActivity.kt index e055e2a..1288fba 100644 --- a/AndroidLibs/sample-app/src/main/java/com/xuqm/sample/MainActivity.kt +++ b/AndroidLibs/sample-app/src/main/java/com/xuqm/sample/MainActivity.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text +import androidx.compose.material3.CardDefaults import androidx.compose.material3.TopAppBar import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -40,8 +41,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.lifecycleScope import com.xuqm.sdk.CoreSDK -import com.xuqm.sdk.compose.components.AccordionGroup -import com.xuqm.sdk.compose.components.FeatureCard +import com.xuqm.sdk.compose.components.accordion.AccordionPanel import com.xuqm.sdk.plugin.PluginPackageManager import com.xuqm.sdk.ui.ToastCenter import com.xuqm.sdk.update.DownloadState @@ -485,7 +485,7 @@ private fun SampleHome( } item { - AccordionGroup(title = "当前方案", initiallyExpanded = true) { + AccordionPanel(title = "当前方案", initiallyExpanded = true) { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("1. 未登录启动时直接进入 lib-szyx 登录页") Text("2. 插件和应用更新都在应用内直接下载,并输出实时进度") @@ -495,7 +495,7 @@ private fun SampleHome( } item { - FeatureCard( + InfoCard( title = "插件结构", description = "宿主 sample-app + 业务插件 plugin-ui,二者共享 commonsdk-core / commonsdk-compose / lib-szyx。", ) @@ -504,6 +504,26 @@ private fun SampleHome( } } +@Composable +private fun InfoCard( + title: String, + description: String, + modifier: Modifier = Modifier, +) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant), + ) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text(title, style = MaterialTheme.typography.titleMedium) + Text(description, style = MaterialTheme.typography.bodyMedium) + } + } +} + private fun DownloadState.toDisplayText(): String { return when (this) { DownloadState.Idle -> "未开始" diff --git a/frontend/ops-platform/src/api/client.ts b/frontend/ops-platform/src/api/client.ts index c1725fa..21f27cb 100644 --- a/frontend/ops-platform/src/api/client.ts +++ b/frontend/ops-platform/src/api/client.ts @@ -48,6 +48,7 @@ export interface ReleaseRecord { entryActivity?: string | null minHostVersionCode?: number | null minHostVersionName?: string | null + forceUpdate?: boolean status: 'DRAFT' | 'PUBLISHED' | 'GRAYSCALE' publishStrategy: string grayRule?: { @@ -100,9 +101,10 @@ export interface QuickSelection { const API_BASE_URL = import.meta.env.VITE_API_BASE_URL ?? 'http://127.0.0.1:8080' async function request(path: string, init?: RequestInit): Promise { + const isFormData = typeof FormData !== 'undefined' && init?.body instanceof FormData const response = await fetch(`${API_BASE_URL}${path}`, { headers: { - 'Content-Type': 'application/json', + ...(isFormData ? {} : { 'Content-Type': 'application/json' }), ...(init?.headers ?? {}), }, ...init, @@ -163,6 +165,20 @@ export const api = { body: JSON.stringify(payload), }) }, + uploadAppArtifact(appId: string, file: File) { + const formData = new FormData() + formData.append('file', file) + return request<{ + packageName: string + versionCode: number + versionName: string + uploadedFileName: string + downloadUrl: string + }>(`/api/v1/ops/apps/${appId}/packages/artifact`, { + method: 'POST', + body: formData, + }) + }, listPlugins(appId: string) { return request(`/api/v1/ops/apps/${appId}/plugins`) }, @@ -187,12 +203,37 @@ export const api = { body: JSON.stringify(payload), }) }, + uploadPluginArtifact(pluginId: string, file: File) { + const formData = new FormData() + formData.append('file', file) + return request<{ + packageName: string + versionCode: number + versionName: string + uploadedFileName: string + downloadUrl: string + }>(`/api/v1/ops/plugins/${pluginId}/packages/artifact`, { + method: 'POST', + body: formData, + }) + }, publishPackage(releaseId: string, payload: Record) { return request(`/api/v1/ops/packages/${releaseId}/publish`, { method: 'POST', body: JSON.stringify(payload), }) }, + updatePackage(releaseId: string, payload: Record) { + return request(`/api/v1/ops/packages/${releaseId}`, { + method: 'PUT', + body: JSON.stringify(payload), + }) + }, + deletePackage(releaseId: string) { + return request(`/api/v1/ops/packages/${releaseId}`, { + method: 'DELETE', + }) + }, listAudienceGroups() { return request('/api/v1/ops/version/audiences/groups') }, diff --git a/frontend/ops-platform/src/styles.css b/frontend/ops-platform/src/styles.css index 8b17225..cfcb103 100644 --- a/frontend/ops-platform/src/styles.css +++ b/frontend/ops-platform/src/styles.css @@ -37,10 +37,14 @@ select { display: flex; flex-direction: column; justify-content: space-between; + position: sticky; + top: 0; + height: 100vh; } .content { padding: 28px; + min-width: 0; } .nav { @@ -162,6 +166,11 @@ button { color: #163454; } +.danger { + background: #fff1f1; + color: #b42318; +} + .app-card { border-top: 1px solid rgba(16, 35, 61, 0.08); margin-top: 20px; @@ -200,6 +209,11 @@ button { color: white; } +.wide-tabs button[disabled] { + opacity: 0.45; + cursor: not-allowed; +} + .sub-panel { margin-top: 20px; padding-top: 20px; @@ -248,6 +262,26 @@ button { margin-top: 12px; } +.modal-mask { + position: fixed; + inset: 0; + background: rgba(7, 19, 37, 0.38); + display: grid; + place-items: center; + padding: 24px; + z-index: 40; +} + +.modal-card { + width: min(960px, 100%); + max-height: 80vh; + overflow: auto; + background: white; + border-radius: 24px; + padding: 24px; + box-shadow: 0 24px 60px rgba(15, 39, 71, 0.18); +} + @media (max-width: 1100px) { .shell, .stack { @@ -256,5 +290,7 @@ button { .sidebar { gap: 24px; + position: static; + height: auto; } } diff --git a/frontend/ops-platform/src/views/AppManagementView.vue b/frontend/ops-platform/src/views/AppManagementView.vue index b9553a9..4bfcdde 100644 --- a/frontend/ops-platform/src/views/AppManagementView.vue +++ b/frontend/ops-platform/src/views/AppManagementView.vue @@ -1,269 +1,600 @@