first commit
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
读取 db/menu_map.json, 构建功能树写入 SQLite(features 表)。
|
||||
层级: level0=顶层模块, level1=分组, level2=菜单页(叶子)
|
||||
并为每个叶子分配期望截图路径 screenshots/<序号_模块>/<分组>/<序号_名称>.jpg
|
||||
|
||||
menu_map.json 结构(由实时爬取生成):
|
||||
{
|
||||
"<模块名>": {
|
||||
"loc": "落地页URL",
|
||||
"desc": "模块释义(可选)",
|
||||
"understanding": "开发理解(可选)",
|
||||
"groups": [ {"title": "分组名", "items": [ {"name":"页名","href":"/路径","desc":"","understanding":""}, ...] } ],
|
||||
"single": {"name":"页名","href":"/路径"} # 无侧边栏的单页模块(可选)
|
||||
},
|
||||
...
|
||||
}
|
||||
"""
|
||||
import json, os, re, sqlite3
|
||||
|
||||
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
MAP = os.path.join(BASE, "db", "menu_map.json")
|
||||
DB = os.path.join(BASE, "db", "research.db")
|
||||
ORIGIN = "https://s-demob.xinrenxinshi.show"
|
||||
|
||||
|
||||
def sanitize(s):
|
||||
s = re.sub(r'[\\/:*?"<>|]', "_", s or "").strip()
|
||||
return s[:40] or "page"
|
||||
|
||||
|
||||
def full_url(href):
|
||||
if not href:
|
||||
return ""
|
||||
if href.startswith("http"):
|
||||
return href
|
||||
return ORIGIN + (href if href.startswith("/") else "/" + href)
|
||||
|
||||
|
||||
def main():
|
||||
data = json.load(open(MAP, encoding="utf-8"))
|
||||
conn = sqlite3.connect(DB)
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
cur.execute("DELETE FROM features")
|
||||
|
||||
def up(obj):
|
||||
keys = list(obj.keys()); cols = ",".join(keys); ph = ",".join(["?"] * len(keys))
|
||||
cur.execute(f"INSERT OR REPLACE INTO features ({cols}) VALUES ({ph})", [obj[k] for k in keys])
|
||||
|
||||
for mi, module in enumerate(data.keys(), 1):
|
||||
v = data[module]
|
||||
mod_key = f"mod::{module}"
|
||||
seq_mod = f"{mi:02d}_{sanitize(module)}"
|
||||
up(dict(node_key=mod_key, parent_key="", level=0, name=module,
|
||||
full_path=module, url=v.get("loc", ""),
|
||||
description=v.get("desc", f"{module}模块。"),
|
||||
understanding=v.get("understanding", ""), sort_order=mi, keep=1))
|
||||
|
||||
groups = v.get("groups", [])
|
||||
single = v.get("single")
|
||||
flat_items = v.get("items")
|
||||
# 扁平模式: 模块直接挂功能页(level2), 无中间分组层
|
||||
if not groups and flat_items:
|
||||
for ii, it in enumerate(flat_items, 1):
|
||||
nm = it["name"]; href = it.get("href", "")
|
||||
leaf_key = f"{module}::{href or nm}"
|
||||
rel = f"screenshots/{seq_mod}/{ii:02d}_{sanitize(nm)}.jpg"
|
||||
up(dict(node_key=leaf_key, parent_key=mod_key, level=2, name=nm,
|
||||
full_path=f"{module} / {nm}", url=full_url(href),
|
||||
description=it.get("desc", f"{module}模块下「{nm}」相关功能页。"),
|
||||
understanding=it.get("understanding", ""),
|
||||
screenshot_path=rel, sort_order=ii, keep=1))
|
||||
continue
|
||||
if not groups and single:
|
||||
nm = single["name"]; href = single.get("href", "")
|
||||
leaf_key = f"{module}::{href or nm}"
|
||||
rel = f"screenshots/{seq_mod}/{sanitize(nm)}.jpg"
|
||||
up(dict(node_key=leaf_key, parent_key=mod_key, level=2, name=nm,
|
||||
full_path=f"{module} / {nm}", url=full_url(href),
|
||||
description=single.get("desc", f"{module}模块下「{nm}」相关功能页。"),
|
||||
understanding=single.get("understanding", ""),
|
||||
screenshot_path=rel, sort_order=1, keep=1))
|
||||
continue
|
||||
|
||||
gi = 0
|
||||
for g in groups:
|
||||
gtitle = g.get("title") or "常规"
|
||||
gi += 1
|
||||
grp_key = f"{mod_key}::grp::{gi}_{gtitle}"
|
||||
up(dict(node_key=grp_key, parent_key=mod_key, level=1, name=gtitle,
|
||||
full_path=f"{module} / {gtitle}", url="",
|
||||
description=f"{module}模块下「{gtitle}」功能分组。",
|
||||
understanding="", sort_order=gi, keep=1))
|
||||
for ii, it in enumerate(g.get("items", []), 1):
|
||||
nm = it["name"]; href = it.get("href", "")
|
||||
leaf_key = f"{module}::{href or (gtitle + '/' + nm)}"
|
||||
rel = f"screenshots/{seq_mod}/{sanitize(gtitle)}/{ii:02d}_{sanitize(nm)}.jpg"
|
||||
up(dict(node_key=leaf_key, parent_key=grp_key, level=2, name=nm,
|
||||
full_path=f"{module} / {gtitle} / {nm}", url=full_url(href),
|
||||
description=it.get("desc", f"{module}模块下「{nm}」相关功能页。"),
|
||||
understanding=it.get("understanding", ""),
|
||||
screenshot_path=rel, sort_order=ii, keep=1))
|
||||
conn.commit()
|
||||
for lv in (0, 1, 2):
|
||||
n = cur.execute("SELECT COUNT(*) FROM features WHERE level=?", (lv,)).fetchone()[0]
|
||||
print(f"level{lv}: {n}")
|
||||
print("total:", cur.execute("SELECT COUNT(*) FROM features").fetchone()[0])
|
||||
conn.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,161 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
薪人薪事(xinrenxinshi) 功能调研 —— SQLite 数据存取工具(通用,无站点耦合)
|
||||
|
||||
用法:
|
||||
python dbtool.py init 初始化数据库表结构
|
||||
python dbtool.py add <json> 新增/更新一个功能节点(传入JSON对象)
|
||||
python dbtool.py addmany <json_array> 批量新增/更新(传入JSON数组)
|
||||
python dbtool.py setshot <node_key> <path> 设置某节点的截图路径
|
||||
python dbtool.py get <node_key> 查询单个节点
|
||||
python dbtool.py tree 以树形结构打印(JSON)
|
||||
python dbtool.py stats 统计各层级节点数
|
||||
|
||||
节点字段:
|
||||
node_key 唯一键(建议用 URL 或 path 组合, 用于去重更新)
|
||||
parent_key 父节点的 node_key (顶层为空)
|
||||
level 0=顶层模块 1=分组 2=菜单页 3=子页/Tab
|
||||
name 功能名称
|
||||
full_path 全路径名称(面包屑, 如 "人事 / 员工管理 / 员工花名册")
|
||||
url 页面URL
|
||||
description 释义(功能是做什么的)
|
||||
understanding 我理解的信息(交互/字段/价值等)
|
||||
screenshot_path 截图相对路径
|
||||
sort_order 排序
|
||||
keep 是否保留(供管理员勾选, 默认1=保留)
|
||||
note 审核备注
|
||||
"""
|
||||
import sqlite3, sys, json, os
|
||||
|
||||
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "db", "research.db")
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS features (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
node_key TEXT UNIQUE NOT NULL,
|
||||
parent_key TEXT,
|
||||
level INTEGER NOT NULL DEFAULT 0,
|
||||
name TEXT NOT NULL,
|
||||
full_path TEXT,
|
||||
url TEXT,
|
||||
description TEXT,
|
||||
understanding TEXT,
|
||||
screenshot_path TEXT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
keep INTEGER DEFAULT 1,
|
||||
note TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime')),
|
||||
updated_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_parent ON features(parent_key);
|
||||
CREATE INDEX IF NOT EXISTS idx_level ON features(level);
|
||||
|
||||
-- 深层界面截图(弹窗/筛选/自定义列/详情)
|
||||
CREATE TABLE IF NOT EXISTS page_shots (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
node_key TEXT NOT NULL,
|
||||
kind TEXT NOT NULL, -- main/add/filter/columns/detail
|
||||
label TEXT,
|
||||
path TEXT NOT NULL,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ps_node ON page_shots(node_key);
|
||||
|
||||
-- 真实导出附件
|
||||
CREATE TABLE IF NOT EXISTS downloads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
node_key TEXT,
|
||||
file_path TEXT NOT NULL,
|
||||
size INTEGER,
|
||||
kind TEXT,
|
||||
created_at TEXT DEFAULT (datetime('now','localtime'))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_dl_node ON downloads(node_key);
|
||||
"""
|
||||
|
||||
FIELDS = ["node_key","parent_key","level","name","full_path","url",
|
||||
"description","understanding","screenshot_path","sort_order","keep","note"]
|
||||
|
||||
def conn():
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
c = sqlite3.connect(DB_PATH)
|
||||
c.row_factory = sqlite3.Row
|
||||
return c
|
||||
|
||||
def init():
|
||||
c = conn()
|
||||
c.executescript(SCHEMA)
|
||||
c.commit(); c.close()
|
||||
print("DB initialized at", DB_PATH)
|
||||
|
||||
def upsert(c, obj):
|
||||
keys = [k for k in FIELDS if k in obj]
|
||||
placeholders = ",".join(["?"]*len(keys))
|
||||
cols = ",".join(keys)
|
||||
updates = ",".join([f"{k}=excluded.{k}" for k in keys if k != "node_key"])
|
||||
sql = f"INSERT INTO features ({cols}) VALUES ({placeholders}) " \
|
||||
f"ON CONFLICT(node_key) DO UPDATE SET {updates}, updated_at=datetime('now','localtime')"
|
||||
c.execute(sql, [obj[k] for k in keys])
|
||||
|
||||
def add(obj):
|
||||
c = conn(); upsert(c, obj); c.commit(); c.close()
|
||||
print("ok:", obj.get("node_key"))
|
||||
|
||||
def addmany(arr):
|
||||
c = conn()
|
||||
for obj in arr:
|
||||
upsert(c, obj)
|
||||
c.commit(); c.close()
|
||||
print("ok addmany:", len(arr))
|
||||
|
||||
def setshot(node_key, path):
|
||||
c = conn()
|
||||
c.execute("UPDATE features SET screenshot_path=?, updated_at=datetime('now','localtime') WHERE node_key=?", (path, node_key))
|
||||
c.commit(); n = c.total_changes; c.close()
|
||||
print("updated" if n else "not found", node_key)
|
||||
|
||||
def get(node_key):
|
||||
c = conn()
|
||||
r = c.execute("SELECT * FROM features WHERE node_key=?", (node_key,)).fetchone()
|
||||
c.close()
|
||||
print(json.dumps(dict(r), ensure_ascii=False) if r else "null")
|
||||
|
||||
def stats():
|
||||
c = conn()
|
||||
rows = c.execute("SELECT level, COUNT(*) n FROM features GROUP BY level ORDER BY level").fetchall()
|
||||
total = c.execute("SELECT COUNT(*) n FROM features").fetchone()["n"]
|
||||
shots = c.execute("SELECT COUNT(*) n FROM features WHERE screenshot_path IS NOT NULL AND screenshot_path<>''").fetchone()["n"]
|
||||
c.close()
|
||||
for r in rows:
|
||||
print(f"level {r['level']}: {r['n']}")
|
||||
print(f"total: {total}, with_screenshot: {shots}")
|
||||
|
||||
def tree():
|
||||
c = conn()
|
||||
rows = [dict(r) for r in c.execute("SELECT * FROM features ORDER BY level, sort_order, id").fetchall()]
|
||||
c.close()
|
||||
by_parent = {}
|
||||
for r in rows:
|
||||
by_parent.setdefault(r["parent_key"] or "", []).append(r)
|
||||
def build(pk):
|
||||
out = []
|
||||
for r in by_parent.get(pk, []):
|
||||
node = {k: r[k] for k in ("node_key","name","level","url","description","understanding","screenshot_path","keep")}
|
||||
node["children"] = build(r["node_key"])
|
||||
out.append(node)
|
||||
return out
|
||||
print(json.dumps(build(""), ensure_ascii=False))
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
if cmd == "init": init()
|
||||
elif cmd == "add": add(json.loads(sys.argv[2]))
|
||||
elif cmd == "addmany": addmany(json.loads(sys.argv[2]))
|
||||
elif cmd == "setshot": setshot(sys.argv[2], sys.argv[3])
|
||||
elif cmd == "get": get(sys.argv[2])
|
||||
elif cmd == "stats": stats()
|
||||
elif cmd == "tree": tree()
|
||||
else:
|
||||
print(__doc__)
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
审核后端服务(薪人薪事 功能调研)。
|
||||
- 打开 http://localhost:8788/ 进入"一页一个功能"的审核界面
|
||||
- 每次点击 保留/删除 立即写回 SQLite(features.keep)
|
||||
- 提供 API: 功能列表 / 更新保留状态 / 备注 / 附件清单 / Excel 预览 / 导出 selection.json
|
||||
依赖: Flask
|
||||
用法: python serve.py 然后浏览器打开 http://localhost:8788
|
||||
"""
|
||||
import os, json, sqlite3, time, re
|
||||
from flask import Flask, request, jsonify, send_from_directory, Response
|
||||
|
||||
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DB = os.path.join(BASE, "db", "research.db")
|
||||
DOCS = os.path.join(BASE, "docs")
|
||||
DL_ROOT = os.path.join(BASE, "downloads")
|
||||
REVIEW_HTML = os.path.join(DOCS, "审核.html")
|
||||
EXPORT_JSON = os.path.join(DOCS, "selection.json")
|
||||
EXCEL_EXT = (".xlsx", ".xls", ".docx", ".doc", ".pdf")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def db():
|
||||
conn = sqlite3.connect(DB)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def load_features():
|
||||
conn = db()
|
||||
rows = [dict(r) for r in conn.execute(
|
||||
"SELECT node_key,parent_key,level,name,full_path,url,description,"
|
||||
"understanding,screenshot_path,sort_order,keep,note FROM features "
|
||||
"ORDER BY sort_order,id").fetchall()]
|
||||
kind_cn = {"add": "主操作弹窗/表单", "filter": "筛选面板",
|
||||
"columns": "自定义列/选择表头", "detail": "行详情/编辑"}
|
||||
deep = {}
|
||||
try:
|
||||
for r in conn.execute("SELECT node_key,kind,label,path FROM page_shots "
|
||||
"WHERE kind<>'main' ORDER BY node_key,id").fetchall():
|
||||
deep.setdefault(r["node_key"], []).append({
|
||||
"kind": r["kind"], "kind_cn": kind_cn.get(r["kind"], r["kind"]),
|
||||
"label": r["label"] or "", "shot": "/shot/" + r["path"]})
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
dls = {}
|
||||
try:
|
||||
for r in conn.execute("SELECT node_key,file_path,size,kind FROM downloads "
|
||||
"WHERE node_key IS NOT NULL AND node_key<>'' ORDER BY file_path").fetchall():
|
||||
fp = r["file_path"] or ""
|
||||
name = fp.rsplit("/", 1)[-1]
|
||||
sz = r["size"]
|
||||
if not sz:
|
||||
ap = os.path.join(BASE, fp)
|
||||
sz = os.path.getsize(ap) if os.path.isfile(ap) else 0
|
||||
dls.setdefault(r["node_key"], []).append(
|
||||
{"name": name, "rel": fp, "size": sz or 0, "kind": r["kind"] or ""})
|
||||
except sqlite3.OperationalError:
|
||||
pass
|
||||
conn.close()
|
||||
by_parent = {}
|
||||
for r in rows:
|
||||
by_parent.setdefault(r["parent_key"] or "", []).append(r)
|
||||
|
||||
leaves = []
|
||||
|
||||
def dfs(pk, module, group):
|
||||
for r in by_parent.get(pk, []):
|
||||
m = r["name"] if r["level"] == 0 else module
|
||||
g = r["name"] if r["level"] == 1 else group
|
||||
children = by_parent.get(r["node_key"])
|
||||
if children:
|
||||
dfs(r["node_key"], m, g)
|
||||
else:
|
||||
shot = ("/shot/" + r["screenshot_path"]) if r.get("screenshot_path") else ""
|
||||
leaves.append({
|
||||
"node_key": r["node_key"], "name": r["name"], "module": m,
|
||||
"group": g if g and g != r["name"] else "",
|
||||
"full_path": r["full_path"], "url": r["url"] or "",
|
||||
"description": r["description"] or "",
|
||||
"understanding": r["understanding"] or "",
|
||||
"shot": shot, "deep_shots": deep.get(r["node_key"], []),
|
||||
"downloads": dls.get(r["node_key"], []),
|
||||
"keep": 1 if (r["keep"] is None or r["keep"] == 1) else 0,
|
||||
"note": r.get("note") or "",
|
||||
})
|
||||
|
||||
dfs("", "", "")
|
||||
return leaves
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
with open(REVIEW_HTML, "r", encoding="utf-8") as f:
|
||||
return Response(f.read(), mimetype="text/html")
|
||||
|
||||
|
||||
@app.route("/api/features")
|
||||
def api_features():
|
||||
feats = load_features()
|
||||
keep = sum(1 for f in feats if f["keep"])
|
||||
return jsonify({"total": len(feats), "keep": keep, "items": feats})
|
||||
|
||||
|
||||
@app.route("/api/keep", methods=["POST"])
|
||||
def api_keep():
|
||||
data = request.get_json(force=True)
|
||||
node_key = data.get("node_key")
|
||||
keep = 1 if data.get("keep") else 0
|
||||
if not node_key:
|
||||
return jsonify({"ok": False, "err": "missing node_key"}), 400
|
||||
now = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
conn = db()
|
||||
conn.execute("UPDATE features SET keep=?, updated_at=? WHERE node_key=?", (keep, now, node_key))
|
||||
conn.commit(); conn.close()
|
||||
return jsonify({"ok": True, "node_key": node_key, "keep": keep})
|
||||
|
||||
|
||||
@app.route("/api/keep_batch", methods=["POST"])
|
||||
def api_keep_batch():
|
||||
data = request.get_json(force=True)
|
||||
keys = data.get("node_keys") or []
|
||||
keep = 1 if data.get("keep") else 0
|
||||
now = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
conn = db()
|
||||
conn.executemany("UPDATE features SET keep=?, updated_at=? WHERE node_key=?",
|
||||
[(keep, now, k) for k in keys])
|
||||
conn.commit(); conn.close()
|
||||
return jsonify({"ok": True, "updated": len(keys), "keep": keep})
|
||||
|
||||
|
||||
@app.route("/api/note", methods=["POST"])
|
||||
def api_note():
|
||||
data = request.get_json(force=True)
|
||||
node_key = data.get("node_key")
|
||||
note = data.get("note") or ""
|
||||
if not node_key:
|
||||
return jsonify({"ok": False, "err": "missing node_key"}), 400
|
||||
now = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
conn = db()
|
||||
conn.execute("UPDATE features SET note=?, updated_at=? WHERE node_key=?", (note, now, node_key))
|
||||
conn.commit(); conn.close()
|
||||
return jsonify({"ok": True, "node_key": node_key})
|
||||
|
||||
|
||||
@app.route("/api/export", methods=["POST", "GET"])
|
||||
def api_export():
|
||||
feats = load_features()
|
||||
kept = sum(1 for f in feats if f["keep"])
|
||||
mods = {}
|
||||
for f in feats:
|
||||
m = mods.setdefault(f["module"], {"module": f["module"], "total": 0, "keep": 0})
|
||||
m["total"] += 1
|
||||
m["keep"] += f["keep"]
|
||||
|
||||
def item(f):
|
||||
return {
|
||||
"module": f["module"], "group": f["group"], "name": f["name"],
|
||||
"node_key": f["node_key"], "full_path": f["full_path"], "url": f["url"],
|
||||
"keep": f["keep"], "description": f["description"],
|
||||
"understanding": f["understanding"], "note": f["note"],
|
||||
"screenshot": f["shot"].replace("/shot/", "") if f["shot"] else "",
|
||||
}
|
||||
|
||||
out = {
|
||||
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"source": "薪人薪事 https://s-demob.xinrenxinshi.show 功能调研",
|
||||
"total": len(feats), "keep": kept, "dropped": len(feats) - kept,
|
||||
"module_summary": sorted(mods.values(), key=lambda x: -x["keep"]),
|
||||
"items": [item(f) for f in feats],
|
||||
"kept_items": [item(f) for f in feats if f["keep"]],
|
||||
}
|
||||
os.makedirs(DOCS, exist_ok=True)
|
||||
with open(EXPORT_JSON, "w", encoding="utf-8") as f:
|
||||
json.dump(out, f, ensure_ascii=False, indent=2)
|
||||
return jsonify({"ok": True, "path": "docs/selection.json",
|
||||
"total": out["total"], "keep": out["keep"], "dropped": out["dropped"]})
|
||||
|
||||
|
||||
@app.route("/shot/<path:relpath>")
|
||||
def shot(relpath):
|
||||
return send_from_directory(BASE, relpath)
|
||||
|
||||
|
||||
def _strip_num(s):
|
||||
return re.sub(r"^\d+_", "", s or "").strip()
|
||||
|
||||
|
||||
def scan_attachments():
|
||||
groups = []
|
||||
if not os.path.isdir(DL_ROOT):
|
||||
return groups
|
||||
for root, _, files in os.walk(DL_ROOT):
|
||||
fs = [f for f in files if f.lower().endswith(EXCEL_EXT)]
|
||||
if not fs:
|
||||
continue
|
||||
rel = os.path.relpath(root, DL_ROOT)
|
||||
segs = [] if rel == "." else rel.split(os.sep)
|
||||
seg0 = segs[0] if segs else ""
|
||||
module = "任务中心" if seg0.startswith("_任务") else _strip_num(seg0)
|
||||
group = segs[1] if len(segs) > 1 else ""
|
||||
items = []
|
||||
for f in sorted(fs):
|
||||
p = os.path.join(root, f)
|
||||
items.append({"name": f, "size": os.path.getsize(p),
|
||||
"rel": os.path.relpath(p, BASE)})
|
||||
groups.append({"dir": rel.replace(os.sep, " / "), "module": module,
|
||||
"group": group, "files": items})
|
||||
groups.sort(key=lambda g: g["dir"])
|
||||
return groups
|
||||
|
||||
|
||||
@app.route("/api/attachments")
|
||||
def api_attachments():
|
||||
gs = scan_attachments()
|
||||
total = sum(len(g["files"]) for g in gs)
|
||||
return jsonify({"total": total, "groups": gs})
|
||||
|
||||
|
||||
@app.route("/dl/<path:relpath>")
|
||||
def dl(relpath):
|
||||
d = os.path.dirname(relpath)
|
||||
fn = os.path.basename(relpath)
|
||||
return send_from_directory(os.path.join(BASE, d), fn, as_attachment=True)
|
||||
|
||||
|
||||
SHEET_EXT = (".xlsx", ".xls")
|
||||
MAX_ROWS = 500
|
||||
MAX_COLS = 60
|
||||
|
||||
|
||||
def _parse_xlsx(path):
|
||||
import openpyxl
|
||||
wb = openpyxl.load_workbook(path, read_only=True, data_only=True)
|
||||
sheets = []
|
||||
for ws in wb.worksheets:
|
||||
rows = []
|
||||
for i, row in enumerate(ws.iter_rows(values_only=True)):
|
||||
if i >= MAX_ROWS:
|
||||
break
|
||||
cells = [("" if v is None else str(v)) for v in row[:MAX_COLS]]
|
||||
rows.append(cells)
|
||||
sheets.append({"name": ws.title, "rows": rows,
|
||||
"total_rows": ws.max_row, "total_cols": ws.max_column})
|
||||
wb.close()
|
||||
return sheets
|
||||
|
||||
|
||||
def _parse_xls(path):
|
||||
import xlrd, warnings
|
||||
warnings.filterwarnings("ignore")
|
||||
wb = xlrd.open_workbook(path)
|
||||
sheets = []
|
||||
for ws in wb.sheets():
|
||||
rows = []
|
||||
for r in range(min(MAX_ROWS, ws.nrows)):
|
||||
cells = []
|
||||
for cc in range(min(MAX_COLS, ws.ncols)):
|
||||
v = ws.cell_value(r, cc)
|
||||
if isinstance(v, float) and v == int(v):
|
||||
v = int(v)
|
||||
cells.append("" if v == "" else str(v))
|
||||
rows.append(cells)
|
||||
sheets.append({"name": ws.name, "rows": rows,
|
||||
"total_rows": ws.nrows, "total_cols": ws.ncols})
|
||||
return sheets
|
||||
|
||||
|
||||
@app.route("/api/preview")
|
||||
def api_preview():
|
||||
rel = request.args.get("rel", "")
|
||||
abspath = os.path.abspath(os.path.join(BASE, rel))
|
||||
if not abspath.startswith(DL_ROOT + os.sep) or not os.path.isfile(abspath):
|
||||
return jsonify({"ok": False, "err": "file not found or forbidden"}), 404
|
||||
low = abspath.lower()
|
||||
if not low.endswith(SHEET_EXT):
|
||||
return jsonify({"ok": False, "err": "not a spreadsheet", "kind": "other"}), 200
|
||||
try:
|
||||
sheets = _parse_xls(abspath) if low.endswith(".xls") else _parse_xlsx(abspath)
|
||||
except Exception as e:
|
||||
return jsonify({"ok": False, "err": str(e)[:200]}), 200
|
||||
return jsonify({"ok": True, "name": os.path.basename(abspath),
|
||||
"rel": rel, "sheets": sheets,
|
||||
"max_rows": MAX_ROWS, "max_cols": MAX_COLS})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=8788, debug=False, threaded=True)
|
||||
@@ -0,0 +1,12 @@
|
||||
import os, sqlite3, datetime
|
||||
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DB = os.path.join(BASE, "db", "research.db")
|
||||
NOTE = "该员工自助培训页为嵌入式LMS,持续加载无法稳定截图,已跳过。"
|
||||
conn = sqlite3.connect(DB)
|
||||
now = datetime.datetime.now().isoformat(timespec="seconds")
|
||||
cur = conn.execute(
|
||||
"UPDATE features SET note=?, updated_at=? WHERE screenshot_path LIKE ?",
|
||||
(NOTE, now, "%09_我的培训%"))
|
||||
conn.commit()
|
||||
print("rows updated:", cur.rowcount)
|
||||
conn.close()
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""列出待截图 / 已截图的功能页,供截图流程驱动。
|
||||
用法:
|
||||
python scripts/shot_queue.py # 列出所有 level2 页面(JSON)
|
||||
python scripts/shot_queue.py --pending # 只列出截图文件尚不存在的页面
|
||||
"""
|
||||
import os, json, sqlite3, sys
|
||||
|
||||
BASE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
DB = os.path.join(BASE, "db", "research.db")
|
||||
|
||||
|
||||
def main():
|
||||
pending = "--pending" in sys.argv
|
||||
conn = sqlite3.connect(DB)
|
||||
conn.row_factory = sqlite3.Row
|
||||
rows = [dict(r) for r in conn.execute(
|
||||
"SELECT name, full_path, url, screenshot_path FROM features "
|
||||
"WHERE level=2 ORDER BY sort_order, id").fetchall()]
|
||||
conn.close()
|
||||
out = []
|
||||
for r in rows:
|
||||
rel = r["screenshot_path"]
|
||||
ap = os.path.join(BASE, rel) if rel else ""
|
||||
exists = bool(ap) and os.path.isfile(ap)
|
||||
if pending and exists:
|
||||
continue
|
||||
out.append({
|
||||
"name": r["name"], "full_path": r["full_path"], "url": r["url"],
|
||||
"rel": rel, "abs": ap.replace("\\", "/"), "exists": exists,
|
||||
})
|
||||
print(json.dumps({"count": len(out), "pages": out}, ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user