├── .babelrc.js ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── database ├── database.sql ├── deleteRepeated.sql ├── likeDislikeDiffer.sql ├── replace.sql └── selectRepeated.sql ├── images ├── oldscreenshot │ ├── 1 │ │ ├── ss1.png │ │ └── ss2.png │ ├── 2 │ │ ├── ss1.png │ │ ├── ss2.png │ │ └── ss3.png │ ├── 3 │ │ ├── ss1.png │ │ ├── ss2.png │ │ ├── ss3.png │ │ └── ss4.png │ └── 4 │ │ ├── ss1.png │ │ ├── ss2.png │ │ ├── ss3.png │ │ ├── ss4.png │ │ ├── ss5.png │ │ └── ss6.png ├── origin │ ├── boss.sai2 │ ├── icons.sai2 │ ├── message.sai2 │ ├── portal.sai2 │ ├── question.sai2 │ └── warning.sai2 ├── ss1.png └── ss2.png ├── mapDivider ├── dlc1.png ├── mapDivider.py ├── underground.jpg ├── underground.png └── underground2.jpg ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── public ├── api │ ├── apothegm.php │ ├── checkAdmin.php │ ├── ipRequest.php │ ├── login.php │ ├── mail.php │ ├── map.php │ ├── map.test.php │ ├── mapReply.php │ ├── private │ │ ├── admin.example.php │ │ ├── dbcfg.example.php │ │ ├── emailcfg.example.php │ │ └── illegal_words_list.example.php │ ├── reply.php │ ├── searchUpload.php │ ├── sqlgenerator.php │ ├── statistics.php │ ├── user.php │ └── utils.php ├── favicon.png ├── global.css ├── icon.png ├── index.html ├── manifest.json ├── mockServiceWorker.js ├── resource │ ├── icons │ │ ├── blue.png │ │ ├── boss.png │ │ ├── collect.png │ │ ├── fireicon.png │ │ ├── green.png │ │ ├── littleboss.png │ │ ├── message.png │ │ ├── portal.png │ │ ├── purple.png │ │ ├── question.png │ │ ├── red.png │ │ ├── warning.png │ │ ├── white.png │ │ └── yellow.png │ └── images │ │ ├── 3dmappreview.png │ │ ├── about.png │ │ ├── apothegm.png │ │ ├── app.png │ │ ├── appicon.webp │ │ ├── dlc1.png │ │ ├── dlc1_narrow.png │ │ ├── dodo.png │ │ ├── fire.png │ │ ├── general.png │ │ ├── hoi4.jpg │ │ ├── kotone1.gif │ │ ├── map.png │ │ ├── modalbg.png │ │ └── qrcode.jpg └── sw.js ├── rollup.config.ts ├── src ├── @types │ └── leaflet-canvas-markers-with-title │ │ └── index.d.ts ├── App.svelte ├── Read-me-before-dev.txt ├── assets │ └── icons │ │ ├── icon-add-mark.svg │ │ ├── icon-close.svg │ │ ├── icon-collect.svg │ │ ├── icon-comment.svg │ │ ├── icon-delete.svg │ │ ├── icon-down-arrow.svg │ │ ├── icon-download_on_the_App_Store_Badge_CNSC_RGB_wht_092917.svg │ │ ├── icon-edit.svg │ │ ├── icon-left-arrow.svg │ │ ├── icon-pin.svg │ │ ├── icon-quit-mark.svg │ │ ├── icon-remark.svg │ │ ├── icon-right-arrow.svg │ │ ├── icon-search.svg │ │ ├── icon-send-myself.svg │ │ ├── icon-subscript-link-10.svg │ │ ├── icon-subscript-link-7.svg │ │ ├── icon-toggle.svg │ │ ├── icon-up-arrow.svg │ │ └── icon-warning.svg ├── components │ ├── CooperationModal.svelte │ ├── MapView.svelte │ ├── MapViewComponents │ │ ├── DirectionControl.svelte │ │ └── RightMenu.svelte │ ├── MenuItem.svelte │ ├── Modal.svelte │ ├── RouteViewer │ │ ├── RouteViewer.svelte │ │ ├── box.css │ │ ├── data.ts │ │ ├── drawer.ts │ │ └── types.ts │ ├── UpdateContentModal.svelte │ ├── button │ │ ├── ExportButton.svelte │ │ ├── ImportButton.svelte │ │ └── LangButton.svelte │ ├── canvasIcons.ts │ ├── icons.css │ └── icons.ts ├── config.ts ├── description.txt ├── global.d.ts ├── locale │ ├── index.ts │ └── lang │ │ ├── en.ts │ │ ├── ja.ts │ │ ├── zh-CN.ts │ │ └── zh-TW.ts ├── main.ts ├── mocks │ ├── browser.ts │ ├── data │ │ ├── admin.js │ │ ├── apothegm.js │ │ └── map.js │ ├── handlers.ts │ └── handles │ │ ├── apothegm.ts │ │ ├── checkAdmin.ts │ │ ├── isRequest.ts │ │ ├── map.ts │ │ └── searchUpload.ts ├── pages │ ├── 3DMap.svelte │ ├── About.svelte │ ├── Apothegm.svelte │ ├── Collect.svelte │ ├── General.svelte │ ├── Home.svelte │ ├── Map.svelte │ └── Routes.svelte ├── privateConfig.example.ts ├── router │ └── router.ts ├── stores.ts └── utils │ ├── convertor.ts │ ├── enum.ts │ ├── filters.ts │ ├── persist.ts │ ├── testdata.ts │ ├── typings.ts │ └── utils.ts └── tsconfig.json /.babelrc.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | "@babel/env", 4 | { 5 | targets: '> 0.25% in CN, not dead' 6 | } 7 | ], 8 | [ 9 | "@babel/typescript", 10 | { 11 | allowNamespaces: true 12 | } 13 | ] 14 | ]; 15 | const plugins = [[ 16 | "@babel/plugin-transform-runtime", 17 | { 18 | corejs: { 19 | version: 3, 20 | proposals: true, 21 | } 22 | } 23 | ]]; 24 | 25 | module.exports = { presets, plugins }; 26 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /config 2 | build -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | //此项是用来告诉eslint找当前配置文件不能往父级查找 5 | root: true, 6 | 7 | //此项是用来指定eslint解析器的,解析器必须符合规则,babel-eslint解析器是对babel解析器的包装使其与ESLint解析 8 | parser: '@typescript-eslint/parser', 9 | 10 | //此项是用来指定javaScript语言类型和风格,sourceType用来指定js导入的方式,默认是script,此处设置为module,指某块导入方式 11 | parserOptions: { 12 | // 设置 script(默认) 或 module,如果代码是在ECMASCRIPT中的模块 13 | sourceType: 'module', 14 | ecmaVersion: 6, 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | }, 19 | 20 | // 此项指定环境的全局变量,下面的配置指定为浏览器环境 21 | env: { 22 | browser: true, 23 | node: true, 24 | commonjs: true, 25 | es6: true, 26 | amd: true, 27 | }, 28 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 29 | // 此项是用来配置标准的js风格,就是说写代码的时候要规范的写,如果你使用vs-code我觉得应该可以避免出错 30 | //extends: 'vue', 31 | // 此项是用来提供插件的,插件名称省略了eslint-plugin-,下面这个配置是用来规范html的 32 | plugins: ['html', 'react'], 33 | /* 34 | 下面这些rules是用来设置从插件来的规范代码的规则,使用必须去掉前缀eslint-plugin- 35 | 主要有如下的设置规则,可以设置字符串也可以设置数字,两者效果一致 36 | "off" -> 0 关闭规则 37 | "warn" -> 1 开启警告规则 38 | "error" -> 2 开启错误规则 39 | */ 40 | rules: { 41 | // 不需要 42 | 'space-before-function-paren': 0, // 函数定义时括号前面要不要有空格 43 | 'eol-last': 0, // 文件以单一的换行符结束 44 | 'no-extra-semi': 0, // 可以多余的冒号 45 | semi: 0, // 语句可以不需要分号结尾 46 | eqeqeq: 0, // 必须使用全等 47 | 'one-var': 0, // 连续声明 48 | 'no-undef': 0, // 可以 有未定义的变量 49 | 50 | // 警告 51 | 'no-extra-boolean-cast': 1, // 不必要的bool转换 52 | 'no-extra-parens': 1, // 非必要的括号 53 | 'no-empty': 1, // 块语句中的内容不能为空 54 | 'no-use-before-define': [1, 'nofunc'], // 未定义前不能使用 55 | complexity: [1, 50], // 循环复杂度 56 | 'no-unused-vars': 1, // 不能有声明后未被使用的变量或参数 57 | 58 | // react 59 | 'react/jsx-uses-react': 2, 60 | 'react/jsx-uses-vars': 2, 61 | 62 | // 错误 63 | 'comma-dangle': [0, 'always'], // 对象字面量项尾不能有逗号 64 | 'no-debugger': 2, // 禁止使用debugger 65 | 'no-constant-condition': 2, // 禁止在条件中使用常量表达式 if(true) if(1) 66 | 'no-dupe-args': 2, // 函数参数不能重复 67 | 'no-dupe-keys': 2, // 在创建对象字面量时不允许键重复 {a:1,a:1} 68 | 'no-duplicate-case': 2, // switch中的case标签不能重复 69 | 'no-empty-character-class': 2, // 正则表达式中的[]内容不能为空 70 | 'no-invalid-regexp': 2, // 禁止无效的正则表达式 71 | 'no-func-assign': 2, // 禁止重复的函数声明 72 | 'valid-typeof': 2, // 必须使用合法的typeof的值 73 | 'no-unreachable': 2, // 不能有无法执行的代码 74 | 'no-unexpected-multiline': 2, // 避免多行表达式 75 | 'no-sparse-arrays': 2, // 禁止稀疏数组, [1,,2] 76 | 'no-shadow-restricted-names': 2, // 严格模式中规定的限制标识符不能作为声明时的变量名使用 77 | 'no-cond-assign': 2, // 禁止在条件表达式中使用赋值语句 78 | 'no-native-reassign': 2, // 不能重写native对象 79 | 80 | // 代码风格 81 | 'no-else-return': 1, // 如果if语句里面有return,后面不能跟else语句 82 | 'no-multi-spaces': 1, // 不能用多余的空格 83 | 'key-spacing': [ 84 | 1, 85 | { 86 | // 对象字面量中冒号的前后空格 87 | beforeColon: false, 88 | afterColon: true, 89 | }, 90 | ], 91 | 'block-scoped-var': 2, // 块语句中使用var 92 | 'consistent-return': 2, // return 后面是否允许省略 93 | 'accessor-pairs': 2, // 在对象中使用getter/setter 94 | 'dot-location': [2, 'property'], // 对象访问符的位置,换行的时候在行首还是行尾 95 | 'no-lone-blocks': 2, // 禁止不必要的嵌套块 96 | 'no-labels': 2, // 禁止标签声明 97 | 'no-extend-native': 2, // 禁止扩展native对象 98 | 'no-floating-decimal': 2, // 禁止省略浮点数中的0 .5 3. 99 | 'no-loop-func': 2, // 禁止在循环中使用函数(如果没有引用外部变量不形成闭包就可以) 100 | 'no-new-func': 2, // 禁止使用new Function 101 | 'no-self-compare': 2, // 不能比较自身 102 | 'no-sequences': 2, // 禁止使用逗号运算符 103 | 'no-throw-literal': 2, // 禁止抛出字面量错误 throw "error"; 104 | 'no-return-assign': [2, 'always'], // return 语句中不能有赋值表达式 105 | 'no-redeclare': [ 106 | 2, 107 | { 108 | // 禁止重复声明变量 109 | builtinGlobals: true, 110 | }, 111 | ], 112 | 'no-unused-expressions': [ 113 | 2, 114 | { 115 | // 禁止无用的表达式 116 | allowShortCircuit: true, 117 | allowTernary: true, 118 | }, 119 | ], 120 | 'no-useless-call': 2, // 禁止不必要的call和apply 121 | 'no-useless-concat': 2, 122 | 'no-void': 2, // 禁用void操作符 123 | 'no-with': 2, // 禁用with 124 | 'space-infix-ops': 2, // 中缀操作符周围要不要有空格 125 | 'valid-jsdoc': [ 126 | 2, 127 | { 128 | // jsdoc规则 129 | requireParamDescription: true, 130 | requireReturnDescription: true, 131 | }, 132 | ], 133 | 'no-warning-comments': [ 134 | 2, 135 | { 136 | // 不能有警告备注 137 | terms: ['todo', 'fixme', 'any other term'], 138 | location: 'anywhere', 139 | }, 140 | ], 141 | curly: 1, // 必须使用 if(){} 中的{} 142 | 143 | // common js 144 | 'no-duplicate-imports': 1, 145 | 'no-anonymous-default-export': 0, 146 | }, 147 | }; 148 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /public/build/ 3 | /dist/ 4 | 5 | .DS_Store 6 | 7 | /public/api/private/* 8 | !/public/api/private/*.example.* 9 | 10 | /public/resource/maps/ 11 | 12 | /mapDivider/map*/ 13 | 14 | /todo.txt 15 | /src/privateConfig.ts 16 | 17 | /public/api/plugin/* 18 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .eslintignore 4 | *.png 5 | *.toml 6 | .gitignore 7 | .prettierignore 8 | LICENSE 9 | /public/build 10 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | //每行最多多少个字符换行 3 | printWidth: 200, 4 | // tab缩进大小,默认为2 5 | tabWidth: 2, 6 | // 使用tab缩进,默认false 7 | useTabs: false, 8 | // 使用分号, 默认true 9 | semi: true, 10 | // 使用单引号, 默认false(在jsx中配置无效, 默认都是双引号) 11 | singleQuote: true, 12 | // 行尾逗号,默认none,可选 none|es5|all 13 | // es5 包括es5中的数组、对象 14 | // all 包括函数对象等所有可选 15 | TrailingCooma: 'all', 16 | // 对象中的空格 默认true 17 | // true: { foo: bar } 18 | // false: {foo: bar} 19 | bracketSpacing: true, 20 | // JSX标签闭合位置 默认false 21 | // false:
25 | // true:
28 | jsxBracketSameLine: false, 29 | // 箭头函数参数括号 默认avoid 可选 avoid| always 30 | // avoid 能省略括号的时候就省略 例如x => x 31 | // always 总是有括号 32 | arrowParens: 'avoid', 33 | }; 34 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "svelte.svelte-vscode", 4 | "lokalise.i18n-ally" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // 使用 IntelliSense 了解相关属性。 3 | // 悬停以查看现有属性的描述。 4 | // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch Edge", 9 | "request": "launch", 10 | "type": "pwa-msedge", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}/src" 13 | }, 14 | { 15 | "name": "Python: mapDivider", 16 | "type": "python", 17 | "request": "launch", 18 | "program": "${file}", 19 | "console": "integratedTerminal", 20 | "justMyCode": true, 21 | "cwd": "${workspaceFolder}/mapDivider" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "i18n-ally.localesPaths": [ 3 | "src/locale/lang" 4 | ], 5 | "i18n-ally.enabledFrameworks": [ 6 | "svelte" 7 | ], 8 | "i18n-ally.enabledParsers": [ 9 | "ts" 10 | ], 11 | "i18n-ally.extract.autoDetect": false, 12 | "i18n-ally.sourceLanguage": "zh-CN", 13 | "i18n-ally.keystyle": "nested", 14 | "i18n-ally.fullReloadOnChanged": true, 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 elpwc/wniko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 |

