Ufi-tools_plugins_2026_4_11_14_55_56.txt

<!-- [KANO_PLUGIN_START] 【请将插件置顶】插件收纳箱.txt -->
//<script>
(function () {
let count_down = 5
let interval = null
const toolBox = document.createElement('div');
toolBox.innerHTML = `
<div class="modal" id="pluginToolsModal" style="width: 77%;max-width: 500px;display: none;z-index: 0;">
<div class="title">插件收纳箱 <span class="countDown" style="font-size:.64rem;opacity:.8"></span></div>
<style>
.kano_cjsnx {
display: grid;
grid-template-columns: repeat(4, 1fr); /* PC 默认 4 列 */
max-height: 350px;
padding: 10px 0px 10px 0px;
gap: 10px;
}
.kano_cjsnx button {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
@media (max-width: 360px) {
.kano_cjsnx {
grid-template-columns: repeat(1, 1fr);
}
}
@media (min-width: 361px) and (max-width: 480px) {
.kano_cjsnx {
grid-template-columns: repeat(2, 1fr);
}
}
@media (min-width: 481px) and (max-width: 767px) {
.kano_cjsnx {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 768px) {
.kano_cjsnx {
grid-template-columns: repeat(4, 1fr);
}
}</style>
<div class="content kano_cjsnx">
</div>
<div class="btn" style="text-align: right;">
<button onclick="closeModal('#pluginToolsModal')">关闭</button>
</div>
</div>
`;

const menuSection = document.body;
menuSection.appendChild(toolBox);

const resetInterval = () => {
interval && interval()
count_down = 5
let el = document.querySelector("#pluginToolsModal .countDown")
if (el) {
el.textContent = ``
}
}

const btn = document.createElement("button")
btn.innerHTML = "插件收纳箱"
btn.onclick = () => {
resetInterval()
showModal("#pluginToolsModal")
}
document.querySelector(".functions-container .actions-buttons").appendChild(btn)
const countDownEl = document.querySelector("#pluginToolsModal .countDown")

document.querySelector('#pluginToolsModal .content').onclick = (e) => {
const target = e.target;
if (target == e.currentTarget) return;
resetInterval()
interval = requestInterval(() => {
count_down--
if (count_down <= 0) {
count_down = 5
if (countDownEl) {
countDownEl.textContent = ``
}
closeModal('#pluginToolsModal');
resetInterval()
} else {
if (countDownEl) {
countDownEl.textContent = `自动关闭:${count_down}S`
}
}
}, 1000)
}


// 重定向按钮添加到正确容器
const originalAppendChild = HTMLElement.prototype.appendChild;
HTMLElement.prototype.appendChild = function (element) {
if (this === collapseBtn_menu?.nextElementSibling?.querySelector('.collapse_box') &&
element.tagName === 'BUTTON') {
return document.querySelector('#pluginToolsModal .content').appendChild(element);
}
return originalAppendChild.call(this, element);
};


const originalCloseModal = closeModal;
//检测到插件有唤起modal时,关闭收纳箱modal
closeModal = (...args) => {
let res = originalCloseModal(...args);
if (args[0] !== '#pluginToolsModal') {
resetInterval()
}
return res
}

const HOOK_SKIP = new Set(['#pluginToolsModal', '#PluginModal', '#plugin_store']);

(function hookShowModalOnce() {
if (showModal.__hooked__) return;
const original = showModal;

showModal = function (...args) {
const target = args[0];
if (!HOOK_SKIP.has(target)) {
resetInterval();
closeModal('#pluginToolsModal');
}
return original.apply(this, args);
};

showModal.__hooked__ = true;
})();
})();
//</script >
<!-- [KANO_PLUGIN_END] 【请将插件置顶】插件收纳箱.txt -->



<!-- [KANO_PLUGIN_START] 直供电模式开关.txt -->
<script>
(async () => {
const checkAdvanceFunc = async () => {
const res = await runShellWithRoot('whoami')
if (res.content) {
if (res.content.includes('root')) {
return true
}
}
return false
}

const toggleCharge = async (flag) => {
if (!checkAdvanceFunc()) {
return createToast("没有开启高级功能,无法使用!")
}
await runShellWithRoot(`
echo ${flag ? "1" : "0"} > /sys/class/power_supply/interface/battery_charging_enabled
echo ${flag ? "1" : "0"} > /sys/class/zte_power_supply/zte_battery/battery_charging_enabled
echo ${flag ? "1" : "0"} > /sys/class/power_supply/battery/battery_charging_enabled
`)
}

const btn = document.createElement('button')
btn.textContent = "直供电模式"
btn.onclick = async (e) => {
const target = e.target
const disabled = target.dataset.enable == "1" ? false : true
await toggleCharge(!disabled)
target.dataset.enable = disabled ? "1" : "0"
target.style.background = disabled ? "var(--dark-btn-color-active)" : ""
createToast("修改成功!")
}

document.querySelector('.actions-buttons').appendChild(btn)

let timer = null
let counter = 0
timer = setInterval(async () => {
counter++
if (counter >= 6) {
clearInterval(timer)
}
try {
if (await checkAdvanceFunc()) {
let res = await runShellWithRoot(`
timeout 2s awk \'{print}\' /sys/class/power_supply/interface/battery_charging_enabled
timeout 2s awk \'{print}\' /sys/class/zte_power_supply/zte_battery/battery_charging_enabled
timeout 2s awk \'{print}\' /sys/class/power_supply/battery/battery_charging_enabled
`)

if (res.content.includes("0")) {
btn.dataset.enable = "1"
btn.style.background = "var(--dark-btn-color-active)"
} else {
btn.dataset.enable = "0"
btn.style.background = ""
}
clearInterval(timer)
} else {
clearInterval(timer)
}
} catch {

}
}, 2000);

})()
</script>
<!-- [KANO_PLUGIN_END] 直供电模式开关.txt -->



