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 | 
13 | 
14 | 
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 | 
36 | 
37 |
38 | ## 关于参与开发
39 |
40 | 老头环地图的日访问量一直维持在20-30万之间,在如此庞大的用户量面前,我笨拙的技术水平已经不足以跟得上日日增加的计划功能、反馈的bug的开发,所以,欢迎通过邮件/QQ/issue联系 参与到开发里来~
41 |
42 | ## 贡献者
43 |
44 | |

|

|
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 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-close.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-collect.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-comment.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-down-arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-download_on_the_App_Store_Badge_CNSC_RGB_wht_092917.svg:
--------------------------------------------------------------------------------
1 |
30 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-left-arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-pin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-quit-mark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-remark.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-right-arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-search.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-send-myself.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-subscript-link-10.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-subscript-link-7.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-toggle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-up-arrow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |

26 |
27 |
法环地图 - 狭间地冒险指南
28 |
29 |
30 |
非常出色的iPhone/iPad端法环离线地图,标记、成就查看等..该有的功能应有尽有
31 |
已经和本站共享了部分数据,使用狭间地冒险指南也可以轻鬆看到这里的个性化标注了~
32 |
33 |

34 |
35 |
36 |
37 |
38 |
点击跳转到App Store页面
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |

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 |
42 |
43 |
74 |
--------------------------------------------------------------------------------
/src/components/MenuItem.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 | {#if currentPath === path}
15 |
16 | {/if}
17 |
18 | {text}
19 |
20 |
21 |
52 |
--------------------------------------------------------------------------------
/src/components/Modal.svelte:
--------------------------------------------------------------------------------
1 |
5 |
28 |
29 | {#if visible}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {#if title}
38 |
{title}
39 |
40 | {/if}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | {#if showOkButton}
52 |
53 | {/if}
54 | {#if showCloseButton}
55 |
56 | {/if}
57 |
58 |
59 |
60 |
61 | {/if}
62 |
63 |
106 |
--------------------------------------------------------------------------------
/src/components/RouteViewer/RouteViewer.svelte:
--------------------------------------------------------------------------------
1 |
87 |
88 |
89 |
90 |
只是个预览,没有功能( )
91 |
数据皆为边想边输入的测试数据,不保证正确性orz
92 |
手机端看不到内容的话请往右边划→
93 |
可能会有连线重叠了的情况
94 |
95 |
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 |
190 |
193 |
194 |
197 |
198 |
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 |

9 |
10 |
做完支线就开工这个(大概)
11 |
(或者说,有来一起开发的老哥吗()
12 |

13 |
14 |
15 |
30 |
--------------------------------------------------------------------------------
/src/pages/Collect.svelte:
--------------------------------------------------------------------------------
1 |
105 |
106 |
107 |
108 |
111 |
112 |
113 |
114 |
115 | {$t('collect.table.type')} |
116 | {$t('collect.table.name')} |
117 | {$t('collect.table.savePlace')} |
118 | |
119 |
120 |
121 |
122 | {#each collectMarkersData as collectMarkerData, index}
123 |
124 | {collectMarkerData.type} |
125 | handleRowClick(collectMarkerData)}>{collectMarkerData.collect.name} |
126 | {collectMarkerData.isLocal ? '💻' + $t('collect.table.local') : '☁' + $t('collect.table.server')} |
127 |
128 |
129 |
130 |
131 |
132 | |
133 |
134 | {/each}
135 |
136 |
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 |
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 |
--------------------------------------------------------------------------------