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>