<!-- [KANO_PLUGIN_START] 充电控制器终极版.js -->
<script>
(async () => {
const CONFIG_FILE = "/sdcard/kano_charge_control_config.conf"
const SH_FILE = "/sdcard/kano_charge_control.sh"
const LOG_FILE = "/sdcard/kano_charge_control_log.log"
const BOOT_SH_FILE = "/sdcard/ufi_tools_boot.sh"
const NAME = "kano_charge_control"
//配置
let CONFIG = {
enabled: false,
max_charge: 80,
start_charge: 20
}

const SCRIPT_CONTENT = `#!/system/bin/sh
CONFIG_FILE="${CONFIG_FILE}"
LOG_FILE="${LOG_FILE}"
CHECK_INTERVAL=5 # 定时检测电量的间隔(秒)

CHARGE_PATHS=(
"/sys/class/power_supply/interface/battery_charging_enabled"
"/sys/class/zte_power_supply/zte_battery/battery_charging_enabled"
"/sys/class/power_supply/battery/battery_charging_enabled"
)

BATTERY_STATUS_PATH="/sys/class/power_supply/battery/status"
CAPACITY_PATH="/sys/class/power_supply/battery/capacity"

rm -rf "$LOG_FILE"

is_magisk_env() {
[ -d /sbin/.magisk ] || command -v magisk >/dev/null
}

log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $1" >> "$LOG_FILE"
}

get_current_switch_state() {
for path in "\${CHARGE_PATHS[@]}"; do
if [ -r "$path" ]; then
state=$(cat "$path" 2>/dev/null)
if [ "$state" = "0" ] || [ "$state" = "1" ]; then
echo "$state"
return
fi
fi
done
echo "-1"
}

get_physical_charging_status() {
if [ -r "$BATTERY_STATUS_PATH" ]; then
status=$(cat "$BATTERY_STATUS_PATH" 2>/dev/null)
case "$status" in
"Charging"|"Full") echo "1" ;;
"Discharging") echo "0" ;;
*) echo "-1" ;;
esac
else
echo "-1"
fi
}

apply_switch_state() {
target="$1"
for path in "\${CHARGE_PATHS[@]}"; do
if [ -w "$path" ]; then
echo "$target" > "$path"
sleep 0.1
if [ "$(cat "$path" 2>/dev/null)" = "$target" ]; then
log "充电开关已设置为 $target"
return
fi
fi
done
log "未能成功设置充电开关为 $target"
}

handle_charging_logic() {
if [ ! -f "$CONFIG_FILE" ]; then
log "配置文件不存在:$CONFIG_FILE"
return
fi

MAX_CHARGE=$(grep '^max_charge=' "$CONFIG_FILE" | cut -d= -f2)
START_CHARGE=$(grep '^start_charge=' "$CONFIG_FILE" | cut -d= -f2)

if [ -z "$MAX_CHARGE" ] || [ -z "$START_CHARGE" ]; then
log "配置文件格式错误"
return
fi

if [ ! -f "$CAPACITY_PATH" ]; then
log "无法读取电池容量"
return
fi

CAPACITY=$(cat "$CAPACITY_PATH")
CURRENT_SWITCH_STATE=$(get_current_switch_state)
CURRENT_PHYSICAL_CHARGING=$(get_physical_charging_status)

TARGET_SWITCH_STATE=$CURRENT_SWITCH_STATE

# 区间逻辑
if [ "$CAPACITY" -ge "$MAX_CHARGE" ]; then
TARGET_SWITCH_STATE=0 # 强制关闭
elif [ "$CAPACITY" -ge "$START_CHARGE" ] && [ "$CAPACITY" -lt "$MAX_CHARGE" ]; then
TARGET_SWITCH_STATE=1 # 区间内强制开启
else
TARGET_SWITCH_STATE=$CURRENT_SWITCH_STATE # 低于 start_charge,不干预
fi

# 插入充电器时,在区间内允许充电
if [ "$LAST_PHYSICAL_CHARGING" = "0" ] && [ "$CURRENT_PHYSICAL_CHARGING" = "1" ]; then
log "检测到插入充电器,电量 $CAPACITY%"
if [ "$CAPACITY" -gt "$START_CHARGE" ] && [ "$CAPACITY" -lt "$MAX_CHARGE" ]; then
TARGET_SWITCH_STATE=1
fi
fi

if [ "$TARGET_SWITCH_STATE" != "$CURRENT_SWITCH_STATE" ] && [ "$TARGET_SWITCH_STATE" != "-1" ]; then
if [ "$TARGET_SWITCH_STATE" = "0" ]; then
# 关闭充电的原因
if [ "$CAPACITY" -ge "$MAX_CHARGE" ]; then
REASON="电量 $CAPACITY% >= 最大 $MAX_CHARGE%,强制关闭充电"
else
REASON="其他原因触发关闭充电"
fi
log "准备关闭充电:$REASON"
else
# 开启充电的原因
if [ "$CAPACITY" -ge "$START_CHARGE" ] && [ "$CAPACITY" -lt "$MAX_CHARGE" ]; then
REASON="电量 $CAPACITY% 在区间 $START_CHARGE%~$MAX_CHARGE%,强制开启充电"
else
REASON="其他原因触发开启充电"
fi
log "准备开启充电:$REASON"
fi
apply_switch_state "$TARGET_SWITCH_STATE"
fi

LAST_PHYSICAL_CHARGING=$CURRENT_PHYSICAL_CHARGING
}

LAST_PHYSICAL_CHARGING=$(get_physical_charging_status)
log "充电控制脚本启动..."

# 启动 inotifyd 监听
inotify_callback() {
log "检测到充电状态文件变化"
handle_charging_logic
}

# 创建 FIFO 路径
TMP_DIR="/data/local/tmp"
FIFO="$TMP_DIR/inotify_pipe"
rm -f "$FIFO"
mkfifo "$FIFO"

# 启动 inotifyd,根据环境选择参数
if is_magisk_env; then
log "检测到 Magisk 环境,正常启动"
# Magisk busybox inotifyd 用法: inotifyd <handler> <file>:<events>
inotifyd /system/bin/sh -c "echo event >> $FIFO" "$BATTERY_STATUS_PATH:m" &
else
log "非 Magisk 环境,正常启动"
inotifyd "$FIFO":"$BATTERY_STATUS_PATH" &
fi

# 读取 FIFO,文件变化时触发回调
(
while read -r event; do
inotify_callback
done < "$FIFO"
) &

# 定时检测
while true; do
handle_charging_logic
sleep "$CHECK_INTERVAL"
done`

const html = `
<div class="title" style="justify-content:space-between">
<span>充电控制器</span>
<button onclick="showHelp()" style="border-radius: 50%">?</button>
</div>
<div class="content" style="max-height: 90%;font-size:14px;overflow-y: scroll; padding-top: 10px;">
<span>总开关:</span>
<div style="margin-top: 8px;display: inline-block;" id="collapse_charge_plugin_btn"></div>
<div id="collapse_charge_plugin" class="collapse" data-name="close" style="height: 0px; overflow: auto;">
<div class="collapse_box" style="overflow:hidden">
<div style="margin: 10px 0;display:flex;justify-content:space-between;align-items:center">
<span style="min-width: 6em;">触发充电(%):</span><input id="start_charge_val" style="flex:1;width:100%" type="range" id="charge-threshold" min="1" max="100" value="80"><span id="start_charge_label" style="min-width: 4em">20 %</span>
</div>
<div style="margin: 10px 0;display:flex;justify-content:space-between;align-items:center">
<span style="min-width: 6em;">停止充电(%):</span><input id="stop_charge_val" style="flex:1;width:100%" type="range" id="charge-threshold" min="1" max="100" value="20"><span id="stop_charge_label" style="min-width: 4em">20 %</span>
</div>
<div style="display:flex;gap:10px;margin-bottom:10px">
<button onclick="submit_charge_settings()" style="flex:1" data-i18n="submit_btn">提交</button>
<button onclick="enable_charge()">启用充电</button>
<button onclick="disable_charge()">禁用充电</button>
</div>

<div style="box-sizing:border-box">
<div class="title" style="font-size:14px;margin-bottom:10px">日志</div>
<textarea id="charger_log" disabled style="margin-bottom:10px;border:none;box-sizing:border-box;width:100%;min-height:100px"></textarea>
</div>
</div>
</div>
</div>
<div class="btn" style="text-align: right;">
<button type="button" onclick="close_charge_settings()" data-i18n="close_btn">关闭</button>
</div>
`

//检查高级功能是否开启
const checkRoot = async () => {
try {
const res = await runShellWithRoot('whoami');
return res.success && res.content.includes('root');
} catch {
return false;
}
};


//上传脚本文件,移动到机内目标目录
const uploadFile = async (filename, content, destPath) => {
try {
const file = new File([content], filename, { type: "text/plain" });
const formData = new FormData();
formData.append("file", file);

const uploadRes = await (await fetch(`${KANO_baseURL}/upload_img`, {
method: "POST",
headers: common_headers,
body: formData,
})).json();

if (uploadRes.url) {
const tempPath = `/data/data/com.minikano.f50_sms/files${uploadRes.url}`;
const moveRes = await runShellWithRoot(`mv ${tempPath} ${destPath}`);
if (moveRes.success) {
return true;
} else { throw new Error(`移动文件失败: ${moveRes.content}`); }
} else { return false; }
} catch (e) {
return false;
}
};

// 切换充电状态
const toggleCharge = async (enableCharge) => {
try {
const flag = enableCharge ? "1" : "0";
const res = await runShellWithRoot(`
echo ${flag} > /sys/class/power_supply/interface/battery_charging_enabled
echo ${flag} > /sys/class/zte_power_supply/zte_battery/battery_charging_enabled
echo ${flag} > /sys/class/power_supply/battery/battery_charging_enabled
`);
return res.success;
} catch (e) {
return false;
}
};

//获取设置
const getConfig = async () => {
if (!(await checkRoot())) {
return false;
}
const res = await runShellWithRoot(`timeout 2s awk \'{print}\' ${CONFIG_FILE}`);
const res1 = await runShellWithRoot(`timeout 2s awk \'{print}\' ${BOOT_SH_FILE}`);
if (res.success) {
const configText = res.content
const maxMatch = configText.match(/max_charge=(\d+)/);
const startMatch = configText.match(/start_charge=(\d+)/);

const maxCharge = maxMatch ? parseInt(maxMatch[1], 10) : null;
const startCharge = startMatch ? parseInt(startMatch[1], 10) : null;

if (maxCharge) {
CONFIG.max_charge = maxCharge
}
if (startCharge) {
CONFIG.start_charge = startCharge
}
}
if (res1) {
let enabled = res1.content.includes(NAME)
CONFIG.enabled = enabled
localStorage.setItem('collapse_charge_plugin', enabled ? 'open' : 'close')
}
}


//获取日志
let oldLog = null
const getLog = async () => {
if (!(await checkRoot())) {
return false;
}
const charger_log = document.querySelector('#charger_log')
if (charger_log) {
let res = await runShellWithRoot(`timeout 2s awk \'{print}\' ${LOG_FILE}`);
if ((res.content != oldLog) || (oldLog == null)) {
setTimeout(() => {
// 滚动到最底部
charger_log.scrollTo({
top: charger_log.scrollHeight,
behavior: "smooth"
})
}, 100);
}
oldLog = res.content
charger_log.value = res.content
}
}

//杀死指定进程
const killProcessByName = async (processName) => {
const psResult = await runShellWithRoot(`ps -ef | grep "${processName}" | grep -v grep`);
const lines = psResult.content.trim().split('\n');

if (lines.length === 0 || (lines.length === 1 && lines[0].trim() === '')) {
return {
success: false,
content: "未找到相关进程"
};
}

let killed = 0;

for (const line of lines) {
const parts = line.trim().split(/\s+/);
const pid = parts[1];
const name = parts.slice(2).join(' ');
if (pid && /^\d+$/.test(pid)) {
const res = await runShellWithRoot(`kill ${pid}`);
killed++;
}
}

if (killed === 0) {
return {
success: false,
content: "未找到可杀死的进程"
};
} else {
return {
success: true,
content: `已杀死 ${killed} 个进程`
};
}
};

//设置阈值
const setRange = async () => {
if (!(await checkRoot())) {
createToast("没有开启高级功能,无法使用!", "red");
return false;
}
//清除日志
await runShellWithRoot(`timeout 2s echo "max_charge=${CONFIG.max_charge}\nstart_charge=${CONFIG.start_charge}" > ${CONFIG_FILE}`);
const charger_log = document.querySelector('#charger_log')
await killProcessByName(NAME)
await runShellWithRoot(`/system/bin/sh ${SH_FILE} &`)
createToast("设置成功,等待日志刷新...")
}

//卸载
const uninstall = async () => {
if (!(await checkRoot())) {
createToast("没有开启高级功能,无法使用!", "red");
return false;
}
await runShellWithRoot(`sed -i '/kano_charge_control/d' /sdcard/ufi_tools_boot.sh`)
await runShellWithRoot(`rm -rf ${CONFIG_FILE}`)
await runShellWithRoot(`rm -rf ${SH_FILE}`)
await runShellWithRoot(`rm -rf ${LOG_FILE}`)
await killProcessByName(NAME)
createToast("正在恢复充电模式")
await toggleCharge(true)
createToast("充电模式已恢复")
return createToast("脚本已停用")
}

//安装
const install = async () => {
if (!(await checkRoot())) {
createToast("没有开启高级功能,无法使用!", "red");
return false;
}
//上传
if (!await uploadFile("kano_charge_control.sh", SCRIPT_CONTENT, SH_FILE)) {
return createToast("传输文件失败!", "red")
}
await runShellWithRoot(`grep -qxF '/system/bin/sh ${SH_FILE} &' /sdcard/ufi_tools_boot.sh || echo '/system/bin/sh ${SH_FILE} &' >> /sdcard/ufi_tools_boot.sh`)
await runShellWithRoot(`/system/bin/sh ${SH_FILE} &`)
return createToast("已启用并设为自启动")
}

window.submit_charge_settings = async () => {
if (Number(CONFIG.max_charge) <= Number(CONFIG.start_charge)) {
return createToast("触发充电值不能大于停止充电值!", 'red')
}
await setRange()
}

window.enable_charge = async () => {
await toggleCharge(true)
createToast("设置成功")
}
window.disable_charge = async () => {
await toggleCharge(false)
createToast("设置成功")
}

const initWindow = async () => {
const start_charge_val = document.querySelector('#start_charge_val')
const stop_charge_val = document.querySelector('#stop_charge_val')

//初始化滑条
if (start_charge_val) {
const start_charge_label = document.querySelector('#start_charge_label')
start_charge_label.innerHTML = CONFIG.start_charge + " %"
start_charge_val.value = CONFIG.start_charge
start_charge_val.oninput = (e) => {
const target = e.target
start_charge_label.innerHTML = target.value + " %"
CONFIG.start_charge = target.value
}
}
if (stop_charge_val) {
const stop_charge_label = document.querySelector('#stop_charge_label')
stop_charge_label.innerHTML = CONFIG.max_charge + " %"
stop_charge_val.value = CONFIG.max_charge
stop_charge_val.oninput = (e) => {
const target = e.target
stop_charge_label.innerHTML = target.value + " %"
CONFIG.max_charge = target.value
}
}
}


const el = document.createElement('div')
el.id = "charge_plugin"
el.classList.add('modal')
el.style.opacity = "1"
el.style.maxWidth = "400px"
el.style.width = '90%'
el.style.display = "none"
el.innerHTML = html
document.querySelector('.container').appendChild(el)



//创建折叠开关
collapseGen('#collapse_charge_plugin_btn', '#collapse_charge_plugin', 'collapse_charge_plugin', async (status) => {
//设置总开关
if (status == "open") {
createToast("开启中..")
await setRange()
await install()
CONFIG.enabled = true
} else {
createToast("关闭中..")
await uninstall()
CONFIG.enabled = false
}
})

await getConfig()
initWindow()

const btn = document.createElement('button')
let timer = null
btn.textContent = "充电控制器"
btn.onclick = async (e) => {
showModal("#charge_plugin")
timer && timer()
getLog()
timer = requestInterval(getLog, 2000)
await getConfig()
initWindow()
}

window.close_charge_settings = () => {
timer && timer()
closeModal('#charge_plugin')
}
document.querySelector('.actions-buttons').appendChild(btn)

window.showHelp = () => {
const message = `安装并配置充电区间后,插件会自动管理充电
当电量在设定区间内时自动开启充电,达到上限自动关闭充电;低于下限保持当前状态,这时您可以手动管理开关充电。
插入充电器时,若电量在区间内也会自动开启充电。
所有操作均有日志记录,无需用户手动干预,保证电池健康和充电安全。
您也可以将“关闭充电”视为直供电模式,此时充电电流为0,可以代替”假电池“
`.replaceAll('\n', "<br>")
const { el, close } = createFixedToast('kano_help_message', `
<div style="pointer-events:all;width:80vw;max-width:300px">
<div class="title" style="margin:0" data-i18n="system_notice">使用说明</div>
<div style="margin:10px 0">${message}</div>
<div style="text-align:right">
<button style="font-size:.64rem" id="close_message_btn" data-i18n="pay_btn_dismiss">${t('pay_btn_dismiss')}</button>
</div>
</div>
`)
const btn = el.querySelector('#close_message_btn')
if (!btn) {
close()
return
}
btn.onclick = async () => {
close()
}
}
})();
</script>
<!-- [KANO_PLUGIN_END] 充电控制器终极版.js -->



