可信源码风险提示:保存或下载前请阅读源码和权限声明。
const API = 'https://zh.minecraft.wiki/api.php';
const PAGE_CHARS = 800; // 每页800字,翻页看完全文
function req(url, timeout = 8000) {
return new Promise((resolve, reject) => {
const t = setTimeout(() => reject(new Error('超时')), timeout);
system.http.request({
url,
method: 'GET',
responseType: 'text',
header: {
'User-Agent': 'JSLab-WikiFull/1.0',
'Api-User-Agent': 'JSLab-WikiFull/1.0',
'Accept': 'application/json'
},
success(res) {
clearTimeout(t);
if (res.code === 204) return reject(new Error('空响应204'));
if (res.code >= 200 && res.code < 300) {
try { resolve(JSON.parse(res.data)); }
catch (e) { reject(new Error('JSON解析失败')); }
} else reject(new Error('HTTP ' + res.code));
},
fail(d, c) { clearTimeout(t); reject(new Error('fail:' + c)); }
});
});
}
// 轻度清洗:保留全文,只去最碍眼的东西
function cleanWiki(wt) {
return wt
.replace(/<nowiki>([\s\S]*?)<\/nowiki>/g, '$1')
.replace(/<!--[\s\S]*?-->/g, '')
.replace(/<ref>[\s\S]*?<\/ref>/g, '') // 脚注不显示
.replace(/<[^>]+>/g, '')
.replace(/\[\[(?:[^\]|]*\|)?([^\]]+)\]\]/g, '$1') // 链接保留文字
.replace(/'{2,}/g, '')
.replace(/[ \t]+\n/g, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function paginate(text, size) {
const pages = [];
for (let i = 0; i < text.length; i += size) {
pages.push(text.slice(i, i + size));
}
return pages.length ? pages : [''];
}
async function main() {
const view = ui.signal('search');
const results = ui.signal([]);
const loading = ui.signal(false);
const error = ui.signal('');
const title = ui.signal('');
const pages = ui.signal([]);
const pageIdx = ui.signal(0);
const fullLen = ui.signal(0);
async function doSearch() {
const kw = await dialog.text({ title: '搜 Wiki', placeholder: '石头 / 红石 / 活塞' });
if (kw.action !== 'confirm') return;
const k = kw.value.trim();
if (!k) return;
loading.set(true); error.set(''); results.set([]); ui.refresh();
try {
const u = API + '?action=query&list=search&srsearch='
+ encodeURIComponent(k) + '&srlimit=5&format=json';
const d = await req(u);
const list = (d.query?.search || []).map(x => ({
title: x.title,
snippet: (x.snippet || '').replace(/<[^>]+>/g, '')
}));
results.set(list);
if (!list.length) error.set('没搜到,换关键词');
} catch (e) {
error.set('搜索失败: ' + e.message);
}
loading.set(false); ui.refresh();
}
async function openPage(t) {
title.set(t);
view.set('reading');
pages.set(['加载全文中...']);
pageIdx.set(0);
ui.refresh();
try {
// 关键:拿完整 wikitext,不传 section,不剪枝
const u = API + '?action=parse&page=' + encodeURIComponent(t)
+ '&prop=wikitext&format=json';
const d = await req(u);
const raw = d.parse?.wikitext?.['*'] || '';
const cleaned = cleanWiki(raw);
fullLen.set(cleaned.length);
// 一字不落:全部切页
pages.set(paginate(cleaned, PAGE_CHARS));
pageIdx.set(0);
} catch (e) {
pages.set(['获取失败: ' + e.message]);
fullLen.set(0);
}
ui.refresh();
}
function render() {
if (view.get() === 'search') {
const nodes = [
ui.heading('MC Wiki 全文', { size: 20 }),
ui.button(loading.get() ? '搜索中...' : '搜索', doSearch, { tone: 'primary' }),
ui.spacer(6)
];
if (error.get()) nodes.push(ui.text(error.get(), { size: 14, color: '#f66' }));
results.get().forEach(it => {
nodes.push(ui.button(it.title, () => openPage(it.title), { tone: 'neutral' }));
});
return ui.column(nodes, { gap: 4 });
}
const ps = pages.get();
const idx = pageIdx.get();
const total = ps.length;
return ui.column([
ui.button('← 返回', () => { view.set('search'); ui.refresh(); }, { tone: 'neutral' }),
ui.spacer(4),
ui.heading(title.get(), { size: 16, lines: 1 }),
ui.text('全文共 ' + fullLen.get() + ' 字,当前第 ' + (idx + 1) + '/' + total + ' 页',
{ size: 12, color: '#888' }),
ui.spacer(4),
ui.text(ps[idx] || '', { size: 14, lineHeight: 20, lines: 16 }),
ui.spacer(6),
ui.buttonRow([
{ text: '上一页', onPress: () => { if (idx > 0) { pageIdx.set(idx - 1); ui.refresh(); } },
tone: 'neutral', disabled: idx <= 0 },
{ text: '下一页', onPress: () => { if (idx < total - 1) { pageIdx.set(idx + 1); ui.refresh(); } },
tone: 'neutral', disabled: idx >= total - 1 }
])
], { gap: 4 });
}
ui.render(render);
}
return main();