Puo's 菜园子 学习园地 http://puo.cn
易记网址: http://wordpress.cn.com

dmit 小组件分享

早上看见大佬分享的 scriptable dmit小组件
https://www.nodeseek.com/post-661149-1
优化了一下,多加了一些信息和小功能,需要可自取,因为我只有一台设备,单设备使用是没问题的,多设备不保证哈~~
20260325051341657
20260325051341787
20260325051342527

 

const ScriptName = “DMIT”;
const COOKIE_NAME = `${ScriptName}_WHMCSlogin_auth_tk`;
const PRODUCTS_CACHE_KEY = `${ScriptName}_products_cache`;
const CANVAS_SIZE = 50;
const RADIUS = 20;
const LINE_WIDTH = 4;
const productsIndex = args.widgetParameter ? parseInt(args.widgetParameter) – 1 : 0;
function getCookie() {
if (Keychain.contains(COOKIE_NAME)) {
return Keychain.get(COOKIE_NAME);
}
return null;
}
function getCachedProducts() {
if (Keychain.contains(PRODUCTS_CACHE_KEY)) {
try {
const cached = JSON.parse(Keychain.get(PRODUCTS_CACHE_KEY));
if (cached.data && cached.data.length > 0) return cached.data;
} catch (e) {}
}
return null;
}
function setCachedProducts(products) {
Keychain.set(PRODUCTS_CACHE_KEY, JSON.stringify({ data: products }));
}
function formatMB(mb) {
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`;
return `${mb.toFixed(1)} MB`;
}
function formatUptime(seconds) {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const mins = Math.floor((seconds % 3600) / 60);
return { days, hours, mins };
}
async function safeLoadJSON(request) {
const text = await request.loadString();
try {
return JSON.parse(text);
} catch (e) {
throw new Error(“服务器返回了非 JSON 数据,可能被 Cloudflare 拦截”);
}
}
async function loadProductDetailStandard(id) {
const resp = new Request(`https://www.dmit.io/clientarea.php?action=productdetails&id=${id}&json=1&pure=1&page=standard&subaction=whmcsdetail`);
resp.method = “GET”;
const cookie = getCookie();
if (!cookie) {
throw new Error(“未找到 Cookie”);
}
resp.headers = {
“Cookie”: cookie
};
const result = await safeLoadJSON(resp);
if (result.result === “success” && result.success) {
return result.success;
}
throw new Error(result.error || “未知错误”);
}
async function loadProducts() {
try {
const reqp = new Request(“https://www.dmit.io/clientarea.php?action=services”);
reqp.method = “GET”;
const cookie = getCookie();
if (!cookie) {
throw new Error(“未找到 Cookie”);
}
reqp.headers = {
“Cookie”: cookie
};
const htmlContent = await reqp.loadString();
const products = parseProductsFromHtml(htmlContent);
if (products.length > 0) {
setCachedProducts(products);
return products;
}
return getCachedProducts() || [];
}
catch (error) {
console.error(error);
return getCachedProducts() || [];
}
}
async function loadAllGraphData(id) {
try {
const resp = new Request(`https://www.dmit.io/clientarea.php?action=productdetails&id=${id}&json=1&pure=1&page=graph&subaction=charts`);
resp.method = “POST”;
resp.body = “data%5Btimeframe%5D=hour”;
const cookie = getCookie();
if (!cookie) throw new Error(“未找到 Cookie”);
resp.headers = { “Cookie”: cookie };
const result = await safeLoadJSON(resp);
if (result.result === “success” && result.success) return result.success;
return {};
} catch (error) {
console.error(error);
return {};
}
}
async function loadVMDetails(id) {
try {
const resp = new Request(`https://www.dmit.io/clientarea.php?action=productdetails&id=${id}&json=1&pure=1&page=home&subaction=detailsVM`);
resp.method = “GET”;
const cookie = getCookie();
if (!cookie) throw new Error(“未找到 Cookie”);
resp.headers = { “Cookie”: cookie };
const result = await safeLoadJSON(resp);
if (result.result === “success” && result.success) return result.success;
return null;
} catch (error) {
console.error(error);
return null;
}
}
function applyPrimaryTextColor(text) {
text.textColor = Color.dynamic(Color.black(), Color.white());
}
function applySecondaryTextColor(text) {
text.textColor = Color.dynamic(Color.gray(), Color.lightGray());
}
function pickProductWithFallback(products, preferredIndex) {
if (products.length === 0) {
return null;
}
const startIndex = Math.min(Math.max(preferredIndex, 0), products.length – 1);
for (let i = startIndex; i >= 0; i–) {
const product = products[i];
if (product) {
return product;
}
}
for (let i = startIndex + 1; i < products.length; i++) {
const product = products[i];
if (product) {
return product;
}
}
return null;
}
function showWidgetErrorAndFinish(widget, message) {
const errorText = widget.addText(message);
errorText.textColor = Color.red();
errorText.font = Font.semiboldSystemFont(14);
Script.setWidget(widget);
Script.complete();
}
async function handleSettingsMenu() {
const setting = new Alert();
setting.title = “设置”;
setting.message = “配置DMIT小组件”;
setting.addAction(“设置 Cookie”);
setting.addDestructiveAction(“删除 Cookie”);
setting.addAction(“取消”);
const response = await setting.present();
if (response === 0) {
const cookieInput = new Alert();
cookieInput.title = “输入 Cookie”;
cookieInput.message = “请输入您的 cookie 值:”;
cookieInput.addTextField(“Cookie”);
cookieInput.addAction(“保存”);
cookieInput.addAction(“取消”);
const cookieResponse = await cookieInput.present();
if (cookieResponse === 0) {
const cookieValue = (cookieInput.textFieldValue(0) ?? “”).trim();
if (!cookieValue) {
const invalidAlert = new Alert();
invalidAlert.title = “Cookie 输入有误”;
invalidAlert.message = “Cookie 不能为空,请重新输入。”;
invalidAlert.addAction(“好的”);
await invalidAlert.present();
}
else {
Keychain.set(COOKIE_NAME, cookieValue);
const confirmAlert = new Alert();
confirmAlert.title = “保存成功”;
confirmAlert.message = “您的 cookie 已成功保存。”;
confirmAlert.addAction(“好的”);
await confirmAlert.present();
}
}
return true;
}
if (response === 1) {
const confirmDelete = new Alert();
confirmDelete.title = “确认删除”;
confirmDelete.message = “您确定要删除已保存的 Cookie 吗?”;
confirmDelete.addDestructiveAction(“删除”);
confirmDelete.addAction(“取消”);
const deleteResponse = await confirmDelete.present();
if (deleteResponse === 0 && Keychain.contains(COOKIE_NAME)) {
Keychain.remove(COOKIE_NAME);
const deletedAlert = new Alert();
deletedAlert.title = “删除成功”;
deletedAlert.message = “已成功删除保存的 Cookie。”;
deletedAlert.addAction(“好的”);
await deletedAlert.present();
}
return true;
}
return false;
}
function parseProductsFromHtml(htmlContent) {
const products = [];
const cleanText = (value) => {
return value
.replace(/<[^>]*>/g, ” “)
.replace(/&nbsp;/g, ” “)
.replace(/\s+/g, ” “)
.trim();
};
const rowRegex = /<li[^>]*class=”[^”]*\bproducts-item\b[^”]*”[^>]*products_id=”(\d+)”[^>]*>[\s\S]*?<\/li>/g;
let rowMatch;
while ((rowMatch = rowRegex.exec(htmlContent)) !== null) {
const rowHtml = rowMatch[0];
const id = parseInt(rowMatch[1], 10);
const nameMatch = rowHtml.match(/<div class=”products-title-text”>([\s\S]*?)<\/div>/);
const name = nameMatch ? cleanText(nameMatch[1]) : “”;
const domainMatch = rowHtml.match(/<div class=”products-domain”>([\s\S]*?)<\/div>/);
let code = “”;
if (domainMatch) {
const domainText = cleanText(domainMatch[1]);
code = domainText.split(” – “)[0].trim();
}
if (!code) {
const fallbackCodeMatch = rowHtml.match(/products-name=”([^”]+)”/);
code = fallbackCodeMatch ? fallbackCodeMatch[1].trim() : “”;
}
if (!name || !code || Number.isNaN(id)) {
continue;
}
products.push({ id, name, code });
}
return products;
}
function gradientColorByRatio(ratio) {
const t = Math.max(0, Math.min(1, ratio));
const start = { r: 46, g: 204, b: 113 };
const end = { r: 231, g: 76, b: 60 };
const r = Math.round(start.r + (end.r – start.r) * t);
const g = Math.round(start.g + (end.g – start.g) * t);
const b = Math.round(start.b + (end.b – start.b) * t);
return new Color(`#${r.toString(16).padStart(2, “0”)}${g.toString(16).padStart(2, “0”)}${b.toString(16).padStart(2, “0”)}`);
}
function drawCircleProgress(progress) {
const clampedProgress = Math.max(0, Math.min(1, progress));
const context = new DrawContext();
context.size = new Size(CANVAS_SIZE, CANVAS_SIZE);
context.opaque = false;
context.respectScreenScale = true;
const center = new Point(CANVAS_SIZE / 2, CANVAS_SIZE / 2);
context.setStrokeColor(new Color(“#222”));
context.setLineWidth(LINE_WIDTH);
context.strokeEllipse(new Rect(center.x – RADIUS, center.y – RADIUS, RADIUS * 2, RADIUS * 2));
const totalDegrees = 360 * clampedProgress;
const stepDegrees = 3.6;
for (let i = 0; i < totalDegrees; i += stepDegrees) {
const ratio = totalDegrees === 0 ? 0 : i / totalDegrees;
context.setFillColor(gradientColorByRatio(ratio));
const angle = (i – 90) * (Math.PI / 180);
const x = center.x + RADIUS * Math.cos(angle);
const y = center.y + RADIUS * Math.sin(angle);
context.fillEllipse(new Rect(x – LINE_WIDTH / 2, y – LINE_WIDTH / 2, LINE_WIDTH, LINE_WIDTH));
}
const percentText = `${Math.round(clampedProgress * 100)}%`;
context.setTextColor(new Color(“#FFFFFF”));
context.setFont(Font.semiboldSystemFont(10));
context.setTextAlignedCenter();
context.drawTextInRect(percentText, new Rect(0, 18, CANVAS_SIZE, 14));
return context.getImage();
}
function createProgressBarImage(total, current, width, height, fillColor, backgroundColor) {
const context = new DrawContext();
context.size = new Size(width, height);
context.opaque = false;
context.respectScreenScale = true;
const bgPath = new Path();
bgPath.addRoundedRect(new Rect(0, 0, width, height), height / 2, height / 2);
context.setFillColor(backgroundColor);
context.addPath(bgPath);
context.fillPath();
const progressWidth = Math.floor(width * Math.min(current / total, 1));
const fillPath = new Path();
fillPath.addRoundedRect(new Rect(0, 0, progressWidth, height), height / 2, height / 2);
context.setFillColor(fillColor);
context.addPath(fillPath);
context.fillPath();
return context.getImage();
}
function createLineChartImage(data, width, height, lineColor = new Color(“#1F6FD4”), secondaryLineColor = new Color(“#16A085”)) {
const context = new DrawContext();
context.size = new Size(width, height);
context.opaque = false;
context.respectScreenScale = true;
const seriesList = Array.isArray(data[0])
? data.filter((series) => series.length > 0)
: [data];
if (seriesList.length === 0) {
return context.getImage();
}
const padding = 4;
const chartWidth = Math.max(1, width – padding * 2);
const chartHeight = Math.max(1, height – padding * 2);
const mergedData = seriesList.flat();
const minValue = Math.min(…mergedData);
const maxValue = Math.max(…mergedData);
const range = Math.max(1e-6, maxValue – minValue);
const getPoints = (series) => {
return series.map((value, index) => {
const x = series.length === 1
? padding + chartWidth / 2
: padding + (index / (series.length – 1)) * chartWidth;
const normalized = (value – minValue) / range;
const y = padding + chartHeight * (1 – normalized);
return new Point(x, y);
});
};
const drawSeries = (points, color, fillAlpha) => {
if (points.length === 0) {
return;
}
const areaPath = new Path();
areaPath.move(new Point(points[0].x, height – padding));
for (const point of points) {
areaPath.addLine(point);
}
areaPath.addLine(new Point(points[points.length – 1].x, height – padding));
areaPath.closeSubpath();
context.setFillColor(new Color(color.hex, fillAlpha));
context.addPath(areaPath);
context.fillPath();
if (points.length === 1) {
context.setFillColor(color);
context.fillEllipse(new Rect(points[0].x – 1.5, points[0].y – 1.5, 3, 3));
return;
}
const linePath = new Path();
linePath.move(points[0]);
for (let i = 1; i < points.length; i++) {
const mid = new Point((points[i – 1].x + points[i].x) / 2, (points[i – 1].y + points[i].y) / 2);
linePath.addQuadCurve(mid, points[i – 1]);
}
linePath.addLine(points[points.length – 1]);
context.setStrokeColor(color);
context.setLineWidth(1.6);
context.addPath(linePath);
context.strokePath();
};
drawSeries(getPoints(seriesList[0]), lineColor, 0.12);
if (seriesList.length > 1) {
drawSeries(getPoints(seriesList[1]), secondaryLineColor, 0.08);
}
return context.getImage();
}
async function renderAccessoryCircular(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const progress = productDetails.bwusage / productDetails.bwlimit;
const chartImage = drawCircleProgress(progress);
widget.addImage(chartImage);
Script.setWidget(widget);
}
async function renderAccessoryInline(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const progress = productDetails.bwusage / productDetails.bwlimit;
const chartImage = drawCircleProgress(progress);
widget.addImage(chartImage);
const label = widget.addText(`${product.code}|${Math.round(progress * 100)}%`);
label.font = Font.semiboldSystemFont(12);
applyPrimaryTextColor(label);
Script.setWidget(widget);
}
async function renderAccessoryRectangular(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const line1Stack = widget.addStack();
const ipText = line1Stack.addText(`IP: ${productDetails.dedicatedip}`);
ipText.font = Font.semiboldSystemFont(12);
applyPrimaryTextColor(ipText);
line1Stack.addSpacer();
const updateTimeText = line1Stack.addText(new Date().toLocaleTimeString([], { hour: “2-digit”, minute: “2-digit” }));
updateTimeText.font = Font.semiboldSystemFont(12);
applySecondaryTextColor(updateTimeText);
const line2Stack = widget.addStack();
const trafficChart = createProgressBarImage(productDetails.bwlimit, productDetails.bwusage, 100, 12, new Color(“#1F6FD4”), new Color(“#E0E0E0”));
line2Stack.addImage(trafficChart);
const line3Stack = widget.addStack();
const usedGb = (productDetails.bwusage / 1024).toFixed(2);
const totalGb = (productDetails.bwlimit / 1024).toFixed(2);
line3Stack.addSpacer();
const trafficText = line3Stack.addText(`流量: ${usedGb}GB / ${totalGb}GB`);
trafficText.font = Font.semiboldSystemFont(10);
applySecondaryTextColor(trafficText);
line3Stack.addSpacer();
Script.setWidget(widget);
}
async function renderSmall(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const isDark = Device.isUsingDarkAppearance();
const primary = Color.dynamic(Color.black(), Color.white());
const secondary = Color.dynamic(new Color(“#6C6C70”), new Color(“#8E8E93”));
const green = new Color(“#30D158”);
const red = new Color(“#FF3B30”);
const progressBg = isDark ? new Color(“#3A3A3C”) : new Color(“#D1D1D6”);
widget.backgroundColor = Color.dynamic(new Color(“#FFFFFF”), new Color(“#1C1C1E”));
widget.setPadding(12, 12, 12, 12);
// 头部行:名称 + 状态
const headerRow = widget.addStack();
headerRow.centerAlignContent();
const nameText = headerRow.addText(productDetails.hostname || product.name);
nameText.font = Font.boldSystemFont(13);
nameText.textColor = primary;
nameText.lineLimit = 1;
const codeText = widget.addText(product.code);
codeText.font = Font.systemFont(9);
codeText.textColor = secondary;
widget.addSpacer(4);
// 流量卡片
const trafficCard = widget.addStack();
trafficCard.layoutVertically();
trafficCard.backgroundColor = Color.dynamic(new Color(“#F2F2F7”), new Color(“#2C2C2E”));
trafficCard.cornerRadius = 10;
trafficCard.setPadding(6, 10, 6, 10);
// 标题行
const trafficHeader = trafficCard.addStack();
trafficHeader.centerAlignContent();
const globeSym = SFSymbol.named(“globe”);
globeSym.applyFont(Font.systemFont(9));
const globeImg = trafficHeader.addImage(globeSym.image);
globeImg.imageSize = new Size(10, 10);
globeImg.tintColor = secondary;
trafficHeader.addSpacer(3);
const trafficLabel = trafficHeader.addText(“Traffic”);
trafficLabel.font = Font.systemFont(9);
trafficLabel.textColor = secondary;
trafficHeader.addSpacer();
const usedGB = (productDetails.bwusage / 1024).toFixed(1);
const totalGB = (productDetails.bwlimit / 1024).toFixed(1);
const usageLabel = trafficHeader.addText(`${usedGB}/${totalGB} GB`);
usageLabel.font = Font.systemFont(9);
usageLabel.textColor = secondary;
trafficCard.addSpacer(2);
// 百分比 + 剩余
const usagePercent = Math.round(productDetails.bwusage / productDetails.bwlimit * 100);
const pctRow = trafficCard.addStack();
pctRow.centerAlignContent();
const percentText = pctRow.addText(`${usagePercent}%`);
percentText.font = Font.boldSystemFont(28);
percentText.textColor = usagePercent >= 80 ? red : green;
pctRow.addSpacer();
const remainGB = ((productDetails.bwlimit – productDetails.bwusage) / 1024).toFixed(1);
const remainText = pctRow.addText(`剩余 ${remainGB} GB`);
remainText.font = Font.systemFont(9);
remainText.textColor = secondary;
trafficCard.addSpacer(2);
// 进度条
const progressBar = createProgressBarImage(
productDetails.bwlimit, productDetails.bwusage,
130, 6, green, progressBg
);
const barRow = trafficCard.addStack();
barRow.size = new Size(0, 6);
const progressImg = barRow.addImage(progressBar);
progressImg.imageSize = new Size(130, 6);
widget.addSpacer(4);
// 底部信息行
const bottomRow = widget.addStack();
bottomRow.centerAlignContent();
const regDate = new Date(productDetails.regdate);
if (!isNaN(regDate.getTime())) {
const now = new Date();
const regDay = regDate.getDate();
const lastReset = new Date(now.getFullYear(), now.getMonth(), regDay);
if (lastReset > now) lastReset.setMonth(lastReset.getMonth() – 1);
const nextReset = new Date(lastReset);
nextReset.setMonth(nextReset.getMonth() + 1);
const daysUntilReset = Math.ceil((nextReset – now) / 86400000);
const resetText = bottomRow.addText(`${daysUntilReset}天后重置`);
resetText.font = Font.systemFont(9);
resetText.textColor = secondary;
}
bottomRow.addSpacer();
const invoiceDateStr = productDetails.nextinvoicedate || “”;
const invoiceDate = new Date(invoiceDateStr);
if (!isNaN(invoiceDate.getTime())) {
const daysLeft = Math.ceil((invoiceDate.getTime() – Date.now()) / 86400000);
const renewBadge = bottomRow.addStack();
renewBadge.backgroundColor = new Color(green.hex, 0.2);
renewBadge.cornerRadius = 4;
renewBadge.setPadding(1, 4, 1, 4);
const renewText = renewBadge.addText(`续费 ${daysLeft}d`);
renewText.font = Font.boldSystemFont(9);
renewText.textColor = daysLeft <= 30 ? red : green;
}
Script.setWidget(widget);
}
async function renderMedium(widget, product) {
const productDetails = await loadProductDetailStandard(product.id.toString());
const isDark = Device.isUsingDarkAppearance();
const primary = Color.dynamic(Color.black(), Color.white());
const secondary = Color.dynamic(new Color(“#6C6C70”), new Color(“#8E8E93”));
const green = new Color(“#30D158”);
const red = new Color(“#FF3B30”);
const blue = new Color(“#007AFF”);
const orange = new Color(“#FF9F0A”);
const progressBg = isDark ? new Color(“#3A3A3C”) : new Color(“#D1D1D6”);
widget.backgroundColor = Color.dynamic(new Color(“#FFFFFF”), new Color(“#1C1C1E”));
widget.setPadding(12, 14, 12, 14);
const nameText = widget.addText(productDetails.hostname || product.name);
nameText.font = Font.boldSystemFont(14);
nameText.textColor = primary;
nameText.lineLimit = 1;
const subRow = widget.addStack();
subRow.spacing = 6;
const codeText = subRow.addText(product.code);
codeText.font = Font.systemFont(10);
codeText.textColor = secondary;
const ipText = subRow.addText(productDetails.dedicatedip || “”);
ipText.font = new Font(“Menlo”, 10);
ipText.textColor = secondary;
widget.addSpacer();
// 流量卡片
const trafficCard = widget.addStack();
trafficCard.layoutVertically();
trafficCard.backgroundColor = Color.dynamic(new Color(“#F2F2F7”), new Color(“#2C2C2E”));
trafficCard.cornerRadius = 10;
trafficCard.setPadding(8, 12, 8, 12);
// 标题行
const trafficHeader = trafficCard.addStack();
trafficHeader.centerAlignContent();
const globeSymbol = SFSymbol.named(“globe”);
globeSymbol.applyFont(Font.systemFont(11));
const globeImg = trafficHeader.addImage(globeSymbol.image);
globeImg.imageSize = new Size(12, 12);
globeImg.tintColor = secondary;
trafficHeader.addSpacer(3);
const trafficLabel = trafficHeader.addText(“Traffic”);
trafficLabel.font = Font.systemFont(11);
trafficLabel.textColor = secondary;
trafficHeader.addSpacer();
const usedGB = (productDetails.bwusage / 1024).toFixed(1);
const totalGB = (productDetails.bwlimit / 1024).toFixed(1);
const usageLabel = trafficHeader.addText(`${usedGB} / ${totalGB} GB`);
usageLabel.font = Font.boldSystemFont(11);
usageLabel.textColor = primary;
trafficCard.addSpacer(4);
// 百分比 + 剩余
const usagePercent = Math.round(productDetails.bwusage / productDetails.bwlimit * 100);
const pctRow = trafficCard.addStack();
pctRow.centerAlignContent();
const percentText = pctRow.addText(`${usagePercent}%`);
percentText.font = Font.boldSystemFont(30);
percentText.textColor = usagePercent >= 80 ? red : green;
pctRow.addSpacer();
const remainGB = ((productDetails.bwlimit – productDetails.bwusage) / 1024).toFixed(1);
const remainLabel = pctRow.addText(`剩余 ${remainGB} GB`);
remainLabel.font = Font.systemFont(9);
remainLabel.textColor = secondary;
trafficCard.addSpacer(4);
// 进度条
const progressBar = createProgressBarImage(
productDetails.bwlimit, productDetails.bwusage,
280, 8, green, progressBg
);
const barRow = trafficCard.addStack();
barRow.size = new Size(0, 8);
const progressImg = barRow.addImage(progressBar);
progressImg.imageSize = new Size(280, 8);
trafficCard.addSpacer(4);
// 入站出站 + 日均 + 重置
const inOutRow = trafficCard.addStack();
inOutRow.centerAlignContent();
const inArrow = inOutRow.addText(“↙”);
inArrow.font = Font.systemFont(10);
inArrow.textColor = blue;
inOutRow.addSpacer(2);
const inLabel = inOutRow.addText(`入站 ${formatMB(productDetails.bwusage_in || 0)}`);
inLabel.font = Font.systemFont(10);
inLabel.textColor = secondary;
inOutRow.addSpacer(8);
const outArrow = inOutRow.addText(“↗”);
outArrow.font = Font.systemFont(10);
outArrow.textColor = orange;
inOutRow.addSpacer(2);
const outLabel = inOutRow.addText(`出站 ${formatMB(productDetails.bwusage_out || 0)}`);
outLabel.font = Font.systemFont(10);
outLabel.textColor = secondary;
trafficCard.addSpacer(2);
const bottomRow = trafficCard.addStack();
bottomRow.centerAlignContent();
const regDate = new Date(productDetails.regdate);
if (!isNaN(regDate.getTime())) {
const now = new Date();
const regDay = regDate.getDate();
const lastReset = new Date(now.getFullYear(), now.getMonth(), regDay);
if (lastReset > now) lastReset.setMonth(lastReset.getMonth() – 1);
const daysSinceReset = Math.max(1, Math.floor((now – lastReset) / 86400000));
const dailyAvgMB = productDetails.bwusage / daysSinceReset;
const nextReset = new Date(lastReset);
nextReset.setMonth(nextReset.getMonth() + 1);
const daysUntilReset = Math.ceil((nextReset – now) / 86400000);
const avgText = bottomRow.addText(`日均 ${formatMB(dailyAvgMB)}`);
avgText.font = Font.systemFont(9);
avgText.textColor = secondary;
bottomRow.addSpacer(8);
const resetText = bottomRow.addText(`${daysUntilReset}天后重置`);
resetText.font = Font.systemFont(9);
resetText.textColor = secondary;
}
bottomRow.addSpacer();
const invoiceDateStr = productDetails.nextinvoicedate || “”;
const invoiceDate = new Date(invoiceDateStr);
if (!isNaN(invoiceDate.getTime())) {
const daysLeft = Math.ceil((invoiceDate.getTime() – Date.now()) / 86400000);
const renewBadge = bottomRow.addStack();
renewBadge.backgroundColor = new Color(green.hex, 0.2);
renewBadge.cornerRadius = 4;
renewBadge.setPadding(1, 4, 1, 4);
const renewText = renewBadge.addText(`${daysLeft}d`);
renewText.font = Font.boldSystemFont(9);
renewText.textColor = daysLeft <= 30 ? red : green;
bottomRow.addSpacer(4);
const dateText = bottomRow.addText(invoiceDateStr.split(” “)[0]);
dateText.font = Font.systemFont(9);
dateText.textColor = secondary;
}

Script.setWidget(widget);

}
async function renderLarge(widget, product) {

const pid = product.id.toString();
const [productDetails, vmDetails, allGraphData] = await Promise.all([
loadProductDetailStandard(pid),
loadVMDetails(pid),
loadAllGraphData(pid),
]);

const cpuData = allGraphData.cpu || { datasets: [] };
const memData = allGraphData.mem || { datasets: [] };
const netData = allGraphData.net || { datasets: [] };
const isDark = Device.isUsingDarkAppearance();
const theme = {
bg: Color.dynamic(new Color(“#FFFFFF”), new Color(“#1C1C1E”)),
card: Color.dynamic(new Color(“#F2F2F7”), new Color(“#2C2C2E”)),
primary: Color.dynamic(Color.black(), Color.white()),
secondary: Color.dynamic(new Color(“#6C6C70”), new Color(“#8E8E93”)),
progressBg: isDark ? new Color(“#3A3A3C”) : new Color(“#D1D1D6”),
green: new Color(“#30D158”),
blue: new Color(“#007AFF”),
orange: new Color(“#FF9F0A”),
red: new Color(“#FF3B30”),
};
widget.backgroundColor = theme.bg;
widget.setPadding(12, 14, 12, 14);

// === ROW 1: HEADER ===
const headerRow = widget.addStack();
headerRow.centerAlignContent();
const headerLeft = headerRow.addStack();
headerLeft.layoutVertically();
const nameText = headerLeft.addText(productDetails.hostname || product.name);
nameText.font = Font.boldSystemFont(16);
nameText.textColor = theme.primary;
nameText.lineLimit = 1;
const subInfo = headerLeft.addStack();
subInfo.spacing = 6;
const codeText = subInfo.addText(product.code);
codeText.font = Font.systemFont(10);
codeText.textColor = theme.secondary;
const ipLabel = subInfo.addText(productDetails.dedicatedip || “”);
ipLabel.font = new Font(“Menlo”, 10);
ipLabel.textColor = theme.secondary;
headerRow.addSpacer();
const isRunning = vmDetails && vmDetails.status === “Running”;
const statusBadge = headerRow.addStack();
statusBadge.backgroundColor = new Color((isRunning ? theme.green : theme.red).hex, 0.15);
statusBadge.cornerRadius = 6;
statusBadge.setPadding(3, 8, 3, 8);
statusBadge.centerAlignContent();
const dotCtx = new DrawContext();
dotCtx.size = new Size(8, 8);
dotCtx.opaque = false;
dotCtx.respectScreenScale = true;
dotCtx.setFillColor(isRunning ? theme.green : theme.red);
dotCtx.fillEllipse(new Rect(0, 0, 8, 8));
const dotImg = statusBadge.addImage(dotCtx.getImage());
dotImg.imageSize = new Size(8, 8);
statusBadge.addSpacer(4);
const statusText = statusBadge.addText(isRunning ? “在线” : “离线”);
statusText.font = Font.boldSystemFont(11);
statusText.textColor = isRunning ? theme.green : theme.red;

widget.addSpacer();

// === ROW 2: [uptime card] + [renewal card] ===
const row2 = widget.addStack();
row2.spacing = 8;
// — Uptime Card —
const uptimeCard = row2.addStack();
uptimeCard.layoutVertically();
uptimeCard.backgroundColor = theme.card;
uptimeCard.cornerRadius = 12;
uptimeCard.setPadding(8, 12, 8, 12);
const uptimeHeader = uptimeCard.addStack();
uptimeHeader.centerAlignContent();
const serverSym = SFSymbol.named(“server.rack”);
serverSym.applyFont(Font.systemFont(11));
const serverIcon = uptimeHeader.addImage(serverSym.image);
serverIcon.imageSize = new Size(13, 13);
serverIcon.tintColor = theme.secondary;
uptimeHeader.addSpacer(4);
const serverLabel = uptimeHeader.addText(“服务器”);
serverLabel.font = Font.systemFont(11);
serverLabel.textColor = theme.secondary;
uptimeHeader.addSpacer();
uptimeCard.addSpacer();
if (vmDetails && vmDetails.uptime) {
const ut = formatUptime(vmDetails.uptime);
const utDaysRow = uptimeCard.addStack();
utDaysRow.centerAlignContent();
const utDays = utDaysRow.addText(`${ut.days}`);
utDays.font = Font.boldSystemFont(28);
utDays.textColor = theme.primary;
const utDayUnit = utDaysRow.addText(` 天`);
utDayUnit.font = Font.systemFont(12);
utDayUnit.textColor = theme.secondary;
const utDetail = uptimeCard.addText(`${ut.hours}h ${ut.mins}m`);
utDetail.font = Font.systemFont(10);
utDetail.textColor = theme.secondary;
} else {
const naText = uptimeCard.addText(“N/A”);
naText.font = Font.boldSystemFont(24);
naText.textColor = theme.secondary;
}
uptimeCard.addSpacer();
// — Renewal Card —
const renewCard = row2.addStack();
renewCard.layoutVertically();
renewCard.backgroundColor = theme.card;
renewCard.cornerRadius = 12;
renewCard.setPadding(8, 12, 8, 12);
const renewHeader = renewCard.addStack();
renewHeader.centerAlignContent();
const calSym = SFSymbol.named(“calendar”);
calSym.applyFont(Font.systemFont(11));
const calIcon = renewHeader.addImage(calSym.image);
calIcon.imageSize = new Size(13, 13);
calIcon.tintColor = theme.secondary;
renewHeader.addSpacer(4);
const renewLabel = renewHeader.addText(“续费”);
renewLabel.font = Font.systemFont(11);
renewLabel.textColor = theme.secondary;
renewHeader.addSpacer();
renewCard.addSpacer();
const invoiceDateStr = productDetails.nextinvoicedate || “”;
const invoiceDate = new Date(invoiceDateStr);
if (!isNaN(invoiceDate.getTime())) {
const daysLeft = Math.ceil((invoiceDate.getTime() – Date.now()) / 86400000);
const daysColor = daysLeft <= 30 ? theme.red : theme.green;
const daysRow = renewCard.addStack();
daysRow.centerAlignContent();
const daysVal = daysRow.addText(`${daysLeft}`);
daysVal.font = Font.boldSystemFont(28);
daysVal.textColor = daysColor;
const daysUnit = daysRow.addText(` 天`);
daysUnit.font = Font.systemFont(12);
daysUnit.textColor = theme.secondary;
const dateLabel = renewCard.addText(invoiceDateStr.split(” “)[0]);
dateLabel.font = Font.systemFont(10);
dateLabel.textColor = theme.secondary;
} else {
const naText = renewCard.addText(“N/A”);
naText.font = Font.boldSystemFont(24);
naText.textColor = theme.secondary;
}
renewCard.addSpacer();

widget.addSpacer();

// === ROW 3: TRAFFIC CARD (full width) ===
const trafficCard = widget.addStack();
trafficCard.layoutVertically();
trafficCard.backgroundColor = theme.card;
trafficCard.cornerRadius = 12;
trafficCard.setPadding(10, 12, 10, 12);
// traffic header
const trafficHeader = trafficCard.addStack();
trafficHeader.centerAlignContent();
const globeSymbol = SFSymbol.named(“globe”);
globeSymbol.applyFont(Font.systemFont(11));
const globeImg = trafficHeader.addImage(globeSymbol.image);
globeImg.imageSize = new Size(12, 12);
globeImg.tintColor = theme.secondary;
trafficHeader.addSpacer(3);
const trafficLabel = trafficHeader.addText(“Traffic”);
trafficLabel.font = Font.systemFont(11);
trafficLabel.textColor = theme.secondary;
trafficHeader.addSpacer();
const usagePercent = Math.round(productDetails.bwusage / productDetails.bwlimit * 100);
const usedGB = (productDetails.bwusage / 1024).toFixed(1);
const totalGB = (productDetails.bwlimit / 1024).toFixed(1);
const usageText = trafficHeader.addText(`${usedGB} / ${totalGB} GB`);
usageText.font = Font.boldSystemFont(11);
usageText.textColor = theme.primary;
trafficCard.addSpacer(4);
// percentage row
const trafficMainRow = trafficCard.addStack();
trafficMainRow.centerAlignContent();
const percentText = trafficMainRow.addText(`${usagePercent}%`);
percentText.font = Font.boldSystemFont(22);
percentText.textColor = usagePercent >= 80 ? theme.red : theme.green;
trafficMainRow.addSpacer();
const remainGB = ((productDetails.bwlimit – productDetails.bwusage) / 1024).toFixed(1);
const remainLabel = trafficMainRow.addText(`剩余 ${remainGB} GB`);
remainLabel.font = Font.systemFont(9);
remainLabel.textColor = theme.secondary;
trafficCard.addSpacer(4);
// progress bar (full width)
const progressBar = createProgressBarImage(
productDetails.bwlimit, productDetails.bwusage,
300, 8, theme.green, theme.progressBg
);
const barRow = trafficCard.addStack();
barRow.size = new Size(0, 8);
const progressImg = barRow.addImage(progressBar);
progressImg.imageSize = new Size(300, 8);
trafficCard.addSpacer(4);
// in/out row
const inOutRow = trafficCard.addStack();
inOutRow.centerAlignContent();
const inArrow = inOutRow.addText(“↙”);
inArrow.font = Font.systemFont(10);
inArrow.textColor = theme.blue;
inOutRow.addSpacer(2);
const inLabel = inOutRow.addText(`入站 ${formatMB(productDetails.bwusage_in || 0)}`);
inLabel.font = Font.systemFont(10);
inLabel.textColor = theme.secondary;
inOutRow.addSpacer();
const outArrow = inOutRow.addText(“↗”);
outArrow.font = Font.systemFont(10);
outArrow.textColor = theme.orange;
inOutRow.addSpacer(2);
const outLabel = inOutRow.addText(`出站 ${formatMB(productDetails.bwusage_out || 0)}`);
outLabel.font = Font.systemFont(10);
outLabel.textColor = theme.secondary;
// daily avg + reset
const regDate = new Date(productDetails.regdate);
if (!isNaN(regDate.getTime())) {
trafficCard.addSpacer(3);
const now = new Date();
const regDay = regDate.getDate();
const lastReset = new Date(now.getFullYear(), now.getMonth(), regDay);
if (lastReset > now) lastReset.setMonth(lastReset.getMonth() – 1);
const daysSinceReset = Math.max(1, Math.floor((now – lastReset) / 86400000));
const dailyAvgMB = productDetails.bwusage / daysSinceReset;
const nextReset = new Date(lastReset);
nextReset.setMonth(nextReset.getMonth() + 1);
const daysUntilReset = Math.ceil((nextReset – now) / 86400000);
const statsRow = trafficCard.addStack();
statsRow.centerAlignContent();
const avgLabel = statsRow.addText(`日均 ${formatMB(dailyAvgMB)}`);
avgLabel.font = Font.systemFont(10);
avgLabel.textColor = theme.secondary;
statsRow.addSpacer();
const resetLabel = statsRow.addText(`${daysUntilReset} 天后重置`);
resetLabel.font = Font.systemFont(10);
resetLabel.textColor = theme.secondary;
}

widget.addSpacer();

// === ROW 4: [CPU chart] [RAM chart] [NET chart] + [detail card] ===
const row3 = widget.addStack();
row3.spacing = 6;
// — 3 chart cards —
const chartsCol = row3.addStack();
chartsCol.layoutVertically();
chartsCol.spacing = 6;
// top row: CPU + RAM
const chartRow1 = chartsCol.addStack();
chartRow1.spacing = 6;
function makeChartCard(parent, iconName, label, iconColor, datasets, minCount, chartColor, secondaryChartColor) {
const card = parent.addStack();
card.layoutVertically();
card.backgroundColor = theme.card;
card.cornerRadius = 10;
card.setPadding(6, 6, 4, 6);
const h = card.addStack();
h.centerAlignContent();
const sym = SFSymbol.named(iconName);
sym.applyFont(Font.systemFont(9));
const icon = h.addImage(sym.image);
icon.imageSize = new Size(10, 10);
icon.tintColor = iconColor;
h.addSpacer(2);
const t = h.addText(label);
t.font = Font.systemFont(9);
t.textColor = theme.secondary;
card.addSpacer(2);
if (datasets.length >= minCount) {
const chartData = minCount >= 2
? [datasets[0].data, datasets[1].data]
: datasets[0].data;
const chart = createLineChartImage(chartData, 80, 30, chartColor, secondaryChartColor);
card.addImage(chart);
}
}
makeChartCard(chartRow1, “cpu”, “CPU”, theme.orange, cpuData.datasets, 1, theme.blue);
makeChartCard(chartRow1, “memorychip”, “RAM”, theme.green, memData.datasets, 1, new Color(“#1662a0”));
// bottom: NET (wider)
const chartRow2 = chartsCol.addStack();
const netCard = chartRow2.addStack();
netCard.layoutVertically();
netCard.backgroundColor = theme.card;
netCard.cornerRadius = 10;
netCard.setPadding(6, 6, 4, 6);
const netH = netCard.addStack();
netH.centerAlignContent();
const netSym = SFSymbol.named(“antenna.radiowaves.left.and.right”);
netSym.applyFont(Font.systemFont(9));
const netIcon = netH.addImage(netSym.image);
netIcon.imageSize = new Size(10, 10);
netIcon.tintColor = theme.orange;
netH.addSpacer(2);
const netLabel = netH.addText(“NET”);
netLabel.font = Font.systemFont(9);
netLabel.textColor = theme.secondary;
netCard.addSpacer(2);
if (netData.datasets && netData.datasets.length >= 2) {
const netChart = createLineChartImage(
[netData.datasets[0].data, netData.datasets[1].data],
170, 30, new Color(“#1F6FD4”), new Color(“#16A085”)
);
netCard.addImage(netChart);
}
// — Right: detail card (spans full height) —
const detailCard = row3.addStack();
detailCard.layoutVertically();
detailCard.backgroundColor = theme.card;
detailCard.cornerRadius = 12;
detailCard.setPadding(8, 10, 8, 10);
function addDetailRow(parent, label, value, valColor) {
const row = parent.addStack();
row.centerAlignContent();
const l = row.addText(label);
l.font = Font.systemFont(9);
l.textColor = theme.secondary;
row.addSpacer();
const v = row.addText(value);
v.font = Font.boldSystemFont(9);
v.textColor = valColor || theme.primary;
v.lineLimit = 1;
parent.addSpacer(3);
}
addDetailRow(detailCard, “注册”, productDetails.regdate ? productDetails.regdate.split(” “)[0] : “N/A”);
if (!isNaN(invoiceDate.getTime())) {
addDetailRow(detailCard, “到期”, invoiceDateStr.split(” “)[0]);
}
addDetailRow(detailCard, “产品”, product.name);
// CPU value
if (cpuData.datasets && cpuData.datasets.length > 0 && cpuData.datasets[0].data.length > 0) {
const cpuVal = cpuData.datasets[0].data[cpuData.datasets[0].data.length – 1];
addDetailRow(detailCard, “CPU”, `${cpuVal.toFixed(1)}%`);
}
// Memory (bytes -> percentage using maxmem)
if (vmDetails && vmDetails.maxmem && vmDetails.mem) {
const memDisplay = `${(vmDetails.mem / vmDetails.maxmem * 100).toFixed(1)}%`;
addDetailRow(detailCard, “内存”, memDisplay);
}
// NET speed
if (netData.datasets && netData.datasets.length >= 2 && netData.datasets[0].data.length > 0) {
const fmtSpeed = (v) => {
if (v >= 1024 * 1024) return `${(v / 1024 / 1024).toFixed(1)}M/s`;
if (v >= 1024) return `${(v / 1024).toFixed(0)}K/s`;
return `${v.toFixed(0)}B/s`;
};
const lastIn = netData.datasets[0].data[netData.datasets[0].data.length – 1];
const lastOut = netData.datasets[1].data[netData.datasets[1].data.length – 1];
addDetailRow(detailCard, “↙入站”, fmtSpeed(lastIn));
addDetailRow(detailCard, “↗出站”, fmtSpeed(lastOut));
}

Script.setWidget(widget);
}
async function main() {
if (config.runsInApp) {
const hasCookie = getCookie();
if (!hasCookie) {
await handleSettingsMenu();
Script.complete();
return;
}
const menu = new Alert();
menu.title = “DMIT 小组件”;
menu.addAction(“预览大号组件”);
menu.addAction(“预览中号组件”);
menu.addAction(“预览小号组件”);
menu.addAction(“设置”);
menu.addCancelAction(“取消”);
const choice = await menu.present();
if (choice === 3) {
await handleSettingsMenu();
Script.complete();
return;
}
if (choice === -1) {
Script.complete();
return;
}
let products = getCachedProducts();
if (!products) {
products = await loadProducts();
}
if (products.length === 0) {
const errAlert = new Alert();
errAlert.title = “错误”;
errAlert.message = “未找到产品数据,请检查 Cookie 是否有效。”;
errAlert.addAction(“重新设置 Cookie”);
errAlert.addCancelAction(“取消”);
const errChoice = await errAlert.present();
if (errChoice === 0) {
await handleSettingsMenu();
}
Script.complete();
return;
}
const product = pickProductWithFallback(products, productsIndex);
if (!product) {
const noProductAlert = new Alert();
noProductAlert.title = “错误”;
noProductAlert.message = “未找到可用产品”;
noProductAlert.addAction(“好的”);
await noProductAlert.present();
Script.complete();
return;
}
const widget = new ListWidget();
if (choice === 0) {
await renderLarge(widget, product);
await widget.presentLarge();
} else if (choice === 1) {
await renderMedium(widget, product);
await widget.presentMedium();
} else {
await renderSmall(widget, product);
await widget.presentSmall();
}
Script.complete();
return;
}
const widget = new ListWidget();
const cookie = getCookie();
if (!cookie) {
showWidgetErrorAndFinish(widget, “请先设置 Cookie”);
return;
}
let products = getCachedProducts();
if (!products) {
products = await loadProducts();
}
if (products.length === 0) {
showWidgetErrorAndFinish(widget, “未找到产品数据”);
return;
}
const product = pickProductWithFallback(products, productsIndex);
if (!product) {
showWidgetErrorAndFinish(widget, “未找到可用产品”);
return;
}
switch (config.widgetFamily) {
case “accessoryCircular”:
await renderAccessoryCircular(widget, product);
break;
case “accessoryInline”:
await renderAccessoryInline(widget, product);
break;
case “accessoryRectangular”:
await renderAccessoryRectangular(widget, product);
break;
case “small”:
await renderSmall(widget, product);
break;
case “medium”:
await renderMedium(widget, product);
break;
case “large”:
case “extraLarge”:
await renderLarge(widget, product);
break;
default:
await renderMedium(widget, product);
break;
}
Script.complete();
}
void main();

打赏
谢谢谅解上文的粗糙,允许转载,请注明转载地址:Puo's 菜园子 » dmit 小组件分享
分享到

评论 抢沙发

做一个好的个人学习园地

主要网建,域名、集装箱物流、生活方法论的学习及研究,整理等内容

我的原创博客-忆秋年Puo's菜园子-我的学习园地

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续提供更多优质内容,让我们一起创建更加美好的网络世界!

支付宝扫一扫

微信扫一扫

登录

找回密码

注册