feat: 第一次上传

This commit is contained in:
2025-12-14 22:21:03 +08:00
commit 366ab0b2e8
19 changed files with 1008 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
data.db
image.jpg
__pycache__/
demo.__pycache__

21
LICENSE Normal file
View File

@@ -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.

13
README.md Normal file
View File

@@ -0,0 +1,13 @@
## 小北学生线上签到平台
**让你免除必须到指定地点签到的烦恼(未完成),本代码采用 [MIT License](LICENSE) , 仅供学习交流**
#### 已有功能
- 登录
- 获取个人信息
- 获取签到信息
## 📜 License
This project is licensed under the [MIT License](LICENSE).

18
app.py Normal file
View File

@@ -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)

18
config.py Normal file
View File

@@ -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, "")

28
db/init_db.py Normal file
View File

@@ -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()

61
db/student.py Normal file
View File

@@ -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()

156
demo.py Normal file
View File

@@ -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()
)

6
models.py Normal file
View File

@@ -0,0 +1,6 @@
from pydantic import BaseModel
class User(BaseModel):
username: str
password: str

38
routers/auth.py Normal file
View File

@@ -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

117
routers/home.py Normal file
View File

@@ -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,
},
)

166
services/services.py Normal file
View File

@@ -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

39
services/student_cache.py Normal file
View File

@@ -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()

44
templates/base.html Normal file
View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<title>{% block title %}XBXS{% endblock %}</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-zinc-100">
<!-- App 容器 -->
<div class="mx-auto max-w-md min-h-screen bg-white relative">
<!-- 主内容 -->
<main class="pb-20">
{% block content %}{% endblock %}
</main>
<!-- 底部 Tab -->
<nav class="fixed bottom-0 left-1/2 -translate-x-1/2
w-full max-w-md
bg-white border-t
flex justify-around items-center
h-16">
<a href="/" class="flex flex-col items-center text-sm
{% if active == 'home' %}text-blue-600{% else %}text-zinc-500{% endif %}">
<span>🏠</span>
首页
</a>
<a href="/profile" class="flex flex-col items-center text-sm
{% if active == 'profile' %}text-blue-600{% else %}text-zinc-500{% endif %}">
<span>👤</span>
我的
</a>
</nav>
</div>
</body>
</html>

21
templates/home.html Normal file
View File

@@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block title %}首页{% endblock %}
{% block content %}
<div class="p-4 space-y-6">
<h1 class="text-xl font-bold">应用</h1>
<!-- 签到 App 卡片 -->
<a href="/sign" class="block bg-blue-500 text-white rounded-2xl p-6 shadow
active:scale-95 transition">
<div class="text-lg font-semibold">📍 签到</div>
<div class="text-sm opacity-90 mt-1">
课程签到 / 定位 / 拍照
</div>
</a>
</div>
{% endblock %}