6 |
7 | 8 | # 老头环协作编辑地图 9 | 10 | ### 𝐄𝐋𝐃𝐄𝐍 𝐑𝐈𝐍𝐆 𝐎𝐍𝐋𝐈𝐍𝐄 𝐌𝐀𝐏 11 | 12 | ![alt GithubStars](https://img.shields.io/github/stars/elpwc/EldenRingOnlineMap.svg?style=flat) 13 | ![alt GithubStars](https://img.shields.io/github/forks/elpwc/EldenRingOnlineMap.svg?style=flat) 14 | ![alt license](https://img.shields.io/badge/license-MIT-green) 15 | 16 | 17 | 地址:https://www.elpwc.com/eldenringmap/ 18 |
19 | 20 | 21 |
22 | 23 | ## 技术栈 24 | 25 | - Frontend: Svelte + Leaflet, via TypeScript 26 | - Backend: PHP + MySQL, based on Apache + nginx, WinServer 27 | 28 | 一开始只是随手写的个人用的小网站,因为一直有在尝鲜 Svelte 框架,就很大胆地使用了 29 | 选用纯 PHP 写后端接口也是图方便省事的原因 30 | 31 | 地图是基于 Leaflet 实现的,现行版本的地标全部使用 DOM 渲染。 32 | 33 | ## 截图 34 | 35 | ![alt screenshot](./images/ss1.png) 36 | ![alt screenshot](./images/ss2.png) 37 | 38 | ## 关于参与开发 39 | 40 | 老头环地图的日访问量一直维持在20-30万之间,在如此庞大的用户量面前,我笨拙的技术水平已经不足以跟得上日日增加的计划功能、反馈的bug的开发,所以,欢迎通过邮件/QQ/issue联系 参与到开发里来~ 41 | 42 | ## 贡献者 43 | 44 | | spking11|Ranger| 45 | |-|-| 46 | |spking11([@spking11](https://github.com/spking11))|Ranger([@RangerChen](https://github.com/RangerChen))| 47 | 48 | 49 | ## 开发与部署 50 | 51 | ### 前端 52 | 53 | 前端框架: 54 | Svelte:一个比较新的轻量化的前端框架,具体信息可以参考[官网](https://svelte.dev/)或[中文网](https://www.sveltejs.cn/) 55 | 56 | > Svelte 是一种全新的构建用户界面的方法。传统框架如 React 和 Vue 在浏览器中需要做大量的工作,而 Svelte 将这些工作放到构建应用程序的编译阶段来处理。 57 | 58 | nodejs version: 16.13.0 59 | 60 | npm version: 8.1.2 61 | 62 | pnpm version: 7.0.0-beta.2 63 | 64 | 1. 克隆仓库到本地 65 | ```bash 66 | git clone https://github.com/elpwc/EldenRingOnlineMap.git 67 | ``` 68 | 69 | 2. 依赖安装 70 | 71 | **由于项目当前使用了 npm 进行包管理,推荐使用 `npm` 或 `pnpm` 进行依赖安装** 72 | 73 | [pnpm传送门](https://www.pnpm.cn/) 74 | 75 | 如果本地的环境使用的是 `yarn`,提交 pull request 是可以忽略 `yarn.lock` 文件 76 | ```bash 77 | npm i // or pnpm install | yarn 78 | ``` 79 | 80 | 3. 开发环境调试 81 | ```bash 82 | npm run dev // or pnpm dev | yarn dev 83 | ``` 84 | 85 | 4. 构建打包 86 | ```bash 87 | npm run build // or pnpm build | yarn build 88 | ``` 89 | 90 | 5. 部署 91 | 92 | 打包后的静态文件位于 `/public` 文件夹下,可以直接作为静态资源部署在静态服务器上。 93 | 94 | ### 后端 95 | 96 | 依赖`php`,`mysql`,, 97 | 98 | 1. 初始化数据库 99 | 100 | 找到数据库初始化脚本文件 `/database.sql`,通过数据库客户端软件(e.g. navicat)执行脚本即可; 101 | 102 | 2. 配置数据库 103 | 104 | 数据库配置在 `/public/api/private/` 下: 105 | 106 | ```bash 107 | ├── public 108 | │ ├── api 109 | │ │ ├── private 110 | │ │ │ ├── admin.example.php // Admin 模式密码 111 | │ │ │ ├── dbcfg.example.php // 数据库配置文件 112 | │ │ │ └── illegal_words_list.example.php // 屏蔽词列表 113 | ``` 114 | 115 | 启动方式: 116 | 117 | 在对应的配置中增加了自己的内容后,重命名,将文件名中的 `.example` 就可以生效了 118 | 119 | e.g. `admin.example.php` -> `admin.php` 120 | 121 | 在前端进入管理员模式的办法可以可以细读 `src/pages/About.svelte` 内容,进入了就可以直接在前端对各个数据删改了(说明页会出现一个(Admin)字样说明已进入 Admin 模式; 122 | 123 | 3. 部署 124 | 125 | 直接将 `/public` 文件夹的内容部署至 Apache 服务器上即可。 126 | 127 | PS:关于各个文件的说明在 `/src/description.txt` 中; 128 | 129 | 130 | ## 开源许可 131 | 132 | MIT 133 | 134 | 在包含此协议的前提下可以随意使用、修改、发布 EldenRingMap 的代码。 135 | 136 | -------------------------------------------------------------------------------- /database/database.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS `eldenRingMap` DEFAULT CHARSET UTF8MB4; 2 | 3 | CREATE TABLE IF NOT EXISTS `map`( 4 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 5 | `type` VARCHAR(8) NOT NULL, 6 | `name` VARCHAR(100) NOT NULL, 7 | `desc` VARCHAR(1024), 8 | `lng` DOUBLE NOT NULL, 9 | `lat` DOUBLE NOT NULL, 10 | `is_underground` BOOLEAN DEFAULT FALSE, 11 | `position` INT UNSIGNED DEFAULT 0, 12 | `is_achievement` BOOLEAN DEFAULT FALSE, 13 | `is_lock` BOOLEAN DEFAULT FALSE, 14 | `delete_request` INT UNSIGNED DEFAULT 0, 15 | `like` INT UNSIGNED DEFAULT 0, 16 | `dislike` INT UNSIGNED DEFAULT 0, 17 | `ip` VARCHAR(20), 18 | `is_deleted` BOOLEAN DEFAULT FALSE, 19 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 20 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP, 21 | `x` DOUBLE DEFAULT NULL, 22 | `y` DOUBLE DEFAULT NULL, 23 | `uid` INT UNSIGNED 24 | )ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 25 | 26 | CREATE TABLE IF NOT EXISTS `apothegm` ( 27 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 28 | `title` VARCHAR(20), 29 | `content` VARCHAR(1024), 30 | `type` VARCHAR(8), 31 | `gesture` INT UNSIGNED DEFAULT 0, 32 | `is_top` BOOLEAN DEFAULT FALSE, 33 | `like` INT UNSIGNED DEFAULT 0, 34 | `dislike` INT UNSIGNED DEFAULT 0, 35 | `ip` VARCHAR(20), 36 | `is_deleted` BOOLEAN DEFAULT FALSE, 37 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 38 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP,, 39 | `uid` INT UNSIGNED 40 | `reply_date` DATETIME DEFAULT CURRENT_TIMESTAMP 41 | 42 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 43 | 44 | CREATE TABLE IF NOT EXISTS `map_reply` ( 45 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 46 | `pid` INT UNSIGNED, 47 | `content` VARCHAR(1024), 48 | `is_seen` BOOLEAN DEFAULT FALSE, 49 | `like` INT UNSIGNED DEFAULT 0, 50 | `dislike` INT UNSIGNED DEFAULT 0, 51 | `ip` VARCHAR(20), 52 | `is_deleted` BOOLEAN DEFAULT FALSE, 53 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 54 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP, 55 | `uid` INT UNSIGNED 56 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 57 | 58 | CREATE TABLE IF NOT EXISTS `apo_reply` ( 59 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 60 | `pid` INT UNSIGNED, 61 | `content` VARCHAR(1024), 62 | `is_seen` BOOLEAN DEFAULT FALSE, 63 | `like` INT UNSIGNED DEFAULT 0, 64 | `dislike` INT UNSIGNED DEFAULT 0, 65 | `ip` VARCHAR(20), 66 | `is_deleted` BOOLEAN DEFAULT FALSE, 67 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 68 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP, 69 | `uid` INT UNSIGNED 70 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 71 | 72 | CREATE TABLE IF NOT EXISTS `search` ( 73 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 74 | `content` VARCHAR(1024), 75 | `ip` VARCHAR(20), 76 | `position` VARCHAR(10) NULL, 77 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP 78 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 79 | 80 | /* 支线中的一个节点 */ 81 | CREATE TABLE IF NOT EXISTS `routes` ( 82 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 83 | `title` VARCHAR(1024), 84 | `content` VARCHAR(1024), 85 | `type` INT NOT NULL DEFAULT 0, 86 | `is_main` BOOLEAN DEFAULT FALSE, /* 是否是主线 */ 87 | `achievement` INT UNSIGNED DEFAULT 0, 88 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 89 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP 90 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 91 | 92 | /* 一条支线 */ 93 | CREATE TABLE IF NOT EXISTS `branches` ( 94 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 95 | `title` VARCHAR(1024), 96 | `content` VARCHAR(1024), 97 | `ip` VARCHAR(20), 98 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 99 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP 100 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 101 | 102 | /* 连接 */ 103 | CREATE TABLE IF NOT EXISTS `link` ( 104 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 105 | `pid` INT UNSIGNED, 106 | `cid` INT UNSIGNED, 107 | `type` INT UNSIGNED, 108 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 109 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP 110 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 111 | 112 | /* 完整支线-节点 中间表 */ 113 | CREATE TABLE IF NOT EXISTS `branch_route` ( 114 | `bid` INT UNSIGNED, 115 | `rid` INT UNSIGNED, 116 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 117 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP 118 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 119 | 120 | /* 节点回复 */ 121 | CREATE TABLE IF NOT EXISTS `routes_reply` ( 122 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 123 | `pid` INT UNSIGNED, 124 | `content` VARCHAR(1024), 125 | `is_seen` BOOLEAN DEFAULT FALSE, 126 | `like` INT UNSIGNED DEFAULT 0, 127 | `dislike` INT UNSIGNED DEFAULT 0, 128 | `ip` VARCHAR(20), 129 | `is_deleted` BOOLEAN DEFAULT FALSE, 130 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 131 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP 132 | ) ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 133 | 134 | create table `user`( 135 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 136 | `name` VARCHAR(100) NOT NULL, 137 | `pw` VARCHAR(100) NOT NULL, 138 | `is_deleted` BOOLEAN DEFAULT FALSE, 139 | `is_banned` BOOLEAN DEFAULT FALSE, 140 | `auth` INT DEFAULT 0, 141 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 142 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP, 143 | `last_login` DATETIME DEFAULT CURRENT_TIMESTAMP, 144 | `email` NOT NULL, 145 | )ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 146 | 147 | create table `collection`( 148 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 149 | `map_id` INT NOT NULL, 150 | `uid` INT NOT NULL UNSIGNED, 151 | `is_deleted` BOOLEAN DEFAULT FALSE, 152 | `flag` BOOLEAN DEFAULT FALSE, 153 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 154 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP 155 | )ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 156 | 157 | create table `hidden`( 158 | `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, 159 | `map_id` INT NOT NULL, 160 | `uid` INT NOT NULL UNSIGNED, 161 | `is_deleted` BOOLEAN DEFAULT FALSE, 162 | `flag` BOOLEAN DEFAULT FALSE, 163 | `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, 164 | `update_date` DATETIME ON UPDATE CURRENT_TIMESTAMP 165 | )ENGINE=InnoDB DEFAULT CHARSET=UTF8MB4; 166 | -------------------------------------------------------------------------------- /database/deleteRepeated.sql: -------------------------------------------------------------------------------- 1 | update `map` set `is_deleted` = 1 where `name` like "%重复%" and `is_deleted` = 0; -------------------------------------------------------------------------------- /database/likeDislikeDiffer.sql: -------------------------------------------------------------------------------- 1 | SELECT * FROM eldenringmap.map WHERE (is_deleted = 0) ORDER BY (CAST(`dislike` AS SIGNED) - CAST(`like` AS SIGNED)) ASC; -------------------------------------------------------------------------------- /database/replace.sql: -------------------------------------------------------------------------------- 1 | update `map` SET `name`=replace(`name`, "🪦", "") where `name` like "🪦%"; -------------------------------------------------------------------------------- /database/selectRepeated.sql: -------------------------------------------------------------------------------- 1 | select * from `map` where `name` like "%重复%" and is_deleted = 0; -------------------------------------------------------------------------------- /images/oldscreenshot/1/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/1/ss1.png -------------------------------------------------------------------------------- /images/oldscreenshot/1/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/1/ss2.png -------------------------------------------------------------------------------- /images/oldscreenshot/2/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/2/ss1.png -------------------------------------------------------------------------------- /images/oldscreenshot/2/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/2/ss2.png -------------------------------------------------------------------------------- /images/oldscreenshot/2/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/2/ss3.png -------------------------------------------------------------------------------- /images/oldscreenshot/3/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/3/ss1.png -------------------------------------------------------------------------------- /images/oldscreenshot/3/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/3/ss2.png -------------------------------------------------------------------------------- /images/oldscreenshot/3/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/3/ss3.png -------------------------------------------------------------------------------- /images/oldscreenshot/3/ss4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/3/ss4.png -------------------------------------------------------------------------------- /images/oldscreenshot/4/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/4/ss1.png -------------------------------------------------------------------------------- /images/oldscreenshot/4/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/4/ss2.png -------------------------------------------------------------------------------- /images/oldscreenshot/4/ss3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/4/ss3.png -------------------------------------------------------------------------------- /images/oldscreenshot/4/ss4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/4/ss4.png -------------------------------------------------------------------------------- /images/oldscreenshot/4/ss5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/4/ss5.png -------------------------------------------------------------------------------- /images/oldscreenshot/4/ss6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/oldscreenshot/4/ss6.png -------------------------------------------------------------------------------- /images/origin/boss.sai2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/origin/boss.sai2 -------------------------------------------------------------------------------- /images/origin/icons.sai2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/origin/icons.sai2 -------------------------------------------------------------------------------- /images/origin/message.sai2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/origin/message.sai2 -------------------------------------------------------------------------------- /images/origin/portal.sai2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/origin/portal.sai2 -------------------------------------------------------------------------------- /images/origin/question.sai2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/origin/question.sai2 -------------------------------------------------------------------------------- /images/origin/warning.sai2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/origin/warning.sai2 -------------------------------------------------------------------------------- /images/ss1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/ss1.png -------------------------------------------------------------------------------- /images/ss2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/images/ss2.png -------------------------------------------------------------------------------- /mapDivider/dlc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/mapDivider/dlc1.png -------------------------------------------------------------------------------- /mapDivider/mapDivider.py: -------------------------------------------------------------------------------- 1 | from ctypes import resize 2 | import os 3 | import math 4 | from PIL import Image 5 | 6 | 7 | def overlap(box1, box2): 8 | minx1, miny1, maxx1, maxy1 = box1 9 | minx2, miny2, maxx2, maxy2 = box2 10 | minx = max(minx1, minx2) 11 | miny = max(miny1, miny2) 12 | maxx = min(maxx1, maxx2) 13 | maxy = min(maxy1, maxy2) 14 | if minx > maxx or miny > maxy: 15 | return False 16 | else: 17 | return True 18 | 19 | 20 | # 概念说明: 21 | # 基准点:全图的4张图块的中心点在原图上的坐标 22 | # 原图:拼接好的地图 23 | # 全图:在缩放级别为 2 (最大)时,容纳原图的所有图块组成的图 24 | 25 | def get_divided_maps(imageFileName, completeLeft, completeTop, resultFolder, currentXCalculator, currentYCalculator): 26 | 27 | img = Image.open(imageFileName) 28 | print(img.size) 29 | 30 | originalSize = img.size 31 | 32 | # 开始级别 33 | minLevel = 7 34 | 35 | # 目标级别 36 | maxLevel = 2 37 | 38 | # 目标图块大小 39 | resW = 200 40 | resH = 200 41 | 42 | # 开始 43 | for level in range(minLevel, maxLevel-1, -1): 44 | print('level: '+str(level)) 45 | 46 | # 当前缩放系数 47 | resizeK = (2 ** (level - 7)) 48 | 49 | # 缩放 50 | img = img.resize( 51 | (math.ceil(originalSize[0] * resizeK), math.ceil(originalSize[1] * resizeK))) 52 | 53 | # 当前原图长宽 54 | currentW = img.size[0] 55 | currentH = img.size[1] 56 | 57 | # 当前左上角坐标 58 | currentLeft = math.ceil(completeLeft * resizeK) 59 | currentTop = math.ceil(completeTop * resizeK) 60 | 61 | # 当前总图长宽 62 | currentAllW = math.ceil(12800 * resizeK) 63 | currentAllH = math.ceil(12800 * resizeK) 64 | 65 | # 左上角的图块的XY编号,每次以2的指数递减 66 | currentX = currentXCalculator(level) 67 | currentY = currentYCalculator(level) 68 | 69 | # 当前图块编号最大值 70 | currentXCount = math.ceil(currentAllW / 200) 71 | currentYCount = math.ceil(currentAllH / 200) 72 | 73 | # 开切! 74 | for X in range(currentX, currentXCount + currentX): 75 | for Y in range(currentY, currentYCount + currentY): 76 | cutX = currentLeft + (X - currentX) * resW 77 | cutY = currentTop + (Y - currentY) * resH 78 | 79 | # 如果有重叠再切 80 | if(overlap((0, 0, currentW, currentH), (cutX, cutY, cutX + resW, cutY+resH))): 81 | # 建立文件夹 82 | if (not os.path.exists("./{resultFolder}/{level}/".format(resultFolder=resultFolder, level=level) + str(X))): 83 | os.makedirs( 84 | "./{resultFolder}/{level}/".format(resultFolder=resultFolder, level=level) + str(X)) 85 | 86 | # 切! 87 | res = Image.new('RGB', (resW, resH), (34, 34, 34)) 88 | res.paste(img, (-cutX, -cutY)) 89 | 90 | # res = img.crop((cutX, cutY, cutX + resW, cutY + resH)) 91 | # 存! 92 | res.save("./{resultFolder}/{level}/{X}/{Y}.jpg".format( 93 | resultFolder=resultFolder, level=level, X=X, Y=Y)) 94 | # 基准点:1085, 2987 95 | # 计算后的全图左上角:1240-6400, 2987-6400 => -5160, -3413 96 | # 计算后的全图长宽:12800, 12800 97 | # 原图长宽:5235, 3278 98 | 99 | 100 | def currentXCalculator_1(level): 101 | return 2 ** (level - 2) 102 | 103 | 104 | def currentYCalculator_1(level): 105 | return 2 ** (level - 1) 106 | 107 | # 基准点:2461, 2032 108 | # 计算后的全图左上角:2461-6400, 2032-6400 => -3939, -4368 109 | # 计算后的全图长宽:12800, 12800 110 | 111 | 112 | def currentXCalculator_2(level): 113 | return 0 114 | 115 | 116 | def currentYCalculator_2(level): 117 | return 2 ** (level - 2) 118 | 119 | 120 | if __name__ == "__main__": 121 | # 希芙拉河 122 | ''' 123 | get_divided_maps( 124 | './underground.jpg', 125 | -5160, -3412, 'map', 126 | currentXCalculator_1, 127 | currentYCalculator_1 128 | ) 129 | ''' 130 | 131 | # 安瑟尔河 132 | ''' 133 | get_divided_maps( 134 | './underground2.jpg', 135 | -4039, -4458, 'map2', 136 | currentXCalculator_2, 137 | currentYCalculator_2 138 | ) 139 | ''' 140 | 141 | # 基准点:3500, 3500 142 | # 计算后的全图左上角:480-6400, 522-6400 => 143 | # 计算后的全图长宽:12800, 12800 144 | 145 | # DLC1 146 | get_divided_maps( 147 | './dlc1.png', 148 | 3500-6400, 3500-6400, 'map3', 149 | currentXCalculator_1, 150 | currentYCalculator_1 151 | ) 152 | -------------------------------------------------------------------------------- /mapDivider/underground.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/mapDivider/underground.jpg -------------------------------------------------------------------------------- /mapDivider/underground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/mapDivider/underground.png -------------------------------------------------------------------------------- /mapDivider/underground2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/mapDivider/underground2.jpg -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elden-ring-online-map", 3 | "version": "3.2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c", 7 | "dev": "rollup -c -w", 8 | "start": "sirv public --no-clear", 9 | "check": "svelte-check --tsconfig ./tsconfig.json" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.17.7", 13 | "@babel/plugin-transform-runtime": "^7.17.0", 14 | "@babel/preset-env": "^7.16.11", 15 | "@babel/preset-typescript": "^7.16.7", 16 | "@rollup/plugin-babel": "^5.3.1", 17 | "@rollup/plugin-commonjs": "^17.0.0", 18 | "@rollup/plugin-node-resolve": "^11.0.0", 19 | "@rollup/plugin-replace": "^4.0.0", 20 | "@tsconfig/svelte": "^2.0.0", 21 | "@types/file-saver": "^2.0.5", 22 | "@types/jquery": "^3.5.14", 23 | "@types/leaflet": "^1.7.9", 24 | "@types/md5": "^2.3.2", 25 | "@types/qs": "^6.9.7", 26 | "msw": "^0.39.1", 27 | "rollup": "^2.3.4", 28 | "rollup-plugin-copy": "^3.4.0", 29 | "rollup-plugin-css-only": "^3.1.0", 30 | "rollup-plugin-delete": "^2.0.0", 31 | "rollup-plugin-livereload": "^2.0.0", 32 | "rollup-plugin-svelte": "^7.0.0", 33 | "rollup-plugin-svelte-svg": "^1.0.0-beta.6", 34 | "rollup-plugin-terser": "^7.0.0", 35 | "svelte": "^3.0.0", 36 | "svelte-check": "^2.0.0", 37 | "svelte-preprocess": "^4.0.0", 38 | "tslib": "^2.0.0", 39 | "typescript": "^4.0.0" 40 | }, 41 | "dependencies": { 42 | "@babel/runtime-corejs3": "^7.17.7", 43 | "@rollup/plugin-json": "^4.1.0", 44 | "axios": "0.21.1", 45 | "d3-dag": "^0.11.3", 46 | "dayjs": "^1.11.0", 47 | "file-saver": "^2.0.5", 48 | "jquery": "^3.6.0", 49 | "leaflet": "^1.9.4", 50 | "leaflet-canvas-markers-with-title": "^0.8.0", 51 | "md5": "^2.3.0", 52 | "qs": "^6.10.3", 53 | "sirv-cli": "^2.0.0", 54 | "spinkit": "^2.0.1", 55 | "svelte-i18n": "^3.3.13", 56 | "svelte-spa-router": "^3.2.0", 57 | "zhconvertor": "^2.0.0" 58 | }, 59 | "msw": { 60 | "workerDirectory": "public" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/api/apothegm.php: -------------------------------------------------------------------------------- 1 | title)); 23 | @$content = trim((string)($data->content)); 24 | @$like = ($data->like); 25 | @$dislike = ($data->dislike); 26 | @$ip = trim((string)($data->ip)); 27 | @$type = trim((string)($data->type)); 28 | 29 | $sql = 'INSERT 30 | INTO apothegm (`title`, `content`, `like`, `dislike`, `ip`, `is_deleted`, `type`) 31 | VALUES ("' . cator_to_cn_censorship(anti_inj($title)) . '","' . cator_to_cn_censorship(anti_inj($content)) . '","' . $like . '","' . $dislike . '","' . anti_inj($ip) . '", "0", "' . anti_inj($type) . '"); 32 | '; 33 | 34 | $result = mysqli_query($sqllink, $sql); 35 | 36 | echo json_encode($result); 37 | break; 38 | case 'GET': 39 | @$id_ori = $_GET['id']; 40 | /** IP */ 41 | @$ip_ori = $_GET['ip']; 42 | /** 个数, 不填为全部 */ 43 | @$count_ori = $_GET['count']; 44 | @$kword_ori = $_GET['kword']; 45 | @$type_ori = $_GET['type']; 46 | 47 | $id = ''; 48 | $ip = ''; 49 | $count = 0; 50 | $kword = ''; 51 | $type = ''; 52 | 53 | if (is_numeric($count_ori)) { 54 | $count = (int)$count_ori; 55 | } 56 | if (is_numeric($id_ori)) { 57 | $id = (int)$id_ori; 58 | } 59 | 60 | if ($count == 0) { 61 | $count = '*'; 62 | } else { 63 | $count = 'TOP ' . $count; 64 | } 65 | 66 | if (isset($ip_ori)) { 67 | $ip = trim(anti_inj((string)$ip_ori)); 68 | } 69 | if (isset($type_ori)) { 70 | $type = trim(anti_inj((string)$type_ori)); 71 | } 72 | if (isset($kword_ori)) { 73 | $kword = trim(anti_inj((string)$kword_ori)); 74 | } 75 | 76 | $select = []; 77 | 78 | if ($id <= 0) { 79 | $select = [ 80 | 'AND', 81 | [ 82 | [ 83 | 'OR', [ 84 | ['LIKE', ['title', $kword]], 85 | ['LIKE', ['content', $kword]] 86 | ] 87 | ], 88 | ['', ['ip', $ip]], 89 | ['', ['type', $type]], 90 | ['', ['is_deleted', '0']] 91 | ] 92 | ]; 93 | } else { 94 | $select = ['', ['id', $id]]; 95 | } 96 | 97 | 98 | $geneRes = get_condition($select); 99 | if ($geneRes != '') { 100 | $geneRes = "WHERE $geneRes"; 101 | } 102 | 103 | $sql = "SELECT $count 104 | FROM apothegm 105 | $geneRes 106 | ORDER BY `reply_date` DESC; 107 | "; 108 | 109 | $result = mysqli_query($sqllink, $sql); 110 | 111 | $res = []; 112 | 113 | if ($result->num_rows > 0) { 114 | $i = 0; 115 | while ($row = $result->fetch_assoc()) { 116 | $crtPid = $row['id']; 117 | $sql2 = "SELECT * 118 | FROM apo_reply 119 | WHERE `pid`='$crtPid' AND `is_deleted`='0' 120 | ORDER BY `create_date`; 121 | "; 122 | $replyResult = mysqli_query($sqllink, $sql2); 123 | 124 | $replies = []; 125 | 126 | if ($replyResult->num_rows > 0) { 127 | $j = 0; 128 | while ($row2 = $replyResult->fetch_assoc()) { 129 | array_push($replies, [ 130 | 'id' => (int)$row2['id'], 131 | 'pid' => (int)$row2['pid'], 132 | 'content' => $row2['content'], 133 | 'like' => (int)$row2['like'], 134 | 'dislike' => (int)$row2['dislike'], 135 | 'ip' => $row2['ip'], 136 | 'is_deleted' => (bool)(int)$row2['is_deleted'], 137 | 'create_date' => $row2['create_date'], 138 | 'update_date' => $row2['update_date'], 139 | ]); 140 | $j++; 141 | } 142 | } 143 | 144 | 145 | array_push($res, [ 146 | 'id' => (int)$row['id'], 147 | 'title' => $row['title'], 148 | 'content' => $row['content'], 149 | 'type' => $row['type'], 150 | 'gesture' => (int)$row['gesture'], 151 | 'is_top' => (bool)(int)$row['is_top'], 152 | 'like' => (int)$row['like'], 153 | 'dislike' => (int)$row['dislike'], 154 | 'ip' => $row['ip'], 155 | 'is_deleted' => (bool)(int)$row['is_deleted'], 156 | 'create_date' => $row['create_date'], 157 | 'update_date' => $row['update_date'], 158 | 'reply_date' => $row['reply_date'], 159 | 'replies' => $replies 160 | ]); 161 | $i++; 162 | } 163 | } 164 | 165 | 166 | 167 | echo json_encode($res); 168 | 169 | break; 170 | case 'DELETE': 171 | @$id = trim((string)($data->id)); 172 | 173 | $sql = "UPDATE apothegm 174 | SET `is_deleted`=1 175 | WHERE `id`=$id;"; 176 | 177 | $result = mysqli_query($sqllink, $sql); 178 | 179 | echo ($result); 180 | 181 | break; 182 | case 'PATCH': 183 | @$id = property_exists($data, 'id') ? trim((string)($data->id)) : null; 184 | @$title = property_exists($data, 'title') ? trim((string)($data->title)) : null; 185 | @$content = property_exists($data, 'content') ? trim((string)($data->content)) : null; 186 | @$type = property_exists($data, 'type') ? trim((string)($data->type)) : null; 187 | @$gesture = property_exists($data, 'gesture') ? (string)($data->gesture) : null; 188 | @$like = property_exists($data, 'like') ? (string)($data->like) : null; 189 | @$dislike = property_exists($data, 'dislike') ? (string)($data->dislike) : null; 190 | @$ip = property_exists($data, 'ip') ? trim((string)($data->ip)) : null; 191 | @$is_deleted = property_exists($data, 'is_deleted') ? (string)($data->is_deleted) : null; 192 | @$reply_date = property_exists($data, 'reply_date') ? (string)($data->reply_date) : null; 193 | 194 | if ($is_deleted == 'false') $is_deleted = "0"; 195 | 196 | $select = [ 197 | ['title', $title], 198 | ['content', $content], 199 | ['type', $type], 200 | ['gesture', $gesture, true], 201 | ['like', $like, true, 'increment'], 202 | ['dislike', $dislike, true, 'increment'], 203 | ['ip', $ip], 204 | ['is_deleted', $is_deleted, true], 205 | ['reply_date', $reply_date !== null ? 'FROM_UNIXTIME(' . $reply_date . ')' : null, true], 206 | ]; 207 | 208 | $geneRes = patch_condition($select); 209 | 210 | $sql = "UPDATE apothegm 211 | SET $geneRes 212 | WHERE `id`=$id;"; 213 | 214 | $result = mysqli_query($sqllink, $sql); 215 | 216 | echo ($result); 217 | break; 218 | default: 219 | break; 220 | } 221 | -------------------------------------------------------------------------------- /public/api/checkAdmin.php: -------------------------------------------------------------------------------- 1 | p));; 17 | 18 | if ($password == ADMINPASSWORD) { 19 | echo json_encode(['validate' => true]); 20 | } else { 21 | echo json_encode(['validate' => false]); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/api/ipRequest.php: -------------------------------------------------------------------------------- 1 | getip()]); 27 | } 28 | -------------------------------------------------------------------------------- /public/api/login.php: -------------------------------------------------------------------------------- 1 | name)); 28 | @$pw = trim((string)($data->pw)); 29 | 30 | // user exist 31 | $usersql = 'SELECT `name` FROM `user` 32 | WHERE `name`="' . $name_email . '" AND `pw`="' . $pw . '" AND `is_deleted`=0 AND `is_banned`=0 33 | ;'; 34 | 35 | // email exist 36 | $emailsql = 'SELECT `email` FROM `user` 37 | WHERE `email`="' . $name_email . '" AND `pw`="' . $pw . '" AND `is_deleted`=0 AND `is_banned`=0 38 | ;'; 39 | 40 | $user_result = mysqli_query($sqllink, $usersql); 41 | 42 | $email_result = mysqli_query($sqllink, $emailsql); 43 | 44 | if (($user_result->num_rows > 0) || ($email_result->num_rows > 0)) { 45 | // exist 46 | @$token = md5(((string)time()) + $name); 47 | $_SESSION["token"] = $token; 48 | echo json_encode(["res" => "ok", "token" => $token]); 49 | } else { 50 | // not exist 51 | echo json_encode(["res" => "fail"]); 52 | } 53 | break; 54 | case 'DELETE': 55 | // 退出登录 56 | unset($_SESSION['token']); 57 | session_destroy(); 58 | break; 59 | default: 60 | break; 61 | } 62 | -------------------------------------------------------------------------------- /public/api/mail.php: -------------------------------------------------------------------------------- 1 | email)); 18 | 19 | $verify_code = mt_rand(100000, 999999); 20 | 21 | $_SESSION["verify_code"] = $verify_code; 22 | 23 | $res = send_verification_mail($email, $verify_code); 24 | 25 | if ($res) { 26 | echo json_encode(["res" => "ok"]); 27 | } else { 28 | echo json_encode(["res" => "fail"]); 29 | } 30 | 31 | break; 32 | default: 33 | break; 34 | } 35 | -------------------------------------------------------------------------------- /public/api/map.test.php: -------------------------------------------------------------------------------- 1 | base64_encode(openssl_encrypt($_GET['text'], "AES-256-CBC", $passphrase= AESKEY, $_GET['pad'],$iv= AESIV)), 26 | ]; 27 | 28 | echo json_encode($res); 29 | 30 | 31 | break; 32 | } 33 | -------------------------------------------------------------------------------- /public/api/mapReply.php: -------------------------------------------------------------------------------- 1 | pid)); 24 | @$content = trim((string)($data->content)); 25 | @$like = ($data->like); 26 | @$dislike = ($data->dislike); 27 | @$ip = trim((string)($data->ip)); 28 | 29 | $sql = 'INSERT 30 | INTO map_reply (`pid`, `content`, `like`, `dislike`, `ip`, `is_deleted`) 31 | VALUES ("' . anti_inj($pid) . '","' . cator_to_cn_censorship(anti_inj($content)) . '","' . $like . '","' . $dislike . '","' . anti_inj($ip) . '", "0"); 32 | '; 33 | 34 | $result = mysqli_query($sqllink, $sql); 35 | 36 | echo json_encode($result); 37 | break; 38 | case 'GET': 39 | @$id_ori = $_GET['id']; 40 | @$pid_ori = $_GET['pid']; 41 | /** IP */ 42 | @$ip_ori = $_GET['ip']; 43 | /** 个数, 不填为全部 */ 44 | @$count_ori = $_GET['count']; 45 | @$kword_ori = $_GET['kword']; 46 | 47 | $id = ''; 48 | $ip = ''; 49 | $pid = 0; 50 | $count = 0; 51 | $kword = ''; 52 | if (is_numeric($pid_ori)) { 53 | $pid = (int)$pid_ori; 54 | } 55 | if (is_numeric($count_ori)) { 56 | $count = (int)$count_ori; 57 | } 58 | if (is_numeric($id_ori)) { 59 | $id = (int)$id_ori; 60 | } 61 | 62 | if ($count == 0) { 63 | $count = '*'; 64 | } else { 65 | $count = 'TOP ' . $count; 66 | } 67 | if (isset($ip_ori)) { 68 | $ip = trim(anti_inj((string)$ip_ori)); 69 | } 70 | if (isset($kword_ori)) { 71 | $kword = trim(anti_inj((string)$kword_ori)); 72 | } 73 | 74 | $select = []; 75 | 76 | if ($id <= 0) { 77 | $select = [ 78 | 'AND', 79 | [ 80 | ['', ['pid', $pid]], 81 | [ 82 | 'OR', [ 83 | ['LIKE', ['content', $kword]] 84 | ] 85 | ], 86 | ['', ['ip', $ip]], 87 | ['', ['is_deleted', '0']] 88 | ] 89 | ]; 90 | } else { 91 | $select = ['', ['id', $id]]; 92 | } 93 | 94 | $geneRes = get_condition($select); 95 | if ($geneRes != '') { 96 | $geneRes = "WHERE $geneRes"; 97 | } 98 | 99 | $sql = "SELECT $count 100 | FROM map_reply 101 | $geneRes 102 | ORDER BY `update_date` DESC; 103 | "; 104 | 105 | $result = mysqli_query($sqllink, $sql); 106 | 107 | $res = []; 108 | 109 | if ($result->num_rows > 0) { 110 | $i = 0; 111 | while ($row = $result->fetch_assoc()) { 112 | array_push($res, [ 113 | 'id' => $row['id'], 114 | 'pid' => $row['pid'], 115 | 'content' => $row['content'], 116 | 'like' => (int)$row['like'], 117 | 'dislike' => (int)$row['dislike'], 118 | 'ip' => $row['ip'], 119 | 'is_deleted' => (bool)(int)$row['is_deleted'], 120 | 'create_date' => $row['create_date'], 121 | 'update_date' => $row['update_date'], 122 | ]); 123 | $i++; 124 | } 125 | } 126 | 127 | echo json_encode($res); 128 | 129 | break; 130 | case 'DELETE': 131 | @$id = trim((string)($data->id)); 132 | 133 | $sql = "UPDATE map_reply 134 | SET `is_deleted`=1 135 | WHERE `id`=$id;"; 136 | 137 | $result = mysqli_query($sqllink, $sql); 138 | 139 | echo ($result); 140 | 141 | break; 142 | case 'PATCH': 143 | @$id =property_exists($data, 'id') ? trim((string)($data->id)): null; 144 | @$pid =property_exists($data, 'pid') ? trim((string)($data->pid)): null; 145 | @$content = property_exists($data, 'content') ? trim((string)($data->name)): null; 146 | @$like =property_exists($data, 'like') ? (string)($data->like): null; 147 | @$dislike =property_exists($data, 'dislike') ? (string)($data->dislike): null; 148 | @$ip =property_exists($data, 'ip') ? trim((string)($data->ip)): null; 149 | @$is_deleted = property_exists($data, 'is_deleted') ? (string)($data->is_deleted): null; 150 | 151 | if ($is_deleted == 'false') $is_deleted = "0"; 152 | 153 | $select = [ 154 | ['pid', $pid], 155 | ['content', $content], 156 | ['like', $like, true, 'increment'], 157 | ['dislike', $dislike, true, 'increment'], 158 | ['ip', $ip], 159 | ['is_deleted', $is_deleted, true], 160 | ]; 161 | 162 | $geneRes = patch_condition($select); 163 | 164 | $sql = "UPDATE map_reply 165 | SET $geneRes 166 | WHERE `id`=$id;"; 167 | 168 | $result = mysqli_query($sqllink, $sql); 169 | 170 | echo ($result); 171 | break; 172 | default: 173 | break; 174 | } 175 | -------------------------------------------------------------------------------- /public/api/private/admin.example.php: -------------------------------------------------------------------------------- 1 | pid)); 24 | @$content = trim((string)($data->content)); 25 | @$like = ($data->like); 26 | @$dislike = ($data->dislike); 27 | @$ip = trim((string)($data->ip)); 28 | 29 | $sql = 'INSERT 30 | INTO apo_reply (`pid`, `content`, `like`, `dislike`, `ip`, `is_deleted`) 31 | VALUES ("' . anti_inj($pid) . '","' . cator_to_cn_censorship(anti_inj($content)) . '","' . $like . '","' . $dislike . '","' . anti_inj($ip) . '", "0"); 32 | '; 33 | 34 | $result = mysqli_query($sqllink, $sql); 35 | 36 | echo json_encode($result); 37 | break; 38 | case 'GET': 39 | @$id_ori = $_GET['id']; 40 | @$pid_ori = $_GET['pid']; 41 | /** IP */ 42 | @$ip_ori = $_GET['ip']; 43 | /** 个数, 不填为全部 */ 44 | @$count_ori = $_GET['count']; 45 | @$kword_ori = $_GET['kword']; 46 | 47 | $id = ''; 48 | $ip = ''; 49 | $pid = 0; 50 | $count = 0; 51 | $kword = ''; 52 | if (is_numeric($pid_ori)) { 53 | $pid = (int)$pid_ori; 54 | } 55 | if (is_numeric($count_ori)) { 56 | $count = (int)$count_ori; 57 | } 58 | if (is_numeric($id_ori)) { 59 | $id = (int)$id_ori; 60 | } 61 | 62 | if ($count == 0) { 63 | $count = '*'; 64 | } else { 65 | $count = 'TOP ' . $count; 66 | } 67 | if (isset($ip_ori)) { 68 | $ip = trim(anti_inj((string)$ip_ori)); 69 | } 70 | if (isset($kword_ori)) { 71 | $kword = trim(anti_inj((string)$kword_ori)); 72 | } 73 | 74 | $select = []; 75 | 76 | if ($id <= 0) { 77 | $select = [ 78 | 'AND', 79 | [ 80 | ['', ['pid', $pid]], 81 | [ 82 | 'OR', [ 83 | ['LIKE', ['content', $kword]] 84 | ] 85 | ], 86 | ['', ['ip', $ip]], 87 | ['', ['is_deleted', '0']] 88 | ] 89 | ]; 90 | } else { 91 | $select = ['', ['id', $id]]; 92 | } 93 | 94 | $geneRes = get_condition($select); 95 | if ($geneRes != '') { 96 | $geneRes = "WHERE $geneRes"; 97 | } 98 | 99 | $sql = "SELECT $count 100 | FROM apo_reply 101 | $geneRes 102 | ORDER BY `update_date` DESC; 103 | "; 104 | 105 | $result = mysqli_query($sqllink, $sql); 106 | 107 | $res = []; 108 | 109 | if ($result->num_rows > 0) { 110 | $i = 0; 111 | while ($row = $result->fetch_assoc()) { 112 | array_push($res, [ 113 | 'id' => $row['id'], 114 | 'pid' => $row['pid'], 115 | 'content' => $row['content'], 116 | 'like' => (int)$row['like'], 117 | 'dislike' => (int)$row['dislike'], 118 | 'ip' => $row['ip'], 119 | 'is_deleted' => (bool)(int)$row['is_deleted'], 120 | 'create_date' => $row['create_date'], 121 | 'update_date' => $row['update_date'], 122 | ]); 123 | $i++; 124 | } 125 | } 126 | 127 | echo json_encode($res); 128 | 129 | break; 130 | case 'DELETE': 131 | @$id = trim((string)($data->id)); 132 | 133 | $sql = "UPDATE apo_reply 134 | SET `is_deleted`=1 135 | WHERE `id`=$id;"; 136 | 137 | $result = mysqli_query($sqllink, $sql); 138 | 139 | echo ($result); 140 | 141 | break; 142 | case 'PATCH': 143 | @$id =property_exists($data, 'id') ? trim((string)($data->id)): null; 144 | @$pid =property_exists($data, 'pid') ? trim((string)($data->pid)): null; 145 | @$content = property_exists($data, 'content') ? trim((string)($data->name)): null; 146 | @$like =property_exists($data, 'like') ? (string)($data->like): null; 147 | @$dislike =property_exists($data, 'dislike') ? (string)($data->dislike): null; 148 | @$ip =property_exists($data, 'ip') ? trim((string)($data->ip)): null; 149 | @$is_deleted = property_exists($data, 'is_deleted') ? (string)($data->is_deleted): null; 150 | 151 | if ($is_deleted == 'false') $is_deleted = "0"; 152 | 153 | $select = [ 154 | ['pid', $pid], 155 | ['content', $content], 156 | ['like', $like, true, 'increment'], 157 | ['dislike', $dislike, true, 'increment'], 158 | ['ip', $ip], 159 | ['is_deleted', $is_deleted, true], 160 | ]; 161 | 162 | $geneRes = patch_condition($select); 163 | 164 | $sql = "UPDATE apo_reply 165 | SET $geneRes 166 | WHERE `id`=$id;"; 167 | 168 | $result = mysqli_query($sqllink, $sql); 169 | 170 | echo ($result); 171 | break; 172 | default: 173 | break; 174 | } 175 | -------------------------------------------------------------------------------- /public/api/searchUpload.php: -------------------------------------------------------------------------------- 1 | content)); 23 | @$ip = trim((string)($data->ip)); 24 | @$position = trim((string)($data->position)); 25 | 26 | $sql = 'INSERT 27 | INTO search (`content`, `ip`, `position`) 28 | VALUES ("' . anti_inj($content) . '","' . anti_inj($ip) . '","' . anti_inj($position) . '"); 29 | '; 30 | 31 | $result = mysqli_query($sqllink, $sql); 32 | 33 | echo json_encode($result); 34 | break; 35 | default: 36 | break; 37 | } 38 | -------------------------------------------------------------------------------- /public/api/sqlgenerator.php: -------------------------------------------------------------------------------- 1 | < >= <=: 28 | * ['>', [列名, 值]]会生成: 29 | * `列名` > 值 30 | * 31 | */ 32 | function get_condition($condition) 33 | { 34 | $res = ''; 35 | if (@$condition[0] == '') { //字符串 36 | if (@$condition[1][1] != '') { 37 | $res .= '`' . $condition[1][0] . '`' . '="' . $condition[1][1] . '"'; 38 | } 39 | } else if ($condition[0] === 'LIKE') { 40 | if ($condition[1][1] != '') { 41 | $res .= '`' . $condition[1][0] . '`' . ' LIKE "%' . $condition[1][1] . '%"'; 42 | } 43 | } else if ($condition[0] === 'AND' || $condition[0] === 'OR') { 44 | if (count($condition[1]) > 1) { 45 | $templist = []; 46 | for ($i = 0; $i < count($condition[1]); $i++) { 47 | $tres = get_condition($condition[1][$i]); 48 | if ($tres != "") { 49 | array_push($templist, $tres); 50 | } 51 | } 52 | 53 | 54 | $res .= '('; 55 | 56 | for ($i = 0; $i < count($templist); $i++) { 57 | $res .= $templist[$i]; 58 | if ($i < count($templist) - 1 && $templist[$i] !== '') { 59 | $res .= " $condition[0] "; 60 | } 61 | } 62 | 63 | $res .= ')'; 64 | if ($res === '()') { 65 | $res = ''; 66 | } 67 | } else { 68 | if (count($condition[1]) === 1) { 69 | $res .= @get_condition($condition[1][0]); 70 | } else { 71 | $res .= ''; 72 | } 73 | } 74 | } else { 75 | // > < >= <= 76 | if (@$condition[1][1] !== '') { 77 | $res .= '`' . $condition[1][0] . '`' . " $condition[0] " . $condition[1][1]; 78 | } 79 | } 80 | return $res; 81 | } 82 | 83 | /** 84 | * 获取PATCH内容,为空会自动忽略 85 | * @author wniko 86 | * 87 | * condition格式: 88 | * [ 89 | * [列名, 值, 是否不加引号?: bool, 递增递减?: 'increment'|'decrement'], 90 | * .... 91 | * ] 92 | */ 93 | function patch_condition($condition) 94 | { 95 | $geneRes = ''; 96 | for ($i = 0; $i < count($condition); $i++) { 97 | $item = $condition[$i]; 98 | if ($item[1] !== null) { 99 | // 引号 100 | if (count($item) >= 3 && $item[2]) { 101 | // 递增递减 102 | if (count($item) >= 4 && $item[3]) { 103 | switch ($item[3]) { 104 | case 'increment': 105 | $geneRes .= "`$item[0]` = `$item[0]` + 1,"; 106 | break; 107 | case 'decrement': 108 | $geneRes .= "`$item[0]` = `$item[0]` - 1,"; 109 | break; 110 | default: 111 | $geneRes .= "`$item[0]` = $item[1],"; 112 | break; 113 | } 114 | } else { 115 | $geneRes .= "`$item[0]` = $item[1],"; 116 | } 117 | } else { 118 | $geneRes .= "`$item[0]` = \"$item[1]\","; 119 | } 120 | } 121 | } 122 | 123 | if (substr($geneRes, -1) === ',') { 124 | $geneRes = substr($geneRes, 0, strlen($geneRes) - 1); 125 | } 126 | return $geneRes; 127 | } 128 | -------------------------------------------------------------------------------- /public/api/statistics.php: -------------------------------------------------------------------------------- 1 | content)); 23 | $res = [ 24 | 'markerCount' => 0, 25 | 'markerCountWithoutDeleted' => 0, 26 | 'mostSearched' => [], 27 | 'types' => [] 28 | ]; 29 | 30 | $sql = 'SELECT COUNT(*) AS "count" 31 | FROM `map` 32 | WHERE `is_deleted`=0; 33 | '; 34 | 35 | $result = mysqli_query($sqllink, $sql); 36 | 37 | if ($result->num_rows > 0) { 38 | $i = 0; 39 | while ($row = $result->fetch_assoc()) { 40 | $res['markerCountWithoutDeleted'] = $row['count']; 41 | $i++; 42 | } 43 | } 44 | 45 | 46 | $sql = 'SELECT COUNT(*) AS "count" 47 | FROM `map` 48 | '; 49 | 50 | $result = mysqli_query($sqllink, $sql); 51 | 52 | if ($result->num_rows > 0) { 53 | $i = 0; 54 | while ($row = $result->fetch_assoc()) { 55 | $res['markerCount'] = $row['count']; 56 | $i++; 57 | } 58 | } 59 | 60 | 61 | $sql = 'SELECT content, COUNT(*) as count from search where `position`="map" group by content order by count desc limit 10; 62 | '; 63 | 64 | $result = mysqli_query($sqllink, $sql); 65 | 66 | if ($result->num_rows > 0) { 67 | $i = 0; 68 | while ($row = $result->fetch_assoc()) { 69 | array_push($res['mostSearched'], [ 70 | 'word' => $row['content'], 71 | 'count' => $row['count'], 72 | ]); 73 | $i++; 74 | } 75 | } 76 | 77 | 78 | $sql = 'SELECT `type`, COUNT(*) as count from map where `is_deleted`=0 group by `type` order by count desc; 79 | '; 80 | 81 | $result = mysqli_query($sqllink, $sql); 82 | 83 | if ($result->num_rows > 0) { 84 | $i = 0; 85 | while ($row = $result->fetch_assoc()) { 86 | array_push($res['types'], [ 87 | 'word' => $row['type'], 88 | 'count' => $row['count'], 89 | ]); 90 | $i++; 91 | } 92 | } 93 | 94 | echo json_encode($res); 95 | break; 96 | default: 97 | break; 98 | } 99 | -------------------------------------------------------------------------------- /public/api/user.php: -------------------------------------------------------------------------------- 1 | name)); 28 | @$email = trim((string)($data->email)); 29 | @$verify_code = trim((string)($data->verify_code)); 30 | @$pw = trim((string)($data->pw)); 31 | 32 | if ($_SESSION["verify_code"] != '' && $verify_code == $_SESSION["verify_code"]) { 33 | 34 | unset($_SESSION['verify_code']); 35 | 36 | // user exist 37 | $usersql = 'SELECT `name` FROM `user` 38 | WHERE `name`="' . $name . '" AND `is_deleted`=0 39 | ;'; 40 | // email exist 41 | $emailsql = 'SELECT `email` FROM `user` 42 | WHERE `email`="' . $email . '" AND `is_deleted`=0 43 | ;'; 44 | 45 | $user_result = mysqli_query($sqllink, $usersql); 46 | 47 | $email_result = mysqli_query($sqllink, $emailsql); 48 | 49 | if (($user_result->num_rows > 0) || ($email_result->num_rows > 0)) { 50 | // exist 51 | echo json_encode(["res" => "exist"]); 52 | } else { 53 | // not exist 54 | $sql = 'INSERT 55 | INTO `user` (`name`, `pw`, `email`) 56 | VALUES ("' . $name . '","' . $pw . '","' . $email . '"); 57 | '; 58 | 59 | $result = mysqli_query($sqllink, $sql); 60 | if ($result == true) { 61 | echo json_encode(["res" => "ok"]); 62 | } else { 63 | echo json_encode(["res" => "unknown_error"]); 64 | } 65 | } 66 | } else { 67 | echo json_encode(["res" => "verification_error"]); 68 | } 69 | 70 | break; 71 | case 'PATCH': 72 | 73 | break; 74 | case 'DELETE': 75 | @$id = trim((string)($data->id)); 76 | 77 | $sql = "UPDATE user 78 | SET `is_deleted`=1 79 | WHERE `id`=$id;"; 80 | 81 | $result = mysqli_query($sqllink, $sql); 82 | 83 | if ($result == true) { 84 | echo json_encode(["res" => "ok"]); 85 | } else { 86 | echo json_encode(["res" => "unknown_error"]); 87 | } 88 | 89 | break; 90 | default: 91 | break; 92 | } 93 | -------------------------------------------------------------------------------- /public/api/utils.php: -------------------------------------------------------------------------------- 1 | setServer(EMAIL_HOST, EMAIL_USER, EMAIL_PASS, EMAIL_PORT, true); 51 | $mail->setFrom(EMAIL_MAIL); 52 | $mail->setReceiver($target); 53 | $mail->addAttachment(""); 54 | $mail->setMail( 55 | "老头环地图 邮箱验证码", 56 | '