<!-- [KANO_PLUGIN_START] 锁频段自动切网.txt -->
<script>
(() => {
const changeNetwork = async (val) => {
const value = val.trim()
try {
const cookie = await login()
if (!cookie) {
createToast(t('login_failed_check_pwd'), 'red')
return null
}
let res = await (await postData(cookie, {
goformId: 'SET_BEARER_PREFERENCE',
BearerPreference: value.trim()
})).json()
if (res.result != 'success') {
createToast(t('toast_oprate_failed'), 'red')
}
} catch (e) {
// createToast(e.message)
}
}

//监听锁频段按钮点击
const btn = document.querySelector('#bandsForm button[type="submit"]')
if (btn) {
btn.addEventListener('click', () => {
setTimeout(async () => {
createToast("正在自动切换网络...",8000)
const netType = document.querySelector('#NET_TYPE')
if (netType) {
const options = document.querySelectorAll('#NET_TYPE option')
const curValue = netType.value
//切到不同网络
if (options.length) {
const net = Array.from(options).find(el => el.value != curValue)
if (net) {
//切网
await changeNetwork(net.value)
//切回来
await changeNetwork(curValue)
createToast('切换成功!')
}
}
}
}, 1000);
})
}
})()
</script>
<!-- [KANO_PLUGIN_END] 锁频段自动切网.txt -->



