/**
* 鸣潮小组件
*
* Author: Vinking
*/
const config = {
apiUrl: "https://example.com/path/to/your/api",
userId: "uid",
design: {
typography: {
small: {
hero: { size: 18, weight: 'heavy' },
title: { size: 12, weight: 'medium' },
body: { size: 10, weight: 'regular' },
caption: { size: 8, weight: 'medium' }
},
medium: {
hero: { size: 24, weight: 'heavy' },
title: { size: 14, weight: 'medium' },
body: { size: 12, weight: 'regular' },
caption: { size: 10, weight: 'medium' }
},
large: {
hero: { size: 28, weight: 'heavy' },
title: { size: 16, weight: 'medium' },
body: { size: 14, weight: 'regular' },
caption: { size: 11, weight: 'medium' }
}
},
spacing: {
small: { xs: 4, sm: 6, md: 8, lg: 12, xl: 16 },
medium: { xs: 6, sm: 8, md: 12, lg: 16, xl: 20 },
large: { xs: 8, sm: 10, md: 16, lg: 20, xl: 24 }
},
radius: {
small: 12,
medium: 16,
large: 20,
element: 8
}
},
colors: {
primary: "#7c3aed",
primarySoft: "#8b5cf6",
success: "#059669",
warning: "#d97706",
accent: "#a855f7",
pitySmall: "#0891b2",
text: {
primary: "#0f172a",
secondary: "#1e293b",
tertiary: "#475569",
muted: "#64748b",
accent: "#4f46e5"
},
surface: {
primary: "#f8fafc",
secondary: "#f1f5f9",
elevated: "#e2e8f0"
},
luck: {
positive: "#eab308",
negative: "#65a30d"
}
}
};
function getColor(colorName) {
if (colorName && colorName.startsWith('#')) {
return new Color(colorName);
}
if (typeof config.colors[colorName] === 'string') {
return new Color(config.colors[colorName]);
}
if (colorName && colorName.includes('#')) {
return new Color(colorName);
}
if (colorName && colorName.includes('.')) {
const [category, subcategory] = colorName.split('.');
if (category === 'text' || category === 'surface') {
return new Color(config.colors[category][subcategory]);
}
if (category === 'luck') {
return new Color(config.colors.luck[subcategory]);
}
}
console.warn(`无法解析颜色: ${colorName}`);
return new Color("#666666");
}
function getWidgetSize() {
return config.widgetFamily || 'medium';
}
function getDesignTokens(size) {
return {
typography: config.design.typography[size],
spacing: config.design.spacing[size],
radius: config.design.radius[size] || config.design.radius.medium
};
}
async function createWidget() {
const widget = new ListWidget();
const size = getWidgetSize();
const tokens = getDesignTokens(size);
setupWidgetStyle(widget, size, tokens);
try {
const data = await fetchGachaData();
if (!data.success || !data.data?.length) {
throw new Error("暂无数据");
}
// 查找限定角色池数据 (poolType = "1")
const characterPool = data.data.find(pool => pool.poolType === "1");
if (!characterPool) {
throw new Error("未找到限定角色池数据");
}
switch (size) {
case 'small':
buildSmallLayout(widget, characterPool, tokens);
break;
case 'medium':
buildMediumLayout(widget, characterPool, tokens);
break;
case 'large':
buildLargeLayout(widget, characterPool, tokens);
break;
}
} catch (error) {
buildErrorLayout(widget, error.message, tokens);
}
return widget;
}
function setupWidgetStyle(widget, size, tokens) {
widget.backgroundColor = getColor('surface.primary');
widget.cornerRadius = tokens.radius;
const padding = tokens.spacing.lg;
widget.setPadding(padding, padding, padding, padding);
}
async function fetchGachaData() {
const request = new Request(config.apiUrl);
request.timeoutInterval = 15;
return await request.loadJSON();
}
function buildSmallLayout(widget, pool, tokens) {
const { totalCount = 0, highestRarityCount = 0, luckIndex = 0 } = pool;
const header = createHeader(widget, tokens, true);
widget.addSpacer(tokens.spacing.md);
const statsContainer = widget.addStack();
statsContainer.layoutHorizontally();
statsContainer.spacing = tokens.spacing.sm;
const totalCard = createStatCard(
statsContainer,
totalCount.toString(),
"总抽数",
getColor('primary'),
tokens
);
const legendaryCard = createStatCard(
statsContainer,
highestRarityCount.toString(),
"五星数",
getColor('accent'),
tokens
);
widget.addSpacer(tokens.spacing.sm);
createLuckIndicator(widget, luckIndex, tokens, 'compact');
}
function buildMediumLayout(widget, pool, tokens) {
const {
totalCount = 0,
highestRarityCount = 0,
averagePull = 0,
isSmallPity,
luckIndex = 0
} = pool;
createHeader(widget, tokens, false);
widget.addSpacer(tokens.spacing.lg);
const mainStats = widget.addStack();
mainStats.layoutHorizontally();
mainStats.spacing = tokens.spacing.md;
createStatColumn(mainStats, totalCount.toString(), "总抽数", getColor('primary'), tokens);
const divider1 = mainStats.addStack();
divider1.backgroundColor = new Color(getColor('text.muted').hex, 0.1);
divider1.cornerRadius = 1;
divider1.size = new Size(1, tokens.typography.hero.size);
createStatColumn(mainStats, highestRarityCount.toString(), "五星数", getColor('accent'), tokens);
const divider2 = mainStats.addStack();
divider2.backgroundColor = new Color(getColor('text.muted').hex, 0.1);
divider2.cornerRadius = 1;
divider2.size = new Size(1, tokens.typography.hero.size);
createStatColumn(mainStats, averagePull.toFixed(1), "平均", getColor('success'), tokens);
widget.addSpacer(tokens.spacing.lg);
const bottomRow = widget.addStack();
bottomRow.layoutHorizontally();
bottomRow.centerAlignContent();
const pityIndicator = createPityStatus(bottomRow, isSmallPity, tokens);
bottomRow.addSpacer();
createLuckIndicator(bottomRow, luckIndex, tokens, 'inline');
}
function buildLargeLayout(widget, pool, tokens) {
const {
totalCount = 0,
highestRarityCount = 0,
averagePull = 0,
isSmallPity,
luckIndex = 0,
lastPullTime
} = pool;
createDetailedHeader(widget, tokens);
widget.addSpacer(tokens.spacing.xl);
const statsGrid = widget.addStack();
statsGrid.layoutHorizontally();
statsGrid.spacing = tokens.spacing.lg;
createStatColumn(statsGrid, totalCount.toString(), "总抽数", getColor('primary'), tokens);
createStatColumn(statsGrid, highestRarityCount.toString(), "五星数", getColor('accent'), tokens);
createStatColumn(statsGrid, averagePull.toFixed(1), "平均", getColor('success'), tokens);
widget.addSpacer(tokens.spacing.xl);
createDivider(widget);
widget.addSpacer(tokens.spacing.lg);
createDetailedPityStatus(widget, isSmallPity, tokens);
widget.addSpacer(tokens.spacing.md);
createLuckIndicator(widget, luckIndex, tokens, 'detailed');
widget.addSpacer(tokens.spacing.lg);
createFooter(widget, tokens);
}
function createHeader(widget, tokens, compact = false) {
const header = widget.addStack();
header.layoutHorizontally();
header.centerAlignContent();
const iconSymbol = SFSymbol.named("person.fill.viewfinder");
iconSymbol.applyFont(Font.systemFont(tokens.typography.title.size + 2));
const icon = header.addImage(iconSymbol.image);
icon.imageSize = new Size(tokens.typography.title.size + 2, tokens.typography.title.size + 2);
icon.tintColor = getColor('primary');
header.addSpacer(tokens.spacing.xs);
const title = header.addText("限定角色");
title.font = Font.boldSystemFont(tokens.typography.title.size);
title.textColor = getColor('text.accent');
title.lineLimit = 1;
if (!compact) {
header.addSpacer();
const uid = header.addText(`UID: ${config.userId}`);
uid.font = Font.mediumSystemFont(tokens.typography.caption.size);
uid.textColor = getColor('text.muted');
}
return header;
}
function createDetailedHeader(widget, tokens) {
const header = createHeader(widget, tokens, false);
widget.addSpacer(tokens.spacing.sm);
const subtitleStack = widget.addStack();
subtitleStack.layoutHorizontally();
const subtitleIcon = SFSymbol.named("square.grid.3x3.bottomright.filled");
subtitleIcon.applyFont(Font.systemFont(tokens.typography.body.size));
const icon = subtitleStack.addImage(subtitleIcon.image);
icon.imageSize = new Size(tokens.typography.body.size, tokens.typography.body.size);
icon.tintColor = getColor('primarySoft');
subtitleStack.addSpacer(tokens.spacing.xs);
const subtitle = subtitleStack.addText("限定角色池数据概览");
subtitle.font = Font.systemFont(tokens.typography.body.size);
subtitle.textColor = getColor('text.secondary');
}
function createStatCard(container, value, label, color, tokens) {
const card = container.addStack();
card.layoutVertically();
card.centerAlignContent();
const bgColor = getColor('surface.elevated');
const borderColor = new Color(color.hex, 0.2);
card.backgroundColor = new Color(bgColor.hex, 0.3);
card.borderColor = borderColor;
card.borderWidth = 1;
card.cornerRadius = config.design.radius.element;
card.setPadding(tokens.spacing.sm, tokens.spacing.md, tokens.spacing.sm, tokens.spacing.md);
const valueText = card.addText(value);
valueText.font = Font.heavySystemFont(tokens.typography.hero.size);
valueText.textColor = color;
valueText.centerAlignText();
valueText.shadowColor = new Color(color.hex, 0.3);
valueText.shadowOffset = new Point(0, 1);
valueText.shadowRadius = 2;
card.addSpacer(2);
const labelText = card.addText(label);
labelText.font = Font.mediumSystemFont(tokens.typography.caption.size);
labelText.textColor = getColor('text.tertiary');
labelText.centerAlignText();
return card;
}
function createStatColumn(container, value, label, color, tokens) {
const column = container.addStack();
column.layoutVertically();
column.centerAlignContent();
const valueText = column.addText(value);
valueText.font = Font.heavySystemFont(tokens.typography.hero.size);
valueText.textColor = color;
valueText.centerAlignText();
valueText.shadowColor = new Color(color.hex, 0.3);
valueText.shadowOffset = new Point(0, 1);
valueText.shadowRadius = 1;
column.addSpacer(tokens.spacing.xs);
const labelText = column.addText(label);
labelText.font = Font.systemFont(tokens.typography.body.size);
labelText.textColor = getColor('text.secondary');
labelText.centerAlignText();
return column;
}
function createPityStatus(container, isSmallPity, tokens) {
const status = container.addStack();
status.layoutHorizontally();
status.centerAlignContent();
const dotColor = isSmallPity
? new Color(config.colors.pitySmall)
: getColor('success');
const bgColor = new Color(dotColor.hex, 0.15);
status.backgroundColor = bgColor;
status.cornerRadius = tokens.spacing.sm;
status.setPadding(tokens.spacing.xs, tokens.spacing.md, tokens.spacing.xs, tokens.spacing.md);
const iconName = isSmallPity ? "diamond.bottomhalf.filled" : "diamond.fill";
const iconSymbol = SFSymbol.named(iconName);
iconSymbol.applyFont(Font.systemFont(tokens.typography.caption.size));
const icon = status.addImage(iconSymbol.image);
icon.imageSize = new Size(tokens.typography.caption.size, tokens.typography.caption.size);
icon.tintColor = dotColor;
status.addSpacer(tokens.spacing.xs);
const text = status.addText(isSmallPity ? "小保底" : "大保底");
text.font = Font.boldSystemFont(tokens.typography.body.size);
text.textColor = dotColor;
return status;
}
function createDetailedPityStatus(widget, isSmallPity, tokens) {
const row = widget.addStack();
row.layoutHorizontally();
row.centerAlignContent();
const label = row.addText("保底状态");
label.font = Font.systemFont(tokens.typography.body.size);
label.textColor = getColor('text.secondary');
row.addSpacer();
createPityStatus(row, isSmallPity, tokens);
}
function createLuckIndicator(container, luckIndex, tokens, style = 'inline') {
const luckColor = getLuckColor(luckIndex);
if (style === 'compact') {
const luck = container.addStack();
luck.layoutHorizontally();
luck.centerAlignContent();
const iconSymbol = SFSymbol.named("sparkles");
iconSymbol.applyFont(Font.systemFont(tokens.typography.body.size));
const icon = luck.addImage(iconSymbol.image);
icon.imageSize = new Size(tokens.typography.body.size, tokens.typography.body.size);
icon.tintColor = luckColor;
luck.addSpacer(tokens.spacing.xs);
const value = luck.addText(`${luckIndex > 0 ? '+' : ''}${luckIndex}%`);
value.font = Font.heavySystemFont(tokens.typography.title.size);
value.textColor = luckColor;
value.shadowColor = new Color(luckColor.hex, 0.3);
value.shadowOffset = new Point(0, 1);
value.shadowRadius = 1;
} else if (style === 'inline') {
const luck = container.addStack();
luck.layoutHorizontally();
luck.centerAlignContent();
const label = luck.addText("欧气");
label.font = Font.systemFont(tokens.typography.body.size);
label.textColor = getColor('text.secondary');
luck.addSpacer(tokens.spacing.xs);
const value = luck.addText(`${luckIndex > 0 ? '+' : ''}${luckIndex}%`);
value.font = Font.heavySystemFont(tokens.typography.title.size);
value.textColor = luckColor;
value.shadowColor = new Color(luckColor.hex, 0.3);
value.shadowOffset = new Point(0, 1);
value.shadowRadius = 1;
} else if (style === 'detailed') {
const row = container.addStack();
row.layoutHorizontally();
row.centerAlignContent();
const iconSymbol = SFSymbol.named("sparkles.square.filled.on.square");
iconSymbol.applyFont(Font.systemFont(tokens.typography.body.size));
const icon = row.addImage(iconSymbol.image);
icon.imageSize = new Size(tokens.typography.body.size, tokens.typography.body.size);
icon.tintColor = luckColor;
row.addSpacer(tokens.spacing.xs);
const label = row.addText("欧气指数");
label.font = Font.systemFont(tokens.typography.body.size);
label.textColor = getColor('text.secondary');
row.addSpacer();
const value = row.addText(`${luckIndex > 0 ? '+' : ''}${luckIndex}%`);
value.font = Font.heavySystemFont(tokens.typography.title.size);
value.textColor = luckColor;
value.shadowColor = new Color(luckColor.hex, 0.3);
value.shadowOffset = new Point(0, 1);
value.shadowRadius = 1;
}
}
function getLuckColor(luckIndex) {
return luckIndex >= 0
? getColor('luck.positive')
: getColor('luck.negative');
}
function createDivider(widget) {
const divider = widget.addStack();
const gradient = new LinearGradient();
gradient.colors = [
new Color(getColor('text.muted').hex, 0.05),
new Color(getColor('text.muted').hex, 0.2),
new Color(getColor('text.muted').hex, 0.05)
];
gradient.locations = [0, 0.5, 1];
divider.backgroundGradient = gradient;
divider.cornerRadius = 1;
divider.size = new Size(0, 1);
}
function createFooter(widget, tokens) {
const footerStack = widget.addStack();
footerStack.layoutHorizontally();
footerStack.centerAlignContent();
const iconSymbol = SFSymbol.named("wave.3.right");
iconSymbol.applyFont(Font.systemFont(tokens.typography.caption.size));
const icon = footerStack.addImage(iconSymbol.image);
icon.imageSize = new Size(tokens.typography.caption.size, tokens.typography.caption.size);
icon.tintColor = getColor('text.muted');
footerStack.addSpacer(tokens.spacing.xs);
const footer = footerStack.addText("Powered by Astrionyx");
footer.font = Font.systemFont(tokens.typography.caption.size);
footer.textColor = getColor('text.muted');
footer.alpha = 0.8;
}
function buildErrorLayout(widget, message, tokens) {
const container = widget.addStack();
container.layoutVertically();
container.centerAlignContent();
const iconSymbol = SFSymbol.named("xmark.octagon.fill");
iconSymbol.applyFont(Font.systemFont(tokens.typography.hero.size));
const icon = container.addImage(iconSymbol.image);
icon.imageSize = new Size(tokens.typography.hero.size, tokens.typography.hero.size);
icon.tintColor = getColor('warning');
container.addSpacer(tokens.spacing.sm);
const errorTitle = container.addText("数据获取失败");
errorTitle.font = Font.boldSystemFont(tokens.typography.title.size);
errorTitle.textColor = getColor('text.primary');
errorTitle.centerAlignText();
container.addSpacer(tokens.spacing.xs);
const errorStack = container.addStack();
errorStack.backgroundColor = new Color(getColor('warning').hex, 0.1);
errorStack.cornerRadius = 8;
errorStack.setPadding(tokens.spacing.sm, tokens.spacing.md, tokens.spacing.sm, tokens.spacing.md);
const text = errorStack.addText(message);
text.font = Font.mediumSystemFont(tokens.typography.body.size);
text.textColor = getColor('warning');
text.centerAlignText();
text.lineLimit = 2;
container.addSpacer(tokens.spacing.md);
const refreshStack = container.addStack();
refreshStack.layoutHorizontally();
refreshStack.centerAlignContent();
const refreshIcon = SFSymbol.named("arrow.clockwise");
refreshIcon.applyFont(Font.systemFont(tokens.typography.caption.size));
const refreshIconImage = refreshStack.addImage(refreshIcon.image);
refreshIconImage.imageSize = new Size(tokens.typography.caption.size, tokens.typography.caption.size);
refreshIconImage.tintColor = getColor('text.tertiary');
refreshStack.addSpacer(tokens.spacing.xs);
const refreshHint = refreshStack.addText("请稍后刷新重试");
refreshHint.font = Font.systemFont(tokens.typography.caption.size);
refreshHint.textColor = getColor('text.tertiary');
}
async function main() {
const widget = await createWidget();
if (config.runsInWidget) {
Script.setWidget(widget);
} else {
const size = config.previewSize || "medium";
switch (size) {
case "small":
widget.presentSmall();
break;
case "large":
widget.presentLarge();
break;
default:
widget.presentMedium();
}
}
Script.complete();
}
await main();