feat: 应用列表按包名合并 + 剪贴板/安装/版本管理显示设备名
两件事都是"列表看不清楚"的问题:
1) **同一个应用传多个版本会并排显示多行**(v1.2 / v1.3 各占一行),看着像重复。
现在按 **package_name 合并成一个应用一行**,显示最新版:
- `apk_manager.list_latest()`:版本按 version_code 排序取最新,其余版本放进
`versions` 字段;解析不出包名的 APK 各自独立成行(不硬合并)
- 管理页版本列显示 `1.3(另有 1 个旧版本)`,多一个「清理旧版本」按钮
- 「删除」语义改为**删掉这个应用的所有版本**(行=应用),另给
`?scope=older` 只清理旧版本;老的单条删除行为保留(不带 scope)
- **设备端商店也走同一个列表** —— 手机上不会再看到同一个 App 的 v1.2/v1.3 两行
2) **设备只显示地址**(`192.168.20.100:5555`),同型号多台时分不清是哪一台。
三处补齐设备名(平台给设备起的名字,如 A08),统一「名称 · 地址」:
- 剪贴板注入的设备勾选列表(`/api/adb/devices` 经 `_merged_device_list` 带 name)
- 应用管理的安装弹窗(`/api/apks/install/devices` 带 name)
- 应用版本管理表格(`/api/tools/appver` 结果里带 name)
- 顺带:安装进度表里的「设备」列原来显示**型号**(而且是 STF 时代残留的空 dict),
改为显示设备名
没名字的设备(不在池里)自动回退只显示地址,不显示空占位
验证:上传同包名 v1.1/v1.2/v1.3 三个版本 → 列表只出现一行(v1.3,标注共 4 个版本)、
设备端 bootstrap 也只剩 2 个应用;`scope=older` 只留最新、`scope=package` 整包删除;
三个面板在浏览器里渲染无 JS 报错,devText 有名字/没名字两种形态都正确。
This commit is contained in:
+90
-4
@@ -127,7 +127,7 @@ class ApkManager:
|
||||
|
||||
# ================== 列表 ==================
|
||||
def list_all(self):
|
||||
"""列出所有 APK。"""
|
||||
"""列出所有 APK(**每个上传版本一行**,内部用)。"""
|
||||
try:
|
||||
with self._db():
|
||||
rows = ApkRow.query.order_by(ApkRow.upload_time.desc()).all()
|
||||
@@ -136,6 +136,42 @@ class ApkManager:
|
||||
_log.error(f"列出 APK 失败: {e}")
|
||||
return []
|
||||
|
||||
def list_latest(self):
|
||||
"""按**包名**合并后的列表:一个应用一行,只显示最新版本。
|
||||
|
||||
为什么:同一个应用传过多个版本(v1.2 / v1.3)时,列表里会并排出现两行——
|
||||
用户看到的是"同一个应用怎么重复了"。这里合并成一行(最新版),
|
||||
其余版本的信息放在 `versions` 里,界面可以提示"另有 N 个旧版本"并按需清理。
|
||||
|
||||
管理页和设备端商店都用这个(设备上选应用时同样不该出现两行同一个 App)。
|
||||
解析不出包名的 APK 各自独立成行,不做合并(避免把无关文件混在一起)。
|
||||
"""
|
||||
rows = self.list_all()
|
||||
groups = {}
|
||||
for r in rows:
|
||||
pkg = (r.get("package_name") or "").strip()
|
||||
key = pkg if pkg else ("__single__" + str(r.get("id")))
|
||||
groups.setdefault(key, []).append(r)
|
||||
|
||||
def sort_key(x):
|
||||
return (x.get("version_code") or 0, x.get("upload_time") or "")
|
||||
|
||||
out = []
|
||||
for items in groups.values():
|
||||
items.sort(key=sort_key, reverse=True)
|
||||
top = dict(items[0])
|
||||
top["version_count"] = len(items)
|
||||
top["versions"] = [{
|
||||
"id": i.get("id"),
|
||||
"version_name": i.get("version_name") or "",
|
||||
"version_code": i.get("version_code") or 0,
|
||||
"size": i.get("size") or 0,
|
||||
"upload_time": i.get("upload_time") or "",
|
||||
} for i in items]
|
||||
out.append(top)
|
||||
out.sort(key=lambda x: x.get("upload_time") or "", reverse=True)
|
||||
return out
|
||||
|
||||
# ================== 删除 ==================
|
||||
def delete(self, apk_id):
|
||||
"""删除 APK 文件和数据库记录。"""
|
||||
@@ -161,6 +197,47 @@ class ApkManager:
|
||||
_log.error(f"删除 APK 失败: {e}")
|
||||
return False, str(e)
|
||||
|
||||
def delete_package(self, apk_id, keep_latest=False):
|
||||
"""按**包名**删除:一个应用的所有版本一起处理。
|
||||
|
||||
keep_latest=True → 只删旧版本、保留最新那版("清理旧版本")
|
||||
keep_latest=False → 整个应用删掉(所有版本)
|
||||
没有包名的 APK 退回单条删除。
|
||||
"""
|
||||
try:
|
||||
with self._db():
|
||||
row = ApkRow.query.get(apk_id)
|
||||
if not row:
|
||||
return False, "APK 不存在"
|
||||
pkg = (row.package_name or "").strip()
|
||||
if not pkg:
|
||||
return self.delete(apk_id)
|
||||
|
||||
items = ApkRow.query.filter_by(package_name=pkg).all()
|
||||
items.sort(key=lambda r: ((r.version_code or 0),
|
||||
(r.upload_time or "")), reverse=True)
|
||||
targets = items[1:] if keep_latest else items
|
||||
if not targets:
|
||||
return True, "没有需要清理的旧版本"
|
||||
name = row.display_name or pkg
|
||||
for r in targets:
|
||||
p = os.path.join(APK_DIR, r.filename)
|
||||
if os.path.exists(p):
|
||||
try:
|
||||
os.remove(p)
|
||||
except Exception as e:
|
||||
_log.warning(f"删除 APK 文件失败: {e}")
|
||||
db.session.delete(r)
|
||||
db.session.commit()
|
||||
_log.info(f"已删除 {name} 的 {len(targets)} 个版本"
|
||||
f"({'保留最新' if keep_latest else '整个应用'})")
|
||||
if keep_latest:
|
||||
return True, f"已清理 {name} 的 {len(targets)} 个旧版本(保留最新)"
|
||||
return True, f"已删除 {name}(共 {len(targets)} 个版本)"
|
||||
except Exception as e:
|
||||
_log.error(f"按包名删除失败: {e}")
|
||||
return False, str(e)
|
||||
|
||||
# ================== 批量安装 ==================
|
||||
def install(self, apk_id, serials):
|
||||
"""批量安装 APK 到指定设备(后台线程执行)。
|
||||
@@ -198,13 +275,22 @@ class ApkManager:
|
||||
# 获取 worker 状态(判断设备是否运行中)
|
||||
worker_status = {w["serial"]: w for w in get_all_worker_status()}
|
||||
|
||||
# 设备名(平台给设备起的名字,如 A08)—— 进度表里显示这个而不是型号:
|
||||
# 8 台同型号时型号根本区分不了,名字才行
|
||||
pool_names = {}
|
||||
try:
|
||||
from core import device_pool
|
||||
for d in device_pool.list_configured(enabled_only=False):
|
||||
if d.get("serial"):
|
||||
pool_names[d["serial"]] = d.get("name") or ""
|
||||
except Exception as e:
|
||||
_log.warning(f"读取设备名失败(进度表将显示地址): {e}")
|
||||
|
||||
# 构造安装任务
|
||||
items = {}
|
||||
for s in serials:
|
||||
dev = all_devices.get(s, {})
|
||||
model = dev.get("model") or dev.get("manufacturer") or s
|
||||
items[s] = {
|
||||
"name": model,
|
||||
"name": pool_names.get(s) or s,
|
||||
"status": "pending",
|
||||
"msg": "",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user