<!-- [KANO_PLUGIN_START] 电池监控最终版(渐变色)魔改.txt -->
<script>
(() => {
const container = document.querySelector('.functions-container');
container.insertAdjacentHTML("afterend", `
<div id="BATTERY_DISPLAY" style="width: 100%; margin-top: 10px;">
<div class="title" style="margin: 6px 0; color: #fff; display: flex; align-items: center; gap: 15px;">
<strong style="color:#fff;">电池监控</strong>
<div style="display: inline-block;" id="collapse_battery_btn"></div>
</div>
<div class="collapse" id="collapse_battery" data-name="close" style="height: 0px; overflow: hidden;">
<div class="collapse_box" style="background: rgba(30, 30, 46, 0.1);">
<ul class="deviceList" style="margin:0;padding:0;list-style:none;">
<li style="padding:2px; margin-bottom: 2px; border-radius: 2px; background: rgba(30, 30, 46, 0.1);">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div style="position: relative; display: flex; flex-direction: column; align-items: center;">
<div style="position: relative; width: 160px; height: 160px;">
<svg width="160" height="160" viewBox="0 0 160 160">
<circle cx="80" cy="80" r="70" fill="none" stroke="#44475a" stroke-width="8" />
<circle id="battery_ring" cx="80" cy="80" r="70" fill="none" stroke="#50fa7b"
stroke-width="8" stroke-dasharray="439.6" stroke-dashoffset="0"
stroke-linecap="round" transform="rotate(-90 80 80)" />
<text id="battery_percent" x="83" y="65" text-anchor="middle"
dominant-baseline="middle" font-size="32" fill="#f8f8f2" font-weight="bold">--%</text>
<text id="battery_status" x="80" y="90" text-anchor="middle"
dominant-baseline="middle" font-size="14" fill="#6272a4">状态</text>
<text x="71" y="110" text-anchor="middle"
dominant-baseline="middle" font-size="12" fill="#bd93f9">健康度</text>
<text id="battery_health_percent" x="102" y="110" text-anchor="middle"
dominant-baseline="middle" font-size="14" fill="#f8f8f2">--%</text>
</svg>
<div id="battery_icon" style="position: absolute; top: 12px; right: 12px; font-size: 20px;">🔋</div>
</div>

<div style="margin-top: 10px; display: flex; gap: 10px;">
<div style="text-align: center; background: rgba(68, 71, 90, 0.1); padding: 8px; border-radius: 8px;">
<div style="font-size: 12px; color: #bd93f9;">电池温度</div>
<div id="battery_temp" style="font-size: 18px; font-weight: bold; color: #50fa7b;">--<span style="color: #5cdbd3;">°C</span></div>
</div>
<div style="text-align: center; background: rgba(68, 71, 90, 0.1); padding: 8px; border-radius: 8px;">
<div style="font-size: 12px; color: #bd93f9;">CPU温度</div>
<div id="cpu_temp" style="font-size: 18px; font-weight: bold; color: #50fa7b;">--<span style="color: #5cdbd3;">°C</span></div>
</div>
</div>
</div>

<div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">

<div style="background: rgba(68, 71, 90, 0.1); padding: 10px; border-radius: 8px;">
<div style="font-size: 12px; color: #bd93f9;">电压<span style="color: #5cdbd3;"> V</span></div>
<div id="battery_voltage" style="font-size: 16px; color: #f8f8f2;">--</div>
</div>
<div style="background: rgba(68, 71, 90, 0.1); padding: 10px; border-radius: 8px;">
<div style="font-size: 12px; color: #bd93f9;">电流<span style="color: #5cdbd3;"> mA</span></div>
<div id="battery_current" style="font-size: 16px; color: #f8f8f2;">--</div>
</div>
<div style="background: rgba(68, 71, 90, 0.1); padding: 10px; border-radius: 8px;">
<div style="font-size: 12px; color: #bd93f9;">功率<span style="color: #5cdbd3;"> W</span></div>
<div id="battery_power" style="font-size: 16px; color: #f8f8f2;">--</div>
</div>
<div style="background: rgba(68, 71, 90, 0.1); padding: 10px; border-radius: 8px;">
<div style="font-size: 12px; color: #bd93f9;">技术<span style="color: #5cdbd3;"> Li</span></div>
<div id="battery_tech" style="font-size: 16px; color: #f8f8f2;">--</div>
</div>
<div style="grid-column: 1 / -1; background: rgba(68, 71, 90, 0.1); padding: 24px 10px; border-radius: 8px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<div style="font-size: 14px; color: #bd93f9;">电池容量</div>
<div id="battery_capacity" style="font-size: 14px; color: #f8f8f2;">0/0 <span style="color: #5cdbd3;">mAh</span></div>
</div>
<div style="height: 12px; background: #44475a; border-radius: 6px; overflow: hidden;">
<div id="battery_capacity_bar" style="height: 100%; width: 0%; background: linear-gradient(90deg, #ff5555, #ffb86c, #50fa7b); border-radius: 6px;"></div>
</div>
</div>
</div>
</div>
</div>
</li>
</ul>
</div>
</div>
</div>
`);

const PLUGIN_CONFIG = {
INSTALL_DIR: "/data/battery_monitor",
CONFIG_FILE: "/sdcard/battery_monitor.conf",
BINARY_PATH: "/data/battery_monitor/battery_monitor",
LOG_FILE: "/data/battery_monitor/monitor.log",
PID_FILE: "/data/battery_monitor/monitor.pid",

SERVICE_NAME: "battery_monitor",

BATTERY_PATHS: {
CAPACITY: "/sys/class/power_supply/battery/capacity",
STATUS: "/sys/class/power_supply/battery/status",
HEALTH: "/sys/class/power_supply/battery/health",
TEMP: "/sys/class/power_supply/battery/temp",
VOLTAGE: "/sys/class/power_supply/battery/voltage_now",
CURRENT: "/sys/class/power_supply/battery/current_now",
TECHNOLOGY: "/sys/class/power_supply/battery/technology",
CHARGE_FULL: "/sys/class/power_supply/battery/charge_full",
CHARGE_FULL_DESIGN: "/sys/class/power_supply/battery/charge_full_design",
CHARGE_COUNTER: "/sys/class/power_supply/battery/charge_counter"
},

CPU_TEMP_PATHS: [
"/sys/class/thermal/thermal_zone0/temp",
"/sys/devices/virtual/thermal/thermal_zone0/temp",
"/sys/class/hwmon/hwmon0/temp1_input",
"/sys/devices/system/cpu/cpu0/cpufreq/cpu_temp",
"/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_temp"
],

STATUS_MAP: {
"Charging": {text: "充电中", color: "#50fa7b", icon: "⚡️"},
"Discharging": {text: "放电中", color: "#ffb86c", icon: "🔋"},
"Full": {text: "已充满", color: "#8be9fd", icon: "💯"},
"Not charging": {text: "未充电", color: "#6272a4", icon: "❌"},
"Unknown": {text: "未知", color: "#bd93f9", icon: "❓"}
},

HEALTH_MAP: {
"Good": "良好",
"Overheat": "过热",
"Dead": "损坏",
"Over voltage": "过压",
"Cold": "过冷",
"Unknown": "未知"
},

VOLTAGE_CURRENT_UPDATE_INTERVAL: 1000,
FULL_UPDATE_INTERVAL: 5000,

DESIGN_CAPACITY: 4500
};

const pluginState = {
installed: false,
running: false,
monitoring: false,
updateTimer: null,
voltageCurrentTimer: null,
batteryData: {
level: 0,
status: "Unknown",
health: "Unknown",
temperature: 0,
voltage: 0,
current: 0,
technology: "Unknown",
chargeFull: 0,
chargeFullDesign: 0,
chargeCounter: 0,
cpuTemperature: 0
}
};

const getElement = (id) => document.getElementById(id);

const checkAdvancedFunc = async () => {
const res = await runShellWithRoot('whoami');
return res.content?.includes('root') ?? false;
};

const validatePermission = async () => {
if (!(await checkAdvancedFunc())) {
createToast("需要开启高级功能才能使用此插件", 'red');
return false;
}
return true;
};

const getBatteryColor = (level) => {
if (level <= 5) return "#ff5555";
if (level <= 10) return "#ff6e6e";
if (level <= 15) return "#ff8787";
if (level <= 20) return "#ffa0a0";
if (level <= 25) return "#ffb9b9";
if (level <= 30) return "#ffb86c";
if (level <= 35) return "#ffc285";
if (level <= 40) return "#ffcc9e";
if (level <= 45) return "#ffd6b7";
if (level <= 50) return "#ffe0d0";
if (level <= 55) return "#e6e05e";
if (level <= 60) return "#e9e97a";
if (level <= 65) return "#ecec96";
if (level <= 70) return "#efefb2";
if (level <= 75) return "#8be9fd";
if (level <= 80) return "#9aecfd";
if (level <= 85) return "#a9effd";
if (level <= 90) return "#b8f2fd";
if (level <= 95) return "#c7f5fd";
return "#50fa7b";
};

const getTemperatureColor = (tempC) => {
if (tempC <= 10) return "#00FFFF";
if (tempC <= 20) return "#00C78C";
if (tempC <= 25) return "#00A86B";
if (tempC <= 30) return "#228B22";
if (tempC <= 35) return "#9ACD32";
if (tempC <= 40) return "#ADFF2F";
if (tempC <= 45) return "#FF8C00";
if (tempC <= 50) return "#FF4500";
if (tempC <= 55) return "#FF6347";
return "#DC143C";
};

const getCpuTemperatureColor = (tempC) => {
if (tempC < 15) return "#f8f8f2";
if (tempC <= 20) return "#50fa7b";
if (tempC <= 25) return "#8be9fd";
if (tempC <= 30) return "#ffb86c";
if (tempC <= 35) return "#ffb86c";
if (tempC <= 40) return "#ffb86c";
if (tempC <= 45) return "#ff5555";
if (tempC <= 50) return "#ff5555";
if (tempC <= 55) return "#ff5555";
if (tempC <= 60) return "#ff5555";
if (tempC <= 65) return "#ff5555";
return "#ff0000";
};

const getBatteryHealthPercentage = (chargeFull, chargeFullDesign) => {
if (chargeFull > 0 && chargeFullDesign > 0) {
const healthPercentage = Math.round((chargeFull / chargeFullDesign) * 100);
return Math.min(healthPercentage, 100);
}
return null;
};

const readCpuTemperature = async () => {
for (const path of PLUGIN_CONFIG.CPU_TEMP_PATHS) {
try {
const result = await runShellWithRoot(`cat ${path} 2>/dev/null`);
if (result.success && result.content.trim() !== "") {
const temp = parseInt(result.content.trim());
if (!isNaN(temp) && temp > 0) {
return temp;
}
}
} catch {
}
}
return 0;
};

const installPlugin = async () => {
if (!(await validatePermission())) return;

createToast("开始安装电池监控插件...");

try {
const createDir = await runShellWithRoot(`
mkdir -p ${PLUGIN_CONFIG.INSTALL_DIR}
chmod 755 ${PLUGIN_CONFIG.INSTALL_DIR}
`);
if (!createDir.success) throw new Error("创建目录失败");

const download = await runShellWithRoot(`
echo '#!/system/bin/sh
while true; do
echo "电池监控中..."
sleep 5
done' > ${PLUGIN_CONFIG.BINARY_PATH}
chmod 755 ${PLUGIN_CONFIG.BINARY_PATH}
`);
if (!download.success) throw new Error("下载监控脚本失败");

const createConfig = await runShellWithRoot(`
echo '{
"update_interval": 5000,
"battery_paths": ${JSON.stringify(PLUGIN_CONFIG.BATTERY_PATHS)},
"cpu_temp_paths": ${JSON.stringify(PLUGIN_CONFIG.CPU_TEMP_PATHS)}
}' > ${PLUGIN_CONFIG.CONFIG_FILE}
`);
if (!createConfig.success) throw new Error("创建配置文件失败");

const setAutostart = await runShellWithRoot(`
echo "nohup ${PLUGIN_CONFIG.BINARY_PATH} > ${PLUGIN_CONFIG.LOG_FILE} 2>&1 &" >> /sdcard/ufi_tools_boot.sh
`);
if (!setAutostart.success) throw new Error("设置自启动失败");

pluginState.installed = true;
createToast("电池监控插件安装成功!", 'green');
} catch (error) {
createToast(`安装失败: ${error.message}`, 'red');
}
};

const uninstallPlugin = async () => {
if (!(await validatePermission())) return;

createToast("开始卸载电池监控插件...");

try {
await stopService();

const removeFiles = await runShellWithRoot(`
rm -rf ${PLUGIN_CONFIG.INSTALL_DIR}
rm -f ${PLUGIN_CONFIG.CONFIG_FILE}
`);
if (!removeFiles.success) throw new Error("删除文件失败");

const removeAutostart = await runShellWithRoot(`
sed -i '/${PLUGIN_CONFIG.SERVICE_NAME}/d' /sdcard/ufi_tools_boot.sh
`);
if (!removeAutostart.success) throw new Error("移除自启动失败");

pluginState.installed = false;
createToast("电池监控插件已卸载", 'green');
} catch (error) {
createToast(`卸载失败: ${error.message}`, 'red');
}
};

const startService = async () => {
if (!(await validatePermission())) return;
if (!pluginState.installed) {
createToast("请先安装插件", 'red');
return;
}

try {
const start = await runShellWithRoot(`
nohup ${PLUGIN_CONFIG.BINARY_PATH} > ${PLUGIN_CONFIG.LOG_FILE} 2>&1 &
echo $! > ${PLUGIN_CONFIG.PID_FILE}
`);

if (start.success) {
pluginState.running = true;
createToast("电池监控服务已启动", 'green');
startMonitoring();
} else {
throw new Error("启动服务失败");
}
} catch (error) {
createToast(`启动失败: ${error.message}`, 'red');
}
};

const stopService = async () => {
if (!(await validatePermission())) return;

try {
const stop = await runShellWithRoot(`
if [ -f "${PLUGIN_CONFIG.PID_FILE}" ]; then
kill -9 $(cat ${PLUGIN_CONFIG.PID_FILE})
rm -f ${PLUGIN_CONFIG.PID_FILE}
else
pkill -f "${PLUGIN_CONFIG.SERVICE_NAME}"
fi
`);

if (stop.success) {
pluginState.running = false,
pluginState.monitoring = false;
clearInterval(pluginState.updateTimer);
clearInterval(pluginState.voltageCurrentTimer);
createToast("电池监控服务已停止", 'green');
} else {
throw new Error("停止服务失败");
}
} catch (error) {
createToast(`停止失败: ${error.message}`, 'red');
}
};

const readBatteryFile = async (path) => {
try {
const result = await runShellWithRoot(`cat ${path} 2>/dev/null`);
return result.success ? result.content.trim() : "未知";
} catch {
return "未知";
}
};

const fetchVoltageCurrentInfo = async () => {
const paths = PLUGIN_CONFIG.BATTERY_PATHS;

const [voltage, current] = await Promise.all([
readBatteryFile(paths.VOLTAGE),
readBatteryFile(paths.CURRENT)
]);

pluginState.batteryData.voltage = parseInt(voltage) || 0;
pluginState.batteryData.current = parseInt(current) || 0;

updateVoltageCurrentDisplay();
};

const fetchBatteryInfo = async () => {
const paths = PLUGIN_CONFIG.BATTERY_PATHS;

const [
capacity, status, health, temp,
technology, chargeFull, chargeFullDesign,
chargeCounter
] = await Promise.all([
readBatteryFile(paths.CAPACITY),
readBatteryFile(paths.STATUS),
readBatteryFile(paths.HEALTH),
readBatteryFile(paths.TEMP),
readBatteryFile(paths.TECHNOLOGY),
readBatteryFile(paths.CHARGE_FULL),
readBatteryFile(paths.CHARGE_FULL_DESIGN),
readBatteryFile(paths.CHARGE_COUNTER)
]);

const cpuTemp = await readCpuTemperature();

pluginState.batteryData = {
...pluginState.batteryData,
level: parseInt(capacity) || 0,
status: status in PLUGIN_CONFIG.STATUS_MAP ? status : "Unknown",
health: health in PLUGIN_CONFIG.HEALTH_MAP ? health : "Unknown",
temperature: parseInt(temp) || 0,
technology: technology,
chargeFull: parseInt(chargeFull) || 0,
chargeFullDesign: parseInt(chargeFullDesign) || 0,
chargeCounter: parseInt(chargeCounter) || 0,
cpuTemperature: cpuTemp
};

updateBatteryDisplay();
};

const updateVoltageCurrentDisplay = () => {
const battery = pluginState.batteryData;

getElement('battery_voltage').textContent = battery.voltage > 0 ?
`${(battery.voltage / 1000000).toFixed(3)}` : "--";

if (battery.current !== 0) {
const currentInMA = battery.current / 1000;
getElement('battery_current').textContent = `${currentInMA.toFixed(0)}`;
} else if (battery.current === 0) {
getElement('battery_current').textContent = "0";
} else {
getElement('battery_current').textContent = "--";
}

if (battery.voltage !== 0 && battery.current !== 0) {
const voltageInV = battery.voltage / 100000;
const currentInA = battery.current / 1000000;
let power = voltageInV * currentInA * 0.1;

if (power < 0) power = 0;

getElement('battery_power').textContent = `${power.toFixed(2)}`;
} else if (battery.voltage === 0 || battery.current === 0) {
getElement('battery_power').textContent = "0";
} else {
getElement('battery_power').textContent = "--";
}
};

const updateBatteryDisplay = () => {
const battery = pluginState.batteryData;
const statusInfo = PLUGIN_CONFIG.STATUS_MAP[battery.status] || PLUGIN_CONFIG.STATUS_MAP.Unknown;

const ring = getElement('battery_ring');
const dashoffset = 439.6 * (1 - battery.level / 100);
ring.style.strokeDashoffset = dashoffset;

const ringColor = getBatteryColor(battery.level);
ring.style.stroke = ringColor;

getElement('battery_percent').textContent = `${battery.level}%`;
getElement('battery_status').textContent = statusInfo.text;
getElement('battery_status').style.fill = statusInfo.color;
getElement('battery_icon').textContent = statusInfo.icon;

const tempC = battery.temperature / 10;
const tempElement = getElement('battery_temp');
tempElement.innerHTML = `${tempC}<span style="color: #5cdbd3;">°C</span>`;

const tempColor = getTemperatureColor(tempC);
tempElement.style.color = tempColor;

const cpuTempC = battery.cpuTemperature > 0 ? (battery.cpuTemperature / 1000).toFixed(1) : "0.0";
const cpuTempElement = getElement('cpu_temp');
cpuTempElement.innerHTML = `${cpuTempC}<span style="color: #5cdbd3;">°C</span>`;

const cpuTempColor = getCpuTemperatureColor(parseFloat(cpuTempC));
cpuTempElement.style.color = cpuTempColor;

const healthPercentage = getBatteryHealthPercentage(battery.chargeFull, battery.chargeFullDesign);
let healthDisplay;

if (healthPercentage !== null) {
healthDisplay = `${healthPercentage} `;
let healthColor = "#50fa7b";
if (healthPercentage <= 60) healthColor = "#ff5555";
else if (healthPercentage <= 80) healthColor = "#ffb86c";
getElement('battery_health_percent').style.fill = healthColor;
} else {
healthDisplay = PLUGIN_CONFIG.HEALTH_MAP[battery.health] || "未知";
let healthColor = "#50fa7b";
if (battery.health === "Dead" || battery.health === "Overheat") {
healthColor = "#ff5555";
} else if (battery.health === "Over voltage" || battery.health === "Cold") {
healthColor = "#ffb86c";
}
getElement('battery_health_percent').style.fill = healthColor;
}

getElement('battery_health_percent').textContent = healthDisplay;

getElement('battery_tech').textContent = battery.technology;

const capacityBar = getElement('battery_capacity_bar');
const capacityText = getElement('battery_capacity');

const currentCapacity = Math.round(PLUGIN_CONFIG.DESIGN_CAPACITY * battery.level / 100);
const designCapacity = PLUGIN_CONFIG.DESIGN_CAPACITY;

capacityBar.style.width = `${battery.level}%`;
capacityText.innerHTML = `${currentCapacity}/${designCapacity} <span style="color: #5cdbd3;">mAh</span>`;
};

const startMonitoring = () => {
if (pluginState.monitoring) return;

fetchBatteryInfo();

pluginState.updateTimer = setInterval(fetchBatteryInfo, PLUGIN_CONFIG.FULL_UPDATE_INTERVAL);

fetchVoltageCurrentInfo();

pluginState.voltageCurrentTimer = setInterval(
fetchVoltageCurrentInfo,
PLUGIN_CONFIG.VOLTAGE_CURRENT_UPDATE_INTERVAL
);

pluginState.monitoring = true;
};

collapseGen("#collapse_battery_btn", "#collapse_battery", "#collapse_battery", (e) => {
if (e.getAttribute('data-name') === 'close') {
clearInterval(pluginState.updateTimer);
clearInterval(pluginState.voltageCurrentTimer);
pluginState.monitoring = false;
} else if (pluginState.installed && pluginState.running) {
startMonitoring();
}
});

const autoInstallAndStart = async () => {
try {
const check = await runShellWithRoot(`[ -d "${PLUGIN_CONFIG.INSTALL_DIR}" ] && echo "installed"`);
pluginState.installed = check.success && check.content.includes('installed');

if (!pluginState.installed) {
createToast("检测到未安装插件,正在自动安装...", "blue");
await installPlugin();
}

const running = await runShellWithRoot(`if [ -f "${PLUGIN_CONFIG.PID_FILE}" ]; then kill -0 $(cat ${PLUGIN_CONFIG.PID_FILE}) && echo "running"; fi`);
pluginState.running = running.success && running.content.includes('running');

if (!pluginState.running) {
createToast("正在自动启动电池监控服务...", "blue");
await startService();
}

if (document.querySelector('#collapse_battery').getAttribute('data-name') !== 'close') {
startMonitoring();
}
} catch (error) {
createToast(`自动启动失败: ${error.message}`, 'red');
}
};

setTimeout(autoInstallAndStart, 1000);
})();
</script>
<!-- [KANO_PLUGIN_END] 电池监控最终版(渐变色)魔改.txt -->



