提交 c27580df authored 作者: 王鹏飞's avatar 王鹏飞

feat(reader): add reader API, bridge, and content handling

- Implemented readerApi for handling POST requests to the reader service. - Created readerBridge for communication with Flutter, including methods to call handlers and open webviews. - Defined READER_ENDPOINTS for various API endpoints related to book information and user interactions. - Added functions to normalize offline payloads and check for valid offline data structures. - Updated Vite configuration to improve project structure and build process, including dynamic HTML entry points and output settings.
上级 843e729c
......@@ -13,12 +13,11 @@
**参数格式**: 逗号分隔的字符串
```javascript
callbackInFlutterComponent('baseURL,book_id,chapter_id,token,position')
callbackInFlutterComponent('book_id,chapter_id,token,position')
```
**参数说明**:
- `baseURL` (string): API 基础地址
- `book_id` (string): 书籍 ID,例如: "175"
- `chapter_id` (string): 章节 ID,例如: "335"
- `token` (string): 用户认证令牌
......@@ -27,7 +26,7 @@ callbackInFlutterComponent('baseURL,book_id,chapter_id,token,position')
**示例**:
```javascript
callbackInFlutterComponent('https://api.example.com,175,335,726cca8efcb328672202656f63e176e8,bgi0jn6p')
callbackInFlutterComponent('175,335,726cca8efcb328672202656f63e176e8,bgi0jn6p')
```
---
......@@ -37,12 +36,11 @@ callbackInFlutterComponent('https://api.example.com,175,335,726cca8efcb328672202
**参数格式**: 逗号分隔的字符串
```javascript
callbackInFlutterComponent('baseURL,book_id,chapter_id,token,position')
callbackInFlutterComponent('book_id,chapter_id,token,position')
```
**参数说明**:
- `baseURL` (string): API 基础地址
- `book_id` (string): 书籍 ID,例如: "323"
- `chapter_id` (string): 章节 ID,例如: "2635"
- `token` (string): 用户认证令牌
......@@ -51,7 +49,7 @@ callbackInFlutterComponent('baseURL,book_id,chapter_id,token,position')
**示例**:
```javascript
callbackInFlutterComponent('https://api.example.com,175,2834,726cca8efcb328672202656f63e176e8,bgi0jn6p')
callbackInFlutterComponent('175,2834,726cca8efcb328672202656f63e176e8,bgi0jn6p')
```
---
......@@ -61,19 +59,18 @@ callbackInFlutterComponent('https://api.example.com,175,2834,726cca8efcb32867220
**参数格式**: 逗号分隔的字符串
```javascript
callbackInFlutterComponent('baseURL,book_id,token')
callbackInFlutterComponent('book_id,token')
```
**参数说明**:
- `baseURL` (string): API 基础地址
- `book_id` (string): 书籍 ID
- `token` (string): 用户认证令牌
**示例**:
```javascript
callbackInFlutterComponent('https://api.example.com,175,726cca8efcb328672202656f63e176e8')
callbackInFlutterComponent('175,726cca8efcb328672202656f63e176e8')
```
---
......@@ -83,12 +80,11 @@ callbackInFlutterComponent('https://api.example.com,175,726cca8efcb328672202656f
**参数格式**: 逗号分隔的字符串
```javascript
callbackInFlutterComponent('baseURL,book_id,chapter_id,token,noteId,searchText')
callbackInFlutterComponent('book_id,chapter_id,token,noteId,searchText')
```
**参数说明**:
- `baseURL` (string): API 基础地址
- `book_id` (string): 书籍 ID,例如: "175" 或 "358"
- `chapter_id` (string): 章节 ID,例如: "335" 或 "1931"
- `token` (string): 用户认证令牌
......@@ -98,46 +94,17 @@ callbackInFlutterComponent('baseURL,book_id,chapter_id,token,noteId,searchText')
**示例**:
```javascript
callbackInFlutterComponent('https://api.example.com,175,335,2f85d063f94485c0c2ef31e652875bd5,,')
callbackInFlutterComponent('175,335,2f85d063f94485c0c2ef31e652875bd5,,')
```
---
### 5. read_unline.html (在线阅读页面)
### 5. read_info.html (阅读信息页面)
**参数格式**: 对象形式
```javascript
callbackInFlutterComponent({
book_id: '175',
chapter_id: '335',
next_chapter: {...}, // 下一章节信息
up_chapter: {...}, // 上一章节信息
color_line: {...}, // 高亮信息
direction: 'scrollUp' // 可选,滚动方向
})
```
**参数说明**:
- `book_id` (string): 书籍 ID
- `chapter_id` (string): 章节 ID
- `next_chapter` (object, 可选): 下一章节信息对象
- `up_chapter` (object, 可选): 上一章节信息对象
- `color_line` (object, 可选): 高亮线条信息
- `direction` (string, 可选): 滚动方向,例如: "scrollUp"
**注意**: 此页面会将参数对象直接传递给 `getBookChapterData` 方法处理。
---
### 6. read_info.html (阅读信息页面)
**参数格式**: 对象形式
```javascript
callbackInFlutterComponent({
base_url: 'https://api.example.com',
book_id: '175',
chapter_id: '2834',
token: '726cca8efcb328672202656f63e176e8',
......@@ -148,7 +115,6 @@ callbackInFlutterComponent({
**参数说明**:
- `base_url` (string): API 基础地址
- `book_id` (string): 书籍 ID,例如: "175" 或 "358"
- `chapter_id` (string): 章节 ID,例如: "2253" 或 "2825"
- `token` (string): 用户认证令牌
......@@ -159,7 +125,6 @@ callbackInFlutterComponent({
```javascript
callbackInFlutterComponent({
base_url: 'https://api.example.com',
book_id: 175,
chapter_id: 2834,
token: '726cca8efcb328672202656f63e176e8',
......@@ -170,7 +135,7 @@ callbackInFlutterComponent({
---
### 7. read_img.html (阅读图片页面)
### 6. read_img.html (阅读图片页面)
**参数格式**: 对象形式
......
......@@ -40,12 +40,15 @@ pnpm preview
## 项目结构
- `*.html` - 多页面入口文件
- `src/*.html` - 多页面入口文件
- `src/reader/` - 阅读器运行时代码
- `assets/` - 静态资源目录
- `css/` - 样式文件
- `js/` - JavaScript 文件
- `images/` - 图片资源
- `fonts/` - 字体文件
- `public/` - 构建时原样复制的静态文件
- `assets/js/` - 传统 JavaScript 依赖
- `agreement/` - 协议页面
## 页面说明
......@@ -53,9 +56,14 @@ pnpm preview
- `evaluating_result.html` - 测评结果页面
- `evaluating_wrong.html` - 错题页面
- `read.html` - 阅读页面
- `read_unline.html` - 在线阅读页面
- `read_info.html` - 阅读信息页面
- `read_img.html` - 阅读图片页面
## 阅读器结构
`read.html` 是统一阅读器入口,在线章节和离线章节都通过
`callbackInFlutterComponent()` 进入同一套渲染逻辑。
阅读器运行时位于 `src/reader/`,构建后通过 `dist/.vite/manifest.json`
和带 hash 的资源文件支持版本化发布。传统依赖位于 `public/assets/js/`,
会原样复制到构建产物。
......@@ -11,7 +11,7 @@
```
Flutter 调用 callbackInFlutterComponent()
↓
解析参数 (baseURL, book_id, chapter_id, token, noteId, searchText)
解析参数 (book_id, chapter_id, token, noteId, searchText)
↓
存储 token 到 localStorage
↓
......@@ -72,7 +72,7 @@ progress = (已滚动高度 + 屏幕高度) / 总内容高度 * 100
#### `callbackInFlutterComponent(args)`
**参数格式**: 逗号分隔的字符串
```
baseURL,book_id,chapter_id,token,noteId,searchText
book_id,chapter_id,token,noteId,searchText
```
**功能**: 初始化页面,加载章节数据
......@@ -264,4 +264,3 @@ refreshTokenSuccess({ token: 'new_token' })
2. **Token 管理**: Token 会自动存储到 `localStorage`
3. **离线支持**: 笔记操作支持离线,通过回调同步到 Flutter
4. **触摸事件**: 移动端使用触摸事件,桌面端可能需要适配
......@@ -142,39 +142,9 @@ light.c-ff4500::selection {
width: 100vw;
overflow-x: hidden;
}
.chapter-container .load-chapter {
position: fixed;
width: 100vw;
height: 1.3333333333333333rem;
z-index: -1;
background-color: #f2f2f2;
opacity: 0;
}
.chapter-container .load-chapter.next-chapter {
bottom: 0;
left: 0;
}
.chapter-container .load-chapter.prev-chapter {
top: 0;
left: 0;
}
.chapter-container .load-chapter.side {
left: 100vw;
top: 0;
}
.chapter-container .load-chapter .chapter-c-title {
font-size: 0.37333333333333335rem;
padding: 0.13333333333333333rem 0.4rem;
box-sizing: border-box;
line-height: 1.0666666666666667rem;
}
.chapter-container .load-chapter .chapter-c-title strong {
font-weight: 600;
}
.chapter-container .chapter-content-list {
max-height: 100vh;
min-height: 100vh;
overflow: hidden;
overflow: visible;
position: relative;
}
.chapter-container .chapter-content-list .show-up {
......@@ -185,11 +155,7 @@ light.c-ff4500::selection {
top: -100%;
}
.chapter-container .chapter-content-list .show {
-webkit-transform: translateY(0);
transform: translateY(0);
position: relative;
transition-timing-function: cubic-bezier(0.1, 0.57, 0.1, 1);
transition-duration: 0ms;
min-height: 100vh;
}
.chapter-container .chapter-content-list .chapter-item,
......@@ -1230,3 +1196,16 @@ light.c-ff4500::selection {
left: 0;
width: 100vw;
}
.reader-loading {
position: fixed;
top: 12px;
left: 50%;
z-index: 20;
transform: translateX(-50%);
padding: 6px 12px;
border-radius: 14px;
background: rgba(0, 0, 0, 0.55);
color: #fff;
font-size: 12px;
pointer-events: none;
}
"use strict";
var sortObjASCII = function sortObjASCII(obj) {
var arr = Object.keys(obj).sort();
if (arr.length === 0) {
return {};
}
var sortObj = {};
for (var ii in arr) {
sortObj[arr[ii]] = obj[arr[ii]];
}
return sortObj;
};
function getToken() {
return window.localStorage.getItem('token') ? window.localStorage.getItem('token') : '';
}
var request = axios.create({
timeout: 120000
}); // 请求拦截器
request.interceptors.request.use(function (config) {
var token = getToken(); // 请求头带上token
if (token) {
config.headers.Authorization = token;
} else {
config.headers.Authorization = "075895df18c38c8950999cef1af1f0d6";
} // 用户编码
config.headers.UserNo = window.localStorage.getItem('user_no'); // 应用设置
var appId = 'dkFv6OwWyQhSeaZG';
var appSecret = 'd34cea92316d1da113e95158bed9ca97';
config.headers.AppId = appId;
config.headers.AppSecret = appSecret; // 签名
var signData = Qs.parse(config.data);
var timeStr = Date.parse(new Date()) / 1000;
signData.appId = appId;
signData.appSecret = appSecret;
signData.timestamp = timeStr;
signData.url = decodeURI(config.url);
signData.token = token || config.headers.Authorization; // 按照ASCII码进行排序
var dataSign = sortObjASCII(signData); // 转换为字符串
var signStr = '';
for (var key in dataSign) {
var str = '';
if (typeof signData[key] !== 'string' && signData[key].constructor !== String) {
str = JSON.stringify(signData[key]);
} else {
str = signData[key];
}
if (signStr === '') {
signStr = key + '=' + str;
} else {
signStr += '&' + key + '=' + str;
}
}
var salt = '&4F6g4Y6b5L4R9';
var sign = signStr + salt;
config.headers.Sign = md5(md5(sign)); // 加密盐
config.headers.Timestamp = timeStr; // 请求时间
return config;
}, function (error) {
return error;
return Promise.reject(error);
}); // 响应拦截器
request.interceptors.response.use(function (response) {
if (response.status === 200) {
if (response.data && response.data.code === 200) {
return response.data;
} else {
return response;
}
} else {
return response;
}
return false;
}, function (error) {
var str = "message: ".concat(error.message, " \n data: ").concat(error.config.data, " \n token: ").concat(error.config.headers.Authorization);
return error;
return Promise.reject(error);
});
\ No newline at end of file
import fs from 'node:fs'
import path from 'node:path'
import chalk from 'chalk'
import OSS from 'ali-oss'
const log = console.log
const __dirname = path.resolve()
const client = new OSS({
region: 'oss-cn-beijing',
accessKeyId: 'LTAI5t6yKc2m47ZUZTBbUBuP',
accessKeySecret: 'mhuPVC43NAkzBaheHyWGjREiIrLax0',
bucket: 'webapp-pub',
})
async function uploadTarget(src, dist) {
try {
const result = await client.put(dist, path.join(__dirname, src))
log(chalk.green('上传成功', result.url))
} catch (e) {
log(chalk.red('上传失败', src))
log(e)
}
}
function generateUploadTarget(src, dist) {
fs.readdir(path.join(__dirname, src), function (err, files) {
if (err) {
log(err)
return
}
files.forEach(function (file) {
const _src = src + '/' + file
const _dist = dist + '/' + file
const stats = fs.statSync(path.join(__dirname, _src))
// 判断是否为文件
stats.isFile() && uploadTarget(_src, _dist)
// 判断是否为文件夹
stats.isDirectory() && generateUploadTarget(_src, _dist)
})
})
}
generateUploadTarget('./dist', '/book-app')
......@@ -5,10 +5,14 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"deploy": "node deploy.js",
"release": "vite build && node deploy.js"
},
"devDependencies": {
"vite": "^7.3.0",
"glob": "^11.0.0"
"ali-oss": "^6.23.0",
"chalk": "^6.0.0",
"glob": "^11.0.0",
"vite": "^8.2.1"
}
}
差异被折叠。
window.BOOK_API_BASE_URL = 'https://project-api.ezijing.com/api/book';
......@@ -69,27 +69,25 @@ request.interceptors.request.use(
return config;
},
function (error) {
return error;
return Promise.reject(error)
}
);
// 响应拦截器
request.interceptors.response.use(
function (response) {
if (response.status === 200) {
if (response.data && response.data.code === 200) {
return response.data;
} else {
return response;
}
} else {
return response;
}
return false;
request.interceptors.response.use(
function (response) {
if (response.status === 200) {
if (response.data && response.data.code === 200) {
return response.data;
} else {
return response;
}
} else {
return response;
}
},
function (error) {
const str = `message: ${error.message} \n data: ${error.config.data} \n token: ${error.config.headers.Authorization}`;
return error;
return Promise.reject(error);
}
);
}
);
window.readerRequest = request;
差异被折叠。
......@@ -6,8 +6,9 @@
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
/>
<title>测评-紫荆教育云教材</title>
<link rel="stylesheet" href="assets/css/evaluating.css" />
<title>测评-紫荆教育云教材</title>
<script src="assets/js/config.js"></script>
<link rel="stylesheet" href="../assets/css/evaluating.css" />
<script src="assets/js/flexible.min.js"></script>
<script src="assets/js/jquery.2.2.4.min.js"></script>
<script src="assets/js/qs.6.11.2.min.js"></script>
......@@ -327,7 +328,7 @@
showHeight:
document.documentElement.clientHeight || document.body.clientHeight,
hideshow: true, //显示或者隐藏footer
baseURL: "",
baseURL: window.BOOK_API_BASE_URL,
book_id: "", // 175
chapter_id: "", // 335
token: "",
......@@ -569,13 +570,23 @@
position: this.position,
});
if (res.code === 200) {
this.callHandlerNoticeFlutterSubmit("dismissLoadingCallback");
if (res.data.is_success === 1) {
this.callHandlerNoticeFlutterSubmit(
"submitSuccessCallback"
);
}
if (res.code === 200) {
this.callHandlerNoticeFlutterSubmit("dismissLoadingCallback");
if (res.data.is_success === 1) {
const resultUrl = new URL(
"evaluating_result.html",
window.location.href
);
resultUrl.searchParams.set("book_id", this.book_id);
resultUrl.searchParams.set("chapter_id", this.chapter_id);
resultUrl.searchParams.set("position", this.position);
resultUrl.searchParams.set("source", "app");
resultUrl.searchParams.set("title", document.title);
window.flutter_inappwebview?.callHandler(
"openWebview",
resultUrl.toString()
);
}
} else {
this.callHandlerNoticeFlutterSubmit("dismissLoadingCallback");
}
......@@ -607,19 +618,28 @@
})();
};
},
// flutter 回调
async callbackInFlutterComponent(args) {
try {
const data = args.split(",");
this.baseURL = data[0];
this.book_id = data[1];
this.chapter_id = data[2];
this.token = data[3];
this.position = data[4];
localStorage.setItem("token", data[3]);
setTimeout(async () => {
await this.getEvaluatingData();
}, 200);
// flutter 回调
async callbackInFlutterComponent(args) {
try {
if (args && typeof args === "object") {
this.baseURL = window.BOOK_API_BASE_URL;
this.book_id = args.book_id || "";
this.chapter_id = args.chapter_id || "";
this.token = args.token || localStorage.getItem("token") || "";
this.position = args.position || "";
} else {
const data = args.split(",");
const offset = data.length >= 5 ? 1 : 0;
this.baseURL = window.BOOK_API_BASE_URL;
this.book_id = data[offset];
this.chapter_id = data[offset + 1];
this.token = data[offset + 2];
this.position = data[offset + 3];
}
if (this.token) localStorage.setItem("token", this.token);
setTimeout(async () => {
await this.getEvaluatingData();
}, 200);
} catch (e) {
console.log(e);
}
......@@ -731,10 +751,20 @@
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
};
const refreshTokenSuccess = (arguments) => {
return app.refreshTokenSuccess(arguments);
};
// callbackInFlutterComponent(',175,2834,726cca8efcb328672202656f63e176e8,bgi0jn6p');
const refreshTokenSuccess = (arguments) => {
return app.refreshTokenSuccess(arguments);
};
const urlParams = new URLSearchParams(window.location.search);
const pageTitle = urlParams.get('title');
if (pageTitle) document.title = pageTitle;
if (urlParams.get('book_id') && urlParams.get('chapter_id')) {
app.callbackInFlutterComponent({
book_id: urlParams.get('book_id'),
chapter_id: urlParams.get('chapter_id'),
position: urlParams.get('position') || '',
});
}
// callbackInFlutterComponent(',175,2834,726cca8efcb328672202656f63e176e8,bgi0jn6p');
</script>
</body>
</html>
......@@ -6,9 +6,10 @@
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
/>
<title>测评-紫荆教育云教材</title>
<link rel="stylesheet" href="assets/css/common.css" />
<link rel="stylesheet" href="assets/css/evaluating.css" />
<title>测评-紫荆教育云教材</title>
<script src="assets/js/config.js"></script>
<link rel="stylesheet" href="../assets/css/common.css" />
<link rel="stylesheet" href="../assets/css/evaluating.css" />
<script src="assets/js/flexible.min.js"></script>
<script src="assets/js/jquery.2.2.4.min.js"></script>
<script src="assets/js/qs.6.11.2.min.js"></script>
......@@ -341,7 +342,7 @@
showHeight:
document.documentElement.clientHeight || document.body.clientHeight,
hideshow: true, //显示或者隐藏footer
baseURL: "",
baseURL: window.BOOK_API_BASE_URL,
book_id: "", // 323
chapter_id: "", // 2635
token: "",
......@@ -514,19 +515,28 @@
this.$forceUpdate();
});
},
// flutter 回调
async callbackInFlutterComponent(args) {
try {
const data = args.split(",");
this.baseURL = data[0];
this.book_id = data[1];
this.chapter_id = data[2];
this.token = data[3];
this.position = data[4];
localStorage.setItem("token", data[3]);
setTimeout(async () => {
await this.getEvaluatingData();
}, 200);
// flutter 回调
async callbackInFlutterComponent(args) {
try {
if (args && typeof args === "object") {
this.baseURL = window.BOOK_API_BASE_URL;
this.book_id = args.book_id || "";
this.chapter_id = args.chapter_id || "";
this.token = args.token || localStorage.getItem("token") || "";
this.position = args.position || "";
} else {
const data = args.split(",");
const offset = data.length >= 5 ? 1 : 0;
this.baseURL = window.BOOK_API_BASE_URL;
this.book_id = data[offset];
this.chapter_id = data[offset + 1];
this.token = data[offset + 2];
this.position = data[offset + 3];
}
if (this.token) localStorage.setItem("token", this.token);
setTimeout(async () => {
await this.getEvaluatingData();
}, 200);
} catch (e) {
console.log(e);
}
......@@ -635,10 +645,20 @@
},
});
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
};
// callbackInFlutterComponent(',175,2834,726cca8efcb328672202656f63e176e8,bgi0jn6p');
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
};
const urlParams = new URLSearchParams(window.location.search);
const pageTitle = urlParams.get('title');
if (pageTitle) document.title = pageTitle;
if (urlParams.get('book_id') && urlParams.get('chapter_id')) {
app.callbackInFlutterComponent({
book_id: urlParams.get('book_id'),
chapter_id: urlParams.get('chapter_id'),
position: urlParams.get('position') || '',
});
}
// callbackInFlutterComponent(',175,2834,726cca8efcb328672202656f63e176e8,bgi0jn6p');
</script>
</body>
</html>
......@@ -6,9 +6,10 @@
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
/>
<title>测评-紫荆教育云教材</title>
<link rel="stylesheet" href="assets/css/common.css" />
<link rel="stylesheet" href="assets/css/evaluating.css" />
<title>错题详情</title>
<script src="assets/js/config.js"></script>
<link rel="stylesheet" href="../assets/css/common.css" />
<link rel="stylesheet" href="../assets/css/evaluating.css" />
<script src="assets/js/flexible.min.js"></script>
<script src="assets/js/jquery.2.2.4.min.js"></script>
<script src="assets/js/qs.6.11.2.min.js"></script>
......@@ -29,7 +30,7 @@
:src="book_info.img"
alt="book logo"
/>
<img v-else src="./assets/images/wrong.png" alt="book logo" />
<img v-else src="../assets/images/wrong.png" alt="book logo" />
</div>
</div>
<div class="book-title-info">
......@@ -192,7 +193,7 @@
showHeight:
document.documentElement.clientHeight || document.body.clientHeight,
hideshow: true, //显示或者隐藏footer
baseURL: "",
baseURL: window.BOOK_API_BASE_URL,
book_id: '',
token: "",
book_info: {},
......@@ -312,16 +313,20 @@
this.$forceUpdate();
});
},
// flutter 回调
async callbackInFlutterComponent(args) {
try {
const data = args.split(',');
this.baseURL = data[0]
this.book_id = data[1];
this.token = data[2];
localStorage.setItem("token", data[2]);
setTimeout(async () => {
await this.getEvaluatingData();
// flutter 回调
async callbackInFlutterComponent(args) {
try {
const data = args && typeof args === "object" ? args : null;
const values = data ? [] : args.split(',');
const offset = values.length >= 3 ? 1 : 0;
this.baseURL = window.BOOK_API_BASE_URL;
this.book_id = data ? data.book_id : values[offset];
this.token = data
? data.token || localStorage.getItem("token") || ""
: values[offset + 1];
if (this.token) localStorage.setItem("token", this.token);
setTimeout(async () => {
await this.getEvaluatingData();
}, 200)
} catch (e) {
console.log(e);
......@@ -361,9 +366,17 @@
},
});
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
}
</script>
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
}
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('book_id')) {
app.callbackInFlutterComponent({
book_id: urlParams.get('book_id'),
token: localStorage.getItem('token') || '',
});
}
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
/>
<title>阅读详情-紫荆教育云教材</title>
<script src="assets/js/config.js"></script>
<link rel="stylesheet" href="../assets/css/common.css" />
<link rel="stylesheet" href="../assets/css/read.css" />
<link rel="stylesheet" href="../assets/css/github.css" />
<script src="assets/js/flexible.min.js"></script>
<script src="assets/js/highlight.min.js"></script>
<script src="assets/js/vue@2.js"></script>
<script src="assets/js/jquery.2.2.4.min.js"></script>
<script src="assets/js/qs.6.11.2.min.js"></script>
<script src="assets/js/blueimp-md5.2.19.0.min.js"></script>
<script src="assets/js/axios.min.js"></script>
<script src="assets/js/request.js"></script>
<script src="assets/js/iconfont.js"></script>
<!-- <script src="assets/js/vconsole.min.js"></script> -->
</head>
<body class="body_evaluating">
<div id="app" v-cloak>
<div class="reader-loading" v-show="nextChapterLoading">加载中...</div>
<div class="chapter-container" ref="chapterContainer">
<div class="chapter-content-list" id="chapter-content-list">
<div class="show-up">
<div
class="chapter-item-up"
:class="[`chapter-item-up-${prev_chapter.id}`]"
id="chapter-item-up"
:data-id="prev_chapter.id"
v-html="prev_chapter.contents"
></div>
</div>
<div class="show">
<div
class="chapter-item"
:class="[`chapter-item-${chapter_id}`]"
id="chapter-item"
:data-id="chapter_id"
v-html="chapterContent"
v-show="!lockedContent"
></div>
<div class="lock-content" v-show="lockedContent">
<div class="lock-img">
<div class="img"></div>
</div>
<div class="lock-desc">未解锁章节</div>
<div class="lock-button">
<span>获取完整版</span>
</div>
</div>
</div>
</div>
<div class="range-controll">
<div
class="controll-container"
v-show="boolControllStatus"
:class="[nowColorId > 0 ? 'colorLine' : '']"
>
<!-- <div class="controll-container" :class="[nowColorId > 0 ? 'colorLine' : '']"> -->
<div class="controll-position">
<div
v-show="rangeMenuStatus"
class="com prev"
@click.stop.prevent="rangeMenuMove('prev', $event)"
><i class="ico"></i></div>
<div class="controll-list-container">
<div class="controll-list">
<div class="controll-item colorLine" v-if="nowColorId > 0">
<span
v-for="(item, index) in colorList"
:class="[`c-${item}`]"
:key="index"
@click.stop.prevent="rangeTextChangeLight"
></span>
</div>
<div
class="controll-item"
@click.stop.prevent="rangeTextInFlutter('discuss', $event)"
>
<div class="opa discuss"><i class="ico"></i></div>
<p>讨论</p>
</div>
<div
class="controll-item"
@click.stop.prevent="rangeTextInFlutter('note', $event)"
>
<div class="opa note"><i class="ico"></i></div>
<p>笔记</p>
</div>
<div
class="controll-item"
@click.stop.prevent="rangeTextInLine($event)"
>
<div class="opa line"><i class="ico"></i></div>
<p v-if="nowLineId > 0">移除划线</p>
<p v-else-if="expandLine.length > 0">扩展划线</p>
<p class="cancel" v-else>划线</p>
</div>
<div
class="controll-item light-color"
@click.stop.prevent="rangeTextInLight($event)"
>
<div class="opa light"><i class="ico"></i></div>
<p v-if="nowColorId > 0">移除高亮</p>
<p v-else-if="expandLight.length > 0">扩展高亮</p>
<p class="cancel" v-else>高亮</p>
</div>
<div
class="controll-item"
@click.stop.prevent="rangeTextInFlutter('baike', $event)"
>
<div class="opa baike"><i class="ico"></i></div>
<p>百科</p>
</div>
<div
class="controll-item"
@click.stop.prevent="rangeTextInFlutter('dict', $event)"
>
<div class="opa dict"><i class="ico"></i></div>
<p>字典</p>
</div>
</div>
</div>
<div
v-show="!rangeMenuStatus"
class="com next"
@click.stop.prevent="rangeMenuMove('next', $event)"
><i class="ico"></i></div>
</div>
<div class="controll-triangle top" v-show="rangeSquareType === 'top'"></div>
<div class="controll-triangle bottom" v-show="rangeSquareType === 'bottom'"></div>
</div>
<!-- light -->
<div class="controll-light-container" v-show="boolLightColor">
<div class="controll-color-list">
<div
class="controll-color-item"
:class="[`c-${item}`, nowActiveColor === item ? 'active' : '']"
v-for="(item, index) in colorList"
:key="index"
@click="setLightColorText(item)"
>
<div class="color"></div>
</div>
</div>
</div>
</div>
<div class="tange-tooptip" v-show="tooltipId">
<div class="square" :style=""></div>
<div class="tooltip-content-container">
<div class="tooltip-content">
<div class="content">
<p>{{tooltipContent.content}}</p>
</div>
<div class="content-opa" v-if="tooltipContent.link">
<a
:href="tooltipContent.link"
target="_blank"
rel="noreferrer"
class="c-link"
>
<svg class="svg-icon" style="color: #ab1941; fill: currentColor; overflow: hidden; width: 20px; height: 20px;" aria-hidden="true"><use xlink:href="#icon-a-link"></use></svg>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<script type="module" src="reader/read-app.js"></script>
</body>
</html>
......@@ -3,9 +3,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>阅读详情-紫荆教育云教材</title>
<link rel="stylesheet" href="assets/css/common.css" />
<link rel="stylesheet" href="assets/css/read.css" />
<title>阅读详情-紫荆教育云教材</title>
<script src="assets/js/config.js"></script>
<link rel="stylesheet" href="../assets/css/common.css" />
<link rel="stylesheet" href="../assets/css/read.css" />
<script src="assets/js/flexible.min.js"></script>
<script src="assets/js/vue@2.js"></script>
<script src="assets/js/jquery.2.2.4.min.js"></script>
......@@ -64,10 +65,18 @@
},
});
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
};
// callbackInFlutterComponent();
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
};
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.has('title')) document.title = urlParams.get('title') || '';
if (urlParams.get('url')) {
app.callbackInFlutterComponent({
url: urlParams.get('url'),
title: urlParams.get('title') || '',
});
}
// callbackInFlutterComponent();
</script>
</body>
</html>
......@@ -6,10 +6,11 @@
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=0"
/>
<title>阅读详情-紫荆教育云教材</title>
<link rel="stylesheet" href="assets/css/common.css" />
<link rel="stylesheet" href="assets/css/read.css" />
<link rel="stylesheet" href="assets/css/github.css" />
<title>阅读详情-紫荆教育云教材</title>
<script src="assets/js/config.js"></script>
<link rel="stylesheet" href="../assets/css/common.css" />
<link rel="stylesheet" href="../assets/css/read.css" />
<link rel="stylesheet" href="../assets/css/github.css" />
<script src="assets/js/flexible.min.js"></script>
<script src="assets/js/highlight.min.js"></script>
<script src="assets/js/vue@2.js"></script>
......@@ -45,14 +46,26 @@
</div>
</div>
<script>
<script>
function openWebview(page, params = {}) {
const url = new URL(page, window.location.href);
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.set(key, value);
}
});
if (window.flutter_inappwebview && window.flutter_inappwebview.callHandler) {
return window.flutter_inappwebview.callHandler("openWebview", url.toString());
}
}
let getChapterGalleryURL = "/v1/book/Information/getGalleryByPosition"; // 文章内容中画廊内容
let getChapterExpandContentURL = "/v1/book/Information/getExtendByPosition"; // 获取扩展的内容
var app = new Vue({
el: "#app",
data: {
baseURL: "",
baseURL: window.BOOK_API_BASE_URL,
book_id: "", // 175 358
chapter_id: "", // 2253 2825
token: "", // token
......@@ -166,12 +179,12 @@
if (target.nodeName.toLowerCase() === "img") {
if ($(target).closest(".show-expand").length > 0) {
const scaleImg = $(target).attr("src");
this.callHandlerNoticeFlutterSubmit("scaleImageCallback", [scaleImg, ""]);
openWebview("read_img.html", { url: scaleImg });
ev.stopPropagation();
ev.preventDefault();
} else if ($(target).closest(".gallery-pic").length > 0) {
const scaleImg = $(target).attr("src");
this.callHandlerNoticeFlutterSubmit("scaleImageCallback", [scaleImg, ""]);
openWebview("read_img.html", { url: scaleImg });
ev.stopPropagation();
ev.preventDefault();
}
......@@ -208,13 +221,15 @@
}
},
// flutter 回调
async callbackInFlutterComponent(args) {
this.chapter_id = args.chapter_id;
this.book_id = args.book_id;
localStorage.setItem("token", args.token);
this.baseURL = args.base_url;
this.type = args.type;
this.position = args.position;
async callbackInFlutterComponent(args) {
this.chapter_id = args.chapter_id;
this.book_id = args.book_id;
if (args.token) {
localStorage.setItem("token", args.token);
}
this.baseURL = window.BOOK_API_BASE_URL;
this.type = args.type;
this.position = args.position;
this.getData();
},
// 通知flutter组件
......@@ -228,10 +243,21 @@
},
});
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
};
// callbackInFlutterComponent({ base_url: '', book_id: 175, chapter_id: 2834, token: '726cca8efcb328672202656f63e176e8', position: '49732578', type: 'gallery'});
const callbackInFlutterComponent = (arguments) => {
return app.callbackInFlutterComponent(arguments);
};
const urlParams = new URLSearchParams(window.location.search);
const pageTitle = urlParams.get('title');
if (pageTitle) document.title = pageTitle;
if (urlParams.get('book_id') && urlParams.get('chapter_id')) {
app.callbackInFlutterComponent({
book_id: urlParams.get('book_id'),
chapter_id: urlParams.get('chapter_id'),
position: urlParams.get('position') || '',
type: urlParams.get('type') || '',
});
}
// callbackInFlutterComponent({ book_id: 175, chapter_id: 2834, token: 'token', position: '49732578', type: 'gallery'});
</script>
</body>
</html>
# Reader runtime
`read.html` is the single reader entry for online and offline chapter data.
- `reader-config.js`: reader API endpoint definitions.
- `reader-api.js`: API adapter shared by the reader runtime.
- `reader-bridge.js`: Flutter bridge adapter; browser usage is a no-op.
- `reader-content.js`: offline payload normalization and mode detection.
- `read-app.js`: Vue reader runtime and interaction behavior.
The App can continue calling `callbackInFlutterComponent()` with the existing
comma-separated online payload. Offline mode passes the chapter payload object
to the same callback. The reader then renders both modes through
`renderChapterData()`.
For direct browser access, pass `book_id`, `chapter_id`, and optionally
`token`, `note_id`, and `search_text` in the URL query string. The API base
URL is defined by the H5 runtime configuration.
The page then requests the chapter content itself.
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0% 或
您添加了 0 人 到此讨论。请谨慎行事。
请先完成此评论的编辑!
请 注册 或者 后发表评论