Initial commit: Flutter 无书应用项目
This commit is contained in:
503
ht/p1/API文档.md
Normal file
503
ht/p1/API文档.md
Normal file
@@ -0,0 +1,503 @@
|
||||
# 古诗文答题系统 API 文档
|
||||
|
||||
## 文件信息
|
||||
- **文件路径**: `p1/api.php`
|
||||
- **功能描述**: 古诗文题目获取与答题系统
|
||||
- **编码格式**: UTF-8
|
||||
- **响应格式**: JSON
|
||||
|
||||
## 安全认证
|
||||
|
||||
### Key 认证机制
|
||||
API 采用动态 Key 认证,防止接口被滥用。
|
||||
|
||||
#### 认证原理
|
||||
1. **客户端生成 Key**: 使用 `SHA256(时间戳 + 密钥)` 生成
|
||||
2. **服务端验证**: 验证 Key 是否在有效期内(默认 300 秒)
|
||||
3. **Key 更新**: 每次成功请求后,服务端返回新的 `new_key`
|
||||
|
||||
#### 密钥配置
|
||||
- **前端**: `api.js` 中的 `SECRET_KEY` 变量
|
||||
- **后端**: `api.php` 中的 `SECRET_KEY` 常量
|
||||
- **默认密钥**: `tzgsc_2026_secret_key`
|
||||
|
||||
#### Key 生成算法
|
||||
|
||||
##### PHP
|
||||
```php
|
||||
<?php
|
||||
define('SECRET_KEY', 'tzgsc_2026_secret_key');
|
||||
|
||||
function generate_key() {
|
||||
$timestamp = time();
|
||||
$data = $timestamp . SECRET_KEY;
|
||||
return hash('sha256', $data);
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
$key = generate_key();
|
||||
$url = "api.php?id=1&key=" . urlencode($key);
|
||||
```
|
||||
|
||||
##### JavaScript (Node.js)
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
|
||||
const SECRET_KEY = 'tzgsc_2026_secret_key';
|
||||
|
||||
function generateKey() {
|
||||
const timestamp = Math.floor(Date.now() / 1000);
|
||||
const data = timestamp + SECRET_KEY;
|
||||
return crypto.createHash('sha256').update(data).digest('hex');
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
const key = generateKey();
|
||||
const url = `api.php?id=1&key=${encodeURIComponent(key)}`;
|
||||
```
|
||||
|
||||
##### Python
|
||||
```python
|
||||
import time
|
||||
import hashlib
|
||||
import urllib.parse
|
||||
|
||||
SECRET_KEY = 'tzgsc_2026_secret_key'
|
||||
|
||||
def generate_key():
|
||||
timestamp = int(time.time())
|
||||
data = str(timestamp) + SECRET_KEY
|
||||
return hashlib.sha256(data.encode()).hexdigest()
|
||||
|
||||
# 使用示例
|
||||
key = generate_key()
|
||||
url = f"api.php?id=1&key={urllib.parse.quote(key)}"
|
||||
```
|
||||
|
||||
##### Java
|
||||
```java
|
||||
import java.security.MessageDigest;
|
||||
import java.time.Instant;
|
||||
|
||||
public class KeyGenerator {
|
||||
private static final String SECRET_KEY = "tzgsc_2026_secret_key";
|
||||
|
||||
public static String generateKey() throws Exception {
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
String data = timestamp + SECRET_KEY;
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(data.getBytes("UTF-8"));
|
||||
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hash) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) hexString.append('0');
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
public static void main(String[] args) throws Exception {
|
||||
String key = generateKey();
|
||||
String url = "api.php?id=1&key=" + java.net.URLEncoder.encode(key, "UTF-8");
|
||||
System.out.println(url);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Go
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
const SECRET_KEY = "tzgsc_2026_secret_key"
|
||||
|
||||
func generateKey() string {
|
||||
timestamp := time.Now().Unix()
|
||||
data := fmt.Sprintf("%d%s", timestamp, SECRET_KEY)
|
||||
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
func main() {
|
||||
key := generateKey()
|
||||
encodedKey := url.QueryEscape(key)
|
||||
apiUrl := fmt.Sprintf("api.php?id=1&key=%s", encodedKey)
|
||||
fmt.Println(apiUrl)
|
||||
}
|
||||
```
|
||||
|
||||
##### C#
|
||||
```csharp
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Web;
|
||||
|
||||
public class KeyGenerator
|
||||
{
|
||||
private const string SECRET_KEY = "tzgsc_2026_secret_key";
|
||||
|
||||
public static string GenerateKey()
|
||||
{
|
||||
long timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
string data = timestamp + SECRET_KEY;
|
||||
|
||||
using (SHA256 sha256 = SHA256.Create())
|
||||
{
|
||||
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(data));
|
||||
StringBuilder builder = new StringBuilder();
|
||||
foreach (byte b in hash)
|
||||
{
|
||||
builder.Append(b.ToString("x2"));
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
public static void Main()
|
||||
{
|
||||
string key = GenerateKey();
|
||||
string url = $"api.php?id=1&key={HttpUtility.UrlEncode(key)}";
|
||||
Console.WriteLine(url);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Ruby
|
||||
```ruby
|
||||
require 'digest'
|
||||
require 'cgi'
|
||||
|
||||
SECRET_KEY = 'tzgsc_2026_secret_key'
|
||||
|
||||
def generate_key
|
||||
timestamp = Time.now.to_i
|
||||
data = "#{timestamp}#{SECRET_KEY}"
|
||||
Digest::SHA256.hexdigest(data)
|
||||
end
|
||||
|
||||
# 使用示例
|
||||
key = generate_key
|
||||
url = "api.php?id=1&key=#{CGI.escape(key)}"
|
||||
puts url
|
||||
```
|
||||
|
||||
##### Rust
|
||||
```rust
|
||||
use sha2::{Sha256, Digest};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const SECRET_KEY: &str = "tzgsc_2026_secret_key";
|
||||
|
||||
fn generate_key() -> String {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let data = format!("{}{}", timestamp, SECRET_KEY);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(data.as_bytes());
|
||||
let result = hasher.finalize();
|
||||
|
||||
hex::encode(result)
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
fn main() {
|
||||
let key = generate_key();
|
||||
let encoded_key = urlencoding::encode(&key);
|
||||
let url = format!("api.php?id=1&key={}", encoded_key);
|
||||
println!("{}", url);
|
||||
}
|
||||
```
|
||||
|
||||
### 请求限制
|
||||
|
||||
| 类型 | 限制 | 说明 |
|
||||
|------|------|------|
|
||||
| 无 Key 请求 | 10 次/分钟 | 基于 IP 地址限制 |
|
||||
| 有 Key 请求 | 无限制 | 验证通过后无频率限制 |
|
||||
|
||||
#### 限制配置
|
||||
```php
|
||||
define('KEY_EXPIRE_TIME', 300); // Key 有效期(秒)
|
||||
define('MAX_REQUESTS_NO_KEY', 10); // 无 Key 最大请求次数
|
||||
define('RATE_LIMIT_WINDOW', 60); // 限流时间窗口(秒)
|
||||
```
|
||||
|
||||
## 接口信息
|
||||
|
||||
### 接口概述
|
||||
该接口用于获取古诗文题目、验证答案、获取提示信息,支持 GET 和 POST 请求方式。
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | string/int | 是 | 题目唯一标识符 |
|
||||
| msg | string | 否 | 操作类型:答案序号/提示 |
|
||||
| key | string | 否 | 认证密钥(推荐携带) |
|
||||
|
||||
### 请求示例
|
||||
|
||||
#### 1. 获取题目
|
||||
```
|
||||
GET p1/api.php?id=1&key=xxx
|
||||
```
|
||||
|
||||
POST 请求:
|
||||
```
|
||||
POST p1/api.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "1",
|
||||
"key": "xxx"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 提交答案
|
||||
```
|
||||
GET p1/api.php?id=1&msg=1&key=xxx
|
||||
```
|
||||
|
||||
POST 请求:
|
||||
```
|
||||
POST p1/api.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "1",
|
||||
"msg": "1",
|
||||
"key": "xxx"
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 获取提示
|
||||
```
|
||||
GET p1/api.php?id=1&msg=提示&key=xxx
|
||||
```
|
||||
|
||||
POST 请求:
|
||||
```
|
||||
POST p1/api.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "1",
|
||||
"msg": "提示",
|
||||
"key": "xxx"
|
||||
}
|
||||
```
|
||||
|
||||
### 响应信息
|
||||
|
||||
#### 获取题目成功响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"new_key": "新的认证密钥",
|
||||
"data": {
|
||||
"id": "1",
|
||||
"question": "题目内容",
|
||||
"options": [
|
||||
{
|
||||
"num": 1,
|
||||
"text": "选项1"
|
||||
},
|
||||
{
|
||||
"num": 2,
|
||||
"text": "选项2"
|
||||
},
|
||||
{
|
||||
"num": 3,
|
||||
"text": "选项3"
|
||||
},
|
||||
{
|
||||
"num": 4,
|
||||
"text": "选项4"
|
||||
}
|
||||
],
|
||||
"author": "作者",
|
||||
"type": "描写类型",
|
||||
"grade": "学习阶段",
|
||||
"dynasty": "年代"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 答案正确响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"type": "correct",
|
||||
"message": "恭喜你,回答正确。请继续下一题",
|
||||
"next_question": {
|
||||
"id": "2",
|
||||
"question": "下一题题目",
|
||||
"options": [
|
||||
...
|
||||
],
|
||||
"author": "作者",
|
||||
"type": "描写类型",
|
||||
"grade": "学习阶段",
|
||||
"dynasty": "年代"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 答案错误响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"type": "wrong",
|
||||
"message": "抱歉,答案不对哦。你可以回复提示获取该题的部分信息哦。"
|
||||
}
|
||||
```
|
||||
|
||||
#### 提示响应
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"type": "hint",
|
||||
"message": "这是首描写[描写类型]的诗,你在[学习阶段]学过它。"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取失败响应
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "抱歉,获取出现错误。"
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误参数响应
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "缺少必要参数: id"
|
||||
}
|
||||
```
|
||||
|
||||
#### 不支持的请求方法
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "不支持的请求方法"
|
||||
}
|
||||
```
|
||||
|
||||
#### 请求频率限制
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "请求过于频繁,请稍后再试"
|
||||
}
|
||||
```
|
||||
HTTP 状态码: 429
|
||||
|
||||
## 核心功能说明
|
||||
|
||||
### 1. 题目数据来源
|
||||
- 从百度汉语 API 获取题目数据
|
||||
- API 地址: `https://hanyu.baidu.com/hanyu/ajax/pingce_data`
|
||||
|
||||
### 2. 数据存储
|
||||
- 题目数据存储在 `data/tzgsc/` 目录
|
||||
- 文件格式: `.json`
|
||||
- 文件命名: `{id}.json`
|
||||
- JSON 缓存: `data/tzgsc.json`
|
||||
|
||||
### 3. 数据格式
|
||||
题目数据存储格式 (JSON):
|
||||
```json
|
||||
{
|
||||
"id": "1",
|
||||
"question": "题目内容",
|
||||
"options": [
|
||||
{"num": 1, "text": "选项1"},
|
||||
{"num": 2, "text": "选项2"},
|
||||
{"num": 3, "text": "选项3"},
|
||||
{"num": 4, "text": "选项4"}
|
||||
],
|
||||
"author": "作者",
|
||||
"type": "描写类型",
|
||||
"grade": "学习阶段",
|
||||
"dynasty": "年代",
|
||||
"correct_answer": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 核心函数
|
||||
|
||||
#### get_question($id)
|
||||
获取并返回题目
|
||||
- **参数**: $id - 题目 ID
|
||||
- **返回**: 包含题目信息的数组
|
||||
|
||||
#### submit_answer($id, $msg)
|
||||
提交答案并返回结果
|
||||
- **参数**:
|
||||
- $id - 题目 ID
|
||||
- $msg - 答案序号或"提示"
|
||||
- **返回**: 包含答题结果的数组
|
||||
|
||||
#### cj($data)
|
||||
采集并缓存题目数据
|
||||
- **参数**: $data - API 返回的原始数据
|
||||
- **功能**: 解析题目数据,去重后保存到 JSON 文件
|
||||
|
||||
#### get_curl($url, $post, $referer, $cookie, $header, $ua, $nobaody)
|
||||
发送 HTTP 请求
|
||||
- **参数**:
|
||||
- $url - 请求地址
|
||||
- $post - POST 数据
|
||||
- $referer - 来源地址
|
||||
- $cookie - Cookie
|
||||
- $header - 是否返回头部
|
||||
- $ua - User-Agent
|
||||
- $nobaody - 是否不返回内容
|
||||
- **功能**: 使用 cURL 发送 HTTP 请求,支持 SSL 和 GZIP 压缩
|
||||
|
||||
#### replace_unicode_escape_sequence($match)
|
||||
Unicode 转义序列转换
|
||||
- **参数**: $match - 匹配到的 Unicode 序列
|
||||
- **返回**: UTF-8 编码的字符串
|
||||
- **功能**: 将 Unicode 转义序列转换为 UTF-8 字符
|
||||
|
||||
## 目录结构
|
||||
```
|
||||
p1/
|
||||
├── api.php # API 接口文件
|
||||
├── index.php # 前端页面(服务端生成初始 Key)
|
||||
├── api.js # 前端交互脚本
|
||||
└── data/
|
||||
├── tzgsc.json # 题目缓存文件
|
||||
├── rate_limit/ # 请求限制记录目录
|
||||
└── tzgsc/
|
||||
└── [id].json # 题目数据文件
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
1. 确保 `data/tzgsc/` 目录具有写入权限
|
||||
2. API 依赖百度汉语接口,需确保网络连接正常
|
||||
3. 题目数据会缓存到本地,避免重复请求
|
||||
4. 答案验证时会读取本地保存的题目数据文件
|
||||
5. 支持 Unicode 编码处理和 GZIP 压缩响应
|
||||
6. 支持 GET 和 POST 两种请求方式
|
||||
7. 所有响应均为 JSON 格式
|
||||
344
ht/p1/api.js
Normal file
344
ht/p1/api.js
Normal file
@@ -0,0 +1,344 @@
|
||||
(function() {
|
||||
'use strict';
|
||||
|
||||
var state = {
|
||||
currentId: 1,
|
||||
correctCount: 0,
|
||||
totalCount: 0,
|
||||
selectedOption: null,
|
||||
isLoading: false,
|
||||
answered: false,
|
||||
currentKey: window.INITIAL_KEY || null
|
||||
};
|
||||
|
||||
var elements = {
|
||||
questionCard: document.getElementById('questionCard'),
|
||||
navigation: document.getElementById('navigation'),
|
||||
details: document.getElementById('details'),
|
||||
loading: document.getElementById('loading'),
|
||||
correctCount: document.getElementById('correctCount'),
|
||||
totalCount: document.getElementById('totalCount'),
|
||||
feedback: document.getElementById('feedback'),
|
||||
feedbackIcon: document.getElementById('feedbackIcon'),
|
||||
feedbackText: document.getElementById('feedbackText'),
|
||||
overlay: document.getElementById('overlay'),
|
||||
detailAuthor: document.getElementById('detailAuthor'),
|
||||
detailDynasty: document.getElementById('detailDynasty'),
|
||||
detailType: document.getElementById('detailType'),
|
||||
detailGrade: document.getElementById('detailGrade')
|
||||
};
|
||||
|
||||
function getUrlParam(name) {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
return urlParams.get(name);
|
||||
}
|
||||
|
||||
function setUrlParam(name, value) {
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set(name, value);
|
||||
window.history.replaceState({}, '', url);
|
||||
}
|
||||
|
||||
function fetchData(url, callback, errorCallback) {
|
||||
if (!state.currentKey) {
|
||||
errorCallback('未初始化 Key');
|
||||
return;
|
||||
}
|
||||
|
||||
var separator = url.indexOf('?') !== -1 ? '&' : '?';
|
||||
var fullUrl = url + separator + 'key=' + encodeURIComponent(state.currentKey);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', fullUrl, true);
|
||||
xhr.setRequestHeader('Accept', 'application/json');
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
try {
|
||||
var response = JSON.parse(xhr.responseText);
|
||||
if (response.new_key) {
|
||||
state.currentKey = response.new_key;
|
||||
}
|
||||
callback(response);
|
||||
} catch (e) {
|
||||
errorCallback('解析响应失败');
|
||||
}
|
||||
} else if (xhr.status === 429) {
|
||||
errorCallback('请求过于频繁,请稍后再试');
|
||||
} else {
|
||||
errorCallback('请求失败,状态码: ' + xhr.status);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
errorCallback('网络错误');
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
}
|
||||
|
||||
function fetchQuestion(id) {
|
||||
if (state.isLoading) return;
|
||||
|
||||
state.isLoading = true;
|
||||
state.answered = false;
|
||||
state.selectedOption = null;
|
||||
|
||||
showLoading();
|
||||
|
||||
fetchData('api.php?id=' + id, function(response) {
|
||||
state.isLoading = false;
|
||||
|
||||
if (response.success) {
|
||||
renderQuestion(response.data);
|
||||
updateNavigation(id);
|
||||
updateDetails(response.data);
|
||||
} else {
|
||||
showError(response.message || '获取题目失败');
|
||||
}
|
||||
}, function(error) {
|
||||
state.isLoading = false;
|
||||
showError(error);
|
||||
});
|
||||
}
|
||||
|
||||
function submitAnswer(id, answer) {
|
||||
if (state.isLoading || state.answered) return;
|
||||
|
||||
state.isLoading = true;
|
||||
|
||||
fetchData('api.php?id=' + id + '&msg=' + answer, function(response) {
|
||||
state.isLoading = false;
|
||||
|
||||
if (response.success) {
|
||||
state.totalCount++;
|
||||
updateStats();
|
||||
|
||||
if (response.type === 'correct') {
|
||||
state.correctCount++;
|
||||
updateStats();
|
||||
showFeedback('correct', '🎉 ' + response.message);
|
||||
markOption(answer, 'correct');
|
||||
|
||||
setTimeout(function() {
|
||||
hideFeedback();
|
||||
if (response.next_question) {
|
||||
state.currentId++;
|
||||
setUrlParam('id', state.currentId);
|
||||
renderQuestion(response.next_question);
|
||||
updateNavigation(state.currentId);
|
||||
updateDetails(response.next_question);
|
||||
} else {
|
||||
state.currentId++;
|
||||
setUrlParam('id', state.currentId);
|
||||
fetchQuestion(state.currentId);
|
||||
}
|
||||
}, 1500);
|
||||
} else if (response.type === 'wrong') {
|
||||
showFeedback('wrong', '😢 ' + response.message);
|
||||
markOption(answer, 'wrong');
|
||||
setTimeout(function() {
|
||||
hideFeedback();
|
||||
clearOptionMark(answer);
|
||||
}, 2000);
|
||||
} else if (response.type === 'hint') {
|
||||
showHintMessage(response.message);
|
||||
}
|
||||
} else {
|
||||
showFeedback('wrong', response.message || '提交失败');
|
||||
setTimeout(hideFeedback, 2000);
|
||||
}
|
||||
}, function(error) {
|
||||
state.isLoading = false;
|
||||
showFeedback('wrong', error);
|
||||
setTimeout(hideFeedback, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function getHint(id) {
|
||||
if (state.isLoading) return;
|
||||
|
||||
state.isLoading = true;
|
||||
|
||||
fetchData('api.php?id=' + id + '&msg=提示', function(response) {
|
||||
state.isLoading = false;
|
||||
|
||||
if (response.success && response.type === 'hint') {
|
||||
showHintMessage(response.message);
|
||||
} else {
|
||||
showFeedback('wrong', response.message || '获取提示失败');
|
||||
setTimeout(hideFeedback, 2000);
|
||||
}
|
||||
}, function(error) {
|
||||
state.isLoading = false;
|
||||
showFeedback('wrong', error);
|
||||
setTimeout(hideFeedback, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
elements.questionCard.innerHTML = '<div class="loading">加载中</div>';
|
||||
elements.details.style.display = 'none';
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
elements.questionCard.innerHTML =
|
||||
'<div class="error-message">' +
|
||||
'<p>❌ ' + message + '</p>' +
|
||||
'<button class="retry-btn" onclick="PoetryQuiz.retry()">🔄 重试</button>' +
|
||||
'</div>';
|
||||
elements.details.style.display = 'none';
|
||||
}
|
||||
|
||||
function renderQuestion(data) {
|
||||
var html = '<span class="question-number">第 ' + data.id + ' 题</span>';
|
||||
html += '<div class="question">' + escapeHtml(data.question) + '</div>';
|
||||
html += '<div class="options">';
|
||||
|
||||
data.options.forEach(function(option) {
|
||||
html += '<button class="option-btn" data-num="' + option.num + '" onclick="PoetryQuiz.selectOption(' + option.num + ')">';
|
||||
html += '<span class="option-num">' + option.num + '</span>';
|
||||
html += '<span class="option-text">' + escapeHtml(option.text) + '</span>';
|
||||
html += '</button>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
html += '<button class="hint-btn" onclick="PoetryQuiz.getHint()">';
|
||||
html += '💡 获取提示';
|
||||
html += '</button>';
|
||||
html += '<div class="hint-message" id="hintMessage"></div>';
|
||||
|
||||
elements.questionCard.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateNavigation(currentId) {
|
||||
var html = '<button class="nav-btn random" onclick="PoetryQuiz.goToRandomQuestion()">🎲 随机</button>';
|
||||
var start = Math.max(1, currentId - 4);
|
||||
var end = currentId + 5;
|
||||
|
||||
for (var i = start; i <= end; i++) {
|
||||
var activeClass = i === currentId ? ' active' : '';
|
||||
html += '<button class="nav-btn' + activeClass + '" onclick="PoetryQuiz.goToQuestion(' + i + ')">' + i + '</button>';
|
||||
}
|
||||
|
||||
elements.navigation.innerHTML = html;
|
||||
}
|
||||
|
||||
function updateDetails(data) {
|
||||
elements.detailAuthor.textContent = data.author || '-';
|
||||
elements.detailDynasty.textContent = data.dynasty || '-';
|
||||
elements.detailType.textContent = data.type || '-';
|
||||
elements.detailGrade.textContent = data.grade || '-';
|
||||
elements.details.style.display = 'grid';
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
elements.correctCount.textContent = state.correctCount;
|
||||
elements.totalCount.textContent = state.totalCount;
|
||||
}
|
||||
|
||||
function selectOption(num) {
|
||||
if (state.answered || state.isLoading) return;
|
||||
|
||||
var buttons = document.querySelectorAll('.option-btn');
|
||||
buttons.forEach(function(btn) {
|
||||
btn.classList.remove('selected');
|
||||
});
|
||||
|
||||
var selectedBtn = document.querySelector('.option-btn[data-num="' + num + '"]');
|
||||
if (selectedBtn) {
|
||||
selectedBtn.classList.add('selected');
|
||||
}
|
||||
|
||||
state.selectedOption = num;
|
||||
submitAnswer(state.currentId, num);
|
||||
}
|
||||
|
||||
function markOption(num, type) {
|
||||
var btn = document.querySelector('.option-btn[data-num="' + num + '"]');
|
||||
if (btn) {
|
||||
btn.classList.add(type);
|
||||
}
|
||||
}
|
||||
|
||||
function clearOptionMark(num) {
|
||||
var btn = document.querySelector('.option-btn[data-num="' + num + '"]');
|
||||
if (btn) {
|
||||
btn.classList.remove('wrong', 'selected');
|
||||
}
|
||||
}
|
||||
|
||||
function showFeedback(type, message) {
|
||||
elements.feedback.className = 'feedback show ' + type;
|
||||
elements.feedbackIcon.textContent = type === 'correct' ? '🎉' : '😢';
|
||||
elements.feedbackText.textContent = message.replace(/^[🎉😢]\s*/, '');
|
||||
elements.overlay.classList.add('show');
|
||||
}
|
||||
|
||||
function hideFeedback() {
|
||||
elements.feedback.classList.remove('show');
|
||||
elements.overlay.classList.remove('show');
|
||||
}
|
||||
|
||||
function showHintMessage(message) {
|
||||
var hintEl = document.getElementById('hintMessage');
|
||||
if (hintEl) {
|
||||
hintEl.textContent = '💡 ' + message;
|
||||
hintEl.classList.add('show');
|
||||
}
|
||||
}
|
||||
|
||||
function goToQuestion(id) {
|
||||
if (state.isLoading) return;
|
||||
|
||||
state.currentId = id;
|
||||
state.answered = false;
|
||||
state.selectedOption = null;
|
||||
setUrlParam('id', id);
|
||||
fetchQuestion(id);
|
||||
}
|
||||
|
||||
function goToRandomQuestion() {
|
||||
if (state.isLoading) return;
|
||||
|
||||
var randomId = Math.floor(Math.random() * 500) + 1;
|
||||
goToQuestion(randomId);
|
||||
}
|
||||
|
||||
function retry() {
|
||||
fetchQuestion(state.currentId);
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function init() {
|
||||
var idParam = getUrlParam('id');
|
||||
if (idParam) {
|
||||
state.currentId = parseInt(idParam, 10) || 1;
|
||||
} else {
|
||||
state.currentId = Math.floor(Math.random() * 500) + 1;
|
||||
}
|
||||
|
||||
fetchQuestion(state.currentId);
|
||||
}
|
||||
|
||||
window.PoetryQuiz = {
|
||||
selectOption: selectOption,
|
||||
getHint: function() { getHint(state.currentId); },
|
||||
goToQuestion: goToQuestion,
|
||||
goToRandomQuestion: goToRandomQuestion,
|
||||
retry: retry
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
375
ht/p1/api.php
Normal file
375
ht/p1/api.php
Normal file
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
header("Content-Type: application/json; charset=UTF-8");
|
||||
|
||||
define('SECRET_KEY', 'tzgsc_2026_secret_key');
|
||||
define('KEY_EXPIRE_TIME', 300);
|
||||
define('MAX_REQUESTS_NO_KEY', 10);
|
||||
define('RATE_LIMIT_WINDOW', 60);
|
||||
|
||||
function replace_unicode_escape_sequence($match) {
|
||||
return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');
|
||||
}
|
||||
|
||||
function send_json_response($data, $status_code = 200) {
|
||||
http_response_code($status_code);
|
||||
echo json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
exit;
|
||||
}
|
||||
|
||||
function error_response($message, $status_code = 400) {
|
||||
send_json_response([
|
||||
'success' => false,
|
||||
'message' => $message
|
||||
], $status_code);
|
||||
}
|
||||
|
||||
function generate_new_key($timestamp = null) {
|
||||
if ($timestamp === null) {
|
||||
$timestamp = time();
|
||||
}
|
||||
$data = $timestamp . SECRET_KEY;
|
||||
return hash('sha256', $data);
|
||||
}
|
||||
|
||||
function verify_key($key, &$new_key = null) {
|
||||
if (empty($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$current_time = time();
|
||||
|
||||
for ($i = 0; $i <= KEY_EXPIRE_TIME; $i++) {
|
||||
$expected_key = generate_new_key($current_time - $i);
|
||||
if (hash_equals($expected_key, $key)) {
|
||||
$new_key = generate_new_key();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function get_client_ip() {
|
||||
$ip = '';
|
||||
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
$ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$ip = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
|
||||
} elseif (!empty($_SERVER['REMOTE_ADDR'])) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
return trim($ip);
|
||||
}
|
||||
|
||||
function check_rate_limit($ip) {
|
||||
$limit_file = "data/rate_limit/" . md5($ip) . ".json";
|
||||
|
||||
if (!is_dir("data/rate_limit")) {
|
||||
mkdir("data/rate_limit", 0755, true);
|
||||
}
|
||||
|
||||
$current_time = time();
|
||||
$requests = [];
|
||||
|
||||
if (file_exists($limit_file)) {
|
||||
$requests = json_decode(file_get_contents($limit_file), true) ?: [];
|
||||
}
|
||||
|
||||
$requests = array_filter($requests, function($time) use ($current_time) {
|
||||
return ($current_time - $time) < RATE_LIMIT_WINDOW;
|
||||
});
|
||||
|
||||
return count($requests);
|
||||
}
|
||||
|
||||
function record_request($ip) {
|
||||
$limit_file = "data/rate_limit/" . md5($ip) . ".json";
|
||||
|
||||
if (!is_dir("data/rate_limit")) {
|
||||
mkdir("data/rate_limit", 0755, true);
|
||||
}
|
||||
|
||||
$current_time = time();
|
||||
$requests = [];
|
||||
|
||||
if (file_exists($limit_file)) {
|
||||
$requests = json_decode(file_get_contents($limit_file), true) ?: [];
|
||||
}
|
||||
|
||||
$requests[] = $current_time;
|
||||
|
||||
$requests = array_filter($requests, function($time) use ($current_time) {
|
||||
return ($current_time - $time) < RATE_LIMIT_WINDOW;
|
||||
});
|
||||
|
||||
file_put_contents($limit_file, json_encode(array_values($requests)));
|
||||
}
|
||||
|
||||
function get_key_param() {
|
||||
if (isset($_GET['key'])) {
|
||||
return $_GET['key'];
|
||||
}
|
||||
if (isset($_POST['key'])) {
|
||||
return $_POST['key'];
|
||||
}
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (isset($input['key'])) {
|
||||
return $input['key'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$key = get_key_param();
|
||||
$ip = get_client_ip();
|
||||
$new_key = null;
|
||||
|
||||
if (!verify_key($key, $new_key)) {
|
||||
$request_count = check_rate_limit($ip);
|
||||
|
||||
if ($request_count >= MAX_REQUESTS_NO_KEY) {
|
||||
error_response('请求过于频繁,请稍后再试', 429);
|
||||
}
|
||||
|
||||
record_request($ip);
|
||||
}
|
||||
|
||||
if ($method === 'GET') {
|
||||
$id = isset($_GET['id']) ? $_GET['id'] : '';
|
||||
$msg = isset($_GET['msg']) ? $_GET['msg'] : '';
|
||||
|
||||
if (empty($id)) {
|
||||
error_response('缺少必要参数: id');
|
||||
}
|
||||
|
||||
if (empty($msg)) {
|
||||
$result = get_question($id);
|
||||
if ($new_key) {
|
||||
$result['new_key'] = $new_key;
|
||||
}
|
||||
send_json_response($result);
|
||||
} else {
|
||||
$result = submit_answer($id, $msg);
|
||||
if ($new_key) {
|
||||
$result['new_key'] = $new_key;
|
||||
}
|
||||
send_json_response($result);
|
||||
}
|
||||
} elseif ($method === 'POST') {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
$id = isset($input['id']) ? $input['id'] : '';
|
||||
$msg = isset($input['msg']) ? $input['msg'] : '';
|
||||
|
||||
if (empty($id)) {
|
||||
error_response('缺少必要参数: id');
|
||||
}
|
||||
|
||||
if (empty($msg)) {
|
||||
$result = get_question($id);
|
||||
if ($new_key) {
|
||||
$result['new_key'] = $new_key;
|
||||
}
|
||||
send_json_response($result);
|
||||
} else {
|
||||
$result = submit_answer($id, $msg);
|
||||
if ($new_key) {
|
||||
$result['new_key'] = $new_key;
|
||||
}
|
||||
send_json_response($result);
|
||||
}
|
||||
} else {
|
||||
error_response('不支持的请求方法', 405);
|
||||
}
|
||||
|
||||
function get_question($id) {
|
||||
$data = get_curl("https://hanyu.baidu.com/hanyu/ajax/pingce_data");
|
||||
$data = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', 'replace_unicode_escape_sequence', $data);
|
||||
|
||||
$cj = cj($data);
|
||||
|
||||
$s = preg_match_all('/{"question_content":"(.*?)","type":{"person":"(.*?)","type":"(.*?)","grade":"(.*?)","dynasty":"(.*?)"},"option_answers":\[(.*?)\]}/', $data, $t);
|
||||
|
||||
if ($s == 0) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '抱歉,获取出现错误。'
|
||||
];
|
||||
}
|
||||
|
||||
$tm = $t[1][0];
|
||||
$z = $t[2][0];
|
||||
$l = $t[3][0];
|
||||
$n = $t[4][0];
|
||||
$nd = $t[5][0];
|
||||
|
||||
preg_match_all('/{"answer_content":"(.*?)","is_standard_answer":(.*?)}/', $t[6][0], $d);
|
||||
|
||||
$options = [];
|
||||
$correct_answer = null;
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$d1 = $d[1][$i];
|
||||
$p = $d[2][$i];
|
||||
$option_num = $i + 1;
|
||||
|
||||
$options[] = [
|
||||
'num' => $option_num,
|
||||
'text' => $d1
|
||||
];
|
||||
|
||||
if ($p == "1") {
|
||||
$correct_answer = $option_num;
|
||||
}
|
||||
}
|
||||
|
||||
$question_data = [
|
||||
'id' => $id,
|
||||
'question' => $tm,
|
||||
'options' => $options,
|
||||
'author' => $z,
|
||||
'type' => $l,
|
||||
'grade' => $n,
|
||||
'dynasty' => $nd,
|
||||
'correct_answer' => $correct_answer
|
||||
];
|
||||
|
||||
file_put_contents("data/tzgsc/" . $id . ".json", json_encode($question_data, JSON_UNESCAPED_UNICODE));
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'id' => $id,
|
||||
'question' => $tm,
|
||||
'options' => $options,
|
||||
'author' => $z,
|
||||
'type' => $l,
|
||||
'grade' => $n,
|
||||
'dynasty' => $nd
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
function submit_answer($id, $msg) {
|
||||
$data_file = "data/tzgsc/" . $id . ".json";
|
||||
|
||||
if (!file_exists($data_file)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => '题目数据不存在,请先获取题目'
|
||||
];
|
||||
}
|
||||
|
||||
$question_data = json_decode(file_get_contents($data_file), true);
|
||||
|
||||
if ($msg === "提示") {
|
||||
return [
|
||||
'success' => true,
|
||||
'type' => 'hint',
|
||||
'message' => '这是首描写' . $question_data['type'] . '的诗,你在' . $question_data['grade'] . '学过它。'
|
||||
];
|
||||
}
|
||||
|
||||
if ($msg == $question_data['correct_answer']) {
|
||||
$next_id = $id + 1;
|
||||
$next_question = get_question($next_id);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'type' => 'correct',
|
||||
'message' => '恭喜你,回答正确。请继续下一题',
|
||||
'next_question' => $next_question['success'] ? $next_question['data'] : null
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
'success' => true,
|
||||
'type' => 'wrong',
|
||||
'message' => '抱歉,答案不对哦。你可以回复提示获取该题的部分信息哦。'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
function cj($data) {
|
||||
if (!$data) return;
|
||||
|
||||
$s = preg_match_all('/{"question_content":"(.*?)","type":(.*?)}]}/', $data, $d);
|
||||
if ($s == 0) return;
|
||||
|
||||
$json_file = "data/tzgsc.json";
|
||||
|
||||
$existing = [];
|
||||
if (file_exists($json_file)) {
|
||||
$content = file_get_contents($json_file);
|
||||
$existing = json_decode($content, true);
|
||||
if (!is_array($existing)) {
|
||||
$existing = [];
|
||||
}
|
||||
}
|
||||
|
||||
$existing_questions = [];
|
||||
foreach ($existing as $item) {
|
||||
if (isset($item['question_content'])) {
|
||||
$existing_questions[$item['question_content']] = true;
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i < $s; $i++) {
|
||||
$d1 = $d[1][$i];
|
||||
$d2 = $d[2][$i];
|
||||
|
||||
if (!isset($existing_questions[$d1])) {
|
||||
$new_item = [
|
||||
'question_content' => $d1,
|
||||
'type' => json_decode('{' . $d2, true)
|
||||
];
|
||||
if ($new_item['type']) {
|
||||
$existing[] = $new_item;
|
||||
$existing_questions[$d1] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
file_put_contents($json_file, json_encode($existing, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
function get_curl($url, $post = 0, $referer = 1, $cookie = 0, $header = 0, $ua = 0, $nobaody = 0) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$httpheader[] = "Accept:application/json";
|
||||
$httpheader[] = "Accept-Encoding:gzip,deflate,sdch";
|
||||
$httpheader[] = "Accept-Language:zh-CN,zh;q=0.8";
|
||||
$httpheader[] = "Connection:close";
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
|
||||
if ($post) {
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
|
||||
}
|
||||
if ($header) {
|
||||
curl_setopt($ch, CURLOPT_HEADER, TRUE);
|
||||
}
|
||||
if ($cookie) {
|
||||
curl_setopt($ch, CURLOPT_DICTAPP_MID, $cookie);
|
||||
}
|
||||
if ($referer) {
|
||||
if ($referer == 1) {
|
||||
curl_setopt($ch, CURLOPT_REFERER, 'http://m.qzone.com/infocenter?g_f=');
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_REFERER, $referer);
|
||||
}
|
||||
}
|
||||
if ($ua) {
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Linux; U; Android 4.4.1; zh-cn) AppleWebKit/533.1 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.5 Mobile Safari/533.1');
|
||||
}
|
||||
if ($nobaody) {
|
||||
curl_setopt($ch, CURLOPT_NOBODY, 1);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_ENCODING, "gzip");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
$ret = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $ret;
|
||||
}
|
||||
?>
|
||||
5456
ht/p1/data/tzgsc.json
Normal file
5456
ht/p1/data/tzgsc.json
Normal file
File diff suppressed because it is too large
Load Diff
74
ht/p1/index.php
Normal file
74
ht/p1/index.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
define('SECRET_KEY', 'tzgsc_2026_secret_key');
|
||||
|
||||
function generate_initial_key() {
|
||||
$timestamp = time();
|
||||
$data = $timestamp . SECRET_KEY;
|
||||
return hash('sha256', $data);
|
||||
}
|
||||
|
||||
$initial_key = generate_initial_key();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>古诗文答题</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header class="header">
|
||||
<h1>📜 古诗文答题</h1>
|
||||
<div class="stats">
|
||||
<span class="correct">✓ 答对: <strong id="correctCount">0</strong></span>
|
||||
<span>📝 总题: <strong id="totalCount">0</strong></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<nav class="navigation" id="navigation">
|
||||
</nav>
|
||||
|
||||
<main class="card" id="questionCard">
|
||||
<div class="loading" id="loading">加载中</div>
|
||||
</main>
|
||||
|
||||
<section class="details" id="details" style="display: none;">
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">作者</div>
|
||||
<div class="detail-value" id="detailAuthor">-</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">朝代</div>
|
||||
<div class="detail-value" id="detailDynasty">-</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">类型</div>
|
||||
<div class="detail-value" id="detailType">-</div>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<div class="detail-label">年级</div>
|
||||
<div class="detail-value" id="detailGrade">-</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
<a href="mailto:developer@example.com?subject=古诗文答题接口咨询" class="contact-btn">
|
||||
📧 联系开发者获取接口
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<div class="overlay" id="overlay"></div>
|
||||
<div class="feedback" id="feedback">
|
||||
<div class="feedback-icon" id="feedbackIcon"></div>
|
||||
<div class="feedback-text" id="feedbackText"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.INITIAL_KEY = '<?php echo $initial_key; ?>';
|
||||
</script>
|
||||
<script src="api.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
469
ht/p1/style.css
Normal file
469
ht/p1/style.css
Normal file
@@ -0,0 +1,469 @@
|
||||
:root {
|
||||
--bg-primary: #f5f0e6;
|
||||
--bg-card: #fffef9;
|
||||
--text-primary: #5d4e37;
|
||||
--text-secondary: #8b7355;
|
||||
--accent: #c94c4c;
|
||||
--border: #d4c5a9;
|
||||
--success: #4a7c59;
|
||||
--error: #c94c4c;
|
||||
--shadow: rgba(93, 78, 55, 0.1);
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', 'PingFang SC', sans-serif;
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
background-image:
|
||||
radial-gradient(circle at 20% 80%, rgba(201, 76, 76, 0.05) 0%, transparent 50%),
|
||||
radial-gradient(circle at 80% 20%, rgba(74, 124, 89, 0.05) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-family: 'KaiTi', 'STKaiti', '楷体', serif;
|
||||
font-size: 28px;
|
||||
font-weight: normal;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.stats span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stats .correct {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.navigation {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
padding: 12px;
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
color: var(--text-secondary);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.nav-btn:hover {
|
||||
background: var(--bg-primary);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.nav-btn.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 30px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 20px var(--shadow);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, var(--accent), var(--success));
|
||||
}
|
||||
|
||||
.question-number {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-primary);
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.question {
|
||||
font-family: 'KaiTi', 'STKaiti', '楷体', serif;
|
||||
font-size: 22px;
|
||||
line-height: 1.8;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, rgba(212, 197, 169, 0.2), transparent);
|
||||
border-radius: 12px;
|
||||
border-left: 3px solid var(--accent);
|
||||
}
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
background: var(--bg-card);
|
||||
border: 2px solid var(--border);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
text-align: left;
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.option-btn:hover {
|
||||
border-color: var(--accent);
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 2px 10px var(--shadow);
|
||||
}
|
||||
|
||||
.option-btn.selected {
|
||||
border-color: var(--accent);
|
||||
background: rgba(201, 76, 76, 0.05);
|
||||
}
|
||||
|
||||
.option-btn.correct {
|
||||
border-color: var(--success);
|
||||
background: rgba(74, 124, 89, 0.1);
|
||||
}
|
||||
|
||||
.option-btn.wrong {
|
||||
border-color: var(--error);
|
||||
background: rgba(201, 76, 76, 0.1);
|
||||
}
|
||||
|
||||
.option-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 6px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-btn:hover .option-num {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.option-btn.selected .option-num {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.option-btn.correct .option-num {
|
||||
background: var(--success);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.option-btn.wrong .option-num {
|
||||
background: var(--error);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hint-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.hint-btn:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
background: rgba(201, 76, 76, 0.05);
|
||||
}
|
||||
|
||||
.details {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--border);
|
||||
padding: 20px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.feedback {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) scale(0.8);
|
||||
background: var(--bg-card);
|
||||
border-radius: 16px;
|
||||
padding: 30px 50px;
|
||||
text-align: center;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.feedback.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
.feedback.correct {
|
||||
border: 2px solid var(--success);
|
||||
}
|
||||
|
||||
.feedback.wrong {
|
||||
border: 2px solid var(--error);
|
||||
}
|
||||
|
||||
.feedback-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.feedback-text {
|
||||
font-size: 18px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.overlay.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.hint-message {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
border: 1px solid var(--accent);
|
||||
padding: 16px;
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hint-message.show {
|
||||
display: block;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.loading::after {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--border);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.error-message {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
padding: 10px 20px;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.nav-btn.random {
|
||||
background: linear-gradient(135deg, var(--accent), #e67e22);
|
||||
color: white;
|
||||
border: none;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.nav-btn.random:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 2px 10px rgba(201, 76, 76, 0.3);
|
||||
}
|
||||
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.contact-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 24px;
|
||||
background: linear-gradient(135deg, var(--accent), #e67e22);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.contact-btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 15px rgba(201, 76, 76, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.question {
|
||||
font-size: 18px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.details {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
97
ht/p1/syx.php
Normal file
97
ht/p1/syx.php
Normal file
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
header("Content-Type:text/html;charset=UTF-8");
|
||||
function replace_unicode_escape_sequence($match){
|
||||
return mb_convert_encoding(pack('H*', $match[1]), 'UTF-8', 'UCS-2BE');}
|
||||
if($_GET["msg"]==""){
|
||||
echo get($_GET["id"]);
|
||||
}else{
|
||||
$data=file_get_contents("data/tzgsc/".$_GET["id"].".txt");
|
||||
preg_match_all('/tm(.*?)z(.*?)l(.*?)n(.*?)nian(.*?)d(.*?)d/',$data,$t);
|
||||
if($_GET["msg"]==$t[6][0]){
|
||||
echo "恭喜你,回答正确。\n请继续下一题\n\n";
|
||||
exit(get($id));}else{
|
||||
if($_GET["msg"]=="提示"){
|
||||
exit("这是首描写".$t[3][0]."的诗,你在".$t[4][0]."学过它。");}
|
||||
exit("抱歉,答案不对哦。\n你可以回复提示获取该题的部分信息哦。");}
|
||||
}
|
||||
|
||||
|
||||
//http://poe.pmmu.cn/tzgsc.php?msg=1
|
||||
function get($id){
|
||||
$data=get_curl("https://hanyu.baidu.com/hanyu/ajax/pingce_data");
|
||||
$data = preg_replace_callback('/\\\\u([0-9a-f]{4})/i', 'replace_unicode_escape_sequence', $data);
|
||||
//exit($data);
|
||||
$cj=cj($data);
|
||||
$s = preg_match_all('/{"question_content":"(.*?)","type":{"person":"(.*?)","type":"(.*?)","grade":"(.*?)","dynasty":"(.*?)"},"option_answers":\[(.*?)\]}/',$data,$t);
|
||||
if($s==0){exit("抱歉,获取出现错误。");}
|
||||
$tm=$t[1][0];//题目
|
||||
$z=$t[2][0];//作者
|
||||
$l=$t[3][0];//描写类型
|
||||
$n=$t[4][0];//学习阶段
|
||||
$nd=$t[5][0];//年代
|
||||
echo $id."如题:".$tm."\n请从下面四个选项中选择一个你认为对的来回答。\n\n";
|
||||
preg_match_all('/{"answer_content":"(.*?)","is_standard_answer":(.*?)}/',$t[6][0],$d);
|
||||
$ps="tm".$tm."z".$z."l".$l."n".$n."nian".$nd;
|
||||
for( $i = 0 ; $i < 4 ; $i ++ ){
|
||||
$d1=$d[1][$i];
|
||||
$p=$d[2][$i];
|
||||
if($p=="1"){file_put_contents("data/tzgsc/".$id.".txt",$ps."d".($i+1)."d");
|
||||
}
|
||||
echo ($i+1)."、".$d1."\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function cj($data){
|
||||
if(!$data)exit("");//无数据返回
|
||||
$s=preg_match_all('/{"question_content":"(.*?)","type":(.*?)}]}/',$data,$d);
|
||||
$json="data/tzgsc/tzgsc.json";
|
||||
@$pd = file_get_contents($json);
|
||||
for( $i = 0 ; $i < $s ; $i ++ ){
|
||||
$d1=$d[1][$i];
|
||||
$d2=$d[2][$i];
|
||||
$a='{"question_content":"'.$d1.'","type":'.$d2.'}]}';
|
||||
$p = explode($d1,$pd);
|
||||
if(count($p)>1){
|
||||
//存在不写入
|
||||
}else{
|
||||
file_put_contents($json, $a, FILE_APPEND | LOCK_EX);//追加写入,防止同时写入。
|
||||
//不存在,继续写入。
|
||||
}
|
||||
}}
|
||||
|
||||
function get_curl($url,$post=0,$referer=1,$cookie=0,$header=0,$ua=0,$nobaody=0){
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL,$url);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$httpheader[] = "Accept:application/json";
|
||||
$httpheader[] = "Accept-Encoding:gzip,deflate,sdch";
|
||||
$httpheader[] = "Accept-Language:zh-CN,zh;q=0.8";
|
||||
$httpheader[] = "Connection:close";
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
|
||||
if($post){
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);}
|
||||
if($header){
|
||||
curl_setopt($ch, CURLOPT_HEADER, TRUE);}
|
||||
if($cookie){
|
||||
curl_setopt($ch, CURLOPT_DICTAPP_MID, $cookie);}
|
||||
if($referer){
|
||||
if($referer==1){
|
||||
curl_setopt($ch, CURLOPT_REFERER, 'http://m.qzone.com/infocenter?g_f=');
|
||||
}else{
|
||||
curl_setopt($ch, CURLOPT_REFERER, $referer);}}
|
||||
if($ua){
|
||||
curl_setopt($ch, CURLOPT_USERAGENT,$ua);
|
||||
}else{
|
||||
curl_setopt($ch, CURLOPT_USERAGENT,'Mozilla/5.0 (Linux; U; Android 4.4.1; zh-cn) AppleWebKit/533.1 (KHTML, like Gecko)Version/4.0 MQQBrowser/5.5 Mobile Safari/533.1');}
|
||||
if($nobaody){
|
||||
curl_setopt($ch, CURLOPT_NOBODY,1);}
|
||||
curl_setopt($ch, CURLOPT_ENCODING, "gzip");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
|
||||
$ret = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $ret;}
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user