bugfix250107.1
全站通知:

Widget:资源计算器/js

来自恋与深空WIKI_BWIKI_哔哩哔哩
跳到导航 跳到搜索

<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>