Initial commit: Flutter 无书应用项目

This commit is contained in:
Developer
2026-03-30 02:35:31 +08:00
commit 9175ff9905
566 changed files with 103261 additions and 0 deletions

525
ht/test/API.md Normal file
View File

@@ -0,0 +1,525 @@
# 诗词答题 API 文档
## 基础信息
| 项目 | 说明 |
|------|------|
| 基础URL | `/api.php` |
| 返回格式 | JSON |
| 编码 | UTF-8 |
| 请求方式 | **GET / POST 都支持** |
### 请求方式说明
| 接口 | 推荐方式 | 原因 |
|------|----------|------|
| 获取题目 (question) | **GET** | 读取操作,简单可缓存 |
| 下一题 (next) | **GET** | 读取操作 |
| 获取新题 (fetch) | **GET** | 读取操作 |
| 提交答案 (answer) | **POST** | 提交操作,更规范 |
| 获取提示 (hint) | **GET** | 读取操作 |
| 题目列表 (list) | **GET** | 读取操作 |
| 刷新缓存 (refresh) | **GET** | 管理操作 |
| 状态统计 (stats) | **GET** | 读取操作 |
## 通用返回格式
```json
{
"code": 0,
"msg": "",
"data": { ... }
}
```
| 字段 | 类型 | 说明 |
|------|------|------|
| code | int | 状态码0=成功,其他=错误 |
| msg | string | 提示信息 |
| data | object | 返回数据 |
---
## 接口列表
### 1. 获取题目
**请求**
```
GET /api.php?action=question&id=0
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 否 | 默认为 question |
| id | int | 否 | 题目ID默认0 |
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": 0,
"total": 10,
"question": "欲把西湖比西子,\"________\"",
"author": "苏轼",
"type": "江南",
"grade": "小学",
"dynasty": "宋朝",
"options": [
{"index": 1, "content": "山色空蒙雨亦奇"},
{"index": 2, "content": "淡妆浓抹总相宜"},
{"index": 3, "content": "门前流水尚能西"},
{"index": 4, "content": "拄杖无时夜叩门"}
]
}
}
```
---
### 2. 获取下一题(自动进度)
**请求**
```
GET /api.php?action=next
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 是 | 固定为 next |
**说明**
- **无需传 id 参数**,系统自动记住当前进度(使用 Session
- 每次刷新自动跳到下一题
- 如果已经是最后一题,自动回到第 0 题(循环)
**使用方式**
| 刷新次数 | 返回的 id |
|----------|-----------|
| 第 1 次 | 1 |
| 第 2 次 | 2 |
| 第 3 次 | 3 |
| ... | ... |
| 第 62 次 | 0 (循环) |
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": 1,
"total": 62,
"question": "人生得意须尽欢,\"__________\"",
"author": "李白",
"type": "豪放",
"grade": "高中",
"dynasty": "唐朝",
"options": [...],
"prev_id": 0
}
}
```
---
### 3. 刷新获取新题(推荐)
**请求**
```
GET /api.php?action=fetch
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 是 | 固定为 fetch |
**说明**
- **每次刷新都从百度 API 获取新题目**
- 新题目自动写入本地缓存(去重)
- 返回本次获取的随机一题
- API 失败时自动降级使用本地缓存
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": null,
"total": 65,
"question": "人生得意须尽欢,\"__________\"",
"author": "李白",
"type": "豪放",
"grade": "高中",
"dynasty": "唐朝",
"options": [...],
"from_cache": false,
"new_questions": 3
}
}
```
**额外返回字段**
| 字段 | 类型 | 说明 |
|------|------|------|
| from_cache | bool | 是否来自本地缓存 |
| new_questions | int | 本次新增题目数量 |
---
### 4. 提交答案
**请求方式 1GET简单**
```
GET /api.php?action=answer&id=0&answer=2
```
**请求方式 2POST推荐**
```
POST /api.php
Content-Type: application/x-www-form-urlencoded
action=answer&id=0&answer=2
```
或使用 JSON
```
POST /api.php
Content-Type: application/json
{
"action": "answer",
"id": 0,
"answer": "2"
}
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 是 | 固定为 answer |
| id | int | 是 | 题目ID |
| answer | string | 是 | 答案序号(1-4) |
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": 0,
"correct": true,
"your_answer": "2",
"correct_answer": "2",
"next_id": 1,
"has_next": true
}
}
```
---
### 5. 获取提示
**请求**
```
GET /api.php?action=hint&id=0
```
| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| action | string | 是 | 固定为 hint |
| id | int | 是 | 题目ID |
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"id": 0,
"hint": "这是首描写江南的诗,你在小学学过它。",
"author": "苏轼",
"dynasty": "宋朝"
}
}
```
---
### 6. 获取题目列表
**请求**
```
GET /api.php?action=list
```
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"total": 10,
"list": [
{
"id": 0,
"question": "欲把西湖比西子,\"________\"...",
"author": "苏轼",
"dynasty": "宋朝"
}
]
}
}
```
---
### 7. 刷新缓存
**请求**
```
GET /api.php?action=refresh
```
**说明**
- 强制从百度 API 获取最新题目
- 合并到本地缓存(去重)
**返回**
```json
{
"code": 0,
"msg": "",
"data": {
"refreshed": true,
"total": 15
}
}
```
---
## 缓存机制
| 配置 | 值 | 说明 |
|------|------|------|
| 缓存文件 | `data/questions.json` | 本地JSON文件 |
| 过期时间 | **永不过期** | 永久保存 |
| 去重方式 | 按 `question_content` | 相同题目不重复存储 |
| 降级策略 | API失败自动使用本地缓存 | 原始URL失效也能使用 |
| API 限频 | 5秒内只请求一次 | 防止并发请求百度 |
### 缓存文件结构
```json
{
"updated": "2024-01-01 12:00:00",
"count": 10,
"questions": [...]
}
```
---
## 性能优化
### 已实现优化
| 优化项 | 说明 |
|--------|------|
| 静态变量缓存 | 同一请求内多次读取只加载一次文件 |
| API 请求锁 | 5秒内多人请求只发一次 API |
| 原子写入 | 先写临时文件再 rename防止数据损坏 |
| 去重优化 | 只在写入时去重,读取时不处理 |
| 超时控制 | API 超时 5 秒,连接超时 3 秒 |
### 性能预估
| 场景 | 响应时间 |
|------|----------|
| 读取本地缓存 | ~2-5ms |
| API 请求成功 | ~200-500ms |
| API 限频降级 | ~1ms直接读缓存 |
| 100人并发 | 无压力(读缓存为主) |
### 查看状态
```
GET /api.php?action=stats
```
返回:
```json
{
"code": 0,
"data": {
"total_questions": 149,
"cache_file_size": "40.5 KB",
"last_updated": "2026-03-29 05:04:04",
"memory_usage": "256 KB"
}
}
```
---
## 错误码
| code | 说明 |
|------|------|
| 0 | 成功 |
| 400 | 参数错误 |
| 404 | 题目不存在 |
---
## 使用示例
### JavaScript
```javascript
// 获取第一题
fetch('/api.php?action=question&id=0')
.then(r => r.json())
.then(d => console.log(d.data.question));
// 获取下一题(自动进度,无需传 id
fetch('/api.php?action=next')
.then(r => r.json())
.then(d => console.log(d.data.id, d.data.question));
// 提交答案
fetch('/api.php?action=answer&id=0&answer=2')
.then(r => r.json())
.then(d => {
if(d.data.correct) {
console.log('回答正确!');
}
});
```
### Flutter/Dart
```dart
import 'package:dio/dio.dart';
final dio = Dio();
// 获取题目
Future<Map> getQuestion(int id) async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'question',
'id': id,
});
return res.data['data'];
}
// 获取下一题(自动进度,无需传 id
Future<Map> getNextQuestion() async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'next',
});
return res.data['data'];
}
// 提交答案POST 方式)
Future<bool> checkAnswer(int id, String answer) async {
final res = await dio.post('/api.php', data: {
'action': 'answer',
'id': id,
'answer': answer,
});
return res.data['data']['correct'];
}
```
---
## App 集成建议
### 方案 1App 自己管理进度(推荐)
使用 `?action=question` 接口App 完全控制进度:
```dart
class QuizApi {
int _currentId = 0;
int _total = 0;
Future<Map> getQuestion() async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'question',
'id': _currentId,
});
final data = res.data['data'];
_total = data['total'] ?? 0;
return data;
}
Future<Map> submitAnswer(String answer) async {
final res = await dio.post('/api.php', data: {
'action': 'answer',
'id': _currentId,
'answer': answer,
});
return res.data['data'];
}
void nextQuestion() {
_currentId++;
if (_currentId >= _total) {
_currentId = 0;
}
}
int get currentId => _currentId;
}
```
**优点**
- 不依赖 SessionApp 完全控制进度
- 可以随时跳转任意题目
- 适合多端同步
---
### 方案 2使用自动进度
使用 `?action=next` 接口API 自动管理:
```dart
Future<Map> getNextQuestion() async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'next',
});
return res.data['data'];
}
```
**优点**
- 简单,无需管理 ID
- 自动循环
**缺点**
- 依赖 Session需要保持 cookie
- 无法自由跳转题目
---
### 方案 3不断获取新题
使用 `?action=fetch` 接口,每次都从百度获取新题:
```dart
Future<Map> getNewQuestion() async {
final res = await dio.get('/api.php', queryParameters: {
'action': 'fetch',
});
return res.data['data'];
}
```
**适用场景**
- 想要不断扩展题库
- 用户每次刷新都可能看到新题

