From 366ab0b2e8fc94efb4ebaaef8f92ba8cde804611 Mon Sep 17 00:00:00 2001 From: zhilv Date: Sun, 14 Dec 2025 22:21:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=AC=AC=E4=B8=80=E6=AC=A1=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 + LICENSE | 21 +++++ README.md | 13 +++ app.py | 18 ++++ config.py | 18 ++++ db/init_db.py | 28 +++++++ db/student.py | 61 ++++++++++++++ demo.py | 156 ++++++++++++++++++++++++++++++++++ models.py | 6 ++ routers/auth.py | 38 +++++++++ routers/home.py | 117 ++++++++++++++++++++++++++ services/services.py | 166 +++++++++++++++++++++++++++++++++++++ services/student_cache.py | 39 +++++++++ templates/base.html | 44 ++++++++++ templates/home.html | 21 +++++ templates/login.html | 55 ++++++++++++ templates/profile.html | 55 ++++++++++++ templates/sign_detail.html | 74 +++++++++++++++++ templates/sign_list.html | 74 +++++++++++++++++ 19 files changed, 1008 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.py create mode 100644 config.py create mode 100644 db/init_db.py create mode 100644 db/student.py create mode 100644 demo.py create mode 100644 models.py create mode 100644 routers/auth.py create mode 100644 routers/home.py create mode 100644 services/services.py create mode 100644 services/student_cache.py create mode 100644 templates/base.html create mode 100644 templates/home.html create mode 100644 templates/login.html create mode 100644 templates/profile.html create mode 100644 templates/sign_detail.html create mode 100644 templates/sign_list.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55f3464 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +data.db +image.jpg +__pycache__/ +demo.__pycache__ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2c76d9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 zhanghoulin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e72c452 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +## 小北学生线上签到平台 + +**让你免除必须到指定地点签到的烦恼(未完成),本代码采用 [MIT License](LICENSE) , 仅供学习交流** + +#### 已有功能 + +- 登录 +- 获取个人信息 +- 获取签到信息 + +## 📜 License + +This project is licensed under the [MIT License](LICENSE). diff --git a/app.py b/app.py new file mode 100644 index 0000000..e99a11f --- /dev/null +++ b/app.py @@ -0,0 +1,18 @@ +from contextlib import asynccontextmanager +from fastapi import FastAPI +import uvicorn +from routers import auth, home +from db.init_db import init_db + +app = FastAPI() + +app.include_router(auth.router) +app.include_router(home.router) + +@asynccontextmanager +async def lifespan(app: FastAPI): + init_db() + yield + +if __name__ == "__main__": + uvicorn.run("app:app", host="0.0.0.0", port=8080, reload=True) diff --git a/config.py b/config.py new file mode 100644 index 0000000..f44aa7f --- /dev/null +++ b/config.py @@ -0,0 +1,18 @@ +import os +from dotenv import load_dotenv +from pathlib import Path + +load_dotenv() + +DEBUG = True + +BASE_URL = "https://xiaobei.yinghuaonline.com" +HTTPX_TIMEOUT = 10 +DEFAULT_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Html5Plus/1.0 (Immersed/20) uni-app" + +BASE_DIR = Path(__file__).resolve().parent +DB_PATH = BASE_DIR / "data.db" + + +def get_env(key: str): + return os.getenv(key, "") diff --git a/db/init_db.py b/db/init_db.py new file mode 100644 index 0000000..f3ac440 --- /dev/null +++ b/db/init_db.py @@ -0,0 +1,28 @@ +import sqlite3 +from config import DB_PATH + + +def init_db(): + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + cur.execute( + """ + CREATE TABLE IF NOT EXISTS student ( + token TEXT PRIMARY KEY, + student_id TEXT, + student_number TEXT, + student_name TEXT, + student_sex TEXT, + dept_name TEXT, + college_name TEXT, + professional_name TEXT, + school_name TEXT, + raw_json TEXT, + updated_at INTEGER + ) + """ + ) + + conn.commit() + conn.close() diff --git a/db/student.py b/db/student.py new file mode 100644 index 0000000..3e4274b --- /dev/null +++ b/db/student.py @@ -0,0 +1,61 @@ +import sqlite3 +import json +import time +from config import DB_PATH + + +def get_student_by_token(token: str): + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + cur.execute( + "SELECT raw_json FROM student WHERE token = ?", + (token,), + ) + row = cur.fetchone() + conn.close() + + if not row: + return None + + return json.loads(row[0]) + + +def save_student(token: str, student: dict): + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + cur.execute( + """ + INSERT OR REPLACE INTO student ( + token, + student_id, + student_number, + student_name, + student_sex, + dept_name, + college_name, + professional_name, + school_name, + raw_json, + updated_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + token, + student.get("id"), + student.get("studentNumber"), + student.get("studentName"), + student.get("studentSex"), + student.get("deptName"), + student.get("collegeName"), + student.get("professionalName"), + student.get("schoolName"), + json.dumps(student, ensure_ascii=False), + int(time.time()), + ), + ) + + conn.commit() + conn.close() diff --git a/demo.py b/demo.py new file mode 100644 index 0000000..22ba5c1 --- /dev/null +++ b/demo.py @@ -0,0 +1,156 @@ +import asyncio +import httpx +import base64 +import os +from dotenv import load_dotenv +from uuid import uuid4 + + +load_dotenv() + + +def get_env(key: str): + return os.getenv(key, "") + + +class XBXS: + def __init__(self, u: str, p: str) -> None: + self.username = u + self.password = base64.b64encode(p.encode()).decode() + self.token = None + + self.client = httpx.AsyncClient( + base_url="https://xiaobei.yinghuaonline.com", + timeout=10, + headers={ + "user-agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 Html5Plus/1.0 (Immersed/20) uni-app", + "accept": "*/*", + "accept-language": "zh-CN,zh-Hans;q=0.9", + "accept-encoding": "gzip, deflate, br", + }, + ) + + async def get_captcha(self): + resp = await self.client.get("/xiaobei-api/captchaImage") + resp.raise_for_status() + result = resp.json() + if "uuid" not in result: + raise RuntimeError(f"captcha error: {result}") + return { + "showCode": result.get("showCode", ""), + "uuid": result.get("uuid", ""), + } + + async def login(self): + data = await self.get_captcha() + resp = await self.client.post( + "/xiaobei-api/userLogin", + json={ + "username": self.username, + "password": self.password, + "code": data.get("showCode", ""), + "uuid": data.get("uuid", ""), + "appUuid": "", + }, + ) + resp.raise_for_status() + + result = resp.json() + + self.token = result.get("token") + + self.client.headers["Authorization"] = f"Bearer {self.token}" + + return resp.json() + + async def get_info(self): + resp = await self.client.get("/xiaobei-api/getInfo") + resp.raise_for_status() + result = resp.json() + return { + "userName": result.get("user", {}).get("userName", ""), + "nickName": result.get("user", {}).get("nickName", ""), + "roles": result.get("roles", []), + "phonenumber": result.get("user", {}).get("phonenumber", ""), + "bindphonenumber": result.get("user", {}).get("bindphonenumber", ""), + "idCard": result.get("user", {}).get("idCard", ""), + "trtcId": result.get("user", {}).get("trtcId", ""), + } + + async def get_student_info(self): + resp = await self.client.get("/xiaobei-api/student/studentInfo") + resp.raise_for_status() + result = resp.json() + return { + "id": result.get("data", {}).get("id", ""), + "studentNumber": result.get("data", {}).get("studentNumber", ""), + "deptId": result.get("data", {}).get("deptId", ""), + "deptName": result.get("data", {}).get("deptName", ""), + "studentName": result.get("data", {}).get("studentName", ""), + "studentIpone": result.get("data", {}).get("studentIpone", ""), + "studentIdCard": result.get("data", {}).get("studentIdCard", ""), + "studentSex": result.get("data", {}).get("studentSex", ""), + "studentSchoolTime": result.get("data", {}).get("studentSchoolTime", ""), + "schoolId": result.get("data", {}).get("schoolId", ""), + "schoolName": result.get("data", {}).get("schoolName", ""), + "collegeId": result.get("data", {}).get("collegeId", ""), + "collegeName": result.get("data", {}).get("collegeName", ""), + "professionalId": result.get("data", {}).get("professionalId", ""), + "professionalName": result.get("data", {}).get("professionalName", ""), + } + + async def dep_id(self): + resp = await self.client.get("/xiaobei-api/student/deptId") + resp.raise_for_status() + result = resp.json() + return result + + async def get_user_sig2(self): + resp = await self.client.get("/xiaobei-api/trtc/genUserSig2") + resp.raise_for_status() + result = resp.json() + return result + + async def get_know_list(self): + resp = await self.client.get("/xiaobei-api/student/know/list") + resp.raise_for_status() + result = resp.json() + if result.get("total", 0) > 0: + pass + return result + + async def update_student_know(self, knowingID: str): + files = { + "file0": ( + str(uuid4()), + open("./image.jpg", "rb"), + "image/jpeg", + ) + } + resp = await self.client.post( + "/xiaobei-api/student/know/updateStudentKnow", + data={ + "address": "1388号", + "remark": "无", + "knowingId": knowingID, + "location": "106.5346788194445:29.343291015625", + "size": "1", + }, + ) + resp.raise_for_status() + + async def main(self): + try: + data = await self.login() + print(data) + finally: + await self.client.aclose() + + +if __name__ == "__main__": + asyncio.run( + XBXS( + get_env("XBXS_USERNAME"), + get_env("XBXS_PASSWORD"), + ).main() + ) diff --git a/models.py b/models.py new file mode 100644 index 0000000..35a2129 --- /dev/null +++ b/models.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class User(BaseModel): + username: str + password: str diff --git a/routers/auth.py b/routers/auth.py new file mode 100644 index 0000000..a9cc68d --- /dev/null +++ b/routers/auth.py @@ -0,0 +1,38 @@ +from fastapi import APIRouter, Request, Response +from fastapi.responses import JSONResponse, RedirectResponse +from models import User +from services.services import XBXS +from starlette.templating import Jinja2Templates + +router = APIRouter() +templates = Jinja2Templates(directory="templates") + + +@router.get("/") +async def index(request: Request): + token = request.cookies.get("xbxs_token") + if token: + return RedirectResponse("/home") + return templates.TemplateResponse("login.html", {"request": request}) + + +@router.post("/login") +async def login(user: User, response: Response): + xbxs = XBXS() + result = await xbxs.login(username=user.username, password=user.password) + + token = result.get("token") + if not token: + return JSONResponse({"msg": "登录失败"}, status_code=401) + + resp = JSONResponse({"msg": "ok"}) + + resp.set_cookie( + key="xbxs_token", + value=token, + max_age=60 * 60 * 24 * 7, # 7 天 + httponly=False, + samesite="lax", + ) + + return resp diff --git a/routers/home.py b/routers/home.py new file mode 100644 index 0000000..d5f9252 --- /dev/null +++ b/routers/home.py @@ -0,0 +1,117 @@ +from datetime import datetime +from fastapi import APIRouter, Depends, HTTPException, Request, status +from fastapi.responses import HTMLResponse +from starlette.templating import Jinja2Templates +from services.services import XBXS + +router = APIRouter() +templates = Jinja2Templates(directory="templates") + + +def get_xbxs(request: Request) -> XBXS: + token = request.cookies.get("xbxs_token") + if not token: + # 没有 token 跳转到登录页面 + raise HTTPException( + status_code=status.HTTP_303_SEE_OTHER, + detail="Redirect to login", + headers={"Location": "/login"}, + ) + return XBXS(token=token) + + +@router.get("/home") +async def home( + request: Request, + xbxs: XBXS = Depends(get_xbxs), +): + + return templates.TemplateResponse( + "home.html", + { + "request": request, + "active": "home", + }, + ) + + +@router.get("/profile") +async def profile( + request: Request, + xbxs: XBXS = Depends(get_xbxs), +): + student = await xbxs.get_student_info_cached() + + return templates.TemplateResponse( + "profile.html", + { + "request": request, + "student": student, + "active": "profile", + }, + ) + + +@router.get("/sign") +async def sign_list( + request: Request, + xbxs: XBXS = Depends(get_xbxs), +): + result = await xbxs.get_know_list() + + rows = result.get("rows", []) + + now = datetime.now() + + for item in rows: + # 1️⃣ 计算是否结束(核心) + end_str = f"{item['endDate']} {item['endTime']}" + end_time = datetime.strptime(end_str, "%Y-%m-%d %H:%M") + item["isEnded"] = now > end_time + + return templates.TemplateResponse( + "sign_list.html", + { + "request": request, + "list": rows, + "total": result.get("total", 0), + }, + ) + + +@router.get("/sign/{knowing_id}", response_class=HTMLResponse) +async def sign_detail( + knowing_id: int, + request: Request, + xbxs: XBXS = Depends(get_xbxs), +): + know_detail = await xbxs.get_know(str(knowing_id)) + + data = know_detail.get("data", {}) + + # 获取具体的数据 + knowing_name = data.get("knowingName", "未知签到") + start_date = data.get("startDate", "未知") + start_time = data.get("startTime", "未知") + end_time = data.get("endTime", "未知") + send_name = data.get("sendName", "未知") + send_role = data.get("sendRole", "未知") + is_check = data.get("isCheck", 0) + is_picture = data.get("isPicture", 0) + is_finish_know = data.get("isFinishKnowing", 0) + + return templates.TemplateResponse( + "sign_detail.html", + { + "request": request, + "knowing_name": knowing_name, + "start_date": start_date, + "start_time": start_time, + "end_time": end_time, + "send_name": send_name, + "send_role": send_role, + "is_check": is_check, + "is_picture": is_picture, + "is_finish_know": is_finish_know, + }, + ) diff --git a/services/services.py b/services/services.py new file mode 100644 index 0000000..8fd39bc --- /dev/null +++ b/services/services.py @@ -0,0 +1,166 @@ +import base64 +from uuid import uuid4 +import httpx +import config +from services.student_cache import get_student_by_token, save_student + + +SEX_MAP = { + 0: "男", + 1: "女", +} + + +class XBXS: + def __init__( + self, + token: str | None = None, + UA: str = config.DEFAULT_USER_AGENT, + ) -> None: + self.token = token + + self.client = httpx.AsyncClient( + base_url=config.BASE_URL, + timeout=config.HTTPX_TIMEOUT, + headers={ + "user-agent": UA, + "accept": "*/*", + "accept-language": "zh-CN,zh-Hans;q=0.9", + "accept-encoding": "gzip, deflate, br", + }, + proxy="http://127.0.0.1:9000" if config.DEBUG else None, + verify=False if config.DEBUG else True, + ) + + if token: + self.client.headers["Authorization"] = f"Bearer {token}" + + async def get_captcha(self): + resp = await self.client.get("/xiaobei-api/captchaImage") + resp.raise_for_status() + result = resp.json() + if "uuid" not in result: + raise RuntimeError(f"captcha error: {result}") + return { + "showCode": result.get("showCode", ""), + "uuid": result.get("uuid", ""), + } + + async def login(self, username: str, password: str): + data = await self.get_captcha() + resp = await self.client.post( + "/xiaobei-api/userLogin", + json={ + "username": username, + "password": base64.b64encode(password.encode()).decode(), + "code": data.get("showCode", ""), + "uuid": data.get("uuid", ""), + "appUuid": "", + }, + ) + resp.raise_for_status() + + result = resp.json() + + self.token = result.get("token") + + self.client.headers["Authorization"] = f"Bearer {self.token}" + + return result + + async def get_info(self): + resp = await self.client.get("/xiaobei-api/getInfo") + resp.raise_for_status() + result = resp.json() + return { + "userName": result.get("user", {}).get("userName", ""), + "nickName": result.get("user", {}).get("nickName", ""), + "roles": result.get("roles", []), + "phonenumber": result.get("user", {}).get("phonenumber", ""), + "bindphonenumber": result.get("user", {}).get("bindphonenumber", ""), + "idCard": result.get("user", {}).get("idCard", ""), + "trtcId": result.get("user", {}).get("trtcId", ""), + } + + async def get_student_info(self): + resp = await self.client.get("/xiaobei-api/student/studentInfo") + resp.raise_for_status() + result = resp.json() + + sex = result.get("data", {}).get("studentSex", "") + sex_text = SEX_MAP.get(sex, "未知") + + return { + "id": result.get("data", {}).get("id", ""), + "studentNumber": result.get("data", {}).get("studentNumber", ""), + "deptId": result.get("data", {}).get("deptId", ""), + "deptName": result.get("data", {}).get("deptName", ""), + "studentName": result.get("data", {}).get("studentName", ""), + "studentIpone": result.get("data", {}).get("studentIpone", ""), + "studentIdCard": result.get("data", {}).get("studentIdCard", ""), + "studentSex": sex_text, + "studentSchoolTime": result.get("data", {}).get("studentSchoolTime", ""), + "schoolId": result.get("data", {}).get("schoolId", ""), + "schoolName": result.get("data", {}).get("schoolName", ""), + "collegeId": result.get("data", {}).get("collegeId", ""), + "collegeName": result.get("data", {}).get("collegeName", ""), + "professionalId": result.get("data", {}).get("professionalId", ""), + "professionalName": result.get("data", {}).get("professionalName", ""), + } + + async def dep_id(self): + resp = await self.client.get("/xiaobei-api/student/deptId") + resp.raise_for_status() + result = resp.json() + return result + + async def get_user_sig2(self): + resp = await self.client.get("/xiaobei-api/trtc/genUserSig2") + resp.raise_for_status() + result = resp.json() + return result + + async def get_know_list(self): + resp = await self.client.get("/xiaobei-api/student/know/list") + resp.raise_for_status() + result = resp.json() + if result.get("total", 0) > 0: + pass + return result + + async def get_know(self, know: str): + resp = await self.client.get(f"/xiaobei-api/student/know/{know}") + resp.raise_for_status() + result = resp.json() + if result.get("total", 0) > 0: + pass + return result + + async def update_student_know(self, knowingID: str): + files = { + "file0": ( + str(uuid4()), + open("./image.jpg", "rb"), + "image/jpeg", + ) + } + resp = await self.client.post( + "/xiaobei-api/student/know/updateStudentKnow", + data={ + "address": "1388号", + "remark": "无", + "knowingId": knowingID, + "location": "106.5346788194445:29.343291015625", + "size": "1", + }, + ) + resp.raise_for_status() + + async def get_student_info_cached(self): + cached = get_student_by_token(str(self.token)) + if cached: + return cached + + student = await self.get_student_info() + save_student(str(self.token), student) + return student diff --git a/services/student_cache.py b/services/student_cache.py new file mode 100644 index 0000000..a695026 --- /dev/null +++ b/services/student_cache.py @@ -0,0 +1,39 @@ +import time +import json +import sqlite3 + +DB_PATH = "data.db" + + +def get_student_by_token(token: str): + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + cur.execute( + "SELECT raw_json FROM student WHERE token = ?", + (token,), + ) + row = cur.fetchone() + conn.close() + + if not row: + return None + + return json.loads(row[0]) + + +def save_student(token: str, student: dict): + conn = sqlite3.connect(DB_PATH) + cur = conn.cursor() + + cur.execute( + """ + INSERT OR REPLACE INTO student + (token, raw_json, updated_at) + VALUES (?, ?, ?) + """, + (token, json.dumps(student, ensure_ascii=False), int(time.time())), + ) + + conn.commit() + conn.close() diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..ff6f896 --- /dev/null +++ b/templates/base.html @@ -0,0 +1,44 @@ + + + + + + + {% block title %}XBXS{% endblock %} + + + + + + +
+ + +
+ {% block content %}{% endblock %} +
+ + + +
+ + + + \ No newline at end of file diff --git a/templates/home.html b/templates/home.html new file mode 100644 index 0000000..49534d2 --- /dev/null +++ b/templates/home.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block title %}首页{% endblock %} + +{% block content %} +
+ +

