
/***********************
* 固定配置(直接改这里)
***********************/
const baseUrl = "https://api.64clouds.com/v1";
const veid = "2012715";
const api_key = "private_X12fsdffeg345gdfg";
const vpsTitle = 'MEGABOX-PRO'
// ─────────────────────────────────────────────
// 工具函数
// ─────────────────────────────────────────────
function formatBytes(bytes, decimals = 1) {
const n = Number(bytes || 0);
if (n === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(n) / Math.log(k));
return `${(n / Math.pow(k, i)).toFixed(decimals)} ${sizes[i]}`;
}
function clamp(v, min, max) {
return Math.max(min, Math.min(max, v));
}
function timestampToDate(ts) {
if (!ts) return null;
const d = new Date(Number(ts) * 1000);
return isNaN(d.getTime()) ? null : d;
}
function daysUntil(date) {
if (!date) return null;
return Math.ceil((date.getTime() - Date.now()) / 86400000);
}
// 根据使用率返回颜色
function ratioColor(ratio) {
if (ratio >= 0.9) return new Color("#FF453A");
if (ratio >= 0.75) return new Color("#FF9F0A");
return new Color("#30D158");
}
// Light/Dark 动态颜色
function dc(light, dark) {
return Color.dynamic(new Color(light), new Color(dark));
}
// ─────────────────────────────────────────────
// DrawContext 绘图工具
// ─────────────────────────────────────────────
function makeProgressBar(ratio, width, height, fillColor) {
const ctx = new DrawContext();
ctx.size = new Size(width, height);
ctx.opaque = false;
ctx.respectScreenScale = true;
const r = height / 2;
// 背景轨道
const bgPath = new Path();
bgPath.addRoundedRect(new Rect(0, 0, width, height), r, r);
ctx.setFillColor(dc("#E5E5EA", "#3A3A3C"));
ctx.addPath(bgPath);
ctx.fillPath();
// 填充
const filled = Math.max(r * 2, width * clamp(ratio, 0, 1));
const fgPath = new Path();
fgPath.addRoundedRect(new Rect(0, 0, filled, height), r, r);
ctx.setFillColor(fillColor || ratioColor(ratio));
ctx.addPath(fgPath);
ctx.fillPath();
return ctx.getImage();
}
// ─────────────────────────────────────────────
// UI 组件辅助
// ─────────────────────────────────────────────
// 小徽章
function addBadge(stack, label, bgHex, fgHex = "#FFFFFF", alpha = 0.9) {
const badge = stack.addStack();
badge.backgroundColor = new Color(bgHex, alpha);
badge.cornerRadius = 4;
badge.setPadding(2, 5, 2, 5);
const t = badge.addText(label);
t.font = Font.boldSystemFont(8);
t.textColor = new Color(fgHex);
}
// 分区标签
function addSectionTitle(parent, title) {
const row = parent.addStack();
row.centerAlignContent();
const t = row.addText(title.toUpperCase());
t.font = Font.boldSystemFont(9);
t.textColor = dc("#9A9A9E", "#636366");
row.addSpacer();
}
// 左色块 + 标签 + 右值
function addSpecRow(parent, label, value, dotColor) {
const row = parent.addStack();
row.centerAlignContent();
const dot = row.addText("■");
dot.font = Font.boldSystemFont(9);
dot.textColor = dotColor;
row.addSpacer(5);
const lbl = row.addText(label);
lbl.font = Font.systemFont(11);
lbl.textColor = dc("#6C6C70", "#8E8E93");
row.addSpacer();
const val = row.addText(value);
val.font = Font.boldSystemFont(11);
val.textColor = dc("#1C1C1E", "#EBEBF5");
}
// ─────────────────────────────────────────────
// API 请求
// ─────────────────────────────────────────────
async function loadServiceInfo() {
const url = `${baseUrl}/getServiceInfo?veid=${veid}&api_key=${api_key}`;
const req = new Request(url);
req.method = "GET";
const json = await req.loadJSON();
if (json.error && json.error !== 0) throw new Error(json.message || "API 错误");
return json;
}
// ─────────────────────────────────────────────
// 渲染:小尺寸
// ─────────────────────────────────────────────
async function renderSmall(widget, data) {
const multiplier = Number(data.monthly_data_multiplier || 1);
const total = Number(data.plan_monthly_data || 0) * multiplier;
const used = Number(data.data_counter || 0) * multiplier;
const ratio = clamp(total > 0 ? used / total : 0, 0, 1);
const resetDate = timestampToDate(data.data_next_reset);
const daysLeft = daysUntil(resetDate);
const ip = (data.ip_addresses || [])[0] || "N/A";
const color = ratioColor(ratio);
const isSuspended = !!data.suspended;
const grad = new LinearGradient();
grad.locations = [0, 1];
grad.colors = [dc("#F2F2F7", "#1C1C1E"), dc("#FFFFFF", "#111113")];
widget.backgroundGradient = grad;
widget.setPadding(12, 14, 12, 14);
// 顶部:状态点 + 主机名
const topRow = widget.addStack();
topRow.centerAlignContent();
const statusDot = topRow.addText("●");
statusDot.font = Font.boldSystemFont(10);
statusDot.textColor = isSuspended ? new Color("#FF453A") : new Color("#30D158");
topRow.addSpacer(5);
const hostT = topRow.addText((vpsTitle || "VPS").split(".")[0]);
hostT.font = Font.boldSystemFont(12);
hostT.textColor = dc("#000000", "#FFFFFF");
hostT.lineLimit = 1;
hostT.minimumScaleFactor = 0.6;
widget.addSpacer(6);
// 大百分比
const pctT = widget.addText(`${Math.round(ratio * 100)}%`);
pctT.font = Font.boldSystemFont(38);
pctT.textColor = color;
widget.addSpacer(4);
// 进度条
widget.addImage(makeProgressBar(ratio, 130, 7, color));
widget.addSpacer(5);
// 流量数值
const trafficT = widget.addText(`${formatBytes(used)} / ${formatBytes(total)}`);
trafficT.font = Font.systemFont(9);
trafficT.textColor = dc("#6C6C70", "#8E8E93");
trafficT.minimumScaleFactor = 0.7;
widget.addSpacer();
// 底部:IP + 剩余天数
const botRow = widget.addStack();
botRow.centerAlignContent();
const ipT = botRow.addText(ip);
ipT.font = new Font("Menlo", 9);
ipT.textColor = dc("#6C6C70", "#8E8E93");
botRow.addSpacer();
if (daysLeft !== null) {
const dT = botRow.addText(`${daysLeft}d`);
dT.font = Font.systemFont(9);
dT.textColor = daysLeft <= 3 ? new Color("#FF9F0A") : dc("#6C6C70", "#8E8E93");
}
}
// ─────────────────────────────────────────────
// 渲染:中号
// ─────────────────────────────────────────────
async function renderMedium(widget, data) {
const multiplier = Number(data.monthly_data_multiplier || 1);
const total = Number(data.plan_monthly_data || 0) * multiplier;
const used = Number(data.data_counter || 0) * multiplier;
const ratio = clamp(total > 0 ? used / total : 0, 0, 1);
const resetDate = timestampToDate(data.data_next_reset);
const daysLeft = daysUntil(resetDate);
const ip = (data.ip_addresses || [])[0] || "N/A";
const isSuspended = !!data.suspended;
const color = ratioColor(ratio);
const vmType = (data.vm_type || "KVM").toUpperCase();
const osRaw = data.os || "";
const osShort = osRaw.split("-").slice(0, 2).join(" ");
const grad = new LinearGradient();
grad.locations = [0, 1];
grad.colors = [dc("#F7F7F7", "#1C1C1E"), dc("#FFFFFF", "#111113")];
widget.backgroundGradient = grad;
widget.setPadding(14, 16, 14, 16);
// ── 标题行 ──
const topRow = widget.addStack();
topRow.centerAlignContent();
const statusDot = topRow.addText("●");
statusDot.font = Font.boldSystemFont(11);
statusDot.textColor = isSuspended ? new Color("#FF453A") : new Color("#30D158");
topRow.addSpacer(6);
const hostT = topRow.addText(vpsTitle || "VPS");
hostT.font = Font.boldSystemFont(15);
hostT.textColor = dc("#000000", "#FFFFFF");
hostT.lineLimit = 1;
hostT.minimumScaleFactor = 0.65;
topRow.addSpacer();
addBadge(topRow, vmType, vmType === "KVM" ? "#0A84FF" : "#9B59B6");
if (data.location_ipv6_ready) {
topRow.addSpacer(4);
addBadge(topRow, "IPv6", "#30D158");
}
if (isSuspended) {
topRow.addSpacer(4);
addBadge(topRow, "暂停", "#FF453A");
}
widget.addSpacer(3);
// ── 副信息行 ──
const subRow = widget.addStack();
subRow.centerAlignContent();
const locT = subRow.addText(`📍 ${data.node_location || "Unknown"}`);
locT.font = Font.systemFont(10);
locT.textColor = dc("#6C6C70", "#8E8E93");
subRow.addSpacer(8);
const planT = subRow.addText(data.plan || "");
planT.font = Font.systemFont(10);
planT.textColor = dc("#6C6C70", "#8E8E93");
planT.lineLimit = 1;
planT.minimumScaleFactor = 0.7;
widget.addSpacer(10);
// ── 流量主区域 ──
const mainRow = widget.addStack();
mainRow.centerAlignContent();
// 左:大百分比
const leftCol = mainRow.addStack();
leftCol.layoutVertically();
const pctT = leftCol.addText(`${Math.round(ratio * 100)}%`);
pctT.font = Font.boldSystemFont(40);
pctT.textColor = color;
pctT.minimumScaleFactor = 0.7;
const pctLabel = leftCol.addText("月流量使用");
pctLabel.font = Font.systemFont(9);
pctLabel.textColor = dc("#9A9A9E", "#636366");
mainRow.addSpacer();
// 右:详细数值
const rightCol = mainRow.addStack();
rightCol.layoutVertically();
const usedT = rightCol.addText(formatBytes(used));
usedT.font = Font.boldSystemFont(14);
usedT.textColor = dc("#1C1C1E", "#EBEBF5");
usedT.rightAlignText();
const totalT = rightCol.addText(`共 ${formatBytes(total)}`);
totalT.font = Font.systemFont(10);
totalT.textColor = dc("#6C6C70", "#8E8E93");
totalT.rightAlignText();
rightCol.addSpacer(6);
const resetT = rightCol.addText(
daysLeft !== null ? `${daysLeft} 天后重置` : "—"
);
resetT.font = Font.systemFont(10);
resetT.textColor =
daysLeft !== null && daysLeft <= 3
? new Color("#FF9F0A")
: dc("#6C6C70", "#8E8E93");
resetT.rightAlignText();
widget.addSpacer(8);
// ── 进度条 ──
widget.addImage(makeProgressBar(ratio, 290, 8, color));
widget.addSpacer(10);
// ── 底部行:IP + OS ──
const botRow = widget.addStack();
botRow.centerAlignContent();
const ipT = botRow.addText(ip);
ipT.font = new Font("Menlo", 10);
ipT.textColor = dc("#3C3C43", "#EBEBF5");
botRow.addSpacer();
const osT = botRow.addText(osShort);
osT.font = Font.systemFont(10);
osT.textColor = dc("#6C6C70", "#8E8E93");
osT.lineLimit = 1;
osT.minimumScaleFactor = 0.7;
}
// ─────────────────────────────────────────────
// 渲染:大尺寸
// ─────────────────────────────────────────────
async function renderLarge(widget, data) {
const multiplier = Number(data.monthly_data_multiplier || 1);
const total = Number(data.plan_monthly_data || 0) * multiplier;
const used = Number(data.data_counter || 0) * multiplier;
const ratio = clamp(total > 0 ? used / total : 0, 0, 1);
const resetDate = timestampToDate(data.data_next_reset);
const daysLeft = daysUntil(resetDate);
const ip = (data.ip_addresses || [])[0] || "N/A";
const isSuspended = !!data.suspended;
const color = ratioColor(ratio);
const vmType = (data.vm_type || "KVM").toUpperCase();
const osRaw = data.os || "";
const osShort = osRaw.split("-").slice(0, 2).join(" ");
const ram = Number(data.plan_ram || 0);
const disk = Number(data.plan_disk || 0);
const swap = Number(data.plan_swap || 0);
const abusePoints = Number(data.total_abuse_points || 0);
const maxAbuse = Number(data.max_abuse_points || 100);
const abuseRatio = clamp(maxAbuse > 0 ? abusePoints / maxAbuse : 0, 0, 1);
const suspCount = Number(data.suspension_count || 0);
const policyViolation = !!data.policy_violation;
const grad = new LinearGradient();
grad.locations = [0, 1];
grad.colors = [dc("#F7F7F7", "#1C1C1E"), dc("#FFFFFF", "#111113")];
widget.backgroundGradient = grad;
widget.setPadding(16, 16, 16, 16);
// ── 标题行 ──
const topRow = widget.addStack();
topRow.centerAlignContent();
const statusDot = topRow.addText("●");
statusDot.font = Font.boldSystemFont(12);
statusDot.textColor = isSuspended ? new Color("#FF453A") : new Color("#30D158");
topRow.addSpacer(6);
const hostT = topRow.addText(vpsTitle || "VPS");
hostT.font = Font.boldSystemFont(16);
hostT.textColor = dc("#000000", "#FFFFFF");
hostT.lineLimit = 1;
hostT.minimumScaleFactor = 0.65;
topRow.addSpacer();
addBadge(topRow, vmType, vmType === "KVM" ? "#0A84FF" : "#9B59B6");
if (data.location_ipv6_ready) {
topRow.addSpacer(4);
addBadge(topRow, "IPv6", "#30D158");
}
if (isSuspended) {
topRow.addSpacer(4);
addBadge(topRow, "已暂停", "#FF453A");
}
if (policyViolation) {
topRow.addSpacer(4);
addBadge(topRow, "违规", "#FF9F0A", "#1C1C1E");
}
widget.addSpacer(3);
// ── 副信息 ──
const infoRow = widget.addStack();
const infoT = infoRow.addText(
`📍 ${data.node_location || "Unknown"} · ${data.plan || ""} · ${osShort}`
);
infoT.font = Font.systemFont(10);
infoT.textColor = dc("#6C6C70", "#8E8E93");
infoT.lineLimit = 1;
infoT.minimumScaleFactor = 0.7;
widget.addSpacer(12);
// ══ 月流量 ══
addSectionTitle(widget, "月流量");
widget.addSpacer(5);
const trafficRow = widget.addStack();
trafficRow.centerAlignContent();
const pctT = trafficRow.addText(`${Math.round(ratio * 100)}%`);
pctT.font = Font.boldSystemFont(34);
pctT.textColor = color;
pctT.minimumScaleFactor = 0.7;
trafficRow.addSpacer(14);
const trafficDetails = trafficRow.addStack();
trafficDetails.layoutVertically();
const usedT = trafficDetails.addText(`已用 ${formatBytes(used)}`);
usedT.font = Font.boldSystemFont(12);
usedT.textColor = dc("#1C1C1E", "#EBEBF5");
const totalT = trafficDetails.addText(`共 ${formatBytes(total)}`);
totalT.font = Font.systemFont(11);
totalT.textColor = dc("#6C6C70", "#8E8E93");
trafficDetails.addSpacer(3);
const resetT = trafficDetails.addText(
daysLeft !== null ? `⏱ ${daysLeft} 天后重置` : "⏱ —"
);
resetT.font = Font.systemFont(10);
resetT.textColor =
daysLeft !== null && daysLeft <= 3
? new Color("#FF9F0A")
: dc("#6C6C70", "#8E8E93");
widget.addSpacer(6);
widget.addImage(makeProgressBar(ratio, 292, 8, color));
widget.addSpacer(14);
// ══ 套餐规格 ══
addSectionTitle(widget, "套餐规格");
widget.addSpacer(6);
addSpecRow(widget, "RAM", formatBytes(ram, 0), new Color("#0A84FF"));
widget.addSpacer(5);
addSpecRow(widget, "磁盘", formatBytes(disk, 0), new Color("#30D158"));
widget.addSpacer(5);
addSpecRow(widget, "SWAP", formatBytes(swap, 0), new Color("#FF9F0A"));
widget.addSpacer(14);
// ══ 安全状态 ══
addSectionTitle(widget, "安全状态");
widget.addSpacer(5);
const abuseRow = widget.addStack();
abuseRow.centerAlignContent();
const abuseLabel = abuseRow.addText("滥用积分");
abuseLabel.font = Font.systemFont(11);
abuseLabel.textColor = dc("#6C6C70", "#8E8E93");
abuseRow.addSpacer();
const abuseVal = abuseRow.addText(`${abusePoints} / ${maxAbuse}`);
abuseVal.font = Font.boldSystemFont(11);
abuseVal.textColor =
abuseRatio >= 0.75 ? new Color("#FF453A") : dc("#1C1C1E", "#EBEBF5");
widget.addSpacer(4);
widget.addImage(makeProgressBar(abuseRatio, 292, 5, ratioColor(abuseRatio)));
widget.addSpacer(12);
// ══ 底部 ══
const botRow = widget.addStack();
botRow.centerAlignContent();
const ipT = botRow.addText(ip);
ipT.font = new Font("Menlo", 10);
ipT.textColor = dc("#3C3C43", "#EBEBF5");
botRow.addSpacer();
if (suspCount > 0) {
const suspT = botRow.addText(`暂停 ${suspCount} 次`);
suspT.font = Font.systemFont(10);
suspT.textColor = new Color("#FF9F0A");
botRow.addSpacer(8);
}
const ipCountText = botRow.addText(
`${(data.ip_addresses || []).length} IPs`
);
ipCountText.font = Font.systemFont(10);
ipCountText.textColor = dc("#6C6C70", "#8E8E93");
}
// ─────────────────────────────────────────────
// 主入口
// ─────────────────────────────────────────────
async function main() {
const widget = new ListWidget();
try {
const data = await loadServiceInfo();
const family = config.widgetFamily;
if (family === "small") {
await renderSmall(widget, data);
} else if (family === "large") {
await renderLarge(widget, data);
} else {
await renderMedium(widget, data);
}
} catch (e) {
widget.backgroundColor = new Color("#1C1C1E");
widget.setPadding(16, 16, 16, 16);
const title = widget.addText("⚠️ 加载失败");
title.font = Font.boldSystemFont(14);
title.textColor = new Color("#FF453A");
widget.addSpacer(6);
const msg = widget.addText(e.message || String(e));
msg.font = Font.systemFont(10);
msg.textColor = new Color("#8E8E93");
msg.lineLimit = 3;
}
Script.setWidget(widget);
Script.complete();
}
await main();