验证码是:' . $verify_code . '

有效期:5分钟

' . date('Y-m-d H:i:s') 57 | ); 58 | return true; 59 | } catch (Exception $e) { 60 | return false; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/favicon.png -------------------------------------------------------------------------------- /public/global.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | position: relative; 4 | width: 100%; 5 | height: 100%; 6 | overflow: hidden; 7 | font-family: 'Times New Roman', Times, serif; 8 | } 9 | 10 | body { 11 | margin: 0; 12 | padding: 0; 13 | background-color: rgb(21, 22, 17); 14 | } 15 | 16 | button, 17 | input, 18 | select, 19 | textarea { 20 | border: solid 1px rgb(208, 200, 181); 21 | box-shadow: 0 0 3px 0 rgb(208, 200, 181); 22 | background-color: rgb(21, 22, 17); 23 | color: rgb(208, 200, 181); 24 | font-family: 'Times New Roman', Times, serif; 25 | transition: all 0.3s; 26 | } 27 | 28 | button, 29 | select { 30 | user-select: none; 31 | min-width: fit-content; 32 | } 33 | 34 | button.active { 35 | border: solid 2px rgb(208, 200, 181); 36 | box-shadow: 0 0 5px 1px rgb(208, 200, 181); 37 | } 38 | 39 | button.checked { 40 | border: solid 2px rgb(208, 200, 181); 41 | box-shadow: 0 0 5px 1px rgb(208, 200, 181); 42 | font-weight: bold; 43 | } 44 | 45 | button:disabled { 46 | border: rgb(172 162 138); 47 | color: rgb(116 113 106); 48 | } 49 | 50 | @media (any-hover: hover) { 51 | button:hover, 52 | select:hover { 53 | border: solid 1px rgb(208, 200, 181); 54 | box-shadow: 0 0 7px 1px rgb(208, 200, 181); 55 | } 56 | } 57 | 58 | button:active, 59 | select:active { 60 | border: solid 1px rgb(208, 200, 181); 61 | box-shadow: 0 0 3px 0 rgb(208, 200, 181); 62 | background-color: rgb(40, 41, 37); 63 | color: rgb(177, 168, 146); 64 | transition: all 0.1s; 65 | } 66 | 67 | html ::-webkit-scrollbar { 68 | width: 6px; 69 | height: 6px; 70 | margin-left: -6px; 71 | } 72 | 73 | html ::-webkit-scrollbar-thumb { 74 | border-radius: 4px; 75 | background: rgb(208, 200, 181); 76 | } 77 | 78 | html ::-webkit-scrollbar-track { 79 | border-radius: 4px; 80 | background: rgb(0, 0, 0, 0); 81 | margin: 0; 82 | } 83 | -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/icon.png -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 老头环地图 - Elden Ring Online Map 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 48 | 49 | 58 | 59 | 60 | 61 | 72 | 73 | 74 | 76 | 77 | 78 | 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "老头环地图", 3 | "short_name": "老头环地图", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#171715", 7 | "description": "艾尔登法环在线标注地图。", 8 | "scope": "./", 9 | "icons": [ 10 | { 11 | "src": "./icon.png", 12 | "sizes": "48x48", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "./icon.png", 17 | "sizes": "72x72", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "./icon.png", 22 | "sizes": "96x96", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "./icon.png", 27 | "sizes": "144x144", 28 | "type": "image/png" 29 | }, 30 | { 31 | "src": "./icon.png", 32 | "sizes": "168x168", 33 | "type": "image/png" 34 | }, 35 | { 36 | "src": "./icon.png", 37 | "sizes": "192x192", 38 | "type": "image/png" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /public/resource/icons/blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/blue.png -------------------------------------------------------------------------------- /public/resource/icons/boss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/boss.png -------------------------------------------------------------------------------- /public/resource/icons/collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/collect.png -------------------------------------------------------------------------------- /public/resource/icons/fireicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/fireicon.png -------------------------------------------------------------------------------- /public/resource/icons/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/green.png -------------------------------------------------------------------------------- /public/resource/icons/littleboss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/littleboss.png -------------------------------------------------------------------------------- /public/resource/icons/message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/message.png -------------------------------------------------------------------------------- /public/resource/icons/portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/portal.png -------------------------------------------------------------------------------- /public/resource/icons/purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/purple.png -------------------------------------------------------------------------------- /public/resource/icons/question.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/question.png -------------------------------------------------------------------------------- /public/resource/icons/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/red.png -------------------------------------------------------------------------------- /public/resource/icons/warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/warning.png -------------------------------------------------------------------------------- /public/resource/icons/white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/white.png -------------------------------------------------------------------------------- /public/resource/icons/yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/icons/yellow.png -------------------------------------------------------------------------------- /public/resource/images/3dmappreview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/3dmappreview.png -------------------------------------------------------------------------------- /public/resource/images/about.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/about.png -------------------------------------------------------------------------------- /public/resource/images/apothegm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/apothegm.png -------------------------------------------------------------------------------- /public/resource/images/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/app.png -------------------------------------------------------------------------------- /public/resource/images/appicon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/appicon.webp -------------------------------------------------------------------------------- /public/resource/images/dlc1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/dlc1.png -------------------------------------------------------------------------------- /public/resource/images/dlc1_narrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/dlc1_narrow.png -------------------------------------------------------------------------------- /public/resource/images/dodo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/dodo.png -------------------------------------------------------------------------------- /public/resource/images/fire.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/fire.png -------------------------------------------------------------------------------- /public/resource/images/general.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/general.png -------------------------------------------------------------------------------- /public/resource/images/hoi4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/hoi4.jpg -------------------------------------------------------------------------------- /public/resource/images/kotone1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/kotone1.gif -------------------------------------------------------------------------------- /public/resource/images/map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/map.png -------------------------------------------------------------------------------- /public/resource/images/modalbg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/modalbg.png -------------------------------------------------------------------------------- /public/resource/images/qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elpwc/EldenRingOnlineMap/69bc9173a533bd9ecc4b6c56297373d67b4a6f08/public/resource/images/qrcode.jpg -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | // PWA 缓存 2 | 3 | const CACHE_NAME = `elpwc-eldenring-map-cache-v1`; 4 | 5 | // Use the install event to pre-cache all initial resources. 6 | self.addEventListener('install', event => { 7 | event.waitUntil( 8 | (async () => { 9 | const cache = await caches.open(CACHE_NAME); 10 | cache.addAll(['./favicon.png', './index.html', './global.css', './build/main.js', './build/bundle.css', './build/zh-TW-3fb73925.js', './build/images/*']); 11 | })() 12 | ); 13 | }); 14 | 15 | self.addEventListener('fetch', event => { 16 | event.respondWith( 17 | (async () => { 18 | const cache = await caches.open(CACHE_NAME); 19 | 20 | try { 21 | // Try to fetch the resource from the network. 22 | const fetchResponse = await fetch(event.request); 23 | 24 | // Save the resource in the cache. 25 | cache.put(event.request, fetchResponse.clone()); 26 | 27 | // And return it. 28 | return fetchResponse; 29 | } catch (e) { 30 | // Fetching didn't work get the resource from the cache. 31 | const cachedResponse = await cache.match(event.request); 32 | 33 | // And return it. 34 | return cachedResponse; 35 | } 36 | })() 37 | ); 38 | }); 39 | -------------------------------------------------------------------------------- /rollup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'rollup'; 2 | import svelte from 'rollup-plugin-svelte'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import livereload from 'rollup-plugin-livereload'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | import { svelteSVG } from "rollup-plugin-svelte-svg"; 8 | import sveltePreprocess from 'svelte-preprocess'; 9 | import css from 'rollup-plugin-css-only'; 10 | import json from '@rollup/plugin-json'; 11 | import { babel } from '@rollup/plugin-babel'; 12 | import replace from '@rollup/plugin-replace'; 13 | import copy from 'rollup-plugin-copy'; 14 | import del from 'rollup-plugin-delete' 15 | 16 | const production = !process.env.ROLLUP_WATCH; 17 | 18 | function serve() { 19 | let server; 20 | 21 | function toExit() { 22 | if (server) server.kill(0); 23 | } 24 | 25 | return { 26 | writeBundle() { 27 | if (server) return; 28 | server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { 29 | stdio: ['ignore', 'inherit', 'inherit'], 30 | shell: true, 31 | }); 32 | 33 | process.on('SIGTERM', toExit); 34 | process.on('exit', toExit); 35 | }, 36 | }; 37 | } 38 | const extensions = ['.js', '.ts', '.svelte'] 39 | 40 | export default defineConfig({ 41 | input: 'src/main.ts', 42 | output: { 43 | sourcemap: true, 44 | format: 'es', 45 | name: 'app', 46 | dir: 'public/build', 47 | }, 48 | // Be careful about the plugins order!!! 49 | plugins: [ 50 | del({ targets: 'public/build/*.js*' }), 51 | json(), 52 | svelteSVG({ 53 | // optional SVGO options 54 | // pass empty object to enable defaults 55 | svgo: {} 56 | }), 57 | svelte({ 58 | preprocess: sveltePreprocess({ sourceMap: !production }), 59 | compilerOptions: { 60 | // enable run-time checks when not in production 61 | dev: !production, 62 | }, 63 | }), 64 | // If you have external dependencies installed from 65 | // npm, you'll most likely need these plugins. In 66 | // some cases you'll need additional configuration - 67 | // consult the documentation for details: 68 | // https://github.com/rollup/plugins/tree/master/packages/commonjs 69 | resolve({ 70 | extensions, 71 | preferBuiltins: false, 72 | browser: true, 73 | dedupe: ['svelte'], 74 | }), 75 | replace({ 76 | 'process.env.NODE_ENV': JSON.stringify(production ? 'production' : 'development'), 77 | }), 78 | commonjs(), 79 | 80 | // compile to good old IE11 compatible ES5 81 | babel({ 82 | extensions, 83 | babelHelpers: 'runtime', 84 | exclude: ['node_modules/**'], 85 | }), 86 | 87 | // leaflet.css 里含有路径来引用图标,将其静态资源拷贝到 bundle.css 的生成路径下 88 | copy({ 89 | targets: [{ src: 'node_modules/leaflet/dist/images', dest: 'public/build/' }], 90 | }), 91 | // we'll extract any component CSS out into 92 | // a separate file - better for performance 93 | css({ output: 'bundle.css' }), 94 | 95 | // In dev mode, call `npm run start` once 96 | // the bundle has been generated 97 | !production && serve(), 98 | 99 | // Watch the `public` directory and refresh the 100 | // browser on changes when not in production 101 | !production && livereload('public'), 102 | 103 | // If we're building for production (npm run build 104 | // instead of npm run dev), minify 105 | production && terser(), 106 | ], 107 | watch: { 108 | clearScreen: false, 109 | }, 110 | }); 111 | -------------------------------------------------------------------------------- /src/@types/leaflet-canvas-markers-with-title/index.d.ts: -------------------------------------------------------------------------------- 1 | import 'leaflet'; 2 | 3 | export interface MarkerTitleStyle { 4 | font?: string; 5 | color?: string; 6 | borderColor?: string; 7 | borderWidth?: number; 8 | } 9 | 10 | export interface MarkerTitleOpt { 11 | normal: MarkerTitleStyle; 12 | hover?: MarkerTitleStyle; 13 | active?: MarkerTitleStyle; 14 | } 15 | 16 | declare module 'leaflet' { 17 | export class CanvasIconLayer extends Layer { 18 | addTo(map: Map | LayerGroup): this; 19 | addMarker(marker: Marker, title: string, titleOpt: MarkerTitleOpt, additional?: any): void; 20 | addMarkers(markers: Array, title: string, titleOpt: MarkerTitleOpt, additional?: any): void; 21 | getBounds(): LatLngBounds; 22 | redraw(): void; 23 | clear(): void; 24 | clearLayers() : void; 25 | removeMarker(marker: Marker): void; 26 | addOnClickListener: (eventHandler: (listener: any, ret: any) => void) => void; 27 | addOnHoverListener: (eventHandler: (listener: any, ret: any) => void) => void; 28 | } 29 | function canvasIconLayer(): CanvasIconLayer; 30 | } 31 | -------------------------------------------------------------------------------- /src/App.svelte: -------------------------------------------------------------------------------- 1 | 4 | 41 | 42 | {#if $isLoading} 43 | Loading... 44 | {:else} 45 |
46 | 47 | 52 | 53 | 54 |
55 | 56 |
57 |
58 | 59 | 60 | { 63 | setCookie('version', config.default.currentVer); 64 | updateVisibility = false; 65 | cooperationModalVisibility = true; 66 | }} 67 | /> 68 | 69 | 70 | {/if} 71 | 72 | 87 | -------------------------------------------------------------------------------- /src/Read-me-before-dev.txt: -------------------------------------------------------------------------------- 1 | 调试请使用 npm run dev 2 | Use `npm run dev` while dev 3 | 4 | 不要使用 npm start 5 | Don't use `npm start` 6 | -------------------------------------------------------------------------------- /src/assets/icons/icon-add-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/icon-collect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-comment.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/icon-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-down-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-download_on_the_App_Store_Badge_CNSC_RGB_wht_092917.svg: -------------------------------------------------------------------------------- 1 | 2 | Download_on_the_App_Store_Badge_CNSC_RGB_wht_092917 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/assets/icons/icon-edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/icon-left-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-pin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-quit-mark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-remark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/icon-right-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-send-myself.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-subscript-link-10.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-subscript-link-7.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/icon-up-arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/icon-warning.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/CooperationModal.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 | { 17 | cooperationModalVisibility = false; 18 | onOKButtonClick(); 19 | }} 20 | okButtonText="OK(只会更新後显示一次)" 21 | > 22 |
23 |
24 |
25 | icon 26 | 27 |

法环地图 - 狭间地冒险指南

28 |
29 | 30 |

非常出色的iPhone/iPad端法环离线地图,标记、成就查看等..该有的功能应有尽有

31 |

已经和本站共享了部分数据,使用狭间地冒险指南也可以轻鬆看到这里的个性化标注了~

32 |
33 | qrcode for app 34 | 35 |
36 | 37 |
38 |

点击跳转到App Store页面

39 | 40 | 41 | 42 | 43 |
44 |
45 |
46 | 47 |
48 |
49 | icon 50 | 51 |

艾尔登法环超级群 - DoDo社区

52 |
53 |

为艾尔登法环玩家准备的交流地,不管是攻略,还是道具图鉴都可以轻松找到~

54 | 点此前往~ 55 |
56 |
57 | 58 | 59 | 84 | -------------------------------------------------------------------------------- /src/components/MapViewComponents/DirectionControl.svelte: -------------------------------------------------------------------------------- 1 | 4 | 13 | 14 |
15 |
16 | 24 |
25 | 26 |
27 | 35 | 43 |
44 | 45 |
46 | 54 |
55 |
56 | 57 | 101 | -------------------------------------------------------------------------------- /src/components/MapViewComponents/RightMenu.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |

{title}

17 | 18 | 25 | 26 | 33 | 34 | 41 |
42 | 43 | 74 | -------------------------------------------------------------------------------- /src/components/MenuItem.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 14 | {#if currentPath === path} 15 |
16 | {/if} 17 | {imgAlt} 18 | {text} 19 | 20 | 21 | 52 | -------------------------------------------------------------------------------- /src/components/Modal.svelte: -------------------------------------------------------------------------------- 1 | 5 | 28 | 29 | {#if visible} 30 |
31 | 32 |
33 | 34 | 35 | 60 |
61 | {/if} 62 | 63 | 106 | -------------------------------------------------------------------------------- /src/components/RouteViewer/RouteViewer.svelte: -------------------------------------------------------------------------------- 1 | 87 | 88 |
89 |
90 |

只是个预览,没有功能( )

91 |

数据皆为边想边输入的测试数据,不保证正确性orz

92 |

手机端看不到内容的话请往右边划→

93 |

可能会有连线重叠了的情况

94 | 95 | 96 | {#each resultLinks as link} 97 | 106 | 120 | {/each} 121 | 122 | 123 | 124 | {#each resultBoxes as box} 125 |
{ 129 | onBoxClick(box); 130 | }} 131 | > 132 |

{box.box.name}

133 |
134 | {/each} 135 | 136 | 137 | {#if showEditPanel} 138 |
139 | 146 | 153 | 160 | 167 |
168 | {/if} 169 |
170 |
171 | 172 | { 182 | addBoxModalVisibility = false; 183 | }} 184 | > 185 |
186 |
187 |

上溯节点

188 | 189 |
190 |
191 | 192 |
193 | 194 |
195 | 196 |
197 | 198 |
199 |

下游节点

200 | 201 |
202 |
203 | 204 | 205 | 206 |
207 |
208 |
209 | 210 | { 221 | boxInfoModalVisibility = false; 222 | }} 223 | > 224 |
rua
225 |
226 | 227 | 319 | -------------------------------------------------------------------------------- /src/components/RouteViewer/box.css: -------------------------------------------------------------------------------- 1 | .box-type0 { 2 | } 3 | 4 | /* Start */ 5 | .box-type1 { 6 | font-family: Arial; 7 | font-size: 20px; 8 | border-color: rgb(230, 229, 131) !important; 9 | color: rgb(230, 229, 131) !important; 10 | } 11 | 12 | /* End */ 13 | .box-type2 { 14 | font-family: Arial; 15 | font-size: 20px; 16 | border-color: rgb(230, 229, 131) !important; 17 | color: rgb(230, 229, 131) !important; 18 | } 19 | 20 | /* Boss */ 21 | .box-type3 { 22 | font-family: Arial; 23 | font-size: 15px; 24 | border-color: rgb(243, 45, 45) !important; 25 | } 26 | 27 | /* Position */ 28 | .box-type4 { 29 | font-family: Arial; 30 | font-size: 15px; 31 | border-color: rgb(121, 236, 245) !important; 32 | } 33 | 34 | /* NPC */ 35 | .box-type5 { 36 | font-family: Arial; 37 | font-size: 10px; 38 | border-color: rgb(116, 255, 98) !important; 39 | } 40 | 41 | /* Action */ 42 | .box-type6 { 43 | font-family: Arial; 44 | font-size: 10px; 45 | } 46 | -------------------------------------------------------------------------------- /src/components/RouteViewer/data.ts: -------------------------------------------------------------------------------- 1 | import { type Box, BoxTypes } from './types'; 2 | 3 | export const boxes: Box[] = [ 4 | { id: 0, name: '开始', type: BoxTypes.Start, pids: [] }, 5 | { id: 7, name: '史东薇尔城', type: BoxTypes.Position, pids: [{ id: 1, type: 0 }] }, 6 | { id: 1, name: '噩兆', type: BoxTypes.Boss, pids: [{ id: 56, type: 0 }] }, 7 | { id: 2, name: '接肢', type: BoxTypes.Boss, pids: [{ id: 7, type: 0 }] }, 8 | { 9 | id: 3, 10 | name: '湖区', 11 | type: BoxTypes.Position, 12 | pids: [ 13 | { id: 2, type: 0 }, 14 | { id: 56, type: 0 }, 15 | ], 16 | }, 17 | { id: 4, name: '满月女王', type: BoxTypes.Boss, pids: [{ id: 13, type: 0 }] }, 18 | { id: 8, name: '王城', type: BoxTypes.Position, pids: [{ id: 48, type: 0 }] }, 19 | { id: 5, name: '初始之王', type: BoxTypes.Boss, pids: [{ id: 8, type: 0 }] }, 20 | { id: 9, name: '菈妮', type: BoxTypes.NPC, pids: [{ id: 75, type: 0 }] }, 21 | { id: 10, name: '狮子混种', type: BoxTypes.Boss, pids: [{ id: 59, type: 0 }] }, 22 | { 23 | id: 11, 24 | name: '拉塔恩', 25 | type: BoxTypes.Boss, 26 | pids: [ 27 | { id: 58, type: 0 }, 28 | { id: 12, type: 0 }, 29 | ], 30 | }, 31 | { id: 12, name: '壶哥', type: BoxTypes.NPC, pids: [{ id: 56, type: 0 }] }, 32 | { id: 13, name: '红狼', type: BoxTypes.Boss, pids: [{ id: 3, type: 0 }] }, 33 | { id: 14, name: '恶兆王', type: BoxTypes.Boss, pids: [{ id: 5, type: 0 }] }, 34 | { id: 15, name: '泪滴哥', type: BoxTypes.Boss, pids: [{ id: 54, type: 0 }] }, 35 | { id: 16, name: '英雄石像鬼', type: BoxTypes.Boss, pids: [{ id: 15, type: 0 }] }, 36 | { 37 | id: 17, 38 | name: '死龙', 39 | type: BoxTypes.Boss, 40 | pids: [ 41 | { id: 18, type: 0 }, 42 | { id: 63, type: 0 }, 43 | ], 44 | }, 45 | { id: 18, name: '死眠', type: BoxTypes.NPC, pids: [{ id: 60, type: 0 }] }, 46 | { id: 19, name: '前往巨人山顶', type: BoxTypes.Position, pids: [{ id: 14, type: 0 }] }, 47 | { id: 20, name: '火焰巨人', type: BoxTypes.Boss, pids: [{ id: 19, type: 0 }] }, 48 | { id: 21, name: '烧树', type: BoxTypes.Action, pids: [{ id: 20, type: 0 }] }, 49 | { id: 22, name: '前往天空城', type: BoxTypes.Position, pids: [{ id: 21, type: 0 }] }, 50 | { id: 23, name: '“黑剑”玛利喀斯', type: BoxTypes.Boss, pids: [{ id: 79, type: 0 }] }, 51 | { id: 24, name: '“龙王”普拉顿桑克斯', type: BoxTypes.Boss, pids: [{ id: 79, type: 0 }] }, 52 | { id: 25, name: '前往灰城', type: BoxTypes.Position, pids: [{ id: 23, type: 0 }] }, 53 | { id: 26, name: '百智爵士', type: BoxTypes.Boss, pids: [{ id: 25, type: 0 }] }, 54 | { id: 27, name: '葛德温', type: BoxTypes.Boss, pids: [{ id: 26, type: 0 }] }, 55 | { id: 28, name: '艾尔登之兽', type: BoxTypes.Boss, pids: [{ id: 27, type: 0 }] }, 56 | { id: 29, name: '衰颓时代', type: BoxTypes.End, pids: [{ id: 35, type: 0 }] }, 57 | { 58 | id: 30, 59 | name: '潜藏者时代', 60 | type: BoxTypes.End, 61 | pids: [ 62 | { id: 39, type: 0 }, 63 | { id: 35, type: 0 }, 64 | ], 65 | }, 66 | { 67 | id: 31, 68 | name: '绝望时代', 69 | type: BoxTypes.End, 70 | pids: [ 71 | { id: 40, type: 0 }, 72 | { id: 35, type: 0 }, 73 | ], 74 | }, 75 | { 76 | id: 32, 77 | name: '律法时代', 78 | type: BoxTypes.End, 79 | pids: [ 80 | { id: 41, type: 0 }, 81 | { id: 35, type: 0 }, 82 | ], 83 | }, 84 | { 85 | id: 33, 86 | name: '群星时代', 87 | type: BoxTypes.End, 88 | pids: [{ id: 36, type: 0 }], 89 | }, 90 | { 91 | id: 34, 92 | name: '癫火之王', 93 | type: BoxTypes.End, 94 | pids: [ 95 | { id: 37, type: 0 }, 96 | { id: 35, type: 0 }, 97 | ], 98 | }, 99 | { id: 35, name: '触碰玛莉卡', type: BoxTypes.Action, pids: [{ id: 28, type: 0 },{ id: 80, type: 0 }] }, 100 | { 101 | id: 36, 102 | name: '触碰菈妮的符文', 103 | type: BoxTypes.Action, 104 | pids: [ 105 | { id: 67, type: 0 }, 106 | { id: 28, type: 0 }, 107 | ], 108 | }, 109 | { 110 | id: 37, 111 | name: '拥抱癫火', 112 | type: BoxTypes.Action, 113 | pids: [ 114 | { id: 38, type: 0 }, 115 | { id: 78, type: 0 }, 116 | ], 117 | }, 118 | { id: 38, name: '三指女巫海妲', type: BoxTypes.NPC, pids: [{ id: 3, type: 0 }] }, 119 | { id: 39, name: '死王子修复卢恩', type: BoxTypes.NPC, pids: [{ id: 17, type: 0 }] }, 120 | { id: 40, name: '食粪者', type: BoxTypes.NPC, pids: [{ id: 77, type: 0 }] }, 121 | { id: 41, name: '金面具', type: BoxTypes.NPC, pids: [{ id: 8, type: 0 }] }, 122 | { id: 42, name: '化圣雪原', type: BoxTypes.Position, pids: [{ id: 14, type: 0 }] }, 123 | { id: 43, name: '仪典镇', type: BoxTypes.Position, pids: [{ id: 42, type: 0 }] }, 124 | { id: 44, name: '圣树', type: BoxTypes.Position, pids: [{ id: 43, type: 0 }] }, 125 | { id: 45, name: '“圣树骑士”罗蕾塔', type: BoxTypes.Boss, pids: [{ id: 44, type: 0 }] }, 126 | { id: 46, name: '玛莲妮亚', type: BoxTypes.Boss, pids: [{ id: 45, type: 0 }] }, 127 | { id: 47, name: '老将尼奥', type: BoxTypes.Boss, pids: [{ id: 19, type: 0 }] }, 128 | { id: 48, name: '亚坦高原', type: BoxTypes.Position, pids: [{ id: 4, type: 0 }] }, 129 | { id: 49, name: '日荫城', type: BoxTypes.Position, pids: [{ id: 48, type: 0 }] }, 130 | { id: 50, name: '火山官邸', type: BoxTypes.Position, pids: [{ id: 48, type: 0 }] }, 131 | { id: 51, name: '神皮贵族', type: BoxTypes.Boss, pids: [{ id: 50, type: 0 }] }, 132 | { id: 52, name: '拉卡德', type: BoxTypes.Boss, pids: [{ id: 51, type: 0 }] }, 133 | { id: 53, name: '“铁棘”艾隆梅尔', type: BoxTypes.Boss, pids: [{ id: 49, type: 0 }] }, 134 | { 135 | id: 54, 136 | name: '永恒之城诺克隆恩', 137 | type: BoxTypes.Position, 138 | pids: [ 139 | { id: 57, type: 0 }, 140 | { id: 11, type: 0 }, 141 | ], 142 | }, 143 | { 144 | id: 55, 145 | name: '希芙拉河', 146 | type: BoxTypes.Position, 147 | pids: [ 148 | { id: 57, type: 0 }, 149 | { id: 54, type: 0 }, 150 | ], 151 | }, 152 | { id: 56, name: '宁姆格福', type: BoxTypes.Position, pids: [{ id: 0, type: 0 }] }, 153 | { id: 57, name: '雾林', type: BoxTypes.Position, pids: [{ id: 56, type: 0 }] }, 154 | { id: 58, name: '盖利德', type: BoxTypes.Position, pids: [{ id: 0, type: 0 }] }, 155 | { id: 59, name: '啜泣半岛', type: BoxTypes.Position, pids: [{ id: 56, type: 0 }] }, 156 | { id: 60, name: '圆桌', type: BoxTypes.Position, pids: [{ id: 0, type: 0 }] }, 157 | { id: 61, name: '祖灵', type: BoxTypes.Boss, pids: [{ id: 55, type: 0 }] }, 158 | { id: 62, name: '祖灵之王', type: BoxTypes.Boss, pids: [{ id: 15, type: 0 }] }, 159 | { 160 | id: 63, 161 | name: '深根', 162 | type: BoxTypes.Position, 163 | pids: [ 164 | { id: 78, type: 0 }, 165 | { id: 16, type: 0 }, 166 | ], 167 | }, 168 | { 169 | id: 64, 170 | name: '安瑟尔河(下)', 171 | type: BoxTypes.Position, 172 | pids: [ 173 | { id: 3, type: 0 }, 174 | { id: 66, type: 0 }, 175 | ], 176 | }, 177 | { 178 | id: 66, 179 | name: '安瑟尔河(上)', 180 | type: BoxTypes.Position, 181 | pids: [ 182 | { id: 9, type: 0 }, 183 | { id: 63, type: 0 }, 184 | ], 185 | }, 186 | { id: 65, name: '腐败湖', type: BoxTypes.Position, pids: [{ id: 66, type: 0 }] }, 187 | { 188 | id: 67, 189 | name: '和菈妮结婚', 190 | type: BoxTypes.Action, 191 | pids: [{ id: 70, type: 0 }], 192 | }, 193 | { id: 68, name: '诺克史黛拉的龙人士兵', type: BoxTypes.Boss, pids: [{ id: 64, type: 0 }] }, 194 | { id: 69, name: '黑暗弃子艾斯提', type: BoxTypes.Boss, pids: [{ id: 65, type: 0 }] }, 195 | { id: 70, name: '月光高原', type: BoxTypes.Position, pids: [{ id: 69, type: 0 }] }, 196 | { id: 71, name: '鲜血王朝', type: BoxTypes.Position, pids: [{ id: 42, type: 0 }] }, 197 | { id: 72, name: '“鲜血君主”蒙格', type: BoxTypes.Boss, pids: [{ id: 71, type: 0 }] }, 198 | { id: 73, name: '卡利亚城寨', type: BoxTypes.Position, pids: [{ id: 3, type: 0 }] }, 199 | { id: 74, name: '禁卫骑士罗蕾塔', type: BoxTypes.Boss, pids: [{ id: 73, type: 0 }] }, 200 | { id: 75, name: '三娣妹塔', type: BoxTypes.Position, pids: [{ id: 74, type: 0 }] }, 201 | { id: 76, name: '恶兆之子蒙格', type: BoxTypes.Boss, pids: [{ id: 77, type: 0 }] }, 202 | { id: 77, name: '王城下水道', type: BoxTypes.Position, pids: [{ id: 8, type: 0 }] }, 203 | { id: 78, name: '癫火封印', type: BoxTypes.Position, pids: [{ id: 76, type: 0 }] }, 204 | { id: 79, name: '神皮双人组', type: BoxTypes.Boss, pids: [{ id: 22, type: 0 }] }, 205 | { 206 | id: 80, 207 | name: '净化癫火', 208 | type: BoxTypes.Action, 209 | pids: [ 210 | { id: 24, type: 0 }, 211 | { id: 81, type: 0 }, 212 | { id: 37, type: 0 }, 213 | ], 214 | }, 215 | { id: 81, name: '取得金针', type: BoxTypes.Action, pids: [{ id: 46, type: 0 }] }, 216 | ]; 217 | -------------------------------------------------------------------------------- /src/components/RouteViewer/drawer.ts: -------------------------------------------------------------------------------- 1 | import type { Box } from './types'; 2 | import { dagStratify, sugiyama, DagNode, zherebko, grid } from 'd3-dag'; 3 | 4 | export default class Drawer { 5 | /** 6 | * 将自定义DAG格式转化为D3-DAG格式 7 | * @param boxes 8 | * @returns 9 | */ 10 | public static getTree = (boxes: Box[]) => { 11 | return boxes.map(box => { 12 | return { 13 | id: String(box.id), 14 | parentIds: box.pids.map(pid => { 15 | return String(pid.id); 16 | }), 17 | }; 18 | }); 19 | }; 20 | 21 | /** 22 | * D3-dag处理getTree的结果 23 | * @param data 24 | * @returns 返回结果和结果的长宽 25 | */ 26 | public static d3DagStratify = (data): { result; size: { width: number; height: number } } => { 27 | const stratify = dagStratify(); 28 | const dag: DagNode = stratify(data) as DagNode; 29 | const layout = sugiyama(); 30 | const sugiyamainfo = layout(dag); 31 | 32 | return { result: dag, size: sugiyamainfo }; 33 | }; 34 | 35 | /** 36 | * 画!(一个节点和他的子节点 37 | * @param node 节点 38 | * @param boxes 所有box 39 | * @param onUpdate 返回的回调 40 | * @param scaleX X方向拉伸 41 | * @param scaleY Y方向拉伸 42 | * @description 43 | * node: { 44 | * cachedChildrenCounts, 45 | * data: {id: string, parentIds: string[]}, 46 | * dataChildren, 47 | * value, 48 | * x: number, 49 | * y: number 50 | * } 51 | */ 52 | public static draw = ( 53 | node, 54 | boxes: Box[], 55 | onUpdate: (box: { top: number; left: number; box: Box }, links: { from: number; to: number; points: { x: number; y: number }[] }[]) => void, 56 | scaleX: number = 50, 57 | scaleY: number = 50 58 | ) => { 59 | // 获取box实例 60 | const instance = boxes.filter(f => { 61 | return f.id === Number(node.data.id); 62 | })?.[0]; 63 | 64 | const links: { from: number; to: number; points: { x: number; y: number }[] }[] = []; 65 | 66 | // 画子节点的线 67 | /* 68 | child: { 69 | child: 70 | data: 71 | points: {x,y}[], 72 | reserved: boolean 73 | } 74 | */ 75 | node?.dataChildren.forEach(child => { 76 | links.push({ 77 | from: instance.id, 78 | to: Number(child?.child?.data.id), 79 | points: child?.points.map(p => { 80 | return { x: p.x * scaleX, y: p.y * scaleY }; 81 | }), 82 | }); 83 | 84 | // 递归到子节点 85 | this.draw(child?.child, boxes, onUpdate, scaleX, scaleY); 86 | }); 87 | 88 | // 返回计算好的DOM属性 89 | onUpdate({ top: node.y * scaleY, left: node.x * scaleX, box: instance }, links); 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /src/components/RouteViewer/types.ts: -------------------------------------------------------------------------------- 1 | export type BoxLink = { 2 | id: number; 3 | type: number; 4 | }; 5 | 6 | export enum BoxTypes { 7 | Default = 0, 8 | Start = 1, 9 | End = 2, 10 | Boss = 3, 11 | Position = 4, 12 | NPC = 5, 13 | Action = 6, 14 | } 15 | 16 | export type Box = { 17 | id: number; 18 | name: string; 19 | pids: BoxLink[]; 20 | type: BoxTypes; 21 | isAchievement?: boolean; 22 | isMain?: boolean; 23 | }; 24 | 25 | export type Point = { 26 | x: number; 27 | y: number; 28 | }; 29 | 30 | -------------------------------------------------------------------------------- /src/components/button/ExportButton.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /src/components/button/ImportButton.svelte: -------------------------------------------------------------------------------- 1 | 60 | 61 | 62 | 70 | 71 | 78 | -------------------------------------------------------------------------------- /src/components/button/LangButton.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 21 | 22 | 29 | -------------------------------------------------------------------------------- /src/components/canvasIcons.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义地图图标 3 | * @description 导出的是生成图标数据的函数,用法:`L.divIcon(iconname(size, fontcolor)(title))` 4 | */ 5 | export const MapCanvasIcon = { 6 | default: 7 | (size: number = 10, fontcolor: string = 'white') => 8 | (title?: string, fontSize: string = '0.8em') => { 9 | return { 10 | iconUrl: './resource/icons/boss.png', 11 | iconSize: [size, size], 12 | iconAnchor: [size / 2, size / 2], 13 | }; 14 | }, 15 | cifu: 16 | (size: number = 20, fontcolor: string = 'white') => 17 | (title?: string, fontSize: string = '0.8em') => { 18 | return { 19 | iconUrl: './resource/icons/boss.png', 20 | iconSize: [size, size], 21 | iconAnchor: [size / 2, size / 2], 22 | }; 23 | }, 24 | boss: 25 | (size: number = 30, fontcolor: string = 'yellow') => 26 | (title?: string, fontSize: string = '0.8em') => { 27 | return { 28 | iconUrl: './resource/icons/boss.png', 29 | iconSize: [size, size], 30 | iconAnchor: [size / 2, size / 2], 31 | }; 32 | }, 33 | littleboss: 34 | (size: number = 28, fontcolor: string = 'yellow') => 35 | (title?: string, fontSize: string = '0.8em') => { 36 | return { 37 | iconUrl: './resource/icons/boss.png', 38 | iconSize: [size, size], 39 | iconAnchor: [size / 2, size / 2], 40 | }; 41 | }, 42 | portal: 43 | (size: number = 24, fontcolor: string = 'white') => 44 | (title?: string, fontSize: string = '0.8em') => { 45 | return { 46 | iconUrl: './resource/icons/boss.png', 47 | iconSize: [size, size], 48 | iconAnchor: [size / 2, size / 2], 49 | }; 50 | }, 51 | message: 52 | (size: number = 20, fontcolor: string = 'white') => 53 | (title?: string, fontSize: string = '0.8em') => { 54 | return { 55 | iconUrl: './resource/icons/boss.png', 56 | iconSize: [size, size], 57 | iconAnchor: [size / 2, size / 2], 58 | }; 59 | }, 60 | warning: 61 | (size: number = 15, fontcolor: string = 'white') => 62 | (title?: string, fontSize: string = '0.8em') => { 63 | return { 64 | iconUrl: './resource/icons/boss.png', 65 | iconSize: [size, size], 66 | iconAnchor: [size / 2, size / 2], 67 | }; 68 | }, 69 | question: 70 | (size: number = 15, fontcolor: string = 'white') => 71 | (title?: string, fontSize: string = '0.8em') => { 72 | return { 73 | iconUrl: './resource/icons/boss.png', 74 | iconSize: [size, size], 75 | iconAnchor: [size / 2, size / 2], 76 | }; 77 | }, 78 | collect: 79 | (size: number = 20, fontcolor: string = 'white') => 80 | (title?: string, fontSize: string = '0.8em') => { 81 | return { 82 | iconUrl: './resource/icons/boss.png', 83 | iconSize: [size, size], 84 | iconAnchor: [size / 2, size / 2], 85 | }; 86 | }, 87 | white: 88 | (size: number = 10, fontcolor: string = 'white') => 89 | (title?: string, fontSize: string = '0.8em') => { 90 | return { 91 | iconUrl: './resource/icons/boss.png', 92 | iconSize: [size, size], 93 | iconAnchor: [size / 2, size / 2], 94 | }; 95 | }, 96 | yellow: 97 | (size: number = 10, fontcolor: string = 'white') => 98 | (title?: string, fontSize: string = '0.8em') => { 99 | return { 100 | iconUrl: './resource/icons/boss.png', 101 | iconSize: [size, size], 102 | iconAnchor: [size / 2, size / 2], 103 | }; 104 | }, 105 | green: 106 | (size: number = 10, fontcolor: string = 'white') => 107 | (title?: string, fontSize: string = '0.8em') => { 108 | return { 109 | iconUrl: './resource/icons/boss.png', 110 | iconSize: [size, size], 111 | iconAnchor: [size / 2, size / 2], 112 | }; 113 | }, 114 | blue: 115 | (size: number = 10, fontcolor: string = 'white') => 116 | (title?: string, fontSize: string = '0.8em') => { 117 | return { 118 | iconUrl: './resource/icons/boss.png', 119 | iconSize: [size, size], 120 | iconAnchor: [size / 2, size / 2], 121 | }; 122 | }, 123 | red: 124 | (size: number = 10, fontcolor: string = 'white') => 125 | (title?: string, fontSize: string = '0.8em') => { 126 | return { 127 | iconUrl: './resource/icons/boss.png', 128 | iconSize: [size, size], 129 | iconAnchor: [size / 2, size / 2], 130 | }; 131 | }, 132 | purple: 133 | (size: number = 10, fontcolor: string = 'white') => 134 | (title?: string, fontSize: string = '0.8em') => { 135 | return { 136 | iconUrl: './resource/icons/boss.png', 137 | iconSize: [size, size], 138 | iconAnchor: [size / 2, size / 2], 139 | }; 140 | }, 141 | }; 142 | -------------------------------------------------------------------------------- /src/components/icons.css: -------------------------------------------------------------------------------- 1 | .icon { 2 | display: flex; 3 | } 4 | 5 | .icon p { 6 | text-shadow: 0 0 4px black; 7 | min-width: max-content; 8 | margin: 0; 9 | display: flex; 10 | flex-direction: column; 11 | cursor: pointer; 12 | font-family: serif; 13 | } 14 | @media (any-hover: hover) { 15 | .icon p:hover { 16 | text-decoration-line: underline; 17 | } 18 | } 19 | 20 | .icon p:active { 21 | text-decoration-line: underline; 22 | } 23 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置文件喵 3 | * @author wniko 4 | */ 5 | const Config = { 6 | APIBaseURL: './api/', 7 | currentVer: '3.3.1.3', // 本来想着稳定到3.1415926的,果然还是跳过了阿... 8 | lastUpdated: '2024-6-23 4:50 UT+8', 9 | /** 是否锁定所有地标 */ 10 | isLockAllMarkers: false, 11 | /** 是否在开发中,为true会导致一些界面上测试按钮被显示出来,并显示测试用地标 */ 12 | inDev: true, 13 | /** 是否使用测试用数据 */ 14 | useTestData:false, 15 | /** 是否第一次开启时显示更新内容页 */ 16 | showUpdateModal: true, 17 | }; 18 | 19 | export default Config; 20 | -------------------------------------------------------------------------------- /src/description.txt: -------------------------------------------------------------------------------- 1 | PUBLIC: 2 | 3 | /resource/ 图片 4 | /resource/icons/ 图标 5 | /resource/images/ 图片 6 | 7 | /api/ 8 | /private/ 隐私文件,不往git库里扔 9 | apothegm.php 讯息后端 10 | map.php 地标后端 11 | reply.php 回复后端 12 | searchUpload.php 搜索内容存储 13 | checkAdmin.php 验证管理员密码 14 | global.css 全局css 15 | favicon.png 网页图标 16 | 17 | 18 | 19 | SRC: 20 | /components/ 组件 21 | /components/icons.ts 地标图标 22 | /components/icons.css 图标css 23 | /components/MapView.svelte 地图 24 | /components/Modal.svelte Modal 25 | 26 | /pages/ 页面 27 | /pages/About.svelte 说明页 28 | /pages/Apothegm.svelte 讯息页 29 | /pages/Home.svelte (未使用) 30 | /pages/Map.svelte 地图页 31 | 32 | /router/ 路由文件 33 | 34 | /utils/ 其他 35 | /utils/enum.ts 各个枚举 36 | /utils/typings.ts 各个类型 37 | /utils/utils.ts 工具方法 38 | /utils/siteTypes.ts 各个地标类型定义 39 | /utils/apoTypes.ts 各个讯息类型定义 40 | 41 | /locale/ i18n 42 | 43 | /mocks/ mocks测试 44 | 45 | App.svelte 主页面,导入路由和菜单栏在这里 46 | config.ts 全局配置 47 | stores.ts svelte stores和全局变量 48 | main.ts 入口文件,获取ip在这里 49 | -------------------------------------------------------------------------------- /src/global.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.svg' { 4 | const src: ConstructorOfATypedSvelteComponent; 5 | export default src; 6 | } 7 | -------------------------------------------------------------------------------- /src/locale/index.ts: -------------------------------------------------------------------------------- 1 | import { addMessages, register, init, waitLocale } from 'svelte-i18n'; 2 | import { get } from 'svelte/store'; 3 | import { lang } from '../stores'; 4 | import { SupportedLang } from '../utils/enum'; 5 | import zhCN from './lang/zh-CN'; 6 | 7 | export async function setupI18n() { 8 | addMessages(SupportedLang.zhCN, zhCN); 9 | register(SupportedLang.zhTW, () => import('./lang/zh-TW')); 10 | register(SupportedLang.ja, () => import('./lang/ja')); 11 | init({ 12 | fallbackLocale: SupportedLang.zhCN, 13 | initialLocale: get(lang), 14 | }); 15 | return waitLocale(); 16 | } 17 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 入口文件喵 3 | */ 4 | import axios from 'axios'; 5 | import App from './App.svelte'; 6 | import Config from './config'; 7 | import { set_client_ip } from './utils/utils'; 8 | import { setupI18n } from './locale'; 9 | import { transferOldStorage } from './stores'; 10 | 11 | // 设置api根目录 12 | axios.defaults.baseURL = Config.APIBaseURL; 13 | 14 | // 开发时 mock api 进行测试 15 | async function bootstrap() { 16 | if (process.env.NODE_ENV === 'development') { 17 | const { worker } = await import('./mocks/browser'); 18 | worker.start(); 19 | } 20 | 21 | // 转移之前的storage 22 | transferOldStorage(); 23 | // 设置语言 24 | setupI18n(); 25 | 26 | // 获取ip到全局变量 27 | try { 28 | set_client_ip(); 29 | } catch (e) { 30 | console.log(e); 31 | } 32 | 33 | // 启动 34 | new App({ 35 | target: document.body, 36 | props: {}, 37 | }); 38 | }; 39 | 40 | bootstrap(); 41 | -------------------------------------------------------------------------------- /src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | import { setupWorker } from 'msw' 2 | import { handlers } from './handlers' 3 | // This configures a Service Worker with the given request handlers. 4 | export const worker = setupWorker(...handlers) 5 | -------------------------------------------------------------------------------- /src/mocks/data/admin.js: -------------------------------------------------------------------------------- 1 | export const ADMINPASSWORD = '123456'; 2 | -------------------------------------------------------------------------------- /src/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import mapHandlers from './handles/map'; 2 | import isRequestHandles from './handles/isRequest'; 3 | import checkAdminHandles from './handles/checkAdmin'; 4 | import apothegmHandles from './handles/apothegm'; 5 | 6 | export const handlers = [ 7 | ...mapHandlers, 8 | ...isRequestHandles, 9 | ...checkAdminHandles, 10 | ...apothegmHandles, 11 | ] 12 | -------------------------------------------------------------------------------- /src/mocks/handles/apothegm.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | import apothegmData from '../data/apothegm'; 3 | import type { Apothegm, Reply } from '../../utils/typings'; 4 | import type { ApothegmType } from '../../utils/enum'; 5 | 6 | /** 7 | * mix apothegm and apo_reply 8 | */ 9 | let apothegm: Apothegm[] = apothegmData; 10 | apothegm = apothegm.sort((a, b) => { 11 | return a.id - b.id; 12 | }) 13 | let next_apothegm_id = apothegm.at(apothegm.length - 1).id; 14 | 15 | let reply: Reply[] = []; 16 | apothegm.forEach((e) => { 17 | reply.push(...e.replies); 18 | }) 19 | reply.sort((a, b) => { 20 | return a.id - b.id; 21 | }) 22 | let next_reply_id = reply.at(reply.length - 1).id; 23 | 24 | export default [ 25 | rest.get('./api/apothegm.php', (req, res, ctx) => { 26 | const id_para = req.url.searchParams.get('id'); 27 | const ip_para = req.url.searchParams.get('ip'); 28 | const count_para = req.url.searchParams.get('count'); 29 | const kword_para = req.url.searchParams.get('kword'); 30 | const type_para = req.url.searchParams.get('type'); 31 | 32 | const id = Number(id_para); 33 | const ip = ip_para?.trim(); 34 | const count = Number(count_para); 35 | const kword = kword_para?.trim(); 36 | const type = type_para?.trim(); 37 | 38 | let selected_data: Apothegm[]; 39 | 40 | if (id_para && id >= 0) { 41 | selected_data = apothegm.filter(e => { 42 | return e.id == id; 43 | }); 44 | } else { 45 | selected_data = apothegm.filter(e => { 46 | return (!ip_para || e.ip == ip) && e.is_deleted == false 47 | && (!kword_para || e.title.match(kword) || e.content.match(kword)) 48 | && (!type_para || type == e.type); 49 | }).sort((a, b) => { 50 | return a.reply_date.localeCompare(b.reply_date); 51 | }); 52 | if (count && selected_data.length > count) { 53 | selected_data = selected_data.slice(0, count - 1); 54 | } 55 | } 56 | // append reply 57 | selected_data.forEach((e) => { 58 | e.replies = reply.filter((r) => { return r.pid == e.id; }); 59 | }) 60 | return res( 61 | ctx.json(selected_data), 62 | ctx.status(200), 63 | ) 64 | }), 65 | rest.post('./api/apothegm.php', (req, res, ctx) => { 66 | let date = new Date().toDateString(); 67 | 68 | apothegm.push(({ 69 | id: next_apothegm_id++, 70 | title: (req.body['title'] as string)?.trim(), 71 | content: (req.body['content'] as string)?.trim(), 72 | type: (req.body['desc'] as string)?.trim() as ApothegmType, 73 | like: req.body['like'], 74 | dislike: req.body['dislike'], 75 | ip: (req.body['ip'] as string)?.trim(), 76 | gesture: 0, 77 | is_top: false, 78 | replies: [], 79 | reply_date: date, 80 | is_deleted: false, 81 | create_date: date, 82 | update_date: date, 83 | })); 84 | return res( 85 | ctx.json(true), 86 | ctx.status(200), 87 | ) 88 | }), 89 | rest.delete('./api/apothegm.php', (req, res, ctx) => { 90 | const id = req.body['id'] as number; 91 | let result = false; 92 | for (let e of apothegm) { 93 | if (e.id == id && !e.is_deleted) { 94 | e.is_deleted = true; 95 | e.update_date = new Date().toDateString(); 96 | result = true; 97 | } 98 | } 99 | return res( 100 | ctx.json(result), 101 | ctx.status(200), 102 | ) 103 | }), 104 | rest.patch('./api/apothegm.php', (req, res, ctx) => { 105 | const id_para: string = req.body['id']; 106 | const title_para: string = req.body['title']; 107 | const content_para: string = req.url.searchParams.get('content'); 108 | const type_para: string = req.body['type']; 109 | const gesture: number = req.body['gesture']; 110 | const like_para: number = req.body['like']; 111 | const dislike_para: number = req.body['dislike']; 112 | const ip_para: string = req.body['ip']; 113 | const is_deleted_para: boolean = req.body['is_deleted']; 114 | const reply_date_para: string = req.body['reply_date']; 115 | 116 | const id = Number(id_para); 117 | 118 | let result = false; 119 | apothegm.every((e) => { 120 | if (e.id == id) { 121 | e.title = title_para ? title_para.trim() : e.title; 122 | e.content = content_para ? content_para.trim() : e.content; 123 | e.type = type_para ? type_para.trim() as ApothegmType : e.type; 124 | e.gesture = gesture ? gesture : e.gesture; 125 | e.like = like_para ? like_para : e.like; 126 | e.dislike = dislike_para ? dislike_para : e.dislike; 127 | e.ip = ip_para ? ip_para.trim() : e.ip; 128 | e.is_deleted = is_deleted_para ? is_deleted_para : e.is_deleted; 129 | e.reply_date = reply_date_para ? e.reply_date : new Date().toDateString(); 130 | e.update_date = new Date().toDateString(); 131 | result = true; 132 | return false; 133 | } 134 | return true; 135 | }) 136 | 137 | return res( 138 | ctx.json(result), 139 | ctx.status(200), 140 | ) 141 | }), 142 | // rest.get('./api/reply.php', (req, res, ctx) => { 143 | // return res( 144 | // ctx.json(''), 145 | // ctx.status(200), 146 | // ) 147 | // }), 148 | rest.post('./api/reply.php', (req, res, ctx) => { 149 | let date = new Date().toDateString(); 150 | 151 | reply.push(({ 152 | id: next_reply_id++, 153 | pid: req.body['pid'], 154 | content: (req.body['content'] as string)?.trim(), 155 | like: req.body['like'], 156 | dislike: req.body['dislike'], 157 | ip: (req.body['ip'] as string)?.trim(), 158 | is_deleted: false, 159 | create_date: date, 160 | update_date: date, 161 | })); 162 | return res( 163 | ctx.json(true), 164 | ctx.status(200), 165 | ) 166 | }), 167 | rest.delete('./api/reply.php', (req, res, ctx) => { 168 | const id = req.body['id'] as number; 169 | let result = false; 170 | for (let e of reply) { 171 | if (e.id == id && !e.is_deleted) { 172 | e.is_deleted = true; 173 | e.update_date = new Date().toDateString(); 174 | result = true; 175 | } 176 | } 177 | return res( 178 | ctx.json(result), 179 | ctx.status(200), 180 | ) 181 | }), 182 | rest.patch('./api/reply.php', (req, res, ctx) => { 183 | const id_para: string = req.body['id']; 184 | const pid_para: number = req.body['pid']; 185 | const content_para: string = req.url.searchParams.get('content'); 186 | const like_para: number = req.body['like']; 187 | const dislike_para: number = req.body['dislike']; 188 | const ip_para: string = req.body['ip']; 189 | const is_deleted_para: boolean = req.body['is_deleted']; 190 | 191 | const id = Number(id_para); 192 | 193 | let result = false; 194 | reply.every((e) => { 195 | if (e.id == id) { 196 | e.pid = pid_para ? pid_para : e.pid; 197 | e.content = content_para ? content_para.trim() : e.content; 198 | e.like = like_para ? like_para : e.like; 199 | e.dislike = dislike_para ? dislike_para : e.dislike; 200 | e.ip = ip_para ? ip_para.trim() : e.ip; 201 | e.is_deleted = is_deleted_para ? is_deleted_para : e.is_deleted; 202 | e.update_date = new Date().toDateString(); 203 | result = true; 204 | return false; 205 | } 206 | return true; 207 | }) 208 | 209 | return res( 210 | ctx.json(result), 211 | ctx.status(200), 212 | ) 213 | }), 214 | ] -------------------------------------------------------------------------------- /src/mocks/handles/checkAdmin.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | import { ADMINPASSWORD } from '../data/admin'; 3 | 4 | export default [ 5 | rest.post('/api/checkAdmin.php', (req, res, ctx) => { 6 | return res( 7 | ctx.json({ 8 | validate: req.body['p'] == ADMINPASSWORD, 9 | }), 10 | ctx.status(200), 11 | ) 12 | }), 13 | ] -------------------------------------------------------------------------------- /src/mocks/handles/isRequest.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | export default [ 4 | rest.post('/api/isRequest.php', (req, res, ctx) => { 5 | return res( 6 | ctx.json({ 7 | ip: 'localhost', 8 | }), 9 | ctx.status(200), 10 | ) 11 | }), 12 | ] -------------------------------------------------------------------------------- /src/mocks/handles/map.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | import type { MapPoint } from '../../utils/typings'; 3 | import type { MapPointType } from '../../utils/enum'; 4 | import mapdata from '../data/map'; 5 | 6 | let map: MapPoint[] = mapdata; 7 | map = map.sort((a, b) => { 8 | return a.id - b.id; 9 | }) 10 | let next_id = map.at(map.length - 1).id; 11 | 12 | export default [ 13 | rest.get('/api/map.php', (req, res, ctx) => { 14 | const id_para = req.url.searchParams.get('id'); 15 | const ip_para = req.url.searchParams.get('ip'); 16 | const count_para = req.url.searchParams.get('count'); 17 | const type_para = req.url.searchParams.get('type'); 18 | const under_para = req.url.searchParams.get('under'); 19 | const kword_para = req.url.searchParams.get('kword'); 20 | 21 | const id = Number(id_para); 22 | const ip = ip_para?.trim(); 23 | const count = Number(count_para); 24 | const types = type_para?.split('|'); 25 | const kword = kword_para?.trim(); 26 | const under = Number(under_para) == 1 ? true : false; 27 | 28 | let selected_data: MapPoint[]; 29 | 30 | if (id_para && id >= 0) { 31 | selected_data = map.filter(e => { 32 | return e.id == id; 33 | }); 34 | } else { 35 | selected_data = map.filter(e => { 36 | return (!ip_para || e.ip == ip) && (!under_para || e.is_underground == under) && e.is_deleted == false 37 | && (!kword_para || e.name.match(kword) || e.desc.match(kword)) 38 | && (!type_para || types.includes(e.type)); 39 | }).sort((a, b) => { 40 | return a.like - b.like; 41 | }); 42 | if (count && selected_data.length > count) { 43 | selected_data = selected_data.slice(0, count - 1); 44 | } 45 | } 46 | return res( 47 | ctx.json(selected_data), 48 | ctx.status(200), 49 | ) 50 | }), 51 | rest.post('/api/map.php', (req, res, ctx) => { 52 | let date = new Date().toDateString(); 53 | 54 | map.push(({ 55 | id: next_id++, 56 | type: (req.body['type'] as string)?.trim() as MapPointType, 57 | name: (req.body['name'] as string)?.trim(), 58 | desc: (req.body['desc'] as string)?.trim(), 59 | lng: req.body['lng'], 60 | lat: req.body['lat'], 61 | like: req.body['like'], 62 | dislike: req.body['dislike'], 63 | ip: (req.body['ip'] as string)?.trim(), 64 | is_underground: req.body['is_underground'] == 1, 65 | is_deleted: false, 66 | is_lock: false, 67 | create_date: date, 68 | update_date: date, 69 | })); 70 | return res( 71 | ctx.json(true), 72 | ctx.status(200), 73 | ) 74 | }), 75 | rest.delete('/api/map.php', (req, res, ctx) => { 76 | const id = req.body['id'] as number; 77 | let result = false; 78 | for (let e of map) { 79 | if (e.id == id && !e.is_deleted) { 80 | e.is_deleted = true; 81 | e.update_date = new Date().toDateString(); 82 | result = true; 83 | } 84 | } 85 | return res( 86 | ctx.json(result), 87 | ctx.status(200), 88 | ) 89 | }), 90 | rest.patch('/api/map.php', (req, res, ctx) => { 91 | const id_para: string = req.body['id']; 92 | const type_para: string = req.body['type']; 93 | const name_para: string = req.body['name']; 94 | const desc_para: string = req.body['desc']; 95 | const lng_para: number = req.body['lng']; 96 | const lat_para: number = req.body['lat']; 97 | const like_para: number = req.body['like']; 98 | const dislike_para: number = req.body['dislike']; 99 | const ip_para: string = req.body['ip']; 100 | const is_underground_para: boolean = req.body['is_underground']; 101 | const is_deleted_para: boolean = req.body['is_deleted']; 102 | const is_lock_para: boolean = req.body['is_lock']; 103 | 104 | const id = Number(id_para); 105 | 106 | let result = false; 107 | map.every((e) => { 108 | if (e.id == id) { 109 | e.type = type_para ? type_para.trim() as MapPointType : e.type; 110 | e.name = name_para ? name_para.trim() : e.name; 111 | e.desc = desc_para ? desc_para.trim() : e.desc; 112 | e.lng = lng_para ? lng_para : e.lng; 113 | e.lat = lat_para ? lat_para : e.lat; 114 | e.like = like_para ? like_para : e.like; 115 | e.dislike = dislike_para ? dislike_para : e.dislike; 116 | e.ip = ip_para ? ip_para.trim() : e.ip; 117 | e.is_deleted = is_deleted_para ? is_deleted_para : e.is_deleted; 118 | e.is_lock = is_lock_para ? is_lock_para : e.is_lock; 119 | e.is_underground = is_underground_para ? is_underground_para : e.is_underground; 120 | e.update_date = new Date().toDateString(); 121 | result = true; 122 | return false; 123 | } 124 | return true; 125 | }) 126 | return res( 127 | ctx.json(result), 128 | ctx.status(200), 129 | ) 130 | }), 131 | ] 132 | -------------------------------------------------------------------------------- /src/mocks/handles/searchUpload.ts: -------------------------------------------------------------------------------- 1 | import { rest } from 'msw'; 2 | 3 | export default [ 4 | rest.post('/api/searchUpload.php', (req, res, ctx) => { 5 | return res( 6 | ctx.json(true), 7 | ctx.status(200), 8 | ) 9 | }), 10 | ] -------------------------------------------------------------------------------- /src/pages/3DMap.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

得到了@纳闷虎先生的授权

7 |

会试着将他精心制作的各区域3D地图以在线可互动(但不可编辑)的形式呈现在这里

8 | 3dmap preview 9 |
10 |

做完支线就开工这个(大概)

11 |

(或者说,有来一起开发的老哥吗()

12 | aqua 13 |
14 | 15 | 30 | -------------------------------------------------------------------------------- /src/pages/Collect.svelte: -------------------------------------------------------------------------------- 1 | 105 | 106 |
107 |
108 | 111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | {#each collectMarkersData as collectMarkerData, index} 123 | 124 | 125 | 126 | 127 | 133 | 134 | {/each} 135 | 136 |
{$t('collect.table.type')}{$t('collect.table.name')}{$t('collect.table.savePlace')}
{collectMarkerData.type} handleRowClick(collectMarkerData)}>{collectMarkerData.collect.name}{collectMarkerData.isLocal ? '💻' + $t('collect.table.local') : '☁' + $t('collect.table.server')} 128 |
129 | 130 | 131 |
132 |
137 |
138 | 139 | 190 | -------------------------------------------------------------------------------- /src/pages/General.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | {$t('general.title')} 18 |
19 |
20 |
21 |

{$t('general.menulang')}

22 |
23 | lang.set(event.detail.lang)} /> 24 | lang.set(event.detail.lang)} /> 25 | lang.set(event.detail.lang)} /> 26 |
27 |
28 | 29 |
30 | 31 |
32 |