应用

+ + + + +
📍 签到
+
+ 课程签到 / 定位 / 拍照 +
+ +
+ +
+{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..180551e --- /dev/null +++ b/templates/login.html @@ -0,0 +1,55 @@ + + + + + + 登录 + + + + + +
+

登录

+ +
+ + + + + +
+ + +
+ + + + + + \ No newline at end of file diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..337ebcb --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,55 @@ +{% extends "base.html" %} +{% block title %}我的{% endblock %} + +{% block content %} +
+ + +
+ +
+ {{ student.studentName }} +
+
+ 学号 {{ student.studentNumber }} +
+
+ + +
+ +
+ 学院 + {{ student.collegeName }} +
+ +
+ 专业 + {{ student.professionalName }} +
+ +
+ 班级 + {{ student.deptName }} +
+ +
+ 性别 + {{ student.studentSex }} +
+ +
+ 手机号 + {{ student.studentIpone }} +
+ +
+ + + + 退出登录 + + +
+{% endblock %} \ No newline at end of file diff --git a/templates/sign_detail.html b/templates/sign_detail.html new file mode 100644 index 0000000..6106e52 --- /dev/null +++ b/templates/sign_detail.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} + +{% block title %}签到详情{% endblock %} + +{% block content %} +
+ + +
+

