Downloads · 30 days
0
Mike990217/mii
mii is a machine learning model from Mike990217. Use it for the machine learning task on the model card, and read the license before you ship it in a product.
Downloads · 30 days
0
Access
Public
Updated Sep 12, 2025
Repo size
—
Likes
0
Public
Click a slice to open those files.
.md19.6 KB · 50%
From the Hugging Face model README
from flask import ( Flask, request, redirect, url_for, render_template_string, jsonify, make_response ) import os, sqlite3, csv, io, datetime from contextlib import closing
APP_TITLE = "校務意見回饋中心" DB_PATH = os.environ.get("FEEDBACK_DB", "feedback.db") ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN") # 若設置,/export.csv 需要 ?token=xxx
app = Flask(name)
def init_db(): with closing(sqlite3.connect(DB_PATH)) as conn: c = conn.cursor() c.execute(""" CREATE TABLE IF NOT EXISTS feedback ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts TEXT NOT NULL, role TEXT, category TEXT, title TEXT, details TEXT, priority TEXT, name TEXT, email TEXT, user_agent TEXT, page TEXT, ip TEXT ); """) c.execute("CREATE INDEX IF NOT EXISTS idx_feedback_ts ON feedback(ts);") conn.commit()
def insert_feedback(data): with closing(sqlite3.connect(DB_PATH)) as conn: c = conn.cursor() c.execute(""" INSERT INTO feedback (ts, role, category, title, details, priority, name, email, user_agent, page, ip) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( datetime.datetime.utcnow().isoformat(), data.get("role","").strip(), data.get("category","").strip(), data.get("title","").strip()[:120], data.get("details","").strip()[:8000], data.get("priority","").strip(), data.get("name","").strip()[:80], data.get("email","").strip()[:120], data.get("userAgent","").strip()[:200], data.get("page","").strip()[:200], data.get("ip","").strip()[:100], )) conn.commit()
def month_range(year:int, month:int): start = datetime.datetime(year, month, 1) if month == 12: end = datetime.datetime(year+1, 1, 1) else: end = datetime.datetime(year, month+1, 1) return start, end
def previous_month(today=None): if not today: today = datetime.date.today() year = today.year month = today.month - 1 if month == 0: year -= 1 month = 12 return year, month
def query_summary(year:int, month:int): start, end = month_range(year, month) start_iso = start.isoformat() end_iso = end.isoformat() with closing(sqlite3.connect(DB_PATH)) as conn: c = conn.cursor() # 總筆數 c.execute("SELECT COUNT(*) FROM feedback WHERE ts >= ? AND ts < ?", (start_iso, end_iso)) total = c.fetchone()[0]
def count_by(field):
c.execute(f"""
SELECT {field}, COUNT(*)
FROM feedback
WHERE ts >= ? AND ts < ?
GROUP BY {field}
ORDER BY COUNT(*) DESC
""", (start_iso, end_iso))
rows = c.fetchall()
return [{"label": (r[0] or "未填"), "count": r[1]} for r in rows]
by_category = count_by("category")
by_role = count_by("role")
by_priority = count_by("priority")
# 範例:抓取該月最新 10 筆(供頁面展示)
c.execute("""
SELECT ts, role, category, title, priority
FROM feedback
WHERE ts >= ? AND ts < ?
ORDER BY ts DESC
LIMIT 10
""", (start_iso, end_iso))
latest = [{"ts": r[0], "role": r[1], "category": r[2], "title": r[3], "priority": r[4]} for r in c.fetchall()]
return {
"period": f"{year:04d}-{month:02d}",
"total": total,
"byCategory": by_category,
"byRole": by_role,
"byPriority": by_priority,
"latest": latest
}
BASE_STYLE = """
<style> :root{--bg:#fff;--text:#0f172a;--muted:#475569;--surface:#f1f5f9;--brand:#155e75;--ring:rgba(21,94,117,.28);} *{box-sizing:border-box} body{margin:0;font-family:ui-sans-serif,-apple-system,BlinkMacSystemFont,"Noto Sans TC","Microsoft JhengHei",Arial,sans-serif;color:var(--text);background:var(--bg);line-height:1.6} .container{max-width:1000px;margin:0 auto;padding:0 16px} .site-header{position:sticky;top:0;z-index:10;background:#ffffffcc;backdrop-filter:saturate(180%) blur(8px);border-bottom:1px solid #e5e7eb} .header-inner{display:flex;align-items:center;justify-content:space-between;min-height:64px} .logo{display:flex;gap:.5rem;align-items:center;font-weight:700;text-decoration:none;color:inherit} .section{padding:56px 0} .tint{background:var(--surface)} h1{font-size:2rem;margin:.2rem 0 1rem} h2{margin:0 0 .75rem} .grid{display:grid;gap:16px} .two{grid-template-columns:repeat(2,1fr)} @media(max-width:820px){.two{grid-template-columns:1fr}} .field{display:grid;gap:.35rem} input,select,textarea{border:1px solid #cbd5e1;border-radius:12px;padding:.7rem 1rem;font:inherit} input:focus,select:focus,textarea:focus{outline:3px solid var(--ring);outline-offset:2px} .hint{color:var(--muted);font-size:.9rem} .actions{display:flex;gap:.6rem;flex-wrap:wrap;margin-top:.2rem} .btn{display:inline-flex;align-items:center;gap:.4rem;padding:.6rem .9rem;border-radius:10px;border:1px solid transparent;cursor:pointer;text-decoration:none;font-weight:700} .primary{background:var(--brand);color:#fff} .ghost{border:1px solid #94a3b8;color:var(--brand);background:#fff} .nav a{display:inline-block;padding:.5rem .8rem;border-radius:10px;text-decoration:none;color:inherit} .card{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:16px} .kvs{display:grid;grid-template-columns:repeat(3,1fr);gap:12px} .kv{border:1px dashed #cbd5e1;border-radius:10px;padding:10px;text-align:center} .muted{color:var(--muted)} .footer{padding:24px 0;border-top:1px solid #e5e7eb} .form-status{min-height:1.4em;color:var(--brand);margin-top:.4rem} small.badge{display:inline-block;border:1px solid #cbd5e1;border-radius:999px;padding:.1rem .5rem;margin-left:.4rem;color:#334155;background:#f8fafc} </style>"""
INDEX_HTML = """
<!doctype html> <html lang="zh-Hant"> <head> <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> <title>{{ title }}</title> <meta name="description" content="蒐集全校教職員生、家長與校友對學校的改進建議,並每月統計彙整。"> """ + BASE_STYLE + """ </head> <body> <header class="site-header"> <div class="container header-inner"> <a class="logo" href="{{ url_for('index') }}">📮 {{ title }}</a> <nav class="nav"> <a href="{{ url_for('index') }}">提交建議</a> <a href="{{ url_for('summary_page') }}">每月彙整</a> </nav> </div> </header> <main> <section class="section tint"> <div class="container"> <h1>一起讓學校更好</h1> <p class="muted">學生、家長、老師、職員、校友都可以提交具體可行的建議。資料將用於校務改善。</p> <form method="post" action="{{ url_for('submit_feedback') }}" class="grid" onsubmit="return validateAgree()"> <div class="grid two"> <div class="field"> <label for="role">您的身分 <small class="badge">必填</small></label> <select id="role" name="role" required> <option value="" selected disabled>請選擇</option> <option>學生</option><option>家長</option><option>教師</option> <option>職員</option><option>校友</option><option>其他</option> </select> <small class="hint">可匿名,但請選擇角色以利統計</small> </div> <div class="field"> <label for="category">建議主題 <small class="badge">必填</small></label> <select id="category" name="category" required> <option value="" selected disabled>請選擇</option> <option>教學與課程</option><option>設備與空間</option><option>校園環境</option> <option>餐飲/福利社</option><option>交通/接駁</option><option>行政流程</option> <option>資訊系統/網路</option><option>安全/衛生</option><option>活動與社團</option><option>其他</option> </select> </div> </div> <div class="grid two">
<div class="field">
<label for="title">建議標題 <small class="badge">必填</small></label>
<input id="title" name="title" type="text" maxlength="120" required placeholder="例:圖書館考前延長開館時間">
</div>
<div class="field">
<label for="priority">優先度 <small class="badge">必填</small></label>
<select id="priority" name="priority" required>
<option value="" selected disabled>請選擇</option>
<option>高</option><option>中</option><option>低</option>
</select>
</div>
</div>
<div class="field">
<label for="details">具體描述 <small class="badge">必填</small></label>
<textarea id="details" name="details" rows="6" required placeholder="說明現況、改善方向、受影響對象、可能方案、可接受時程…"></textarea>
</div>
<details>
<summary>(可選)聯絡方式</summary>
<div class="grid two">
<div class="field"><label for="name">姓名/暱稱</label><input id="name" name="name" type="text" maxlength="80"></div>
<div class="field"><label for="email">Email</label><input id="email" name="email" type="email" maxlength="120" placeholder="you@example.com"></div>
</div>
</details>
<div class="field">
<label><input id="agree" type="checkbox" required> 我已閱讀並同意下方隱私聲明</label>
<div class="card" style="margin-top:.5rem">
<strong>隱私簡要:</strong>
<ul>
<li>可匿名;若提供 Email 僅用於必要聯繫,不對外公開。</li>
<li>資料僅用於校務改善統計與決策,不做其他用途。</li>
<li>系統會記錄提交時間與基本技術資訊(如瀏覽器/IP)以防濫用。</li>
</ul>
</div>
</div>
<div class="actions">
<button class="btn primary" type="submit">送出建議</button>
<button class="btn ghost" type="reset">清除</button>
</div>
<p id="status" class="form-status"></p>
</form>
</div>
</section>
</main>
<footer class="footer">
<div class="container">
<span>© {{ year }} {{ title }}</span>
<span class="muted" style="float:right">Made with Flask + SQLite</span>
</div>
</footer>
<script>
function validateAgree(){
if(!document.getElementById('agree').checked){
document.getElementById('status').textContent = "請先勾選同意隱私聲明。";
return false;
}
return true;
}
</script>
</body>
</html>
"""
SUMMARY_HTML = """
<!doctype html> <html lang="zh-Hant"> <head> <meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"> <title>每月彙整 │ {{ title }}</title> """ + BASE_STYLE + """ </head> <body> <header class="site-header"> <div class="container header-inner"> <a class="logo" href="{{ url_for('index') }}">📮 {{ title }}</a> <nav class="nav"><a href="{{ url_for('index') }}">提交建議</a></nav> </div> </header> <main class="section"> <div class="container"> <h1>每月彙整</h1> <form method="get" class="actions" style="margin:0 0 12px"> <label>年份 <input type="number" name="year" value="{{ year }}" min="2000" max="2100"></label> <label>月份 <input type="number" name="month" value="{{ month }}" min="1" max="12"></label> <button class="btn primary" type="submit">切換</button> <a class="btn ghost" href="{{ url_for('export_csv') }}?year={{ year }}&month={{ month }}{% if token %}&token={{ token }}{% endif %}">下載 CSV</a> </form>{% if summary.total == 0 %}
<p class="muted">該月份尚無資料。</p>
{% else %}
<div class="card">
<strong>期間:</strong>{{ summary.period }}
<strong>總筆數:</strong>{{ summary.total }}
</div>
<div class="grid two" style="margin-top:12px">
<div class="card">
<h3>依主題</h3>
<div class="kvs">
{% for it in summary.byCategory %}
<div class="kv"><div>{{ it.label or '未填' }}</div><div><strong>{{ it.count }}</strong></div></div>
{% endfor %}
</div>
</div>
<div class="card">
<h3>依身分</h3>
<div class="kvs">
{% for it in summary.byRole %}
<div class="kv"><div>{{ it.label or '未填' }}</div><div><strong>{{ it.count }}</strong></div></div>
{% endfor %}
</div>
</div>
</div>
<div class="card" style="margin-top:12px">
<h3>依優先度</h3>
<div class="kvs">
{% for it in summary.byPriority %}
<div class="kv"><div>{{ it.label or '未填' }}</div><div><strong>{{ it.count }}</strong></div></div>
{% endfor %}
</div>
</div>
<div class="card" style="margin-top:12px">
<h3>最新 10 筆</h3>
<ul>
{% for it in summary.latest %}
<li>[{{ it.ts[:16].replace('T',' ') }}] ({{ it.role or '未填' }}/{{ it.category or '未填' }}/{{ it.priority or '未填' }}):{{ it.title }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</main>
<footer class="footer">
<div class="container">
<span>© {{ now.year }} {{ title }}</span>
</div>
</footer>
</body>
</html>
"""
@app.before_first_request def _bootstrap(): init_db()
@app.route("/", methods=["GET"]) def index(): return render_template_string(INDEX_HTML, title=APP_TITLE, year=datetime.date.today().year)
@app.route("/submit", methods=["POST"]) def submit_feedback(): # 來自 HTML form data = { "role": request.form.get("role",""), "category": request.form.get("category",""), "title": request.form.get("title",""), "details": request.form.get("details",""), "priority": request.form.get("priority",""), "name": request.form.get("name",""), "email": request.form.get("email",""), "userAgent": request.headers.get("User-Agent",""), "page": request.referrer or "", "ip": request.headers.get("X-Forwarded-For", request.remote_addr or "") } # 簡單驗證 if not (data["role"] and data["category"] and data["title"] and data["details"] and data["priority"]): return "欄位不足", 400 insert_feedback(data) return redirect(url_for("thanks"))
@app.route("/thanks") def thanks(): # 簡單感謝頁 html = f"""<!doctype html><meta charset="utf-8"> <title>感謝提交 │ {APP_TITLE}</title> {BASE_STYLE} <div class="container" style="padding:64px 0"> <div class="card"> <h2>已收到您的建議,感謝!</h2> <p class="muted">我們會納入統計與後續決策參考。</p> <div class="actions"> <a class="btn primary" href="{url_for('index')}">回到首頁</a> <a class="btn ghost" href="{url_for('summary_page')}">查看每月彙整</a> </div> </div> </div>""" return html
@app.route("/summary", methods=["GET"]) def summary_page(): # 解析年月(預設上個月) try: year = int(request.args.get("year") or 0) month = int(request.args.get("month") or 0) except: year = month = 0 if not (year and month): y, m = previous_month() year, month = y, m
summary = query_summary(year, month)
return render_template_string(
SUMMARY_HTML,
title=APP_TITLE,
year=year, month=month,
summary=summary,
now=datetime.date.today(),
token=(request.args.get("token") if ADMIN_TOKEN else None)
)
@app.route("/api/feedback", methods=["POST"]) def api_feedback(): # JSON 提交(供未來前端或他處串接) try: data = request.get_json(force=True) or {} except Exception: return jsonify({"ok": False, "error": "invalid_json"}), 400
# 基本驗證
required = ["role","category","title","details","priority"]
if not all(data.get(k) for k in required):
return jsonify({"ok": False, "error": "missing_fields"}), 400
# 附加環境資訊
data["userAgent"] = request.headers.get("User-Agent","")
data["page"] = data.get("page","")
data["ip"] = request.headers.get("X-Forwarded-For", request.remote_addr or "")
insert_feedback(data)
return jsonify({"ok": True})
@app.route("/api/summary", methods=["GET"]) def api_summary(): # year, month(預設上個月) try: year = int(request.args.get("year")) if request.args.get("year") else None month = int(request.args.get("month")) if request.args.get("month") else None except: year = month = None if not (year and month): y, m = previous_month() year, month = y, m
return jsonify({"ok": True, "summary": query_summary(year, month)})
@app.route("/export.csv", methods=["GET"]) def export_csv(): # 可選的簡單權限:若設定 ADMIN_TOKEN 則必須帶 token=? 才能下載 if ADMIN_TOKEN: if request.args.get("token") != ADMIN_TOKEN: return "Unauthorized", 401
try:
year = int(request.args.get("year"))
month = int(request.args.get("month"))
except:
y, m = previous_month()
year, month = y, m
start, end = month_range(year, month)
start_iso, end_iso = start.isoformat(), end.isoformat()
with closing(sqlite3.connect(DB_PATH)) as conn:
c = conn.cursor()
c.execute("""
SELECT ts, role, category, title, details, priority, name, email, user_agent, page, ip
FROM feedback
WHERE ts >= ? AND ts < ?
ORDER BY ts ASC
""", (start_iso, end_iso))
rows = c.fetchall()
output = io.StringIO()
w = csv.writer(output)
w.writerow(["timestamp","role","category","title","details","priority","name","email","user_agent","page","ip"])
w.writerows(rows)
resp = make_response(output.getvalue())
resp.headers["Content-Type"] = "text/csv; charset=utf-8"
resp.headers["Content-Disposition"] = f'attachment; filename="feedback_{year:04d}-{month:02d}.csv"'
return resp
if name == "main": init_db() # 開發用:Debug=True;正式環境建議用 WSGI/ASGI(gunicorn/uvicorn+hypercorn 等) app.run(host="127.0.0.1", port=8000, debug=True)