55
templates/login.html Normal file
View File

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>登录</title>
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="min-h-screen flex items-center justify-center bg-gray-100">
<div class="w-full max-w-sm bg-white rounded-xl shadow-lg p-8">
<h2 class="text-2xl font-bold text-center mb-6">登录</h2>
<div class="space-y-4">
<input id="username" type="text" placeholder="用户名"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring" />
<input id="password" type="password" placeholder="密码"
class="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring" />
<button onclick="login()"
class="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 transition">
登录
</button>
</div>
<p id="error" class="text-red-500 text-sm mt-4 hidden"></p>
</div>
<script>
async function login() {
const username = document.getElementById("username").value;
const password = document.getElementById("password").value;
const res = await fetch("/login", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
if (res.ok) {
window.location.href = "/home";
} else {
document.getElementById("error").innerText = "登录失败";
document.getElementById("error").classList.remove("hidden");
}
}
</script>
</body>
</html>

55
templates/profile.html Normal file
View File

@@ -0,0 +1,55 @@
{% extends "base.html" %}
{% block title %}我的{% endblock %}
{% block content %}
<div class="p-4 space-y-6">
<!-- 头像 + 名字 -->
<div class="bg-gradient-to-r from-blue-500 to-indigo-500
rounded-2xl p-6 text-white">
<div class="text-lg font-semibold">
{{ student.studentName }}
</div>
<div class="text-sm opacity-90">
学号 {{ student.studentNumber }}
</div>
</div>
<!-- 信息卡 -->
<div class="bg-white rounded-2xl shadow divide-y text-sm">
<div class="flex justify-between p-4">
<span class="text-zinc-500">学院</span>
<span>{{ student.collegeName }}</span>
</div>
<div class="flex justify-between p-4">
<span class="text-zinc-500">专业</span>
<span>{{ student.professionalName }}</span>
</div>
<div class="flex justify-between p-4">
<span class="text-zinc-500">班级</span>
<span>{{ student.deptName }}</span>
</div>
<div class="flex justify-between p-4">
<span class="text-zinc-500">性别</span>
<span>{{ student.studentSex }}</span>
</div>
<div class="flex justify-between p-4">
<span class="text-zinc-500">手机号</span>
<span>{{ student.studentIpone }}</span>
</div>
</div>
<!-- 操作 -->
<a href="/logout" class="block text-center text-red-500 py-3">
退出登录
</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %}签到详情{% endblock %}
{% block content %}
<div class="p-4 space-y-4">
<!-- 标题 -->
<div class="flex justify-between items-center">
<h1 class="text-xl font-bold">签到详情</h1>
<a href="/sign" class="text-sm text-blue-500 hover:underline">返回签到列表</a>
</div>
<hr />
<!-- 详情信息 -->
<div class="space-y-4">
<div>
<span class="text-zinc-500">签到名称</span>
<div class="font-medium">{{ knowing_name }}</div>
</div>
<div>
<span class="text-zinc-500">日期</span>
<div class="font-medium">{{ start_date }}</div>
</div>
<div>
<span class="text-zinc-500">时间</span>
<div class="font-medium">{{ start_time }} - {{ end_time }}</div>
</div>
<div>
<span class="text-zinc-500">发起人</span>
<div class="font-medium">{{ send_name }} ({{ send_role }})</div>
</div>
<div>
<span class="text-zinc-500">签到状态</span>
<div class="font-medium">
{% if is_finish_know == 1 %}
<span class="text-green-600">已签到</span>
{% elif is_check == 1 %}
<span class="text-green-600">已签到</span>
{% else %}
<span class="text-red-600">未签到</span>
{% endif %}
</div>
</div>
{% if is_picture == 1 %}
<div>
<span class="text-zinc-500">签到需要拍照</span>
<div class="font-medium"></div>
</div>
{% else %}
<div>
<span class="text-zinc-500">签到需要拍照</span>
<div class="font-medium"></div>
</div>
{% endif %}
</div>
<!-- 完成签到按钮 -->
{% if is_finish_know == 0 and is_check == 0 %}
<div class="mt-4">
<form method="post" action="/sign/{{ know_id }}/complete">
<button type="submit" class="bg-blue-600 text-white py-2 px-4 rounded-lg">完成签到</button>
</form>
</div>
{% endif %}
</div>
{% endblock %}

74
templates/sign_list.html Normal file
View File

@@ -0,0 +1,74 @@
{% extends "base.html" %}
{% block title %}签到{% endblock %}
{% block content %}
<div class="p-4 space-y-4">
<!-- 标题 -->
<div class="flex justify-between items-center">
<h1 class="text-xl font-bold">签到任务</h1>
<span class="text-sm text-zinc-500">
共 {{ total }} 条
</span>
</div>
{% if total == 0 %}
<div class="text-center text-zinc-400 py-10">
暂无签到任务
</div>
{% endif %}
<!-- 列表 -->
<div class="space-y-3">
{% for item in list %}
<a href="/sign/{{ item.id }}" class="block bg-white rounded-2xl p-4 shadow
active:scale-95 transition">
<!-- 第一行 -->
<div class="flex justify-between items-center">
<div class="font-semibold text-base">
{{ item.knowingName }}
</div>
{% if item.isFinishKnowing == 1 %}
<span class="text-xs bg-green-100 text-green-600 px-2 py-0.5 rounded-full">
已签到
</span>
{% elif item.isEnded %}
<span class="text-xs bg-zinc-100 text-zinc-400 px-2 py-0.5 rounded-full">
已结束
</span>
{% else %}
<span class="text-xs bg-blue-100 text-blue-600 px-2 py-0.5 rounded-full">
进行中
</span>
{% endif %}
</div>
<!-- 时间 -->
<div class="text-sm text-zinc-500 mt-1">
{{ item.startDate }} {{ item.startTime }} - {{ item.endTime }}
</div>
<!-- 发起人 -->
<div class="flex justify-between items-center mt-2 text-sm">
<div class="text-zinc-600">
{{ item.sendRole }} · {{ item.sendName }}
</div>
{% if item.isPicture == 0 %}
<span class="text-xs bg-orange-100 text-orange-600 px-2 py-0.5 rounded">
需拍照
</span>
{% endif %}
</div>
</a>
{% endfor %}
</div>
</div>
{% endblock %}