1
This commit is contained in:
@@ -15,3 +15,4 @@ training/runs/
|
||||
|
||||
# 文生图配置(含API key,不入库)
|
||||
#training/gen_images_config.json
|
||||
.gstack/
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<!-- App 内更新安装 APK(PackageInstaller 会话安装,见 InstallerChannel.kt) -->
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<application
|
||||
android:label="视野"
|
||||
android:name="${applicationName}"
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.example.observer
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageInstaller
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.provider.Settings
|
||||
import java.io.File
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
/**
|
||||
* 原生 APK 安装通道(App 内更新安装):PackageInstaller 会话安装,
|
||||
* 安装进度经 EventChannel 实时回传 Flutter(下载进度由 Flutter 侧 http 流式下载自算)。
|
||||
*
|
||||
* 通道:
|
||||
* - MethodChannel "observer/installer":install(path)
|
||||
* - result.success("installing"):已提交安装
|
||||
* - result.success("permission_required"):未开「安装未知应用」,已拉起系统设置页
|
||||
* - EventChannel "observer/installer/progress":{event: progress/finished/failed, ...}
|
||||
*/
|
||||
class InstallerChannel(
|
||||
private val activity: FlutterActivity,
|
||||
private val engine: FlutterEngine,
|
||||
) {
|
||||
private val messenger = engine.dartExecutor.binaryMessenger
|
||||
private val method = MethodChannel(messenger, "observer/installer")
|
||||
private val progress = EventChannel(messenger, "observer/installer/progress")
|
||||
|
||||
private var progressSink: EventChannel.EventSink? = null
|
||||
private var activeSession: PackageInstaller.Session? = null
|
||||
|
||||
fun register() {
|
||||
method.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
|
||||
when (call.method) {
|
||||
"install" -> install(call.argument<String>("path") ?: "", result)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
progress.setStreamHandler(object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
progressSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
progressSink = null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun install(path: String, result: MethodChannel.Result) {
|
||||
val file = File(path)
|
||||
if (!file.exists()) {
|
||||
result.error("FILE_NOT_FOUND", "APK 文件不存在: $path", null)
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
!activity.packageManager.canRequestPackageInstalls()
|
||||
) {
|
||||
val intent = Intent(
|
||||
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||
Uri.parse("package:${activity.packageName}"),
|
||||
).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
activity.startActivity(intent)
|
||||
result.success("permission_required")
|
||||
return
|
||||
}
|
||||
try {
|
||||
val pm = activity.packageManager
|
||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL).apply {
|
||||
setSize(file.length())
|
||||
}
|
||||
val sessionId = pm.packageInstaller.createSession(params)
|
||||
val session = pm.packageInstaller.openSession(sessionId)
|
||||
activeSession = session
|
||||
// API 36 起 registerSessionCallback(int, ...) 变体被移除,只剩全局注册形式,
|
||||
// 回调按 sessionId 过滤,避免响应其他会话事件
|
||||
val callback = object : PackageInstaller.SessionCallback() {
|
||||
override fun onCreated(id: Int) {}
|
||||
|
||||
override fun onBadgingChanged(id: Int) {}
|
||||
|
||||
override fun onActiveChanged(id: Int, active: Boolean) {}
|
||||
|
||||
override fun onProgressChanged(id: Int, progressPercent: Float) {
|
||||
if (id != sessionId) return
|
||||
emit("progress", "progress" to progressPercent.toInt())
|
||||
}
|
||||
|
||||
override fun onFinished(id: Int, success: Boolean) {
|
||||
if (id != sessionId) return
|
||||
emit("finished", "success" to success)
|
||||
pm.packageInstaller.unregisterSessionCallback(this)
|
||||
activeSession = null
|
||||
}
|
||||
}
|
||||
pm.packageInstaller.registerSessionCallback(callback, Handler(Looper.getMainLooper()))
|
||||
// 写 APK 到会话:1MB 缓冲流式拷贝,完成后 commit 弹系统确认框
|
||||
Thread {
|
||||
try {
|
||||
session.openWrite("apk", 0, file.length()).use { out ->
|
||||
file.inputStream().use { input ->
|
||||
val buf = ByteArray(1 shl 20)
|
||||
while (true) {
|
||||
val n = input.read(buf)
|
||||
if (n < 0) break
|
||||
out.write(buf, 0, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
val sender = PendingIntent.getActivity(
|
||||
activity,
|
||||
0,
|
||||
Intent(activity, MainActivity::class.java),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
).intentSender
|
||||
session.commit(sender)
|
||||
} catch (e: Exception) {
|
||||
session.abandon()
|
||||
emit("failed", "error" to (e.message ?: "安装会话写入失败"))
|
||||
activeSession = null
|
||||
}
|
||||
}.start()
|
||||
result.success("installing")
|
||||
} catch (e: Exception) {
|
||||
result.error("INSTALL_FAILED", e.message, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun emit(event: String, vararg pairs: Pair<String, Any>) {
|
||||
progressSink?.success(mapOf("event" to event, *pairs))
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import io.flutter.embedding.engine.FlutterEngine
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
private var cameraChannel: CameraChannel? = null
|
||||
private var installerChannel: InstallerChannel? = null
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
@@ -13,6 +14,8 @@ class MainActivity : FlutterActivity() {
|
||||
// configureFlutterEngine(onCreate 阶段)只注册通道与 viewFactory,
|
||||
// 实际 bindToLifecycle 由 Flutter 相机页 start 时触发(此时已 RESUMED)
|
||||
cameraChannel = CameraChannel(this, flutterEngine).also { it.register() }
|
||||
// App 内更新安装 APK(PackageInstaller 会话安装 + 进度回传)
|
||||
installerChannel = InstallerChannel(this, flutterEngine).also { it.register() }
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
|
||||
@@ -116,6 +116,6 @@
|
||||
```
|
||||
|
||||
- **仅 Android 调用**:`UpdateChecker.fetch()` 在非 Android 平台直接返回空(iOS 不做版本下发,用户从 App Store 自行更新)
|
||||
- 客户端以「语义化版本号」按数字段比较(`1.10.0 > 1.9.9`),**服务器版本 > 已装版本即强制更新**:弹全屏阻塞页(禁返回,仅「立即更新」),`url_launcher` 打开系统浏览器下载固定地址 `{apiBaseUrl}/download/observer-latest.apk`(后端静态托管,永远是最新 APK)
|
||||
- 客户端以「语义化版本号」按数字段比较(`1.10.0 > 1.9.9`),**服务器版本 > 已装版本即强制更新**:弹全屏阻塞页(禁返回,仅「立即更新」),**App 内直接下载安装**(不跳浏览器):http 流式下载固定地址 `{apiBaseUrl}/download/observer-latest.apk`(后端静态托管,永远是最新 APK)到缓存目录(`.part` 原子落盘后改名,避免半包)→ 原生 `PackageInstaller` 会话安装(`InstallerChannel.kt`),页面实时显示**下载进度与安装进度**;首次安装需在系统设置允许「安装未知应用」(未开启时自动拉起系统设置页,APK 已缓存、返回后再次点击直达安装)
|
||||
- **双版本比较防反复提示**:用户点「立即更新」时把服务器版本号写入本地(`SessionStore.accepted_update_version`);判定条件是服务器版本 > 已装版本 **或** 服务器版本 > 已接受版本。APK 版本号不递增(每次打的包 versionName 相同)时,已更新完成的手机重启也不会再次弹更新
|
||||
- 网络异常/响应异常时静默跳过检查,不阻塞启动
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// 安装进度事件(原生 PackageInstaller 会话回调经 EventChannel 回传)
|
||||
class InstallEvent {
|
||||
/// progress / finished / failed
|
||||
final String event;
|
||||
|
||||
/// event=progress 时的安装进度 0-100
|
||||
final int progress;
|
||||
|
||||
/// event=finished 时是否安装成功
|
||||
final bool? success;
|
||||
|
||||
/// event=failed 时的错误描述
|
||||
final String? error;
|
||||
|
||||
const InstallEvent({
|
||||
required this.event,
|
||||
this.progress = 0,
|
||||
this.success,
|
||||
this.error,
|
||||
});
|
||||
}
|
||||
|
||||
/// App 内安装 APK:原生侧 PackageInstaller 会话安装(InstallerChannel.kt)
|
||||
class ApkInstaller {
|
||||
static const _method = MethodChannel('observer/installer');
|
||||
static const _progress = EventChannel('observer/installer/progress');
|
||||
|
||||
/// 提交安装。返回 installing(已进入安装流程)/ permission_required
|
||||
/// (未允许「安装未知应用」,原生侧已拉起系统设置页)。
|
||||
static Future<String> install(String path) =>
|
||||
_method.invokeMethod<String>('install', {'path': path}).then(
|
||||
(v) => v ?? 'installing',
|
||||
);
|
||||
|
||||
/// 安装进度流:progress(0-100) → finished(success) / failed(error)
|
||||
static Stream<InstallEvent> progress() {
|
||||
return _progress.receiveBroadcastStream().map((e) {
|
||||
final m = e as Map;
|
||||
return InstallEvent(
|
||||
event: m['event'] as String? ?? '',
|
||||
progress: (m['progress'] as num?)?.toInt() ?? 0,
|
||||
success: m['success'] as bool?,
|
||||
error: m['error'] as String?,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'installer.dart';
|
||||
|
||||
/// 强制更新页:检测到新版本时的全屏阻塞页。
|
||||
/// PopScope 禁返回(Android 系统返回 / iOS 边缘滑动均不可退出),
|
||||
/// 仅「立即更新」跳转系统浏览器下载 APK。
|
||||
/// PopScope 禁返回(Android 系统返回 / iOS 边缘滑动均不可退出)。
|
||||
/// 点「立即更新」在 App 内流式下载 APK(显示下载进度)→ PackageInstaller
|
||||
/// 会话安装(显示安装进度),不再跳浏览器。
|
||||
/// 点击更新时回调 onUpdateAccepted(调用方持久化服务器版本号,
|
||||
/// 使 APK 版本号不递增时也不反复提示)。
|
||||
class UpdateScreen extends StatelessWidget {
|
||||
class UpdateScreen extends StatefulWidget {
|
||||
final String version;
|
||||
final String url;
|
||||
final String notes;
|
||||
@@ -20,20 +27,150 @@ class UpdateScreen extends StatelessWidget {
|
||||
this.onUpdateAccepted,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UpdateScreen> createState() => _UpdateScreenState();
|
||||
}
|
||||
|
||||
enum _Stage { idle, downloading, installing, finished, failed }
|
||||
|
||||
class _UpdateScreenState extends State<UpdateScreen> {
|
||||
final http.Client _client = http.Client();
|
||||
final File _apkFile = File('${Directory.systemTemp.path}/observer-latest.apk');
|
||||
final File _apkPart =
|
||||
File('${Directory.systemTemp.path}/observer-latest.apk.part');
|
||||
|
||||
_Stage _stage = _Stage.idle;
|
||||
|
||||
/// 进度百分比 0-100;null = 总量未知(不确定进度条)
|
||||
double? _progress;
|
||||
String? _message;
|
||||
StreamSubscription<InstallEvent>? _installSub;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_installSub?.cancel();
|
||||
_client.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _launch() async {
|
||||
onUpdateAccepted?.call();
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri == null) return;
|
||||
if (_stage == _Stage.downloading ||
|
||||
_stage == _Stage.installing ||
|
||||
_stage == _Stage.finished) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_stage = _Stage.idle;
|
||||
_message = null;
|
||||
});
|
||||
widget.onUpdateAccepted?.call();
|
||||
if (!Platform.isAndroid) {
|
||||
// 更新检查本就仅 Android 触发,这里兜底非 Android 走浏览器
|
||||
final uri = Uri.tryParse(widget.url);
|
||||
if (uri == null) return;
|
||||
try {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
// APK 已下载完成(下载是原子落盘,.part 改名后文件才存在)→ 跳过下载直接安装
|
||||
if (_apkFile.existsSync()) {
|
||||
await _install();
|
||||
return;
|
||||
}
|
||||
await _download();
|
||||
}
|
||||
|
||||
Future<void> _download() async {
|
||||
setState(() {
|
||||
_stage = _Stage.downloading;
|
||||
_progress = 0;
|
||||
});
|
||||
try {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
if (_apkPart.existsSync()) _apkPart.deleteSync();
|
||||
final res = await _client.send(http.Request('GET', Uri.parse(widget.url)));
|
||||
if (res.statusCode != 200) {
|
||||
throw HttpException('HTTP ${res.statusCode}');
|
||||
}
|
||||
final total = res.contentLength ?? -1;
|
||||
final sink = _apkPart.openWrite();
|
||||
var received = 0;
|
||||
await for (final chunk in res.stream) {
|
||||
sink.add(chunk);
|
||||
received += chunk.length;
|
||||
if (mounted && total > 0) {
|
||||
setState(() => _progress = received / total * 100);
|
||||
}
|
||||
}
|
||||
await sink.close();
|
||||
// 原子落盘:下载完成后才重命名为正式文件,避免残留半包被当成完整 APK
|
||||
_apkPart.renameSync(_apkFile.path);
|
||||
if (!mounted) return;
|
||||
await _install();
|
||||
} catch (_) {
|
||||
// 跳转失败保持页面,用户可重试
|
||||
if (_apkPart.existsSync()) _apkPart.deleteSync();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_stage = _Stage.failed;
|
||||
_progress = null;
|
||||
_message = '下载失败,请检查网络后重试';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _install() async {
|
||||
setState(() {
|
||||
_stage = _Stage.installing;
|
||||
_progress = 0;
|
||||
_message = null;
|
||||
});
|
||||
await _installSub?.cancel();
|
||||
_installSub = ApkInstaller.progress().listen((e) {
|
||||
if (!mounted) return;
|
||||
switch (e.event) {
|
||||
case 'progress':
|
||||
setState(() => _progress = e.progress.toDouble());
|
||||
break;
|
||||
case 'finished':
|
||||
final ok = e.success == true;
|
||||
setState(() {
|
||||
_stage = ok ? _Stage.finished : _Stage.failed;
|
||||
_message = ok ? '安装完成,请从桌面打开新版应用' : '安装失败,请重试';
|
||||
});
|
||||
break;
|
||||
case 'failed':
|
||||
setState(() {
|
||||
_stage = _Stage.failed;
|
||||
_message = e.error ?? '安装失败,请重试';
|
||||
});
|
||||
break;
|
||||
}
|
||||
}, onError: (Object _) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_stage = _Stage.failed;
|
||||
_message = '安装失败,请重试';
|
||||
});
|
||||
});
|
||||
final result = await ApkInstaller.install(_apkFile.path);
|
||||
if (!mounted) return;
|
||||
if (result == 'permission_required') {
|
||||
// 原生侧已拉起系统设置页;APK 已缓存,用户开启后返回再点直达安装
|
||||
setState(() {
|
||||
_stage = _Stage.failed;
|
||||
_message = '请在系统设置中允许「安装未知应用」,返回后再次点击「立即更新」(APK 已缓存,无需重新下载)';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final busy = _stage == _Stage.downloading || _stage == _Stage.installing;
|
||||
final progressText = _stage == _Stage.downloading
|
||||
? (_progress == null ? '下载中…' : '下载中 ${_progress!.round()}%')
|
||||
: (_progress == null ? '安装中…' : '安装中 ${_progress!.round()}%');
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: Scaffold(
|
||||
@@ -46,23 +183,47 @@ class UpdateScreen extends StatelessWidget {
|
||||
Icon(Icons.system_update_alt,
|
||||
size: 64, color: theme.colorScheme.primary),
|
||||
const SizedBox(height: 16),
|
||||
Text('发现新版本 $version',
|
||||
Text('发现新版本 ${widget.version}',
|
||||
style: theme.textTheme.headlineSmall),
|
||||
const SizedBox(height: 12),
|
||||
if (notes.isNotEmpty)
|
||||
Text(notes,
|
||||
if (widget.notes.isNotEmpty)
|
||||
Text(widget.notes,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(height: 1.6)),
|
||||
const SizedBox(height: 24),
|
||||
if (busy) ...[
|
||||
LinearProgressIndicator(
|
||||
value: _progress == null ? null : _progress! / 100,
|
||||
minHeight: 6,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(progressText),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
FilledButton.icon(
|
||||
onPressed: _launch,
|
||||
onPressed: busy || _stage == _Stage.finished
|
||||
? null
|
||||
: _launch,
|
||||
icon: const Icon(Icons.download),
|
||||
label: const Text('立即更新'),
|
||||
label: Text(_stage == _Stage.finished ? '已完成' : '立即更新'),
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(200, 48),
|
||||
textStyle: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
if (_message != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
_message!,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
height: 1.5,
|
||||
color: _stage == _Stage.failed
|
||||
? theme.colorScheme.error
|
||||
: Colors.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Text('不更新将无法继续使用',
|
||||
style: theme.textTheme.bodySmall
|
||||
|
||||
@@ -2,7 +2,7 @@ name: observer
|
||||
description: "视野 - 动物实时识别 (野鸡/生境), YOLOv8 + 充值付费"
|
||||
publish_to: 'none'
|
||||
|
||||
version: 1.0.2+3
|
||||
version: 1.0.4+5
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
@@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>视野管理端</title>
|
||||
<script type="module" crossorigin src="/admin/assets/index-BhTaosMq.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-COh41tJN.css">
|
||||
<script type="module" crossorigin src="/admin/assets/index-FJsP2pyZ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/admin/assets/index-BcKljL0h.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@@ -91,4 +91,43 @@ function logout() {
|
||||
.app-main {
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 手机竖屏:侧栏收为顶部横向菜单,页面自然滚动 */
|
||||
@media (max-width: 767px) {
|
||||
.app-layout {
|
||||
display: block;
|
||||
height: auto;
|
||||
}
|
||||
.app-aside {
|
||||
width: 100% !important;
|
||||
}
|
||||
.app-logo {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
font-size: 15px;
|
||||
}
|
||||
.app-aside :deep(.el-menu) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: none;
|
||||
}
|
||||
.app-aside :deep(.el-menu-item) {
|
||||
flex: 1;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
line-height: 44px;
|
||||
min-width: 96px;
|
||||
}
|
||||
.app-header {
|
||||
height: 48px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
.app-main {
|
||||
overflow: visible;
|
||||
padding: 10px;
|
||||
}
|
||||
.app-main :deep(.el-card) {
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -15,3 +15,25 @@ body {
|
||||
color: #303133;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
/* 手机竖屏适配:筛选表单换行、控件全宽、分页换行居中 */
|
||||
@media (max-width: 767px) {
|
||||
.el-form--inline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.el-form--inline .el-form-item {
|
||||
margin-right: 0;
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
.el-form--inline .el-input,
|
||||
.el-form--inline .el-select,
|
||||
.el-form--inline .el-date-editor {
|
||||
width: 100% !important;
|
||||
}
|
||||
.el-pagination {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
row-gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ onMounted(load)
|
||||
@change="load"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="addVisible" title="下发新版本(Android)" width="480px">
|
||||
<el-dialog v-model="addVisible" title="下发新版本(Android)" width="min(480px, 92vw)">
|
||||
<el-form label-width="90px">
|
||||
<el-form-item label="更新说明">
|
||||
<el-input
|
||||
@@ -184,4 +184,12 @@ onMounted(load)
|
||||
font-size: 12px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.toolbar {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -161,7 +161,7 @@ onMounted(load)
|
||||
@change="load"
|
||||
/>
|
||||
|
||||
<el-dialog v-model="grantVisible" title="手动授权" width="420px">
|
||||
<el-dialog v-model="grantVisible" title="手动授权" width="min(420px, 92vw)">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="手机号">
|
||||
<el-input :model-value="grantForm.phoneNum" disabled />
|
||||
@@ -178,7 +178,7 @@ onMounted(load)
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="remarkVisible" title="账号备注" width="420px">
|
||||
<el-dialog v-model="remarkVisible" title="账号备注" width="min(420px, 92vw)">
|
||||
<el-form label-width="80px">
|
||||
<el-form-item label="手机号">
|
||||
<el-input :model-value="remarkForm.phoneNum" disabled />
|
||||
|
||||
@@ -64,7 +64,7 @@ function login() {
|
||||
background: #001529;
|
||||
}
|
||||
.login-card {
|
||||
width: 380px;
|
||||
width: min(380px, 92vw);
|
||||
padding: 12px 8px;
|
||||
}
|
||||
.login-title {
|
||||
|
||||
Reference in New Issue
Block a user