first commit

This commit is contained in:
shuiyu1123 committed 2026-04-29 18:29:59 +08:00
commit 6bc4ef1c41
84 files changed
+13968

No files matched your search

+11
View File
@@ -0,0 +1,11 @@
<script>
export default {
onCreate() {
console.log('[搜哩浏览器] 应用启动')
},
onDestroy() {
console.log('[搜哩浏览器] 应用销毁')
}
}
</script>
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

+217
View File
@@ -0,0 +1,217 @@
/**
* 工具函数库 - 搜哩浏览器
* 提供HTML解析、URL处理、数据存储等核心功能
*/
// ==================== 常量定义 ====================
const HTML_ENTITY_MAP = {
'&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"', '&#39;': "'",
'&nbsp;': ' ', '&#x2F;': '/', '&#x60;': '`', '&#x3D;': '=',
'&copy;': '©', '&reg;': '®', '&trade;': '™', '&#34;': '"'
};
const TAGS_TO_REMOVE = [
'script', 'style', 'noscript', 'iframe', 'canvas', 'svg',
'video', 'audio', 'embed', 'object', 'applet', 'meta', 'link'
];
// ==================== HTML处理 ====================
export function decodeHtmlEntities(text) {
if (!text || typeof text !== 'string') return '';
return text.replace(/&[a-zA-Z0-9#]+;/g, entity => HTML_ENTITY_MAP[entity] || entity);
}
export function cleanHtmlText(text) {
if (!text || typeof text !== 'string') return '';
return text
.replace(/<[^>]+>/g, '')
.replace(/&[a-zA-Z0-9#]+;/g, entity => HTML_ENTITY_MAP[entity] || entity)
.replace(/\s+/g, ' ')
.trim();
}
export function preprocessHtml(html) {
if (!html) return '';
let content = html;
// 移除危险标签
TAGS_TO_REMOVE.forEach(tag => {
const regex = new RegExp(`<${tag}[^>]*>[\\s\\S]*?<\\/${tag}>|<${tag}[^>]*\\/?>`, 'gi');
content = content.replace(regex, ' ');
});
// 移除注释
content = content.replace(/<!--[\s\S]*?-->/g, ' ');
// 提取body内容
const bodyMatch = content.match(/<body[^>]*>([\s\S]*)<\/body>/i);
return bodyMatch?.[1] || content;
}
export function extractTitle(html) {
if (!html) return '';
const match = html.match(/<title[^>]*>([^<]*)<\/title>/i);
return match?.[1] ? decodeHtmlEntities(match[1].trim()) : '';
}
// ==================== 内联元素解析 ====================
export function parseInlineElements(text, baseUrl) {
if (!text) return [];
const parts = [];
let remaining = text;
// 解析链接
remaining = remaining.replace(/<a[^>]*href=["']([^"']+)["'][^>]*>([\s\S]*?)<\/a>/gi,
(match, url, linkText) => {
const t = cleanHtmlText(linkText);
if (t) parts.push({ type: 'a', text: t, url: resolveUrl(url, baseUrl) });
return '';
});
// 解析加粗
remaining = remaining.replace(/<(strong|b)[^>]*>([\s\S]*?)<\/\1>/gi,
(match, tag, txt) => {
const t = cleanHtmlText(txt);
if (t) parts.push({ type: 'strong', text: t });
return '';
});
// 解析斜体
remaining = remaining.replace(/<(em|i)[^>]*>([\s\S]*?)<\/\1>/gi,
(match, tag, txt) => {
const t = cleanHtmlText(txt);
if (t) parts.push({ type: 'em', text: t });
return '';
});
// 剩余文本放前面
const plainText = cleanHtmlText(remaining);
if (plainText) parts.unshift({ type: 'text', text: plainText });
return parts;
}
// ==================== URL处理 ====================
export function getBaseUrl(url) {
if (!url) return '';
try {
const match = url.match(/^(https?:\/\/[^\/]+)/i);
return match ? match[1] : url;
} catch (e) {
return url;
}
}
export function resolveUrl(url, baseUrl) {
if (!url) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
if (url.startsWith('//')) return 'https:' + url;
if (url.startsWith('/')) return baseUrl + url;
return baseUrl + '/' + url;
}
export function normalizeUrl(url) {
if (!url) return '';
url = url.trim().toLowerCase();
return (url.startsWith('http://') || url.startsWith('https://'))
? url
: 'https://' + url;
}
export function formatDisplayUrl(url) {
if (!url) return '';
return url.replace(/^https?:\/\//, '').replace(/\/$/, '').split('/')[0];
}
export function isValidUrl(url) {
return url && /^(https?:\/\/)?([\w-]+\.)+[\w-]+/.test(url);
}
// ==================== 数据存储 ====================
export function getStorageData(storage, key) {
return new Promise((resolve) => {
storage.get({
key,
success: (data) => {
try { resolve(JSON.parse(data || '[]')); }
catch (e) { resolve([]); }
},
fail: () => resolve([])
});
});
}
export function setStorageData(storage, key, value) {
return new Promise((resolve) => {
storage.set({
key,
value: JSON.stringify(value),
success: () => resolve(true),
fail: () => resolve(false)
});
});
}
// ==================== 工具函数 ====================
export function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
export function throttle(fn, interval = 300) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
export function truncateText(text, maxLength = 50) {
if (!text || text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
}
export function generateId() {
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
}
export function showToast(page, msg, duration = 2000) {
page.toastMsg = msg;
page.showToast = true;
setTimeout(() => page.showToast = false, duration);
}
// ==================== 导出 ====================
export default {
decodeHtmlEntities,
cleanHtmlText,
preprocessHtml,
extractTitle,
parseInlineElements,
getBaseUrl,
resolveUrl,
normalizeUrl,
formatDisplayUrl,
isValidUrl,
getStorageData,
setStorageData,
debounce,
throttle,
truncateText,
generateId,
showToast
};
+782
View File
@@ -0,0 +1,782 @@
<template>
<div class="page" style="flex-direction: column;" show="{{!hide}}">
<div style="background-color:black" >
<!-- 圆�62 -->
<div if="{{screentype==='circle'}}" style="width: 480px;height: 321px;">
<!-- 全键�-->
<div if="{{keyboardtype!='T9'}}" style="width: 480px;height: 321px;">
<img src="/components/InputMethod/assets/full/back2.png" style="position: absolute;top:38px;left:7px;width:466px;height:52px;" @click="onBtnClick('switchCn')" show="{{numFlag}}" />
<img src="/components/InputMethod/assets/full/123.png" style="position: absolute;top:266px;left:119px;width:120px;height:48px;" @click="onBtnClick('switchNum')" show="{{downFlag==='' && !numFlag && lang==='cn'}}" />
<img src="/components/InputMethod/assets/full/bigA.png" style="position: absolute;top:266px;left:119px;width:120px;height:48px;" @click="onBtnClick('switchLow')" show="{{downFlag==='' && upperFlag && lang==='en'}}" />
<img src="/components/InputMethod/assets/full/A.png" style="position: absolute;top:266px;left:119px;width:120px;height:48px;" @click="onBtnClick('switchUpper')" show="{{downFlag==='' && !upperFlag && lang==='en'}}" />
<div style="position: absolute;top:38px;left:78px;width:324px;height:52px;background-color:rgb(38,38,38);border-radius: 12px;border: 3px solid #333333" show="{{downFlag===''&& !numFlag}}"></div>
<img show="{{resultList.length > 0}}" style="position: absolute;top:43px;left:355px;" src="/components/InputMethod/assets/full/down.png" @click="onBtnClick('down')" />
<!-- 带��的相对路径�aiot-tookit 2.0.4 中修�-->
<img if="{{lang==='cn'}}" src="/components/InputMethod/assets/full/cn.png" style="position: absolute;top:38px;left:7px;width:67px;height:52px;" @click="onBtnClick('lang')" show="{{downFlag==='' && !numFlag}}" />
<img if="{{lang==='en'}}" src="/components/InputMethod/assets/full/en.png" style="position: absolute;top:38px;left:7px;width:67px;height:52px;" @click="onBtnClick('lang')" show="{{downFlag==='' && !numFlag}}" />
<div style="position: absolute;top:-4px;left:78px;width:324px;" show="{{downFlag==='' && !numFlag && cval}}">
<text class="caltext" style="width:296px;"> {{cval}}_ </text>
</div>
<div style="position: absolute;top:38px;left:80px;width:277px" show="{{lang === 'cn'&& !numFlag}}">
<div class="item column center" for="{{cvalList}}">
<input show='{{resultList.length > $idx}}' class="calbtn0" type="button" value="{{resultList[$idx]}}" @click="onRsSelect(resultList[$idx])" />
</div>
</div>
<div style="position: absolute;top:38px;left:80px;width:320px;height:52px;align-content: center;align-items: center;justify-content: center" show="{{downFlag==='' && !numFlag && lang==='en'}}" @click="onBtnClick('switchNum')" >
<img src="/components/InputMethod/assets/full/123_boardless.png" />
</div>
<!-- 这里使用show会导致�次输入都会加载全部候选列表,很� -->
<list class="list3" if="{{downFlag==='down'}}">
<list-item type="waitingRows62t9" class="item3" for="{{itemArray in resultList2}}">
<div class="item column center" style="height:52px;" for="{{item in itemArray}}">
<input class="calbtn0" type="button" value="{{item}}" @click="onRsSelect(item)" />
</div>
</list-item>
</list>
<div style="position: absolute;top:95px;left:8px;width:464px;height:52px;" show="{{downFlag===''&&!numFlag}}">
<img src="/components/InputMethod/assets/full/Q.png" style="width:54px;height:52px;margin-right: 4px;" @click="onSelect('Q')" />
<text class="calbtnfull" for="{{item in keys['full62'][0]}}" @click="onSelect(item)">{{item}}</text>
<img src="/components/InputMethod/assets/full/P.png" style="width:54px;height:52px;" @click="onSelect('P')" />
</div>
<div style="position: absolute;top:152px;left:23px;width:438px;height:52px;" show="{{downFlag===''&&!numFlag}}">
<img src="/components/InputMethod/assets/full/btA.png" style="width:60px;height:52px;margin-right: 4px;" @click="onSelect('A')" />
<text class="calbtnfull" for="{{item in keys['full62'][1]}}" @click="onSelect(item)">{{item}}</text>
<img src="/components/InputMethod/assets/full/L.png" style="width:60px;height:52px;" @click="onSelect('L')" />
</div>
<div style="position: absolute;top:209px;left:56px;width:368px;height:52px;" show="{{downFlag===''&&!numFlag}}">
<img src="/components/InputMethod/assets/full/Z.png" style="width:72px;height:52px;margin-right: 4px;" @click="onSelect('Z')" />
<text class="calbtnfull" for="{{item in keys['full62'][2]}}" @click="onSelect(item)">{{item}}</text>
<img src="/components/InputMethod/assets/full/M.png" style="width:72px;height:52px;" @click="onSelect('M')" />
</div>
<div style="position: absolute;top:95px;left:8px;width:464px;height:52px;" show="{{numFlag}}">
<img src="/components/InputMethod/assets/full/1.png" style="width:54px;height:52px;margin-right: 4px;" @click="onSelect('1')" />
<text class="calbtnfull" for="{{item in keys['sign62'][0]}}" @click="onSelect(item)">{{item}}</text>
<img src="/components/InputMethod/assets/full/0.png" style="width:54px;height:52px;" @click="onSelect('0')" />
</div>
<div style="position: absolute;top:152px;left:23px;width:438px;height:52px;" show="{{numFlag}}">
<img src="/components/InputMethod/assets/full/2-1.png" style="width:60px;height:52px;margin-right: 4px;" @click="onSelect('~')" />
<text class="calbtnfull" for="{{item in keys['sign62'][1]}}" @click="onSelect(item)">{{item}}</text>
<img src="/components/InputMethod/assets/full/2-2.png" style="width:60px;height:52px;" @click="onSelect('?')" />
</div>
<div style="position: absolute;top:209px;left:56px;width:368px;height:52px;" show="{{numFlag}}">
<img src="/components/InputMethod/assets/full/3-1.png" style="width:72px;height:52px;margin-right: 4px;" @click="onSelect('(')" />
<text class="calbtnfull" for="{{item in keys['sign62'][2]}}" @click="onSelect(item)">{{item}}</text>
<img src="/components/InputMethod/assets/full/3-2.png" style="width:72px;height:52px;" @click="onSelect(')')" />
</div>
<img src="/components/InputMethod/assets/full/del.png" style="position: absolute;top:38px;left:406px;width:67px;height:52px;" @click="onBtnClick('D')" show="{{downFlag==='' && !numFlag }}" />
<img src="/components/InputMethod/assets/full/space.png" style="position: absolute;top:266px;left:242px;width:120px;height:48px;" @click="onBtnClick('space')" show="{{downFlag==='' && !numFlag }}" />
<img src="/components/InputMethod/assets/full/4-2.png" style="position: absolute;top:266px;left:242px;width:120px;height:48px;" @click="onSelect('&apos;')" show="{{numFlag }}" />
<img src="/components/InputMethod/assets/full/4-1.png" style="position: absolute;top:266px;left:119px;width:120px;height:48px;" @click="onSelect(',')" show="{{numFlag }}" />
<img style="position: absolute;top:204px;left:78px;" src="/components/InputMethod/assets/full/up.png" @click="onBtnClick('down')" show="{{downFlag==='down'}}" />
</div>
<!-- �键 -->
<div else style="width: 480px;height: 321px;">
<img src="/components/InputMethod/assets/t9/back2.png" style="position: absolute;top:35px;left:31px;width: 60px;height: 60px;" @click="onBtnClick('switchCn')" show="{{numFlag}}" />
<img src="/components/InputMethod/assets/t9/123.png" style="position: absolute;top:99px;left:31px;width: 60px;height: 60px;" @click="onBtnClick('switchNum')" show="{{downFlag==='' && !numFlag && lang==='cn'}}" />
<img src="/components/InputMethod/assets/t9/bigA.png" style="position: absolute;top:99px;left:31px;width: 60px;height: 60px;" @click="onBtnClick('switchLow')" show="{{downFlag==='' && !numFlag && upperFlag && lang==='en'}}" />
<img src="/components/InputMethod/assets/t9/a.png" style="position: absolute;top:99px;left:31px;width: 60px;height: 60px;" @click="onBtnClick('switchUpper')" show="{{downFlag==='' && !numFlag && !upperFlag && lang==='en'}}" />
<div style="position: absolute;top:35px;left:95px;width:290px;height:60px;background-color:rgb(38,38,38);border-radius: 999px;border: 3px solid #333333" show="{{downFlag===''&& !numFlag}}"></div>
<img show="{{resultList.length > 0}}" style="position: absolute;top:44px;left:338px;" src="/components/InputMethod/assets/full/down.png" @click="onBtnClick('down')" />
<!-- 带��的相对路径�aiot-tookit 2.0.4 中修�-->
<img if="{{lang==='cn'}}" src="/components/InputMethod/assets/t9/cn.png" style="position: absolute;top:35px;left:31px;width: 60px;height: 60px;" @click="onBtnClick('lang')" show="{{downFlag==='' && !numFlag}}" />
<img if="{{lang==='en'}}" src="/components/InputMethod/assets/t9/en.png" style="position: absolute;top:35px;left:31px;width: 60px;height: 60px;" @click="onBtnClick('lang')" show="{{downFlag==='' && !numFlag}}" />
<div style="position: absolute;top:-4px;left:95px;width:145px;height:40px;" show="{{downFlag==='' && !numFlag && cval}}">
<text class="caltext" style="width:145px;"> {{cval}}_ </text>
</div>
<div show="{{downFlag==='' && !numFlag}}" style="position: absolute;top:-4px;left:240px;width:145px;height:40px;justify-content:flex-end">
<text for="{{waitingList}}" class="waiting-keys" style="color:{{$idx===waitingIndex ? 'rgb(13,132,255)' : 'white'}};" @click="onSelectWaiting($idx)">{{waitingList[$idx]}}</text>
</div>
<div style="position: absolute;top:39px;left:105px;width:233px" show="{{lang === 'cn'&& !numFlag}}">
<div class="item column center" for="{{cvalList}}">
<input show='{{resultList.length > $idx}}' class="calbtn0" type="button" value="{{resultList[$idx]}}" @click="onRsSelect(resultList[$idx])" />
</div>
</div>
<div style="position: absolute;top:35px;left:95px;width:290px;height:60px;align-content: center;align-items: center;justify-content: center" show="{{downFlag==='' && !numFlag && lang==='en'}}" @click="onBtnClick('switchNum')" >
<img src="/components/InputMethod/assets/full/123_boardless.png" />
</div>
<!-- 这里使用show会导致�次输入都会加载全部候选列表,很� -->
<list class="list3" if="{{downFlag==='down'}}">
<list-item type="waitingRows62full" class="item3" for="{{itemArray in resultList2}}">
<div class="item column center" style="height:52px;" for="{{item in itemArray}}">
<input class="calbtn0" type="button" value="{{item}}" @click="onRsSelect(item)" />
</div>
</list-item>
</list>
<div style="position: absolute;top:99px;left:95px;width:294px;height:60px;" show="{{downFlag===''&&!numFlag}}">
<text class="calbtnt9" @click="onSelect('select')">选择</text>
<text class="calbtnt9" for="{{item in keys['t9'][0]}}" @click="onSelect(item)">{{item.toUpperCase()}}</text>
</div>
<div style="position: absolute;top:163px;left:95px;width:294px;height:60px;" show="{{downFlag===''&&!numFlag}}">
<text class="calbtnt9" for="{{item in keys['t9'][1]}}" @click="onSelect(item)">{{item.toUpperCase()}}</text>
</div>
<div style="position: absolute;top:227px;left:95px;width:294px;height:60px;" show="{{downFlag===''&&!numFlag}}">
<text class="calbtnt9" for="{{item in keys['t9'][2]}}" @click="onSelect(item)">{{item.toUpperCase()}}</text>
</div>
<div style="position: absolute;top:35px;left:95px;width:294px;height:60px;" show="{{numFlag}}">
<text class="calbtnt9" @click="onSelect('7')">7</text>
<text class="calbtnt9" @click="onSelect('8')">8</text>
<text class="calbtnt9" @click="onSelect('9')">9</text>
</div>
<div style="position: absolute;top:99px;left:95px;width:294px;height:60px;" show="{{numFlag}}">
<text class="calbtnt9" @click="onSelect('4')">4</text>
<text class="calbtnt9" @click="onSelect('5')">5</text>
<text class="calbtnt9" @click="onSelect('6')">6</text>
</div>
<div style="position: absolute;top:163px;left:95px;width:294px;height:60px;" show="{{numFlag}}">
<text class="calbtnt9" @click="onSelect('1')">1</text>
<text class="calbtnt9" @click="onSelect('2')">2</text>
<text class="calbtnt9" @click="onSelect('3')">3</text>
</div>
<div style="position: absolute;top:227px;left:95px;width:294px;height:60px;" show="{{numFlag}}">
<text class="calbtnt9" @click="onSelect('.')">.</text>
<text class="calbtnt9" @click="onSelect('0')">0</text>
<text class="calbtnt9" @click="onSelect(',')">,</text>
</div>
<img src="/components/InputMethod/assets/t9/del.png" style="position: absolute;top:35px;left:389px;width: 60px;height: 60px;" @click="onBtnClick('D')" show="{{downFlag==='' }}" />
<img src="/components/InputMethod/assets/t9/space.png" style="position: absolute;top:99px;left:389px;width: 60px;height: 60px;" @click="onBtnClick('space')" show="{{downFlag==='' && !numFlag }}" />
<img style="position: absolute;top:204px;left:78px;" src="/components/InputMethod/assets/full/up.png" @click="onBtnClick('down')" show="{{downFlag==='down'}}" />
</div>
</div>
<!-- 方�67 -->
<div if="{{screentype==='rect'}}" style="width: 100%;height: 255px;flex-direction: column">
<!-- �键中文 -->
<div if="{{keyboardtype=='T9' && !numFlag}}" style="position:absolute;top:-11px;width:100%;height:276px;justify-content: center;">
<div style="top:77px;height:189px;width:100%;align-items: stretch;justify-content: space-between;flex-direction: column;padding:6px 3px">
<div class="keyboard-rows-rect-t9">
<text class="calbtnt9 calbtnt9-rect" @click="onSelect('select')">
选择
<span if="{{waitingList.length != 0}}" class="waiting-keys" style="color:rgb(13,132,255);" @click="onSelectWaiting(waitingIndex)">
{{waitingList[waitingIndex].toUpperCase()}}
</span>
</text>
<text class="calbtnt9 calbtnt9-rect" for="{{item in keys['t9'][0]}}" @click="onSelect(item)">{{item.toUpperCase()}}</text>
</div>
<div class="keyboard-rows-rect-t9">
<text class="calbtnt9 calbtnt9-rect" for="{{item in keys['t9'][1]}}" @click="onSelect(item)">{{item.toUpperCase()}}</text>
</div>
<div class="keyboard-rows-rect-t9">
<text class="calbtnt9 calbtnt9-rect" for="{{item in keys['t9'][2]}}" @click="onSelect(item)">{{item.toUpperCase()}}</text>
</div>
</div>
</div>
<div else style="position:absolute;top:-11px;width:100%;height:276px;justify-content: center">
<progress percent="{{percent67}}" style="position:absolute;bottom: 12px;width:80px;color:#ffffff;stroke-width:6px;layer-color:#262626;"></progress>
<scroll id="keyboard67" scroll-x="{{true}}" onscroll="handleScroll">
<div if="{{!numFlag}}" style="left: 6px; flex-direction: column;">
<div style="margin-left: 0px;margin-top: 0px;height: 60px;">
<text class="calbtn67" for="{{item in keys['full'][0]}}" @click="onSelect(item)">{{item}}</text>
</div>
<div style="margin-left: 32px;margin-top: -5px;height: 60px;">
<text class="calbtn67" for="{{item in keys['full'][1]}}" @click="onSelect(item)">{{item}}</text>
</div>
<div style="margin-left: 64px;margin-top: -5px;height: 60px;">
<text class="calbtn67" for="{{item in keys['full'][2]}}" @click="onSelect(item)">{{item}}</text>
<img src="/components/InputMethod/assets/horizontal/space.png" style="width: 60px;height: 60px;" @click="onBtnClick('space')" />
</div>
</div>
<div else style="left: 6px; flex-direction: column;">
<div style="margin-left: 0px;margin-top: 0px;height: 60px;">
<text class="calbtn67" for="{{item in keys['sign'][0]}}" @click="onSelect(item)">{{item}}</text>
</div>
<div style="margin-left: 32px;margin-top: -5px;height: 60px;">
<text class="calbtn67" for="{{item in keys['sign'][1]}}" @click="onSelect(item)">{{item}}</text>
</div>
<div style="margin-left: 64px;margin-top: -5px;height: 60px;">
<text class="calbtn67" for="{{item in keys['sign'][2]}}" @click="onSelect(item)">{{item}}</text>
</div>
</div>
</scroll>
</div>
<div style="width: 100%; flex-direction: row; justify-content: center; top:6px; padding:0 6px;">
<img src="/components/InputMethod/assets/horizontal/cn.png" style="padding: 6px;width: 60px;height: 60px;" @click="onBtnClick('lang')" if="{{downFlag==='' && !numFlag && lang==='cn'}}" />
<div if="{{lang === 'cn' && !numFlag}}" style="margin-left: 6px;flex: 1;height: 60px;background-color:#262626;border-color: #333333; border-width: 3px; border-radius: 100px;flex-direction: row;align-items:center">
<scroll id="cvalWaiting" scroll-x="{{true}}" style="position:absolute;width: 85%;height: 42px;">
<div style="position: absolute;left: 0px;height: 42px;padding-left:20px;padding-right:20px">
<text class="calbtn02" style="padding-right:10px" @click="pushCval">{{cval}}</text>
<text for="{{cvalList}}" show="{{resultList.length > $idx}}" class="calbtn02" style="padding-right:10px" @click="onRsSelect(resultList[$idx])">{{resultList[$idx]}}</text>
</div>
</scroll>
<img if="{{resultList.length > 0}}" style="position:absolute;right: 8px; width: 60px;height: 40px;" src="/components/InputMethod/assets/horizontal/down2.png" @click="onBtnClick('down')" />
</div>
<img src="/components/InputMethod/assets/horizontal/en.png" style="padding: 6px;width: 60px;height: 60px;" @click="onBtnClick('lang')" if="{{downFlag==='' && !numFlag && lang==='en'}}" />
<img src="/components/InputMethod/assets/horizontal/bigA.png" style="padding: 6px;margin-left: 6px;width:94px;height:60px;" @click="onBtnClick('switchLow')" if="{{downFlag==='' && upperFlag && lang==='en'&& !numFlag}}" />
<img src="/components/InputMethod/assets/horizontal/a.png" style="padding: 6px;margin-left: 6px;width:94px;height:60px;" @click="onBtnClick('switchUpper')" if="{{downFlag==='' && !upperFlag && lang==='en'&& !numFlag}}" />
<img src="/components/InputMethod/assets/horizontal/123.png" style="margin-left: 6px;padding: 6px;margin-left: 6px;width: 94px;height: 60px;" @click="onBtnClick('switchNum')" if="{{downFlag==='' && !numFlag && lang==='en'}}" />
<img src="/components/InputMethod/assets/horizontal/back2.png" style="margin-left: 6px;padding: 6px;width: 159px;height: 60px;" @click="onBtnClick('switchCn')" if="{{numFlag}}" />
<img if="{{!numFlag}}" src="/components/InputMethod/assets/horizontal/del.png" style="margin-left: 6px;padding: 6px;width: 60px;height: 60px;" @click="onBtnClick('D')" />
<img else src="/components/InputMethod/assets/horizontal/del2.png" style="margin-left: 6px;padding: 6px;" @click="onBtnClick('D')" />
</div>
<!-- 这里使用show会导致�次输入都会加载全部候选列表,很� -->
<div style="position: absolute;left: 0px;top: 0px;width: 100%;height: 252px;background-color: black; justify-content:center; flex-direction:column; align-items:center" if="{{downFlag==='down'}}">
<div class="list67">
<list style="width:100%;height:100%;">
<list-item type="waitingRows67" class="item67" for="{{itemArray in resultList2}}">
<div class="item column center" for="{{item in itemArray}}">
<input class="calbtn02" type="button" value="{{item}}" @click="onRsSelect(item)" />
</div>
</list-item>
</list>
</div>
<img style="margin-top:5px" src="/components/InputMethod/assets/horizontal/up2.png" @click="onBtnClick('down')" />
</div>
</div>
<!-- 胶囊66 -->
<div if="{{screentype==='pill-shaped'}}" style="width: 100%;height: 305px">
<div style="position:absolute;left:0px;top:34px;width:100%;height:276px;">
<progress percent="{{30+percent66}}" type="arc" style="start-angle:204deg;total-angle:-48deg;width:188px;height:188px;top:82px;left:2px;position:absolute;color:#ffffff;stroke-width:6px;layer-color:#262626;margin-left: {{(screenWidth - 192)/2}}px;"></progress>
<scroll id="keyboard66" scroll-x="{{true}}" onscroll="handleScroll" style="padding-left: {{(screenWidth - 192)/2}}px;padding-right: {{(screenWidth - 192)/2}}px;">
<div if="{{!numFlag}}" style="left: 3px; flex-direction: column;">
<div style="margin-left: 0px;margin-top: 0px;height: 60px;">
<text class="calbtn66" for="{{item in keys['full'][0]}}" @click="onSelect(item)">{{item}}</text>
</div>
<div style="margin-left: 32px;margin-top: -5px;height: 60px;">
<text class="calbtn66" for="{{item in keys['full'][1]}}" @click="onSelect(item)">{{item}}</text>
</div>
<div style="margin-left: 64px;margin-top: -5px;height: 60px;">
<text class="calbtn66" for="{{item in keys['full'][2]}}" @click="onSelect(item)">{{item}}</text>
<img src="/components/InputMethod/assets/arc/space.png" style="width: 60px;height: 60px;" @click="onBtnClick('space')" />
</div>
</div>
<div else style="left: 3px; flex-direction: column;">
<div style="margin-left: 0px;margin-top: 0px;height: 60px;">
<text class="calbtn66" for="{{item in keys['sign'][0]}}" @click="onSelect(item)">{{item}}</text>
</div>
<div style="margin-left: 32px;margin-top: -5px;height: 60px;">
<text class="calbtn66" for="{{item in keys['sign'][1]}}" @click="onSelect(item)">{{item}}</text>
</div>
<div style="margin-left: 64px;margin-top: -5px;height: 60px;">
<text class="calbtn66" for="{{item in keys['sign'][2]}}" @click="onSelect(item)">{{item}}</text>
</div>
</div>
</scroll>
</div>
<div style="position: absolute;left: {{(screenWidth - 192)/2}}px;top: 0px;width: 192px;height: 110px;">
<img style="position: absolute;left: 3px;top: 47px;width: 186px;height: 60px;" src="/components/InputMethod/assets/arc/search.png" />
<scroll id="cvalWaiting" scroll-x="{{true}}" style="position: absolute;left: 15px;top: 56px;width: 144px;height: 42px;">
<div style="position: absolute;left: 0px;top: 0px;height: 42px;padding-right:20px">
<text class="calbtn02" style="padding-right:10px" @click="pushCval">{{cval}}</text>
<text for="{{cvalList}}" show="{{resultList.length > $idx}}" class="calbtn02" style="padding-right:10px" @click="onRsSelect(resultList[$idx])">{{resultList[$idx]}}</text>
</div>
</scroll>
<img show="{{resultList.length > 0}}" style="position: absolute;left: 120px;top: 57px;width: 60px;height: 40px;" src="/components/InputMethod/assets/arc/down2.png" @click="onBtnClick('down')" />
<!-- 带��的相对路径�aiot-tookit 2.0.4 中修�-->
<img if="{{lang==='cn'}}" src="/components/InputMethod/assets/arc/cn.png" style="position: absolute;top:0px;left:9px;width: 48px;height: 42px;" @click="onBtnClick('lang')" show="{{downFlag==='' && !numFlag}}" />
<img if="{{lang==='en'}}" src="/components/InputMethod/assets/arc/en.png" style="position: absolute;top:0px;left:9px;width: 48px;height: 42px;" @click="onBtnClick('lang')" show="{{downFlag==='' && !numFlag}}" />
<img src="/components/InputMethod/assets/arc/back2.png" style="position: absolute;top:0px;left:9px;width: 48px;height: 42px;" @click="onBtnClick('switchCn')" show="{{numFlag && lang==='cn'}}" />
<img src="/components/InputMethod/assets/arc/123.png" style="position: absolute;left: 70px;top: 0px;width: 52px;height: 42px;" @click="onBtnClick('switchNum')" show="{{downFlag==='' && !numFlag && lang==='cn'}}" />
<img src="/components/InputMethod/assets/arc/bigA.png" style="position: absolute;top:0px;left:72px;width:48px;height:42px;" @click="onBtnClick('switchLow')" show="{{downFlag==='' && upperFlag && lang==='en'}}" />
<img src="/components/InputMethod/assets/arc/a.png" style="position: absolute;top:0px;left:72px;width:48px;height:42px;" @click="onBtnClick('switchUpper')" show="{{downFlag==='' && !upperFlag && lang==='en'}}" />
<img src="/components/InputMethod/assets/arc/del.png" style="position: absolute;left: 135px;top: 0px;width: 48px;height: 42px;" @click="onBtnClick('D')" />
</div>
<!-- 这里使用show会导致�次输入都会加载全部候选列表,很� -->
<div style="position: absolute;top: 47px;width: 100%;height: 263px;background-color: black;" if="{{downFlag==='down'}}">
<div style="position: absolute;left: {{(screenWidth - 192)/2}}px;width: 192px;height: 263px;">
<list class="list66">
<list-item type="waitingRows66" class="item66" for="{{itemArray in resultList2}}">
<div class="item column center" for="{{item in itemArray}}">
<input class="calbtn0" type="button" value="{{item}}" @click="onRsSelect(item)" />
</div>
</list-item>
</list>
<img style="position: absolute;top:196px;left:56px;width: 80px;height: 60px;" src="/components/InputMethod/assets/arc/up2.png" @click="onBtnClick('down')" />
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import vibrator from "@system.vibrator";
import device from '@system.device'
import { SimpleInputMethod } from "./assets/dicUtil.js";
function doSearchDic(word, cb) {
let hanzi = SimpleInputMethod.getHanzi(word);
if (hanzi && hanzi[0]) {
cb(hanzi[0]);
} else {
cb([]);
}
}
function deleteLast(t) {
if (t) {
return t.substr(0, t.length - 1);
}
return "";
}
export default {
props: {
hide: {
default: true,
},
keyboardtype: {
default: "QWERTY",
},
maxlength: {
default: 5,
},
vibratemode: {
default: "",
},
screentype: {
default: "circle",
},
},
data: {
cval: "",
resultList: [],
resultList2: [],
waitingList: [],
waitingIndex: -1,
lastWaitingStr: "",
downFlag: "",
lang: "cn",
numFlag: false,
upperFlag: false,
cvalList: [0, 1, 2, 3, 4],
percent67: 52,
percent66: 0,
// 针对screenShape为rect的设备,会自动获�screenWidth并绑定到根div
// 这样便能�时适�n67和o65甚至是�续设备,但实际效果�能�designWidth影�
screenWidth: 336,
keys: {
full: [
["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"],
["A", "S", "D", "F", "G", "H", "J", "K", "L"],
["Z", "X", "C", "V", "B", "N", "M"],
],
sign: [
["1", "2", "3", "4", "5", "6", "7", "8", "9", "0"],
["~", "!", "@", "#", "%", "\u201C", "\u201D", "*", "?", "/"],
["(", ")", "-", "_", ":", ";", "\uFF0C", "\u3002", "."],
],
sign62: [
["2", "3", "4", "5", "6", "7", "8", "9"],
["!", "@", "#", "%", "\u201C", "\u201D", "*"],
[")", "-", "_", ":", ";"],
],
full62: [
["W", "E", "R", "T", "Y", "U", "I", "O"],
["S", "D", "F", "G", "H", "J", "K"],
["X", "C", "V", "B", "N"],
],
t9: [
["abc", "def"],
["ghi", "jkl", "mno"],
["pqrs", "tuv", "wxyz"],
],
},
},
onInit() {
if (this.maxlength) {
const tempCvalList = [];
for (let i = 0; i < this.maxlength; i++) {
tempCvalList.push(i);
}
this.cvalList = tempCvalList;
}
if (this.screentype === "rect" || this.screentype === "pill-shaped") {
this.adjustScreenWidth();
}
this.$watch("hide", "watchHidePropsChange");
this.$watch("maxlength", "watchMaxLengthPropsChange");
},
addAllTxt(txt) {
this.$emit("complete", { content: txt });
},
onRsSelect(txt) {
this.onVibrate();
this.cval = "";
this.addAllTxt(txt);
this.clearWaiting();
this.resetResultList();
this.downFlag = "";
},
onBtnClick(sign) {
this.onVibrate();
switch (sign) {
case "AC":
this.cval = "";
this.clearWaiting();
this.resetResultList();
break;
case "lang":
if (this.lang === "cn") {
this.lang = "en";
} else {
this.lang = "cn";
}
this.cval = "";
this.clearWaiting();
this.resetResultList();
break;
case "D":
if (this.waitingIndex >= 0) {
this.clearWaiting();
this.resetResultList();
} else if (this.cval.length > 0) {
this.cval = deleteLast(this.cval);
this.resetResultList();
} else {
this.$emit("delete", {});
}
break;
case "space":
this.addAllTxt(" ");
break;
case "down":
this.downFlag = this.downFlag === "down" ? "" : "down";
break;
case "select":
if (this.lastWaitingStr != sign && this.lastWaitingStr) {
if (this.lang === "cn") {
this.cval += this.waitingList[this.waitingIndex];
} else {
if (this.upperFlag) {
this.addAllTxt(this.waitingList[this.waitingIndex].toUpperCase());
} else {
this.addAllTxt(this.waitingList[this.waitingIndex].toLowerCase());
}
}
this.clearWaiting();
this.resetResultList();
}
break;
case "switchNum":
this.numFlag = true;
this.cval = "";
this.clearWaiting();
this.resetResultList();
break;
case "switchCn":
this.numFlag = false;
break;
case "switchUpper":
this.upperFlag = true;
break;
case "switchLow":
this.upperFlag = false;
break;
default:
if (sign.length === 1) {
this.addAllTxt(sign);
} else {
if (this.waitingIndex >= 0) {
if (this.lastWaitingStr === sign) {
this.waitingIndex++;
if (this.waitingIndex >= this.lastWaitingStr.length) {
this.waitingIndex = 0;
}
} else {
if (this.lang === "cn") {
this.cval += this.waitingList[this.waitingIndex];
} else {
if (this.upperFlag) {
this.addAllTxt(
this.waitingList[this.waitingIndex].toUpperCase(),
);
} else {
this.addAllTxt(
this.waitingList[this.waitingIndex].toLowerCase(),
);
}
}
this.lastWaitingStr = sign;
this.waitingIndex = 0;
this.waitingList = sign.split("");
}
} else {
this.lastWaitingStr = sign;
this.waitingIndex = 0;
this.waitingList = sign.split("");
}
this.resetResultList();
}
break;
}
},
clearWaiting() {
this.waitingList = [];
this.waitingIndex = -1;
this.lastWaitingStr = "";
},
resetResultList() {
if (this.screentype != "circle") {
this.$element("cvalWaiting").scrollTo({
top: 0,
left: 0,
behavior: "smooth",
});
}
let watingStr = "";
if (this.lastWaitingStr && this.lastWaitingStr[this.waitingIndex]) {
watingStr = this.lastWaitingStr[this.waitingIndex];
}
if (!(this.cval + watingStr) || this.lang != "cn") {
this.resultList = [];
this.setResultListAll();
return;
}
this.getResultByWord(this.cval + watingStr);
},
setResultListAll() {
this.resultList2 = [];
let array = [];
for (let i = 0; i < this.resultList.length; i++) {
array.push(this.resultList[i]);
if (array.length === parseInt(this.maxlength)) {
this.resultList2.push(array);
array = [];
}
}
if (array.length > 0 && array.length < parseInt(this.maxlength)) {
this.resultList2.push(array);
}
},
getResultByWord(val) {
const that = this;
doSearchDic(val, function (data) {
that.resultList = data;
that.setResultListAll();
});
},
onSelect(num) {
this.$emit("keydown", { content: num });
if (this.keyboardtype === "T9" && this.screentype !== "pill-shaped") {
this.onBtnClick(num);
return;
}
this.onVibrate();
if (this.lang === "cn" && !this.numFlag) {
this.cval += num.toLowerCase();
} else if (this.lang === "en" && !this.numFlag) {
if (this.upperFlag) {
this.addAllTxt(num.toUpperCase());
} else {
this.addAllTxt(num.toLowerCase());
}
} else {
this.addAllTxt(num);
}
this.resetResultList();
},
onSelectWaiting(num) {
this.onVibrate();
if (this.lang === "cn") {
this.cval += this.waitingList[num].toString();
} else {
if (this.upperFlag) {
this.addAllTxt(this.waitingList[num].toUpperCase());
} else {
this.addAllTxt(this.waitingList[num].toLowerCase());
}
}
this.clearWaiting();
this.resetResultList();
},
watchHidePropsChange(newV, _oldV) {
this.$emit("visibilityChange", { visible: newV });
},
watchMaxLengthPropsChange(newV, _oldV) {
if (newV) {
const tempCvalList = [];
for (let i = 0; i < newV; i++) {
tempCvalList.push(i);
}
this.cvalList = tempCvalList;
}
},
onVibrate() {
if (this.vibratemode != "") {
vibrator.vibrate({ mode: this.vibratemode });
}
},
handleScroll(event) {
let percentTemp67 = (event.scrollX / 636) * 100 + 52.8;
this.percent67 = parseInt(percentTemp67 <= 100 ? percentTemp67 : 100);
let percentTemp66 = (event.scrollX / 633) * 100;
this.percent66 = parseInt(percentTemp66 <= 100 ? percentTemp66 : 100);
},
pushCval() {
this.onVibrate();
let temp = this.cval;
this.cval = "";
this.clearWaiting();
this.resetResultList();
this.addAllTxt(temp);
},
adjustScreenWidth(){
device.getInfo({
success: (data) => {
this.screenWidth = data.screenWidth;
}
})
}
};
</script>
<style>
.page {
width:100%;
position:absolute;
left:0;
bottom:0
}
.item {
height:52px;
flex:1
}
.calbtn0 {
color:#fff;
font-size:28px;
background-color:rgba(38,38,38,0);
border-radius:0;
height:52px;
width:52px;
text-align:center
}
.calbtn02 {
color:rgb(255,255,255);
background-color:rgba(38,38,38,0);
border-radius:0px;
font-size:32px;
text-align:center;
height:42px;
}
.calbtnfull {
color:#fff;
font-size:24px;
font-weight:bold;
background-color:#262626;
border-radius:12px;
margin-right:4px;
height:52px;
width:40px;
text-align:center;
border:3px solid rgba(255,255,255,0.06)
}
.calbtnt9 {
color:#fff;
font-size:25px;
font-weight:bold;
background-color:#262626;
border-radius:999px;
margin-right:4px;
width:94px;
height:60px;
text-align:center;
border:3px solid rgba(255,255,255,0.06)
}
.caltext {
text-align:left;
line-height:38px;
lines:1;
text-overflow:ellipsis;
color:#0d84ff;
height:45px;
font-size:28px;
text-align:left;
font-weight:bold;
padding-left:8px
}
.list3 {
position:absolute;
top:38px;
left:78px;
width:324px;
height:160px;
flex-direction:column;
background-color:#262626;
border-radius:12px
}
.item3 {
width:324px;
height:52px
}
.calbtn67 {
color:rgb(255,255,255);
font-size:32px;
font-weight:bold;
background-color:rgb(38,38,38);
margin-right:4px;
width:60px;
height:60px;
border-radius:30px;
text-align:center;
border:3px solid rgba(255,255,255,0.06);
}
#keyboard67 {
position:absolute;
left:0px;
top:82px;
width:100%;
height:170px;
}
#keyboard66 {
position:absolute;
left:0px;
top:82px;
width:100%;
height:170px;
}
.list67 {
top:0px;
width:96.4%;
height:170px;
border-radius:30px;
background-color:#262626;
border:3px solid rgba(255,255,255,0.06);
padding:0px 10px;
}
.item67 {
height:50px;
}
.calbtn66 {
color:rgb(255,255,255);
font-size:32px;
font-weight:bold;
background-color:rgb(38,38,38);
margin-right:3px;
width:60px;
height:60px;
border-radius:30px;
text-align:center;
border:3px solid rgba(255,255,255,0.06);
}
.list66 {
position:absolute;
left:3px;
top:0px;
width:186px;
height:186px;
border-radius:30px;
background-color:#262626;
border:3px solid rgba(255,255,255,0.06);
padding:10px
}
.item66 {
height:42px;
}
.waiting-keys {
width:36px;
height:40px;
text-align: center;
}
.keyboard-rows-rect-t9 {
align-items: center;
justify-content: space-between;
flex-direction: row;
flex-shrink: 0;
height: 55px;
width: 100%;
}
.calbtnt9-rect {
flex:1;
height: 55px;
margin: 0 3px;
width: unset;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 759 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 687 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

+6
View File
@@ -0,0 +1,6 @@
/**
* 收录常用汉字6763个,不支持声调,支持多音字,并按照汉字使用频率由低到高排序
*/
const dict = {"a":"阿啊呵腌嗄吖锕","e":"额阿俄恶鹅遏鄂厄饿峨扼娥鳄哦蛾噩愕讹锷垩婀鹗萼谔莪腭锇颚呃阏屙苊轭","ai":"爱埃艾碍癌哀挨矮隘蔼唉皑哎霭捱暧嫒嗳瑷嗌锿砹","ei":"诶","xi":"系西席息希习吸喜细析戏洗悉锡溪惜稀袭夕洒晰昔牺腊烯熙媳栖膝隙犀蹊硒兮熄曦禧嬉玺奚汐徙羲铣淅嘻歙熹矽蟋郗唏皙隰樨浠忾蜥檄郄翕阋鳃舾屣葸螅咭粞觋欷僖醯鼷裼穸饩舄禊诶菥蓰","yi":"一以已意议义益亿易医艺食依移衣异伊仪宜射遗疑毅谊亦疫役忆抑尾乙译翼蛇溢椅沂泄逸蚁夷邑怡绎彝裔姨熠贻矣屹颐倚诣胰奕翌疙弈轶蛾驿壹猗臆弋铱旖漪迤佚翊诒怿痍懿饴峄揖眙镒仡黟肄咿翳挹缢呓刈咦嶷羿钇殪荑薏蜴镱噫癔苡悒嗌瘗衤佾埸圯舣酏劓","an":"安案按岸暗鞍氨俺胺铵谙庵黯鹌桉埯犴揞厂广","han":"厂汉韩含旱寒汗涵函喊憾罕焊翰邯撼瀚憨捍酣悍鼾邗颔蚶晗菡旰顸犴焓撖","ang":"昂仰盎肮","ao":"奥澳傲熬凹鳌敖遨鏖袄坳翱嗷拗懊岙螯骜獒鏊艹媪廒聱","wa":"瓦挖娃洼袜蛙凹哇佤娲呙腽","yu":"于与育余预域予遇奥语誉玉鱼雨渔裕愈娱欲吁舆宇羽逾豫郁寓吾狱喻御浴愉禹俞邪榆愚渝尉淤虞屿峪粥驭瑜禺毓钰隅芋熨瘀迂煜昱汩於臾盂聿竽萸妪腴圄谕觎揄龉谀俣馀庾妤瘐鬻欤鹬阈嵛雩鹆圉蜮伛纡窬窳饫蓣狳肀舁蝓燠","niu":"牛纽扭钮拗妞忸狃","o":"哦噢喔","ba":"把八巴拔伯吧坝爸霸罢芭跋扒叭靶疤笆耙鲅粑岜灞钯捌菝魃茇","pa":"怕帕爬扒趴琶啪葩耙杷钯筢","pi":"被批副否皮坏辟啤匹披疲罢僻毗坯脾譬劈媲屁琵邳裨痞癖陂丕枇噼霹吡纰砒铍淠郫埤濞睥芘蚍圮鼙罴蜱疋貔仳庀擗甓陴","bi":"比必币笔毕秘避闭佛辟壁弊彼逼碧鼻臂蔽拂泌璧庇痹毙弼匕鄙陛裨贲敝蓖吡篦纰俾铋毖筚荸薜婢哔跸濞秕荜愎睥妣芘箅髀畀滗狴萆嬖襞舭","bai":"百白败摆伯拜柏佰掰呗擘捭稗","bo":"波博播勃拨薄佛伯玻搏柏泊舶剥渤卜驳簿脖膊簸菠礴箔铂亳钵帛擘饽跛钹趵檗啵鹁擗踣","bei":"北被备倍背杯勃贝辈悲碑臂卑悖惫蓓陂钡狈呗焙碚褙庳鞴孛鹎邶鐾","ban":"办版半班般板颁伴搬斑扮拌扳瓣坂阪绊钣瘢舨癍","pan":"判盘番潘攀盼拚畔胖叛拌蹒磐爿蟠泮袢襻丬","bin":"份宾频滨斌彬濒殡缤鬓槟摈膑玢镔豳髌傧","bang":"帮邦彭旁榜棒膀镑绑傍磅蚌谤梆浜蒡","pang":"旁庞乓磅螃彷滂逄耪","beng":"泵崩蚌蹦迸绷甭嘣甏堋","bao":"报保包宝暴胞薄爆炮饱抱堡剥鲍曝葆瀑豹刨褒雹孢苞煲褓趵鸨龅勹","bu":"不部步布补捕堡埔卜埠簿哺怖钚卟瓿逋晡醭钸","pu":"普暴铺浦朴堡葡谱埔扑仆蒲曝瀑溥莆圃璞濮菩蹼匍噗氆攵镨攴镤","mian":"面棉免绵缅勉眠冕娩腼渑湎沔黾宀眄","po":"破繁坡迫颇朴泊婆泼魄粕鄱珀陂叵笸泺皤钋钷","fan":"反范犯繁饭泛翻凡返番贩烦拚帆樊藩矾梵蕃钒幡畈蘩蹯燔","fu":"府服副负富复福夫妇幅付扶父符附腐赴佛浮覆辅傅伏抚赋辐腹弗肤阜袱缚甫氟斧孚敷俯拂俘咐腑孵芙涪釜脯茯馥宓绂讣呋罘麸蝠匐芾蜉跗凫滏蝮驸绋蚨砩桴赙菔呒趺苻拊阝鲋怫稃郛莩幞祓艴黻黼鳆","ben":"本体奔苯笨夯贲锛畚坌","feng":"风丰封峰奉凤锋冯逢缝蜂枫疯讽烽俸沣酆砜葑唪","bian":"变便边编遍辩鞭辨贬匾扁卞汴辫砭苄蝙鳊弁窆笾煸褊碥忭缏","pian":"便片篇偏骗翩扁骈胼蹁谝犏缏","zhen":"镇真针圳振震珍阵诊填侦臻贞枕桢赈祯帧甄斟缜箴疹砧榛鸩轸稹溱蓁胗椹朕畛浈","biao":"表标彪镖裱飚膘飙镳婊骠飑杓髟鳔灬瘭","piao":"票朴漂飘嫖瓢剽缥殍瞟骠嘌莩螵","huo":"和活或货获火伙惑霍祸豁嚯藿锪蠖钬耠镬夥灬劐攉","bie":"别鳖憋瘪蹩","min":"民敏闽闵皿泯岷悯珉抿黾缗玟愍苠鳘","fen":"分份纷奋粉氛芬愤粪坟汾焚酚吩忿棼玢鼢瀵偾鲼","bing":"并病兵冰屏饼炳秉丙摒柄槟禀枋邴冫","geng":"更耕颈庚耿梗埂羹哽赓绠鲠","fang":"方放房防访纺芳仿坊妨肪邡舫彷枋鲂匚钫","xian":"现先县见线限显险献鲜洗宪纤陷闲贤仙衔掀咸嫌掺羡弦腺痫娴舷馅酰铣冼涎暹籼锨苋蚬跹岘藓燹鹇氙莶霰跣猃彡祆筅","fou":"不否缶","ca":"拆擦嚓礤","cha":"查察差茶插叉刹茬楂岔诧碴嚓喳姹杈汊衩搽槎镲苴檫馇锸猹","cai":"才采财材菜彩裁蔡猜踩睬","can":"参残餐灿惨蚕掺璨惭粲孱骖黪","shen":"信深参身神什审申甚沈伸慎渗肾绅莘呻婶娠砷蜃哂椹葚吲糁渖诜谂矧胂","cen":"参岑涔","san":"三参散伞叁糁馓毵","cang":"藏仓苍沧舱臧伧","zang":"藏脏葬赃臧奘驵","chen":"称陈沈沉晨琛臣尘辰衬趁忱郴宸谌碜嗔抻榇伧谶龀肜","cao":"草操曹槽糙嘈漕螬艚屮","ce":"策测册侧厕栅恻","ze":"责则泽择侧咋啧仄箦赜笮舴昃迮帻","zhai":"债择Line truncated
export { dict }
@@ -0,0 +1,42 @@
import { dict } from './dic.js'
let SimpleInputMethod = {
dict: {}
}
SimpleInputMethod.initDict = function() {
this.dict.py2hz = dict;
this.dict.py2hz2 = {};
this.dict.py2hz2['i'] = 'i'; // i比较特殊,没有符合的汉字,所以特殊处理
for (let key in this.dict.py2hz) {
let ch = key[0];
if (!this.dict.py2hz2[ch]) {
this.dict.py2hz2[ch] = this.dict.py2hz[key];
}
}
};
SimpleInputMethod.getSingleHanzi = function(pinyin){
return this.dict.py2hz2[pinyin] || this.dict.py2hz[pinyin] || '';
}
SimpleInputMethod.getHanzi = function(pinyin) {
let result = this.getSingleHanzi(pinyin);
if (result) return [result.split(''), pinyin];
let temp = '';
let start = Math.min(pinyin.length, 6);
for (let i = start; i >= 1; i--) {
let str = pinyin.substr(0, i);
let rs = this.getSingleHanzi(str);
if (rs) return [rs.split(''), str];
}
return [[], '']; // 理论上一般不会出现这种情况
};
SimpleInputMethod.initDict();
export { SimpleInputMethod } //换成export default SimpleInputMethod;不能用
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 649 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,241 @@
<template>
<div class="keyboard" if="{{visible}}">
<!-- 输入显示区 -->
<div class="input-display">
<text class="input-text">{{inputValue || placeholder}}</text>
<div class="clear-btn" onclick="handleClear">
<text class="clear-icon">X</text>
</div>
</div>
<!-- 数字行 -->
<div class="row">
<div class="key" onclick="handleKey('1')"><text class="key-text">1</text></div>
<div class="key" onclick="handleKey('2')"><text class="key-text">2</text></div>
<div class="key" onclick="handleKey('3')"><text class="key-text">3</text></div>
<div class="key" onclick="handleKey('4')"><text class="key-text">4</text></div>
<div class="key" onclick="handleKey('5')"><text class="key-text">5</text></div>
<div class="key" onclick="handleKey('6')"><text class="key-text">6</text></div>
<div class="key" onclick="handleKey('7')"><text class="key-text">7</text></div>
<div class="key" onclick="handleKey('8')"><text class="key-text">8</text></div>
<div class="key" onclick="handleKey('9')"><text class="key-text">9</text></div>
<div class="key" onclick="handleKey('0')"><text class="key-text">0</text></div>
</div>
<!-- QWERTY第一行 -->
<div class="row">
<div class="key" onclick="handleKey('q')"><text class="key-text">Q</text></div>
<div class="key" onclick="handleKey('w')"><text class="key-text">W</text></div>
<div class="key" onclick="handleKey('e')"><text class="key-text">E</text></div>
<div class="key" onclick="handleKey('r')"><text class="key-text">R</text></div>
<div class="key" onclick="handleKey('t')"><text class="key-text">T</text></div>
<div class="key" onclick="handleKey('y')"><text class="key-text">Y</text></div>
<div class="key" onclick="handleKey('u')"><text class="key-text">U</text></div>
<div class="key" onclick="handleKey('i')"><text class="key-text">I</text></div>
<div class="key" onclick="handleKey('o')"><text class="key-text">O</text></div>
<div class="key" onclick="handleKey('p')"><text class="key-text">P</text></div>
</div>
<!-- QWERTY第二行 -->
<div class="row">
<div class="key" onclick="handleKey('a')"><text class="key-text">A</text></div>
<div class="key" onclick="handleKey('s')"><text class="key-text">S</text></div>
<div class="key" onclick="handleKey('d')"><text class="key-text">D</text></div>
<div class="key" onclick="handleKey('f')"><text class="key-text">F</text></div>
<div class="key" onclick="handleKey('g')"><text class="key-text">G</text></div>
<div class="key" onclick="handleKey('h')"><text class="key-text">H</text></div>
<div class="key" onclick="handleKey('j')"><text class="key-text">J</text></div>
<div class="key" onclick="handleKey('k')"><text class="key-text">K</text></div>
<div class="key" onclick="handleKey('l')"><text class="key-text">L</text></div>
<div class="key" onclick="handleKey('.')"><text class="key-text">.</text></div>
</div>
<!-- QWERTY第三行 -->
<div class="row">
<div class="key" onclick="handleKey('z')"><text class="key-text">Z</text></div>
<div class="key" onclick="handleKey('x')"><text class="key-text">X</text></div>
<div class="key" onclick="handleKey('c')"><text class="key-text">C</text></div>
<div class="key" onclick="handleKey('v')"><text class="key-text">V</text></div>
<div class="key" onclick="handleKey('b')"><text class="key-text">B</text></div>
<div class="key" onclick="handleKey('n')"><text class="key-text">N</text></div>
<div class="key" onclick="handleKey('m')"><text class="key-text">M</text></div>
<div class="key" onclick="handleKey('-')"><text class="key-text">-</text></div>
<div class="key" onclick="handleKey('/')"><text class="key-text">/</text></div>
<div class="key del-key" onclick="handleDelete">
<text class="key-text"><-</text>
</div>
</div>
<!-- 快捷输入行 -->
<div class="row">
<div class="key action-key" onclick="handleKey('https://')">
<text class="action-text">https://</text>
</div>
<div class="key action-key" onclick="handleKey('www.')">
<text class="action-text">www.</text>
</div>
<div class="key action-key" onclick="handleKey('.com')">
<text class="action-text">.com</text>
</div>
</div>
<!-- 底部操作行 -->
<div class="control-row">
<div class="ctrl-btn cancel-btn" onclick="handleCancel">
<text class="ctrl-text">取消</text>
</div>
<div class="ctrl-btn confirm-btn" onclick="handleConfirm">
<text class="ctrl-text">前往</text>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
visible: {
default: false
},
inputValue: {
default: ""
},
placeholder: {
default: "输入网址"
}
},
handleKey(char) {
this.$emit("keyinput", { value: char })
},
handleDelete() {
this.$emit("keydelete", {})
},
handleClear() {
this.$emit("keyclear", {})
},
handleCancel() {
this.$emit("keycancel", {})
},
handleConfirm() {
this.$emit("keyconfirm", {})
}
}
</script>
<style>
.keyboard {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #1c1c1e;
flex-direction: column;
padding: 8px;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
/* 输入显示 */
.input-display {
height: 40px;
flex-direction: row;
align-items: center;
padding: 0 12px;
margin-bottom: 8px;
border-radius: 10px;
background-color: #2c2c2e;
}
.input-text {
flex: 1;
color: #ffffff;
font-size: 15px;
}
.clear-btn {
width: 28px;
height: 28px;
border-radius: 14px;
background-color: #3a3a3c;
justify-content: center;
align-items: center;
}
.clear-icon {
color: #8e8e93;
font-size: 12px;
}
/* 按键行 */
.row {
flex-direction: row;
justify-content: space-between;
margin-bottom: 4px;
}
.key {
width: 30px;
height: 40px;
border-radius: 6px;
background-color: #2c2c2e;
justify-content: center;
align-items: center;
}
.key-text {
color: #ffffff;
font-size: 14px;
font-weight: 500;
}
.del-key {
background-color: #3a3a3c;
}
.action-key {
flex: 1;
margin: 0 2px;
width: auto;
}
.action-text {
color: #0a84ff;
font-size: 12px;
}
/* 底部操作 */
.control-row {
flex-direction: row;
justify-content: space-between;
margin-top: 4px;
padding: 0 4px;
}
.ctrl-btn {
flex: 1;
height: 40px;
margin: 0 4px;
border-radius: 10px;
justify-content: center;
align-items: center;
}
.cancel-btn {
background-color: #3a3a3c;
}
.confirm-btn {
background-color: #0a84ff;
}
.ctrl-text {
color: #ffffff;
font-size: 15px;
font-weight: 500;
}
</style>
+1
View File
@@ -0,0 +1 @@
{}
+6
View File
@@ -0,0 +1,6 @@
{
"app": {
"name": "搜哩浏览器",
"version": "v1.0.0"
}
}
+56
View File
@@ -0,0 +1,56 @@
{
"app": {
"name": "Souli Browser",
"version": "v1.0.0",
"description": "Lightweight browser for smartwatches"
},
"common": {
"confirm": "OK",
"cancel": "Cancel",
"back": "Back",
"save": "Save",
"delete": "Delete",
"edit": "Edit",
"done": "Done",
"loading": "Loading",
"empty": "No content",
"retry": "Retry",
"close": "Close"
},
"home": {
"searchPlaceholder": "Search or enter URL",
"bookmarks": "My Bookmarks",
"shortcuts": "Shortcuts",
"functions": "Functions",
"viewAll": "View All"
},
"browser": {
"inputUrl": "Enter URL",
"startBrowsing": "Start Browsing",
"enterUrlHint": "Tap above to enter URL",
"loading": "Loading",
"loadFailed": "Failed to load",
"networkError": "Network error, please check connection",
"noContent": "Page has no content or unsupported format",
"saveSuccess": "Saved successfully",
"saveFailed": "Failed to save",
"noContentToSave": "No content to save"
},
"bookmarks": {
"title": "Bookmarks",
"empty": "No bookmarks",
"emptyHint": "Add bookmarks while browsing",
"deleted": "Deleted"
},
"offline": {
"title": "Offline Pages",
"empty": "No offline pages",
"emptyHint": "Save offline pages while browsing",
"elements": "elements"
},
"about": {
"title": "About",
"features": "Features",
"tech": "Technology"
}
}
+56
View File
@@ -0,0 +1,56 @@
{
"app": {
"name": "搜哩浏览器",
"version": "v1.0.0",
"description": "专为智能手表设计的轻量浏览器"
},
"common": {
"confirm": "确定",
"cancel": "取消",
"back": "返回",
"save": "保存",
"delete": "删除",
"edit": "编辑",
"done": "完成",
"loading": "加载中",
"empty": "暂无内容",
"retry": "重试",
"close": "关闭"
},
"home": {
"searchPlaceholder": "搜索或输入网址",
"bookmarks": "我的收藏",
"shortcuts": "常用网站",
"functions": "功能",
"viewAll": "全部"
},
"browser": {
"inputUrl": "输入网址",
"startBrowsing": "开始浏览",
"enterUrlHint": "点击上方输入网址",
"loading": "加载中",
"loadFailed": "获取失败",
"networkError": "网络错误,请检查连接",
"noContent": "页面无内容或格式不支持",
"saveSuccess": "保存成功",
"saveFailed": "保存失败",
"noContentToSave": "无内容可保存"
},
"bookmarks": {
"title": "收藏夹",
"empty": "暂无收藏",
"emptyHint": "浏览网页时点击收藏按钮添加",
"deleted": "已删除"
},
"offline": {
"title": "离线页面",
"empty": "暂无离线页面",
"emptyHint": "浏览网页时选择保存离线页面",
"elements": "个元素"
},
"about": {
"title": "关于",
"features": "功能特性",
"tech": "技术支持"
}
}
+61
View File
@@ -0,0 +1,61 @@
{
"package": "com.xinflow.browser",
"name": "搜哩浏览器",
"versionName": "1.0.0",
"versionCode": 1,
"minPlatformVersion": 1000,
"icon": "/common/logo.png",
"deviceTypeList": [
"watch"
],
"features": [
{
"name": "system.router"
},
{
"name": "system.webview"
},
{
"name": "system.storage"
},
{
"name": "system.fetch"
},
{
"name": "system.vibrator"
},
{
"name": "system.device"
},
{
"name": "system.network"
}
],
"config": {
"logLevel": "log",
"designWidth": 336,
"trustedUrl": [
"*"
]
},
"router": {
"entry": "pages/home",
"pages": {
"pages/home": {
"component": "home"
},
"pages/index": {
"component": "index"
},
"pages/detail": {
"component": "detail"
},
"pages/offline": {
"component": "offline"
},
"pages/about": {
"component": "about"
}
}
}
}
+177
View File
@@ -0,0 +1,177 @@
<template>
<div class="page">
<div class="header">
<div class="back-btn" onclick="goBack">
<text class="back-icon">&lt;</text>
</div>
<text class="title">关于</text>
</div>
<scroll class="content" scroll-y="true">
<div class="main">
<div class="logo-section">
<div class="logo">
<text class="logo-text">搜</text>
</div>
<text class="app-name">{{app.name}}</text>
<text class="app-version">{{app.version}}</text>
</div>
<text class="description">{{app.description}}</text>
<div class="info-section" for="{{section in sections}}">
<text class="section-title">{{section.title}}</text>
<text class="section-item" for="{{item in section.items}}">{{item}}</text>
</div>
<text class="copyright">{{app.copyright}}</text>
</div>
</scroll>
</div>
</template>
<script>
import router from "@system.router"
export default {
private: {
app: {
name: '搜哩浏览器',
version: 'v1.0.0',
description: '专为智能手表设计的轻量浏览器',
copyright: '© 2024 搜哩浏览器'
},
sections: [
{
title: '功能特性',
items: ['• 网页浏览与解析', '• 自定义键盘输入', '• 离线页面保存', '• 书签收藏管理']
},
{
title: '技术支持',
items: ['• 小米Vela快应用框架', '• 智能手表优化适配', '• HTML内容解析渲染']
}
]
},
goBack() {
router.back()
}
}
</script>
<style>
.page {
flex-direction: column;
height: 480px;
background-color: #000;
}
.header {
height: 44px;
flex-direction: row;
align-items: center;
padding: 0 10px;
background-color: #1c1c1e;
}
.back-btn {
width: 32px;
height: 32px;
border-radius: 16px;
background-color: #2c2c2e;
justify-content: center;
align-items: center;
}
.back-icon {
color: #0a84ff;
font-size: 18px;
}
.title {
flex: 1;
text-align: center;
color: #fff;
font-size: 17px;
font-weight: 600;
margin-right: 42px;
}
.content {
height: 436px;
}
.main {
flex-direction: column;
align-items: center;
padding: 20px 16px;
}
.logo-section {
flex-direction: column;
align-items: center;
margin-bottom: 16px;
}
.logo {
width: 72px;
height: 72px;
border-radius: 18px;
background-color: #0a84ff;
justify-content: center;
align-items: center;
}
.logo-text {
color: #fff;
font-size: 32px;
font-weight: bold;
}
.app-name {
color: #fff;
font-size: 20px;
font-weight: 600;
margin-top: 12px;
}
.app-version {
color: #8e8e93;
font-size: 13px;
margin-top: 4px;
}
.description {
color: #8e8e93;
font-size: 14px;
margin-bottom: 16px;
text-align: center;
}
.info-section {
width: 296px;
margin-bottom: 12px;
padding: 14px;
border-radius: 12px;
background-color: #1c1c1e;
}
.section-title {
color: #0a84ff;
font-size: 15px;
font-weight: 600;
margin-bottom: 10px;
}
.section-item {
color: #e5e5e7;
font-size: 13px;
margin-top: 6px;
}
.copyright {
color: #48484a;
font-size: 11px;
margin-top: 20px;
}
</style>
+277
View File
@@ -0,0 +1,277 @@
<template>
<div class="page">
<div class="header">
<div class="back-btn" onclick="goBack">
<text class="back-icon">&lt;</text>
</div>
<text class="title">收藏夹</text>
<div class="edit-btn" onclick="toggleEdit">
<text class="edit-text">{{isEditing ? '完成' : '编辑'}}</text>
</div>
</div>
<scroll class="content" scroll-y="true">
<div class="list" if="{{bookmarks.length > 0}}">
<div class="item" for="{{(index, item) in bookmarks}}">
<div class="delete-btn" if="{{isEditing}}" onclick="deleteBookmark($idx)">
<text class="delete-icon">−</text>
</div>
<div class="item-content" onclick="openBookmark($idx)">
<div class="item-icon" style="background-color: {{item.color}}">
<text class="icon-text">{{item.icon}}</text>
</div>
<div class="item-info">
<text class="item-name">{{item.name}}</text>
<text class="item-url">{{formatUrl(item.url)}}</text>
</div>
<text class="item-arrow" if="{{!isEditing}}">›</text>
</div>
</div>
</div>
<div class="empty" if="{{bookmarks.length === 0}}">
<text class="empty-icon">*</text>
<text class="empty-text">暂无收藏</text>
<text class="empty-desc">浏览网页时点击收藏按钮添加</text>
</div>
</scroll>
<div class="toast" if="{{showToast}}">
<text class="toast-text">{{toastMsg}}</text>
</div>
</div>
</template>
<script>
import router from "@system.router"
import storage from "@system.storage"
import { getStorageData, setStorageData, formatDisplayUrl } from "../../common/utils.js"
export default {
private: {
bookmarks: [],
isEditing: false,
showToast: false,
toastMsg: ''
},
onShow() {
this.loadBookmarks()
},
async loadBookmarks() {
const data = await getStorageData(storage, 'user_bookmarks')
this.bookmarks = data.map(b => ({
...b,
color: b.color || '#0a84ff',
icon: b.icon || b.name.charAt(0).toUpperCase()
}))
},
formatUrl(url) {
return formatDisplayUrl(url)
},
openBookmark(index) {
if (this.isEditing) return
const item = this.bookmarks[index]
if (item && item.url) {
router.push({ uri: 'pages/index', params: { url: item.url } })
}
},
toggleEdit() {
this.isEditing = !this.isEditing
},
async deleteBookmark(index) {
const item = this.bookmarks[index]
this.bookmarks.splice(index, 1)
const success = await setStorageData(storage, 'user_bookmarks', this.bookmarks)
if (success) {
this.showTip(`已删除 ${item.name}`)
if (this.bookmarks.length === 0) {
this.isEditing = false
}
} else {
this.showTip('删除失败')
this.bookmarks.splice(index, 0, item)
}
},
showTip(msg) {
this.toastMsg = msg
this.showToast = true
setTimeout(() => this.showToast = false, 2000)
},
goBack() {
router.back()
}
}
</script>
<style>
.page {
flex-direction: column;
height: 480px;
background-color: #000;
}
.header {
height: 44px;
flex-direction: row;
align-items: center;
padding: 0 10px;
background-color: #1c1c1e;
}
.back-btn {
width: 32px;
height: 32px;
border-radius: 16px;
background-color: #2c2c2e;
justify-content: center;
align-items: center;
}
.back-icon {
color: #0a84ff;
font-size: 18px;
}
.title {
flex: 1;
text-align: center;
color: #fff;
font-size: 17px;
font-weight: 600;
}
.edit-btn {
padding: 6px 12px;
border-radius: 6px;
background-color: #2c2c2e;
}
.edit-text {
color: #0a84ff;
font-size: 14px;
}
.content {
height: 436px;
}
.list {
padding: 8px;
}
.item {
flex-direction: row;
align-items: center;
margin-bottom: 6px;
border-radius: 12px;
background-color: #1c1c1e;
}
.delete-btn {
width: 28px;
height: 28px;
border-radius: 14px;
background-color: #ff453a;
justify-content: center;
align-items: center;
margin-left: 12px;
}
.delete-icon {
color: #fff;
font-size: 18px;
font-weight: bold;
}
.item-content {
flex: 1;
height: 56px;
flex-direction: row;
align-items: center;
padding: 0 12px;
}
.item-icon {
width: 40px;
height: 40px;
border-radius: 10px;
justify-content: center;
align-items: center;
margin-right: 12px;
}
.icon-text {
color: #fff;
font-size: 16px;
font-weight: bold;
}
.item-info {
flex: 1;
flex-direction: column;
}
.item-name {
color: #fff;
font-size: 15px;
font-weight: 500;
}
.item-url {
color: #8e8e93;
font-size: 11px;
margin-top: 2px;
}
.item-arrow {
color: #48484a;
font-size: 18px;
}
.empty {
flex: 1;
justify-content: center;
align-items: center;
}
.empty-icon {
color: #3a3a3c;
font-size: 48px;
margin-bottom: 12px;
}
.empty-text {
color: #8e8e93;
font-size: 16px;
margin-bottom: 8px;
}
.empty-desc {
color: #48484a;
font-size: 13px;
}
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 20px;
background-color: rgba(48, 209, 88, 0.95);
}
.toast-text {
color: #000;
font-size: 14px;
}
</style>
+394
View File
@@ -0,0 +1,394 @@
<template>
<div class="page">
<!-- 顶部搜索区 -->
<div class="hero">
<div class="logo-circle">
<text class="logo-text">S</text>
</div>
<text class="brand">搜哩浏览器</text>
<div class="search-card" onclick="openBrowser">
<text class="search-hint">[搜索] 输入网址</text>
</div>
</div>
<!-- 快捷入口 -->
<div class="quick-section">
<div class="quick-grid">
<div class="quick-item" for="{{site in quickSites}}" onclick="openUrl(site.url)">
<div class="quick-icon" style="background-color: {{site.color}}">
<text class="quick-letter">{{site.icon}}</text>
</div>
<text class="quick-name">{{site.name}}</text>
</div>
</div>
</div>
<!-- 我的收藏 -->
<div class="fav-section" if="{{bookmarks.length > 0}}">
<div class="section-header">
<text class="section-title">[*] 收藏夹</text>
<text class="section-more" onclick="openBookmarks">查看全部</text>
</div>
<scroll class="fav-scroll" scroll-x="true">
<div class="fav-list">
<div class="fav-card" for="{{item in bookmarks}}" onclick="openUrl(item.url)">
<div class="fav-color" style="background-color: {{item.color}}"></div>
<text class="fav-title">{{item.name}}</text>
<text class="fav-url">{{formatUrl(item.url)}}</text>
</div>
</div>
</scroll>
</div>
<!-- 功能菜单 -->
<div class="menu-section">
<div class="menu-grid">
<div class="menu-cell" for="{{menu in menus}}" onclick="openMenu($idx)">
<div class="menu-icon-wrapper" style="background-color: {{menu.color}}">
<text class="menu-icon">{{menu.icon}}</text>
</div>
<text class="menu-label">{{menu.name}}</text>
<div class="badge" if="{{menu.count > 0}}">
<text class="badge-text">{{menu.count}}</text>
</div>
</div>
</div>
</div>
<!-- 底部 -->
<div class="footer">
<text class="version">腕上JM v1.0</text>
</div>
</div>
</template>
<script>
import router from "@system.router"
import storage from "@system.storage"
import { getStorageData, formatDisplayUrl } from "../../common/utils.js"
const QUICK_SITES = [
{ name: '百度', url: 'https://m.baidu.com', icon: 'B', color: '#2932e1' },
{ name: '必应', url: 'https://m.bing.com', icon: 'Bi', color: '#008373' },
{ name: 'B站', url: 'https://m.bilibili.com', icon: 'Bi', color: '#fb7299' },
{ name: '微博', url: 'https://m.weibo.cn', icon: 'W', color: '#e6162d' }
]
export default {
private: {
bookmarks: [],
bookmarkCount: 0,
offlineCount: 0,
quickSites: QUICK_SITES,
menus: []
},
onInit() {
this.updateMenus()
},
onShow() {
this.loadData()
},
async loadData() {
const bookmarks = await getStorageData(storage, 'user_bookmarks')
this.bookmarks = bookmarks.slice(0, 5).map(b => ({
...b,
color: b.color || this.getRandomColor(),
icon: b.icon || b.name.charAt(0).toUpperCase()
}))
this.bookmarkCount = bookmarks.length
const pages = await getStorageData(storage, 'offline_pages')
this.offlineCount = pages.length
this.updateMenus()
},
updateMenus() {
this.menus = [
{ name: '收藏夹', icon: '*', color: '#0a84ff', action: 'openBookmarks', count: this.bookmarkCount },
{ name: '离线页', icon: '=', color: '#30d158', action: 'openOffline', count: this.offlineCount },
{ name: '关于', icon: 'i', color: '#8e8e93', action: 'openAbout', count: 0 }
]
},
getRandomColor() {
const colors = ['#0a84ff', '#30d158', '#ff375f', '#ff9f0a', '#bf5af2', '#64d2ff']
return colors[Math.floor(Math.random() * colors.length)]
},
formatUrl(url) {
return formatDisplayUrl(url)
},
openBrowser() {
router.push({ uri: 'pages/index' })
},
openBookmarks() {
router.push({ uri: 'pages/detail' })
},
openOffline() {
router.push({ uri: 'pages/offline' })
},
openAbout() {
router.push({ uri: 'pages/about' })
},
openMenu(index) {
const menu = this.menus[index]
if (!menu) return
switch (menu.action) {
case 'openBookmarks':
this.openBookmarks()
break
case 'openOffline':
this.openOffline()
break
case 'openAbout':
this.openAbout()
break
}
},
openUrl(url) {
if (url) router.push({ uri: 'pages/index', params: { url } })
}
}
</script>
<style>
.page {
flex-direction: column;
height: 480px;
background-color: #0a0a0a;
}
/* 顶部区域 */
.hero {
padding: 24px 20px 20px;
align-items: center;
}
.logo-circle {
width: 64px;
height: 64px;
border-radius: 20px;
background-color: #0a84ff;
justify-content: center;
align-items: center;
margin-bottom: 12px;
}
.logo-text {
color: #ffffff;
font-size: 32px;
font-weight: 700;
}
.brand {
color: #ffffff;
font-size: 20px;
font-weight: 700;
margin-bottom: 16px;
letter-spacing: 1px;
}
.search-card {
width: 280px;
height: 44px;
background-color: #1c1c1e;
border-radius: 22px;
border: 1px solid #2c2c2e;
justify-content: center;
align-items: center;
}
.search-hint {
color: #8e8e93;
font-size: 14px;
}
/* 快捷入口 */
.quick-section {
padding: 0 16px;
margin-bottom: 20px;
}
.quick-grid {
flex-direction: row;
justify-content: space-between;
}
.quick-item {
width: 68px;
align-items: center;
}
.quick-icon {
width: 52px;
height: 52px;
border-radius: 16px;
justify-content: center;
align-items: center;
margin-bottom: 6px;
}
.quick-letter {
color: #ffffff;
font-size: 20px;
font-weight: 700;
}
.quick-name {
color: #98989d;
font-size: 11px;
}
/* 收藏夹 */
.fav-section {
margin-bottom: 20px;
}
.section-header {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 20px;
margin-bottom: 12px;
}
.section-title {
color: #ffffff;
font-size: 15px;
font-weight: 600;
}
.section-more {
color: #0a84ff;
font-size: 12px;
}
.fav-scroll {
height: 90px;
}
.fav-list {
flex-direction: row;
padding: 0 16px;
}
.fav-card {
width: 140px;
height: 80px;
background-color: #1c1c1e;
border-radius: 12px;
padding: 12px;
margin-right: 10px;
flex-direction: column;
justify-content: flex-end;
border: 1px solid #2c2c2e;
}
.fav-color {
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
border-top-left-radius: 12px;
border-bottom-left-radius: 12px;
}
.fav-title {
color: #ffffff;
font-size: 14px;
font-weight: 500;
margin-bottom: 4px;
lines: 1;
text-overflow: ellipsis;
}
.fav-url {
color: #8e8e93;
font-size: 10px;
lines: 1;
text-overflow: ellipsis;
}
/* 功能菜单 */
.menu-section {
padding: 0 16px;
margin-bottom: 16px;
}
.menu-grid {
flex-direction: row;
justify-content: space-between;
}
.menu-cell {
width: 96px;
height: 80px;
background-color: #1c1c1e;
border-radius: 16px;
align-items: center;
justify-content: center;
border: 1px solid #2c2c2e;
}
.menu-icon-wrapper {
width: 40px;
height: 40px;
border-radius: 12px;
justify-content: center;
align-items: center;
margin-bottom: 6px;
}
.menu-icon {
color: #ffffff;
font-size: 18px;
}
.menu-label {
color: #98989d;
font-size: 12px;
}
.badge {
position: absolute;
top: 8px;
right: 8px;
min-width: 18px;
height: 18px;
background-color: #ff375f;
border-radius: 9px;
justify-content: center;
align-items: center;
padding: 0 5px;
}
.badge-text {
color: #ffffff;
font-size: 10px;
font-weight: 600;
}
/* 底部 */
.footer {
flex: 1;
justify-content: flex-end;
align-items: center;
padding-bottom: 12px;
}
.version {
color: #3a3a3c;
font-size: 11px;
}
</style>
+969
View File
@@ -0,0 +1,969 @@
<template>
<div class="page">
<!-- 导航栏 -->
<div class="navbar">
<div class="nav-back" onclick="goBack">
<text class="icon-back">&lt;</text>
</div>
<div class="nav-input" onclick="showKeyboard">
<text class="input-text" if="{{inputText}}">{{displayUrl}}</text>
<text class="input-placeholder" else>输入网址</text>
</div>
<div class="nav-go" onclick="confirmInput">
<text class="icon-go">&gt;</text>
</div>
</div>
<!-- 内容区 -->
<div class="content-area">
<!-- 加载状态 -->
<div class="status-loading" if="{{isLoading}}">
<div class="spinner"></div>
<text class="loading-text">加载中...</text>
</div>
<!-- 错误状态 -->
<div class="status-error" if="{{errorMsg && !isLoading}}">
<text class="error-icon">!</text>
<text class="error-text">{{errorMsg}}</text>
<div class="btn-retry" onclick="refreshPage">
<text class="btn-text">重试</text>
</div>
</div>
<!-- 空状态 -->
<div class="status-empty" if="{{!currentUrl && !isLoading && !errorMsg}}">
<text class="empty-icon">WWW</text>
<text class="empty-title">开始浏览</text>
<text class="empty-desc">点击上方输入网址</text>
</div>
<!-- 网页内容 - 使用scroll -->
<scroll class="web-content"
if="{{!isLoading && !errorMsg && htmlElements.length > 0}}"
scroll-y="true">
<div class="content-inner">
<text class="page-title" if="{{pageTitle}}">{{pageTitle}}</text>
<div class="html-element" for="{{(index, elem) in htmlElements}}">
<!-- 标题 h1-h6 -->
<div class="elem-block" if="{{elem.type === 'h1'}}">
<text class="text-h1" for="{{part in elem.parts}}">{{part.text}}</text>
</div>
<div class="elem-block" if="{{elem.type === 'h2'}}">
<text class="text-h2" for="{{part in elem.parts}}">{{part.text}}</text>
</div>
<div class="elem-block" if="{{elem.type === 'h3'}}">
<text class="text-h3" for="{{part in elem.parts}}">{{part.text}}</text>
</div>
<div class="elem-block" if="{{elem.type === 'h4'}}">
<text class="text-h4" for="{{part in elem.parts}}">{{part.text}}</text>
</div>
<div class="elem-block" if="{{elem.type === 'h5'}}">
<text class="text-h5" for="{{part in elem.parts}}">{{part.text}}</text>
</div>
<div class="elem-block" if="{{elem.type === 'h6'}}">
<text class="text-h6" for="{{part in elem.parts}}">{{part.text}}</text>
</div>
<!-- 段落 -->
<div class="elem-block" if="{{elem.type === 'p'}}">
<div class="paragraph" for="{{part in elem.parts}}">
<text class="text-normal" if="{{part.type === 'text'}}">{{part.text}}</text>
<text class="text-link" if="{{part.type === 'a'}}" onclick="openLink(part.url)">{{part.text}}</text>
<text class="text-bold" if="{{part.type === 'strong'}}">{{part.text}}</text>
<text class="text-italic" if="{{part.type === 'em'}}">{{part.text}}</text>
</div>
</div>
<!-- 列表项 -->
<div class="elem-block" if="{{elem.type === 'li'}}">
<div class="list-item">
<text class="bullet">•</text>
<div class="list-content" for="{{part in elem.parts}}">
<text class="text-normal" if="{{part.type === 'text'}}">{{part.text}}</text>
<text class="text-link" if="{{part.type === 'a'}}" onclick="openLink(part.url)">{{part.text}}</text>
</div>
</div>
</div>
<!-- 链接按钮 -->
<div class="elem-block" if="{{elem.type === 'a'}}">
<div class="link-button" onclick="openLink(elem.url)">
<text class="button-text">{{elem.text}}</text>
</div>
</div>
<!-- 引用 -->
<div class="elem-block" if="{{elem.type === 'blockquote'}}">
<div class="quote-block" for="{{part in elem.parts}}">
<text class="text-quote" if="{{part.type === 'text'}}">{{part.text}}</text>
<text class="text-link" if="{{part.type === 'a'}}" onclick="openLink(part.url)">{{part.text}}</text>
</div>
</div>
<!-- 分隔线 -->
<div class="elem-block" if="{{elem.type === 'hr'}}">
<div class="divider"></div>
</div>
<!-- 代码 -->
<div class="elem-block" if="{{elem.type === 'pre'}}">
<div class="code-block">
<text class="text-code">{{elem.text}}</text>
</div>
</div>
<!-- 图片 -->
<div class="elem-block" if="{{elem.type === 'img'}}">
<div class="image-container" onclick="viewImage(elem)">
<image class="image-content" src="{{elem.thumbUrl}}" if="{{elem.thumbUrl}}" />
<div class="image-placeholder" else>
<text class="placeholder-text">🖼</text>
</div>
<text class="image-alt" if="{{elem.alt}}">{{elem.alt}}</text>
</div>
</div>
</div>
</div>
</scroll>
</div>
<!-- 底部工具栏 -->
<div class="toolbar">
<div class="tool-btn {{canGoBack ? '' : 'disabled'}}" onclick="goBackHistory">
<text class="tool-icon">&lt;</text>
</div>
<div class="tool-btn {{canGoForward ? '' : 'disabled'}}" onclick="goForward">
<text class="tool-icon">&gt;</text>
</div>
<div class="tool-btn primary" onclick="refreshPage">
<text class="primary-icon">R</text>
</div>
<div class="tool-btn" onclick="savePage">
<text class="tool-icon">*</text>
</div>
<div class="tool-btn" onclick="goHome">
<text class="tool-icon">H</text>
</div>
</div>
<!-- 键盘 -->
<div class="keyboard-overlay" if="{{keyboardVisible}}" onclick="hideKeyboard"></div>
<div class="keyboard" if="{{keyboardVisible}}">
<div class="kb-display">
<text class="kb-input">{{inputText}}_</text>
<div class="kb-delete" onclick="deleteChar">
<text class="kb-del-icon">X</text>
</div>
</div>
<div class="kb-row" for="{{row in keyRows}}">
<text class="kb-key {{k === ' ' ? 'kb-space' : ''}}" for="{{k in row}}" onclick="typeChar(k)">{{k}}</text>
</div>
<div class="kb-row">
<text class="kb-key kb-wide" onclick="typeChar('.')">.</text>
<text class="kb-key" onclick="typeChar('-')">-</text>
<text class="kb-key" onclick="typeChar('/')">/</text>
<text class="kb-key" onclick="typeChar(':')">:</text>
<text class="kb-key kb-space" onclick="typeChar(' ')">空格</text>
</div>
</div>
<!-- 图片预览 -->
<div class="image-modal" if="{{showImageModal}}" onclick="closeImageModal">
<image class="modal-image" src="{{currentImage.thumbUrl}}" />
<text class="modal-url">{{currentImage.url}}</text>
<div class="modal-close" onclick="closeImageModal">
<text class="close-text">关闭</text>
</div>
</div>
<!-- Toast -->
<div class="toast" if="{{showToast}}">
<text class="toast-text">{{toastMsg}}</text>
</div>
</div>
</template>
<script>
import router from "@system.router"
import fetch from "@system.fetch"
import storage from "@system.storage"
import {
decodeHtmlEntities, cleanHtmlText, getBaseUrl, resolveUrl,
preprocessHtml, extractTitle, parseInlineElements,
formatDisplayUrl, normalizeUrl, truncateText
} from "../../common/utils.js"
/**
* HTML解析器
*/
class HtmlParser {
constructor(baseUrl) {
this.baseUrl = baseUrl
this.elements = []
this.foundIndices = new Set()
}
parse(html) {
const content = preprocessHtml(html)
if (!content) return []
this.elements = []
this.foundIndices = new Set()
this.parseHeadings(content)
this.parseImages(content)
this.parseParagraphs(content)
this.parseLists(content)
this.parseBlockquotes(content)
this.parsePreformatted(content)
this.parseHorizontalRules(content)
this.parseDivs(content)
if (this.elements.length === 0) {
this.parseGenericContent(content)
}
return this.elements.sort((a, b) => a._index - b._index)
}
findMatches(content, regex, callback) {
regex.lastIndex = 0
let match
while ((match = regex.exec(content)) !== null) {
if (this.foundIndices.has(match.index)) continue
const result = callback(match)
if (result) {
this.foundIndices.add(match.index)
this.elements.push({ ...result, _index: match.index })
}
}
}
parseHeadings(content) {
for (let i = 1; i <= 6; i++) {
const regex = new RegExp(`<h${i}[^>]*>([\\s\\S]*?)<\\/h${i}>`, 'gi')
this.findMatches(content, regex, (match) => {
const parts = parseInlineElements(match[1], this.baseUrl)
return parts.length > 0 ? { type: `h${i}`, parts } : null
})
}
}
parseParagraphs(content) {
const regex = /<p[^>]*>([\s\S]*?)<\/p>/gi
this.findMatches(content, regex, (match) => {
const parts = parseInlineElements(match[1], this.baseUrl)
return parts.length > 0 ? { type: 'p', parts } : null
})
}
parseLists(content) {
const regex = /<li[^>]*>([\s\S]*?)<\/li>/gi
this.findMatches(content, regex, (match) => {
const parts = parseInlineElements(match[1], this.baseUrl)
return parts.length > 0 ? { type: 'li', parts } : null
})
}
parseBlockquotes(content) {
const regex = /<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi
this.findMatches(content, regex, (match) => {
const parts = parseInlineElements(match[1], this.baseUrl)
return parts.length > 0 ? { type: 'blockquote', parts } : null
})
}
parsePreformatted(content) {
const regex = /<pre[^>]*>([\s\S]*?)<\/pre>/gi
this.findMatches(content, regex, (match) => {
const text = cleanHtmlText(match[1])
return text ? { type: 'pre', text } : null
})
}
parseHorizontalRules(content) {
const regex = /<hr[^>]*\/?>/gi
this.findMatches(content, regex, () => ({ type: 'hr' }))
}
parseDivs(content) {
const regex = /<div[^>]*>([\s\S]*?)<\/div>/gi
this.findMatches(content, regex, (match) => {
const innerContent = match[1]
if (/<[a-z][^>]*>/i.test(innerContent)) return null
const text = cleanHtmlText(innerContent)
return text && text.length > 0 ? { type: 'p', parts: [{ type: 'text', text }] } : null
})
}
parseImages(content) {
const patterns = [
/<img[^>]*src=["']([^"']+)["'][^>]*alt=["']([^"]*)["'][^>]*\/?>/gi,
/<img[^>]*alt=["']([^"]*)["'][^>]*src=["']([^"']+)["'][^>]*\/?>/gi,
/<img[^>]*src=["']([^"']+)["'][^>]*\/?>/gi
]
patterns.forEach((regex, idx) => {
this.findMatches(content, regex, (match) => {
const src = idx === 1 ? match[2] : match[1]
const alt = idx === 1 ? match[1] : (match[2] || '')
return {
type: 'img',
url: resolveUrl(src, this.baseUrl),
thumbUrl: resolveUrl(src, this.baseUrl),
alt: decodeHtmlEntities(alt)
}
})
})
}
parseGenericContent(content) {
const textRegex = />([^<]{20,})</g
this.findMatches(content, textRegex, (match) => {
const text = cleanHtmlText(match[1])
return text && text.length >= 20 ? { type: 'p', parts: [{ type: 'text', text }] } : null
})
}
}
export default {
protected: { url: '', tempId: '' },
private: {
currentUrl: '',
pageTitle: '',
htmlElements: [],
historyUrls: [],
historyIndex: -1,
isLoading: false,
errorMsg: '',
inputText: '',
keyboardVisible: false,
keyRows: [
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'],
['Z', 'X', 'C', 'V', 'B', 'N', 'M']
],
showImageModal: false,
currentImage: null,
showToast: false,
toastMsg: ''
},
computed: {
displayUrl() { return formatDisplayUrl(this.currentUrl) },
canGoBack() { return this.historyIndex > 0 },
canGoForward() { return this.historyIndex < this.historyUrls.length - 1 }
},
onInit() {
console.log('Index onInit, url:', this.url, 'tempId:', this.tempId)
},
onReady() {
console.log('Index onReady, url:', this.url, 'tempId:', this.tempId)
if (this.tempId) {
this.loadOfflineData(this.tempId)
} else if (this.url) {
this.inputText = this.url
this.loadUrl(this.url)
}
},
loadOfflineData(tempId) {
storage.get({
key: tempId,
success: (data) => {
try {
const offline = JSON.parse(data)
this.pageTitle = offline.title || ''
this.currentUrl = offline.url || ''
this.inputText = offline.url || ''
this.htmlElements = offline.elements || []
storage.delete({ key: tempId })
} catch (e) {
this.errorMsg = '加载失败'
}
},
fail: () => this.errorMsg = '加载失败'
})
},
goBack() { router.back() },
goHome() { router.push({ uri: 'pages/home' }) },
goBackHistory() {
if (this.historyIndex > 0) {
this.historyIndex--
this.loadUrl(this.historyUrls[this.historyIndex])
} else {
router.back()
}
},
goForward() {
if (this.historyIndex < this.historyUrls.length - 1) {
this.historyIndex++
this.loadUrl(this.historyUrls[this.historyIndex])
}
},
showKeyboard() { this.keyboardVisible = true },
hideKeyboard() { this.keyboardVisible = false },
typeChar(char) { this.inputText += char },
deleteChar() { this.inputText = this.inputText.slice(0, -1) },
confirmInput() {
const url = normalizeUrl(this.inputText)
if (!url) {
this.showToast('请输入有效网址')
return
}
this.keyboardVisible = false
this.loadUrl(url)
},
loadUrl(url) {
this.currentUrl = url
this.isLoading = true
this.errorMsg = ''
if (this.historyIndex < this.historyUrls.length - 1) {
this.historyUrls = this.historyUrls.slice(0, this.historyIndex + 1)
}
this.historyUrls.push(url)
this.historyIndex = this.historyUrls.length - 1
fetch.fetch({
url,
method: 'GET',
responseType: 'text',
success: (res) => this.handleSuccess(res),
fail: () => this.handleError()
})
},
handleSuccess(response) {
if (response.code === 200 && response.data) {
this.parseHtml(response.data)
} else {
this.errorMsg = `错误: ${response.code}`
}
this.isLoading = false
},
handleError() {
this.errorMsg = '网络错误'
this.isLoading = false
},
parseHtml(html) {
this.pageTitle = extractTitle(html) || this.displayUrl
const parser = new HtmlParser(getBaseUrl(this.currentUrl))
this.htmlElements = parser.parse(html)
if (this.htmlElements.length === 0) {
this.errorMsg = '页面无内容'
}
},
refreshPage() {
if (this.currentUrl) this.loadUrl(this.currentUrl)
},
openLink(url) { if (url) this.loadUrl(url) },
viewImage(elem) {
this.currentImage = elem
this.showImageModal = true
},
closeImageModal() {
this.showImageModal = false
this.currentImage = null
},
savePage() {
if (!this.currentUrl || this.htmlElements.length === 0) {
this.showToast('无内容可保存')
return
}
const safeElements = this.htmlElements.slice(0, 100).map(e => ({
type: e.type,
parts: e.parts,
text: e.text,
url: e.url,
thumbUrl: e.thumbUrl,
alt: e.alt
}))
const pageData = {
id: Date.now().toString(),
title: truncateText(this.pageTitle, 50),
url: this.currentUrl,
elements: safeElements,
saveTime: new Date().toLocaleString()
}
let jsonStr
try {
jsonStr = JSON.stringify(pageData)
if (jsonStr.length > 100000) {
this.showToast('页面过大')
return
}
} catch (e) {
this.showToast('保存失败')
return
}
storage.get({
key: 'offline_pages',
success: (data) => {
let pages = []
try { pages = JSON.parse(data || '[]') } catch (e) {}
if (pages.length >= 20) pages = pages.slice(0, 19)
pages.unshift(pageData)
storage.set({
key: 'offline_pages',
value: JSON.stringify(pages),
success: () => this.showToast('保存成功'),
fail: () => this.showToast('保存失败')
})
},
fail: () => {
storage.set({
key: 'offline_pages',
value: JSON.stringify([pageData]),
success: () => this.showToast('保存成功'),
fail: () => this.showToast('保存失败')
})
}
})
},
showToast(msg) {
this.toastMsg = msg
this.showToast = true
setTimeout(() => this.showToast = false, 2000)
}
}
</script>
<style>
.page {
flex-direction: column;
height: 480px;
background-color: #000;
}
.navbar {
height: 44px;
flex-direction: row;
align-items: center;
padding: 0 8px;
background-color: #1c1c1e;
}
.nav-back, .nav-go {
width: 32px;
height: 32px;
border-radius: 16px;
background-color: #2c2c2e;
justify-content: center;
align-items: center;
}
.nav-go { background-color: #0a84ff; }
.icon-back { color: #0a84ff; font-size: 16px; }
.icon-go { color: #fff; font-size: 16px; }
.nav-input {
flex: 1;
height: 32px;
margin: 0 8px;
padding: 0 12px;
border-radius: 16px;
background-color: #2c2c2e;
justify-content: center;
}
.input-text {
color: #fff;
font-size: 13px;
lines: 1;
text-overflow: ellipsis;
}
.input-placeholder {
color: #8e8e93;
font-size: 13px;
}
.content-area {
flex: 1;
background-color: #000;
}
.web-content {
height: 392px;
padding: 12px;
}
.content-inner {
flex-direction: column;
}
.page-title {
color: #0a84ff;
font-size: 16px;
font-weight: bold;
margin-bottom: 12px;
lines: -1;
}
.html-element {
margin-bottom: 8px;
}
.elem-block {
flex-direction: column;
}
.text-h1, .text-h2, .text-h3, .text-h4, .text-h5, .text-h6 {
color: #0a84ff;
font-weight: bold;
lines: -1;
}
.text-h1 { font-size: 18px; }
.text-h2 { font-size: 16px; }
.text-h3 { font-size: 15px; }
.text-h4 { font-size: 14px; }
.text-h5 { font-size: 13px; }
.text-h6 { font-size: 12px; }
.paragraph {
flex-direction: column;
}
.text-normal, .text-link, .text-bold, .text-italic {
font-size: 13px;
line-height: 20px;
lines: -1;
}
.text-normal { color: #c9d1d9; }
.text-link { color: #0a84ff; }
.text-bold { color: #c9d1d9; font-weight: bold; }
.text-italic { color: #c9d1d9; font-style: italic; }
.list-item {
flex-direction: row;
margin-bottom: 4px;
padding-left: 8px;
}
.bullet {
color: #8b949e;
font-size: 13px;
margin-right: 8px;
}
.list-content {
flex-direction: column;
flex: 1;
}
.link-button {
background-color: #21262d;
border-radius: 8px;
padding: 10px 12px;
margin: 6px 0;
}
.button-text {
color: #0a84ff;
font-size: 13px;
}
.quote-block {
border-left-width: 3px;
border-left-color: #30363d;
padding-left: 12px;
margin: 8px 0;
background-color: #161b22;
}
.text-quote {
color: #8b949e;
font-size: 13px;
font-style: italic;
lines: -1;
}
.divider {
height: 1px;
background-color: #30363d;
margin: 12px 0;
}
.code-block {
background-color: #161b22;
border-radius: 6px;
padding: 8px;
margin: 8px 0;
}
.text-code {
color: #c9d1d9;
font-size: 11px;
font-family: monospace;
lines: -1;
}
.image-container {
margin: 8px 0;
}
.image-content {
width: 100%;
border-radius: 8px;
}
.image-placeholder {
height: 80px;
background-color: #21262d;
border-radius: 8px;
justify-content: center;
align-items: center;
}
.placeholder-text {
color: #484f58;
font-size: 24px;
}
.image-alt {
color: #8b949e;
font-size: 11px;
margin-top: 4px;
}
.status-loading, .status-error, .status-empty {
flex: 1;
justify-content: center;
align-items: center;
}
.spinner {
width: 40px;
height: 40px;
border-radius: 20px;
border-width: 3px;
border-color: #3a3a3c;
border-top-color: #0a84ff;
}
.loading-text {
color: #8e8e93;
font-size: 14px;
margin-top: 12px;
}
.error-icon {
color: #ff453a;
font-size: 48px;
margin-bottom: 12px;
}
.error-text {
color: #8e8e93;
font-size: 14px;
margin-bottom: 16px;
}
.btn-retry {
padding: 10px 24px;
border-radius: 20px;
background-color: #0a84ff;
}
.btn-text {
color: #fff;
font-size: 14px;
}
.empty-icon {
color: #3a3a3c;
font-size: 48px;
margin-bottom: 16px;
}
.empty-title {
color: #8e8e93;
font-size: 18px;
margin-bottom: 8px;
}
.empty-desc {
color: #48484a;
font-size: 14px;
}
.toolbar {
height: 44px;
flex-direction: row;
align-items: center;
justify-content: space-around;
padding: 0 8px;
background-color: #1c1c1e;
}
.tool-btn {
width: 36px;
height: 36px;
border-radius: 18px;
background-color: #2c2c2e;
justify-content: center;
align-items: center;
}
.tool-btn.disabled { opacity: 0.3; }
.tool-btn.primary { background-color: #0a84ff; }
.tool-icon {
color: #0a84ff;
font-size: 18px;
}
.primary-icon {
color: #fff;
font-size: 18px;
}
.keyboard-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 44px;
background-color: rgba(0, 0, 0, 0.5);
}
.keyboard {
position: fixed;
bottom: 44px;
left: 0;
right: 0;
background-color: #1c1c1e;
padding: 8px;
flex-direction: column;
}
.kb-display {
height: 36px;
flex-direction: row;
align-items: center;
padding: 0 8px;
margin-bottom: 8px;
background-color: #2c2c2e;
border-radius: 8px;
}
.kb-input {
flex: 1;
color: #0a84ff;
font-size: 14px;
}
.kb-delete {
width: 28px;
height: 28px;
border-radius: 14px;
background-color: #ff453a;
justify-content: center;
align-items: center;
}
.kb-del-icon {
color: #fff;
font-size: 14px;
}
.kb-row {
flex-direction: row;
justify-content: center;
margin-bottom: 4px;
}
.kb-key {
width: 30px;
height: 36px;
margin: 0 2px;
border-radius: 6px;
background-color: #2c2c2e;
justify-content: center;
align-items: center;
color: #fff;
font-size: 14px;
}
.kb-wide { width: 50px; }
.kb-space { width: 70px; }
.image-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.95);
justify-content: center;
align-items: center;
}
.modal-image {
width: 100%;
max-height: 350px;
}
.modal-url {
color: #8b949e;
font-size: 11px;
margin-top: 12px;
max-width: 280px;
text-align: center;
}
.modal-close {
margin-top: 20px;
padding: 10px 24px;
border-radius: 8px;
background-color: #0a84ff;
}
.close-text {
color: #fff;
font-size: 14px;
}
.toast {
position: fixed;
top: 100px;
left: 68px;
width: 200px;
height: 36px;
border-radius: 8px;
background-color: rgba(48, 209, 88, 0.95);
justify-content: center;
align-items: center;
}
.toast-text {
color: #fff;
font-size: 13px;
}
</style>
+290
View File
@@ -0,0 +1,290 @@
<template>
<div class="page">
<div class="header">
<div class="back-btn" onclick="goBack">
<text class="back-icon">&lt;</text>
</div>
<text class="title">离线页面</text>
<div class="edit-btn" if="{{pages.length > 0}}" onclick="toggleEdit">
<text class="edit-text">{{isEditing ? '完成' : '编辑'}}</text>
</div>
</div>
<scroll class="content" scroll-y="true" if="{{pages.length > 0}}">
<div class="list">
<div class="item" for="{{(index, item) in pages}}">
<div class="delete-btn" if="{{isEditing}}" onclick="deletePage($idx)">
<text class="delete-icon">−</text>
</div>
<div class="item-content" onclick="openPage($idx)">
<div class="item-info">
<text class="item-title">{{item.title || '无标题'}}</text>
<text class="item-url">{{formatUrl(item.url)}}</text>
<div class="item-meta">
<text class="meta-text">{{item.saveTime}}</text>
<text class="meta-text" if="{{item.elements}}">{{item.elements.length}} 项</text>
</div>
</div>
<text class="item-arrow" if="{{!isEditing}}">›</text>
</div>
</div>
</div>
</scroll>
<div class="empty" if="{{pages.length === 0}}">
<text class="empty-icon">=</text>
<text class="empty-text">暂无离线页面</text>
<text class="empty-desc">浏览网页时选择"保存离线页面"</text>
</div>
<div class="toast" if="{{showToast}}">
<text class="toast-text">{{toastMsg}}</text>
</div>
</div>
</template>
<script>
import router from "@system.router"
import storage from "@system.storage"
import { getStorageData, setStorageData, formatDisplayUrl, truncateText } from "../../common/utils.js"
export default {
private: {
pages: [],
isEditing: false,
showToast: false,
toastMsg: ''
},
onShow() {
this.loadPages()
},
async loadPages() {
const data = await getStorageData(storage, 'offline_pages')
this.pages = data.map(p => ({
...p,
title: truncateText(p.title, 30)
}))
},
formatUrl(url) {
return formatDisplayUrl(url)
},
openPage(index) {
if (this.isEditing) return
const page = this.pages[index]
if (!page) return
if (page.elements && page.elements.length > 0) {
const tempId = 'offline_' + Date.now()
const offlineData = {
title: page.title,
url: page.url,
elements: page.elements
}
storage.set({
key: tempId,
value: JSON.stringify(offlineData),
success: () => {
router.push({ uri: 'pages/index', params: { tempId } })
}
})
} else if (page.url) {
router.push({ uri: 'pages/index', params: { url: page.url } })
}
},
toggleEdit() {
this.isEditing = !this.isEditing
},
async deletePage(index) {
const item = this.pages[index]
this.pages.splice(index, 1)
const success = await setStorageData(storage, 'offline_pages', this.pages)
if (success) {
this.showTip('已删除')
if (this.pages.length === 0) {
this.isEditing = false
}
} else {
this.showTip('删除失败')
this.pages.splice(index, 0, item)
}
},
showTip(msg) {
this.toastMsg = msg
this.showToast = true
setTimeout(() => this.showToast = false, 2000)
},
goBack() {
router.back()
}
}
</script>
<style>
.page {
flex-direction: column;
height: 480px;
background-color: #000;
}
.header {
height: 44px;
flex-direction: row;
align-items: center;
padding: 0 10px;
background-color: #1c1c1e;
}
.back-btn {
width: 32px;
height: 32px;
border-radius: 16px;
background-color: #2c2c2e;
justify-content: center;
align-items: center;
}
.back-icon {
color: #0a84ff;
font-size: 18px;
}
.title {
flex: 1;
text-align: center;
color: #fff;
font-size: 17px;
font-weight: 600;
}
.edit-btn {
padding: 6px 12px;
border-radius: 6px;
background-color: #2c2c2e;
}
.edit-text {
color: #0a84ff;
font-size: 14px;
}
.content {
height: 436px;
}
.list {
padding: 8px;
}
.item {
flex-direction: row;
align-items: center;
margin-bottom: 6px;
border-radius: 12px;
background-color: #1c1c1e;
}
.delete-btn {
width: 28px;
height: 28px;
border-radius: 14px;
background-color: #ff453a;
justify-content: center;
align-items: center;
margin-left: 12px;
}
.delete-icon {
color: #fff;
font-size: 18px;
font-weight: bold;
}
.item-content {
flex: 1;
flex-direction: row;
align-items: center;
padding: 12px;
}
.item-info {
flex: 1;
flex-direction: column;
}
.item-title {
color: #fff;
font-size: 15px;
font-weight: 500;
}
.item-url {
color: #8e8e93;
font-size: 11px;
margin-top: 2px;
}
.item-meta {
flex-direction: row;
margin-top: 4px;
}
.meta-text {
color: #48484a;
font-size: 10px;
margin-right: 8px;
}
.item-arrow {
color: #48484a;
font-size: 18px;
}
.empty {
flex: 1;
justify-content: center;
align-items: center;
}
.empty-icon {
color: #3a3a3c;
font-size: 48px;
margin-bottom: 12px;
}
.empty-text {
color: #8e8e93;
font-size: 16px;
margin-bottom: 8px;
}
.empty-desc {
color: #48484a;
font-size: 13px;
}
.toast {
position: fixed;
bottom: 80px;
left: 50%;
transform: translateX(-50%);
padding: 10px 20px;
border-radius: 20px;
background-color: rgba(48, 209, 88, 0.95);
}
.toast-text {
color: #000;
font-size: 14px;
}
</style>