<!-- [KANO_PLUGIN_START] AI大屏数据看板插件1.1.2.js -->
//<script>
(() => {
// 如果已存在则先清理
const EXISTING_ID = 'ufi-dashboard-iframe';
const old = document.getElementById(EXISTING_ID);
if (old) old.remove();
// 获取本地存储的看板常驻状态
let ufi_dashboard_state = localStorage.getItem('ufi_dashboard_state') || '0';

// 注入 iframe
const iframe = document.createElement('iframe');
iframe.id = EXISTING_ID;
iframe.style.position = 'fixed';
iframe.style.inset = '0';
iframe.style.width = '100%';
iframe.style.height = '100%';
iframe.style.border = '0';
iframe.style.transition = 'all .3s';
iframe.style.opacity = '0';
iframe.style.display = 'none';
iframe.style.background = 'transparent';
document.body.appendChild(iframe);
const container = document.querySelector('.container')

const btn = document.createElement('button')
btn.textContent = "打开数据看板"
let timer_ani = null
let timer_ani1 = null
btn.onclick = () => {
iframe.style.display = ''
timer_ani && clearTimeout(timer_ani)
timer_ani = setTimeout(() => {
iframe.style.opacity = '1';
container.style.display = 'none'
ufi_dashboard_state = "1"
localStorage.setItem('ufi_dashboard_state', '1')
}, 350);
}

document.querySelector('.actions-buttons').appendChild(btn)

if (ufi_dashboard_state === '1') btn.click()

//接收子页面的数据
window.addEventListener('message', (event) => {
if (event.data && event.data.type == "BACK_TO_UFITOOLS") {
iframe.style.opacity = '0';
container.style.display = ''
timer_ani1 && clearTimeout(timer_ani1)
timer_ani1 = setTimeout(() => {
iframe.style.display = 'none'
ufi_dashboard_state = "0"
localStorage.setItem('ufi_dashboard_state', '0')
}, 310);
}
});

// 从父页面桥接数据
const parentData = (window.UFI_DATA && typeof window.UFI_DATA === 'object') ? { ...window.UFI_DATA, QORS_MESSAGE: QORS_MESSAGE || "-" } : null;
// 内联 HTML + CSS + JS
const html = `<kano_!DOCTYPE html>
<kano_html lang="zh-CN">
<kano_head>
<kano_meta charset="UTF-8">
<kano_meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>UFI-TOOLS-AI看板</title>
<kano_script src="/script/lib/chart.js"></kano_script>
<kano_style>
/* 基础变量 */
:root {
--bg: #00000059;
--card-bg: rgba(255, 255, 255, 0.06);
--card-border: rgba(255, 255, 255, 0.12);
--text: #e7ebf5;
--muted: #a8b0c3;
--accent: #4aa8ff;
--good: #22c55e;
--warn: #f59e0b;
--bad: #ef4444;
--glass-blur: 16px;
--radius: 14px;
}
* { box-sizing: border-box; }

html, body {
margin: 0;
font-family: 'Inter', system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Noto Sans", "Apple Color Emoji", "Segoe UI Emoji";
color: var(--text);
background: var(--bg);
}

.dashboard-container { min-height: 100vh; padding: 20px 16px 40px; }
.header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 8px auto 16px; max-width: 1400px; }
.header-left { display: flex; align-items: center; gap: 10px; }
.net-badge { padding: 4px 8px; border-radius: 8px; background: rgba(42,120,255,0.18); border: 1px solid var(--card-border); font-weight: 700; font-size: 12px; color: #dbe7ff; }
.title { font-weight: 700; letter-spacing: 0.3px; font-size: 20px; }
.header-right { display: flex; align-items: center; gap: 14px; }
.signal { display: inline-flex; gap: 2px; align-items: flex-end; height: 16px; }
.signal .bar { width: 3px; background: rgba(255,255,255,0.3); border-radius: 2px; display: inline-block; }
.signal .b1 { height: 4px; } .signal .b2 { height: 7px; } .signal .b3 { height: 10px; } .signal .b4 { height: 13px; }
.signal .on { background: #6ee7ff; box-shadow: 0 0 6px rgba(110,231,255,0.7); }
.battery { display: inline-flex; align-items: center; gap: 6px; }
.battery-body { position:relative;width: 28px; height: 14px; border: 1px solid var(--card-border); border-radius: 3px; position: relative; background: rgba(255,255,255,0.06);}
.battery-fill { height: 100%; width: 0%; background: linear-gradient(90deg,#22c55e,#4ade80); border-radius: 3px;}
.battery-cap { width: 3px; height: 8px; background: var(--card-border); border-radius: 1px; margin-left: -6px; }
.battery-text { font-weight: 700; font-size: 12px; color: var(--muted); }

.grid { max-width: 1400px; margin: 0 auto; display: grid; grid-template-columns: repeat(12, 1fr); gap: 16px; }
.card { backdrop-filter: blur(var(--glass-blur)); -webkit-backdrop-filter: blur(var(--glass-blur)); border: 1px solid var(--card-border); border-radius: var(--radius); padding: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.3), inset 0 1px 0 rgba(255,255,255,0.04); }

.kpis { grid-column: span 12; display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 12px; }
#card-network > .card-header { grid-column: 1 / -1; }
.kpis.compact { grid-template-columns: repeat(6, 1fr); }
.kpi.wide { grid-column: span 2; }
.duplex { font-weight: 700; font-size: 18px; }
.duplex .rx { color: #93c5fd; } .duplex .tx { color: #86efac; } .duplex .sep { color: var(--muted); margin: 0 6px; }

.kpi { background: linear-gradient(180deg, rgba(255,255,255,0.06), rgba(255,255,255,0.03)); border: 1px solid var(--card-border); border-radius: 12px; padding: 12px; }
.kpi-label { color: var(--muted); font-size: 12px; margin-bottom: 6px; }
.kpi-value { font-size: 18px; font-weight: 700; }
.kpi-value.small { font-size: 14px; word-break: break-all; }

.chart-card { grid-column: span 6; }
.progress-card { grid-column: span 6; }

#card-temp-mem .progress-grid { display: grid; grid-template-columns: repeat(1, 1fr); gap: 30px; }
#card-temp-mem .progress-block { grid-template-columns: 120px 1fr 70px; }

#card-signal .progress-fill { background: linear-gradient(90deg,#60a5fa,#34d399); }
.level-strong { background: linear-gradient(90deg,#22c55e,#86efac) !important; }
.level-medium { background: linear-gradient(90deg,#f59e0b,#fbbf24) !important; }
.level-weak { background: linear-gradient(90deg,#ef4444,#f97316) !important; }

.card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
.card-title { font-weight: 700; }
.card-extra { color: var(--muted); font-size: 12px; }
.btn-mask { border: 1px solid var(--card-border); background: rgba(255,255,255,0.06); color: var(--text); font-weight: 600; font-size: 12px; padding: 6px 10px; border-radius: 8px; cursor: pointer; transition: background 160ms ease, box-shadow 160ms ease; }
.btn-mask[aria-pressed="true"] { box-shadow: 0 0 0 2px rgba(102,227,255,0.25) inset; }
.btn-mask:hover { background: rgba(255,255,255,0.09); }

canvas { width: 100% !important; height: 288px !important; display: block; }
.chart-card canvas { image-rendering: -moz-crisp-edges; image-rendering: pixelated; }

.progress-block { display: grid; grid-template-columns: 120px 1fr 70px; gap: 10px; align-items: center; }
.progress-label { color: var(--muted); font-size: 13px; }
.progress-bar { width: 100%; height: 12px; border-radius: 999px; background: rgba(255,255,255,0.06); border: 1px solid var(--card-border); overflow: hidden; position: relative; }
.progress-fill { height: 100%; width: 0%; border-radius: 999px; background: linear-gradient(90deg, rgba(74,168,255,0.9), rgba(42,120,255,0.9)); box-shadow: 0 0 10px rgba(74,168,255,0.6); transition: width 300ms ease; }
.progress-value { font-weight: 700; font-size: 14px; text-align: right; }
.divider { height: 10px; }

@media (max-width: 1200px) {
.chart-card { grid-column: span 12; }
.progress-card { grid-column: span 12; }
.kpis { grid-template-columns: repeat(3, 1fr); }
#card-temp-mem .progress-grid { grid-template-columns: 1fr; }
}
@media (max-width: 720px) {
.title { font-size: 16px; }
.subtitle { font-size: 12px; }
.kpis { grid-template-columns: repeat(2, 1fr); }
canvas { height: 220px !important; }
.progress-block { grid-template-columns: 88px 1fr 58px; }
}
#kpi-nr,#kpi-lte { font-size: 12px; }
.span-col-12 { grid-column: span 12; }
#val-rsrp,#val-sinr{font-size:12px}
#kpi-qci{font-size:12px}
#val-storage-int{font-size:12px}
.battery-body.charging::after {
content: "⚡";
position: absolute;
top: 9px;
left: 9px;
font-size: 18px;
transform: rotate(20deg) translate(-50%, -50%);
pointer-events: none;
}
</kano_style>
</kano_head>
<kano_body>
<div class="dashboard-container">
<header class="header">
<div class="header-left">
<div class="btn-mask" id="back_to_ufitools" style="user-select:none">返回</div>
<div class="title">UFI-数据看板</div>
</div>
<div class="header-right">
<div class="signal" id="icon-signal" aria-label="信号强度">
<span class="bar b1"></span><span class="bar b2"></span><span class="bar b3"></span><span class="bar b4"></span>
</div>
<div class="battery" id="icon-battery" aria-label="电池">
<div class="battery-body"><div class="battery-fill" id="battery-fill"></div></div>
<div class="battery-cap"></div>
<div class="battery-text" id="battery-text">-</div>
</div>
</div>
</header>

<main class="grid">
<section class="card kpis compact" id="card-throughput">
<section class="wide progress-card span-col-12" id="card-signal">
<div class="card-header">
<div class="card-title">无线信号质量</div>
<div class="card-extra" id="signal-label">-</div>
</div>
<div class="progress-block">
<div class="progress-label">RSRP</div>
<div class="progress-bar" aria-label="RSRP"><div class="progress-fill" id="bar-rsrp"></div></div>
<div class="progress-value" id="val-rsrp">-</div>
</div>
<div class="divider"></div>
<div class="progress-block">
<div class="progress-label">SINR</div>
<div class="progress-bar" aria-label="SINR"><div class="progress-fill" id="bar-sinr"></div></div>
<div class="progress-value" id="val-sinr">-</div>
</div>
</section>
<section class="kpis">
<div class="kpi wide">
<div class="kpi-label">实时下上行</div>
<div class="duplex"><span class="rx" id="kpi-rx">-</span><span class="sep">/</span><span class="tx" id="kpi-tx">-</span></div>
</div>
<div class="kpi"><div class="kpi-label">QCI</div><div class="kpi-value" id="kpi-qci">-</div></div>
<div class="kpi"><div class="kpi-label">当日流量</div><div class="kpi-value" id="kpi-daily">-</div></div>
<div class="kpi"><div class="kpi-label">月流量</div><div class="kpi-value small" id="kpi-month">-</div></div>
<div class="kpi"><div class="kpi-label">连接设备</div><div class="kpi-value" id="kpi-sta">-</div></div>
</section>
</section>

<section class="card kpis" id="card-network">
<div class="card-header">
<div class="card-title">网络信息</div>
<button class="btn-mask" id="btn-mask" type="button" aria-pressed="true">脱敏:开</button>
</div>
<div class="kpi"><div class="kpi-label">网络类型</div><div class="kpi-value" id="kpi-network-type">-</div></div>
<div class="kpi"><div class="kpi-label">运营商</div><div class="kpi-value" id="kpi-provider">-</div></div>
<div class="kpi"><div class="kpi-label">IPv6 地址</div><div class="kpi-value small" id="kpi-ipv6">-</div></div>
<div class="kpi"><div class="kpi-label">LAN 网关</div><div class="kpi-value" id="kpi-lan">-</div></div>
<div class="kpi"><div class="kpi-label">5G SINR</div><div class="kpi-value" id="kpi-sinr">-</div></div>
<div class="kpi"><div class="kpi-label">5G RSRP</div><div class="kpi-value" id="kpi-rsrp">-</div></div>
<div class="kpi"><div class="kpi-label">ICCID</div><div class="kpi-value small" id="kpi-iccid">-</div></div>
<div class="kpi"><div class="kpi-label">IMEI</div><div class="kpi-value small" id="kpi-imei">-</div></div>
<div class="kpi"><div class="kpi-label">NR 基站</div><div class="kpi-value small" id="kpi-nr">-</div></div>
<div class="kpi"><div class="kpi-label">LTE 基站</div><div class="kpi-value small" id="kpi-lte">-</div></div>
<div class="kpi"><div class="kpi-label">UFI-TOOLS版本</div><div class="kpi-value small" id="kpi-version">-</div></div>
<div class="kpi"><div class="kpi-label">设备型号</div><div class="kpi-value" id="kpi-model">-</div></div>
</section>

<section class="card progress-card" id="card-temp-mem">
<div class="card-header"><div class="card-title">温度 / 内存</div><div class="card-extra" id="temp-mem-label">-</div></div>
<div class="progress-grid">
<div class="progress-block"><div class="progress-label">CPU 温度</div><div class="progress-bar" aria-label="CPU 温度"><div class="progress-fill" id="bar-temp-cpu"></div></div><div class="progress-value" id="val-temp-cpu">-</div></div>
<div class="progress-block"><div class="progress-label">PA 温度</div><div class="progress-bar" aria-label="SoC 温度"><div class="progress-fill" id="bar-temp-soc"></div></div><div class="progress-value" id="val-temp-soc">-</div></div>
<div class="progress-block"><div class="progress-label">内存使用</div><div class="progress-bar" aria-label="内存使用率"><div class="progress-fill" id="bar-mem"></div></div><div class="progress-value" id="val-mem">-</div></div>
<div class="progress-block"><div class="progress-label">SWAP 使用</div><div class="progress-bar" aria-label="SWAP 使用率"><div class="progress-fill" id="bar-swap"></div></div><div class="progress-value" id="val-swap">-</div></div>
<div class="progress-block" id="row-storage-int"><div class="progress-label">内部存储</div><div class="progress-bar" aria-label="内部存储"><div class="progress-fill" id="bar-storage-int"></div></div><div class="progress-value" id="val-storage-int">-</div></div>
<div class="progress-block" id="row-storage-ext"><div class="progress-label">SD 卡</div><div class="progress-bar" aria-label="SD 卡"><div class="progress-fill" id="bar-storage-ext"></div></div><div class="progress-value" id="val-storage-ext">-</div></div>
</div>
</section>

<section class="card chart-card"><div class="card-header"><div class="card-title">核心总占用</div><div class="card-extra" id="cpu-total-label">-</div></div><canvas id="chartCpuTotal"></canvas></section>
<section class="card chart-card"><div class="card-header"><div class="card-title">核心使用率</div><div class="card-extra" style="font-size:.6rem" id="cpu-cores-label">-</div></div><canvas id="chartCpuCores"></canvas></section>
<section class="card chart-card"><div class="card-header"><div class="card-title">核心频率 (MHz)</div><div class="card-extra" id="cpu-freq-label">-</div></div><canvas id="chartCpuFreq"></canvas></section>
</main>
</div>

<kano_script>
(() => {
'use strict';
console.log('[dashboard] injected bundle loaded');

// Bridge UFI_DATA
window.UFI_DATA = ${JSON.stringify(parentData || null)} || (window.parent && window.parent.UFI_DATA && ({...window.parent.UFI_DATA,QORS_MESSAGE})) || '{}';
// 等待 Chart.js + 监听父窗口数据 postMessage
function onChartReady(cb) {
if (window.Chart) return cb();
const timer = setInterval(() => { if (window.Chart) { clearInterval(timer); cb(); } }, 50);
}

window.addEventListener('message', (ev) => {
if (ev && ev.data && ev.data.type === 'QORS_MESSAGE') {
try {
window.UFI_DATA.QORS_MESSAGE = ev.data.payload
} catch (e) {}
}
});

onChartReady(() => {
Chart.defaults.responsive = true;
Chart.defaults.maintainAspectRatio = false;
Chart.defaults.devicePixelRatio = () => window.devicePixelRatio || 1;

const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
const toPercentText = (v) => \`\${v.toFixed(1)}%\`;
const toMbps = (bytesPerSec) => \`\${(bytesPerSec * 8 / 1_000_000).toFixed(2)} Mbps\`;
const toGB = (bytes) => (bytes / (1024 ** 3)).toFixed(2) + ' GB';
const toHumanBytes = (bytes) => { if (!bytes) return '0 B'; const u=['B','KB','MB','GB','TB']; let v=bytes,i=0; while(v>=1024&&i<u.length-1){v/=1024;i++;} return \`\${v.toFixed(v<10?2:1)} \${u[i]}\`; };
const celsiusFromMilli = (m) => m / 1000;
const el = (id) => document.getElementById(id);

const backBtn = el('back_to_ufitools')

if(backBtn) {
backBtn.onclick = ()=>{
try {
window.parent.postMessage({ type: 'BACK_TO_UFITOOLS', value: 1 },'*' );
console.log("Send Message:BACK_TO_UFITOOLS to parent window")
} catch (e) { console.warn('postMessage bridge failed', e); }
}
}

// 数据桥接:每次刷新前优先从父/顶层同步 UFI_DATA,避免初次为空或后续更新丢失
function refreshDataFromParent() {
let d = null;
try { if (window.parent && window.parent.UFI_DATA) d = window.parent.UFI_DATA; } catch (e) {}
if (!d) { try { if (window.top && window.top.UFI_DATA) d = window.top.UFI_DATA; } catch (e) {} }
if (!d && typeof window.UFI_DATA === 'object') d = window.UFI_DATA;
if (d) window.UFI_DATA = d;
}

// 图表
const cpuTotalChart = new Chart(document.getElementById('chartCpuTotal'), {
type: 'line',
data: { labels: [], datasets: [{ label: 'CPU 使用率', data: [], borderColor: 'rgba(74,168,255,1)', backgroundColor: 'rgba(74,168,255,0.15)', borderWidth: 2, tension: 0.25, fill: true, pointRadius: 0 }]},
options: { animation: { duration: 400, easing: 'easeOutQuart' }, responsive: true, maintainAspectRatio: false,
layout: { padding: { top: 8, right: 12, bottom: 16, left: 12 } },
scales: { y: { beginAtZero: true, max: 100, grid: { color: 'rgba(255,255,255,0.08)' }, ticks: { color: '#a8b0c3', autoSkip: true, maxTicksLimit: 5,padding: 6, callback: v => v + '%' } },
x: { grid: { display: false }, ticks: { color: '#a8b0c3', autoSkip: true, maxTicksLimit: 6 } } },
plugins: { legend: { display: false } }
}
});
const cpuCoresChart = new Chart(document.getElementById('chartCpuCores'), {
type: 'bar',
data: { labels: [], datasets: [{ label: '核心使用率', data: [], backgroundColor: 'rgba(42,120,255,0.8)' }]},
options: { animation: { duration: 400, easing: 'easeOutQuart' }, responsive: true, maintainAspectRatio: false,
layout: { padding: { top: 8, right: 12, bottom: 16, left: 12 } },
scales: { y: { beginAtZero: true, max: 100, grid: { color: 'rgba(255,255,255,0.08)' }, ticks: { color: '#a8b0c3', autoSkip: true, maxTicksLimit: 5,padding: 6, callback: v => v + '%' } },
x: { grid: { display: false }, ticks: { color: '#a8b0c3', autoSkip: true, maxTicksLimit: 8 } } },
plugins: { legend: { display: false } }
}
});
const cpuFreqChart = new Chart(document.getElementById('chartCpuFreq'), {
type: 'bar',
data: { labels: [], datasets: [{ label: '当前频率', data: [], backgroundColor: 'rgba(74,168,255,0.85)' }, { label: '最大频率', data: [], backgroundColor: 'rgba(168, 181, 255, 0.55)' }]},
options: { animation: { duration: 400, easing: 'easeOutQuart' }, responsive: true, maintainAspectRatio: false,
layout: { padding: { top: 8, right: 12, bottom: 16, left: 12 } },
scales: { y: { beginAtZero: true, grid: { color: 'rgba(255,255,255,0.08)' }, ticks: { color: '#a8b0c3', autoSkip: true, maxTicksLimit: 6 } },
x: { stacked: false, grid: { display: false }, ticks: { color: '#a8b0c3', autoSkip: true, maxTicksLimit: 8 } } },
plugins: { legend: { labels: { color: '#a8b0c3' } } }
}
});

function fixCanvasResolution(chart) {
const dpr = window.devicePixelRatio || 1;
const canvas = chart.canvas, parent = canvas.parentNode, rect = parent.getBoundingClientRect();
const w = Math.floor(rect.width), h = Math.floor(rect.height);
canvas.style.width = w + 'px'; canvas.style.height = h + 'px';
canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr);
chart.resize();
}
function updateFonts(chart) {
const w = chart.canvas.clientWidth || 300;
const base = Math.max(10, Math.min(14, Math.round(w / 40)));
const small = Math.max(9, base - 1);
chart.options.scales.x.ticks.font = { size: small };
chart.options.scales.y.ticks.font = { size: small };
if (chart.options.plugins?.legend?.labels) chart.options.plugins.legend.labels.font = { size: base };
}
const ro = new ResizeObserver(() => [cpuTotalChart, cpuCoresChart, cpuFreqChart].forEach(c => { fixCanvasResolution(c); updateFonts(c); c.update('none'); }));
ro.observe(document.querySelector('.grid'));

// 脱敏
const maskState = { on: true };
const maskICCID = v => v ? v.replace(/(\\d{4})\\d+(\\d{4})/, '$1****$2') : '-';
const maskIMEI = v => v ? v.replace(/(\\d{3})\\d+(\\d{2})/, '$1****$2') : '-';
const maskIPv6 = v => v ? v.replace(/(^.{4}).*(.{4}$)/, '$1:****:$2') : '-';
const btnMask = document.getElementById('btn-mask');
if (btnMask) {
btnMask.setAttribute('aria-pressed', 'true');
btnMask.addEventListener('click', () => {
maskState.on = !maskState.on;
btnMask.setAttribute('aria-pressed', maskState.on ? 'true' : 'false');
btnMask.textContent = maskState.on ? '脱敏:开' : '脱敏:关';
try { updateKpis(window.UFI_DATA || {}); } catch(e){}
});
}

function el_dashboard(id){ return document.getElementById(id); }
function updateKpis(data) {
el_dashboard('kpi-network-type').textContent = data.network_type == "20" ? "5G" : data.network_type == "12" ? "4G" : (data.network_type || "3G");
el_dashboard('kpi-provider').textContent = data.network_provider ?? '-';
el_dashboard('kpi-ipv6').textContent = maskState.on ? maskIPv6(data.ipv6_wan_ipaddr ?? '') : (data.ipv6_wan_ipaddr ?? '-');
el_dashboard('kpi-lan').textContent = data.lan_ipaddr ?? '-';
el_dashboard('kpi-sinr').textContent = (data.Nr_snr || data.Lte_snr || '-') + '';
el_dashboard('kpi-rsrp').textContent = (data.nr_rsrp || data.Z5g_rsrp || data.lte_rsrp || data.Lte_signal_strength || '-') + '';
el_dashboard('kpi-qci').innerHTML = window.UFI_DATA.QORS_MESSAGE.replace('⬇️',"<br/>⬇️") || "-";
el_dashboard('kpi-rx').textContent = toMbps(data.realtime_rx_thrpt ?? 0);
el_dashboard('kpi-tx').textContent = toMbps(data.realtime_tx_thrpt ?? 0);
el_dashboard('kpi-daily').textContent = toGB(data.daily_data ?? 0);
el_dashboard('kpi-sta').textContent = data.wifi_access_sta_num ?? '-';
el_dashboard('kpi-model').textContent = data.model ?? '-';
el_dashboard('kpi-version').textContent = \`\${data.app_ver ?? '-'} (\${data.app_ver_code ?? '-'})\`;

const nrInfo = [
data.Nr_bands ? \`频段:N\${data.Nr_bands}<br>\` : null,
data.Nr_fcn ? \`频点:\${data.Nr_fcn}<br>\` : null,
data.Nr_pci ? \`PCI:\${data.Nr_pci}\` : null,
].filter(Boolean).join(' ');
const lteInfo = [
data.Lte_bands ? \`频段:B\${data.Lte_bands}<br>\` : null,
data.Lte_fcn ? \` 频点:\${data.Lte_fcn}<br>\` : null,
data.Lte_pci ? \` PCI:\${data.Lte_pci}\` : null,
].filter(Boolean).join('');
const iccid = data.iccid ?? '';
const imei = data.imei ?? '';

const elNR = el_dashboard('kpi-nr'); if (elNR) elNR.innerHTML = nrInfo || '-';
const elLTE = el_dashboard('kpi-lte'); if (elLTE) elLTE.innerHTML = lteInfo || '-';
const elICCID = el_dashboard('kpi-iccid'); if (elICCID) elICCID.textContent = maskState.on ? maskICCID(iccid) : (iccid || '-');
const elIMEI = el_dashboard('kpi-imei'); if (elIMEI) elIMEI.textContent = maskState.on ? maskIMEI(imei) : (imei || '-');

// 顶部
const battPercent = Number(data.battery || data.battery_vol_percent || 0);
const battFill = el_dashboard('battery-fill'), battText = el_dashboard('battery-text');
const battBody = document.querySelector('.battery-body');
if(battBody) {
const icon = el_dashboard('icon-battery')
const signal = el_dashboard('icon-signal')

if(data.battery_charging == '') {
icon && (icon.style.display = 'none');
signal && (signal.style.marginRight = '10px');
} else {
icon && (icon.style.display = '');
signal && (signal.style.marginRight = '');
}
data.battery_charging == '1' ? battBody.classList.add('charging') : battBody.classList.remove('charging');
}
if (battFill) {
battFill.style.width = \`\${Math.max(0, Math.min(100, battPercent))}%\`
};
if (battText) battText.textContent = battPercent ? \`\${battPercent}%\` : '-';
const bars = document.querySelectorAll('#icon-signal .bar');
const rssiBars = Number(data.rssi || data.nr_rssi || data.network_signalbar || data.network_rssi || 0);
bars.forEach((b, i) => { if (i < rssiBars) b.classList.add('on'); else b.classList.remove('on'); });
}

function barFill(id, p, mode){ const f = el_dashboard(id); if (!f) return; f.style.width = \`\${clamp(p,0,100)}%\`; if(mode==='temp'){ let c=p; let g='linear-gradient(90deg,#4aa8ff,#2a78ff)'; if(c>=80) g='linear-gradient(90deg,#ef4444,#f97316)'; else if(c>=65) g='linear-gradient(90deg,#f59e0b,#fbbf24)'; f.style.background=g; } }
function updateTemps(data){
const cpuC = celsiusFromMilli(data.cpu_temp || 0);
let paMilli = 0; if (Array.isArray(data.cpu_temp_list)) { const pa = data.cpu_temp_list.find(z => z.type && z.type.indexOf('pa-thmzone') !== -1); paMilli = pa ? Number(pa.temp) : 0; }
const socC = celsiusFromMilli(paMilli);
barFill('bar-temp-cpu', clamp(cpuC,0,100), 'temp'); barFill('bar-temp-soc', clamp(socC,0,100), 'temp');
el_dashboard('val-temp-cpu').textContent = \`\${cpuC.toFixed(1)} °C\`; el_dashboard('val-temp-soc').textContent = paMilli ? \`\${socC.toFixed(1)} °C\` : '-';
const usedB = Number((data.memInfo||{}).mem_used_kb||0)*1024; const totalB = Number((data.memInfo||{}).mem_total_kb||0)*1024;
el_dashboard('temp-mem-label').textContent = \`最高 \${Math.max(cpuC,socC).toFixed(1)} °C · 内存 \${toGB(usedB)} / \${toGB(totalB)}\`;
}
function updateMemory(data){
const m = data.memInfo || {};
const memPct = Number(m.mem_usage_percent || 0), swapPct = Number(m.swap_usage_percent || 0);
barFill('bar-mem', memPct); barFill('bar-swap', swapPct);
el_dashboard('val-mem').textContent = toPercentText(memPct); el_dashboard('val-swap').textContent = toPercentText(swapPct);
}
function updateStorage(data){
const intUsed = Number(data.internal_used_storage||0), intTotal = Number(data.internal_total_storage||0);
const intPct = intTotal ? clamp((intUsed/intTotal)*100,0,100) : 0;
const intRow = document.getElementById('row-storage-int'); if (intRow) intRow.style.display = intTotal ? '' : 'none';
barFill('bar-storage-int', intPct); const iv = el_dashboard('val-storage-int'); if (iv) iv.textContent = intTotal ? \`\${toGB(intUsed)} / \${toGB(intTotal)}\` : '-';

const extUsed = Number(data.external_used_storage||0), extTotal = Number(data.external_total_storage||0);
const extPct = extTotal ? clamp((extUsed/extTotal)*100,0,100) : 0;
const extRow = document.getElementById('row-storage-ext'); if (extRow) extRow.style.display = extTotal ? '' : 'none';
barFill('bar-storage-ext', extPct); const ev = el_dashboard('val-storage-ext'); if (ev) ev.textContent = extTotal ? \`\${toGB(extUsed)} / \${toGB(extTotal)}\` : '-';
}
function updateCpuTotal(data){
const now = new Date(), label = now.toLocaleTimeString();
const usage = Number(data.cpu_usage ?? (data.cpuUsageInfo?.cpu ?? 0));
el_dashboard('cpu-total-label').textContent = toPercentText(usage);
const L = cpuTotalChart.data.labels, S = cpuTotalChart.data.datasets[0].data;
L.push(label); S.push(clamp(usage,0,100)); if(L.length>60){L.shift(); S.shift();}
cpuTotalChart.update();
}
function updateCpuCores(data){
const info = data.cpuUsageInfo || {}; const labels = [], values = [];
for(let i=0;i<8;i++){ const k = \`cpu\${i}\`; if(info[k]!==undefined){ labels.push(k.toUpperCase()); values.push(clamp(Number(info[k]),0,100)); } }
cpuCoresChart.data.labels = labels; cpuCoresChart.data.datasets[0].data = values;
el_dashboard('cpu-cores-label').textContent = values.map(v=>v.toFixed(0)).join('% ')+(values.length?'%':'');
cpuCoresChart.update();
}
function updateCpuFreq(data){
const f = data.cpuFreqInfo || {}; const labels=[], cur=[], max=[];
Object.keys(f).sort((a,b)=>Number(a.replace('cpu',''))-Number(b.replace('cpu',''))).forEach(k=>{ labels.push(k.toUpperCase()); cur.push(Number(f[k].cur||0)); max.push(Number(f[k].max||0)); });
cpuFreqChart.data.labels = labels; cpuFreqChart.data.datasets[0].data = cur; cpuFreqChart.data.datasets[1].data = max;
el_dashboard('cpu-freq-label').textContent = cur.length ? \`Max: \${Math.max(...max)} MHz\` : '-';
cpuFreqChart.update();
}
function updateSignalBars(data){
const sinr = Number(data.Nr_snr || data.Lte_snr || 0); const sinrPct = clamp(((sinr - (-10)) / (13 - (-10))) * 100, 0, 100);
const sf = el_dashboard('bar-sinr'); if (sf){ sf.classList.remove('level-strong','level-medium','level-weak'); sf.style.width = sinrPct + '%'; if (sinr >= 13) sf.classList.add('level-strong'); else if (sinr > 0) sf.classList.add('level-medium'); else sf.classList.add('level-weak'); }
el_dashboard('val-sinr').textContent = isFinite(sinr) ? \`\${sinr.toFixed(0)} dB\` : '-';

const rsrp = Number(data.nr_rsrp ?? data.Z5g_rsrp ?? data.lte_rsrp ?? 0);
const rsrpPct = clamp(((rsrp - (-125)) / ((-81) - (-125))) * 100, 0, 100);
const rf = el_dashboard('bar-rsrp'); if (rf){ rf.classList.remove('level-strong','level-medium','level-weak'); rf.style.width = rsrpPct + '%'; if (rsrp >= -90) rf.classList.add('level-strong'); else if (rsrp >= -100) rf.classList.add('level-medium'); else rf.classList.add('level-weak'); }
el_dashboard('val-rsrp').textContent = isFinite(rsrp) ? \`\${rsrp.toFixed(0)} dBm\` : '-';
el_dashboard('signal-label').textContent = 'SINR/RSRP';
}

function tick(){
refreshDataFromParent();
const d = window.UFI_DATA || {};
updateKpis(d); updateCpuTotal(d); updateCpuCores(d); updateCpuFreq(d);
updateTemps(d); updateMemory(d); updateStorage(d); updateThroughput(d); updateSignalBars(d);
}
function updateThroughput(d){
const usedMonth = Number((d.monthly_tx_bytes||0)) + Number((d.monthly_rx_bytes||0));
let text = toHumanBytes(usedMonth);
if (Number(d.data_volume_limit_switch) === 1 && typeof d.data_volume_limit_size === 'string') {
const [numStr, unitStr] = d.data_volume_limit_size.split('_'); const num = Number(numStr); const unitMB = Number(unitStr);
const totalBytes = num * unitMB * 1024 * 1024; text = \`\${toHumanBytes(usedMonth)} / \${toHumanBytes(totalBytes)}\`;
}
const elMonth = el_dashboard('kpi-month'); if (elMonth) elMonth.textContent = text;
}

try { tick(); } catch(e){ console.error(e); }
setInterval(() => { try {
tick();
} catch(e){ console.error(e); } }, 1000);
});
})();
</kano_script>
</kano_body>
</kano_html>`;

// 写入 iframe
iframe.srcdoc = html
.replaceAll('kano_html', 'html')
.replaceAll('kano_head', 'head')
.replaceAll('kano_style', 'style')
.replaceAll('kano_body', 'body')
.replaceAll('kano_script', 'script')
.replaceAll('kano_meta', 'meta')
.replaceAll('<kano_!DOCTYPE html>', '<!DOCTYPE html>');

// 向 iframe 主动同步父页面数据(跨域安全:仅发送,不读取)
iframe.addEventListener('load', () => {
try {
setInterval(() => {
iframe.contentWindow.postMessage({ type: 'QORS_MESSAGE', payload: QORS_MESSAGE }, '*')
}, 1000);
} catch (e) { console.warn('postMessage bridge failed', e); }
});

})();
//</script>
<!-- [KANO_PLUGIN_END] AI大屏数据看板插件1.1.2.js -->