签到详情

+ 返回签到列表 +
+ +
+ + +
+
+ 签到名称 +
{{ knowing_name }}
+
+ +
+ 日期 +
{{ start_date }}
+
+ +
+ 时间 +
{{ start_time }} - {{ end_time }}
+
+ +
+ 发起人 +
{{ send_name }} ({{ send_role }})
+
+ +
+ 签到状态 +
+ {% if is_finish_know == 1 %} + 已签到 + {% elif is_check == 1 %} + 已签到 + {% else %} + 未签到 + {% endif %} +
+
+ + {% if is_picture == 1 %} +
+ 签到需要拍照 +
+
+ {% else %} +
+ 签到需要拍照 +
+
+ {% endif %} +
+ + + {% if is_finish_know == 0 and is_check == 0 %} +
+
+ +
+
+ {% endif %} + +
+{% endblock %} \ No newline at end of file diff --git a/templates/sign_list.html b/templates/sign_list.html new file mode 100644 index 0000000..2c8c432 --- /dev/null +++ b/templates/sign_list.html @@ -0,0 +1,74 @@ +{% extends "base.html" %} +{% block title %}签到{% endblock %} + +{% block content %} +
+ + +
+

签到任务

+ + 共 {{ total }} 条 + +
+ + {% if total == 0 %} +
+ 暂无签到任务 +
+ {% endif %} + + + + +
+{% endblock %} \ No newline at end of file