353
ht/test/api.php Normal file
View File

@@ -0,0 +1,353 @@
<?php
header("Content-Type:application/json;charset=UTF-8");
session_start();
define('CACHE_DIR', __DIR__ . '/data/');
define('CACHE_FILE', CACHE_DIR . 'questions.json');
define('API_LOCK_FILE', CACHE_DIR . 'api.lock');
define('API_INTERVAL', 5);
static $cacheData = null;
$result = ['code' => 0, 'msg' => '', 'data' => null];
$action = $_REQUEST['action'] ?? 'question';
$id = isset($_REQUEST['id']) ? intval($_REQUEST['id']) : null;
switch($action){
case 'question':
$result['data'] = getQuestion($id ?? getCurrentId());
break;
case 'next':
$result['data'] = getNextQuestionAuto();
break;
case 'fetch':
$result['data'] = fetchNewQuestion();
break;
case 'answer':
$result['data'] = checkAnswer($id, $_REQUEST['answer'] ?? '');
break;
case 'hint':
$result['data'] = getHint($id);
break;
case 'list':
$result['data'] = getQuestionList();
break;
case 'refresh':
$result['data'] = refreshCache();
break;
case 'stats':
$result['data'] = getStats();
break;
default:
$result['code'] = 400;
$result['msg'] = '未知操作';
}
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
function getStats(){
global $cacheData;
$cache = loadCache();
return [
'total_questions' => count($cache),
'cache_file_size' => file_exists(CACHE_FILE) ? round(filesize(CACHE_FILE) / 1024, 2) . ' KB' : 0,
'last_updated' => file_exists(CACHE_FILE) ? date('Y-m-d H:i:s', filemtime(CACHE_FILE)) : 'N/A',
'memory_usage' => round(memory_get_peak_usage(true) / 1024, 2) . ' KB'
];
}
function getQuestion($id){
$cache = loadCache();
if(empty($cache)){
return ['error' => '没有题目数据'];
}
if($id < 0 || $id >= count($cache)){
return ['error' => '题目不存在', 'total' => count($cache)];
}
$item = $cache[$id];
return [
'id' => $id,
'total' => count($cache),
'question' => $item['question_content'],
'author' => $item['type']['person'] ?? '',
'type' => $item['type']['type'] ?? '',
'grade' => $item['type']['grade'] ?? '',
'dynasty' => $item['type']['dynasty'] ?? '',
'options' => formatOptions($item['option_answers'] ?? [])
];
}
function getNextQuestionAuto(){
$cache = loadCache();
if(empty($cache)){
return ['error' => '没有题目数据'];
}
$total = count($cache);
$currentId = getCurrentId();
$nextId = $currentId + 1;
if($nextId >= $total){
$nextId = 0;
}
setCurrentId($nextId);
$result = getQuestion($nextId);
$result['prev_id'] = $currentId;
return $result;
}
function getCurrentId(){
return isset($_SESSION['current_question_id']) ? intval($_SESSION['current_question_id']) : 0;
}
function setCurrentId($id){
$_SESSION['current_question_id'] = $id;
}
function fetchNewQuestion(){
$apiData = fetchFromBaiduApi();
if($apiData === null){
$cache = loadCache();
if(!empty($cache)){
$randomId = array_rand($cache);
$result = formatQuestionItem($randomId, $cache[$randomId], count($cache));
$result['from_cache'] = true;
$result['api_status'] = 'failed';
return $result;
}
return ['error' => 'API请求失败且无本地缓存'];
}
$cache = loadCache();
$newCount = mergeToCache($apiData, $cache);
$randomItem = $apiData[array_rand($apiData)];
$result = formatQuestionItem(null, $randomItem, count($cache));
$result['from_cache'] = false;
$result['new_questions'] = $newCount;
$result['api_status'] = 'success';
return $result;
}
function formatQuestionItem($id, $item, $total, $fromCache = false, $newCount = 0){
return [
'id' => $id,
'total' => $total,
'question' => $item['question_content'],
'author' => $item['type']['person'] ?? '',
'type' => $item['type']['type'] ?? '',
'grade' => $item['type']['grade'] ?? '',
'dynasty' => $item['type']['dynasty'] ?? '',
'options' => formatOptions($item['option_answers'] ?? []),
'from_cache' => $fromCache,
'new_questions' => $newCount
];
}
function checkAnswer($id, $answer){
$cache = loadCache();
if(empty($cache)){
return ['error' => '没有题目数据'];
}
if($id < 0 || $id >= count($cache)){
return ['error' => '题目不存在'];
}
$item = $cache[$id];
$correctAnswer = findAnswer($item['option_answers'] ?? []);
$isCorrect = ($answer === $correctAnswer);
return [
'id' => $id,
'correct' => $isCorrect,
'your_answer' => $answer,
'correct_answer' => $correctAnswer,
'next_id' => $id + 1,
'has_next' => $id + 1 < count($cache)
];
}
function getHint($id){
$cache = loadCache();
if(empty($cache)){
return ['error' => '没有题目数据'];
}
if($id < 0 || $id >= count($cache)){
return ['error' => '题目不存在'];
}
$item = $cache[$id];
return [
'id' => $id,
'hint' => '这是首描写' . ($item['type']['type'] ?? '') . '的诗,你在' . ($item['type']['grade'] ?? '') . '学过它。',
'author' => $item['type']['person'] ?? '',
'dynasty' => $item['type']['dynasty'] ?? ''
];
}
function getQuestionList(){
$cache = loadCache();
if(empty($cache)){
return ['total' => 0, 'list' => []];
}
$list = [];
foreach($cache as $i => $item){
$list[] = [
'id' => $i,
'question' => mb_substr($item['question_content'], 0, 30, 'UTF-8') . '...',
'author' => $item['type']['person'] ?? '',
'dynasty' => $item['type']['dynasty'] ?? ''
];
}
return ['total' => count($list), 'list' => $list];
}
function refreshCache(){
$apiData = fetchFromBaiduApi();
if($apiData === null){
return ['refreshed' => false, 'error' => 'API请求失败'];
}
$cache = loadCache();
$newCount = mergeToCache($apiData, $cache);
return ['refreshed' => true, 'total' => count($cache), 'new_questions' => $newCount];
}
function loadCache(){
global $cacheData;
if($cacheData !== null){
return $cacheData;
}
if(!file_exists(CACHE_FILE)){
$cacheData = [];
return [];
}
$content = file_get_contents(CACHE_FILE);
if($content === false){
$cacheData = [];
return [];
}
$data = json_decode($content, true);
if(!is_array($data) || !isset($data['questions'])){
$cacheData = [];
return [];
}
$cacheData = $data['questions'];
return $cacheData;
}
function saveCache($questions){
global $cacheData;
if(!is_dir(CACHE_DIR)){
mkdir(CACHE_DIR, 0755, true);
}
$data = [
'updated' => date('Y-m-d H:i:s'),
'count' => count($questions),
'questions' => $questions
];
$tempFile = CACHE_FILE . '.tmp';
$result = file_put_contents($tempFile, json_encode($data, JSON_UNESCAPED_UNICODE), LOCK_EX);
if($result !== false){
rename($tempFile, CACHE_FILE);
}
$cacheData = $questions;
}
function fetchFromBaiduApi(){
if(file_exists(API_LOCK_FILE)){
$lockTime = intval(file_get_contents(API_LOCK_FILE));
if(time() - $lockTime < API_INTERVAL){
return null;
}
}
file_put_contents(API_LOCK_FILE, time());
$url = 'https://hanyu.baidu.com/hanyu/ajax/pingce_data';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_TIMEOUT => 5,
CURLOPT_CONNECTTIMEOUT => 3,
CURLOPT_USERAGENT => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
CURLOPT_HTTPHEADER => [
'Accept: application/json',
'Accept-Language: zh-CN,zh;q=0.9'
]
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if($httpCode !== 200 || empty($response)){
return null;
}
$json = json_decode($response, true);
if(!isset($json['data']) || !is_array($json['data'])){
return null;
}
return $json['data'];
}
function mergeToCache($newData, &$existingCache){
if(empty($newData)){
return 0;
}
$existingKeys = [];
foreach($existingCache as $item){
$key = $item['question_content'] ?? '';
$existingKeys[$key] = true;
}
$newCount = 0;
foreach($newData as $item){
$key = $item['question_content'] ?? '';
if(!isset($existingKeys[$key])){
$existingCache[] = $item;
$existingKeys[$key] = true;
$newCount++;
}
}
if($newCount > 0){
saveCache($existingCache);
}
return $newCount;
}
function formatOptions($options){
$result = [];
foreach($options as $i => $opt){
$result[] = [
'index' => $i + 1,
'content' => $opt['answer_content'] ?? ''
];
}
return $result;
}
function findAnswer($options){
foreach($options as $i => $opt){
if(($opt['is_standard_answer'] ?? 0) === 1){
return (string)($i + 1);
}
}
return '';
}
?>