<!-- [KANO_PLUGIN_START] 射频吞吐性能增强开关.txt -->
<script>
(() => {
const refreshBtn_kano = document.createElement('button')
refreshBtn_kano.classList.add('btn')
refreshBtn_kano.textContent = "射频性能增强"
let status = false

const checkAdvanceFunc = async () => {
const res = await runShellWithRoot('whoami')
if (res.content) {
if (res.content.includes('root')) {
return true
}
}
return false
}

const togglePM = async (flag = false) => {
try {
if (flag) {
await handleAT('AT+SPASENGMD =\"#las_set_speedtest_control\",1')
const res = await runShellWithRoot('settings put global perfermance_enhancement 1')
if (!res.success) {
throw new Error('设置失败')
}
}
else {
await handleAT('AT+SPASENGMD =\"#las_set_speedtest_control\",0')
const res = await runShellWithRoot('settings put global perfermance_enhancement 0')
if (!res.success) {
throw new Error('设置失败')
}
}
} catch {
createToast('射频性能增强切换失败')
}
}

const init = async () => {
const res = await runShellWithRoot('settings get global perfermance_enhancement')
if (res.content) {
if (res.content.includes('1')) {
status = true
refreshBtn_kano.style.backgroundColor = 'var(--dark-btn-color-active)'
}
}
}

const start = async () => {
const canUse = await checkAdvanceFunc()
if (!canUse) {
createToast('请先打开高级功能')
return
}
if (status) {
await togglePM(false)
status = false
createToast('已关闭射频性能增强')
refreshBtn_kano.style.backgroundColor = ''
}
else {
await togglePM(true)
status = true
refreshBtn_kano.style.backgroundColor = 'var(--dark-btn-color-active)'
createToast('已开启射频性能增强')
}
}

refreshBtn_kano.onclick = start

collapseBtn_menu.nextElementSibling.querySelector('.collapse_box').appendChild(refreshBtn_kano)

setTimeout(() => {
init()
}, 300);
})()
</script>
<!-- [KANO_PLUGIN_END] 射频吞吐性能增强开关.txt -->

0 Downloads (87.7 KB)