{$t('general.maplang')}

33 |
34 | convertTargetStore.set(event.detail.lang)} /> 35 | convertTargetStore.set(event.detail.lang)} /> 36 | convertTargetStore.set(event.detail.lang)} /> 37 |
38 |
39 | 40 |
41 | 42 |
43 |

{$t('general.localData.title')}

44 |
45 | {$t('general.localData.tooltip')} 46 | 47 |
48 | 49 |
50 | 51 | 52 |
53 |
54 | 55 |
56 | 57 |
58 | 74 |
75 | 76 | 77 | {#if new Date().getMonth() === 3 && new Date().getDate() === 1} 78 |
79 |

{$t('general.april.title')}

80 |
81 |

{$t('general.april.content')}

82 | 88 |

{`-wniko- ${new Date().getFullYear()}.4.1`}

89 |
90 | {/if} 91 | 92 | {#if config.default.inDev} 93 |
94 |

以下是测试内容

95 |

如果你在使用中看到了,请不要触碰并立即报告bug

96 |
...
97 |
98 | {/if} 99 | 100 |

{$t('general.developing')}

101 |
102 | 103 | 186 | -------------------------------------------------------------------------------- /src/pages/Home.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |
home
9 |
10 | 11 | 18 | -------------------------------------------------------------------------------- /src/pages/Map.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |
30 | 31 |
32 | 33 | 40 | -------------------------------------------------------------------------------- /src/pages/Routes.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | 17 |
18 | 19 | 31 | -------------------------------------------------------------------------------- /src/privateConfig.example.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 隐私配置 3 | * @author wniko 4 | */ 5 | const Config = { 6 | reCaptchaV2SiteKey: '114514', 7 | }; 8 | 9 | export default Config; 10 | -------------------------------------------------------------------------------- /src/router/router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由文件喵 3 | * @author wniko 4 | */ 5 | import Home from '../pages/Home.svelte'; 6 | import About from '../pages/About.svelte'; 7 | import Apothegm from '../pages/Apothegm.svelte'; 8 | import Map from '../pages/Map.svelte'; 9 | import General from '../pages/General.svelte'; 10 | import Routes from '../pages/Routes.svelte'; 11 | import ThreeDimeMap from '../pages/3DMap.svelte'; 12 | import Collect from '../pages/Collect.svelte'; 13 | 14 | export const routes = { 15 | '/': Map, 16 | '/about': About, 17 | '/apothegm/:id?': Apothegm, 18 | '/map': Map, 19 | '/general': General, 20 | '/routes': Routes, 21 | '/3dmap': ThreeDimeMap, 22 | '/collect': Collect, 23 | }; 24 | -------------------------------------------------------------------------------- /src/stores.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Stores和全局变量 3 | * @author wniko 4 | */ 5 | import { get, Writable, writable } from 'svelte/store'; 6 | import type { MapPoint } from './utils/typings'; 7 | import { ConvertType } from 'zhconvertor'; 8 | import { persist, PersistentStore } from './utils/persist'; 9 | import { locale } from 'svelte-i18n'; 10 | import type { SupportedLang } from './utils/enum'; 11 | import { getCookie, setCookie } from './utils/utils'; 12 | import Config from './config'; 13 | 14 | // Stores 15 | 16 | /** 是否是管理员Mode的store */ 17 | export let isAdminModeStore = writable(false); 18 | 19 | /** 界面语言 */ 20 | export let lang = persist(locale as Writable, 'lang'); 21 | 22 | /** 内容语言转换 */ 23 | export let convertTargetStore = persist(writable(ConvertType.dont), 'convertTarget'); 24 | 25 | // id集合类 26 | class pointSet { 27 | store: PersistentStore>; 28 | constructor(key: string) { 29 | this.store = persist(writable>(new Set()), key); 30 | } 31 | public getStore() { 32 | const getPoints = () => { 33 | return get(this.store); 34 | }; 35 | const setPoints = (points: Set) => { 36 | this.store.set(points); 37 | }; 38 | const addPoint = (p: number) => { 39 | setPoints(getPoints().add(p)); 40 | }; 41 | const addPoints = (ps: number[] | Set) => { 42 | ps.forEach((p: number) => { 43 | getPoints().add(p); 44 | }); 45 | setPoints(getPoints()); 46 | }; 47 | const removePoint = (p: number) => { 48 | getPoints().delete(p); 49 | setPoints(getPoints()); 50 | }; 51 | const clear = () => { 52 | setPoints(new Set()); 53 | }; 54 | return { 55 | getPoints, 56 | addPoint, 57 | addPoints, 58 | removePoint, 59 | clear, 60 | ...this.store, 61 | }; 62 | } 63 | } 64 | 65 | /** 收藏点的id集合 */ 66 | export let collectionSet = new pointSet('collections'); 67 | 68 | /** 隐藏点的id集合 */ 69 | export let hiddenSet = new pointSet('hiddens'); 70 | 71 | // 检查旧的本地存储是否转移完毕 72 | let version = persist(writable('' as string), 'version'); 73 | export function transferOldStorage() { 74 | if (!get(version)) { 75 | if (getCookie('lang') !== '') { 76 | localStorage.setItem('lang', getCookie('lang')); 77 | setCookie('lang', '', 0); 78 | } 79 | 80 | { 81 | // collections 82 | let old: string = ''; 83 | if (getCookie('collect') !== '') { 84 | old += getCookie('collect') + '|'; 85 | setCookie('collect', '', 0); 86 | } 87 | old += localStorage.getItem('collect') ?? ''; 88 | 89 | const transafered = old?.split('|') ?? []; 90 | if (transafered.length > 0) { 91 | collectionSet.getStore().addPoints(transafered.filter(s => s).map(s => Number(s))); 92 | } 93 | // 先不删,怕出问题 94 | // localStorage.removeItem('collect'); 95 | } 96 | 97 | { 98 | // hiddens 99 | let old: string = ''; 100 | if (getCookie('hidden') !== '') { 101 | old += getCookie('hidden') + '|'; 102 | setCookie('hidden', '', 0); 103 | } 104 | old += localStorage.getItem('hidden') ?? ''; 105 | 106 | const transafered = old?.split('|') ?? []; 107 | if (transafered.length > 0) { 108 | hiddenSet.getStore().addPoints(transafered.filter(s => s).map(s => Number(s))); 109 | } 110 | // 先不删,怕出问题 111 | // localStorage.removeItem('hidden'); 112 | } 113 | 114 | // 转移完毕 115 | version.set(Config.currentVer); 116 | } 117 | } 118 | 119 | //全局变量 120 | 121 | /** ip */ 122 | export let ip = ''; 123 | /** 设置ip */ 124 | export const setIp = newip => { 125 | ip = newip; 126 | }; 127 | 128 | /** 是否是便携式设备 */ 129 | export const isMobile: boolean = /Android|webOS|iPhone|iPod|BlackBerry/i.test(navigator.userAgent); 130 | 131 | /** 服务端获取到的所有markers */ 132 | export let allMarkers: MapPoint[] = []; 133 | export let setAllMarkers = (markers: MapPoint[]) => { 134 | allMarkers = markers; 135 | }; 136 | -------------------------------------------------------------------------------- /src/utils/convertor.ts: -------------------------------------------------------------------------------- 1 | import zhConvertor, { ConvertType } from 'zhconvertor'; 2 | import { convertTargetStore } from '../stores'; 3 | import { get } from 'svelte/store'; 4 | export { ConvertType } from 'zhconvertor'; 5 | 6 | export const getConvertedText = (str: string) => { 7 | return zhConvertor.convert(str, get(convertTargetStore)); 8 | }; 9 | 10 | export const getKeywordText = (str: string) => { 11 | return zhConvertor.convert(str, ConvertType.s2t) + '|' + zhConvertor.convert(str, ConvertType.t2s); 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 这个文件用来存各种enum喵 3 | * @author wniko 4 | */ 5 | 6 | /** 7 | * 地标类型 8 | */ 9 | export enum MapPointType { 10 | Empty = '', 11 | // 地点 12 | Cifu = 'cifu', 13 | Jiejing = 'pass', 14 | Portal = 'portal', 15 | SoulSite = 'soulsite', 16 | Shop = 'shop', 17 | NPC = 'npc', 18 | Map = 'map', 19 | Place = 'place', 20 | Lianji = 'lianji', 21 | Cave = 'cave', 22 | Cemetery = 'cemetery', 23 | Wind = 'wind', 24 | Sword = 'sword', 25 | Temple = 'temple', 26 | Arena = 'arena', 27 | 28 | // 敌人 29 | BigBoss = 'bigboss', 30 | Boss = 'boss', 31 | LittleBoss = 'littboss', 32 | RedSoul = 'redsoul', 33 | Jingyingguai = 'jingying', 34 | 35 | // 道具 36 | Item = 'item', 37 | ImportantItem = 'impoitem', 38 | Stone = 'stone', 39 | GoldenSeed = 'goldseed', 40 | Ludi = 'ludi', 41 | ShengbeiLudi = 'sbludi', 42 | Key = 'key', 43 | Sigen = 'sigen', 44 | Bead = 'bead', 45 | Orchid = 'orchid', 46 | Material = 'material', 47 | Tear = 'tear', 48 | ScadutreeFragment = 'scadutreefragment', 49 | SpiritAshes = 'spiritashes', 50 | 51 | // 收集 52 | Paint = 'paint', 53 | Gesture = 'gesture', 54 | 55 | // 武器 56 | Magic = 'magic', 57 | Weapon = 'weapon', 58 | Daogao = 'daogao', 59 | Clothes = 'clothes', 60 | Zhanhui = 'zhanhui', 61 | Guhui = 'guhui', 62 | Ring = 'ring', 63 | 64 | // 留言 65 | Text = 'text', 66 | Warn = 'warn', 67 | Question = 'question', 68 | Taoke = 'taoke', 69 | } 70 | 71 | /** 72 | * 讯息类型 73 | */ 74 | export enum ApothegmType { 75 | Empty = '', 76 | 77 | Feature = 'feature', 78 | Suggest = 'suggest', 79 | BugReport = 'bug', 80 | 81 | Strategy = 'strategy', 82 | Kokoroe = 'kokoroe', 83 | Ask = 'ask', 84 | 85 | Message = 'message', 86 | 87 | Water = 'water', 88 | } 89 | 90 | export enum SupportedLang { 91 | zhCN = 'zh-CN', 92 | zhTW = 'zh-TW', 93 | ja = 'ja', 94 | } 95 | 96 | export enum TranslateTargetLang { 97 | zhCN = 'zh-CN', 98 | zhTW = 'zh-TW', 99 | } 100 | 101 | /** 地表地标所处位置 */ 102 | export enum PointPosition { 103 | /** 地面 */ 104 | Surface = 0, 105 | /** 洞窟墓穴下水道 */ 106 | Cave = 1, 107 | /** 灰城 */ 108 | AfterBurning = 2, 109 | } 110 | 111 | /** 显示的地图类型 */ 112 | export enum MapType { 113 | /** 默认的地表地图 */ 114 | Default = 0, 115 | /** 地下地图 */ 116 | Underground = 1, 117 | /** DLC Shadow of the Erdtree地图 */ 118 | DLC_shadow_of_the_erdtree = 2, 119 | } 120 | -------------------------------------------------------------------------------- /src/utils/filters.ts: -------------------------------------------------------------------------------- 1 | import { MapIcon } from '../components/icons'; 2 | import { ApothegmType, MapPointType } from './enum'; 3 | 4 | /** 5 | * 所有筛选选项 6 | * @description hr: 是否是分割线,functional: 是否是功能性选项 7 | */ 8 | export const getSiteTypeFilters = $t => [ 9 | { name: $t('siteTypes.functionalFilters.myPoints'), value: 'self', functional: true }, 10 | { name: $t('siteTypes.functionalFilters.myCollect'), value: 'collect', functional: true }, 11 | { name: $t('siteTypes.functionalFilters.showHidden'), value: 'hide', functional: true }, 12 | { name: $t('siteTypes.functionalFilters.hideBad'), value: 'hidebad', functional: true }, 13 | { name: $t('siteTypes.functionalFilters.selectAll'), value: 'all', functional: true }, 14 | 15 | { name: $t('siteTypes.filterGroupNames.sites'), hr: true }, 16 | { name: $t('siteTypes.filters.cifu'), value: MapPointType.Cifu, icon: MapIcon.cifu(true) }, 17 | { name: $t('siteTypes.filters.jiejing'), value: MapPointType.Jiejing, icon: MapIcon.yellow(), emoji: '🎈' }, 18 | { name: $t('siteTypes.filters.portal'), value: MapPointType.Portal, icon: MapIcon.portal(true) }, 19 | { name: $t('siteTypes.filters.soulsite'), value: MapPointType.SoulSite, icon: MapIcon.yellow(), emoji: '💎' }, 20 | { name: $t('siteTypes.filters.shop'), value: MapPointType.Shop, icon: MapIcon.yellow(), emoji: '💰' }, 21 | { name: $t('siteTypes.filters.npc'), value: MapPointType.NPC, icon: MapIcon.yellow(), emoji: '🙂' }, 22 | { name: $t('siteTypes.filters.map'), value: MapPointType.Map, icon: MapIcon.yellow(), emoji: '🧭' }, 23 | { name: $t('siteTypes.filters.lianji'), value: MapPointType.Lianji, icon: MapIcon.yellow(), emoji: '🚩' }, 24 | { name: $t('siteTypes.filters.wind'), value: MapPointType.Wind, icon: MapIcon.yellow(), emoji: '🌀' }, 25 | { name: $t('siteTypes.filters.cave'), value: MapPointType.Cave, icon: MapIcon.yellow(), emoji: '🌋' }, 26 | { name: $t('siteTypes.filters.cemetery'), value: MapPointType.Cemetery, icon: MapIcon.yellow(), emoji: '🗻' }, 27 | { name: $t('siteTypes.filters.sword'), value: MapPointType.Sword, icon: MapIcon.yellow(), emoji: '◉' }, 28 | { name: $t('siteTypes.filters.temple'), value: MapPointType.Temple, icon: MapIcon.yellow(), emoji: '🐢' }, 29 | { name: $t('siteTypes.filters.arena'), value: MapPointType.Arena, icon: MapIcon.yellow(), emoji: '⚔️' }, 30 | { name: $t('siteTypes.filters.place'), value: MapPointType.Place, icon: MapIcon.yellow(true) }, 31 | 32 | { name: $t('siteTypes.filterGroupNames.enemy'), hr: true }, 33 | { name: $t('siteTypes.filters.bigboss'), value: MapPointType.BigBoss, icon: MapIcon.boss(true, 35) }, 34 | { name: $t('siteTypes.filters.boss'), value: MapPointType.Boss, icon: MapIcon.boss(true) }, 35 | { name: $t('siteTypes.filters.littleboss'), value: MapPointType.LittleBoss, icon: MapIcon.littleboss(true) }, 36 | { name: $t('siteTypes.filters.redsoul'), value: MapPointType.RedSoul, icon: MapIcon.red(true) }, 37 | { name: $t('siteTypes.filters.jingyingguai'), value: MapPointType.Jingyingguai, icon: MapIcon.red(), emoji: '🔰' }, 38 | 39 | { name: $t('siteTypes.filterGroupNames.items'), hr: true }, 40 | { name: $t('siteTypes.filters.stone'), value: MapPointType.Stone, icon: MapIcon.blue(), emoji: '🗿' }, 41 | { name: $t('siteTypes.filters.orchid'), value: MapPointType.Orchid, icon: MapIcon.blue(), emoji: '🥀' }, 42 | { name: $t('siteTypes.filters.goldenseed'), value: MapPointType.GoldenSeed, icon: MapIcon.blue(), noname: true, emoji: '🌟' }, 43 | { name: $t('siteTypes.filters.ludi'), value: MapPointType.Ludi, icon: MapIcon.blue(), emoji: '🧿' }, 44 | { name: $t('siteTypes.filters.shengbeiludi'), value: MapPointType.ShengbeiLudi, icon: MapIcon.blue(), noname: true, emoji: '🍷' }, 45 | { name: $t('siteTypes.filters.bead'), value: MapPointType.Bead, icon: MapIcon.blue(), emoji: '🏐' }, 46 | { name: $t('siteTypes.filters.key'), value: MapPointType.Key, icon: MapIcon.blue(), noname: true, emoji: '🔑' }, 47 | { name: $t('siteTypes.filters.sigen'), value: MapPointType.Sigen, icon: MapIcon.blue(), noname: true, emoji: '🌰' }, 48 | { name: $t('siteTypes.filters.tear'), value: MapPointType.Tear, icon: MapIcon.blue(), noname: true, emoji: '🥚' }, 49 | { name: $t('siteTypes.filters.metarial'), value: MapPointType.Material, icon: MapIcon.white(true, 6) }, 50 | { name: $t('siteTypes.filters.importantitem'), value: MapPointType.ImportantItem, icon: MapIcon.purple(true) }, 51 | { name: $t('siteTypes.filters.item'), value: MapPointType.Item, icon: MapIcon.blue(true) }, 52 | 53 | { name: $t('siteTypes.filterGroupNames.dlc1items'), hr: true }, 54 | { name: $t('siteTypes.filters.scadutreefragment'), value: MapPointType.ScadutreeFragment, icon: MapIcon.blue(), emoji: '🌳' }, 55 | { name: $t('siteTypes.filters.spiritashes'), value: MapPointType.SpiritAshes, icon: MapIcon.blue(), emoji: '🍙' }, 56 | 57 | { name: $t('siteTypes.filterGroupNames.collection'), hr: true }, 58 | { name: $t('siteTypes.filters.gesture'), value: MapPointType.Gesture, icon: MapIcon.blue(), emoji: '🕺' }, 59 | { name: $t('siteTypes.filters.paint'), value: MapPointType.Paint, icon: MapIcon.blue(), emoji: '🎨' }, 60 | 61 | { name: $t('siteTypes.filterGroupNames.weapons'), hr: true }, 62 | { name: $t('siteTypes.filters.magic'), value: MapPointType.Magic, icon: MapIcon.purple(), emoji: '🔯' }, 63 | { name: $t('siteTypes.filters.daogao'), value: MapPointType.Daogao, icon: MapIcon.purple(), emoji: '🎇' }, 64 | { name: $t('siteTypes.filters.weapon'), value: MapPointType.Weapon, icon: MapIcon.purple(), emoji: '🔪' }, 65 | { name: $t('siteTypes.filters.clothes'), value: MapPointType.Clothes, icon: MapIcon.purple(), emoji: '🥋' }, 66 | { name: $t('siteTypes.filters.ring'), value: MapPointType.Ring, icon: MapIcon.purple(), emoji: '📿' }, 67 | { name: $t('siteTypes.filters.zhanhui'), value: MapPointType.Zhanhui, icon: MapIcon.purple(), emoji: '🔱' }, 68 | { name: $t('siteTypes.filters.guhui'), value: MapPointType.Guhui, icon: MapIcon.purple(), emoji: '🔔' }, 69 | 70 | { name: $t('siteTypes.filterGroupNames.message'), hr: true }, 71 | { name: $t('siteTypes.filters.text'), value: MapPointType.Text, icon: MapIcon.message(), emoji: '💬' }, 72 | { name: $t('siteTypes.filters.warn'), value: MapPointType.Warn, icon: MapIcon.warning(true) }, 73 | { name: $t('siteTypes.filters.question'), value: MapPointType.Question, icon: MapIcon.question(), emoji: '❔' }, 74 | { name: $t('siteTypes.filters.taoke'), value: MapPointType.Taoke, icon: MapIcon.message(), emoji: '🚀' }, 75 | ]; 76 | 77 | /** 78 | * 所有筛选选项 79 | * @description hr: 是否是分割线,functional: 是否是功能性选项 80 | */ 81 | export const getApoFilters = $t => [ 82 | { name: $t('apoTypes.functionalFilters.all'), value: 'all', color: '', show: false, functional: true }, 83 | { name: $t('apoTypes.functionalFilters.my'), value: 'my', color: '', show: false, functional: true }, 84 | 85 | { name: $t('apoTypes.filterGroupNames.website'), hr: true }, 86 | { name: $t('apoTypes.filters.update'), value: ApothegmType.Feature, color: 'yellow', show: true, admin: true }, 87 | { name: $t('apoTypes.filters.suggest'), value: ApothegmType.Suggest, color: 'orange', show: true }, 88 | { name: $t('apoTypes.filters.bug'), value: ApothegmType.BugReport, color: '#ff3535', show: true }, 89 | 90 | { name: $t('apoTypes.filterGroupNames.game'), hr: true }, 91 | { name: $t('apoTypes.filters.strategy'), value: ApothegmType.Strategy, color: '#10d118', show: true }, 92 | { name: $t('apoTypes.filters.kokoroe'), value: ApothegmType.Kokoroe, color: '#85ff00', show: true }, 93 | { name: $t('apoTypes.filters.ask'), value: ApothegmType.Ask, color: '#f44aff', show: true }, 94 | 95 | { name: $t('apoTypes.filterGroupNames.others'), hr: true }, 96 | { name: $t('apoTypes.filters.message'), value: ApothegmType.Message, color: 'default', show: false }, 97 | { name: $t('apoTypes.filters.water'), value: ApothegmType.Water, color: '#03a9f4', show: true }, 98 | ]; 99 | -------------------------------------------------------------------------------- /src/utils/persist.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * references: 3 | * https://github.com/vueuse/vueuse/blob/main/packages/core/useStorage/index.ts 4 | * https://github.com/MacFJA/svelte-persistent-store/blob/main/src/index.ts 5 | */ 6 | 7 | import type { Writable } from "svelte/store"; 8 | import { get } from 'svelte/store'; 9 | 10 | export function guessSerializerType(rawInit: T) { 11 | return rawInit == null 12 | ? 'any' 13 | : rawInit instanceof Set 14 | ? 'set' 15 | : rawInit instanceof Map 16 | ? 'map' 17 | : rawInit instanceof Date 18 | ? 'date' 19 | : typeof rawInit === 'boolean' 20 | ? 'boolean' 21 | : typeof rawInit === 'string' 22 | ? 'string' 23 | : typeof rawInit === 'object' 24 | ? 'object' 25 | : Array.isArray(rawInit) 26 | ? 'object' 27 | : !Number.isNaN(rawInit) 28 | ? 'number' 29 | : 'any'; 30 | } 31 | 32 | export interface Serializer { 33 | read(raw: string): T 34 | write(value: T): string 35 | } 36 | 37 | export const StorageSerializers: Record<'boolean' | 'object' | 'number' | 'any' | 'string' | 'map' | 'set' | 'date', Serializer> = { 38 | boolean: { 39 | read: (v: string) => v === 'true', 40 | write: (v: any) => String(v), 41 | }, 42 | object: { 43 | read: (v: string) => JSON.parse(v), 44 | write: (v: any) => JSON.stringify(v), 45 | }, 46 | number: { 47 | read: (v: string) => Number.parseFloat(v), 48 | write: (v: any) => String(v), 49 | }, 50 | any: { 51 | read: (v: string) => v, 52 | write: (v: any) => String(v), 53 | }, 54 | string: { 55 | read: (v: string) => v, 56 | write: (v: any) => String(v), 57 | }, 58 | map: { 59 | read: (v: string) => new Map(JSON.parse(v)), 60 | write: (v: any) => JSON.stringify(Array.from((v as Map).entries())), 61 | }, 62 | set: { 63 | read: (v: string) => new Set(JSON.parse(v)), 64 | write: (v: any) => JSON.stringify(Array.from(v as Set)), 65 | }, 66 | date: { 67 | read: (v: string) => new Date(v), 68 | write: (v: any) => v.toISOString(), 69 | }, 70 | } 71 | 72 | /** 73 | * A store that keep it's value in time. 74 | */ 75 | export interface PersistentStore extends Writable { 76 | /** 77 | * Delete the store value from the persistent storage 78 | * Use when only do not need the store 79 | */ 80 | delete(): void; 81 | } 82 | 83 | export interface StorageOptions { 84 | storage?: Storage; 85 | serializer?: Serializer; 86 | } 87 | 88 | /** 89 | * Make a store persistent 90 | * @param {Writable<*>} store The store to enhance 91 | * @param {string} key The name of the data key 92 | * @param {StorageOptions} options More options 93 | */ 94 | export function persist(store: Writable, key: string, options: StorageOptions = {}): PersistentStore { 95 | const storage = options.storage || localStorage; 96 | 97 | const type = guessSerializerType(get(store)); 98 | const serializer = options.serializer ?? StorageSerializers[type]; 99 | 100 | const value = storage.getItem(key); 101 | if (value !== null) { 102 | try { 103 | store.set(serializer.read(value)); 104 | } catch (e) { 105 | console.error(e); 106 | } 107 | } 108 | 109 | store.subscribe(value => { 110 | storage.setItem(key, value ? serializer.write(value) : ''); 111 | }); 112 | 113 | return { 114 | ...store, 115 | delete() { 116 | store.set(undefined); 117 | storage.removeItem(key); 118 | }, 119 | }; 120 | } 121 | -------------------------------------------------------------------------------- /src/utils/typings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 这个文件用来定义各种类型喵 3 | * @author wniko 4 | */ 5 | import type { ApothegmType, MapPointType, MapType, PointPosition } from './enum'; 6 | export type { langType } from '../locale/lang/zh-CN'; 7 | 8 | /** 地标 */ 9 | export type MapPoint = { 10 | id: number; 11 | type: MapPointType; 12 | name: string; 13 | desc: string; 14 | lng: number; 15 | lat: number; 16 | like: number; 17 | dislike: number; 18 | is_achievement: boolean; 19 | delete_request: number; 20 | ip: string; 21 | is_deleted: boolean; 22 | is_lock: boolean; 23 | mapType: MapType; 24 | position: PointPosition; 25 | create_date: string; 26 | update_date: string; 27 | x: string | number; 28 | y: string | number; 29 | }; 30 | 31 | /** 讯息回复 */ 32 | export type Reply = { 33 | id: number; 34 | pid: number; 35 | content: string; 36 | like: number; 37 | dislike: number; 38 | ip: string; 39 | is_deleted: boolean; 40 | create_date: string; 41 | update_date: string; 42 | }; 43 | 44 | /** 讯息 */ 45 | export type Apothegm = { 46 | id: number; 47 | title: string; 48 | content: string; 49 | type: ApothegmType; 50 | gesture: number; 51 | is_top: boolean; 52 | like: number; 53 | dislike: number; 54 | ip: string; 55 | is_deleted: boolean; 56 | create_date: string; 57 | update_date: string; 58 | reply_date: string; 59 | /** 回复 */ 60 | replies: Reply[]; 61 | }; 62 | 63 | /** 统计数据 */ 64 | export type Statistics = { 65 | markerCount: number; 66 | markerCountWithoutDeleted: number; 67 | mostSearched: { 68 | word: string; 69 | count: number; 70 | }[]; 71 | types: { 72 | word: string; 73 | count: number; 74 | }[]; 75 | }; 76 | 77 | /** 78 | * 获取IP/地址结果 79 | */ 80 | export type GetIPPositionReturn = { 81 | /** IP */ 82 | cip: string; 83 | /** 行政区划编码 */ 84 | cid: string; 85 | /** 位置 */ 86 | cname: string; 87 | }; 88 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 工具类喵 3 | * @author wniko 4 | */ 5 | import axios from 'axios'; 6 | import md5 from 'md5'; 7 | import { setIp } from '../stores'; 8 | import type { GetIPPositionReturn } from './typings'; 9 | 10 | /** 11 | * 获取IP和地址 12 | * @returns 13 | * @author wniko 14 | */ 15 | export const get_ip_position = (): GetIPPositionReturn => { 16 | // 判断是否可用,如果浏览去开启拦截广告,就不会读取,导致错误 17 | // @ts-ignore 18 | if (returnCitySN) { 19 | // @ts-ignore 20 | const value = returnCitySN; // 见index.html 21 | if (value) { 22 | return value as GetIPPositionReturn; 23 | } 24 | } else { 25 | return { 26 | cip: '', 27 | cid: '', 28 | cname: '', 29 | }; 30 | } 31 | }; 32 | 33 | /** 34 | * 设置用户ip 35 | */ 36 | export const set_client_ip = () => { 37 | axios.get('./ipRequest.php').then(res => { 38 | setIp(res?.data?.ip); 39 | }); 40 | }; 41 | 42 | /** 43 | * 设置Cookie 44 | * @param cname 名 45 | * @param cvalue 值 46 | * @param exdays 死亡日 47 | */ 48 | export const setCookie = (cname: string, cvalue: any, exdays: number = 30) => { 49 | const d = new Date(); 50 | d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); 51 | const expires = 'expires=' + d.toUTCString(); 52 | document.cookie = cname + '=' + cvalue + '; ' + expires; 53 | }; 54 | 55 | /** 56 | * 读取Cookie 57 | * @param cname 名 58 | * @returns 值 59 | */ 60 | export const getCookie = (cname: string): string => { 61 | const name = cname + '='; 62 | const ca = document.cookie.split(';'); 63 | for (let i = 0; i < ca.length; i++) { 64 | const c = ca[i].trim(); 65 | if (c.indexOf(name) == 0) return c.substring(name.length, c.length); 66 | } 67 | return ''; 68 | }; 69 | 70 | /** 71 | * 根据ip获取对应的匿名id 72 | * @param ip IP 73 | */ 74 | export const getMD5Id = (ip: string) => { 75 | return ip === 'unknown' || ip === '' ? '' : md5(ip).substring(0, 6); 76 | }; 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "declarationDir": "dist/types", 5 | "outDir": "dist", 6 | "declaration": true, 7 | "target": "esnext", 8 | "module": "esnext", 9 | "strict": false, 10 | //"verbatimModuleSyntax": true, 11 | //"ignoreDeprecations": "5.0" 12 | }, 13 | "include": ["src/**/*", "rollup.config.ts"], 14 | "exclude": [] 15 | } 16 | --------------------------------------------------------------------------------