From 659f818df069fdae8933bb0319d808363d1ff803 Mon Sep 17 00:00:00 2001 From: mengyxu Date: Fri, 9 May 2025 17:16:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AD=98=E6=A1=A3=E5=8A=9F=E8=83=BD=E6=9B=B4?= =?UTF-8?q?=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1.调整存档存储位置 2.导出存档加密 3.增加导入存档时装备强化等级校验 --- package.json | 1 + src/config/base.ts | 4 +- src/config/equips/bean.ts | 5 ++- src/store/action.ts | 33 ++++++---------- src/tool/IndexedDB.ts | 81 +++++++++++++++++++++++++++++++++++++++ src/tool/archive.ts | 75 ++++++++++++++++++++++++++++++++++++ src/tool/crypot.ts | 41 ++++++++++++++++++++ src/tool/index.ts | 3 ++ src/views/archive.vue | 20 ++++++---- src/views/save-game.vue | 2 +- yarn.lock | 5 +++ 11 files changed, 238 insertions(+), 32 deletions(-) create mode 100644 src/tool/IndexedDB.ts create mode 100644 src/tool/archive.ts create mode 100644 src/tool/crypot.ts diff --git a/package.json b/package.json index 13af129..fd17789 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "dependencies": { "core-js": "^3.8.3", + "crypto-js": "^4.2.0", "js-base64": "^3.7.7", "vue": "^3.2.13", "vue-class-component": "^8.0.0-0", diff --git a/src/config/base.ts b/src/config/base.ts index 4d2d341..47d214d 100644 --- a/src/config/base.ts +++ b/src/config/base.ts @@ -1,3 +1,6 @@ +import { Player, RebornPoints } from './beings'; +import { Equip } from './equips'; + export const backpack_num = 32; export const reborn_arrts = ['hp', 'atk', 'crit', 'critDmg', 'def', 'bloc', 'recoverSpeed', 'moveSpeed', 'battleSpeed']; export const reborn_point_coefficients = { @@ -13,4 +16,3 @@ export const reborn_point_coefficients = { }; export const player_move_time = 3000; export const player_battle_time = 1000; -export const archive_name = 'transmigration_game_archive'; diff --git a/src/config/equips/bean.ts b/src/config/equips/bean.ts index 84bdbd7..d3a144d 100644 --- a/src/config/equips/bean.ts +++ b/src/config/equips/bean.ts @@ -1,4 +1,4 @@ -import { quality_collor, extra_entry_num } from './constant'; +import { extra_entry_num } from './constant'; export class Entry { type: string; @@ -21,7 +21,7 @@ export class Entry { export class Quality { quality: string; qualityCoefficient: number; -// color: string; + // color: string; extraEntryNum: number; constructor(quality: string, coefficient: number) { this.quality = quality; @@ -45,6 +45,7 @@ export class EquipBase { } export class Equip { + id?: string; type: string; lv: number; locked: boolean = false; diff --git a/src/store/action.ts b/src/store/action.ts index 6e32b6a..7739818 100644 --- a/src/store/action.ts +++ b/src/store/action.ts @@ -1,6 +1,5 @@ -import { initialWeapon, initialArmor, initialNeck, initialRing, i18n, archive_name, audio_mp3 } from '@/config'; -import { conisOfsell } from '@/tool'; -import { Base64 } from 'js-base64'; +import { initialWeapon, initialArmor, initialNeck, initialRing, i18n, audio_mp3 } from '@/config'; +import { conisOfsell, saveArchive, getArchive, getFromStore } from '@/tool'; const backgound = new Audio(audio_mp3.background); backgound.loop = true; @@ -68,28 +67,20 @@ export const useEquip = ({ state, commit }, index) => { }; export const saveGame = ({ state, commit }) => { - const player = state.playerAttribute; - const data = { - equips: [player.weapon, player.armor, player.ring, player.neck, player.jewelry, player.pants, player.shoes, player.bracers], - lv: player.lv, - coins: player.coins, - backpack: state.grid, - autoSell: state.autoSell, - shop: state.shop, - reborn: state.rebornPoints, - }; - var saveData = Base64.encode(Base64.encode(JSON.stringify(data))); - localStorage.setItem(archive_name, saveData); + saveArchive(state); commit('set_sys_info', { msg: t('saveGame.2'), type: 'win' }); }; export const loadGame = ({ commit }, data?) => { - if (!data) { - try { - const archive = localStorage.getItem(archive_name) || ''; - data = JSON.parse(Base64.decode(Base64.decode(archive))); - } catch (error) {} + if (data) { + loadArchive(commit, data); + return; } + getArchive().then((rsp: any) => { + loadArchive(commit, rsp); + }); +}; +const loadArchive = (commit, data) => { if (!data) { commit('set_sys_info', { msg: t('loadEmpty'), type: 'warning' }); } else if (data == 'undfind') { @@ -98,7 +89,7 @@ export const loadGame = ({ commit }, data?) => { commit('set_player_equips', data.equips); commit('set_player_lv', data.lv || 1); commit('set_player_coins', data.coins || 0); - commit('set_backpack', data.backpack); + commit('set_backpack', data.grid); commit('set_auto_sell', data.autoSell); commit('set_shop', data.shop); data.reborn && (data.reborn.recoverSpeed = 0); diff --git a/src/tool/IndexedDB.ts b/src/tool/IndexedDB.ts new file mode 100644 index 0000000..cd74f30 --- /dev/null +++ b/src/tool/IndexedDB.ts @@ -0,0 +1,81 @@ +import { deepCopy } from './caller'; + +const db_name = 'transmigration_game'; +export const store_name_archive = 'archive'; +export const key_name_archive = 'version'; + +const version = 1; +let db; +/** + * 打开数据库 + * @param {object} dbName 数据库的名字 + * @param {string} version 数据库的版本 + * @return {object} 该函数会返回一个数据库实例 + */ +function openDB() { + return new Promise((resolve, reject) => { + // 兼容浏览器 + var indexedDB = window.indexedDB; + + // 打开数据库,若没有则会创建 + const request = indexedDB.open(db_name, version); + // 数据库打开成功回调 + request.onsuccess = function (e) { + db = request.result; // 数据库对象 + console.log('数据库打开成功'); + resolve(db); + }; + // 数据库打开失败的回调 + request.onerror = function (e) { + console.log('数据库打开报错'); + }; + // 数据库有更新时候的回调 + request.onupgradeneeded = function (e) { + // 数据库创建或升级的时候会触发 + console.log('onupgradeneeded'); + db = request.result; // 数据库对象 + createArchiveStore(); + }; + }); +} + +const createArchiveStore = () => { + if (!db.objectStoreNames.contains(store_name_archive)) { + const objectStore = db.createObjectStore(store_name_archive, { keyPath: key_name_archive }); + // 创建索引,在后面查询数据的时候可以根据索引查 + // objectStore.createIndex(index_name_equip, index_name_equip, { unique: false }); + } +}; + +await openDB(); + +export const insertToStore = (storeName, data) => { + data = deepCopy(data); + return new Promise((resolve, reject) => { + const request = db + .transaction([storeName], 'readwrite') // 事务对象 指定表格名称和操作模式("只读"或"读写") + .objectStore(storeName) // 仓库对象 + .put(data); + + request.onsuccess = function (event) { + resolve(true); + }; + + request.onerror = function (event) { + resolve(false); + }; + }); +}; + +export const getFromStore = (storeName, id) => { + return new Promise((resolve, reject) => { + const store = db.transaction([storeName], 'readonly').objectStore(storeName); + const request = store.get(id); + request.onsuccess = (e) => { + resolve(request.result); + }; + request.onerror = (e) => { + resolve([]); + }; + }); +}; diff --git a/src/tool/archive.ts b/src/tool/archive.ts new file mode 100644 index 0000000..428717f --- /dev/null +++ b/src/tool/archive.ts @@ -0,0 +1,75 @@ +import { Equip, Player, RebornPoints } from '@/config'; +import { getFromStore, insertToStore, store_name_archive } from './IndexedDB'; +import { uuid } from './random'; +import { version } from 'vue'; +const archive_version = '1.0'; +const archive_version_strengthen = '1.0_flag'; + +export class GameArchive { + version: String; + equips: Equip[]; + lv: number; + coins: number; + grid: any[]; + autoSell: string[]; + shop: any[]; + reborn: RebornPoints; + + constructor(player: Player, grid: any[], autoSell: any[], shop: any[], reboren: RebornPoints) { + this.version = archive_version; + this.equips = [player.weapon, player.armor, player.ring, player.neck, player.jewelry, player.pants, player.shoes, player.bracers]; + this.lv = player.lv; + this.coins = player.coins; + this.grid = grid; + this.autoSell = autoSell; + this.shop = shop; + this.reborn = reboren; + } +} + +export const saveArchive = (state) => { + const archive = new GameArchive(state.playerAttribute, state.grid, state.autoSell, state.shop, state.rebornPoints); + getFromStore(store_name_archive, archive_version_strengthen).then((flag: any) => { + const time = new Date().getTime(); + if (!flag || !flag.time || flag.time + 10 * 60 * 1000 < time) { + flag = { version: archive_version_strengthen, time: time }; + } + const equips = new Array(); + Array.prototype.push.apply(equips, archive.equips); + Array.prototype.push.apply(equips, archive.grid); + equips.forEach((equip) => { + if (!equip) return; + + if (equip.strengthenLv > 0 && !equip.id) { + equip.id = uuid(); + } + if (equip.id) { + flag[equip.id] = equip.strengthenLv; + } + }); + insertToStore(store_name_archive, archive); + insertToStore(store_name_archive, flag); + }); +}; + +export const checkImportArchive = async (data): Promise => { + getFromStore(store_name_archive, archive_version_strengthen).then((rsp: any) => { + if (!rsp) return; + const equips = new Array(); + Array.prototype.push.apply(equips, data.equips); + Array.prototype.push.apply(equips, data.grid); + equips.forEach((equip) => { + if (!equip || !equip.id) return; + const slv = rsp[equip.id]; + slv && (equip.strengthenLv = slv); + }); + }); +}; + +export const getArchive = () => { + return new Promise((resolve, reject) => { + getFromStore(store_name_archive, archive_version).then((rsp: any) => { + resolve(rsp); + }); + }); +}; diff --git a/src/tool/crypot.ts b/src/tool/crypot.ts new file mode 100644 index 0000000..69b2efb --- /dev/null +++ b/src/tool/crypot.ts @@ -0,0 +1,41 @@ +import CryptoJS from 'crypto-js'; +/** + * AES-256-CBC对称加密 + * @param text {string} 要加密的明文 + * @param secretKey {string} 密钥,43位随机大小写与数字 + * @returns {string} 加密后的密文,Base64格式 + */ +export const AES_CBC_ENCRYPT = (text, secretKey) => { + var keyHex = CryptoJS.enc.Base64.parse(secretKey); + var ivHex = keyHex.clone(); + // 前16字节作为向量 + ivHex.sigBytes = 16; + ivHex.words.splice(4); + var messageHex = CryptoJS.enc.Utf8.parse(text); + var encrypted = CryptoJS.AES.encrypt(messageHex, keyHex, { + iv: ivHex, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }); + return encrypted.toString(); +}; + +/** + * AES-256-CBC对称解密 + * @param textBase64 {string} 要解密的密文,Base64格式 + * @param secretKey {string} 密钥,43位随机大小写与数字 + * @returns {string} 解密后的明文 + */ +export const AES_CBC_DECRYPT = (textBase64, secretKey) => { + var keyHex = CryptoJS.enc.Base64.parse(secretKey); + var ivHex = keyHex.clone(); + // 前16字节作为向量 + ivHex.sigBytes = 16; + ivHex.words.splice(4); + var decrypt = CryptoJS.AES.decrypt(textBase64, keyHex, { + iv: ivHex, + mode: CryptoJS.mode.CBC, + padding: CryptoJS.pad.Pkcs7, + }); + return CryptoJS.enc.Utf8.stringify(decrypt); +}; diff --git a/src/tool/index.ts b/src/tool/index.ts index ce66072..4c8d00f 100644 --- a/src/tool/index.ts +++ b/src/tool/index.ts @@ -3,6 +3,9 @@ export * from './formatter'; export * from './random'; export * from './mixins'; export * from './mobile'; +export * from './IndexedDB'; +export * from './crypot'; +export * from './archive'; export const getArrayEmptyIdx = (array) => { for (let i = 0; i < array.length; i++) { diff --git a/src/views/archive.vue b/src/views/archive.vue index d3b9a69..f9a0612 100644 --- a/src/views/archive.vue +++ b/src/views/archive.vue @@ -22,8 +22,9 @@ import { useStore } from "vuex"; import { computed, onMounted, ref, onBeforeUnmount } from "vue"; import { useI18n } from "vue3-i18n"; import { Tooltip, Dialog } from "@/components" -import { menu_icons, archive_name } from "@/config"; -import { Base64 } from "js-base64"; +import { menu_icons } from "@/config"; +import { getArchive, AES_CBC_ENCRYPT, AES_CBC_DECRYPT, checkImportArchive, saveArchive } from "@/tool"; + const { t } = useI18n(); const { state, commit, dispatch } = useStore(); @@ -31,13 +32,16 @@ const showArchive = computed(() => { return state.curMenu == 'archive'; }); const archive = ref(''); +const key = 'KUf4hM5rThssysJhcRFCfxLR8Imihjl0eMsyhh1M7Wk'; const showMenu = () => { if (showArchive.value) { state.curMenu = null; } else { state.curMenu = 'archive'; - archive.value = localStorage.getItem(archive_name) || ''; + getArchive().then((rsp: any) => { + archive.value = AES_CBC_ENCRYPT(JSON.stringify(rsp), key); + }); } } @@ -59,16 +63,18 @@ const pasteArchive = () => { }); } -const importArchive = () => { +const importArchive = async () => { try { - const data = JSON.parse(Base64.decode(Base64.decode(archive.value))); + const data = JSON.parse(AES_CBC_DECRYPT(archive.value, key)); if (!data || data == 'undfind') { commit('set_sys_info', { msg: t('loadEmpty'), type: 'warning' }); } - dispatch('loadGame', data); - localStorage.setItem(archive_name, archive.value); + await checkImportArchive(data); + await dispatch('loadGame', data); + saveArchive(state); showMenu(); } catch (error) { + console.log(error); commit('set_sys_info', { msg: t('loadError'), type: 'warning' }); } } diff --git a/src/views/save-game.vue b/src/views/save-game.vue index f102e48..562ac34 100644 --- a/src/views/save-game.vue +++ b/src/views/save-game.vue @@ -9,7 +9,7 @@ import { useStore } from "vuex"; import { onBeforeUnmount, onMounted, ref } from "vue"; import { useI18n } from "vue3-i18n"; import { Tooltip } from "@/components" -import { menu_icons, archive_name } from "@/config"; +import { menu_icons } from "@/config"; import { Base64 } from "js-base64"; const { t } = useI18n(); diff --git a/yarn.lock b/yarn.lock index ed29009..9c824d7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2448,6 +2448,11 @@ cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypto-js@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + css-declaration-sorter@^6.3.1: version "6.4.1" resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz#28beac7c20bad7f1775be3a7129d7eae409a3a71"