5
ht/test/data/.htaccess Normal file
View File

@@ -0,0 +1,5 @@
order allow,deny
deny from all
<Files ~ "\.(json|txt|lock|tmp)$">
deny from all
</Files>

4029
ht/test/data/questions.json Normal file

File diff suppressed because it is too large Load Diff

422
ht/test/index.html Normal file
View File

@@ -0,0 +1,422 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>诗词答题 API 测试</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5rem;
font-weight: 600;
margin-bottom: 10px;
}
.header p {
font-size: 1.1rem;
opacity: 0.9;
}
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
@media (max-width: 768px) {
.content {
grid-template-columns: 1fr;
}
}
.card {
background: white;
border-radius: 20px;
padding: 25px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
}
.card h2 {
font-size: 1.4rem;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.btn-group {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
margin-bottom: 20px;
}
.btn {
padding: 14px 20px;
border: none;
border-radius: 12px;
font-size: 0.95rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #f5f5f7;
color: #333;
}
.btn-secondary:hover {
background: #e8e8ed;
}
.input-group {
margin-bottom: 15px;
}
.input-group label {
display: block;
margin-bottom: 8px;
color: #666;
font-size: 0.9rem;
font-weight: 500;
}
.input-group input {
width: 100%;
padding: 12px 16px;
border: 2px solid #e5e5ea;
border-radius: 10px;
font-size: 1rem;
transition: all 0.3s ease;
}
.input-group input:focus {
outline: none;
border-color: #667eea;
}
.url-display {
background: #f5f5f7;
padding: 12px 16px;
border-radius: 10px;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 0.85rem;
word-break: break-all;
margin-bottom: 15px;
color: #667eea;
}
.response-area {
background: #1a1a2e;
border-radius: 12px;
padding: 20px;
overflow: auto;
max-height: 400px;
}
.response-area pre {
margin: 0;
color: #a8e6cf;
font-family: 'SF Mono', Monaco, 'Courier New', monospace;
font-size: 0.85rem;
line-height: 1.6;
}
.status {
padding: 10px 15px;
border-radius: 10px;
margin-bottom: 15px;
font-size: 0.9rem;
display: flex;
align-items: center;
gap: 8px;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-error {
background: #f8d7da;
color: #721c24;
}
.status-loading {
background: #cce5ff;
color: #004085;
}
.question-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 25px;
border-radius: 15px;
margin-bottom: 20px;
}
.question-card .question {
font-size: 1.3rem;
font-weight: 600;
margin-bottom: 20px;
}
.question-card .meta {
display: flex;
gap: 15px;
flex-wrap: wrap;
font-size: 0.9rem;
opacity: 0.9;
}
.question-card .meta span {
background: rgba(255, 255, 255, 0.2);
padding: 5px 12px;
border-radius: 20px;
}
.options-list {
display: grid;
gap: 10px;
}
.option-btn {
background: white;
border: 2px solid #e5e5ea;
padding: 15px 20px;
border-radius: 12px;
font-size: 1rem;
cursor: pointer;
transition: all 0.3s ease;
text-align: left;
}
.option-btn:hover {
border-color: #667eea;
background: #f8f9ff;
}
.option-btn.correct {
border-color: #34c759;
background: #d4edda;
}
.option-btn.wrong {
border-color: #ff3b30;
background: #f8d7da;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📚 诗词答题 API 测试</h1>
<p>测试所有接口功能</p>
</div>
<div class="content">
<div class="card">
<h2>🎯 接口测试</h2>
<div class="btn-group">
<button class="btn btn-primary" onclick="testQuestion()">📖 获取题目</button>
<button class="btn btn-primary" onclick="testNext()">➡️ 下一题</button>
<button class="btn btn-primary" onclick="testFetch()">🔄 获取新题</button>
<button class="btn btn-primary" onclick="testList()">📋 题目列表</button>
<button class="btn btn-primary" onclick="testRefresh()">🔃 刷新缓存</button>
<button class="btn btn-primary" onclick="testStats()">📊 状态统计</button>
</div>
<div class="input-group">
<label>题目 ID</label>
<input type="number" id="questionId" value="0" placeholder="输入题目ID">
</div>
<div class="input-group">
<label>答案 (1-4)</label>
<input type="number" id="answer" value="1" placeholder="输入答案">
</div>
<div class="btn-group">
<button class="btn btn-secondary" onclick="testAnswer()">✅ 提交答案</button>
<button class="btn btn-secondary" onclick="testHint()">💡 获取提示</button>
</div>
<div class="url-display" id="urlDisplay">等待请求...</div>
<div id="status"></div>
</div>
<div class="card">
<h2>📤 响应结果</h2>
<div id="questionPreview"></div>
<div class="response-area">
<pre id="response">点击左侧按钮开始测试...</pre>
</div>
</div>
</div>
</div>
<script>
let currentQuestion = null;
function showStatus(msg, type) {
const statusDiv = document.getElementById('status');
const icons = {
success: '✅',
error: '❌',
loading: '⏳'
};
statusDiv.innerHTML = `<div class="status status-${type}">${icons[type]} ${msg}</div>`;
}
function showUrl(url) {
document.getElementById('urlDisplay').textContent = url;
}
function showResponse(data) {
document.getElementById('response').textContent = JSON.stringify(data, null, 2);
}
function showQuestion(data) {
const preview = document.getElementById('questionPreview');
if (!data || data.error) {
preview.innerHTML = '';
return;
}
preview.innerHTML = `
<div class="question-card">
<div class="question">${data.question}</div>
<div class="meta">
<span>👤 ${data.author || '未知'}</span>
<span>📅 ${data.dynasty || '未知'}</span>
<span>📚 ${data.type || '未知'}</span>
<span>🎓 ${data.grade || '未知'}</span>
<span>📊 ${data.id + 1}/${data.total}</span>
</div>
</div>
<div class="options-list">
${(data.options || []).map(opt => `
<button class="option-btn" onclick="selectOption(${opt.index})">
${opt.index}. ${opt.content}
</button>
`).join('')}
</div>
`;
currentQuestion = data;
}
function selectOption(index) {
if (!currentQuestion) return;
document.getElementById('answer').value = index;
testAnswer();
}
async function apiRequest(action, params = {}) {
const query = new URLSearchParams({ action, ...params }).toString();
const url = `api.php?${query}`;
showUrl(url);
showStatus('请求中...', 'loading');
try {
const res = await fetch(url);
const data = await res.json();
showStatus('请求成功', 'success');
showResponse(data);
if (data.data && data.data.question) {
showQuestion(data.data);
} else {
document.getElementById('questionPreview').innerHTML = '';
}
return data;
} catch (err) {
showStatus('请求失败: ' + err.message, 'error');
showResponse({ error: err.message });
return null;
}
}
function testQuestion() {
const id = document.getElementById('questionId').value;
apiRequest('question', { id });
}
function testNext() {
apiRequest('next');
}
function testFetch() {
apiRequest('fetch');
}
function testAnswer() {
const id = document.getElementById('questionId').value;
const answer = document.getElementById('answer').value;
apiRequest('answer', { id, answer });
}
function testHint() {
const id = document.getElementById('questionId').value;
apiRequest('hint', { id });
}
function testList() {
apiRequest('list');
}
function testRefresh() {
apiRequest('refresh');
}
function testStats() {
apiRequest('stats');
}
window.onload = () => {
testQuestion();
};
</script>
</body>
</html>