init
这个提交包含在:
父节点
6e44428e8a
当前提交
0314acc18e
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
.idea/
|
||||
.m2/
|
||||
.yarn/
|
||||
.gradle/
|
||||
.kotlin/
|
||||
build/
|
||||
@ -8,6 +9,7 @@ target/
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.pnp.*
|
||||
*.iml
|
||||
*.log
|
||||
AndroidLibs/.gradle-home/
|
||||
|
||||
@ -4,8 +4,9 @@
|
||||
|
||||
## 模块结构
|
||||
|
||||
- `commonsdk-core`: SDK 核心,承载网络、共享缓存、插件管理、App 更新、设备信息与时间工具。
|
||||
- `commonsdk-compose`: Compose 扩展组件。
|
||||
- `commonsdk-core`: SDK 核心,承载网络、下载、文件、权限、缓存、日志、全局弹窗、设备与时间等基础能力。
|
||||
- `commonsdk-compose`: Compose 扩展组件,提供下拉刷新、折叠面板、滑动操作、WebView、图片等业务可复用组件。
|
||||
- `commonsdk-update`: 更新 SDK,独立承载 APK 更新、插件更新、版本比较与安装编排。
|
||||
- `lib-szyx`: 项目专属 SDK,承载真实登录接口、签名、业务 Header 与会话管理。
|
||||
- `sample-app`: 示例宿主应用。
|
||||
- `plugins/plugin-ui`: UI 演示插件,可独立运行,也可被宿主拉起。
|
||||
@ -48,6 +49,10 @@ nexus.password=your-password
|
||||
- `commonsdk-core` 提供:
|
||||
- `HttpManager / RetrofitManager`
|
||||
- `SharedCacheManager / SharedCacheProvider`
|
||||
- `DownloadManager / FileHelper`
|
||||
- `PermissionManager / GlobalCache / Logger / DialogCenter`
|
||||
- `commonsdk-update` 提供:
|
||||
- `UpdateSDK`
|
||||
- `PluginPackageManager`
|
||||
- `AppUpdater`
|
||||
- `lib-szyx` 提供:
|
||||
|
||||
@ -32,8 +32,11 @@ dependencies {
|
||||
api(project(":commonsdk-core"))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(libs.androidx.activity.ktx)
|
||||
implementation(libs.androidx.webkit)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
debugImplementation(libs.bundles.compose.debug)
|
||||
}
|
||||
|
||||
@ -1,3 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest />
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="com.xuqm.sdk.compose.activity.WebViewPageActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="false" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
package com.xuqm.sdk.compose.activity
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import com.xuqm.sdk.compose.components.webview.CommonWebView
|
||||
import com.xuqm.sdk.compose.components.webview.WebViewConfig
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
||||
class WebViewPageActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val url = intent.getStringExtra(EXTRA_URL).orEmpty()
|
||||
val title = intent.getStringExtra(EXTRA_TITLE)
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
CommonWebView(
|
||||
url = url,
|
||||
config = WebViewConfig(title = title),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_URL = "extra_url"
|
||||
private const val EXTRA_TITLE = "extra_title"
|
||||
|
||||
fun createIntent(
|
||||
context: Context,
|
||||
url: String,
|
||||
title: String? = null,
|
||||
): Intent {
|
||||
return Intent(context, WebViewPageActivity::class.java).apply {
|
||||
putExtra(EXTRA_URL, url)
|
||||
putExtra(EXTRA_TITLE, title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
package com.xuqm.sdk.compose.components.accordion
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.foundation.lazy.rememberLazyListState
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.xuqm.sdk.compose.components.refresh.RefreshableLazyColumn
|
||||
|
||||
data class AccordionItem<T>(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val data: T,
|
||||
val initiallyExpanded: Boolean = false,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AccordionPanel(
|
||||
title: String,
|
||||
modifier: Modifier = Modifier,
|
||||
initiallyExpanded: Boolean = false,
|
||||
headerBackground: Color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(initiallyExpanded) }
|
||||
Card(modifier = modifier.fillMaxWidth()) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(headerBackground)
|
||||
.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) {
|
||||
Box(modifier = Modifier.padding(16.dp)) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> RefreshableAccordionList(
|
||||
items: List<AccordionItem<T>>,
|
||||
isRefreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
itemContent: @Composable (AccordionItem<T>) -> Unit,
|
||||
) {
|
||||
RefreshableLazyColumn(
|
||||
items = items,
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
modifier = modifier,
|
||||
listState = rememberLazyListState(),
|
||||
itemContent = { _, item ->
|
||||
AccordionPanel(
|
||||
title = item.title,
|
||||
initiallyExpanded = item.initiallyExpanded,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
) {
|
||||
itemContent(item)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package com.xuqm.sdk.compose.components.image
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.ImageRequest
|
||||
|
||||
enum class ImageAdaptMode {
|
||||
WIDTH_FIXED,
|
||||
HEIGHT_FIXED,
|
||||
BOTH_FIXED,
|
||||
FILL_WIDTH,
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AdaptiveImage(
|
||||
model: Any?,
|
||||
modifier: Modifier = Modifier,
|
||||
adaptMode: ImageAdaptMode = ImageAdaptMode.FILL_WIDTH,
|
||||
width: Dp? = null,
|
||||
height: Dp? = null,
|
||||
aspectRatio: Float = 1f,
|
||||
shape: Shape = RoundedCornerShape(16.dp),
|
||||
contentScale: ContentScale = ContentScale.Crop,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
val imageModifier = when (adaptMode) {
|
||||
ImageAdaptMode.WIDTH_FIXED -> Modifier.width(width ?: 160.dp).aspectRatio(aspectRatio)
|
||||
ImageAdaptMode.HEIGHT_FIXED -> Modifier.height(height ?: 160.dp).aspectRatio(aspectRatio)
|
||||
ImageAdaptMode.BOTH_FIXED -> Modifier.width(width ?: 160.dp).height(height ?: 160.dp)
|
||||
ImageAdaptMode.FILL_WIDTH -> Modifier.fillMaxWidth().aspectRatio(aspectRatio)
|
||||
}
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(model).crossfade(true).build(),
|
||||
contentDescription = contentDescription,
|
||||
modifier = modifier.then(imageModifier).clip(shape),
|
||||
contentScale = contentScale,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CircleImage(
|
||||
model: Any?,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 72.dp,
|
||||
borderWidth: Dp = 0.dp,
|
||||
borderColor: Color = Color.Transparent,
|
||||
maskColor: Color = Color.Transparent,
|
||||
overlayBrush: Brush? = null,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.width(size)
|
||||
.height(size)
|
||||
.clip(CircleShape)
|
||||
.border(borderWidth, borderColor, CircleShape)
|
||||
.background(borderColor),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
AsyncImage(
|
||||
model = ImageRequest.Builder(LocalContext.current).data(model).crossfade(true).build(),
|
||||
contentDescription = contentDescription,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
if (maskColor != Color.Transparent || overlayBrush != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape)
|
||||
.background(overlayBrush ?: Brush.verticalGradient(listOf(maskColor, maskColor))),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.xuqm.sdk.compose.components.refresh
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SwipeRefreshContainer(
|
||||
isRefreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val refreshState = rememberPullToRefreshState()
|
||||
PullToRefreshBox(
|
||||
state = refreshState,
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> RefreshableLazyColumn(
|
||||
items: List<T>,
|
||||
isRefreshing: Boolean,
|
||||
onRefresh: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
listState: LazyListState = rememberLazyListState(),
|
||||
enableLoadMore: Boolean = false,
|
||||
isLoadingMore: Boolean = false,
|
||||
hasMore: Boolean = true,
|
||||
onLoadMore: () -> Unit = {},
|
||||
emptyContent: (@Composable BoxScope.() -> Unit)? = null,
|
||||
itemContent: @Composable (index: Int, item: T) -> Unit,
|
||||
) {
|
||||
val shouldLoadMore by remember(items, enableLoadMore, isLoadingMore, hasMore, listState) {
|
||||
derivedStateOf {
|
||||
if (!enableLoadMore || isLoadingMore || !hasMore) return@derivedStateOf false
|
||||
val layoutInfo = listState.layoutInfo
|
||||
val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||
lastVisible >= items.lastIndex - 1 && items.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(shouldLoadMore) {
|
||||
if (shouldLoadMore) {
|
||||
onLoadMore()
|
||||
}
|
||||
}
|
||||
|
||||
SwipeRefreshContainer(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = onRefresh,
|
||||
modifier = modifier,
|
||||
) {
|
||||
if (items.isEmpty() && emptyContent != null) {
|
||||
Box(modifier = Modifier.fillMaxSize(), content = emptyContent)
|
||||
} else {
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
state = listState,
|
||||
) {
|
||||
itemsIndexed(items) { index, item ->
|
||||
itemContent(index, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package com.xuqm.sdk.compose.components.swipe
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
data class SwipeAction(
|
||||
val label: String,
|
||||
val backgroundColor: Color,
|
||||
val contentColor: Color = Color.White,
|
||||
val onClick: () -> Unit,
|
||||
val content: (@Composable () -> Unit)? = null,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SwipeActionItem(
|
||||
modifier: Modifier = Modifier,
|
||||
actions: List<SwipeAction>,
|
||||
actionWidthDp: Int = 88,
|
||||
content: @Composable () -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val density = LocalDensity.current
|
||||
var offsetX by remember { mutableFloatStateOf(0f) }
|
||||
val maxOffset = with(density) { -(actions.size * actionWidthDp.dp.toPx()) }
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.fillMaxHeight(),
|
||||
horizontalArrangement = Arrangement.End,
|
||||
) {
|
||||
actions.forEach { action ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.width(actionWidthDp.dp)
|
||||
.fillMaxHeight()
|
||||
.background(action.backgroundColor)
|
||||
.clickable {
|
||||
scope.launch { offsetX = 0f }
|
||||
action.onClick()
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (action.content != null) {
|
||||
action.content.invoke()
|
||||
} else {
|
||||
Text(
|
||||
text = action.label,
|
||||
color = action.contentColor,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
modifier = Modifier.padding(8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.offset { IntOffset(offsetX.roundToInt(), 0) }
|
||||
.pointerInput(actions) {
|
||||
detectHorizontalDragGestures(
|
||||
onHorizontalDrag = { _, dragAmount ->
|
||||
offsetX = (offsetX + dragAmount).coerceIn(maxOffset, 0f)
|
||||
},
|
||||
onDragEnd = {
|
||||
scope.launch {
|
||||
offsetX = if (offsetX < maxOffset / 2) maxOffset else 0f
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
.background(MaterialTheme.colorScheme.surface, RoundedCornerShape(16.dp)),
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
package com.xuqm.sdk.compose.components.webview
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
||||
data class WebViewConfig(
|
||||
val title: String? = null,
|
||||
val javaScriptEnabled: Boolean = true,
|
||||
val domStorageEnabled: Boolean = true,
|
||||
val userAgentString: String? = null,
|
||||
)
|
||||
|
||||
interface WebViewCallback {
|
||||
fun onPageStarted(url: String?) {}
|
||||
fun onPageFinished(url: String?) {}
|
||||
fun onProgressChanged(progress: Int) {}
|
||||
fun onReceivedTitle(title: String?) {}
|
||||
fun shouldOverrideUrlLoading(url: String?): Boolean = false
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CommonWebView(
|
||||
url: String,
|
||||
modifier: Modifier = Modifier,
|
||||
config: WebViewConfig = WebViewConfig(),
|
||||
callback: WebViewCallback? = null,
|
||||
onClose: (() -> Unit)? = null,
|
||||
) {
|
||||
var webView: WebView? by remember { mutableStateOf(null) }
|
||||
var title by remember { mutableStateOf(config.title ?: "") }
|
||||
var progress by remember { mutableIntStateOf(0) }
|
||||
|
||||
BackHandler(enabled = webView?.canGoBack() == true) {
|
||||
webView?.goBack()
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose { webView?.destroy() }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(if (title.isBlank()) url else title) },
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
AndroidView(
|
||||
factory = { context ->
|
||||
WebView(context).apply {
|
||||
settings.javaScriptEnabled = config.javaScriptEnabled
|
||||
settings.domStorageEnabled = config.domStorageEnabled
|
||||
settings.cacheMode = WebSettings.LOAD_DEFAULT
|
||||
config.userAgentString?.let { settings.userAgentString = it }
|
||||
webViewClient = object : WebViewClient() {
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
callback?.onPageStarted(url)
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean {
|
||||
return callback?.shouldOverrideUrlLoading(request?.url?.toString()) ?: false
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
callback?.onPageFinished(url)
|
||||
}
|
||||
}
|
||||
webChromeClient = object : WebChromeClient() {
|
||||
override fun onReceivedTitle(view: WebView?, newTitle: String?) {
|
||||
title = newTitle ?: config.title.orEmpty()
|
||||
callback?.onReceivedTitle(newTitle)
|
||||
}
|
||||
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
progress = newProgress
|
||||
callback?.onProgressChanged(newProgress)
|
||||
}
|
||||
}
|
||||
loadUrl(url)
|
||||
webView = this
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.fillMaxSize(),
|
||||
update = { view ->
|
||||
if (view.url != url) {
|
||||
view.loadUrl(url)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ kotlin {
|
||||
|
||||
dependencies {
|
||||
api(libs.androidx.core.ktx)
|
||||
api(libs.androidx.activity.ktx)
|
||||
api(libs.bundles.network)
|
||||
api(libs.kotlinx.serialization.json)
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
@ -1,17 +1,24 @@
|
||||
package com.xuqm.sdk
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import com.xuqm.sdk.cache.GlobalCache
|
||||
import com.xuqm.sdk.network.HttpConfig
|
||||
import com.xuqm.sdk.network.HttpManager
|
||||
import com.xuqm.sdk.plugin.PluginPackageManager
|
||||
import com.xuqm.sdk.update.AppUpdater
|
||||
import com.xuqm.sdk.ui.DialogCenter
|
||||
import com.xuqm.sdk.ui.ToastCenter
|
||||
import com.xuqm.sdk.update.DownloadManager
|
||||
import com.xuqm.sdk.utils.DeviceUtils
|
||||
import com.xuqm.sdk.utils.FileHelper
|
||||
import com.xuqm.sdk.utils.Logger
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
object CoreSDK {
|
||||
private var appContext: Context? = null
|
||||
private var config: SDKConfig = SDKConfig()
|
||||
private var topActivityRef: WeakReference<Activity>? = null
|
||||
|
||||
data class SDKConfig(
|
||||
val debugMode: Boolean = false,
|
||||
@ -20,20 +27,66 @@ object CoreSDK {
|
||||
|
||||
fun init(context: Context, config: SDKConfig = SDKConfig()) {
|
||||
if (appContext != null) return
|
||||
appContext = context.applicationContext
|
||||
val applicationContext = context.applicationContext
|
||||
appContext = applicationContext
|
||||
this.config = config
|
||||
HttpManager.init(HttpConfig(debugMode = config.debugMode))
|
||||
Logger.init(config.debugMode)
|
||||
GlobalCache.init(applicationContext)
|
||||
ToastCenter.init(applicationContext)
|
||||
DialogCenter.init(applicationContext)
|
||||
FileHelper.init(applicationContext)
|
||||
registerLifecycleCallbacks(applicationContext)
|
||||
}
|
||||
|
||||
fun context(): Context = requireNotNull(appContext) { "CoreSDK not initialized" }
|
||||
|
||||
fun pluginPackageManager(): PluginPackageManager = PluginPackageManager.getInstance(context())
|
||||
fun config(): SDKConfig = config
|
||||
|
||||
fun currentActivity(): Activity? = topActivityRef?.get()
|
||||
|
||||
fun downloadManager(): DownloadManager = DownloadManager.getInstance(context())
|
||||
|
||||
fun appUpdater(): AppUpdater = AppUpdater.getInstance(context())
|
||||
fun fileHelper(): FileHelper = FileHelper
|
||||
|
||||
fun cache(): GlobalCache = GlobalCache
|
||||
|
||||
fun logger(): Logger = Logger
|
||||
|
||||
fun dialogCenter(): DialogCenter = DialogCenter
|
||||
|
||||
fun deviceId(): String = DeviceUtils.getDeviceId(context())
|
||||
|
||||
fun deviceInfo() = DeviceUtils.getDeviceInfo(context())
|
||||
|
||||
private fun registerLifecycleCallbacks(context: Context) {
|
||||
val application = context as? Application ?: return
|
||||
application.registerActivityLifecycleCallbacks(
|
||||
object : Application.ActivityLifecycleCallbacks {
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
topActivityRef = WeakReference(activity)
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) {
|
||||
topActivityRef = WeakReference(activity)
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
topActivityRef = WeakReference(activity)
|
||||
}
|
||||
|
||||
override fun onActivityPaused(activity: Activity) = Unit
|
||||
|
||||
override fun onActivityStopped(activity: Activity) = Unit
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
if (topActivityRef?.get() === activity) {
|
||||
topActivityRef = null
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
package com.xuqm.sdk.cache
|
||||
|
||||
import android.content.Context
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
object GlobalCache {
|
||||
private const val DEFAULT_TTL = 24 * 60 * 60 * 1000L
|
||||
private var cacheManager: SharedCacheManager? = null
|
||||
|
||||
internal fun init(context: Context) {
|
||||
if (cacheManager == null) {
|
||||
cacheManager = SharedCacheManager.getInstance(context.applicationContext)
|
||||
}
|
||||
}
|
||||
|
||||
fun putString(key: String, value: String, ttl: Long = DEFAULT_TTL) {
|
||||
requireManager().put(key, value, ttl)
|
||||
}
|
||||
|
||||
fun getString(key: String, defaultValue: String? = null): String? {
|
||||
return requireManager().getSync(key) ?: defaultValue
|
||||
}
|
||||
|
||||
fun putBoolean(key: String, value: Boolean, ttl: Long = DEFAULT_TTL) {
|
||||
putString(key, value.toString(), ttl)
|
||||
}
|
||||
|
||||
fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
|
||||
return getString(key)?.toBooleanStrictOrNull() ?: defaultValue
|
||||
}
|
||||
|
||||
fun putInt(key: String, value: Int, ttl: Long = DEFAULT_TTL) {
|
||||
putString(key, value.toString(), ttl)
|
||||
}
|
||||
|
||||
fun getInt(key: String, defaultValue: Int = 0): Int {
|
||||
return getString(key)?.toIntOrNull() ?: defaultValue
|
||||
}
|
||||
|
||||
fun putLong(key: String, value: Long, ttl: Long = DEFAULT_TTL) {
|
||||
putString(key, value.toString(), ttl)
|
||||
}
|
||||
|
||||
fun getLong(key: String, defaultValue: Long = 0L): Long {
|
||||
return getString(key)?.toLongOrNull() ?: defaultValue
|
||||
}
|
||||
|
||||
fun putJson(key: String, value: JSONObject, ttl: Long = DEFAULT_TTL) {
|
||||
putString(key, value.toString(), ttl)
|
||||
}
|
||||
|
||||
fun getJson(key: String): JSONObject? {
|
||||
return getString(key)?.let { runCatching { JSONObject(it) }.getOrNull() }
|
||||
}
|
||||
|
||||
fun putJsonArray(key: String, value: JSONArray, ttl: Long = DEFAULT_TTL) {
|
||||
putString(key, value.toString(), ttl)
|
||||
}
|
||||
|
||||
fun getJsonArray(key: String): JSONArray? {
|
||||
return getString(key)?.let { runCatching { JSONArray(it) }.getOrNull() }
|
||||
}
|
||||
|
||||
fun remove(key: String) {
|
||||
requireManager().remove(key)
|
||||
}
|
||||
|
||||
private fun requireManager(): SharedCacheManager {
|
||||
return requireNotNull(cacheManager) { "GlobalCache not initialized. Call CoreSDK.init() first." }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
package com.xuqm.sdk.communication
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.util.UUID
|
||||
|
||||
data class PluginMessage(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val from: String,
|
||||
val to: String? = null,
|
||||
val topic: String,
|
||||
val payload: String? = null,
|
||||
val timestamp: Long = System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
object PluginMessenger {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val bus = MutableSharedFlow<PluginMessage>(extraBufferCapacity = 64)
|
||||
|
||||
fun send(
|
||||
from: String,
|
||||
topic: String,
|
||||
payload: String? = null,
|
||||
to: String? = null,
|
||||
) {
|
||||
bus.tryEmit(
|
||||
PluginMessage(
|
||||
from = from,
|
||||
to = to,
|
||||
topic = topic,
|
||||
payload = payload,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun subscribe(
|
||||
owner: String,
|
||||
topic: String? = null,
|
||||
onReceive: (PluginMessage) -> Unit,
|
||||
): Job {
|
||||
return bus
|
||||
.filter { message ->
|
||||
val topicMatches = topic == null || topic == message.topic
|
||||
val targetMatches = message.to.isNullOrBlank() || message.to == owner
|
||||
topicMatches && targetMatches
|
||||
}
|
||||
.onEach(onReceive)
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
@ -29,7 +29,7 @@ class RetrofitManager private constructor() {
|
||||
private val serviceCache = ConcurrentHashMap<String, Any>()
|
||||
private var globalConfig: HttpConfig = HttpConfig()
|
||||
|
||||
fun init(config: HttpConfig = HttpConfig()) {
|
||||
internal fun init(config: HttpConfig = HttpConfig()) {
|
||||
globalConfig = config
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ class RetrofitManager private constructor() {
|
||||
}
|
||||
|
||||
object HttpManager {
|
||||
fun init(config: HttpConfig = HttpConfig()) {
|
||||
internal fun init(config: HttpConfig = HttpConfig()) {
|
||||
RetrofitManager.getInstance().init(config)
|
||||
}
|
||||
|
||||
@ -83,4 +83,3 @@ object HttpManager {
|
||||
return RetrofitManager.getInstance().getService(baseUrl, serviceClass, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,81 @@
|
||||
package com.xuqm.sdk.permission
|
||||
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import com.xuqm.sdk.ui.DialogCenter
|
||||
|
||||
data class PermissionRequest(
|
||||
val permission: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
)
|
||||
|
||||
data class PermissionResult(
|
||||
val granted: List<String>,
|
||||
val denied: List<String>,
|
||||
) {
|
||||
fun allGranted(): Boolean = denied.isEmpty()
|
||||
}
|
||||
|
||||
class PermissionManager private constructor(
|
||||
private val launcher: ActivityResultLauncher<Array<String>>,
|
||||
private val callback: (PermissionResult) -> Unit,
|
||||
) {
|
||||
private var pendingRequests: List<PermissionRequest> = emptyList()
|
||||
|
||||
fun request(
|
||||
requests: List<PermissionRequest>,
|
||||
confirmTitle: String = "权限申请说明",
|
||||
confirmButton: String = "继续申请",
|
||||
cancelButton: String = "取消",
|
||||
onCancelled: (() -> Unit)? = null,
|
||||
) {
|
||||
if (requests.isEmpty()) {
|
||||
callback(PermissionResult(emptyList(), emptyList()))
|
||||
return
|
||||
}
|
||||
pendingRequests = requests
|
||||
val message = buildString {
|
||||
appendLine("应用将申请以下权限:")
|
||||
requests.forEach {
|
||||
appendLine()
|
||||
append("• ")
|
||||
append(it.title)
|
||||
append(":")
|
||||
append(it.description)
|
||||
}
|
||||
}.trim()
|
||||
val shown = DialogCenter.showConfirm(
|
||||
title = confirmTitle,
|
||||
message = message,
|
||||
confirmText = confirmButton,
|
||||
cancelText = cancelButton,
|
||||
cancelable = true,
|
||||
canceledOnTouchOutside = false,
|
||||
onConfirm = {
|
||||
launcher.launch(requests.map { it.permission }.toTypedArray())
|
||||
},
|
||||
onCancel = onCancelled,
|
||||
)
|
||||
if (!shown) {
|
||||
onCancelled?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun register(
|
||||
caller: ActivityResultCaller,
|
||||
onResult: (PermissionResult) -> Unit,
|
||||
): PermissionManager {
|
||||
lateinit var manager: PermissionManager
|
||||
val launcher = caller.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
|
||||
val granted = manager.pendingRequests.map { it.permission }.filter { result[it] == true }
|
||||
val denied = manager.pendingRequests.map { it.permission }.filterNot { result[it] == true }
|
||||
onResult(PermissionResult(granted = granted, denied = denied))
|
||||
}
|
||||
manager = PermissionManager(launcher = launcher, callback = onResult)
|
||||
return manager
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
package com.xuqm.sdk.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import com.xuqm.sdk.CoreSDK
|
||||
|
||||
object DialogCenter {
|
||||
data class LoadingConfig(
|
||||
val message: String = "加载中...",
|
||||
val cancelable: Boolean = false,
|
||||
val canceledOnTouchOutside: Boolean = false,
|
||||
val onUserCancel: (() -> Unit)? = null,
|
||||
)
|
||||
|
||||
private var appContext: Context? = null
|
||||
private var currentDialog: AlertDialog? = null
|
||||
private var loadingDialog: Dialog? = null
|
||||
|
||||
internal fun init(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
|
||||
fun showConfirm(
|
||||
title: String,
|
||||
message: String,
|
||||
confirmText: String = "确定",
|
||||
cancelText: String = "取消",
|
||||
cancelable: Boolean = true,
|
||||
canceledOnTouchOutside: Boolean = true,
|
||||
onConfirm: () -> Unit,
|
||||
onCancel: (() -> Unit)? = null,
|
||||
): Boolean {
|
||||
val activity = currentActivity() ?: return false
|
||||
activity.runOnUiThread {
|
||||
dismissCurrentDialog()
|
||||
currentDialog = AlertDialog.Builder(activity)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(confirmText) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onConfirm()
|
||||
}
|
||||
.setNegativeButton(cancelText) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
onCancel?.invoke()
|
||||
}
|
||||
.create()
|
||||
.apply {
|
||||
setCancelable(cancelable)
|
||||
setCanceledOnTouchOutside(canceledOnTouchOutside)
|
||||
show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun showLoading(config: LoadingConfig = LoadingConfig()): Boolean {
|
||||
val activity = currentActivity() ?: return false
|
||||
activity.runOnUiThread {
|
||||
dismissLoading()
|
||||
loadingDialog = Dialog(activity).apply {
|
||||
setCancelable(config.cancelable)
|
||||
setCanceledOnTouchOutside(config.canceledOnTouchOutside)
|
||||
setOnCancelListener { config.onUserCancel?.invoke() }
|
||||
setContentView(
|
||||
LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
val padding = 48
|
||||
setPadding(padding, padding, padding, padding)
|
||||
addView(ProgressBar(activity))
|
||||
addView(
|
||||
TextView(activity).apply {
|
||||
text = config.message
|
||||
textSize = 16f
|
||||
setPadding(24, 0, 0, 0)
|
||||
},
|
||||
)
|
||||
},
|
||||
)
|
||||
show()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun dismissLoading() {
|
||||
loadingDialog?.dismiss()
|
||||
loadingDialog = null
|
||||
}
|
||||
|
||||
fun dismissCurrentDialog() {
|
||||
currentDialog?.dismiss()
|
||||
currentDialog = null
|
||||
}
|
||||
|
||||
private fun currentActivity(): Activity? = CoreSDK.currentActivity()
|
||||
}
|
||||
@ -9,7 +9,7 @@ object ToastCenter {
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var appContext: Context? = null
|
||||
|
||||
fun init(context: Context) {
|
||||
internal fun init(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
package com.xuqm.sdk.update
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import androidx.core.content.FileProvider
|
||||
import com.xuqm.sdk.utils.FileHelper
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -159,20 +156,7 @@ class DownloadManager private constructor(private val context: Context) {
|
||||
}
|
||||
|
||||
fun installApk(file: File): Boolean {
|
||||
return runCatching {
|
||||
val authority = "${context.packageName}.sdk.fileprovider"
|
||||
val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
FileProvider.getUriForFile(context, authority, file)
|
||||
} else {
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
},
|
||||
)
|
||||
}.isSuccess
|
||||
return FileHelper.installApk(file)
|
||||
}
|
||||
|
||||
private fun resolvePath(storagePath: StoragePath, customPath: String?): String {
|
||||
|
||||
@ -16,5 +16,38 @@ object DateTimeUtils {
|
||||
}
|
||||
|
||||
fun now(pattern: String = "yyyy-MM-dd HH:mm:ss"): String = format(System.currentTimeMillis(), pattern)
|
||||
}
|
||||
|
||||
fun nowMillis(): Long = System.currentTimeMillis()
|
||||
|
||||
fun parse(
|
||||
value: String,
|
||||
pattern: String = "yyyy-MM-dd HH:mm:ss",
|
||||
timeZone: TimeZone = TimeZone.getDefault(),
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): Long? {
|
||||
return runCatching {
|
||||
SimpleDateFormat(pattern, locale).apply { this.timeZone = timeZone }.parse(value)?.time
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun isToday(timeMillis: Long): Boolean {
|
||||
val today = format(System.currentTimeMillis(), "yyyy-MM-dd")
|
||||
return format(timeMillis, "yyyy-MM-dd") == today
|
||||
}
|
||||
|
||||
fun addDays(timeMillis: Long, days: Int): Long = timeMillis + days * 24L * 60L * 60L * 1000L
|
||||
|
||||
fun addHours(timeMillis: Long, hours: Int): Long = timeMillis + hours * 60L * 60L * 1000L
|
||||
|
||||
fun between(startMillis: Long, endMillis: Long): Long = endMillis - startMillis
|
||||
|
||||
fun toFriendlyText(timeMillis: Long): String {
|
||||
val delta = System.currentTimeMillis() - timeMillis
|
||||
return when {
|
||||
delta < 60_000L -> "刚刚"
|
||||
delta < 60L * 60L * 1000L -> "${delta / 60_000L}分钟前"
|
||||
delta < 24L * 60L * 60L * 1000L -> "${delta / (60L * 60L * 1000L)}小时前"
|
||||
else -> format(timeMillis, "yyyy-MM-dd")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
package com.xuqm.sdk.utils
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.provider.MediaStore
|
||||
import androidx.activity.result.ActivityResultCaller
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.content.FileProvider
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
object FileHelper {
|
||||
private var appContext: Context? = null
|
||||
|
||||
internal fun init(context: Context) {
|
||||
appContext = context.applicationContext
|
||||
}
|
||||
|
||||
fun installApk(file: File): Boolean {
|
||||
val context = requireContext()
|
||||
return runCatching {
|
||||
val uri = toUri(file)
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
},
|
||||
)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
fun openFile(file: File, mimeType: String = "*/*"): Boolean {
|
||||
val context = requireContext()
|
||||
return runCatching {
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
setDataAndType(toUri(file), mimeType)
|
||||
},
|
||||
)
|
||||
}.isSuccess
|
||||
}
|
||||
|
||||
fun createTempImageFile(prefix: String = "IMG"): File {
|
||||
val context = requireContext()
|
||||
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
|
||||
val directory = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) ?: context.cacheDir
|
||||
return File(directory, "${prefix}_${timeStamp}.jpg").apply {
|
||||
parentFile?.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
fun toUri(file: File): Uri {
|
||||
val context = requireContext()
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
FileProvider.getUriForFile(context, "${context.packageName}.sdk.fileprovider", file)
|
||||
} else {
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
fun registerFilePicker(
|
||||
caller: ActivityResultCaller,
|
||||
onResult: (Uri?) -> Unit,
|
||||
): ActivityResultLauncher<Array<String>> {
|
||||
return caller.registerForActivityResult(ActivityResultContracts.OpenDocument(), onResult)
|
||||
}
|
||||
|
||||
fun registerImagePicker(
|
||||
caller: ActivityResultCaller,
|
||||
onResult: (Uri?) -> Unit,
|
||||
): ActivityResultLauncher<PickVisualMediaRequest> {
|
||||
return caller.registerForActivityResult(ActivityResultContracts.PickVisualMedia(), onResult)
|
||||
}
|
||||
|
||||
fun registerCameraCapture(
|
||||
caller: ActivityResultCaller,
|
||||
outputFileProvider: () -> File = { createTempImageFile() },
|
||||
onResult: (Boolean, Uri?) -> Unit,
|
||||
): Pair<ActivityResultLauncher<Uri>, () -> Uri> {
|
||||
var pendingUri: Uri? = null
|
||||
val launcher = caller.registerForActivityResult(ActivityResultContracts.TakePicture()) { success ->
|
||||
onResult(success, pendingUri)
|
||||
}
|
||||
val uriProvider = {
|
||||
pendingUri = toUri(outputFileProvider())
|
||||
requireNotNull(pendingUri)
|
||||
}
|
||||
return launcher to uriProvider
|
||||
}
|
||||
|
||||
fun createImageCaptureIntent(outputUri: Uri): Intent {
|
||||
return Intent(MediaStore.ACTION_IMAGE_CAPTURE).apply {
|
||||
putExtra(MediaStore.EXTRA_OUTPUT, outputUri)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireContext(): Context {
|
||||
return requireNotNull(appContext) { "FileHelper not initialized. Call CoreSDK.init() first." }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
package com.xuqm.sdk.utils
|
||||
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
|
||||
object Logger {
|
||||
private var debugEnabled: Boolean = false
|
||||
private const val DEFAULT_TAG = "CoreSDK"
|
||||
|
||||
internal fun init(debug: Boolean) {
|
||||
debugEnabled = debug
|
||||
}
|
||||
|
||||
fun d(message: Any?, tag: String = DEFAULT_TAG) {
|
||||
if (debugEnabled) {
|
||||
Log.d(tag, stringify(message))
|
||||
}
|
||||
}
|
||||
|
||||
fun i(message: Any?, tag: String = DEFAULT_TAG) {
|
||||
Log.i(tag, stringify(message))
|
||||
}
|
||||
|
||||
fun w(message: Any?, tag: String = DEFAULT_TAG) {
|
||||
Log.w(tag, stringify(message))
|
||||
}
|
||||
|
||||
fun e(message: Any?, throwable: Throwable? = null, tag: String = DEFAULT_TAG) {
|
||||
Log.e(tag, stringify(message), throwable)
|
||||
}
|
||||
|
||||
fun json(json: String, tag: String = DEFAULT_TAG) {
|
||||
val pretty = runCatching {
|
||||
when {
|
||||
json.trim().startsWith("{") -> JSONObject(json).toString(2)
|
||||
json.trim().startsWith("[") -> JSONArray(json).toString(2)
|
||||
else -> json
|
||||
}
|
||||
}.getOrDefault(json)
|
||||
Log.d(tag, pretty)
|
||||
}
|
||||
|
||||
private fun stringify(message: Any?): String = when (message) {
|
||||
null -> "null"
|
||||
is Throwable -> message.stackTraceToString()
|
||||
else -> message.toString()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.library)
|
||||
}
|
||||
|
||||
apply(from = rootProject.file("gradle/publishing.gradle.kts"))
|
||||
|
||||
android {
|
||||
namespace = "com.xuqm.sdk.updatekit"
|
||||
compileSdk = libs.versions.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
minSdk = libs.versions.minSdk.get().toInt()
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvmToolchain(21)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":commonsdk-core"))
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
|
||||
testImplementation(libs.junit4)
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
|
||||
@ -3,11 +3,9 @@ package com.xuqm.sdk.plugin
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.core.content.FileProvider
|
||||
import com.xuqm.sdk.cache.CacheKeys
|
||||
import com.xuqm.sdk.cache.SharedCacheManager
|
||||
import com.xuqm.sdk.cache.GlobalCache
|
||||
import com.xuqm.sdk.update.DownloadDecision
|
||||
import com.xuqm.sdk.update.DownloadManager
|
||||
import com.xuqm.sdk.update.DownloadRequest
|
||||
@ -16,6 +14,7 @@ import com.xuqm.sdk.update.VersionCheckResult
|
||||
import com.xuqm.sdk.update.VersionCheckStrategy
|
||||
import com.xuqm.sdk.update.VersionComparator
|
||||
import com.xuqm.sdk.update.VersionInfo
|
||||
import com.xuqm.sdk.utils.FileHelper
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
|
||||
@ -41,7 +40,6 @@ class PluginPackageManager private constructor(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
private val cacheManager = SharedCacheManager.getInstance(context)
|
||||
private val downloadManager = DownloadManager.getInstance(context)
|
||||
|
||||
fun cacheCurrentUser(
|
||||
@ -57,11 +55,15 @@ class PluginPackageManager private constructor(private val context: Context) {
|
||||
put("timestamp", System.currentTimeMillis())
|
||||
extraData.forEach { (key, value) -> put(key, value) }
|
||||
}
|
||||
cacheManager.put(CacheKeys.CURRENT_USER, json.toString(), 10 * 60 * 1000)
|
||||
GlobalCache.putString(CacheKeys.CURRENT_USER, json.toString(), 10 * 60 * 1000)
|
||||
}
|
||||
|
||||
fun getCachedUser(appPackageName: String? = null): String? {
|
||||
return cacheManager.getSync(CacheKeys.CURRENT_USER, appPackageName)
|
||||
return if (appPackageName.isNullOrBlank() || appPackageName == context.packageName) {
|
||||
GlobalCache.getString(CacheKeys.CURRENT_USER)
|
||||
} else {
|
||||
com.xuqm.sdk.cache.SharedCacheManager.getInstance(context).getSync(CacheKeys.CURRENT_USER, appPackageName)
|
||||
}
|
||||
}
|
||||
|
||||
fun isPluginInstalled(packageName: String): Boolean {
|
||||
@ -92,11 +94,6 @@ class PluginPackageManager private constructor(private val context: Context) {
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun compareVersion(packageName: String, remoteVersionCode: Long): Int {
|
||||
val local = getLocalPluginInfo(packageName) ?: return 1
|
||||
return remoteVersionCode.compareTo(local.versionCode)
|
||||
}
|
||||
|
||||
fun shouldDownloadPlugin(
|
||||
packageName: String,
|
||||
remoteVersionCode: Long? = null,
|
||||
@ -207,22 +204,7 @@ class PluginPackageManager private constructor(private val context: Context) {
|
||||
return runCatching { context.startActivity(launchIntent) }.isSuccess
|
||||
}
|
||||
|
||||
fun installPlugin(apkFile: File): Boolean {
|
||||
return runCatching {
|
||||
val authority = "${context.packageName}.sdk.fileprovider"
|
||||
val uri: Uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
FileProvider.getUriForFile(context, authority, apkFile)
|
||||
} else {
|
||||
Uri.fromFile(apkFile)
|
||||
}
|
||||
context.startActivity(
|
||||
Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "application/vnd.android.package-archive")
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
},
|
||||
)
|
||||
}.isSuccess
|
||||
}
|
||||
fun installPlugin(apkFile: File): Boolean = FileHelper.installApk(apkFile)
|
||||
|
||||
fun loadPlugin(apkFile: File): Boolean = installPlugin(apkFile)
|
||||
|
||||
@ -3,6 +3,7 @@ package com.xuqm.sdk.update
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import com.xuqm.sdk.utils.FileHelper
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
data class UpdateInfo(
|
||||
@ -119,7 +120,7 @@ class AppUpdater private constructor(private val context: Context) {
|
||||
|
||||
fun clear(taskId: String): Boolean = downloadManager.clear(taskId)
|
||||
|
||||
fun installApk(file: java.io.File): Boolean = downloadManager.installApk(file)
|
||||
fun installApk(file: java.io.File): Boolean = FileHelper.installApk(file)
|
||||
|
||||
fun installFromTask(taskId: String): Boolean {
|
||||
val file = downloadManager.getDownloadedFile(taskId) ?: return false
|
||||
@ -0,0 +1,14 @@
|
||||
package com.xuqm.sdk.updatekit
|
||||
|
||||
import android.content.Context
|
||||
import com.xuqm.sdk.CoreSDK
|
||||
import com.xuqm.sdk.plugin.PluginPackageManager
|
||||
import com.xuqm.sdk.update.AppUpdater
|
||||
|
||||
object UpdateSDK {
|
||||
fun appUpdater(context: Context = CoreSDK.context()): AppUpdater = AppUpdater.getInstance(context)
|
||||
|
||||
fun pluginPackageManager(context: Context = CoreSDK.context()): PluginPackageManager {
|
||||
return PluginPackageManager.getInstance(context)
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ minSdk = "24"
|
||||
coreKtx = "1.18.0"
|
||||
lifecycle = "2.10.0"
|
||||
activityCompose = "1.13.0"
|
||||
activityKtx = "1.13.0"
|
||||
composeBom = "2026.03.00"
|
||||
coroutines = "1.10.2"
|
||||
datastore = "1.1.7"
|
||||
@ -14,6 +15,8 @@ retrofit = "3.0.0"
|
||||
okhttp = "5.3.2"
|
||||
gson = "2.13.2"
|
||||
jserialization = "1.9.0"
|
||||
webkit = "1.14.0"
|
||||
coil = "2.7.0"
|
||||
junit4 = "4.13.2"
|
||||
androidxJunit = "1.3.0"
|
||||
espresso = "3.7.0"
|
||||
@ -23,6 +26,7 @@ androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref =
|
||||
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycle" }
|
||||
androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" }
|
||||
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
|
||||
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
|
||||
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
|
||||
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
|
||||
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
|
||||
@ -34,6 +38,8 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"
|
||||
androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
|
||||
androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||
kotlinx-serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "jserialization" }
|
||||
androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref = "webkit" }
|
||||
coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" }
|
||||
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" }
|
||||
|
||||
@ -31,7 +31,6 @@ class SzyxLoginActivity : ComponentActivity() {
|
||||
if (!SzyxSDK.isInitialized()) {
|
||||
SzyxSDK.init(this)
|
||||
}
|
||||
ToastCenter.init(this)
|
||||
setContent { MaterialTheme { LoginScreen { finish() } } }
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ android {
|
||||
manifestPlaceholders["authProviderAuthority"] = "com.xuqm.sample.auth"
|
||||
manifestPlaceholders["sharedCacheAuthority"] = "com.xuqm.sample.sdk.cache.provider"
|
||||
manifestPlaceholders["coreFileProviderAuthority"] = "com.xuqm.sample.sdk.fileprovider"
|
||||
buildConfigField("String", "UPDATE_SERVER_BASE_URL", "\"http://192.168.116.9:3000/\"")
|
||||
buildConfigField("String", "UPDATE_SERVER_BASE_URL", "\"http://192.168.113.162:8080/\"")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
@ -48,6 +48,7 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(project(":commonsdk-core"))
|
||||
implementation(project(":commonsdk-compose"))
|
||||
implementation(project(":commonsdk-update"))
|
||||
implementation(project(":lib-szyx"))
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
|
||||
@ -49,6 +49,7 @@ import com.xuqm.sdk.update.StoragePath
|
||||
import com.xuqm.sdk.update.UpdateInfo
|
||||
import com.xuqm.sdk.update.VersionCheckResult
|
||||
import com.xuqm.sdk.update.VersionCheckStrategy
|
||||
import com.xuqm.sdk.updatekit.UpdateSDK
|
||||
import com.xuqm.sdk.utils.DateTimeUtils
|
||||
import com.xuqm.sample.update.UpdateRepository
|
||||
import com.xuqm.szyx.SzyxSDK
|
||||
@ -89,7 +90,7 @@ class MainActivity : ComponentActivity() {
|
||||
if (intent.action == Intent.ACTION_PACKAGE_ADDED && reloadPluginAfterInstall) {
|
||||
reloadPluginAfterInstall = false
|
||||
val pluginUpdateInfo = currentPluginUpdateInfo
|
||||
CoreSDK.pluginPackageManager().reloadPlugin(
|
||||
UpdateSDK.pluginPackageManager().reloadPlugin(
|
||||
packageName = PLUGIN_PACKAGE_NAME,
|
||||
entryActivity = pluginUpdateInfo?.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
|
||||
extras = pluginUpdateInfo?.extras ?: mapOf("hostPackageName" to this@MainActivity.packageName),
|
||||
@ -117,7 +118,6 @@ class MainActivity : ComponentActivity() {
|
||||
debugMode = true,
|
||||
),
|
||||
)
|
||||
ToastCenter.init(this)
|
||||
refreshState()
|
||||
ensureLoginOnLaunch()
|
||||
registerPackageChangeReceiver()
|
||||
@ -170,7 +170,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun refreshState() {
|
||||
refreshSession()
|
||||
pluginInstalledState.value = CoreSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
|
||||
pluginInstalledState.value = UpdateSDK.pluginPackageManager().isPluginInstalled(PLUGIN_PACKAGE_NAME)
|
||||
}
|
||||
|
||||
private fun openLogin() {
|
||||
@ -199,7 +199,7 @@ class MainActivity : ComponentActivity() {
|
||||
return
|
||||
}
|
||||
|
||||
val pluginManager = CoreSDK.pluginPackageManager()
|
||||
val pluginManager = UpdateSDK.pluginPackageManager()
|
||||
pluginManager.cacheCurrentUser(
|
||||
userId = session.loginModel.userId,
|
||||
sessionId = session.loginModel.sessionId,
|
||||
@ -224,7 +224,7 @@ class MainActivity : ComponentActivity() {
|
||||
entryActivity = remoteUpdate.entryActivity ?: PLUGIN_ENTRY_ACTIVITY,
|
||||
extras = mapOf("hostPackageName" to packageName),
|
||||
)
|
||||
val checkResult = CoreSDK.pluginPackageManager().checkPluginUpdate(
|
||||
val checkResult = UpdateSDK.pluginPackageManager().checkPluginUpdate(
|
||||
packageName = pluginUpdateInfo.packageName,
|
||||
remoteVersionCode = pluginUpdateInfo.versionCode,
|
||||
remoteVersionName = pluginUpdateInfo.versionName,
|
||||
@ -235,7 +235,7 @@ class MainActivity : ComponentActivity() {
|
||||
return@onSuccess
|
||||
}
|
||||
currentPluginUpdateInfo = pluginUpdateInfo
|
||||
val taskId = CoreSDK.pluginPackageManager().downloadPlugin(
|
||||
val taskId = UpdateSDK.pluginPackageManager().downloadPlugin(
|
||||
updateInfo = pluginUpdateInfo,
|
||||
fileName = "plugin-ui-release.apk",
|
||||
storagePath = StoragePath.EXTERNAL_FILES,
|
||||
@ -259,7 +259,7 @@ class MainActivity : ComponentActivity() {
|
||||
updateRepository.fetchLatestAppUpdate(packageName)
|
||||
.onSuccess { updateInfo ->
|
||||
when (
|
||||
val checkResult = CoreSDK.appUpdater().checkUpdate(
|
||||
val checkResult = UpdateSDK.appUpdater().checkUpdate(
|
||||
updateInfo = updateInfo,
|
||||
strategy = VersionCheckStrategy.VERSION_CODE_OR_NAME,
|
||||
)
|
||||
@ -281,7 +281,7 @@ class MainActivity : ComponentActivity() {
|
||||
private fun confirmDownloadAppUpdate() {
|
||||
val updateInfo = pendingAppUpdateState.value ?: return
|
||||
pendingAppUpdateState.value = null
|
||||
val taskId = CoreSDK.appUpdater().downloadUpdate(
|
||||
val taskId = UpdateSDK.appUpdater().downloadUpdate(
|
||||
updateInfo = updateInfo,
|
||||
fileName = "sample-app-update.apk",
|
||||
storagePath = StoragePath.EXTERNAL_FILES,
|
||||
@ -308,7 +308,7 @@ class MainActivity : ComponentActivity() {
|
||||
is DownloadState.Success -> {
|
||||
ToastCenter.show("插件下载完成,准备重新加载")
|
||||
reloadPluginAfterInstall = true
|
||||
if (!CoreSDK.pluginPackageManager().loadPlugin(state.file)) {
|
||||
if (!UpdateSDK.pluginPackageManager().loadPlugin(state.file)) {
|
||||
reloadPluginAfterInstall = false
|
||||
ToastCenter.show("插件加载拉起失败")
|
||||
}
|
||||
@ -343,7 +343,7 @@ class MainActivity : ComponentActivity() {
|
||||
when (state) {
|
||||
is DownloadState.Success -> {
|
||||
ToastCenter.show("安装包下载完成,准备安装")
|
||||
if (!CoreSDK.appUpdater().installApk(state.file)) {
|
||||
if (!UpdateSDK.appUpdater().installApk(state.file)) {
|
||||
ToastCenter.show("应用安装拉起失败")
|
||||
}
|
||||
CoreSDK.downloadManager().clear(taskId)
|
||||
|
||||
@ -20,6 +20,7 @@ rootProject.name = "AndroidLibs"
|
||||
|
||||
include(":commonsdk-core")
|
||||
include(":commonsdk-compose")
|
||||
include(":commonsdk-update")
|
||||
include(":lib-szyx")
|
||||
include(":sample-app")
|
||||
include(":plugins:plugin-ui")
|
||||
|
||||
35
doc/01-project-overview.md
普通文件
35
doc/01-project-overview.md
普通文件
@ -0,0 +1,35 @@
|
||||
# 01 项目概览
|
||||
|
||||
## 项目目标
|
||||
|
||||
本项目用于承载 Android SDK、示例宿主应用、插件能力、运营平台、管理平台以及配套的版本管理服务端。
|
||||
|
||||
当前目标重点:
|
||||
|
||||
- 保持 Android 端 `sample-app` 与 `plugin-ui` 的版本查询能力可继续使用
|
||||
- 增加运营平台与管理平台两个 Vue 3 前端
|
||||
- 服务端升级为 Spring Boot 微服务形态,便于后续继续扩展即时通讯、推送、版本管理等模块
|
||||
- 首期优先落地版本管理、插件化开关、灰度发布与用户钩子能力
|
||||
|
||||
## 目录结构
|
||||
|
||||
```text
|
||||
AndroidLibsGroup/
|
||||
├── AndroidLibs/ Android SDK、示例宿主、插件工程
|
||||
├── frontend/ Yarn workspace 前端工程
|
||||
│ ├── ops-platform/ 运营平台
|
||||
│ └── admin-platform/ 管理平台
|
||||
├── server/ Spring Boot 服务端与旧版 Node 示例服务
|
||||
│ ├── version-management-service/
|
||||
│ └── version-service/
|
||||
└── doc/ 项目文档
|
||||
```
|
||||
|
||||
## 当前已实现内容
|
||||
|
||||
- Android SDK 与示例应用基础工程
|
||||
- Vue 3 运营平台与管理平台基础页面
|
||||
- Spring Boot 版本管理服务
|
||||
- MySQL 持久化
|
||||
- Redis 缓存灰度用户列表
|
||||
- Android 更新兼容接口
|
||||
37
doc/02-architecture.md
普通文件
37
doc/02-architecture.md
普通文件
@ -0,0 +1,37 @@
|
||||
# 02 架构设计
|
||||
|
||||
## 总体分层
|
||||
|
||||
### Android 侧
|
||||
|
||||
- `commonsdk-core`:网络、下载、文件、权限、缓存、日志、全局弹窗、时间等基础能力
|
||||
- `commonsdk-compose`:下拉刷新、折叠面板、左滑操作、WebView、图片等 Compose 组件扩展
|
||||
- `commonsdk-update`:独立更新 SDK,负责宿主 APK / 插件版本比较、下载编排与安装拉起
|
||||
- `lib-szyx`:业务登录与会话管理
|
||||
- `sample-app`:宿主示例
|
||||
- `plugins/plugin-ui`:插件示例
|
||||
|
||||
### 前端侧
|
||||
|
||||
- `ops-platform`:运营使用,开放注册、版本上传、发布、灰度圈选
|
||||
- `admin-platform`:平台治理使用,审核主账户、禁用账户、管理子账户权限
|
||||
|
||||
### 服务端
|
||||
|
||||
- `version-management-service`:当前主服务
|
||||
- 后续可继续拆分为账号中心、推送中心、IM 服务、文件服务等独立微服务
|
||||
|
||||
## 当前服务端结构
|
||||
|
||||
- `controller`:接口层
|
||||
- `service`:业务编排层
|
||||
- `persistence/entity`:JPA 实体
|
||||
- `persistence/repository`:JPA 仓储
|
||||
- `config`:CORS、Redis、初始化数据等配置
|
||||
|
||||
## 演进方向
|
||||
|
||||
- 账号体系独立成 `account-service`
|
||||
- 版本包上传独立成文件存储服务,对接 MinIO 或对象存储
|
||||
- 灰度用户钩子改为对接真实用户平台 API,而不是初始化数据
|
||||
- 增加统一鉴权与租户隔离
|
||||
65
doc/03-frontend.md
普通文件
65
doc/03-frontend.md
普通文件
@ -0,0 +1,65 @@
|
||||
# 03 前端说明
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3
|
||||
- Vue Router 4
|
||||
- Pinia
|
||||
- Vite 6
|
||||
- TypeScript 5
|
||||
- Yarn workspace
|
||||
|
||||
## 包管理
|
||||
|
||||
前端统一使用 Yarn,不再使用 npm。
|
||||
|
||||
根目录:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
yarn install
|
||||
```
|
||||
|
||||
开发命令:
|
||||
|
||||
```bash
|
||||
yarn dev:ops
|
||||
yarn dev:admin
|
||||
```
|
||||
|
||||
构建命令:
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
## 项目职责
|
||||
|
||||
### 运营平台 `ops-platform`
|
||||
|
||||
- 主账户开放注册
|
||||
- 版本管理
|
||||
- 插件化开关
|
||||
- 版本包上传
|
||||
- 全量发布
|
||||
- 灰度发布
|
||||
- 灰度用户圈选
|
||||
|
||||
### 管理平台 `admin-platform`
|
||||
|
||||
- 查看运营主账户
|
||||
- 审核主账户
|
||||
- 禁用账户
|
||||
- 查看子账户
|
||||
- 管理子账户权限
|
||||
|
||||
## 环境变量
|
||||
|
||||
- `VITE_API_BASE_URL`:服务端接口地址,默认 `http://127.0.0.1:8080`
|
||||
|
||||
## 后续规划
|
||||
|
||||
- 引入统一 UI 组件体系
|
||||
- 增加登录鉴权与路由守卫
|
||||
- 加入页面级权限控制
|
||||
- 加入文件上传组件和上传进度状态
|
||||
56
doc/04-backend.md
普通文件
56
doc/04-backend.md
普通文件
@ -0,0 +1,56 @@
|
||||
# 04 服务端说明
|
||||
|
||||
## 当前主服务
|
||||
|
||||
`server/version-management-service`
|
||||
|
||||
## 技术基线
|
||||
|
||||
- JDK 21
|
||||
- Spring Boot 3.4.4
|
||||
- Spring Web
|
||||
- Spring Data JPA
|
||||
- Spring Data Redis
|
||||
- MySQL 8+
|
||||
|
||||
## 已实现接口能力
|
||||
|
||||
### 公开接口
|
||||
|
||||
- 运营平台主账户注册
|
||||
|
||||
### 管理平台接口
|
||||
|
||||
- 账户列表
|
||||
- 账户审核/禁用
|
||||
- 子账户权限更新
|
||||
|
||||
### 运营平台接口
|
||||
|
||||
- 应用列表
|
||||
- 插件化开关
|
||||
- 版本上传
|
||||
- 版本发布
|
||||
- 灰度用户列表
|
||||
- 分组列表
|
||||
- 快速选择列表
|
||||
|
||||
### Android 兼容接口
|
||||
|
||||
- `GET /api/v1/updates/app/latest`
|
||||
- `GET /api/v1/updates/plugin/latest`
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd server
|
||||
mvn -pl version-management-service spring-boot:run
|
||||
```
|
||||
|
||||
## 后续演进建议
|
||||
|
||||
- 增加 Spring Security + JWT
|
||||
- 引入 Flyway 管理表结构
|
||||
- 加入 OpenAPI/Swagger
|
||||
- 增加单元测试与集成测试
|
||||
- 版本包下载地址改为文件服务生成
|
||||
60
doc/05-infrastructure.md
普通文件
60
doc/05-infrastructure.md
普通文件
@ -0,0 +1,60 @@
|
||||
# 05 数据库与缓存
|
||||
|
||||
## MySQL
|
||||
|
||||
- Host: `xuqinmin.com`
|
||||
- Port: `3306`
|
||||
- Database: `androidLibsServer`
|
||||
- Username: `androidLibsServer`
|
||||
|
||||
当前通过 Spring JPA 自动建表,后续建议切换到 Flyway 管理。
|
||||
|
||||
当前 `version-management-service` 的数据库与 Redis 配置风格参考了
|
||||
`/Users/xuqinmin/Projects/TrustProjects/AppManager/AppManagerWeb/RuoYi-Vue`
|
||||
项目,重点对齐了:
|
||||
|
||||
- MySQL JDBC 参数格式
|
||||
- Redis 基础连接项
|
||||
- Redis Lettuce 连接池参数
|
||||
- 数据库连接池的超时与空闲参数
|
||||
|
||||
支持通过环境变量覆盖:
|
||||
|
||||
- `DB_URL`
|
||||
- `DB_USERNAME`
|
||||
- `DB_PASSWORD`
|
||||
|
||||
## Redis
|
||||
|
||||
- Host: `redisdev.xuqinmin.com`
|
||||
- Port: `6379`
|
||||
- Database: `0`
|
||||
|
||||
支持通过环境变量覆盖:
|
||||
|
||||
- `REDIS_HOST`
|
||||
- `REDIS_PORT`
|
||||
- `REDIS_DATABASE`
|
||||
- `REDIS_PASSWORD`
|
||||
|
||||
## 当前数据用途
|
||||
|
||||
### MySQL
|
||||
|
||||
- 运营主账户
|
||||
- 子账户
|
||||
- 应用配置
|
||||
- 版本记录
|
||||
- 灰度用户基础数据
|
||||
- 用户分组
|
||||
- 快速选择配置
|
||||
|
||||
### Redis
|
||||
|
||||
- 灰度用户列表查询缓存
|
||||
- 分组与快速选择组合查询结果缓存
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 当前数据库与 Redis 密码已直接写入配置文件,仅适合当前内部开发阶段
|
||||
- 后续应迁移到环境变量、配置中心或密钥管理服务
|
||||
55
doc/06-version-management.md
普通文件
55
doc/06-version-management.md
普通文件
@ -0,0 +1,55 @@
|
||||
# 06 版本管理与灰度发布
|
||||
|
||||
## 业务范围
|
||||
|
||||
当前版本管理覆盖:
|
||||
|
||||
- App 版本管理
|
||||
- 插件版本管理
|
||||
- 插件化开关
|
||||
- 版本包上传登记
|
||||
- 全量发布
|
||||
- 灰度发布
|
||||
|
||||
## 灰度发布规则
|
||||
|
||||
灰度发布基于用户平台钩子能力,当前支持三种圈选方式:
|
||||
|
||||
- 分组选择
|
||||
- 快速选择
|
||||
- 单选用户
|
||||
|
||||
系统返回的用户字段包含:
|
||||
|
||||
- 用户 ID
|
||||
- 昵称
|
||||
- 手机号
|
||||
- 邮箱
|
||||
- 地区
|
||||
- 分组信息
|
||||
|
||||
其中 ID、昵称、手机号、邮箱在前台展示时进行脱敏。
|
||||
|
||||
## Android 拉取版本逻辑
|
||||
|
||||
### 全量版本
|
||||
|
||||
所有用户都可拉取到最新已发布版本。
|
||||
|
||||
### 灰度版本
|
||||
|
||||
只有命中灰度规则的用户才能拉取到灰度版本。
|
||||
|
||||
支持命中条件:
|
||||
|
||||
- 命中指定用户 ID
|
||||
- 命中指定用户分组
|
||||
- 命中指定快速选择集合
|
||||
|
||||
## 后续升级项
|
||||
|
||||
- 支持灰度比例发布
|
||||
- 支持按渠道、设备、地区、版本范围灰度
|
||||
- 支持灰度回滚
|
||||
- 支持发布时间窗口
|
||||
- 支持上传真实 APK/AAB/插件包并持久化元数据
|
||||
42
doc/07-development-workflow.md
普通文件
42
doc/07-development-workflow.md
普通文件
@ -0,0 +1,42 @@
|
||||
# 07 开发与交付流程
|
||||
|
||||
## 本地开发
|
||||
|
||||
### Android
|
||||
|
||||
在 `AndroidLibs/` 下使用 Gradle 进行开发与构建。
|
||||
|
||||
```bash
|
||||
cd AndroidLibs
|
||||
./gradlew :sample-app:assembleDebug :plugins:plugin-ui:assembleDebug
|
||||
```
|
||||
|
||||
### 前端
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
yarn install
|
||||
yarn dev:ops
|
||||
yarn dev:admin
|
||||
```
|
||||
|
||||
### 服务端
|
||||
|
||||
```bash
|
||||
cd server
|
||||
mvn -pl version-management-service spring-boot:run
|
||||
```
|
||||
|
||||
## 提交流程
|
||||
|
||||
- 功能开发完成后,同步更新 `doc/` 文档
|
||||
- 变更接口时,同时更新前端调用与服务端说明
|
||||
- 变更基础设施配置时,同时更新 `05-infrastructure.md`
|
||||
- 变更版本管理逻辑时,同时更新 `06-version-management.md`
|
||||
- 变更 Android SDK 模块结构或公共组件时,同时更新 `08-android-sdk.md`
|
||||
|
||||
## 持续维护要求
|
||||
|
||||
- `doc/` 为项目正式文档目录,后续新增能力必须持续补充
|
||||
- 若引入新微服务,需要新增单独文档章节
|
||||
- 若引入 CI/CD、容器化部署、测试基线,需要补充新的专题文档
|
||||
54
doc/08-android-sdk.md
普通文件
54
doc/08-android-sdk.md
普通文件
@ -0,0 +1,54 @@
|
||||
# 08 Android SDK 说明
|
||||
|
||||
## 当前模块拆分
|
||||
|
||||
- `commonsdk-core`
|
||||
- 网络能力:`HttpManager`、`RetrofitManager`
|
||||
- 下载能力:`DownloadManager`
|
||||
- 文件能力:`FileHelper`
|
||||
- 权限能力:`PermissionManager`
|
||||
- 缓存能力:`GlobalCache`、`SharedCacheManager`
|
||||
- 通信能力:`PluginMessenger`
|
||||
- 通用 UI 能力:`ToastCenter`、`DialogCenter`
|
||||
- 工具能力:`Logger`、`DateTimeUtils`
|
||||
- `commonsdk-compose`
|
||||
- 下拉刷新:`SwipeRefreshContainer`、`RefreshableLazyColumn`
|
||||
- 折叠面板:`AccordionPanel`、`RefreshableAccordionList`
|
||||
- 左滑操作:`SwipeActionItem`
|
||||
- WebView:`CommonWebView`、`WebViewPageActivity`
|
||||
- 图片:`AdaptiveImage`、`CircleImage`
|
||||
- `commonsdk-update`
|
||||
- 宿主更新:`AppUpdater`
|
||||
- 插件更新:`PluginPackageManager`
|
||||
- SDK 门面:`UpdateSDK`
|
||||
- 版本比较:`VersionComparator`
|
||||
|
||||
## 设计原则
|
||||
|
||||
- 下载与文件安装归 `core`,保证后续任意业务 SDK 都能复用。
|
||||
- 宿主更新和插件更新归 `commonsdk-update`,避免核心模块承担业务升级策略。
|
||||
- Compose 组件保持业务无关,App 可直接引用或二次封装。
|
||||
|
||||
## 接入方式
|
||||
|
||||
### 宿主应用
|
||||
|
||||
- 初始化基础 SDK:`CoreSDK.init(context, CoreSDK.SDKConfig(...))`
|
||||
- 使用更新能力:`UpdateSDK.appUpdater()`、`UpdateSDK.pluginPackageManager()`
|
||||
- 使用公共能力:`CoreSDK.fileHelper()`、`CoreSDK.cache()`、`CoreSDK.dialogCenter()`
|
||||
|
||||
### WebView 页面
|
||||
|
||||
- 内嵌组件:直接使用 `CommonWebView(url = "...")`
|
||||
- 独立页面:通过 `WebViewPageActivity.createIntent(context, url, title)` 跳转
|
||||
|
||||
### 权限申请
|
||||
|
||||
- 通过 `PermissionManager.register(...)` 注册权限请求器
|
||||
- 请求时传入权限标题和描述,框架会先弹出确认说明,再真正调用系统权限弹窗
|
||||
|
||||
## 当前验证
|
||||
|
||||
- 2026-03-27 已执行:
|
||||
- `./gradlew --no-daemon --no-configuration-cache :sample-app:assembleDebug :plugins:plugin-ui:assembleDebug`
|
||||
- 结果:构建通过
|
||||
20
doc/README.md
普通文件
20
doc/README.md
普通文件
@ -0,0 +1,20 @@
|
||||
# AndroidLibsGroup 文档
|
||||
|
||||
当前目录用于沉淀项目的长期文档,后续新增模块、接口、部署说明、发布流程都持续更新在这里。
|
||||
|
||||
## 文档索引
|
||||
|
||||
- [01-项目概览](./01-project-overview.md)
|
||||
- [02-架构设计](./02-architecture.md)
|
||||
- [03-前端说明](./03-frontend.md)
|
||||
- [04-服务端说明](./04-backend.md)
|
||||
- [05-数据库与缓存](./05-infrastructure.md)
|
||||
- [06-版本管理与灰度发布](./06-version-management.md)
|
||||
- [07-开发与交付流程](./07-development-workflow.md)
|
||||
- [08-Android SDK 说明](./08-android-sdk.md)
|
||||
|
||||
## 维护约定
|
||||
|
||||
- 新增模块时,同步更新对应章节。
|
||||
- 接口或数据结构发生变化时,同步更新服务端、前端和版本管理文档。
|
||||
- 部署地址、账号、依赖版本变更时,优先更新 `04-05` 章节。
|
||||
@ -1,10 +1,17 @@
|
||||
# frontend
|
||||
|
||||
当前目录新增两个 Vue 3 前端项目:
|
||||
当前目录新增两个 Vue 3 前端项目,并统一使用 Yarn workspace 管理:
|
||||
|
||||
- `ops-platform`:运营平台,提供开放注册、版本管理、插件化开关、全量/灰度发布。
|
||||
- `admin-platform`:管理平台,负责审核运营账户、禁用账户、管理子账户权限。
|
||||
|
||||
## 安装依赖
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
yarn install
|
||||
```
|
||||
|
||||
## 启动方式
|
||||
|
||||
先启动后端:
|
||||
@ -17,15 +24,20 @@ mvn -pl version-management-service spring-boot:run
|
||||
再分别启动前端:
|
||||
|
||||
```bash
|
||||
cd frontend/ops-platform
|
||||
npm install
|
||||
npm run dev
|
||||
cd frontend
|
||||
yarn dev:ops
|
||||
```
|
||||
|
||||
```bash
|
||||
cd frontend/admin-platform
|
||||
npm install
|
||||
npm run dev
|
||||
cd frontend
|
||||
yarn dev:admin
|
||||
```
|
||||
|
||||
## 构建
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
yarn build
|
||||
```
|
||||
|
||||
前端默认请求 `http://127.0.0.1:8080`,如需调整可通过 `VITE_API_BASE_URL` 覆盖。
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"name": "admin-platform",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
"name": "ops-platform",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@ -1,22 +1,7 @@
|
||||
<template>
|
||||
<div class="shell">
|
||||
<aside class="sidebar">
|
||||
<div>
|
||||
<p class="eyebrow">Vue3 · 运营平台</p>
|
||||
<h1>版本运营工作台</h1>
|
||||
<p class="muted">开放注册、子账户、版本管理、灰度发布都集中在这里。</p>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<RouterLink to="/register">平台注册</RouterLink>
|
||||
<RouterLink to="/versions">版本管理</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
import { RouterView } from 'vue-router'
|
||||
</script>
|
||||
|
||||
@ -18,15 +18,36 @@ export interface Account {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
account: Account
|
||||
tenantAccountId: string
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
id: string
|
||||
appId: string
|
||||
name: string
|
||||
packageName: string
|
||||
entryActivity?: string | null
|
||||
description?: string | null
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface ReleaseRecord {
|
||||
id: string
|
||||
appId: string
|
||||
pluginId?: string | null
|
||||
packageType: 'APP' | 'PLUGIN'
|
||||
packageName: string
|
||||
versionCode: number
|
||||
versionName: string
|
||||
title: string
|
||||
changelog: string
|
||||
downloadUrl: string
|
||||
uploadedFileName: string
|
||||
entryActivity?: string | null
|
||||
minHostVersionCode?: number | null
|
||||
minHostVersionName?: string | null
|
||||
status: 'DRAFT' | 'PUBLISHED' | 'GRAYSCALE'
|
||||
publishStrategy: string
|
||||
grayRule?: {
|
||||
@ -42,10 +63,12 @@ export interface ApplicationDetail {
|
||||
id: string
|
||||
name: string
|
||||
packageName: string
|
||||
pluginPackageName: string
|
||||
pluginPackageName?: string | null
|
||||
description?: string | null
|
||||
pluginManagementEnabled: boolean
|
||||
businessModules: string[]
|
||||
}
|
||||
plugins: PluginConfig[]
|
||||
releases: ReleaseRecord[]
|
||||
}
|
||||
|
||||
@ -92,29 +115,80 @@ async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
}
|
||||
|
||||
export const api = {
|
||||
registerAccount(payload: Pick<Account, 'accountName' | 'contactName' | 'email' | 'phone'>) {
|
||||
registerAccount(payload: {
|
||||
accountName: string
|
||||
contactName: string
|
||||
email: string
|
||||
phone: string
|
||||
password: string
|
||||
}) {
|
||||
return request<Account>('/api/v1/open/accounts/register', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
listApplications() {
|
||||
return request<ApplicationDetail[]>('/api/v1/ops/version/applications')
|
||||
},
|
||||
togglePluginManagement(appId: string, enabled: boolean) {
|
||||
return request(`/api/v1/ops/version/applications/${appId}/plugin-management`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ enabled }),
|
||||
})
|
||||
},
|
||||
uploadRelease(appId: string, payload: Record<string, unknown>) {
|
||||
return request<ReleaseRecord>(`/api/v1/ops/version/applications/${appId}/releases/upload`, {
|
||||
login(payload: { email: string; password: string }) {
|
||||
return request<LoginResult>('/api/v1/open/accounts/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
publishRelease(appId: string, releaseId: string, payload: Record<string, unknown>) {
|
||||
return request<ReleaseRecord>(`/api/v1/ops/version/applications/${appId}/releases/${releaseId}/publish`, {
|
||||
listApplications() {
|
||||
return request<ApplicationDetail[]>('/api/v1/ops/apps')
|
||||
},
|
||||
createApplication(payload: Record<string, unknown>) {
|
||||
return request('/api/v1/ops/apps', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
updateApplication(appId: string, payload: Record<string, unknown>) {
|
||||
return request(`/api/v1/ops/apps/${appId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
togglePluginManagement(appId: string, enabled: boolean) {
|
||||
return request(`/api/v1/ops/apps/${appId}/plugin-management`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ enabled }),
|
||||
})
|
||||
},
|
||||
listAppPackages(appId: string) {
|
||||
return request<ReleaseRecord[]>(`/api/v1/ops/apps/${appId}/packages`)
|
||||
},
|
||||
uploadAppPackage(appId: string, payload: Record<string, unknown>) {
|
||||
return request<ReleaseRecord>(`/api/v1/ops/apps/${appId}/packages`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
listPlugins(appId: string) {
|
||||
return request<PluginConfig[]>(`/api/v1/ops/apps/${appId}/plugins`)
|
||||
},
|
||||
createPlugin(appId: string, payload: Record<string, unknown>) {
|
||||
return request<PluginConfig>(`/api/v1/ops/apps/${appId}/plugins`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
updatePlugin(pluginId: string, payload: Record<string, unknown>) {
|
||||
return request<PluginConfig>(`/api/v1/ops/plugins/${pluginId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
listPluginPackages(pluginId: string) {
|
||||
return request<ReleaseRecord[]>(`/api/v1/ops/plugins/${pluginId}/packages`)
|
||||
},
|
||||
uploadPluginPackage(pluginId: string, payload: Record<string, unknown>) {
|
||||
return request<ReleaseRecord>(`/api/v1/ops/plugins/${pluginId}/packages`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
},
|
||||
publishPackage(releaseId: string, payload: Record<string, unknown>) {
|
||||
return request<ReleaseRecord>(`/api/v1/ops/packages/${releaseId}/publish`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
})
|
||||
|
||||
@ -1,12 +1,23 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import RegisterView from '../views/RegisterView.vue'
|
||||
import VersionManagementView from '../views/VersionManagementView.vue'
|
||||
import LoginView from '../views/LoginView.vue'
|
||||
import OpsShellView from '../views/OpsShellView.vue'
|
||||
import ModulePlaceholderView from '../views/ModulePlaceholderView.vue'
|
||||
import AppManagementView from '../views/AppManagementView.vue'
|
||||
|
||||
export default createRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{ path: '/', redirect: '/register' },
|
||||
{ path: '/register', component: RegisterView },
|
||||
{ path: '/versions', component: VersionManagementView },
|
||||
{ path: '/', redirect: '/login' },
|
||||
{ path: '/login', component: LoginView },
|
||||
{
|
||||
path: '/console',
|
||||
component: OpsShellView,
|
||||
children: [
|
||||
{ path: '', redirect: '/console/apps' },
|
||||
{ path: 'im', component: ModulePlaceholderView, props: { title: 'IM 模块', description: '预留即时通讯管理控制台。' } },
|
||||
{ path: 'push', component: ModulePlaceholderView, props: { title: 'Push 模块', description: '预留推送策略、模板与发布能力。' } },
|
||||
{ path: 'apps', component: AppManagementView },
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@ -66,6 +66,13 @@ select {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.auth-layout {
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.stack {
|
||||
grid-template-columns: 1.25fr 1fr;
|
||||
align-items: start;
|
||||
@ -167,6 +174,38 @@ button {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-row,
|
||||
.list-grid {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.list-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.list-card {
|
||||
text-align: left;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
background: #eef5fb;
|
||||
color: #163454;
|
||||
}
|
||||
|
||||
.list-card[data-active='true'] {
|
||||
background: linear-gradient(135deg, #0d72ff, #11b8a5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.sub-panel {
|
||||
margin-top: 20px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(16, 35, 61, 0.08);
|
||||
}
|
||||
|
||||
.chips span,
|
||||
.chip-button {
|
||||
padding: 6px 10px;
|
||||
|
||||
@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<section class="page stack">
|
||||
<div class="panel">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<p class="section-tag">App 管理</p>
|
||||
<h2>应用列表</h2>
|
||||
</div>
|
||||
<button class="primary" @click="showCreateApp = !showCreateApp">创建 App</button>
|
||||
</div>
|
||||
|
||||
<form v-if="showCreateApp" class="form-grid" @submit.prevent="createApp">
|
||||
<label><span>名称</span><input v-model="appForm.name" required /></label>
|
||||
<label><span>包名</span><input v-model="appForm.packageName" required /></label>
|
||||
<label><span>插件包名前缀</span><input v-model="appForm.pluginPackageName" /></label>
|
||||
<label class="full"><span>说明</span><input v-model="appForm.description" /></label>
|
||||
<label class="toggle"><input type="checkbox" v-model="appForm.pluginManagementEnabled" />支持插件化</label>
|
||||
<button class="primary">保存 App</button>
|
||||
</form>
|
||||
|
||||
<div class="list-grid">
|
||||
<button
|
||||
v-for="item in applications"
|
||||
:key="item.application.id"
|
||||
class="list-card"
|
||||
:data-active="selectedApp?.application.id === item.application.id"
|
||||
@click="selectApp(item)"
|
||||
>
|
||||
<strong>{{ item.application.name }}</strong>
|
||||
<span>{{ item.application.packageName }}</span>
|
||||
<span>{{ item.application.pluginManagementEnabled ? '支持插件化' : '仅宿主' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel" v-if="selectedApp">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<p class="section-tag">当前应用</p>
|
||||
<h2>{{ selectedApp.application.name }}</h2>
|
||||
</div>
|
||||
<button class="secondary" @click="loadAll">刷新</button>
|
||||
</div>
|
||||
|
||||
<form class="form-grid" @submit.prevent="saveApp">
|
||||
<label><span>名称</span><input v-model="appEdit.name" required /></label>
|
||||
<label><span>包名</span><input v-model="appEdit.packageName" required /></label>
|
||||
<label><span>插件包名前缀</span><input v-model="appEdit.pluginPackageName" /></label>
|
||||
<label class="full"><span>说明</span><input v-model="appEdit.description" /></label>
|
||||
<label class="toggle"><input type="checkbox" v-model="appEdit.pluginManagementEnabled" />支持插件化</label>
|
||||
<button class="primary">修改 App 信息</button>
|
||||
</form>
|
||||
|
||||
<div class="tab-row">
|
||||
<button :class="tab === 'packages' ? 'primary' : 'secondary'" @click="tab = 'packages'">安装包</button>
|
||||
<button
|
||||
v-if="selectedApp.application.pluginManagementEnabled"
|
||||
:class="tab === 'plugins' ? 'primary' : 'secondary'"
|
||||
@click="tab = 'plugins'"
|
||||
>
|
||||
插件列表
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<template v-if="tab === 'packages'">
|
||||
<form class="form-grid" @submit.prevent="uploadAppPackage">
|
||||
<label><span>包名</span><input v-model="appPackageForm.packageName" required /></label>
|
||||
<label><span>版本名</span><input v-model="appPackageForm.versionName" required /></label>
|
||||
<label><span>版本码</span><input v-model.number="appPackageForm.versionCode" type="number" required /></label>
|
||||
<label><span>文件名</span><input v-model="appPackageForm.uploadedFileName" required /></label>
|
||||
<label class="full"><span>下载地址</span><input v-model="appPackageForm.downloadUrl" required /></label>
|
||||
<label class="full"><span>标题</span><input v-model="appPackageForm.title" required /></label>
|
||||
<label class="full"><span>更新说明</span><textarea v-model="appPackageForm.changelog" rows="3" /></label>
|
||||
<button class="primary">上传安装包</button>
|
||||
</form>
|
||||
|
||||
<table class="table">
|
||||
<thead><tr><th>版本</th><th>状态</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="release in appPackages" :key="release.id">
|
||||
<td>{{ release.versionName }} ({{ release.versionCode }})</td>
|
||||
<td>{{ release.status }}</td>
|
||||
<td class="actions">
|
||||
<button class="ghost" @click="publishFull(release.id)">发布当前安装包</button>
|
||||
<button class="ghost" @click="prepareGray(release.id)">配置灰度</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<form class="form-grid" @submit.prevent="createPlugin">
|
||||
<label><span>插件名</span><input v-model="pluginForm.name" required /></label>
|
||||
<label><span>插件包名</span><input v-model="pluginForm.packageName" required /></label>
|
||||
<label class="full"><span>入口 Activity</span><input v-model="pluginForm.entryActivity" /></label>
|
||||
<label class="full"><span>说明</span><input v-model="pluginForm.description" /></label>
|
||||
<button class="primary">新建插件</button>
|
||||
</form>
|
||||
|
||||
<div class="list-grid">
|
||||
<button
|
||||
v-for="plugin in plugins"
|
||||
:key="plugin.id"
|
||||
class="list-card"
|
||||
:data-active="selectedPlugin?.id === plugin.id"
|
||||
@click="selectPlugin(plugin)"
|
||||
>
|
||||
<strong>{{ plugin.name }}</strong>
|
||||
<span>{{ plugin.packageName }}</span>
|
||||
<span>{{ plugin.enabled ? '已启用' : '未启用' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedPlugin" class="sub-panel">
|
||||
<h3>{{ selectedPlugin.name }} 安装包</h3>
|
||||
<form class="form-grid" @submit.prevent="uploadPluginPackage">
|
||||
<label><span>版本名</span><input v-model="pluginPackageForm.versionName" required /></label>
|
||||
<label><span>版本码</span><input v-model.number="pluginPackageForm.versionCode" type="number" required /></label>
|
||||
<label><span>宿主最低版本码</span><input v-model.number="pluginPackageForm.minHostVersionCode" type="number" /></label>
|
||||
<label><span>宿主最低版本名</span><input v-model="pluginPackageForm.minHostVersionName" /></label>
|
||||
<label><span>文件名</span><input v-model="pluginPackageForm.uploadedFileName" required /></label>
|
||||
<label class="full"><span>下载地址</span><input v-model="pluginPackageForm.downloadUrl" required /></label>
|
||||
<label class="full"><span>标题</span><input v-model="pluginPackageForm.title" required /></label>
|
||||
<label class="full"><span>更新说明</span><textarea v-model="pluginPackageForm.changelog" rows="3" /></label>
|
||||
<button class="primary">上传插件安装包</button>
|
||||
</form>
|
||||
|
||||
<table class="table">
|
||||
<thead><tr><th>版本</th><th>宿主最低版本</th><th>状态</th><th>操作</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="release in pluginPackages" :key="release.id">
|
||||
<td>{{ release.versionName }} ({{ release.versionCode }})</td>
|
||||
<td>{{ release.minHostVersionName || '-' }}</td>
|
||||
<td>{{ release.status }}</td>
|
||||
<td class="actions">
|
||||
<button class="ghost" @click="publishFull(release.id)">发版</button>
|
||||
<button class="ghost" @click="prepareGray(release.id)">灰度</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="grayReleaseId" class="sub-panel">
|
||||
<h3>灰度信息配置</h3>
|
||||
<div class="filters">
|
||||
<label><span>分组</span>
|
||||
<select v-model="filters.groupCode" @change="loadUsers">
|
||||
<option value="">全部</option>
|
||||
<option v-for="group in groups" :key="group.code" :value="group.code">{{ group.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label><span>快选</span>
|
||||
<select v-model="filters.quickSelectionCode" @change="loadUsers">
|
||||
<option value="">全部</option>
|
||||
<option v-for="item in quickSelections" :key="item.code" :value="item.code">{{ item.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="grow"><span>搜索</span><input v-model="filters.keyword" @input="loadUsers" /></label>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th></th><th>ID</th><th>昵称</th><th>地区</th><th>分组</th></tr></thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td><input type="checkbox" :checked="selectedUsers.includes(user.id)" @change="toggleUser(user.id)" /></td>
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.nickname }}</td>
|
||||
<td>{{ user.region }}</td>
|
||||
<td>{{ user.groupName }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="actions">
|
||||
<button class="primary" @click="publishGray">确认灰度发布</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { api, type ApplicationDetail, type AudienceGroup, type AudienceUser, type PluginConfig, type QuickSelection, type ReleaseRecord } from '../api/client'
|
||||
|
||||
const applications = ref<ApplicationDetail[]>([])
|
||||
const selectedApp = ref<ApplicationDetail | null>(null)
|
||||
const selectedPlugin = ref<PluginConfig | null>(null)
|
||||
const appPackages = ref<ReleaseRecord[]>([])
|
||||
const pluginPackages = ref<ReleaseRecord[]>([])
|
||||
const plugins = ref<PluginConfig[]>([])
|
||||
const users = ref<AudienceUser[]>([])
|
||||
const groups = ref<AudienceGroup[]>([])
|
||||
const quickSelections = ref<QuickSelection[]>([])
|
||||
const selectedUsers = ref<string[]>([])
|
||||
const grayReleaseId = ref('')
|
||||
const showCreateApp = ref(false)
|
||||
const tab = ref<'packages' | 'plugins'>('packages')
|
||||
|
||||
const filters = reactive({ keyword: '', groupCode: '', quickSelectionCode: '' })
|
||||
const appForm = reactive({ name: '', packageName: '', pluginPackageName: '', description: '', pluginManagementEnabled: true, businessModules: ['IM', 'PUSH', 'VERSION'] })
|
||||
const appEdit = reactive({ name: '', packageName: '', pluginPackageName: '', description: '', pluginManagementEnabled: true, businessModules: ['IM', 'PUSH', 'VERSION'] })
|
||||
const pluginForm = reactive({ name: '', packageName: '', entryActivity: '', description: '' })
|
||||
const appPackageForm = reactive({ packageName: '', versionCode: 1, versionName: '', title: '发现新版本', changelog: '', downloadUrl: '', uploadedFileName: '', entryActivity: '', forceUpdate: false })
|
||||
const pluginPackageForm = reactive({ versionCode: 1, versionName: '', title: '插件更新', changelog: '', downloadUrl: '', uploadedFileName: '', entryActivity: '', minHostVersionCode: 0, minHostVersionName: '' })
|
||||
|
||||
async function loadAll() {
|
||||
applications.value = await api.listApplications()
|
||||
const [groupList, quickList] = await Promise.all([api.listAudienceGroups(), api.listQuickSelections()])
|
||||
groups.value = groupList
|
||||
quickSelections.value = quickList
|
||||
if (applications.value.length > 0 && !selectedApp.value) {
|
||||
await selectApp(applications.value[0])
|
||||
}
|
||||
}
|
||||
|
||||
async function selectApp(item: ApplicationDetail) {
|
||||
selectedApp.value = item
|
||||
selectedPlugin.value = null
|
||||
tab.value = 'packages'
|
||||
Object.assign(appEdit, item.application)
|
||||
appPackageForm.packageName = item.application.packageName
|
||||
appPackages.value = await api.listAppPackages(item.application.id)
|
||||
plugins.value = await api.listPlugins(item.application.id)
|
||||
}
|
||||
|
||||
async function createApp() {
|
||||
await api.createApplication(appForm)
|
||||
showCreateApp.value = false
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
async function saveApp() {
|
||||
if (!selectedApp.value) return
|
||||
await api.updateApplication(selectedApp.value.application.id, appEdit)
|
||||
await loadAll()
|
||||
}
|
||||
|
||||
async function uploadAppPackage() {
|
||||
if (!selectedApp.value) return
|
||||
await api.uploadAppPackage(selectedApp.value.application.id, appPackageForm)
|
||||
appPackages.value = await api.listAppPackages(selectedApp.value.application.id)
|
||||
}
|
||||
|
||||
async function createPlugin() {
|
||||
if (!selectedApp.value) return
|
||||
await api.createPlugin(selectedApp.value.application.id, pluginForm)
|
||||
plugins.value = await api.listPlugins(selectedApp.value.application.id)
|
||||
}
|
||||
|
||||
async function selectPlugin(plugin: PluginConfig) {
|
||||
selectedPlugin.value = plugin
|
||||
pluginPackages.value = await api.listPluginPackages(plugin.id)
|
||||
}
|
||||
|
||||
async function uploadPluginPackage() {
|
||||
if (!selectedPlugin.value) return
|
||||
await api.uploadPluginPackage(selectedPlugin.value.id, pluginPackageForm)
|
||||
pluginPackages.value = await api.listPluginPackages(selectedPlugin.value.id)
|
||||
}
|
||||
|
||||
async function publishFull(releaseId: string) {
|
||||
await api.publishPackage(releaseId, { grayPublish: false })
|
||||
if (selectedApp.value) appPackages.value = await api.listAppPackages(selectedApp.value.application.id)
|
||||
if (selectedPlugin.value) pluginPackages.value = await api.listPluginPackages(selectedPlugin.value.id)
|
||||
}
|
||||
|
||||
function prepareGray(releaseId: string) {
|
||||
grayReleaseId.value = releaseId
|
||||
void loadUsers()
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
users.value = await api.listAudienceUsers(filters)
|
||||
}
|
||||
|
||||
function toggleUser(userId: string) {
|
||||
selectedUsers.value = selectedUsers.value.includes(userId)
|
||||
? selectedUsers.value.filter(item => item !== userId)
|
||||
: [...selectedUsers.value, userId]
|
||||
}
|
||||
|
||||
async function publishGray() {
|
||||
if (!grayReleaseId.value) return
|
||||
await api.publishPackage(grayReleaseId.value, {
|
||||
grayPublish: true,
|
||||
hookName: 'user-platform-gray-hook',
|
||||
groupCodes: filters.groupCode ? [filters.groupCode] : [],
|
||||
quickSelectionCodes: filters.quickSelectionCode ? [filters.quickSelectionCode] : [],
|
||||
userIds: selectedUsers.value,
|
||||
})
|
||||
grayReleaseId.value = ''
|
||||
}
|
||||
|
||||
onMounted(() => { void loadAll() })
|
||||
</script>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<section class="auth-layout">
|
||||
<div class="panel">
|
||||
<p class="section-tag">运营平台</p>
|
||||
<h1>登录或注册</h1>
|
||||
<p class="muted">登录后进入 IM、Push、App 管理三大模块。</p>
|
||||
|
||||
<div class="tab-row">
|
||||
<button :class="mode === 'login' ? 'primary' : 'secondary'" @click="mode = 'login'">登录</button>
|
||||
<button :class="mode === 'register' ? 'primary' : 'secondary'" @click="mode = 'register'">注册</button>
|
||||
</div>
|
||||
|
||||
<form class="form-grid" @submit.prevent="submit">
|
||||
<label v-if="mode === 'register'">
|
||||
企业名称
|
||||
<input v-model="form.accountName" placeholder="星云运营中心" required />
|
||||
</label>
|
||||
<label v-if="mode === 'register'">
|
||||
联系人
|
||||
<input v-model="form.contactName" placeholder="林青" required />
|
||||
</label>
|
||||
<label>
|
||||
邮箱
|
||||
<input v-model="form.email" type="email" placeholder="ops@nebula.example" required />
|
||||
</label>
|
||||
<label v-if="mode === 'register'">
|
||||
手机号
|
||||
<input v-model="form.phone" placeholder="13800138000" required />
|
||||
</label>
|
||||
<label class="full">
|
||||
密码
|
||||
<input v-model="form.password" type="password" placeholder="请输入密码" required />
|
||||
</label>
|
||||
<button class="primary full">{{ mode === 'login' ? '登录并进入工作台' : '注册并登录' }}</button>
|
||||
</form>
|
||||
|
||||
<p v-if="message" class="success-text">{{ message }}</p>
|
||||
<p class="muted">内置测试账号:`ops@nebula.example / 123456`</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { api } from '../api/client'
|
||||
|
||||
const router = useRouter()
|
||||
const mode = ref<'login' | 'register'>('login')
|
||||
const message = ref('')
|
||||
const form = reactive({
|
||||
accountName: '',
|
||||
contactName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
async function submit() {
|
||||
message.value = ''
|
||||
try {
|
||||
if (mode.value === 'register') {
|
||||
await api.registerAccount(form)
|
||||
}
|
||||
const result = await api.login({ email: form.email, password: form.password })
|
||||
localStorage.setItem('ops-session', JSON.stringify(result))
|
||||
await router.push('/console/apps')
|
||||
} catch (error) {
|
||||
message.value = error instanceof Error ? error.message : '操作失败'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<section class="panel">
|
||||
<p class="section-tag">业务模块</p>
|
||||
<h2>{{ title }}</h2>
|
||||
<p class="muted">{{ description }}</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{ title: string; description: string }>()
|
||||
</script>
|
||||
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div class="shell">
|
||||
<aside class="sidebar">
|
||||
<div>
|
||||
<p class="eyebrow">Vue3 · 运营平台</p>
|
||||
<h1>运营控制台</h1>
|
||||
<p class="muted">登录后统一管理 IM、Push 和 App/插件发布。</p>
|
||||
</div>
|
||||
<nav class="nav">
|
||||
<RouterLink to="/console/im">IM</RouterLink>
|
||||
<RouterLink to="/console/push">Push</RouterLink>
|
||||
<RouterLink to="/console/apps">App 管理</RouterLink>
|
||||
</nav>
|
||||
</aside>
|
||||
<main class="content">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { RouterLink, RouterView } from 'vue-router'
|
||||
</script>
|
||||
14
frontend/package.json
普通文件
14
frontend/package.json
普通文件
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "android-libs-frontend-workspace",
|
||||
"private": true,
|
||||
"packageManager": "yarn@1.22.22",
|
||||
"workspaces": [
|
||||
"ops-platform",
|
||||
"admin-platform"
|
||||
],
|
||||
"scripts": {
|
||||
"dev:ops": "yarn workspace ops-platform dev",
|
||||
"dev:admin": "yarn workspace admin-platform dev",
|
||||
"build": "yarn workspaces run build"
|
||||
}
|
||||
}
|
||||
738
frontend/yarn.lock
普通文件
738
frontend/yarn.lock
普通文件
@ -0,0 +1,738 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@babel/helper-string-parser@^7.27.1":
|
||||
version "7.27.1"
|
||||
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
|
||||
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
|
||||
|
||||
"@babel/helper-validator-identifier@^7.28.5":
|
||||
version "7.28.5"
|
||||
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
|
||||
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
|
||||
|
||||
"@babel/parser@^7.29.2":
|
||||
version "7.29.2"
|
||||
resolved "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz#58bd50b9a7951d134988a1ae177a35ef9a703ba1"
|
||||
integrity sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==
|
||||
dependencies:
|
||||
"@babel/types" "^7.29.0"
|
||||
|
||||
"@babel/types@^7.29.0":
|
||||
version "7.29.0"
|
||||
resolved "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7"
|
||||
integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==
|
||||
dependencies:
|
||||
"@babel/helper-string-parser" "^7.27.1"
|
||||
"@babel/helper-validator-identifier" "^7.28.5"
|
||||
|
||||
"@esbuild/aix-ppc64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c"
|
||||
integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==
|
||||
|
||||
"@esbuild/android-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752"
|
||||
integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==
|
||||
|
||||
"@esbuild/android-arm@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a"
|
||||
integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==
|
||||
|
||||
"@esbuild/android-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16"
|
||||
integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==
|
||||
|
||||
"@esbuild/darwin-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd"
|
||||
integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==
|
||||
|
||||
"@esbuild/darwin-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e"
|
||||
integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe"
|
||||
integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==
|
||||
|
||||
"@esbuild/freebsd-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3"
|
||||
integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==
|
||||
|
||||
"@esbuild/linux-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977"
|
||||
integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==
|
||||
|
||||
"@esbuild/linux-arm@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9"
|
||||
integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==
|
||||
|
||||
"@esbuild/linux-ia32@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0"
|
||||
integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==
|
||||
|
||||
"@esbuild/linux-loong64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0"
|
||||
integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==
|
||||
|
||||
"@esbuild/linux-mips64el@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd"
|
||||
integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==
|
||||
|
||||
"@esbuild/linux-ppc64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869"
|
||||
integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==
|
||||
|
||||
"@esbuild/linux-riscv64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6"
|
||||
integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==
|
||||
|
||||
"@esbuild/linux-s390x@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663"
|
||||
integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==
|
||||
|
||||
"@esbuild/linux-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306"
|
||||
integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4"
|
||||
integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==
|
||||
|
||||
"@esbuild/netbsd-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076"
|
||||
integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd"
|
||||
integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==
|
||||
|
||||
"@esbuild/openbsd-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679"
|
||||
integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==
|
||||
|
||||
"@esbuild/openharmony-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d"
|
||||
integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==
|
||||
|
||||
"@esbuild/sunos-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6"
|
||||
integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==
|
||||
|
||||
"@esbuild/win32-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323"
|
||||
integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==
|
||||
|
||||
"@esbuild/win32-ia32@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267"
|
||||
integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==
|
||||
|
||||
"@esbuild/win32-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
|
||||
integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.5.5":
|
||||
version "1.5.5"
|
||||
resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba"
|
||||
integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
|
||||
|
||||
"@rollup/rollup-android-arm-eabi@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz#7e158ddfc16f78da99c0d5ccbae6cae403ef3284"
|
||||
integrity sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==
|
||||
|
||||
"@rollup/rollup-android-arm64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz#49f4ae0e22b6f9ffbcd3818b9a0758fa2d10b1cd"
|
||||
integrity sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==
|
||||
|
||||
"@rollup/rollup-darwin-arm64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz#bb200269069acf5c1c4d79ad142524f77e8b8236"
|
||||
integrity sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==
|
||||
|
||||
"@rollup/rollup-darwin-x64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz#1bf7a92b27ebdd5e0d1d48503c7811160773be1a"
|
||||
integrity sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==
|
||||
|
||||
"@rollup/rollup-freebsd-arm64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz#5ccf537b99c5175008444702193ad0b1c36f7f16"
|
||||
integrity sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==
|
||||
|
||||
"@rollup/rollup-freebsd-x64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz#1196ecd7bf4e128624ef83cd1f9d785114474a77"
|
||||
integrity sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz#cc147633a4af229fee83a737bf2334fbac3dc28e"
|
||||
integrity sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz#3559f9f060153ea54594a42c3b87a297bedcc26e"
|
||||
integrity sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz#e91f887b154123485cfc4b59befe2080fcd8f2df"
|
||||
integrity sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz#660752f040df9ba44a24765df698928917c0bf21"
|
||||
integrity sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz#cb0e939a5fa479ccef264f3f45b31971695f869c"
|
||||
integrity sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz#42f86fbc82cd1a81be2d346476dd3231cf5ee442"
|
||||
integrity sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz#39776a647a789dc95ea049277c5ef8f098df77f9"
|
||||
integrity sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz#466f20029a8e8b3bb2954c7ddebc9586420cac2c"
|
||||
integrity sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz#cff9877c78f12e7aa6246f6902ad913e99edb2b7"
|
||||
integrity sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz#9a762fb99b5a82a921017f56491b7e892b9fb17d"
|
||||
integrity sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz#9d25ad8ac7dab681935baf78ac5ea92d14629cdf"
|
||||
integrity sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz#5e5139e11819fa38a052368da79422cb4afcf466"
|
||||
integrity sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==
|
||||
|
||||
"@rollup/rollup-linux-x64-musl@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz#b6211d46e11b1f945f5504cc794fce839331ed08"
|
||||
integrity sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==
|
||||
|
||||
"@rollup/rollup-openbsd-x64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz#e6e09eebaa7012bb9c7331b437a9e992bd94ca35"
|
||||
integrity sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==
|
||||
|
||||
"@rollup/rollup-openharmony-arm64@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz#f7d99ae857032498e57a5e7259fb7100fd24a87e"
|
||||
integrity sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz#41e392f5d9f3bf1253fdaf2f6d6f6b1bfc452856"
|
||||
integrity sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz#f41b0490be0e5d3cf459b4dc076a192b532adea9"
|
||||
integrity sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz#0fcf9f1fcb750f0317b13aac3b3231687e6397a5"
|
||||
integrity sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc@4.60.0":
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz#3afdb30405f6d4248df5e72e1ca86c5eab55fab8"
|
||||
integrity sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==
|
||||
|
||||
"@types/estree@1.0.8":
|
||||
version "1.0.8"
|
||||
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
|
||||
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
|
||||
|
||||
"@vitejs/plugin-vue@^5.2.3":
|
||||
version "5.2.4"
|
||||
resolved "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz#9e8a512eb174bfc2a333ba959bbf9de428d89ad8"
|
||||
integrity sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==
|
||||
|
||||
"@volar/language-core@2.4.15":
|
||||
version "2.4.15"
|
||||
resolved "https://registry.npmmirror.com/@volar/language-core/-/language-core-2.4.15.tgz#759d04cb4eab9920560b8bcfa4515d5b08a1b7ce"
|
||||
integrity sha512-3VHw+QZU0ZG9IuQmzT68IyN4hZNd9GchGPhbD9+pa8CVv7rnoOZwo7T8weIbrRmihqy3ATpdfXFnqRrfPVK6CA==
|
||||
dependencies:
|
||||
"@volar/source-map" "2.4.15"
|
||||
|
||||
"@volar/source-map@2.4.15":
|
||||
version "2.4.15"
|
||||
resolved "https://registry.npmmirror.com/@volar/source-map/-/source-map-2.4.15.tgz#18aba09994c0268e59a418f9d738e4a85302781d"
|
||||
integrity sha512-CPbMWlUN6hVZJYGcU/GSoHu4EnCHiLaXI9n8c9la6RaI9W5JHX+NqG+GSQcB0JdC2FIBLdZJwGsfKyBB71VlTg==
|
||||
|
||||
"@volar/typescript@2.4.15":
|
||||
version "2.4.15"
|
||||
resolved "https://registry.npmmirror.com/@volar/typescript/-/typescript-2.4.15.tgz#1445d23f8e4f9ad821b6bfa58cf4a2b980dc5f97"
|
||||
integrity sha512-2aZ8i0cqPGjXb4BhkMsPYDkkuc2ZQ6yOpqwAuNwUoncELqoy5fRgOQtLR9gB0g902iS0NAkvpIzs27geVyVdPg==
|
||||
dependencies:
|
||||
"@volar/language-core" "2.4.15"
|
||||
path-browserify "^1.0.1"
|
||||
vscode-uri "^3.0.8"
|
||||
|
||||
"@vue/compiler-core@3.5.31":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.31.tgz#db20d99eb3e8e9ce3c008b8cc79bdb7dab3dfe61"
|
||||
integrity sha512-k/ueL14aNIEy5Onf0OVzR8kiqF/WThgLdFhxwa4e/KF/0qe38IwIdofoSWBTvvxQOesaz6riAFAUaYjoF9fLLQ==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.29.2"
|
||||
"@vue/shared" "3.5.31"
|
||||
entities "^7.0.1"
|
||||
estree-walker "^2.0.2"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
"@vue/compiler-dom@3.5.31", "@vue/compiler-dom@^3.5.0":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.31.tgz#7d4b5688a8daef1513ee18f566ea129bded36ff7"
|
||||
integrity sha512-BMY/ozS/xxjYqRFL+tKdRpATJYDTTgWSo0+AJvJNg4ig+Hgb0dOsHPXvloHQ5hmlivUqw1Yt2pPIqp4e0v1GUw==
|
||||
dependencies:
|
||||
"@vue/compiler-core" "3.5.31"
|
||||
"@vue/shared" "3.5.31"
|
||||
|
||||
"@vue/compiler-sfc@3.5.31":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.31.tgz#ab3670bc81f0bf60ccd0766b16042ad5b334d821"
|
||||
integrity sha512-M8wpPgR9UJ8MiRGjppvx9uWJfLV7A/T+/rL8s/y3QG3u0c2/YZgff3d6SuimKRIhcYnWg5fTfDMlz2E6seUW8Q==
|
||||
dependencies:
|
||||
"@babel/parser" "^7.29.2"
|
||||
"@vue/compiler-core" "3.5.31"
|
||||
"@vue/compiler-dom" "3.5.31"
|
||||
"@vue/compiler-ssr" "3.5.31"
|
||||
"@vue/shared" "3.5.31"
|
||||
estree-walker "^2.0.2"
|
||||
magic-string "^0.30.21"
|
||||
postcss "^8.5.8"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
"@vue/compiler-ssr@3.5.31":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.31.tgz#bc68bb14cedab04ff5230460badfca983de5391b"
|
||||
integrity sha512-h0xIMxrt/LHOvJKMri+vdYT92BrK3HFLtDqq9Pr/lVVfE4IyKZKvWf0vJFW10Yr6nX02OR4MkJwI0c1HDa1hog==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.31"
|
||||
"@vue/shared" "3.5.31"
|
||||
|
||||
"@vue/compiler-vue2@^2.7.16":
|
||||
version "2.7.16"
|
||||
resolved "https://registry.npmmirror.com/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz#2ba837cbd3f1b33c2bc865fbe1a3b53fb611e249"
|
||||
integrity sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==
|
||||
dependencies:
|
||||
de-indent "^1.0.2"
|
||||
he "^1.2.0"
|
||||
|
||||
"@vue/devtools-api@^6.6.4":
|
||||
version "6.6.4"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343"
|
||||
integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==
|
||||
|
||||
"@vue/devtools-api@^7.7.7":
|
||||
version "7.7.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.9.tgz#999dbea50da6b00cf59a1336f11fdc2b43d9e063"
|
||||
integrity sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==
|
||||
dependencies:
|
||||
"@vue/devtools-kit" "^7.7.9"
|
||||
|
||||
"@vue/devtools-kit@^7.7.9":
|
||||
version "7.7.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.9.tgz#bc218a815616e8987df7ab3e10fc1fb3b8706c58"
|
||||
integrity sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==
|
||||
dependencies:
|
||||
"@vue/devtools-shared" "^7.7.9"
|
||||
birpc "^2.3.0"
|
||||
hookable "^5.5.3"
|
||||
mitt "^3.0.1"
|
||||
perfect-debounce "^1.0.0"
|
||||
speakingurl "^14.0.1"
|
||||
superjson "^2.2.2"
|
||||
|
||||
"@vue/devtools-shared@^7.7.9":
|
||||
version "7.7.9"
|
||||
resolved "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz#fa4c096b744927081a7dda5fcf05f34b1ae6ca14"
|
||||
integrity sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==
|
||||
dependencies:
|
||||
rfdc "^1.4.1"
|
||||
|
||||
"@vue/language-core@2.2.12":
|
||||
version "2.2.12"
|
||||
resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz#d01f7e865f593f968cb65c12a13d8337e65641f0"
|
||||
integrity sha512-IsGljWbKGU1MZpBPN+BvPAdr55YPkj2nB/TBNGNC32Vy2qLG25DYu/NBN2vNtZqdRbTRjaoYrahLrToim2NanA==
|
||||
dependencies:
|
||||
"@volar/language-core" "2.4.15"
|
||||
"@vue/compiler-dom" "^3.5.0"
|
||||
"@vue/compiler-vue2" "^2.7.16"
|
||||
"@vue/shared" "^3.5.0"
|
||||
alien-signals "^1.0.3"
|
||||
minimatch "^9.0.3"
|
||||
muggle-string "^0.4.1"
|
||||
path-browserify "^1.0.1"
|
||||
|
||||
"@vue/reactivity@3.5.31":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.31.tgz#c8deaab09bd26185d3153d3e4447e2c6590a6608"
|
||||
integrity sha512-DtKXxk9E/KuVvt8VxWu+6Luc9I9ETNcqR1T1oW1gf02nXaZ1kuAx58oVu7uX9XxJR0iJCro6fqBLw9oSBELo5g==
|
||||
dependencies:
|
||||
"@vue/shared" "3.5.31"
|
||||
|
||||
"@vue/runtime-core@3.5.31":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.31.tgz#bbe07e3bda17caf3ca2ba289bdd2d22badf3dce4"
|
||||
integrity sha512-AZPmIHXEAyhpkmN7aWlqjSfYynmkWlluDNPHMCZKFHH+lLtxP/30UJmoVhXmbDoP1Ng0jG0fyY2zCj1PnSSA6Q==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.31"
|
||||
"@vue/shared" "3.5.31"
|
||||
|
||||
"@vue/runtime-dom@3.5.31":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.31.tgz#3fe39a7bbbbf3ef2cdd6c51f8a1ea63d13959ec6"
|
||||
integrity sha512-xQJsNRmGPeDCJq/u813tyonNgWBFjzfVkBwDREdEWndBnGdHLHgkwNBQxLtg4zDrzKTEcnikUy1UUNecb3lJ6g==
|
||||
dependencies:
|
||||
"@vue/reactivity" "3.5.31"
|
||||
"@vue/runtime-core" "3.5.31"
|
||||
"@vue/shared" "3.5.31"
|
||||
csstype "^3.2.3"
|
||||
|
||||
"@vue/server-renderer@3.5.31":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.31.tgz#d5dbc14dedc37315d197d0a846ef817e02257778"
|
||||
integrity sha512-GJuwRvMcdZX/CriUnyIIOGkx3rMV3H6sOu0JhdKbduaeCji6zb60iOGMY7tFoN24NfsUYoFBhshZtGxGpxO4iA==
|
||||
dependencies:
|
||||
"@vue/compiler-ssr" "3.5.31"
|
||||
"@vue/shared" "3.5.31"
|
||||
|
||||
"@vue/shared@3.5.31", "@vue/shared@^3.5.0":
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.31.tgz#83276e1d450fea7d20dd15c3bbafbea5aada122d"
|
||||
integrity sha512-nBxuiuS9Lj5bPkPbWogPUnjxxWpkRniX7e5UBQDWl6Fsf4roq9wwV+cR7ezQ4zXswNvPIlsdj1slcLB7XCsRAw==
|
||||
|
||||
alien-signals@^1.0.3:
|
||||
version "1.0.13"
|
||||
resolved "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz#8d6db73462f742ee6b89671fbd8c37d0b1727a7e"
|
||||
integrity sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==
|
||||
|
||||
balanced-match@^1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
birpc@^2.3.0:
|
||||
version "2.9.0"
|
||||
resolved "https://registry.npmmirror.com/birpc/-/birpc-2.9.0.tgz#b59550897e4cd96a223e2a6c1475b572236ed145"
|
||||
integrity sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==
|
||||
|
||||
brace-expansion@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.3.tgz#0493338bdd58e319b1039c67cf7ee439892c01d9"
|
||||
integrity sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
copy-anything@^4:
|
||||
version "4.0.5"
|
||||
resolved "https://registry.npmmirror.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea"
|
||||
integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==
|
||||
dependencies:
|
||||
is-what "^5.2.0"
|
||||
|
||||
csstype@^3.2.3:
|
||||
version "3.2.3"
|
||||
resolved "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a"
|
||||
integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==
|
||||
|
||||
de-indent@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
|
||||
integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==
|
||||
|
||||
entities@^7.0.1:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b"
|
||||
integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==
|
||||
|
||||
esbuild@^0.25.0:
|
||||
version "0.25.12"
|
||||
resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.25.12.tgz#97a1d041f4ab00c2fce2f838d2b9969a2d2a97a5"
|
||||
integrity sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.25.12"
|
||||
"@esbuild/android-arm" "0.25.12"
|
||||
"@esbuild/android-arm64" "0.25.12"
|
||||
"@esbuild/android-x64" "0.25.12"
|
||||
"@esbuild/darwin-arm64" "0.25.12"
|
||||
"@esbuild/darwin-x64" "0.25.12"
|
||||
"@esbuild/freebsd-arm64" "0.25.12"
|
||||
"@esbuild/freebsd-x64" "0.25.12"
|
||||
"@esbuild/linux-arm" "0.25.12"
|
||||
"@esbuild/linux-arm64" "0.25.12"
|
||||
"@esbuild/linux-ia32" "0.25.12"
|
||||
"@esbuild/linux-loong64" "0.25.12"
|
||||
"@esbuild/linux-mips64el" "0.25.12"
|
||||
"@esbuild/linux-ppc64" "0.25.12"
|
||||
"@esbuild/linux-riscv64" "0.25.12"
|
||||
"@esbuild/linux-s390x" "0.25.12"
|
||||
"@esbuild/linux-x64" "0.25.12"
|
||||
"@esbuild/netbsd-arm64" "0.25.12"
|
||||
"@esbuild/netbsd-x64" "0.25.12"
|
||||
"@esbuild/openbsd-arm64" "0.25.12"
|
||||
"@esbuild/openbsd-x64" "0.25.12"
|
||||
"@esbuild/openharmony-arm64" "0.25.12"
|
||||
"@esbuild/sunos-x64" "0.25.12"
|
||||
"@esbuild/win32-arm64" "0.25.12"
|
||||
"@esbuild/win32-ia32" "0.25.12"
|
||||
"@esbuild/win32-x64" "0.25.12"
|
||||
|
||||
estree-walker@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||
|
||||
fdir@^6.4.4, fdir@^6.5.0:
|
||||
version "6.5.0"
|
||||
resolved "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
|
||||
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
|
||||
|
||||
fsevents@~2.3.2, fsevents@~2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
||||
|
||||
he@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmmirror.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
hookable@^5.5.3:
|
||||
version "5.5.3"
|
||||
resolved "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
|
||||
integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==
|
||||
|
||||
is-what@^5.2.0:
|
||||
version "5.5.0"
|
||||
resolved "https://registry.npmmirror.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4"
|
||||
integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==
|
||||
|
||||
magic-string@^0.30.21:
|
||||
version "0.30.21"
|
||||
resolved "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91"
|
||||
integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==
|
||||
dependencies:
|
||||
"@jridgewell/sourcemap-codec" "^1.5.5"
|
||||
|
||||
minimatch@^9.0.3:
|
||||
version "9.0.9"
|
||||
resolved "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
|
||||
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
|
||||
dependencies:
|
||||
brace-expansion "^2.0.2"
|
||||
|
||||
mitt@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
|
||||
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
|
||||
|
||||
muggle-string@^0.4.1:
|
||||
version "0.4.1"
|
||||
resolved "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz#3b366bd43b32f809dc20659534dd30e7c8a0d328"
|
||||
integrity sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==
|
||||
|
||||
nanoid@^3.3.11:
|
||||
version "3.3.11"
|
||||
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
|
||||
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
|
||||
|
||||
path-browserify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||
|
||||
perfect-debounce@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a"
|
||||
integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==
|
||||
|
||||
picocolors@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
|
||||
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
|
||||
|
||||
picomatch@^4.0.2, picomatch@^4.0.3:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589"
|
||||
integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==
|
||||
|
||||
pinia@^3.0.1:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmmirror.com/pinia/-/pinia-3.0.4.tgz#75dde12784a61e34c1fa6abcd13c1a1061c360c0"
|
||||
integrity sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^7.7.7"
|
||||
|
||||
postcss@^8.5.3, postcss@^8.5.8:
|
||||
version "8.5.8"
|
||||
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz#6230ecc8fb02e7a0f6982e53990937857e13f399"
|
||||
integrity sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==
|
||||
dependencies:
|
||||
nanoid "^3.3.11"
|
||||
picocolors "^1.1.1"
|
||||
source-map-js "^1.2.1"
|
||||
|
||||
rfdc@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca"
|
||||
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
|
||||
|
||||
rollup@^4.34.9:
|
||||
version "4.60.0"
|
||||
resolved "https://registry.npmmirror.com/rollup/-/rollup-4.60.0.tgz#d7d68c8cda873e96e08b2443505609b7e7be9eb8"
|
||||
integrity sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==
|
||||
dependencies:
|
||||
"@types/estree" "1.0.8"
|
||||
optionalDependencies:
|
||||
"@rollup/rollup-android-arm-eabi" "4.60.0"
|
||||
"@rollup/rollup-android-arm64" "4.60.0"
|
||||
"@rollup/rollup-darwin-arm64" "4.60.0"
|
||||
"@rollup/rollup-darwin-x64" "4.60.0"
|
||||
"@rollup/rollup-freebsd-arm64" "4.60.0"
|
||||
"@rollup/rollup-freebsd-x64" "4.60.0"
|
||||
"@rollup/rollup-linux-arm-gnueabihf" "4.60.0"
|
||||
"@rollup/rollup-linux-arm-musleabihf" "4.60.0"
|
||||
"@rollup/rollup-linux-arm64-gnu" "4.60.0"
|
||||
"@rollup/rollup-linux-arm64-musl" "4.60.0"
|
||||
"@rollup/rollup-linux-loong64-gnu" "4.60.0"
|
||||
"@rollup/rollup-linux-loong64-musl" "4.60.0"
|
||||
"@rollup/rollup-linux-ppc64-gnu" "4.60.0"
|
||||
"@rollup/rollup-linux-ppc64-musl" "4.60.0"
|
||||
"@rollup/rollup-linux-riscv64-gnu" "4.60.0"
|
||||
"@rollup/rollup-linux-riscv64-musl" "4.60.0"
|
||||
"@rollup/rollup-linux-s390x-gnu" "4.60.0"
|
||||
"@rollup/rollup-linux-x64-gnu" "4.60.0"
|
||||
"@rollup/rollup-linux-x64-musl" "4.60.0"
|
||||
"@rollup/rollup-openbsd-x64" "4.60.0"
|
||||
"@rollup/rollup-openharmony-arm64" "4.60.0"
|
||||
"@rollup/rollup-win32-arm64-msvc" "4.60.0"
|
||||
"@rollup/rollup-win32-ia32-msvc" "4.60.0"
|
||||
"@rollup/rollup-win32-x64-gnu" "4.60.0"
|
||||
"@rollup/rollup-win32-x64-msvc" "4.60.0"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
source-map-js@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
|
||||
speakingurl@^14.0.1:
|
||||
version "14.0.1"
|
||||
resolved "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53"
|
||||
integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==
|
||||
|
||||
superjson@^2.2.2:
|
||||
version "2.2.6"
|
||||
resolved "https://registry.npmmirror.com/superjson/-/superjson-2.2.6.tgz#a223a3a988172a5f9656e2063fe5f733af40d099"
|
||||
integrity sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==
|
||||
dependencies:
|
||||
copy-anything "^4"
|
||||
|
||||
tinyglobby@^0.2.13:
|
||||
version "0.2.15"
|
||||
resolved "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz#e228dd1e638cea993d2fdb4fcd2d4602a79951c2"
|
||||
integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
|
||||
dependencies:
|
||||
fdir "^6.5.0"
|
||||
picomatch "^4.0.3"
|
||||
|
||||
typescript@^5.8.2:
|
||||
version "5.9.3"
|
||||
resolved "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
|
||||
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
|
||||
|
||||
vite@^6.2.2:
|
||||
version "6.4.1"
|
||||
resolved "https://registry.npmmirror.com/vite/-/vite-6.4.1.tgz#afbe14518cdd6887e240a4b0221ab6d0ce733f96"
|
||||
integrity sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==
|
||||
dependencies:
|
||||
esbuild "^0.25.0"
|
||||
fdir "^6.4.4"
|
||||
picomatch "^4.0.2"
|
||||
postcss "^8.5.3"
|
||||
rollup "^4.34.9"
|
||||
tinyglobby "^0.2.13"
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.3"
|
||||
|
||||
vscode-uri@^3.0.8:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz#dd09ec5a66a38b5c3fffc774015713496d14e09c"
|
||||
integrity sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==
|
||||
|
||||
vue-router@^4.5.0:
|
||||
version "4.6.4"
|
||||
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
|
||||
integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
|
||||
dependencies:
|
||||
"@vue/devtools-api" "^6.6.4"
|
||||
|
||||
vue-tsc@^2.2.8:
|
||||
version "2.2.12"
|
||||
resolved "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz#5f719b08ef7390a763c1a20169ca5c9d09d55688"
|
||||
integrity sha512-P7OP77b2h/Pmk+lZdJ0YWs+5tJ6J2+uOQPo7tlBnY44QqQSPYvS0qVT4wqDJgwrZaLe47etJLLQRFia71GYITw==
|
||||
dependencies:
|
||||
"@volar/typescript" "2.4.15"
|
||||
"@vue/language-core" "2.2.12"
|
||||
|
||||
vue@^3.5.13:
|
||||
version "3.5.31"
|
||||
resolved "https://registry.npmmirror.com/vue/-/vue-3.5.31.tgz#ff20b2ca7893b4f9ae576a2064dbd3e2f5850118"
|
||||
integrity sha512-iV/sU9SzOlmA/0tygSmjkEN6Jbs3nPoIPFhCMLD2STrjgOU8DX7ZtzMhg4ahVwf5Rp9KoFzcXeB1ZrVbLBp5/Q==
|
||||
dependencies:
|
||||
"@vue/compiler-dom" "3.5.31"
|
||||
"@vue/compiler-sfc" "3.5.31"
|
||||
"@vue/runtime-dom" "3.5.31"
|
||||
"@vue/server-renderer" "3.5.31"
|
||||
"@vue/shared" "3.5.31"
|
||||
@ -17,6 +17,11 @@
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<spring-boot.version>3.4.4</spring-boot.version>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
@ -30,4 +35,27 @@
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<build>
|
||||
<pluginManagement>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
<release>${maven.compiler.release}</release>
|
||||
<encoding>${project.build.sourceEncoding}</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@ -15,6 +15,8 @@
|
||||
<description>Version management microservice for operator/admin platforms</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||
<maven.compiler.release>${java.version}</maven.compiler.release>
|
||||
</properties>
|
||||
|
||||
@ -62,8 +64,11 @@
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
<release>${java.version}</release>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
@ -5,12 +5,14 @@ import com.xuqm.versionmanagement.persistence.entity.AccountEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.HookUserEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.QuickSelectionEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
|
||||
import com.xuqm.versionmanagement.persistence.repository.AccountRepository;
|
||||
import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
|
||||
import com.xuqm.versionmanagement.persistence.repository.HookGroupRepository;
|
||||
import com.xuqm.versionmanagement.persistence.repository.HookUserRepository;
|
||||
import com.xuqm.versionmanagement.persistence.repository.PluginRepository;
|
||||
import com.xuqm.versionmanagement.persistence.repository.QuickSelectionRepository;
|
||||
import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
|
||||
import java.time.LocalDateTime;
|
||||
@ -26,6 +28,7 @@ public class DataInitializer {
|
||||
CommandLineRunner seedVersionManagementData(
|
||||
AccountRepository accountRepository,
|
||||
ApplicationRepository applicationRepository,
|
||||
PluginRepository pluginRepository,
|
||||
ReleaseRepository releaseRepository,
|
||||
HookUserRepository hookUserRepository,
|
||||
HookGroupRepository hookGroupRepository,
|
||||
@ -41,6 +44,7 @@ public class DataInitializer {
|
||||
main.setAccountName("星云运营中心");
|
||||
main.setContactName("林青");
|
||||
main.setEmail("ops@nebula.example");
|
||||
main.setPassword("123456");
|
||||
main.setPhone("13800138000");
|
||||
main.setType(PlatformData.AccountType.MAIN);
|
||||
main.setStatus(PlatformData.AccountStatus.ACTIVE);
|
||||
@ -52,6 +56,7 @@ public class DataInitializer {
|
||||
sub.setAccountName("星云发布子账号");
|
||||
sub.setContactName("苏宁");
|
||||
sub.setEmail("release@nebula.example");
|
||||
sub.setPassword("123456");
|
||||
sub.setPhone("13900139000");
|
||||
sub.setType(PlatformData.AccountType.SUB);
|
||||
sub.setStatus(PlatformData.AccountStatus.ACTIVE);
|
||||
@ -65,25 +70,42 @@ public class DataInitializer {
|
||||
app.setName("宿主 App");
|
||||
app.setPackageName("com.xuqm.sample");
|
||||
app.setPluginPackageName("com.xuqm.plugin.ui");
|
||||
app.setDescription("Android 宿主应用,支持登录、检查更新、插件加载");
|
||||
app.setPluginManagementEnabled(true);
|
||||
app.setBusinessModules(List.of("IM", "PUSH", "VERSION"));
|
||||
app.setCreatedAt(LocalDateTime.of(2026, 3, 27, 9, 0));
|
||||
applicationRepository.save(app);
|
||||
|
||||
PluginEntity plugin = new PluginEntity();
|
||||
plugin.setId("PLG-001");
|
||||
plugin.setAppId(app.getId());
|
||||
plugin.setName("UI 插件");
|
||||
plugin.setPackageName("com.xuqm.plugin.ui");
|
||||
plugin.setEntryActivity("com.xuqm.plugin.ui.PluginUiActivity");
|
||||
plugin.setDescription("演示插件,支持宿主启动和独立安装");
|
||||
plugin.setEnabled(true);
|
||||
plugin.setCreatedAt(LocalDateTime.of(2026, 3, 27, 9, 5));
|
||||
pluginRepository.save(plugin);
|
||||
|
||||
releaseRepository.saveAll(List.of(
|
||||
release("REL-APP-001", app.getId(), PlatformData.PackageType.APP, "com.xuqm.sample", 2, "0.2.0",
|
||||
release("REL-APP-001", app.getId(), null, PlatformData.PackageType.APP, "com.xuqm.sample", 2, "0.2.0",
|
||||
"发现新版本", "1. 新增统一下载管理\n2. 新增插件版本管理\n3. 优化宿主更新流程",
|
||||
"http://192.168.116.9:10223/app.apk", null, false, "sample-app-release-v0.2.0.apk",
|
||||
"http://192.168.116.9:10223/app.apk", null, null, null, false, "sample-app-release-v0.2.0.apk",
|
||||
PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
|
||||
LocalDateTime.of(2026, 3, 27, 9, 30), LocalDateTime.of(2026, 3, 27, 9, 45)),
|
||||
release("REL-PLUGIN-001", app.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 2, "0.2.0",
|
||||
release("REL-PLUGIN-001", app.getId(), plugin.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 2, "0.2.0",
|
||||
"插件 UI 更新", "1. 修复插件页面展示问题\n2. 优化宿主拉起体验",
|
||||
"http://192.168.116.9:10223/plugin-ui-release.apk", "com.xuqm.plugin.ui.PluginUiActivity", false, "plugin-ui-release-v0.2.0.apk",
|
||||
"http://192.168.116.9:10223/plugin-ui-release.apk", "com.xuqm.plugin.ui.PluginUiActivity", 1, "0.1.0", false, "plugin-ui-release-v0.2.0.apk",
|
||||
PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
|
||||
LocalDateTime.of(2026, 3, 27, 9, 35), LocalDateTime.of(2026, 3, 27, 9, 50)),
|
||||
release("REL-APP-002", app.getId(), PlatformData.PackageType.APP, "com.xuqm.sample", 3, "0.3.0-beta",
|
||||
release("REL-PLUGIN-002", app.getId(), plugin.getId(), PlatformData.PackageType.PLUGIN, "com.xuqm.plugin.ui", 5, "0.5.0",
|
||||
"插件高版本", "1. 新增高版本插件能力\n2. 要求宿主至少升级到 0.2.0",
|
||||
"http://192.168.116.9:10223/plugin-ui-v0.5.0.apk", "com.xuqm.plugin.ui.PluginUiActivity", 2, "0.2.0", false, "plugin-ui-release-v0.5.0.apk",
|
||||
PlatformData.ReleaseStatus.PUBLISHED, "FULL", null, List.of(), List.of(), List.of(),
|
||||
LocalDateTime.of(2026, 3, 27, 10, 5), LocalDateTime.of(2026, 3, 27, 10, 18)),
|
||||
release("REL-APP-002", app.getId(), null, PlatformData.PackageType.APP, "com.xuqm.sample", 3, "0.3.0-beta",
|
||||
"0.3.0 灰度测试", "1. 新增版本平台灰度能力\n2. 支持用户分组圈选",
|
||||
"http://192.168.116.9:10223/app-beta.apk", null, false, "sample-app-release-v0.3.0-beta.apk",
|
||||
"http://192.168.116.9:10223/app-beta.apk", null, null, null, false, "sample-app-release-v0.3.0-beta.apk",
|
||||
PlatformData.ReleaseStatus.GRAYSCALE, "GRAY", "user-platform-gray-hook", List.of("beta-core"), List.of("vip-seed"), List.of("USER-1001"),
|
||||
LocalDateTime.of(2026, 3, 27, 10, 0), LocalDateTime.of(2026, 3, 27, 10, 15))
|
||||
));
|
||||
@ -112,6 +134,7 @@ public class DataInitializer {
|
||||
private ReleaseEntity release(
|
||||
String id,
|
||||
String appId,
|
||||
String pluginId,
|
||||
PlatformData.PackageType packageType,
|
||||
String packageName,
|
||||
int versionCode,
|
||||
@ -120,6 +143,8 @@ public class DataInitializer {
|
||||
String changelog,
|
||||
String downloadUrl,
|
||||
String entryActivity,
|
||||
Integer minHostVersionCode,
|
||||
String minHostVersionName,
|
||||
boolean forceUpdate,
|
||||
String uploadedFileName,
|
||||
PlatformData.ReleaseStatus status,
|
||||
@ -134,6 +159,7 @@ public class DataInitializer {
|
||||
ReleaseEntity entity = new ReleaseEntity();
|
||||
entity.setId(id);
|
||||
entity.setAppId(appId);
|
||||
entity.setPluginId(pluginId);
|
||||
entity.setPackageType(packageType);
|
||||
entity.setPackageName(packageName);
|
||||
entity.setVersionCode(versionCode);
|
||||
@ -142,6 +168,8 @@ public class DataInitializer {
|
||||
entity.setChangelog(changelog);
|
||||
entity.setDownloadUrl(downloadUrl);
|
||||
entity.setEntryActivity(entryActivity);
|
||||
entity.setMinHostVersionCode(minHostVersionCode);
|
||||
entity.setMinHostVersionName(minHostVersionName);
|
||||
entity.setForceUpdate(forceUpdate);
|
||||
entity.setUploadedFileName(uploadedFileName);
|
||||
entity.setStatus(status);
|
||||
|
||||
@ -43,15 +43,27 @@ public class CompatibilityUpdateController {
|
||||
@GetMapping("/api/v1/updates/plugin/latest")
|
||||
public ApiResponse<Map<String, Object>> latestPlugin(
|
||||
@RequestParam String packageName,
|
||||
@RequestParam(required = false) String userId
|
||||
@RequestParam(required = false) String userId,
|
||||
@RequestParam(required = false) Integer hostVersionCode
|
||||
) {
|
||||
PlatformData.ReleaseRecord release = versionManagementService.getLatestPluginRelease(packageName, userId);
|
||||
PlatformData.ReleaseRecord release = versionManagementService.getLatestPluginRelease(packageName, userId, hostVersionCode);
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("packageName", release.getPackageName());
|
||||
payload.put("versionCode", release.getVersionCode());
|
||||
payload.put("versionName", release.getVersionName());
|
||||
payload.put("downloadUrl", release.getDownloadUrl());
|
||||
payload.put("entryActivity", release.getEntryActivity());
|
||||
payload.put("minHostVersionCode", release.getMinHostVersionCode());
|
||||
payload.put("minHostVersionName", release.getMinHostVersionName());
|
||||
return ApiResponse.success(payload);
|
||||
}
|
||||
|
||||
@GetMapping("/api/v1/updates/plugins/catalog")
|
||||
public ApiResponse<java.util.List<VersionManagementService.PluginCatalogItem>> pluginCatalog(
|
||||
@RequestParam String appPackageName,
|
||||
@RequestParam(required = false) String userId,
|
||||
@RequestParam long hostVersionCode
|
||||
) {
|
||||
return ApiResponse.success(versionManagementService.getPluginCatalog(appPackageName, userId, hostVersionCode));
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,7 +5,6 @@ import com.xuqm.versionmanagement.model.PlatformData;
|
||||
import com.xuqm.versionmanagement.service.UserHookService;
|
||||
import com.xuqm.versionmanagement.service.VersionManagementService;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@ -19,7 +18,7 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/ops/version")
|
||||
@RequestMapping("/api/v1/ops")
|
||||
public class OpsVersionController {
|
||||
|
||||
private final VersionManagementService versionManagementService;
|
||||
@ -30,12 +29,46 @@ public class OpsVersionController {
|
||||
this.userHookService = userHookService;
|
||||
}
|
||||
|
||||
@GetMapping("/applications")
|
||||
@GetMapping("/apps")
|
||||
public ApiResponse<List<VersionManagementService.ApplicationDetail>> listApplications() {
|
||||
return ApiResponse.success(versionManagementService.listApplications());
|
||||
}
|
||||
|
||||
@PutMapping("/applications/{appId}/plugin-management")
|
||||
@PostMapping("/apps")
|
||||
public ApiResponse<PlatformData.ApplicationConfig> createApplication(@RequestBody @Validated AppRequest request) {
|
||||
PlatformData.ApplicationConfig application = versionManagementService.createApplication(
|
||||
new VersionManagementService.CreateApplicationCommand(
|
||||
request.name(),
|
||||
request.packageName(),
|
||||
request.pluginPackageName(),
|
||||
request.description(),
|
||||
request.pluginManagementEnabled(),
|
||||
request.businessModules()
|
||||
)
|
||||
);
|
||||
return ApiResponse.success(application, "应用创建成功");
|
||||
}
|
||||
|
||||
@PutMapping("/apps/{appId}")
|
||||
public ApiResponse<PlatformData.ApplicationConfig> updateApplication(
|
||||
@PathVariable String appId,
|
||||
@RequestBody @Validated AppRequest request
|
||||
) {
|
||||
PlatformData.ApplicationConfig application = versionManagementService.updateApplication(
|
||||
appId,
|
||||
new VersionManagementService.UpdateApplicationCommand(
|
||||
request.name(),
|
||||
request.packageName(),
|
||||
request.pluginPackageName(),
|
||||
request.description(),
|
||||
request.pluginManagementEnabled(),
|
||||
request.businessModules()
|
||||
)
|
||||
);
|
||||
return ApiResponse.success(application, "应用信息已更新");
|
||||
}
|
||||
|
||||
@PutMapping("/apps/{appId}/plugin-management")
|
||||
public ApiResponse<PlatformData.ApplicationConfig> togglePluginManagement(
|
||||
@PathVariable String appId,
|
||||
@RequestBody TogglePluginManagementRequest request
|
||||
@ -44,7 +77,12 @@ public class OpsVersionController {
|
||||
return ApiResponse.success(config, "插件化能力已更新");
|
||||
}
|
||||
|
||||
@PostMapping("/applications/{appId}/releases/upload")
|
||||
@GetMapping("/apps/{appId}/packages")
|
||||
public ApiResponse<List<PlatformData.ReleaseRecord>> listAppPackages(@PathVariable String appId) {
|
||||
return ApiResponse.success(versionManagementService.listAppPackages(appId));
|
||||
}
|
||||
|
||||
@PostMapping("/apps/{appId}/packages")
|
||||
public ApiResponse<PlatformData.ReleaseRecord> uploadRelease(
|
||||
@PathVariable String appId,
|
||||
@RequestBody @Validated UploadReleaseRequest request
|
||||
@ -52,13 +90,16 @@ public class OpsVersionController {
|
||||
PlatformData.ReleaseRecord release = versionManagementService.uploadRelease(
|
||||
appId,
|
||||
new VersionManagementService.UploadReleaseCommand(
|
||||
request.packageType(),
|
||||
PlatformData.PackageType.APP,
|
||||
request.packageName(),
|
||||
request.versionCode(),
|
||||
request.versionName(),
|
||||
request.title(),
|
||||
request.changelog(),
|
||||
request.downloadUrl(),
|
||||
request.entryActivity(),
|
||||
null,
|
||||
null,
|
||||
request.forceUpdate(),
|
||||
request.uploadedFileName()
|
||||
)
|
||||
@ -66,14 +107,79 @@ public class OpsVersionController {
|
||||
return ApiResponse.success(release, "版本包已上传");
|
||||
}
|
||||
|
||||
@PostMapping("/applications/{appId}/releases/{releaseId}/publish")
|
||||
public ApiResponse<PlatformData.ReleaseRecord> publishRelease(
|
||||
@GetMapping("/apps/{appId}/plugins")
|
||||
public ApiResponse<List<PlatformData.PluginConfig>> listPlugins(@PathVariable String appId) {
|
||||
return ApiResponse.success(versionManagementService.listPlugins(appId));
|
||||
}
|
||||
|
||||
@PostMapping("/apps/{appId}/plugins")
|
||||
public ApiResponse<PlatformData.PluginConfig> createPlugin(
|
||||
@PathVariable String appId,
|
||||
@RequestBody @Validated PluginRequest request
|
||||
) {
|
||||
PlatformData.PluginConfig plugin = versionManagementService.createPlugin(
|
||||
appId,
|
||||
new VersionManagementService.CreatePluginCommand(
|
||||
request.name(),
|
||||
request.packageName(),
|
||||
request.entryActivity(),
|
||||
request.description()
|
||||
)
|
||||
);
|
||||
return ApiResponse.success(plugin, "插件创建成功");
|
||||
}
|
||||
|
||||
@PutMapping("/plugins/{pluginId}")
|
||||
public ApiResponse<PlatformData.PluginConfig> updatePlugin(
|
||||
@PathVariable String pluginId,
|
||||
@RequestBody @Validated PluginUpdateRequest request
|
||||
) {
|
||||
PlatformData.PluginConfig plugin = versionManagementService.updatePlugin(
|
||||
pluginId,
|
||||
new VersionManagementService.UpdatePluginCommand(
|
||||
request.name(),
|
||||
request.packageName(),
|
||||
request.entryActivity(),
|
||||
request.description(),
|
||||
request.enabled()
|
||||
)
|
||||
);
|
||||
return ApiResponse.success(plugin, "插件信息已更新");
|
||||
}
|
||||
|
||||
@GetMapping("/plugins/{pluginId}/packages")
|
||||
public ApiResponse<List<PlatformData.ReleaseRecord>> listPluginPackages(@PathVariable String pluginId) {
|
||||
return ApiResponse.success(versionManagementService.listPluginPackages(pluginId));
|
||||
}
|
||||
|
||||
@PostMapping("/plugins/{pluginId}/packages")
|
||||
public ApiResponse<PlatformData.ReleaseRecord> uploadPluginRelease(
|
||||
@PathVariable String pluginId,
|
||||
@RequestBody @Validated UploadPluginReleaseRequest request
|
||||
) {
|
||||
PlatformData.ReleaseRecord release = versionManagementService.uploadPluginRelease(
|
||||
pluginId,
|
||||
new VersionManagementService.UploadPluginReleaseCommand(
|
||||
request.versionCode(),
|
||||
request.versionName(),
|
||||
request.title(),
|
||||
request.changelog(),
|
||||
request.downloadUrl(),
|
||||
request.entryActivity(),
|
||||
request.minHostVersionCode(),
|
||||
request.minHostVersionName(),
|
||||
request.uploadedFileName()
|
||||
)
|
||||
);
|
||||
return ApiResponse.success(release, "插件安装包已上传");
|
||||
}
|
||||
|
||||
@PostMapping("/packages/{releaseId}/publish")
|
||||
public ApiResponse<PlatformData.ReleaseRecord> publishRelease(
|
||||
@PathVariable String releaseId,
|
||||
@RequestBody PublishReleaseRequest request
|
||||
) {
|
||||
PlatformData.ReleaseRecord release = versionManagementService.publishRelease(
|
||||
appId,
|
||||
releaseId,
|
||||
new VersionManagementService.PublishReleaseCommand(
|
||||
request.grayPublish(),
|
||||
@ -105,11 +211,24 @@ public class OpsVersionController {
|
||||
return ApiResponse.success(userHookService.getAudienceBundle(null, null, null).quickSelections());
|
||||
}
|
||||
|
||||
public record AppRequest(
|
||||
@NotBlank(message = "不能为空") String name,
|
||||
@NotBlank(message = "不能为空") String packageName,
|
||||
String pluginPackageName,
|
||||
String description,
|
||||
boolean pluginManagementEnabled,
|
||||
List<String> businessModules
|
||||
) {
|
||||
public List<String> businessModules() {
|
||||
return businessModules == null || businessModules.isEmpty() ? List.of("IM", "PUSH", "VERSION") : businessModules;
|
||||
}
|
||||
}
|
||||
|
||||
public record TogglePluginManagementRequest(boolean enabled) {
|
||||
}
|
||||
|
||||
public record UploadReleaseRequest(
|
||||
@NotNull(message = "不能为空") PlatformData.PackageType packageType,
|
||||
@NotBlank(message = "不能为空") String packageName,
|
||||
int versionCode,
|
||||
@NotBlank(message = "不能为空") String versionName,
|
||||
@NotBlank(message = "不能为空") String title,
|
||||
@ -121,6 +240,36 @@ public class OpsVersionController {
|
||||
) {
|
||||
}
|
||||
|
||||
public record PluginRequest(
|
||||
@NotBlank(message = "不能为空") String name,
|
||||
@NotBlank(message = "不能为空") String packageName,
|
||||
String entryActivity,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
|
||||
public record PluginUpdateRequest(
|
||||
@NotBlank(message = "不能为空") String name,
|
||||
@NotBlank(message = "不能为空") String packageName,
|
||||
String entryActivity,
|
||||
String description,
|
||||
boolean enabled
|
||||
) {
|
||||
}
|
||||
|
||||
public record UploadPluginReleaseRequest(
|
||||
int versionCode,
|
||||
@NotBlank(message = "不能为空") String versionName,
|
||||
@NotBlank(message = "不能为空") String title,
|
||||
String changelog,
|
||||
@NotBlank(message = "不能为空") String downloadUrl,
|
||||
String entryActivity,
|
||||
Integer minHostVersionCode,
|
||||
String minHostVersionName,
|
||||
@NotBlank(message = "不能为空") String uploadedFileName
|
||||
) {
|
||||
}
|
||||
|
||||
public record PublishReleaseRequest(
|
||||
boolean grayPublish,
|
||||
String hookName,
|
||||
|
||||
@ -31,10 +31,16 @@ public class PublicAccountController {
|
||||
request.accountName(),
|
||||
request.contactName(),
|
||||
request.email(),
|
||||
request.phone()
|
||||
request.phone(),
|
||||
request.password()
|
||||
)
|
||||
);
|
||||
return ApiResponse.success(account, "运营平台注册成功,等待管理平台审核");
|
||||
return ApiResponse.success(account, "运营平台注册成功");
|
||||
}
|
||||
|
||||
@PostMapping("/api/v1/open/accounts/login")
|
||||
public ApiResponse<AccountService.LoginAccountView> login(@RequestBody @Validated LoginRequest request) {
|
||||
return ApiResponse.success(accountService.login(request.email(), request.password()), "登录成功");
|
||||
}
|
||||
|
||||
@PostMapping("/api/v1/ops/accounts/{accountId}/sub-accounts")
|
||||
@ -59,7 +65,14 @@ public class PublicAccountController {
|
||||
@NotBlank(message = "不能为空") String accountName,
|
||||
@NotBlank(message = "不能为空") String contactName,
|
||||
@Email(message = "格式不正确") String email,
|
||||
@NotBlank(message = "不能为空") String phone
|
||||
@NotBlank(message = "不能为空") String phone,
|
||||
@NotBlank(message = "不能为空") String password
|
||||
) {
|
||||
}
|
||||
|
||||
public record LoginRequest(
|
||||
@Email(message = "格式不正确") String email,
|
||||
@NotBlank(message = "不能为空") String password
|
||||
) {
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ public class PlatformData {
|
||||
|
||||
private List<Account> accounts = new ArrayList<>();
|
||||
private List<ApplicationConfig> applications = new ArrayList<>();
|
||||
private List<PluginConfig> plugins = new ArrayList<>();
|
||||
private List<ReleaseRecord> releases = new ArrayList<>();
|
||||
private List<HookUser> hookUsers = new ArrayList<>();
|
||||
private List<HookGroup> hookGroups = new ArrayList<>();
|
||||
@ -29,6 +30,14 @@ public class PlatformData {
|
||||
this.applications = applications;
|
||||
}
|
||||
|
||||
public List<PluginConfig> getPlugins() {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public void setPlugins(List<PluginConfig> plugins) {
|
||||
this.plugins = plugins;
|
||||
}
|
||||
|
||||
public List<ReleaseRecord> getReleases() {
|
||||
return releases;
|
||||
}
|
||||
@ -181,6 +190,7 @@ public class PlatformData {
|
||||
private String name;
|
||||
private String packageName;
|
||||
private String pluginPackageName;
|
||||
private String description;
|
||||
private boolean pluginManagementEnabled;
|
||||
private List<String> businessModules = new ArrayList<>();
|
||||
private LocalDateTime createdAt;
|
||||
@ -217,6 +227,14 @@ public class PlatformData {
|
||||
this.pluginPackageName = pluginPackageName;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean isPluginManagementEnabled() {
|
||||
return pluginManagementEnabled;
|
||||
}
|
||||
@ -242,9 +260,85 @@ public class PlatformData {
|
||||
}
|
||||
}
|
||||
|
||||
public static class PluginConfig {
|
||||
private String id;
|
||||
private String appId;
|
||||
private String name;
|
||||
private String packageName;
|
||||
private String entryActivity;
|
||||
private String description;
|
||||
private boolean enabled;
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAppId() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getEntryActivity() {
|
||||
return entryActivity;
|
||||
}
|
||||
|
||||
public void setEntryActivity(String entryActivity) {
|
||||
this.entryActivity = entryActivity;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReleaseRecord {
|
||||
private String id;
|
||||
private String appId;
|
||||
private String pluginId;
|
||||
private PackageType packageType;
|
||||
private String packageName;
|
||||
private int versionCode;
|
||||
@ -253,6 +347,8 @@ public class PlatformData {
|
||||
private String changelog;
|
||||
private String downloadUrl;
|
||||
private String entryActivity;
|
||||
private Integer minHostVersionCode;
|
||||
private String minHostVersionName;
|
||||
private boolean forceUpdate;
|
||||
private String uploadedFileName;
|
||||
private ReleaseStatus status;
|
||||
@ -277,6 +373,14 @@ public class PlatformData {
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
public String getPluginId() {
|
||||
return pluginId;
|
||||
}
|
||||
|
||||
public void setPluginId(String pluginId) {
|
||||
this.pluginId = pluginId;
|
||||
}
|
||||
|
||||
public PackageType getPackageType() {
|
||||
return packageType;
|
||||
}
|
||||
@ -341,6 +445,22 @@ public class PlatformData {
|
||||
this.entryActivity = entryActivity;
|
||||
}
|
||||
|
||||
public Integer getMinHostVersionCode() {
|
||||
return minHostVersionCode;
|
||||
}
|
||||
|
||||
public void setMinHostVersionCode(Integer minHostVersionCode) {
|
||||
this.minHostVersionCode = minHostVersionCode;
|
||||
}
|
||||
|
||||
public String getMinHostVersionName() {
|
||||
return minHostVersionName;
|
||||
}
|
||||
|
||||
public void setMinHostVersionName(String minHostVersionName) {
|
||||
this.minHostVersionName = minHostVersionName;
|
||||
}
|
||||
|
||||
public boolean isForceUpdate() {
|
||||
return forceUpdate;
|
||||
}
|
||||
|
||||
@ -27,6 +27,9 @@ public class AccountEntity {
|
||||
@Column(nullable = false, length = 128)
|
||||
private String email;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String password;
|
||||
|
||||
@Column(nullable = false, length = 32)
|
||||
private String phone;
|
||||
|
||||
@ -88,6 +91,14 @@ public class AccountEntity {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public PlatformData.AccountType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@ -24,6 +24,9 @@ public class ApplicationEntity {
|
||||
@Column(length = 128)
|
||||
private String pluginPackageName;
|
||||
|
||||
@Column(length = 512)
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean pluginManagementEnabled;
|
||||
|
||||
@ -66,6 +69,14 @@ public class ApplicationEntity {
|
||||
this.pluginPackageName = pluginPackageName;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean isPluginManagementEnabled() {
|
||||
return pluginManagementEnabled;
|
||||
}
|
||||
|
||||
@ -0,0 +1,100 @@
|
||||
package com.xuqm.versionmanagement.persistence.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "vm_plugin")
|
||||
public class PluginEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false, length = 128)
|
||||
private String packageName;
|
||||
|
||||
@Column(length = 256)
|
||||
private String entryActivity;
|
||||
|
||||
@Column(length = 512)
|
||||
private String description;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean enabled;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(String id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getAppId() {
|
||||
return appId;
|
||||
}
|
||||
|
||||
public void setAppId(String appId) {
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getPackageName() {
|
||||
return packageName;
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
|
||||
public String getEntryActivity() {
|
||||
return entryActivity;
|
||||
}
|
||||
|
||||
public void setEntryActivity(String entryActivity) {
|
||||
this.entryActivity = entryActivity;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,9 @@ public class ReleaseEntity {
|
||||
@Column(nullable = false, length = 64)
|
||||
private String appId;
|
||||
|
||||
@Column(length = 64)
|
||||
private String pluginId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 16)
|
||||
private PlatformData.PackageType packageType;
|
||||
@ -46,6 +49,11 @@ public class ReleaseEntity {
|
||||
@Column(length = 256)
|
||||
private String entryActivity;
|
||||
|
||||
private Integer minHostVersionCode;
|
||||
|
||||
@Column(length = 64)
|
||||
private String minHostVersionName;
|
||||
|
||||
@Column(nullable = false)
|
||||
private boolean forceUpdate;
|
||||
|
||||
@ -95,6 +103,14 @@ public class ReleaseEntity {
|
||||
this.appId = appId;
|
||||
}
|
||||
|
||||
public String getPluginId() {
|
||||
return pluginId;
|
||||
}
|
||||
|
||||
public void setPluginId(String pluginId) {
|
||||
this.pluginId = pluginId;
|
||||
}
|
||||
|
||||
public PlatformData.PackageType getPackageType() {
|
||||
return packageType;
|
||||
}
|
||||
@ -159,6 +175,22 @@ public class ReleaseEntity {
|
||||
this.entryActivity = entryActivity;
|
||||
}
|
||||
|
||||
public Integer getMinHostVersionCode() {
|
||||
return minHostVersionCode;
|
||||
}
|
||||
|
||||
public void setMinHostVersionCode(Integer minHostVersionCode) {
|
||||
this.minHostVersionCode = minHostVersionCode;
|
||||
}
|
||||
|
||||
public String getMinHostVersionName() {
|
||||
return minHostVersionName;
|
||||
}
|
||||
|
||||
public void setMinHostVersionName(String minHostVersionName) {
|
||||
this.minHostVersionName = minHostVersionName;
|
||||
}
|
||||
|
||||
public boolean isForceUpdate() {
|
||||
return forceUpdate;
|
||||
}
|
||||
|
||||
@ -10,4 +10,6 @@ public interface AccountRepository extends JpaRepository<AccountEntity, String>
|
||||
List<AccountEntity> findByTypeOrderByCreatedAtAsc(PlatformData.AccountType type);
|
||||
|
||||
List<AccountEntity> findByParentAccountIdOrderByCreatedAtAsc(String parentAccountId);
|
||||
|
||||
java.util.Optional<AccountEntity> findByEmailAndPassword(String email, String password);
|
||||
}
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
package com.xuqm.versionmanagement.persistence.repository;
|
||||
|
||||
import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface PluginRepository extends JpaRepository<PluginEntity, String> {
|
||||
|
||||
List<PluginEntity> findByAppIdOrderByCreatedAtAsc(String appId);
|
||||
}
|
||||
@ -10,6 +10,8 @@ public interface ReleaseRepository extends JpaRepository<ReleaseEntity, String>
|
||||
|
||||
List<ReleaseEntity> findByAppIdOrderByUploadedAtDesc(String appId);
|
||||
|
||||
List<ReleaseEntity> findByPluginIdOrderByUploadedAtDesc(String pluginId);
|
||||
|
||||
List<ReleaseEntity> findByPackageNameAndPackageType(String packageName, PlatformData.PackageType packageType);
|
||||
|
||||
Optional<ReleaseEntity> findByIdAndAppId(String id, String appId);
|
||||
|
||||
@ -27,14 +27,22 @@ public class AccountService {
|
||||
entity.setAccountName(command.accountName());
|
||||
entity.setContactName(command.contactName());
|
||||
entity.setEmail(command.email());
|
||||
entity.setPassword(command.password());
|
||||
entity.setPhone(command.phone());
|
||||
entity.setType(PlatformData.AccountType.MAIN);
|
||||
entity.setStatus(PlatformData.AccountStatus.PENDING);
|
||||
entity.setStatus(PlatformData.AccountStatus.ACTIVE);
|
||||
entity.setPermissions(List.of("version:read", "version:write", "release:publish", "subaccount:grant"));
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return platformMapper.toAccount(accountRepository.save(entity));
|
||||
}
|
||||
|
||||
public LoginAccountView login(String email, String password) {
|
||||
AccountEntity entity = accountRepository.findByEmailAndPassword(email, password)
|
||||
.orElseThrow(() -> new IllegalArgumentException("账号或密码错误"));
|
||||
PlatformData.Account account = platformMapper.toAccount(entity);
|
||||
return new LoginAccountView(account, entity.getType() == PlatformData.AccountType.SUB ? entity.getParentAccountId() : entity.getId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.Account createSubAccount(String parentAccountId, CreateSubAccountCommand command) {
|
||||
findAccount(parentAccountId);
|
||||
@ -90,7 +98,7 @@ public class AccountService {
|
||||
return prefix + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
}
|
||||
|
||||
public record RegisterAccountCommand(String accountName, String contactName, String email, String phone) {
|
||||
public record RegisterAccountCommand(String accountName, String contactName, String email, String phone, String password) {
|
||||
}
|
||||
|
||||
public record CreateSubAccountCommand(
|
||||
@ -104,4 +112,7 @@ public class AccountService {
|
||||
|
||||
public record AccountView(PlatformData.Account mainAccount, List<PlatformData.Account> subAccounts) {
|
||||
}
|
||||
|
||||
public record LoginAccountView(PlatformData.Account account, String tenantAccountId) {
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import com.xuqm.versionmanagement.persistence.entity.AccountEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.HookGroupEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.HookUserEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.QuickSelectionEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
|
||||
import java.util.List;
|
||||
@ -34,16 +35,31 @@ public class PlatformMapper {
|
||||
config.setName(entity.getName());
|
||||
config.setPackageName(entity.getPackageName());
|
||||
config.setPluginPackageName(entity.getPluginPackageName());
|
||||
config.setDescription(entity.getDescription());
|
||||
config.setPluginManagementEnabled(entity.isPluginManagementEnabled());
|
||||
config.setBusinessModules(entity.getBusinessModules());
|
||||
config.setCreatedAt(entity.getCreatedAt());
|
||||
return config;
|
||||
}
|
||||
|
||||
public PlatformData.PluginConfig toPlugin(PluginEntity entity) {
|
||||
PlatformData.PluginConfig plugin = new PlatformData.PluginConfig();
|
||||
plugin.setId(entity.getId());
|
||||
plugin.setAppId(entity.getAppId());
|
||||
plugin.setName(entity.getName());
|
||||
plugin.setPackageName(entity.getPackageName());
|
||||
plugin.setEntryActivity(entity.getEntryActivity());
|
||||
plugin.setDescription(entity.getDescription());
|
||||
plugin.setEnabled(entity.isEnabled());
|
||||
plugin.setCreatedAt(entity.getCreatedAt());
|
||||
return plugin;
|
||||
}
|
||||
|
||||
public PlatformData.ReleaseRecord toRelease(ReleaseEntity entity) {
|
||||
PlatformData.ReleaseRecord release = new PlatformData.ReleaseRecord();
|
||||
release.setId(entity.getId());
|
||||
release.setAppId(entity.getAppId());
|
||||
release.setPluginId(entity.getPluginId());
|
||||
release.setPackageType(entity.getPackageType());
|
||||
release.setPackageName(entity.getPackageName());
|
||||
release.setVersionCode(entity.getVersionCode());
|
||||
@ -52,6 +68,8 @@ public class PlatformMapper {
|
||||
release.setChangelog(entity.getChangelog());
|
||||
release.setDownloadUrl(entity.getDownloadUrl());
|
||||
release.setEntryActivity(entity.getEntryActivity());
|
||||
release.setMinHostVersionCode(entity.getMinHostVersionCode());
|
||||
release.setMinHostVersionName(entity.getMinHostVersionName());
|
||||
release.setForceUpdate(entity.isForceUpdate());
|
||||
release.setUploadedFileName(entity.getUploadedFileName());
|
||||
release.setStatus(entity.getStatus());
|
||||
@ -98,18 +116,27 @@ public class PlatformMapper {
|
||||
return selection;
|
||||
}
|
||||
|
||||
public ReleaseEntity toReleaseEntity(String appId, PlatformData.ApplicationConfig app, VersionManagementService.UploadReleaseCommand command, String id) {
|
||||
public ReleaseEntity toReleaseEntity(
|
||||
String appId,
|
||||
String pluginId,
|
||||
PlatformData.ApplicationConfig app,
|
||||
VersionManagementService.UploadReleaseCommand command,
|
||||
String id
|
||||
) {
|
||||
ReleaseEntity entity = new ReleaseEntity();
|
||||
entity.setId(id);
|
||||
entity.setAppId(appId);
|
||||
entity.setPluginId(pluginId);
|
||||
entity.setPackageType(command.packageType());
|
||||
entity.setPackageName(command.packageType() == PlatformData.PackageType.PLUGIN ? app.getPluginPackageName() : app.getPackageName());
|
||||
entity.setPackageName(command.packageName());
|
||||
entity.setVersionCode(command.versionCode());
|
||||
entity.setVersionName(command.versionName());
|
||||
entity.setTitle(command.title());
|
||||
entity.setChangelog(command.changelog());
|
||||
entity.setDownloadUrl(command.downloadUrl());
|
||||
entity.setEntryActivity(command.entryActivity());
|
||||
entity.setMinHostVersionCode(command.minHostVersionCode());
|
||||
entity.setMinHostVersionName(command.minHostVersionName());
|
||||
entity.setForceUpdate(command.forceUpdate());
|
||||
entity.setUploadedFileName(command.uploadedFileName());
|
||||
entity.setStatus(PlatformData.ReleaseStatus.DRAFT);
|
||||
|
||||
@ -2,8 +2,10 @@ package com.xuqm.versionmanagement.service;
|
||||
|
||||
import com.xuqm.versionmanagement.model.PlatformData;
|
||||
import com.xuqm.versionmanagement.persistence.entity.ApplicationEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.PluginEntity;
|
||||
import com.xuqm.versionmanagement.persistence.entity.ReleaseEntity;
|
||||
import com.xuqm.versionmanagement.persistence.repository.ApplicationRepository;
|
||||
import com.xuqm.versionmanagement.persistence.repository.PluginRepository;
|
||||
import com.xuqm.versionmanagement.persistence.repository.ReleaseRepository;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Comparator;
|
||||
@ -16,17 +18,20 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
public class VersionManagementService {
|
||||
|
||||
private final ApplicationRepository applicationRepository;
|
||||
private final PluginRepository pluginRepository;
|
||||
private final ReleaseRepository releaseRepository;
|
||||
private final UserHookService userHookService;
|
||||
private final PlatformMapper platformMapper;
|
||||
|
||||
public VersionManagementService(
|
||||
ApplicationRepository applicationRepository,
|
||||
PluginRepository pluginRepository,
|
||||
ReleaseRepository releaseRepository,
|
||||
UserHookService userHookService,
|
||||
PlatformMapper platformMapper
|
||||
) {
|
||||
this.applicationRepository = applicationRepository;
|
||||
this.pluginRepository = pluginRepository;
|
||||
this.releaseRepository = releaseRepository;
|
||||
this.userHookService = userHookService;
|
||||
this.platformMapper = platformMapper;
|
||||
@ -37,13 +42,43 @@ public class VersionManagementService {
|
||||
.map(platformMapper::toApplication)
|
||||
.map(app -> new ApplicationDetail(
|
||||
app,
|
||||
pluginRepository.findByAppIdOrderByCreatedAtAsc(app.getId()).stream()
|
||||
.map(platformMapper::toPlugin)
|
||||
.toList(),
|
||||
releaseRepository.findByAppIdOrderByUploadedAtDesc(app.getId()).stream()
|
||||
.filter(release -> release.getPluginId() == null)
|
||||
.map(platformMapper::toRelease)
|
||||
.toList()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.ApplicationConfig createApplication(CreateApplicationCommand command) {
|
||||
ApplicationEntity entity = new ApplicationEntity();
|
||||
entity.setId(nextId("APP"));
|
||||
entity.setName(command.name());
|
||||
entity.setPackageName(command.packageName());
|
||||
entity.setPluginPackageName(command.pluginPackageName());
|
||||
entity.setDescription(command.description());
|
||||
entity.setPluginManagementEnabled(command.pluginManagementEnabled());
|
||||
entity.setBusinessModules(command.businessModules());
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return platformMapper.toApplication(applicationRepository.save(entity));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.ApplicationConfig updateApplication(String appId, UpdateApplicationCommand command) {
|
||||
ApplicationEntity entity = findApplicationEntity(appId);
|
||||
entity.setName(command.name());
|
||||
entity.setPackageName(command.packageName());
|
||||
entity.setPluginPackageName(command.pluginPackageName());
|
||||
entity.setDescription(command.description());
|
||||
entity.setPluginManagementEnabled(command.pluginManagementEnabled());
|
||||
entity.setBusinessModules(command.businessModules());
|
||||
return platformMapper.toApplication(applicationRepository.save(entity));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.ApplicationConfig togglePluginManagement(String appId, boolean enabled) {
|
||||
ApplicationEntity entity = findApplicationEntity(appId);
|
||||
@ -51,18 +86,93 @@ public class VersionManagementService {
|
||||
return platformMapper.toApplication(applicationRepository.save(entity));
|
||||
}
|
||||
|
||||
public List<PlatformData.ReleaseRecord> listAppPackages(String appId) {
|
||||
findApplicationEntity(appId);
|
||||
return releaseRepository.findByAppIdOrderByUploadedAtDesc(appId).stream()
|
||||
.filter(item -> item.getPluginId() == null)
|
||||
.map(platformMapper::toRelease)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.ReleaseRecord uploadRelease(String appId, UploadReleaseCommand command) {
|
||||
PlatformData.ApplicationConfig app = platformMapper.toApplication(findApplicationEntity(appId));
|
||||
ReleaseEntity entity = platformMapper.toReleaseEntity(appId, app, command, nextId("REL"));
|
||||
ReleaseEntity entity = platformMapper.toReleaseEntity(appId, null, app, command, nextId("REL"));
|
||||
entity.setUploadedAt(LocalDateTime.now());
|
||||
return platformMapper.toRelease(releaseRepository.save(entity));
|
||||
}
|
||||
|
||||
public List<PlatformData.PluginConfig> listPlugins(String appId) {
|
||||
findApplicationEntity(appId);
|
||||
return pluginRepository.findByAppIdOrderByCreatedAtAsc(appId).stream()
|
||||
.map(platformMapper::toPlugin)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.PluginConfig createPlugin(String appId, CreatePluginCommand command) {
|
||||
findApplicationEntity(appId);
|
||||
PluginEntity entity = new PluginEntity();
|
||||
entity.setId(nextId("PLG"));
|
||||
entity.setAppId(appId);
|
||||
entity.setName(command.name());
|
||||
entity.setPackageName(command.packageName());
|
||||
entity.setEntryActivity(command.entryActivity());
|
||||
entity.setDescription(command.description());
|
||||
entity.setEnabled(true);
|
||||
entity.setCreatedAt(LocalDateTime.now());
|
||||
return platformMapper.toPlugin(pluginRepository.save(entity));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.PluginConfig updatePlugin(String pluginId, UpdatePluginCommand command) {
|
||||
PluginEntity entity = findPluginEntity(pluginId);
|
||||
entity.setName(command.name());
|
||||
entity.setPackageName(command.packageName());
|
||||
entity.setEntryActivity(command.entryActivity());
|
||||
entity.setDescription(command.description());
|
||||
entity.setEnabled(command.enabled());
|
||||
return platformMapper.toPlugin(pluginRepository.save(entity));
|
||||
}
|
||||
|
||||
public List<PlatformData.ReleaseRecord> listPluginPackages(String pluginId) {
|
||||
findPluginEntity(pluginId);
|
||||
return releaseRepository.findByPluginIdOrderByUploadedAtDesc(pluginId).stream()
|
||||
.map(platformMapper::toRelease)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.ReleaseRecord uploadPluginRelease(String pluginId, UploadPluginReleaseCommand command) {
|
||||
PluginEntity plugin = findPluginEntity(pluginId);
|
||||
PlatformData.ApplicationConfig app = platformMapper.toApplication(findApplicationEntity(plugin.getAppId()));
|
||||
ReleaseEntity entity = platformMapper.toReleaseEntity(
|
||||
plugin.getAppId(),
|
||||
pluginId,
|
||||
app,
|
||||
new UploadReleaseCommand(
|
||||
PlatformData.PackageType.PLUGIN,
|
||||
plugin.getPackageName(),
|
||||
command.versionCode(),
|
||||
command.versionName(),
|
||||
command.title(),
|
||||
command.changelog(),
|
||||
command.downloadUrl(),
|
||||
command.entryActivity() == null || command.entryActivity().isBlank() ? plugin.getEntryActivity() : command.entryActivity(),
|
||||
command.minHostVersionCode(),
|
||||
command.minHostVersionName(),
|
||||
false,
|
||||
command.uploadedFileName()
|
||||
),
|
||||
nextId("REL")
|
||||
);
|
||||
entity.setUploadedAt(LocalDateTime.now());
|
||||
return platformMapper.toRelease(releaseRepository.save(entity));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public PlatformData.ReleaseRecord publishRelease(String appId, String releaseId, PublishReleaseCommand command) {
|
||||
findApplicationEntity(appId);
|
||||
ReleaseEntity release = findReleaseEntity(appId, releaseId);
|
||||
public PlatformData.ReleaseRecord publishRelease(String releaseId, PublishReleaseCommand command) {
|
||||
ReleaseEntity release = findReleaseEntity(releaseId);
|
||||
if (command.grayPublish()) {
|
||||
List<PlatformData.HookUser> matchedUsers = userHookService.findUsersByIds(command.userIds());
|
||||
if (!command.userIds().isEmpty() && matchedUsers.size() != command.userIds().size()) {
|
||||
@ -90,8 +200,41 @@ public class VersionManagementService {
|
||||
return selectLatestRelease(packageName, PlatformData.PackageType.APP, userId);
|
||||
}
|
||||
|
||||
public PlatformData.ReleaseRecord getLatestPluginRelease(String packageName, String userId) {
|
||||
return selectLatestRelease(packageName, PlatformData.PackageType.PLUGIN, userId);
|
||||
public PlatformData.ReleaseRecord getLatestPluginRelease(String packageName, String userId, Integer hostVersionCode) {
|
||||
return selectLatestPluginRelease(packageName, userId, hostVersionCode, true);
|
||||
}
|
||||
|
||||
public List<PluginCatalogItem> getPluginCatalog(String appPackageName, String userId, long hostVersionCode) {
|
||||
ApplicationEntity app = applicationRepository.findAll().stream()
|
||||
.filter(item -> appPackageName.equals(item.getPackageName()))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("应用不存在"));
|
||||
PlatformData.HookUser user = userId == null ? null : userHookService.findUsersByIds(List.of(userId)).stream().findFirst().orElse(null);
|
||||
|
||||
return pluginRepository.findByAppIdOrderByCreatedAtAsc(app.getId()).stream()
|
||||
.map(platformMapper::toPlugin)
|
||||
.map(plugin -> {
|
||||
List<PlatformData.ReleaseRecord> visibleReleases = releaseRepository.findByPluginIdOrderByUploadedAtDesc(plugin.getId()).stream()
|
||||
.map(platformMapper::toRelease)
|
||||
.filter(release -> release.getStatus() == PlatformData.ReleaseStatus.PUBLISHED
|
||||
|| (release.getStatus() == PlatformData.ReleaseStatus.GRAYSCALE && userHookService.matchesGrayRule(user, release.getGrayRule())))
|
||||
.toList();
|
||||
|
||||
PlatformData.ReleaseRecord latest = visibleReleases.stream()
|
||||
.max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode))
|
||||
.orElse(null);
|
||||
PlatformData.ReleaseRecord compatible = visibleReleases.stream()
|
||||
.filter(release -> isHostCompatible(release, hostVersionCode))
|
||||
.max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode))
|
||||
.orElse(null);
|
||||
|
||||
boolean blocked = latest != null && !isHostCompatible(latest, hostVersionCode);
|
||||
String message = blocked
|
||||
? "当前宿主版本过低,不能升级到插件最新版本"
|
||||
: compatible == null ? "暂无可安装插件版本" : "可升级或进入插件";
|
||||
return new PluginCatalogItem(plugin, latest, compatible, blocked, message);
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
|
||||
private PlatformData.ReleaseRecord selectLatestRelease(String packageName, PlatformData.PackageType packageType, String userId) {
|
||||
@ -106,13 +249,35 @@ public class VersionManagementService {
|
||||
.orElseThrow(() -> new IllegalArgumentException("版本配置不存在"));
|
||||
}
|
||||
|
||||
private PlatformData.ReleaseRecord selectLatestPluginRelease(String packageName, String userId, Integer hostVersionCode, boolean compatibleOnly) {
|
||||
PlatformData.HookUser user = userId == null ? null : userHookService.findUsersByIds(List.of(userId)).stream().findFirst().orElse(null);
|
||||
return releaseRepository.findByPackageNameAndPackageType(packageName, PlatformData.PackageType.PLUGIN).stream()
|
||||
.map(platformMapper::toRelease)
|
||||
.filter(release -> release.getStatus() == PlatformData.ReleaseStatus.PUBLISHED
|
||||
|| (release.getStatus() == PlatformData.ReleaseStatus.GRAYSCALE && userHookService.matchesGrayRule(user, release.getGrayRule())))
|
||||
.filter(release -> !compatibleOnly || isHostCompatible(release, hostVersionCode == null ? Long.MAX_VALUE : hostVersionCode.longValue()))
|
||||
.max(Comparator.comparingInt(PlatformData.ReleaseRecord::getVersionCode)
|
||||
.thenComparing(PlatformData.ReleaseRecord::getPublishedAt, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||
.orElseThrow(() -> new IllegalArgumentException("插件版本配置不存在"));
|
||||
}
|
||||
|
||||
private boolean isHostCompatible(PlatformData.ReleaseRecord release, long hostVersionCode) {
|
||||
Integer minHostVersionCode = release.getMinHostVersionCode();
|
||||
return minHostVersionCode == null || hostVersionCode >= minHostVersionCode;
|
||||
}
|
||||
|
||||
private ApplicationEntity findApplicationEntity(String appId) {
|
||||
return applicationRepository.findById(appId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("应用不存在"));
|
||||
}
|
||||
|
||||
private ReleaseEntity findReleaseEntity(String appId, String releaseId) {
|
||||
return releaseRepository.findByIdAndAppId(releaseId, appId)
|
||||
private PluginEntity findPluginEntity(String pluginId) {
|
||||
return pluginRepository.findById(pluginId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("插件不存在"));
|
||||
}
|
||||
|
||||
private ReleaseEntity findReleaseEntity(String releaseId) {
|
||||
return releaseRepository.findById(releaseId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("版本不存在"));
|
||||
}
|
||||
|
||||
@ -120,22 +285,88 @@ public class VersionManagementService {
|
||||
return prefix + "-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
|
||||
}
|
||||
|
||||
public record ApplicationDetail(PlatformData.ApplicationConfig application, List<PlatformData.ReleaseRecord> releases) {
|
||||
public record ApplicationDetail(
|
||||
PlatformData.ApplicationConfig application,
|
||||
List<PlatformData.PluginConfig> plugins,
|
||||
List<PlatformData.ReleaseRecord> releases
|
||||
) {
|
||||
}
|
||||
|
||||
public record PluginCatalogItem(
|
||||
PlatformData.PluginConfig plugin,
|
||||
PlatformData.ReleaseRecord latestRelease,
|
||||
PlatformData.ReleaseRecord compatibleRelease,
|
||||
boolean updateBlockedByHost,
|
||||
String message
|
||||
) {
|
||||
}
|
||||
|
||||
public record CreateApplicationCommand(
|
||||
String name,
|
||||
String packageName,
|
||||
String pluginPackageName,
|
||||
String description,
|
||||
boolean pluginManagementEnabled,
|
||||
List<String> businessModules
|
||||
) {
|
||||
}
|
||||
|
||||
public record UpdateApplicationCommand(
|
||||
String name,
|
||||
String packageName,
|
||||
String pluginPackageName,
|
||||
String description,
|
||||
boolean pluginManagementEnabled,
|
||||
List<String> businessModules
|
||||
) {
|
||||
}
|
||||
|
||||
public record CreatePluginCommand(
|
||||
String name,
|
||||
String packageName,
|
||||
String entryActivity,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
|
||||
public record UpdatePluginCommand(
|
||||
String name,
|
||||
String packageName,
|
||||
String entryActivity,
|
||||
String description,
|
||||
boolean enabled
|
||||
) {
|
||||
}
|
||||
|
||||
public record UploadReleaseCommand(
|
||||
PlatformData.PackageType packageType,
|
||||
String packageName,
|
||||
int versionCode,
|
||||
String versionName,
|
||||
String title,
|
||||
String changelog,
|
||||
String downloadUrl,
|
||||
String entryActivity,
|
||||
Integer minHostVersionCode,
|
||||
String minHostVersionName,
|
||||
boolean forceUpdate,
|
||||
String uploadedFileName
|
||||
) {
|
||||
}
|
||||
|
||||
public record UploadPluginReleaseCommand(
|
||||
int versionCode,
|
||||
String versionName,
|
||||
String title,
|
||||
String changelog,
|
||||
String downloadUrl,
|
||||
String entryActivity,
|
||||
Integer minHostVersionCode,
|
||||
String minHostVersionName,
|
||||
String uploadedFileName
|
||||
) {
|
||||
}
|
||||
|
||||
public record PublishReleaseCommand(
|
||||
boolean grayPublish,
|
||||
String hookName,
|
||||
|
||||
@ -5,11 +5,20 @@ spring:
|
||||
application:
|
||||
name: version-management-service
|
||||
datasource:
|
||||
url: jdbc:mysql://xuqinmin.com:3306/androidLibsServer?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
|
||||
username: androidLibsServer
|
||||
password: iXc8rHydtzRpYFHJ
|
||||
url: ${DB_URL:jdbc:mysql://39.107.53.187:3306/android-libs?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8}
|
||||
username: ${DB_USERNAME:android-libs}
|
||||
password: ${DB_PASSWORD:fiiL8mxhxrDCHc86}
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||
hikari:
|
||||
minimum-idle: 10
|
||||
maximum-pool-size: 20
|
||||
connection-timeout: 30000
|
||||
validation-timeout: 5000
|
||||
idle-timeout: 300000
|
||||
max-lifetime: 900000
|
||||
connection-test-query: SELECT 1
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.MySQLDialect
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
open-in-view: false
|
||||
@ -18,10 +27,17 @@ spring:
|
||||
format_sql: true
|
||||
data:
|
||||
redis:
|
||||
host: redisdev.xuqinmin.com
|
||||
port: 6379
|
||||
database: 0
|
||||
password: xuqinmin1022
|
||||
host: ${REDIS_HOST:redisdev.xuqinmin.com}
|
||||
port: ${REDIS_PORT:6379}
|
||||
database: ${REDIS_DATABASE:0}
|
||||
password: ${REDIS_PASSWORD:xuqinmin1022}
|
||||
timeout: 10s
|
||||
lettuce:
|
||||
pool:
|
||||
min-idle: 0
|
||||
max-idle: 8
|
||||
max-active: 8
|
||||
max-wait: -1ms
|
||||
cache:
|
||||
type: redis
|
||||
redis:
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
# version-service
|
||||
|
||||
用于给 `sample-app` 和 `plugin-ui` 提供统一的版本管理接口。
|
||||
|
||||
## 启动
|
||||
|
||||
```bash
|
||||
cd __server__/version-service
|
||||
npm start
|
||||
```
|
||||
|
||||
默认监听:
|
||||
|
||||
```text
|
||||
http://0.0.0.0:3000
|
||||
```
|
||||
|
||||
## 接口
|
||||
|
||||
### 健康检查
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:3000/health
|
||||
```
|
||||
|
||||
### 获取 App 最新版本
|
||||
|
||||
```bash
|
||||
curl "http://127.0.0.1:3000/api/v1/updates/app/latest?packageName=com.xuqm.sample"
|
||||
```
|
||||
|
||||
### 获取插件最新版本
|
||||
|
||||
```bash
|
||||
curl "http://127.0.0.1:3000/api/v1/updates/plugin/latest?packageName=com.xuqm.plugin.ui"
|
||||
```
|
||||
|
||||
### 更新 App 配置
|
||||
|
||||
```bash
|
||||
curl --location --request PUT "http://127.0.0.1:3000/api/v1/admin/updates/app" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"packageName": "com.xuqm.sample",
|
||||
"versionCode": 2,
|
||||
"versionName": "0.2.0",
|
||||
"title": "发现新版本",
|
||||
"changelog": "更新内容",
|
||||
"downloadUrl": "http://192.168.116.9:10223/app.apk",
|
||||
"forceUpdate": false
|
||||
}'
|
||||
```
|
||||
|
||||
### 更新插件配置
|
||||
|
||||
```bash
|
||||
curl --location --request PUT "http://127.0.0.1:3000/api/v1/admin/updates/plugin" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"packageName": "com.xuqm.plugin.ui",
|
||||
"versionCode": 2,
|
||||
"versionName": "0.2.0",
|
||||
"downloadUrl": "http://192.168.116.9:10223/plugin-ui-release.apk",
|
||||
"entryActivity": "com.xuqm.plugin.ui.PluginUiActivity"
|
||||
}'
|
||||
```
|
||||
|
||||
版本数据存放在 [`data/version-config.json`](./data/version-config.json)。
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"apps": {
|
||||
"com.xuqm.sample": {
|
||||
"packageName": "com.xuqm.sample",
|
||||
"versionCode": 2,
|
||||
"versionName": "0.2.0",
|
||||
"title": "发现新版本",
|
||||
"changelog": "1. 新增统一下载管理\n2. 新增插件版本管理\n3. 优化宿主更新流程",
|
||||
"downloadUrl": "http://192.168.116.9:10223/app.apk",
|
||||
"forceUpdate": false
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"com.xuqm.plugin.ui": {
|
||||
"packageName": "com.xuqm.plugin.ui",
|
||||
"versionCode": 2,
|
||||
"versionName": "0.2.0",
|
||||
"downloadUrl": "http://192.168.116.9:10223/plugin-ui-release.apk",
|
||||
"entryActivity": "com.xuqm.plugin.ui.PluginUiActivity"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "@xuqm/version-service",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Version service for Android host app and plugin update management",
|
||||
"scripts": {
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
}
|
||||
@ -1,141 +0,0 @@
|
||||
const http = require("http");
|
||||
const fs = require("fs/promises");
|
||||
const path = require("path");
|
||||
const { URL } = require("url");
|
||||
|
||||
const HOST = process.env.HOST || "0.0.0.0";
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
const DATA_FILE = path.join(__dirname, "..", "data", "version-config.json");
|
||||
|
||||
async function readConfig() {
|
||||
const raw = await fs.readFile(DATA_FILE, "utf8");
|
||||
return JSON.parse(raw);
|
||||
}
|
||||
|
||||
async function writeConfig(config) {
|
||||
await fs.writeFile(DATA_FILE, JSON.stringify(config, null, 2), "utf8");
|
||||
}
|
||||
|
||||
function sendJson(res, statusCode, payload) {
|
||||
res.writeHead(statusCode, {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET,PUT,OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type"
|
||||
});
|
||||
res.end(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function ok(data, message = "success") {
|
||||
return { code: 200, status: "0", data, message };
|
||||
}
|
||||
|
||||
function fail(statusCode, message) {
|
||||
return {
|
||||
statusCode,
|
||||
payload: { code: statusCode, status: String(statusCode), data: null, message }
|
||||
};
|
||||
}
|
||||
|
||||
async function parseBody(req) {
|
||||
const chunks = [];
|
||||
for await (const chunk of req) chunks.push(chunk);
|
||||
const raw = Buffer.concat(chunks).toString("utf8");
|
||||
return raw ? JSON.parse(raw) : {};
|
||||
}
|
||||
|
||||
async function handleGetLatestApp(url, res) {
|
||||
const packageName = url.searchParams.get("packageName");
|
||||
if (!packageName) {
|
||||
const result = fail(400, "packageName is required");
|
||||
return sendJson(res, result.statusCode, result.payload);
|
||||
}
|
||||
|
||||
const config = await readConfig();
|
||||
const app = config.apps[packageName];
|
||||
if (!app) {
|
||||
const result = fail(404, `app config not found for ${packageName}`);
|
||||
return sendJson(res, result.statusCode, result.payload);
|
||||
}
|
||||
return sendJson(res, 200, ok(app));
|
||||
}
|
||||
|
||||
async function handleGetLatestPlugin(url, res) {
|
||||
const packageName = url.searchParams.get("packageName");
|
||||
if (!packageName) {
|
||||
const result = fail(400, "packageName is required");
|
||||
return sendJson(res, result.statusCode, result.payload);
|
||||
}
|
||||
|
||||
const config = await readConfig();
|
||||
const plugin = config.plugins[packageName];
|
||||
if (!plugin) {
|
||||
const result = fail(404, `plugin config not found for ${packageName}`);
|
||||
return sendJson(res, result.statusCode, result.payload);
|
||||
}
|
||||
return sendJson(res, 200, ok(plugin));
|
||||
}
|
||||
|
||||
async function handleUpdateConfig(kind, req, res) {
|
||||
const body = await parseBody(req);
|
||||
const packageName = body.packageName;
|
||||
if (!packageName) {
|
||||
const result = fail(400, "packageName is required");
|
||||
return sendJson(res, result.statusCode, result.payload);
|
||||
}
|
||||
|
||||
const config = await readConfig();
|
||||
const target = kind === "app" ? config.apps : config.plugins;
|
||||
target[packageName] = body;
|
||||
await writeConfig(config);
|
||||
return sendJson(res, 200, ok(body, `${kind} config updated`));
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
res.writeHead(204, {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET,PUT,OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type"
|
||||
});
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/health") {
|
||||
return sendJson(res, 200, ok({ status: "UP" }));
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/v1/updates/app/latest") {
|
||||
return handleGetLatestApp(url, res);
|
||||
}
|
||||
|
||||
if (req.method === "GET" && url.pathname === "/api/v1/updates/plugin/latest") {
|
||||
return handleGetLatestPlugin(url, res);
|
||||
}
|
||||
|
||||
if (req.method === "PUT" && url.pathname === "/api/v1/admin/updates/app") {
|
||||
return handleUpdateConfig("app", req, res);
|
||||
}
|
||||
|
||||
if (req.method === "PUT" && url.pathname === "/api/v1/admin/updates/plugin") {
|
||||
return handleUpdateConfig("plugin", req, res);
|
||||
}
|
||||
|
||||
const result = fail(404, "route not found");
|
||||
return sendJson(res, result.statusCode, result.payload);
|
||||
} catch (error) {
|
||||
return sendJson(res, 500, {
|
||||
code: 500,
|
||||
status: "500",
|
||||
data: null,
|
||||
message: error.message || "internal server error"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`version-service is running at http://${HOST}:${PORT}`);
|
||||
});
|
||||
正在加载...
在新工单中引用
屏蔽一个用户