fix fix fix
This commit is contained in:
423
walletsgui.py
Normal file
423
walletsgui.py
Normal file
@@ -0,0 +1,423 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wallets.json 图形管理(PySide6)。默认同目录下 servers/walletman/cmd/wallets.json。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QAction, QCloseEvent, QKeySequence
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
QFileDialog,
|
||||
QFormLayout,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
QLineEdit,
|
||||
QMainWindow,
|
||||
QMessageBox,
|
||||
QPushButton,
|
||||
QTableWidget,
|
||||
QTableWidgetItem,
|
||||
QTextEdit,
|
||||
QVBoxLayout,
|
||||
QWidget,
|
||||
)
|
||||
|
||||
# 与 servers/walletman/cmd/tools/main.go 中 walletTypeMenu 一致
|
||||
WALLET_TYPES = [
|
||||
"paytm",
|
||||
"phonepe",
|
||||
"mobikwik",
|
||||
"freecharge",
|
||||
"bharatpe business",
|
||||
"paytm business",
|
||||
"phonepe business",
|
||||
"googlepay business",
|
||||
]
|
||||
|
||||
|
||||
def default_wallets_path() -> Path:
|
||||
root = Path(__file__).resolve().parent
|
||||
for rel in (
|
||||
root / "servers" / "walletman" / "cmd" / "wallets.json",
|
||||
root / "wallets.json",
|
||||
):
|
||||
if rel.is_file():
|
||||
return rel
|
||||
return root / "servers" / "walletman" / "cmd" / "wallets.json"
|
||||
|
||||
|
||||
def parse_params(s: str) -> dict[str, Any]:
|
||||
s = s.strip()
|
||||
if not s:
|
||||
raise ValueError("params 为空")
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def format_params_pretty(s: str) -> str:
|
||||
s = s.strip()
|
||||
if not s:
|
||||
return ""
|
||||
return json.dumps(json.loads(s), ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def compact_params_json(s: str) -> str:
|
||||
return json.dumps(json.loads(s), ensure_ascii=False, separators=(",", ":"))
|
||||
|
||||
|
||||
def suggest_wallet_id(wallet_type: str, params: str) -> str:
|
||||
try:
|
||||
p = json.loads(params)
|
||||
except json.JSONDecodeError:
|
||||
return ""
|
||||
mob = p.get("mobile")
|
||||
if isinstance(mob, str) and mob:
|
||||
return f"{wallet_type}_{mob}"
|
||||
return ""
|
||||
|
||||
|
||||
class WalletEditDialog(QDialog):
|
||||
def __init__(
|
||||
self,
|
||||
parent: QWidget | None,
|
||||
title: str,
|
||||
wallet_type: str,
|
||||
wallet_id: str,
|
||||
params: str,
|
||||
allow_change_id: bool = True,
|
||||
*,
|
||||
is_add: bool = False,
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle(title)
|
||||
self.setMinimumSize(640, 480)
|
||||
self._is_add = is_add
|
||||
self._id_locked = not allow_change_id and bool(wallet_id)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
form = QFormLayout()
|
||||
self.edit_uid: QLineEdit | None = None
|
||||
if is_add:
|
||||
self.edit_uid = QLineEdit("10000")
|
||||
self.edit_uid.setPlaceholderText("如 10000")
|
||||
form.addRow("用户", self.edit_uid)
|
||||
self.combo_type = QComboBox()
|
||||
self.combo_type.setEditable(True)
|
||||
for t in WALLET_TYPES:
|
||||
self.combo_type.addItem(t)
|
||||
idx = self.combo_type.findText(wallet_type, Qt.MatchFlag.MatchFixedString)
|
||||
if idx >= 0:
|
||||
self.combo_type.setCurrentIndex(idx)
|
||||
else:
|
||||
self.combo_type.setCurrentText(wallet_type)
|
||||
self.edit_id = QLineEdit(wallet_id)
|
||||
self.edit_id.setPlaceholderText("留空则根据 params 里 mobile 自动生成")
|
||||
if self._id_locked:
|
||||
self.edit_id.setReadOnly(True)
|
||||
form.addRow("类型", self.combo_type)
|
||||
form.addRow("钱包 key", self.edit_id)
|
||||
layout.addLayout(form)
|
||||
self.text_params = QTextEdit()
|
||||
self.text_params.setFontFamily("Menlo" if sys.platform == "darwin" else "Consolas")
|
||||
self.text_params.setPlainText(params)
|
||||
layout.addWidget(QLabel("params (JSON)"))
|
||||
layout.addWidget(self.text_params, 1)
|
||||
box = QDialogButtonBox(
|
||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||
)
|
||||
box.accepted.connect(self._accept)
|
||||
box.rejected.connect(self.reject)
|
||||
layout.addWidget(box)
|
||||
|
||||
def _accept(self) -> None:
|
||||
ptxt = self.text_params.toPlainText().strip()
|
||||
try:
|
||||
parse_params(ptxt)
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
QMessageBox.warning(self, "JSON 无效", str(e))
|
||||
return
|
||||
self._ok = True
|
||||
self.accept()
|
||||
|
||||
@property
|
||||
def user_id(self) -> str:
|
||||
if not self._is_add or self.edit_uid is None:
|
||||
return "10000"
|
||||
return self.edit_uid.text().strip() or "10000"
|
||||
return self.combo_type.currentText().strip()
|
||||
|
||||
@property
|
||||
def wallet_id(self) -> str:
|
||||
return self.edit_id.text().strip()
|
||||
|
||||
@property
|
||||
def params_compact(self) -> str:
|
||||
return compact_params_json(self.text_params.toPlainText())
|
||||
|
||||
|
||||
class WalletsWindow(QMainWindow):
|
||||
COL_USER, COL_ID, COL_TYPE, COL_PREVIEW = 0, 1, 2, 3
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.setWindowTitle("Wallets 管理器")
|
||||
self.resize(1100, 600)
|
||||
self._path: Path = default_wallets_path()
|
||||
self._data: dict[str, dict[str, dict[str, str]]] = {}
|
||||
self._dirty = False
|
||||
|
||||
central = QWidget()
|
||||
self.setCentralWidget(central)
|
||||
v = QVBoxLayout(central)
|
||||
path_row = QHBoxLayout()
|
||||
self.label_path = QLabel()
|
||||
path_row.addWidget(self.label_path)
|
||||
btn_browse = QPushButton("打开…")
|
||||
btn_browse.clicked.connect(self._open_file)
|
||||
path_row.addWidget(btn_browse)
|
||||
v.addLayout(path_row)
|
||||
|
||||
self.table = QTableWidget(0, 4)
|
||||
self.table.setHorizontalHeaderLabels(["用户", "Wallet ID", "类型", "params 摘要"])
|
||||
self.table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
|
||||
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||
self.table.doubleClicked.connect(self._on_double_click)
|
||||
v.addWidget(self.table)
|
||||
|
||||
row = QHBoxLayout()
|
||||
for text, fn in [
|
||||
("重新加载", self._reload),
|
||||
("保存", self._save),
|
||||
("添加", self._add),
|
||||
("编辑", self._edit),
|
||||
("删除", self._delete),
|
||||
]:
|
||||
b = QPushButton(text)
|
||||
b.clicked.connect(fn)
|
||||
row.addWidget(b)
|
||||
v.addLayout(row)
|
||||
self._update_path_label()
|
||||
self._build_menu()
|
||||
self.statusBar().showMessage("就绪")
|
||||
self._load_file(self._path, quiet=True)
|
||||
|
||||
def _build_menu(self) -> None:
|
||||
m = self.menuBar().addMenu("文件")
|
||||
a_open = QAction("打开…", self)
|
||||
a_open.setShortcut(QKeySequence.StandardKey.Open)
|
||||
a_open.triggered.connect(self._open_file)
|
||||
m.addAction(a_open)
|
||||
a_save = QAction("保存", self)
|
||||
a_save.setShortcut(QKeySequence.StandardKey.Save)
|
||||
a_save.triggered.connect(self._save)
|
||||
m.addAction(a_save)
|
||||
m.addSeparator()
|
||||
a_exit = QAction("退出", self)
|
||||
a_exit.setShortcut(QKeySequence.StandardKey.Quit)
|
||||
a_exit.triggered.connect(self.close)
|
||||
m.addAction(a_exit)
|
||||
|
||||
def _update_path_label(self) -> None:
|
||||
self.label_path.setText(f"文件: {self._path} " + ("● 未保存" if self._dirty else ""))
|
||||
self.label_path.setTextInteractionFlags(
|
||||
Qt.TextInteractionFlag.TextSelectableByMouse
|
||||
)
|
||||
|
||||
def _set_dirty(self, d: bool) -> None:
|
||||
self._dirty = d
|
||||
self._update_path_label()
|
||||
|
||||
def _load_file(self, path: Path, quiet: bool = False) -> None:
|
||||
if not path.is_file():
|
||||
self._data = {}
|
||||
self._rebuild_table()
|
||||
self._set_dirty(False)
|
||||
if not quiet:
|
||||
QMessageBox.information(self, "提示", f"文件不存在,将使用空数据:\n{path}")
|
||||
return
|
||||
try:
|
||||
raw = path.read_text(encoding="utf-8")
|
||||
self._data = json.loads(raw)
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
QMessageBox.critical(self, "读取失败", str(e))
|
||||
return
|
||||
self._path = path.resolve()
|
||||
self._rebuild_table()
|
||||
self._set_dirty(False)
|
||||
self.setWindowTitle(f"Wallets 管理器 — {self._path.name}")
|
||||
|
||||
def _reload(self) -> None:
|
||||
if self._dirty:
|
||||
r = QMessageBox.question(
|
||||
self, "未保存", "是否丢弃未保存的修改?",
|
||||
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||
)
|
||||
if r != QMessageBox.StandardButton.Yes:
|
||||
return
|
||||
self._load_file(self._path, quiet=True)
|
||||
|
||||
def _save(self) -> None:
|
||||
self._path.parent.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
text = json.dumps(self._data, ensure_ascii=False, indent=2) + "\n"
|
||||
self._path.write_text(text, encoding="utf-8")
|
||||
except OSError as e:
|
||||
QMessageBox.critical(self, "保存失败", str(e))
|
||||
return
|
||||
self._set_dirty(False)
|
||||
QMessageBox.information(self, "已保存", str(self._path))
|
||||
|
||||
def _open_file(self) -> None:
|
||||
if self._dirty:
|
||||
r = QMessageBox.question(
|
||||
self, "未保存", "是否先保存?",
|
||||
QMessageBox.StandardButton.Save
|
||||
| QMessageBox.StandardButton.Discard
|
||||
| QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
if r == QMessageBox.StandardButton.Save:
|
||||
self._save()
|
||||
elif r == QMessageBox.StandardButton.Cancel:
|
||||
return
|
||||
path, _ = QFileDialog.getOpenFileName(
|
||||
self, "选择 wallets.json", str(self._path.parent), "JSON (*.json);;*.*"
|
||||
)
|
||||
if path:
|
||||
self._load_file(Path(path))
|
||||
|
||||
def _rebuild_table(self) -> None:
|
||||
rows: list[tuple[str, str, str, str]] = []
|
||||
for uid, wallets in self._data.items():
|
||||
for wid, ent in wallets.items():
|
||||
wt = ent.get("walletType", "")
|
||||
p = ent.get("params", "")
|
||||
prev = p[:80] + ("…" if len(p) > 80 else "")
|
||||
rows.append((str(uid), wid, str(wt), prev))
|
||||
rows.sort(key=lambda x: (x[0], x[1]))
|
||||
self.table.setRowCount(len(rows))
|
||||
for i, (uid, wid, wt, prev) in enumerate(rows):
|
||||
self.table.setItem(i, self.COL_USER, QTableWidgetItem(uid))
|
||||
self.table.setItem(i, self.COL_ID, QTableWidgetItem(wid))
|
||||
self.table.setItem(i, self.COL_TYPE, QTableWidgetItem(wt))
|
||||
self.table.setItem(i, self.COL_PREVIEW, QTableWidgetItem(prev))
|
||||
|
||||
def _selected_cell(self) -> tuple[int, int] | None:
|
||||
idx = self.table.currentIndex()
|
||||
if not idx.isValid():
|
||||
return None
|
||||
return idx.row(), idx.column()
|
||||
|
||||
def _row_to_keys(self, row: int) -> tuple[str, str] | None:
|
||||
u = self.table.item(row, self.COL_USER)
|
||||
w = self.table.item(row, self.COL_ID)
|
||||
if not u or not w:
|
||||
return None
|
||||
return u.text(), w.text()
|
||||
|
||||
def _on_double_click(self) -> None:
|
||||
self._edit()
|
||||
|
||||
def _add(self) -> None:
|
||||
default_type = WALLET_TYPES[0]
|
||||
dlg = WalletEditDialog(self, "添加钱包", default_type, "", "{}", allow_change_id=True, is_add=True)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
uid = dlg.user_id
|
||||
wt, pid, pcompact = dlg.wallet_type, dlg.wallet_id, dlg.params_compact
|
||||
if not pid:
|
||||
pid = suggest_wallet_id(wt, pcompact)
|
||||
if not pid:
|
||||
QMessageBox.warning(self, "错误", "无法生成 Wallet ID,请填写「钱包 key」或确保 params 含 mobile")
|
||||
return
|
||||
if uid not in self._data:
|
||||
self._data[uid] = {}
|
||||
existed = pid in self._data[uid]
|
||||
self._data[uid][pid] = {"walletType": wt, "params": pcompact}
|
||||
self._rebuild_table()
|
||||
self._set_dirty(True)
|
||||
if existed:
|
||||
self.statusBar().showMessage(f"已更新(原记录已存在): {uid} / {pid}", 5000)
|
||||
else:
|
||||
self.statusBar().showMessage(f"已添加: {uid} / {pid}", 5000)
|
||||
|
||||
def _edit(self) -> None:
|
||||
sel = self._selected_cell()
|
||||
if not sel:
|
||||
QMessageBox.information(self, "提示", "先选中一行")
|
||||
return
|
||||
row = sel[0]
|
||||
keys = self._row_to_keys(row)
|
||||
if not keys:
|
||||
return
|
||||
uid, wid = keys
|
||||
ent = self._data.get(uid, {}).get(wid)
|
||||
if not ent:
|
||||
return
|
||||
wt = ent["walletType"]
|
||||
try:
|
||||
pretty = format_params_pretty(ent["params"])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
pretty = str(ent.get("params", ""))
|
||||
dlg = WalletEditDialog(self, "编辑钱包", wt, wid, pretty, allow_change_id=False, is_add=False)
|
||||
if dlg.exec() != QDialog.DialogCode.Accepted:
|
||||
return
|
||||
nwt, _, pcompact = dlg.wallet_type, dlg.wallet_id, dlg.params_compact
|
||||
self._data[uid][wid] = {"walletType": nwt, "params": pcompact}
|
||||
self._rebuild_table()
|
||||
self._set_dirty(True)
|
||||
|
||||
def _delete(self) -> None:
|
||||
sel = self._selected_cell()
|
||||
if not sel:
|
||||
return
|
||||
keys = self._row_to_keys(sel[0])
|
||||
if not keys:
|
||||
return
|
||||
uid, wid = keys
|
||||
if (
|
||||
QMessageBox.question(
|
||||
self, "删除", f"确定删除 {uid} / {wid} ?",
|
||||
)
|
||||
!= QMessageBox.StandardButton.Yes
|
||||
):
|
||||
return
|
||||
self._data.get(uid, {}).pop(wid, None)
|
||||
if self._data.get(uid) == {}:
|
||||
self._data.pop(uid, None)
|
||||
self._rebuild_table()
|
||||
self._set_dirty(True)
|
||||
|
||||
def closeEvent(self, event: QCloseEvent) -> None:
|
||||
if self._dirty:
|
||||
r = QMessageBox.question(
|
||||
self, "未保存", "是否保存后退出?",
|
||||
QMessageBox.StandardButton.Save
|
||||
| QMessageBox.StandardButton.Discard
|
||||
| QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
if r == QMessageBox.StandardButton.Save:
|
||||
self._save()
|
||||
elif r == QMessageBox.StandardButton.Cancel:
|
||||
event.ignore()
|
||||
return
|
||||
event.accept()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
app = QApplication(sys.argv)
|
||||
w = WalletsWindow()
|
||||
w.show()
|
||||
sys.exit(app.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user