Widget:资源计算器/js
<script>//通用常量配置 const { ElMessage, ElMessageBox } = ElementPlus
const Config = {
itemJsonPath: 'MediaWiki:CalcItem.json', cardDataPath: 'MediaWiki:CalcTemplate.json', storageKey: 'calculator_tabs_data', propCountsKey: 'calculator_prop_counts', // 新增:道具数量存储key dateRangeKey: 'calculator_date_range' // 新增:日期范围存储key
}
// 日期选择器配置 const DatePickerConfig = {
mode: "range", dateFormat: "Y-m-d", locale: "zh", showMonths: 1, static: true, placeholder: "请选择日期范围"
}
// ================ 1. 工具函数 ================ const utils = {
// 日期相关
formatDate(date) {
if (!date) return
// 如果已经是字符串格式,直接返回
if (typeof date === 'string') return date
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
},
getDayCount(start, end) {
if (!start || !end) return 0
// 使用UTC时间来计算天数,避免时区影响
const startDate = new Date(start + 'T00:00:00Z')
const endDate = new Date(end + 'T00:00:00Z')
return Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)) + 1
},
getWeekCount(start, end, days) {
if (!start || !end || !days || days.length === 0) return 0
// 转换为日期对象
const startDate = new Date(start + 'T00:00:00Z')
const endDate = new Date(end + 'T00:00:00Z')
let count = 0
let currentDate = new Date(startDate)
// 遍历日期范围内的每一天
while (currentDate <= endDate) {
// 获取当前是星期几(0-6,0是星期日)
let dayOfWeek = currentDate.getUTCDay()
// 转换为1-7的格式(1是星期一)
dayOfWeek = dayOfWeek === 0 ? 7 : dayOfWeek
// 如果当天是选中的星期几之一,计数加1
if (days.includes(dayOfWeek.toString())) {
count++
}
// 移动到下一天
currentDate.setUTCDate(currentDate.getUTCDate() + 1)
}
return count
},
// 添加 API 加载等待函数
onApiReady: function (callback) {
var maxRetry = 32;
var retryTimeout = 81;
var retryCount = 0;
function waitApi() {
if (window.mw && typeof mw.Api === 'function') {
setTimeout(callback, retryTimeout);
} else {
retryCount++;
if (retryCount < maxRetry) {
setTimeout(waitApi, retryTimeout);
} else {
console.error(`等待mw.Api加载超时。已经等待 ${maxRetry} 次,累计时间:${maxRetry * retryTimeout} 毫秒。`);
}
}
}
waitApi();
},
// 控制页面元素层级
controlZIndex: function (isDialogOpen) {
const elements = [
{
selector: '.bili-game-header-nav',
defaultZIndex: 1000,
dialogOpenZIndex: 0
},
{
selector: '.bui-sns-info',
defaultZIndex: 9,
dialogOpenZIndex: 0
},
{
selector: '.wiki-componment-rank',
defaultZIndex: 9,
dialogOpenZIndex: 0
},
{
selector: '.wiki-header',
defaultZIndex: 3,
dialogOpenZIndex: 0
}
// 可以在这里添加更多需要控制的元素
];
elements.forEach(item => {
const element = document.querySelector(item.selector);
if (element) {
element.style.setProperty('z-index', isDialogOpen ? item.dialogOpenZIndex : item.defaultZIndex, 'important');
}
});
}
}
// ================ 2. Vue应用 ================ const app = Vue.createApp({
data() {
return {
dateRange: {
start: ,
end:
},
cardList: [], // 卡片列表
summaryData: {}, // 汇总数据
propList: {}, // 道具列表
propCounts: {} // 添加道具初始值数据
}
},
methods: {
// 日期相关
handleDateChange(dates) {
if (dates.length === 2) {
// 使用本地时间格式化日期
this.dateRange.start = utils.formatDate(dates[0])
this.dateRange.end = utils.formatDate(dates[1])
// 保存日期范围到localStorage
localStorage.setItem(Config.dateRangeKey, JSON.stringify(this.dateRange))
}
},
// 卡片操作
addCard(card) {
this.cardList.push(card)
// 保存到localStorage
localStorage.setItem(Config.storageKey, JSON.stringify(this.cardList))
},
removeCard(index) {
this.cardList.splice(index, 1)
// 保存到localStorage
localStorage.setItem(Config.storageKey, JSON.stringify(this.cardList))
},
updateCard(index, card) {
// 完全替换原有卡片数据
this.cardList[index] = [...card]
// 保存到localStorage
localStorage.setItem(Config.storageKey, JSON.stringify(this.cardList))
},
clearCards() {
this.cardList = []
// 保存到localStorage
localStorage.setItem(Config.storageKey, '[]')
},
// 加载道具数据
async loadPropList() {
const data = await DataFetcher.fetchWikiJson(Config.itemJsonPath);
if (data) {
this.propList = data;
// 初始化道具数量
Object.keys(this.propList).forEach(name => {
this.propCounts[name] = 0;
});
// 加载保存的数量
const savedCounts = localStorage.getItem(Config.propCountsKey);
if (savedCounts) {
const counts = JSON.parse(savedCounts);
Object.keys(counts).forEach(name => {
if (name in this.propList) {
this.propCounts[name] = counts[name];
}
});
}
}
}
},
mounted() {
// 加载保存的日期范围
const savedDateRange = localStorage.getItem(Config.dateRangeKey)
if (savedDateRange) {
this.dateRange = JSON.parse(savedDateRange)
}
// 初始化日期选择器
const dateInput = document.querySelector('#date-range')
if (dateInput) {
flatpickr(dateInput, {
...DatePickerConfig,
defaultDate: this.dateRange.start && this.dateRange.end ? [
new Date(this.dateRange.start + 'T00:00:00'),
new Date(this.dateRange.end + 'T00:00:00')
] : null,
onChange: (dates) => this.handleDateChange(dates)
})
}
// 加载保存的数据
const savedData = localStorage.getItem(Config.storageKey)
if (savedData) {
this.cardList = JSON.parse(savedData)
}
// 加载道具数据
this.loadPropList()
}
})
// ================ 3. 组件注册 ================ // 卡片编辑弹窗组件 app.component('card-editor', {
template: '#card-editor-template',
props: {
type: {
type: String,
required: true
},
propList: {
type: Object,
required: true
},
editCard: {
type: [Array, Object], // 允许数组或对象类型
default: null
}
},
data() {
return {
visible: false,
isEdit: false,
form: {
remark: ,
count: 1,
days: [],
startDate: ,
duration: 7
},
selectedProps: {},
weekDays: [
{ value: '1', label: '周一' },
{ value: '2', label: '周二' },
{ value: '3', label: '周三' },
{ value: '4', label: '周四' },
{ value: '5', label: '周五' },
{ value: '6', label: '周六' },
{ value: '7', label: '周日' }
]
}
},
watch: {
visible(newVal) {
utils.controlZIndex(newVal);
},
editCard: {
immediate: true,
handler(card) {
if (card) {
this.isEdit = true
this.initFormFromCard(card)
}
}
}
},
methods: {
show() {
this.visible = true
if (!this.isEdit) {
this.handleReset()
}
},
handleClose() {
this.visible = false
this.isEdit = false
this.handleReset()
},
handleReset() {
// 重置表单
this.form = {
remark: ,
count: 1,
days: [],
startDate: ,
duration: 7
}
// 初始化所有道具数量为0
this.selectedProps = Object.keys(this.propList).reduce((acc, name) => {
acc[name] = 0
return acc
}, {})
},
formatRemark() {
let remark = this.form.remark
switch (this.type) {
case 'basic':
return `${this.form.count}-${remark}`
case 'weekly':
return this.form.days.length > 0 ? `${this.form.days.sort().join()}-${remark}` : remark
case 'cycle':
const date = this.form.startDate ? utils.formatDate(this.form.startDate) :
return `${date}-${this.form.duration}-${remark}`
default:
return remark
}
},
handleConfirm() {
// 构建卡片数据,只收集数量不为0的道具
const props = Object.entries(this.selectedProps)
.filter(([_, count]) => count !== 0)
.map(([name, count]) => [name, count])
const card = [
this.formatRemark(),
this.type,
props
]
// 发送事件
this.$emit(this.isEdit ? 'update' : 'add', card)
this.handleClose()
},
initFormFromCard(card) {
// 如果是对象格式,转换为数组格式
const cardArray = Array.isArray(card) ? card : [card[0], card[1], card[2]]
const [remark, , props] = cardArray
// 解析备注
switch (this.type) {
case 'basic': {
const [count, text] = remark.split('-')
this.form.count = parseInt(count)
this.form.remark = text
break
}
case 'weekly': {
const [days, text] = remark.split('-')
this.form.days = days.split()
this.form.remark = text
break
}
case 'cycle': {
const [date, duration, text] = remark.split('-')
this.form.startDate = date
this.form.duration = parseInt(duration)
this.form.remark = text
break
}
default:
this.form.remark = remark
}
// 设置道具
this.selectedProps = {}
props.forEach(([name, count]) => {
this.selectedProps[name] = count
})
},
toggleWeekDay(value) {
const index = this.form.days.indexOf(value)
if (index === -1) {
this.form.days.push(value)
} else {
this.form.days.splice(index, 1)
}
// 保持数组有序
this.form.days.sort()
}
}
})
// 卡片组件 app.component('card', {
template: '#card-template',
props: {
card: {
type: Object,
required: true
},
index: {
type: Number,
required: true
},
propList: {
type: Object,
required: true
}
},
emits: ['remove', 'edit'],
computed: {
sortedProps() {
// 将道具数组转换为Map以便快速查找数量
const propsMap = new Map(this.card[2])
// 按照propList的顺序返回道具数组
return Object.keys(this.propList)
.filter(name => propsMap.has(name))
.map(name => [name, propsMap.get(name)])
},
cardRemark() {
return this.card[0]
}
},
methods: {
handleCardClick() {
this.$emit('edit', this.card)
}
}
})
// 导入弹窗组件 app.component('import-dialog', {
template: '#import-dialog-template',
emits: ['import'],
data() {
return {
visible: false,
activeCategory: ,
templateData: {},
importData:
}
},
watch: {
visible(newVal) {
utils.controlZIndex(newVal);
}
},
methods: {
show() {
this.visible = true
this.loadTemplates()
},
async loadTemplates() {
const data = await DataFetcher.fetchWikiJson(Config.cardDataPath);
if (data) {
this.templateData = data;
if (Object.keys(this.templateData).length > 0) {
this.activeCategory = Object.keys(this.templateData)[0];
}
}
},
handleTemplateClick(template) {
this.importData = JSON.stringify([template])
},
handleClear() {
this.importData =
},
handleImport() {
try {
const data = JSON.parse(this.importData)
if (!Array.isArray(data)) {
throw new Error('数据格式错误')
}
this.$emit('import', data)
this.visible = false
ElMessage.success('导入成功')
} catch (error) {
ElMessage.error('导入失败:数据格式错误')
}
}
}
})
// 卡片面板组件 app.component('card-panel', {
template: '#card-panel-template',
props: {
cardList: {
type: Array,
required: true
},
propList: {
type: Object,
required: true
}
},
emits: ['add', 'remove', 'update', 'clear'],
data() {
return {
activeTab: 'daily',
editingCard: null,
editingIndex: -1 // 添加编辑中的卡片索引
}
},
computed: {
filteredCards() {
return this.cardList.map((card, index) => ({
...card,
originalIndex: index
})).filter(card => card[1] === this.activeTab)
}
},
methods: {
handleImport() {
this.$refs.importDialog.show()
},
handleExport() {
// 导出当前数据
const data = JSON.stringify(this.cardList)
navigator.clipboard.writeText(data)
ElMessage.success('数据已复制到剪贴板')
},
handleClear() {
ElMessageBox.confirm('确定要清空所有卡片吗?', '提示', {
type: 'warning'
}).then(() => {
this.$emit('clear')
ElMessage.success('已清空所有卡片')
})
},
handleAdd() {
this.editingCard = null
this.editingIndex = -1
this.$refs.editor.show()
},
handleEdit(card) {
// 找到原始卡片的索引
this.editingIndex = card.originalIndex
this.editingCard = this.cardList[this.editingIndex]
this.$refs.editor.show()
},
handleCardUpdate(card) {
if (this.editingIndex !== -1) {
// 完全替换原有卡片数据
this.$emit('update', this.editingIndex, [...card])
} else {
this.$emit('add', card)
}
},
handleImportData(data) {
data.forEach(card => {
this.$emit('add', card)
})
},
handleRemove(index) {
// 使用保存的原始索引来删除
const originalIndex = this.filteredCards[index].originalIndex
this.$emit('remove', originalIndex)
}
}
})
// 道具展示组件 app.component('stats-panel', {
template: '#stats-panel-template',
props: {
cardList: {
type: Array,
required: true
},
propList: {
type: Object,
required: true
},
propCounts: {
type: Object,
required: true
},
dateRange: {
type: Object,
required: true
}
},
data() {
return {
activeTab: 'summary' // 默认显示汇总标签页
}
},
computed: {
summaryData() {
// 初始化所有道具的统计数据
const summary = Object.keys(this.propList).map(name => ({
name,
total: 0,
initial: this.propCounts[name] || 0,
daily: 0,
weekly: 0,
cycle: 0,
basic: 0
}))
// 计算日期范围内的天数
const dayCount = utils.getDayCount(this.dateRange.start, this.dateRange.end)
// 按类型统计各道具数量
this.cardList.forEach(card => {
const [remark, type, props] = card
const propsMap = new Map(props)
summary.forEach(item => {
const count = propsMap.get(item.name) || 0
if (type === 'daily') {
// 每日类型的数量直接乘以天数
item.daily += count * dayCount
item.total += count * dayCount
} else if (type === 'weekly') {
// 解析备注中的星期几
const weekDays = remark.split('-')[0].split()
// 计算选中的星期几在日期范围内出现的次数
const weekCount = utils.getWeekCount(this.dateRange.start, this.dateRange.end, weekDays)
item.weekly += count * weekCount
item.total += count * weekCount
} else if (type === 'cycle') {
// 解析备注中的开始日期和周期天数
const [startDate, duration] = remark.split('-')
// 计算在统计范围内会出现几次
const cycleStart = new Date(startDate + 'T00:00:00Z')
const rangeStart = new Date(this.dateRange.start + 'T00:00:00Z')
const rangeEnd = new Date(this.dateRange.end + 'T00:00:00Z')
let cycleCount = 0
let currentDate = new Date(cycleStart)
// 计算在日期范围内出现的次数
while (currentDate <= rangeEnd) {
if (currentDate >= rangeStart && currentDate <= rangeEnd) {
cycleCount++
}
// 移动到下一个周期
currentDate.setUTCDate(currentDate.getUTCDate() + parseInt(duration))
}
item.cycle += count * cycleCount
item.total += count * cycleCount
} else if (type === 'basic') {
// 解析备注中的次数
const [times] = remark.split('-')
const repeatCount = parseInt(times) || 1
item.basic += count * repeatCount
item.total += count * repeatCount
} else {
item[type] += count
item.total += count
}
})
})
// 加上初始值
summary.forEach(item => {
item.total += item.initial
})
// 过滤掉所有数值都为0的道具
const filteredSummary = summary.filter(item =>
item.total !== 0 ||
item.initial !== 0 ||
item.daily !== 0 ||
item.weekly !== 0 ||
item.cycle !== 0 ||
item.basic !== 0
)
// 添加空行
filteredSummary.push({
name: 'empty',
total: ,
initial: ,
daily: ,
weekly: ,
cycle: ,
basic:
})
return filteredSummary
}
},
methods: {
handleCountChange(name) {
// 保存数量到localStorage
localStorage.setItem(Config.propCountsKey, JSON.stringify(this.propCounts))
},
handleReset() {
// 弹出确认对话框
ElMessageBox.confirm('确定要重置所有道具预设数量为0吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 重置所有道具数量为0
Object.keys(this.propCounts).forEach(name => {
this.propCounts[name] = 0
})
// 保存到localStorage
localStorage.setItem(Config.propCountsKey, JSON.stringify(this.propCounts))
// 显示成功提示
ElMessage.success('已重置所有道具预设数量')
}).catch(() => {
// 用户取消操作,不做任何处理
})
}
}
})
// 数据获取工具 const DataFetcher = {
async fetchWikiJson(pageName) {
try {
// 等待 API 加载完成
await new Promise((resolve) => {
utils.onApiReady(resolve);
});
const api = new mw.Api();
const params = {
action: 'query',
prop: 'revisions',
titles: pageName,
rvprop: 'content',
rvslots: 'main',
formatversion: '2',
format: 'json'
};
const response = await api.get(params);
if (!response.query || !response.query.pages || !response.query.pages[0] || !response.query.pages[0].revisions) {
throw new Error('页面不存在或无法访问');
}
const content = response.query.pages[0].revisions[0].slots.main.content;
return JSON.parse(content);
} catch (error) {
console.error(`加载${pageName}失败:`, error);
ElMessage.error(`加载${pageName}失败: ${error.message}`);
return null;
}
}
};
// ================ 4. 应用挂载 ================ app.use(ElementPlus) app.mount('#app') </script>

沪公网安备 31011002002714 号