├── .editorconfig ├── .env ├── .eslintrc ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .umirc.js ├── LICENSE ├── README.md ├── functions ├── QuantumultXScriptAddDeviceID.js ├── QuantumultXScriptSubscriptionAddDeviceID.js ├── SSR2Clash.js ├── SSRDecode.js ├── SurgeProfile2SurgeList.js ├── TestGetEnvInfo.js ├── ds │ └── Surge.js ├── protect │ └── password.js └── testDownload.js ├── mock └── .gitkeep ├── netlify.toml ├── package.json ├── src ├── app.js ├── assets │ └── yay.jpg ├── global.css ├── layouts │ ├── __tests__ │ │ └── index.test.js │ ├── index.css │ └── index.js ├── models │ └── .gitkeep └── pages │ ├── 404.css │ ├── 404.js │ ├── __tests__ │ └── index.test.js │ ├── generator │ ├── QuantumultXScriptAddDeviceID.css │ ├── QuantumultXScriptAddDeviceID.js │ ├── QuantumultXScriptSubscriptionAddDeviceID.css │ ├── QuantumultXScriptSubscriptionAddDeviceID.js │ ├── QuantumultXScriptSubscriptionAddDeviceIDPreset.css │ └── QuantumultXScriptSubscriptionAddDeviceIDPreset.js │ ├── index.css │ └── index.js ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BROWSER=none 2 | ESLINT=1 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint-config-umi" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # netlify 2 | .netlify/ 3 | 4 | # node 5 | node_modules/ 6 | /npm-debug.log* 7 | /yarn-error.log 8 | 9 | # production 10 | /dist 11 | 12 | # misc 13 | .DS_Store 14 | 15 | # umi 16 | .umi 17 | .umi-production -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.md 2 | **/*.svg 3 | **/*.ejs 4 | **/*.html 5 | package.json 6 | .umi 7 | .umi-production 8 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100, 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.umirc.js: -------------------------------------------------------------------------------- 1 | // ref: https://umijs.org/config/ 2 | export default { 3 | treeShaking: true, 4 | routes: [ 5 | { 6 | path: '/', 7 | component: '../layouts/index', 8 | routes: [ 9 | { 10 | path: '/generator/QuantumultXScriptAddDeviceID', 11 | component: './generator/QuantumultXScriptAddDeviceID', 12 | }, 13 | { 14 | path: '/generator/QuantumultXScriptSubscriptionAddDeviceID', 15 | component: './generator/QuantumultXScriptSubscriptionAddDeviceID', 16 | }, 17 | { 18 | path: '/generator/QuantumultXScriptSubscriptionAddDeviceIDPreset', 19 | component: './generator/QuantumultXScriptSubscriptionAddDeviceIDPreset', 20 | }, 21 | { 22 | path: '/', 23 | component: '../pages/index', 24 | }, 25 | { 26 | path: '/404', 27 | component: '../pages/404', 28 | }, 29 | ], 30 | }, 31 | ], 32 | plugins: [ 33 | // ref: https://umijs.org/plugin/umi-plugin-react.html 34 | [ 35 | 'umi-plugin-react', 36 | { 37 | antd: true, 38 | dva: true, 39 | dynamicImport: false, 40 | title: 'CC', 41 | dll: true, 42 | routes: { 43 | exclude: [ 44 | /models\//, 45 | /services\//, 46 | /model\.(t|j)sx?$/, 47 | /service\.(t|j)sx?$/, 48 | /components\//, 49 | ], 50 | }, 51 | }, 52 | ], 53 | ] 54 | }; 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Singee 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 | # ConfigConverter 2 | [![Netlify Status](https://api.netlify.com/api/v1/badges/b8d38664-9076-461a-aa8e-d419ee365f9c/deploy-status)](https://app.netlify.com/sites/config-converter/deploys) 3 | 4 | 将各种代理软件的配置文件进行转换 5 | 6 | ## API Endpoint 7 | 8 | 使用方式请参考 [部署与说明文档](https://www.markeditor.com/file/get/eb581bd61fad7c345853e2ac1a5482f8?t=1574667122) 9 | 10 | - `/api/SurgeProfile2SurgeList` 将 Surge 配置文件转换为 List 11 | - `/api/QuantumultXScriptAddDeviceID` 将 QX 脚本中添加设备 ID 12 | - `/api/QuantumultXScriptSubscriptionAddDeviceID` 自动为 QX 脚本订阅添加设备 ID 行 13 | 14 | 15 | ## 自部署 16 | 17 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/ImSingee/ConfigConverter) 18 | 19 | 相关更新将在 [Singee 的日常](https://t.me/singee_daily) 中发布 20 | 21 | 有问题可以通过 [@Bryan](https://t.me/atbryanbot) 联系我 -------------------------------------------------------------------------------- /functions/QuantumultXScriptAddDeviceID.js: -------------------------------------------------------------------------------- 1 | const request = require('flyio'); 2 | const isUrl = require('is-url'); 3 | const { checkPassword } = require('./protect/password'); 4 | 5 | exports.handler = function (event, context, callback) { 6 | if (!checkPassword(event)) { 7 | return callback(null, { 8 | headers: { 9 | "Content-Type": "text/plain; charset=utf-8" 10 | }, 11 | statusCode: 401, 12 | body: "未提供密码或提供的密码不正确。" 13 | }); 14 | } 15 | 16 | const { queryStringParameters } = event; 17 | const url = queryStringParameters['src']; 18 | const deviceId = queryStringParameters['id']; 19 | 20 | console.log('url: ', url); 21 | console.log('deviceId: ', deviceId); 22 | 23 | if (!isUrl(url)) { 24 | console.log('URL is invlid'); 25 | return callback(null, { 26 | headers: { 27 | "Content-Type": "text/plain; charset=utf-8" 28 | }, 29 | statusCode: 400, 30 | body: "参数 src 无效,请检查是否提供了正确的脚本文件托管地址。" 31 | }); 32 | } 33 | if (!deviceId) { 34 | console.log('deviceId is not found'); 35 | return callback(null, { 36 | headers: { 37 | "Content-Type": "text/plain; charset=utf-8" 38 | }, 39 | statusCode: 400, 40 | body: "参数 id 无效,请检查是否提供了正确的设备 ID。" 41 | }); 42 | } 43 | 44 | request.get(url).then(({ data }) => { 45 | console.log('File fetched success.'); 46 | const result = `/**\n * @supported ${deviceId}\n */\n\n` + data; 47 | 48 | return callback(null, { 49 | headers: { 50 | "Content-Type": "text/plain; charset=utf-8" 51 | }, 52 | statusCode: 200, 53 | body: result 54 | }); 55 | }).catch(err => { 56 | return callback(null, { 57 | headers: { 58 | "Content-Type": "text/plain; charset=utf-8" 59 | }, 60 | statusCode: 400, 61 | body: err 62 | }); 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /functions/QuantumultXScriptSubscriptionAddDeviceID.js: -------------------------------------------------------------------------------- 1 | const request = require('flyio'); 2 | const isUrl = require('is-url'); 3 | 4 | const URLSafeBase64 = require('urlsafe-base64'); 5 | const QueryString = require('query-string'); 6 | const { checkPassword } = require('./protect/password'); 7 | 8 | const { URL: HOST, PRESET_NUMBER } = process.env; 9 | 10 | const PRESETS = {}; 11 | if (Number(PRESET_NUMBER) > 0) { 12 | for (let i = 1; i <= PRESET_NUMBER; i++) { 13 | PRESETS[i] = process.env[`PRESET_${i}`]; 14 | } 15 | } 16 | 17 | exports.handler = function (event, context, callback) { 18 | if (!checkPassword(event)) { 19 | return callback(null, { 20 | headers: { 21 | "Content-Type": "text/plain; charset=utf-8" 22 | }, 23 | statusCode: 401, 24 | body: "未提供密码或提供的密码不正确。" 25 | }); 26 | } 27 | 28 | const { queryStringParameters } = event; 29 | const preset = Number(queryStringParameters['preset']); 30 | const paramsB64 = queryStringParameters['b64']; 31 | const password = queryStringParameters['pwd'] || ''; 32 | let url, deviceId; 33 | 34 | if (isNaN(preset)) { 35 | if (paramsB64) { 36 | const params = QueryString.parse(URLSafeBase64.decode(paramsB64).toString()); 37 | url = params.src; 38 | deviceId = params.id; 39 | } else { 40 | url = queryStringParameters['src']; 41 | const deviceIdRaw = queryStringParameters['id']; 42 | if (deviceIdRaw) { 43 | deviceId = deviceIdRaw.replace(/\./g, ''); 44 | } else { 45 | const deviceIdB64 = queryStringParameters['idb64']; 46 | if (deviceIdB64) { 47 | deviceId = URLSafeBase64.decode(deviceIdB64).toString(); 48 | } else { 49 | console.log('deviceIdB64 is not found'); 50 | return callback(null, { 51 | headers: { 52 | "Content-Type": "text/plain; charset=utf-8" 53 | }, 54 | statusCode: 400, 55 | body: "参数 id (或其他替换结果)无效,请检查是否提供了正确的设备 ID。" 56 | }); 57 | } 58 | 59 | } 60 | 61 | } 62 | } else { 63 | if (preset > 0 && preset <= PRESET_NUMBER) { 64 | const paramsStr = PRESETS[preset]; 65 | if (!paramsStr) { 66 | return callback(null, { 67 | headers: { 68 | "Content-Type": "text/plain; charset=utf-8" 69 | }, 70 | statusCode: 400, 71 | body: "参数 preset 对应的预设不存在。" 72 | }); 73 | } 74 | 75 | const params = QueryString.parse(paramsStr); 76 | url = params.src; 77 | deviceId = params.id; 78 | } else { 79 | return callback(null, { 80 | headers: { 81 | "Content-Type": "text/plain; charset=utf-8" 82 | }, 83 | statusCode: 400, 84 | body: "参数 preset 不在允许的范围内。" 85 | }); 86 | } 87 | } 88 | 89 | console.log('url: ', url); 90 | console.log('deviceId: ', deviceId); 91 | 92 | 93 | if (!isUrl(url)) { 94 | console.log('URL is invlid'); 95 | return callback(null, { 96 | headers: { 97 | "Content-Type": "text/plain; charset=utf-8" 98 | }, 99 | statusCode: 400, 100 | body: "参数 src 无效,请检查是否提供了正确的脚本订阅文件托管地址。" 101 | }); 102 | } 103 | if (!deviceId) { 104 | console.log('deviceId is not found'); 105 | return callback(null, { 106 | headers: { 107 | "Content-Type": "text/plain; charset=utf-8" 108 | }, 109 | statusCode: 400, 110 | body: "参数 id (或其他替换结果)无效,请检查是否提供了正确的设备 ID。" 111 | }); 112 | } 113 | 114 | request.get(url).then(({ data }) => { 115 | console.log('File fetched success.'); 116 | const allLines = data.split('\n'); 117 | const resultLines = []; 118 | 119 | for (const singleLine of allLines) { 120 | const singleLineTrimed = singleLine.trim(); 121 | if (singleLineTrimed === '') { 122 | ;// Do nothing 123 | } else if (singleLineTrimed.startsWith('hostname')) { 124 | resultLines.push(singleLineTrimed); 125 | } else if (singleLineTrimed.startsWith('#')) { 126 | ;// Do nothing 127 | } else if (singleLineTrimed.startsWith(';')) { 128 | ;// Do nothing 129 | } else { 130 | const currentLineElements = singleLineTrimed.split(/\s+/); 131 | if (currentLineElements.length < 4) { 132 | resultLines.push(singleLineTrimed); 133 | } else if (currentLineElements[2] !== 'script-response-body') { 134 | resultLines.push(singleLineTrimed); 135 | } else { 136 | currentLineElements[3] = `${HOST}/api/QuantumultXScriptAddDeviceID?id=${encodeURIComponent(deviceId)}&src=${encodeURIComponent(currentLineElements[3])}`; 137 | if (password) { 138 | currentLineElements[3] += `&pwd=${encodeURIComponent(password)}`; 139 | } 140 | resultLines.push(currentLineElements.join(' ')); 141 | } 142 | } 143 | 144 | } 145 | 146 | return callback(null, { 147 | headers: { 148 | "Content-Type": "text/plain; charset=utf-8" 149 | }, 150 | statusCode: 200, 151 | body: `;deviceId = ${deviceId}\n;url = ${url}\n\n` + resultLines.join('\n') 152 | }); 153 | }).catch(err => { 154 | return callback(null, { 155 | headers: { 156 | "Content-Type": "text/plain; charset=utf-8" 157 | }, 158 | statusCode: 400, 159 | body: err 160 | }); 161 | }) 162 | } 163 | -------------------------------------------------------------------------------- /functions/SSR2Clash.js: -------------------------------------------------------------------------------- 1 | const fly = require("flyio"); 2 | const atob = require('atob'); 3 | const isUrl = require('is-url'); 4 | 5 | exports.handler = function(event, context, callback) { 6 | const { queryStringParameters } = event; 7 | 8 | const url = queryStringParameters['src']; 9 | 10 | if (!isUrl(url)) { 11 | return callback(null, { 12 | headers: { 13 | "Content-Type": "text/plain; charset=utf-8" 14 | }, 15 | statusCode: 400, 16 | body: "参数 src 无效,请检查是否提供了正确的节点订阅地址。" 17 | }); 18 | } 19 | 20 | fly.get(url).then(response => { 21 | const bodyDecoded = atob(response.data); 22 | const links = bodyDecoded.split('\n'); 23 | const filteredLinks = links.filter(link => { 24 | // Only support ss & ssr now 25 | if (link.startsWith('ss://')) return true; 26 | if (link.startsWith('ssr://')) return true; 27 | return false; 28 | }); 29 | 30 | if (filteredLinks.length == 0) { 31 | return callback(null, { 32 | headers: { 33 | "Content-Type": "text/plain; charset=utf-8" 34 | }, 35 | statusCode: 400, 36 | body: "订阅地址中没有节点信息。" 37 | }); 38 | } 39 | const processedLinks = new Array(); 40 | filteredLinks.forEach(link => { 41 | // 将订阅链接包装为对象 42 | if (link.startsWith('ss://')) { 43 | 44 | } 45 | 46 | // 过滤非 origin、plain 的 SSR 节点(Clash 暂时只支持 SS) 47 | 48 | // DEBUG 49 | processedLinks.push(link); 50 | }) 51 | 52 | if (processedLinks.length == 0) { 53 | return callback(null, { 54 | statusCode: 400, 55 | body: "订阅地址中没有节点信息。" 56 | }); 57 | } 58 | 59 | 60 | 61 | // DEBUG 62 | return callback(null, { 63 | headers: { 64 | "Content-Type": "text/plain; charset=utf-8" 65 | }, 66 | statusCode: 200, 67 | body: JSON.stringify(processedLinks) 68 | }); 69 | }).catch(error => { 70 | // 404 71 | if (error && !isNaN(error.status)) { 72 | return callback(null, { 73 | headers: { 74 | "Content-Type": "text/plain; charset=utf-8" 75 | }, 76 | statusCode: 400, 77 | body: "订阅地址网站出现了一个 " + String(error.status) + " 错误。" 78 | }); 79 | } 80 | 81 | // Unknown 82 | return callback(null, { 83 | headers: { 84 | "Content-Type": "text/plain; charset=utf-8" 85 | }, 86 | statusCode: 500, 87 | body: "Unexpected Error.\n" + JSON.stringify(error) 88 | }); 89 | }) 90 | 91 | } 92 | -------------------------------------------------------------------------------- /functions/SSRDecode.js: -------------------------------------------------------------------------------- 1 | const URLSafeBase64 = require('urlsafe-base64'); 2 | 3 | exports.handler = function(event, context, callback) { 4 | const { queryStringParameters } = event; 5 | const encodedStr = queryStringParameters['s']; 6 | if (!URLSafeBase64.validate(encodedStr)) { 7 | return callback(null, { 8 | headers: { 9 | "Content-Type": "text/plain; charset=utf-8" 10 | }, 11 | statusCode: 400, 12 | body: "参数无效" 13 | }) 14 | } 15 | 16 | const decodedStr = URLSafeBase64.decode(encodedStr).toString(); 17 | 18 | let host, port, protocol, method, obfs, base64password, password; 19 | let base64obfsparam, obfsparam, base64protoparam, protoparam, base64remarks, remarks, base64group, group, udpport, uot; 20 | 21 | const requiredParams = decodedStr.split(':'); 22 | if (requiredParams.length != 6) { 23 | return callback(null, { 24 | headers: { 25 | "Content-Type": "text/plain; charset=utf-8" 26 | }, 27 | statusCode: 400, 28 | body: "参数无效" 29 | }) 30 | } 31 | 32 | host = requiredParams[0]; 33 | port = requiredParams[1]; 34 | protocol = requiredParams[2]; 35 | method = requiredParams[3]; 36 | obfs = requiredParams[4]; 37 | 38 | const tempGroup = requiredParams[5].split('/?') 39 | base64password = tempGroup[0]; 40 | password = URLSafeBase64.decode(base64password).toString(); 41 | 42 | if (tempGroup.length > 1) { 43 | const optionalParams = tempGroup[1]; 44 | optionalParams.split('&').forEach(param => { 45 | const temp = param.split('='); 46 | let key = temp[0], value; 47 | if (temp.length > 1) { 48 | value = temp[1]; 49 | } 50 | 51 | if (value) { 52 | switch (key) { 53 | case 'obfsparam': 54 | base64obfsparam = value; 55 | obfsparam = URLSafeBase64.decode(base64obfsparam).toString(); 56 | break; 57 | case 'protoparam': 58 | base64protoparam = value; 59 | protoparam = URLSafeBase64.decode(base64protoparam).toString(); 60 | break; 61 | case 'remarks': 62 | base64remarks = value; 63 | remarks = URLSafeBase64.decode(base64remarks).toString(); 64 | break; 65 | case 'group': 66 | base64group = value; 67 | group = URLSafeBase64.decode(base64group).toString(); 68 | break; 69 | case 'udpport': 70 | udpport = value; 71 | break; 72 | case 'uot': 73 | uot = value; 74 | break; 75 | } 76 | } 77 | }) 78 | } 79 | 80 | result = { 81 | type: 'ss/ssr', 82 | host, port, protocol, method, obfs, base64password, password, 83 | base64obfsparam, obfsparam, base64protoparam, protoparam, base64remarks, remarks, base64group, group, udpport, uot 84 | } 85 | 86 | 87 | 88 | callback(null, { 89 | headers: { 90 | "Content-Type": "text/plain; charset=utf-8" 91 | }, 92 | statusCode: 200, 93 | body: JSON.stringify(result) 94 | }) 95 | } -------------------------------------------------------------------------------- /functions/SurgeProfile2SurgeList.js: -------------------------------------------------------------------------------- 1 | const request = require('flyio'); 2 | const isUrl = require('is-url'); 3 | const Surge = require('./ds/Surge'); 4 | const { checkPassword } = require('./protect/password'); 5 | 6 | exports.handler = function (event, context, callback) { 7 | if (!checkPassword(event)) { 8 | return callback(null, { 9 | headers: { 10 | "Content-Type": "text/plain; charset=utf-8" 11 | }, 12 | statusCode: 401, 13 | body: "未提供密码或提供的密码不正确。" 14 | }); 15 | } 16 | 17 | const { queryStringParameters } = event; 18 | const url = queryStringParameters['src']; 19 | const preset = queryStringParameters['preset']; 20 | const filter = queryStringParameters['filter']; 21 | const filterURL = queryStringParameters['filter_url']; 22 | 23 | console.log('url: ', url); 24 | 25 | if (!isUrl(url)) { 26 | console.log('URL is invlid'); 27 | return callback(null, { 28 | headers: { 29 | "Content-Type": "text/plain; charset=utf-8" 30 | }, 31 | statusCode: 400, 32 | body: "参数 src 无效,请检查是否提供了正确的 Surge Profile 托管地址。" 33 | }); 34 | } 35 | 36 | request.get(url).then(({ data }) => { 37 | console.log('Profile fetched success.'); 38 | const surge = new Surge(data); 39 | console.log('Build Surge object success.'); 40 | let result; 41 | 42 | if (preset) { 43 | result = surge.preset(preset); 44 | } else { 45 | result = surge.list(); 46 | if (filter) { 47 | result = result.filter(filter); 48 | } 49 | if (filterURL) { 50 | result = result.filterURL(filterURL); 51 | } 52 | result = result.generate(); 53 | } 54 | 55 | return callback(null, { 56 | headers: { 57 | "Content-Type": "text/plain; charset=utf-8" 58 | }, 59 | statusCode: 200, 60 | body: result 61 | }); 62 | }).catch(err => { 63 | return callback(null, { 64 | headers: { 65 | "Content-Type": "text/plain; charset=utf-8" 66 | }, 67 | statusCode: 400, 68 | body: err 69 | }); 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /functions/TestGetEnvInfo.js: -------------------------------------------------------------------------------- 1 | const { checkPassword } = require('./protect/password'); 2 | 3 | exports.handler = function (event, context, callback) { 4 | if (!checkPassword(event, true)) { 5 | return callback(null, { 6 | headers: { 7 | "Content-Type": "text/plain; charset=utf-8" 8 | }, 9 | statusCode: 401, 10 | body: "未提供密码或提供的密码不正确。" 11 | }); 12 | } 13 | 14 | const env = process.env; 15 | 16 | return callback(null, { 17 | headers: { 18 | "Content-Type": "text/plain; charset=utf-8" 19 | }, 20 | statusCode: 200, 21 | body: JSON.stringify({ event, context, env }) 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /functions/ds/Surge.js: -------------------------------------------------------------------------------- 1 | const ini = require('ini'); 2 | 3 | class Names { 4 | constructor(names, Proxy) { 5 | this.names = names; 6 | this.Proxy = Proxy; 7 | } 8 | 9 | generate() { 10 | let result = ''; 11 | for (let name of this.names) { 12 | result += `${name} = ${this.Proxy[name]}\n` 13 | } 14 | return result; 15 | } 16 | 17 | filter(keyword) { 18 | const keywords = keyword.split('+'); 19 | const names = this.names.filter(name => { 20 | for (const kw of keywords) { 21 | if (name.indexOf(kw) !== -1) { 22 | return true; 23 | } 24 | } 25 | }) 26 | 27 | return new Names(names, this.Proxy); 28 | } 29 | 30 | filterURL(keyword) { 31 | const keywords = keyword.split('+'); 32 | const names = this.names.filter(name => { 33 | const splitResult = this.Proxy[name].split(','); 34 | const url = splitResult[1].trim(); 35 | 36 | for (const kw of keywords) { 37 | if (url.indexOf(kw) !== -1) { 38 | return true; 39 | } 40 | } 41 | }) 42 | 43 | return new Names(names, this.Proxy); 44 | } 45 | 46 | static join(...exists) { 47 | const names = []; 48 | const set = new Set(); 49 | for (const exist of exists) { 50 | exist.names.map(name => { 51 | if (!set.has(name)) { 52 | names.push(name); 53 | set.add(name); 54 | } 55 | }) 56 | } 57 | 58 | return new Names(names, exists[0].Proxy); 59 | } 60 | } 61 | 62 | class Surge { 63 | constructor(profile) { 64 | const config = ini.parse(profile); 65 | const { Proxy } = config; 66 | // console.log(Proxy); 67 | // console.log(Object.keys(Proxy)); 68 | this.Proxy = Proxy; 69 | } 70 | 71 | list() { 72 | return new Names(Object.keys(this.Proxy), this.Proxy); 73 | } 74 | 75 | preset(presetName) { 76 | if (presetName === 'netflix') { 77 | // default: HK+MO+TW 78 | return this.list().filter('HKT+HKBN+CTM+江苏中转+HINET').generate(); 79 | } else if (presetName === 'netflix_hk') { 80 | return this.list().filter('HKT+HKBN').generate(); 81 | } else if (presetName === 'netflix_mo') { 82 | return this.list().filter('CTM+江苏中转').generate(); 83 | } else if (presetName === 'netflix_tw') { 84 | return this.list().filter('HINET').generate(); 85 | } else if (presetName === 'netflix_jp') { 86 | return this.list().filter('IDCF+软银').generate(); 87 | } else if (presetName === 'netflix_us') { 88 | return this.list().filter('美国').filter('CN2').generate(); 89 | } else if (presetName === 'netflix_iplc') { 90 | const listBase = this.list().filter('IPLC'); 91 | return Names.join(listBase.filter('深港+沪港'), listBase.filterURL('hanggang.001+hanggang.002')).generate(); 92 | } 93 | 94 | return this.list().generate(); 95 | } 96 | 97 | } 98 | 99 | module.exports = Surge; -------------------------------------------------------------------------------- /functions/protect/password.js: -------------------------------------------------------------------------------- 1 | const { FORCE_PASSWORD: FORCE_PASSWORD_str, PASSWORD } = process.env; 2 | const FORCE_PASSWORD = FORCE_PASSWORD_str === 'True' ? true : false; 3 | 4 | function checkPassword(event, force = false) { 5 | const { queryStringParameters } = event; 6 | const { pwd: password } = queryStringParameters; 7 | 8 | if (!force && !FORCE_PASSWORD) return true; 9 | 10 | return password === PASSWORD; 11 | } 12 | 13 | module.exports = { checkPassword }; -------------------------------------------------------------------------------- /functions/testDownload.js: -------------------------------------------------------------------------------- 1 | const request = require('request-promise'); 2 | const isUrl = require('is-url'); 3 | 4 | exports.handler = function (event, context, callback) { 5 | const { queryStringParameters } = event; 6 | const url = queryStringParameters['src']; 7 | 8 | console.log('url: ', url); 9 | 10 | if (!isUrl(url)) { 11 | console.log('URL is invlid'); 12 | return callback(null, { 13 | headers: { 14 | "Content-Type": "text/plain; charset=utf-8" 15 | }, 16 | statusCode: 400, 17 | body: "参数 src 无效,请检查是否提供了正确的网址。" 18 | }); 19 | } 20 | 21 | request.get(url).then(data => { 22 | console.log('File fetched success.'); 23 | 24 | return callback(null, { 25 | headers: { 26 | "Content-Type": "text/plain; charset=utf-8" 27 | }, 28 | statusCode: 200, 29 | body: data 30 | }); 31 | }).catch(err => { 32 | console.error('Error', err); 33 | return callback(null, { 34 | headers: { 35 | "Content-Type": "text/plain; charset=utf-8" 36 | }, 37 | statusCode: 400, 38 | body: '请求错误,请查看日志' 39 | }); 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /mock/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImSingee/ConfigConverter/56d2e380925b5d57d6a400a61905492a2a430a08/mock/.gitkeep -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | functions = "./functions" 3 | command = "yarn build" 4 | 5 | [build.environment] 6 | NODE_VERSION = "12" 7 | 8 | [template.environment] 9 | FORCE_PASSWORD = "设置为 True 表示强制设置密码" 10 | PASSWORD = "设置一个密码" 11 | PRESET_NUMBER = "启用的预设数量" 12 | PRESET_1 = "预设 1 的参数" 13 | PRESET_2 = "预设 2 的参数" 14 | PRESET_3 = "预设 3 的参数" 15 | PRESET_4 = "预设 4 的参数" 16 | PRESET_5 = "预设 5 的参数" 17 | 18 | [[redirects]] 19 | from = "/api/*" 20 | to = "/.netlify/functions/:splat" 21 | status = 200 22 | force = true 23 | 24 | [[redirects]] 25 | from = "/umi.css" 26 | to = "/dist/umi.css" 27 | status = 200 28 | 29 | [[redirects]] 30 | from = "/umi.js" 31 | to = "/dist/umi.js" 32 | status = 200 33 | 34 | [[redirects]] 35 | from = "/*" 36 | to = "/dist/index.html" 37 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "umi dev", 4 | "build": "umi build", 5 | "test": "umi test", 6 | "lint": "eslint --ext .js src mock tests", 7 | "precommit": "lint-staged" 8 | }, 9 | "dependencies": { 10 | "antd": "^3.19.5", 11 | "atob": "^2.1.2", 12 | "copy-to-clipboard": "^3.2.0", 13 | "dva": "^2.6.0-beta.6", 14 | "flyio": "^0.6.14", 15 | "ini": "^1.3.5", 16 | "is-url": "^1.2.4", 17 | "query-string": "^6.9.0", 18 | "react": "^16.8.6", 19 | "react-dom": "^16.8.6", 20 | "request": "^2.88.0", 21 | "request-promise": "^4.2.5", 22 | "urlsafe-base64": "^1.0.0" 23 | }, 24 | "devDependencies": { 25 | "babel-eslint": "^9.0.0", 26 | "eslint": "^5.4.0", 27 | "eslint-config-umi": "^1.4.0", 28 | "eslint-plugin-flowtype": "^2.50.0", 29 | "eslint-plugin-import": "^2.14.0", 30 | "eslint-plugin-jsx-a11y": "^5.1.1", 31 | "eslint-plugin-react": "^7.11.1", 32 | "husky": "^0.14.3", 33 | "lint-staged": "^7.2.2", 34 | "react-test-renderer": "^16.7.0", 35 | "umi": "^2.7.7", 36 | "umi-plugin-react": "^1.8.4" 37 | }, 38 | "lint-staged": { 39 | "*.{js,jsx}": [ 40 | "eslint --fix", 41 | "git add" 42 | ] 43 | }, 44 | "engines": { 45 | "node": ">=8.0.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | export const dva = { 2 | config: { 3 | onError(err) { 4 | err.preventDefault(); 5 | console.error(err.message); 6 | }, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/assets/yay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImSingee/ConfigConverter/56d2e380925b5d57d6a400a61905492a2a430a08/src/assets/yay.jpg -------------------------------------------------------------------------------- /src/global.css: -------------------------------------------------------------------------------- 1 | html, body, #root { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/layouts/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import BasicLayout from '..'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | describe('Layout: BasicLayout', () => { 5 | it('Render correctly', () => { 6 | const wrapper = renderer.create(); 7 | expect(wrapper.root.children.length).toBe(1); 8 | const outerLayer = wrapper.root.children[0]; 9 | expect(outerLayer.type).toBe('div'); 10 | const title = outerLayer.children[0]; 11 | expect(title.type).toBe('h1'); 12 | expect(title.children[0]).toBe('Yay! Welcome to umi!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/layouts/index.css: -------------------------------------------------------------------------------- 1 | .logo { 2 | color: white; 3 | height: 32px; 4 | line-height: 32px; 5 | margin: 16px; 6 | text-align: center; 7 | } -------------------------------------------------------------------------------- /src/layouts/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import styles from './index.css'; 3 | 4 | import { Layout, Menu, Icon } from 'antd'; 5 | 6 | const { Content, Footer, Sider } = Layout; 7 | const { SubMenu } = Menu; 8 | 9 | 10 | class BasicLayoutFuture extends React.Component { 11 | state = { 12 | collapsed: false, 13 | }; 14 | 15 | onCollapse = collapsed => { 16 | this.setState({ collapsed }); 17 | }; 18 | 19 | render() { 20 | return ( 21 | 22 | 23 |
CC
24 | 25 | 26 | 27 | Option 1 28 | 29 | 30 | 31 | Option 2 32 | 33 | 37 | 38 | User 39 | 40 | } 41 | > 42 | Tom 43 | Bill 44 | Alex 45 | 46 | 50 | 51 | Team 52 | 53 | } 54 | > 55 | Team 1 56 | Team 2 57 | 58 | 59 | 60 | File 61 | 62 | 63 |
64 | 65 | 66 | {this.props.children} 67 | 68 |
ConfigConverter ©2019 Created by Singee
69 |
70 |
71 | ); 72 | } 73 | } 74 | 75 | function BasicLayout(props) { 76 | return ( 77 | 78 | 79 | {props.children} 80 | 81 |
ConfigConverter ©2019 Created by Singee
82 |
83 | ); 84 | } 85 | 86 | export default BasicLayout; 87 | -------------------------------------------------------------------------------- /src/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ImSingee/ConfigConverter/56d2e380925b5d57d6a400a61905492a2a430a08/src/models/.gitkeep -------------------------------------------------------------------------------- /src/pages/404.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | font-family: Georgia, sans-serif; 4 | margin-top: 4em; 5 | text-align: center; 6 | font-size: 1.2em; 7 | margin: 40vh 0 0; 8 | line-height: 1.5em; 9 | } -------------------------------------------------------------------------------- /src/pages/404.js: -------------------------------------------------------------------------------- 1 | import styles from './index.css'; 2 | 3 | export default function() { 4 | return ( 5 |
6 |
404 Not Found
7 |
8 | You may want to 9 | Move to Homepage 10 | or 11 | Move to Github 12 | 13 |
14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/pages/__tests__/index.test.js: -------------------------------------------------------------------------------- 1 | import Index from '..'; 2 | import renderer from 'react-test-renderer'; 3 | 4 | 5 | describe('Page: index', () => { 6 | it('Render correctly', () => { 7 | const wrapper = renderer.create(); 8 | expect(wrapper.root.children.length).toBe(1); 9 | const outerLayer = wrapper.root.children[0]; 10 | expect(outerLayer.type).toBe('div'); 11 | expect(outerLayer.children.length).toBe(2); 12 | 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/pages/generator/QuantumultXScriptAddDeviceID.css: -------------------------------------------------------------------------------- 1 | .form { 2 | padding: 24px; 3 | background: #fbfbfb; 4 | border: 1px solid #d9d9d9; 5 | border-radius: 6px; 6 | } 7 | 8 | .normal { 9 | margin: 1em 0; 10 | } -------------------------------------------------------------------------------- /src/pages/generator/QuantumultXScriptAddDeviceID.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { Breadcrumb, Form, Row, Col, Input, Button, message } from 'antd'; 4 | import styles from './QuantumultXScriptAddDeviceID.css'; 5 | import URLSafeBase64 from 'urlsafe-base64'; 6 | import copy from 'copy-to-clipboard'; 7 | 8 | class QuantumultXScriptAddDeviceIDGenerateForm extends React.Component { 9 | state = { 10 | showResult: false, 11 | result: '' 12 | }; 13 | 14 | getFields() { 15 | const { getFieldDecorator } = this.props.form; 16 | 17 | return ( 18 | <> 19 | 20 | 21 | 22 | {getFieldDecorator('password', { 23 | required: false, 24 | rules: [ 25 | { 26 | whitespace: true, 27 | message: '密码不能为纯空格', 28 | }, 29 | ] 30 | })()} 31 | 32 | 33 | 34 | 35 | 36 | 37 | {getFieldDecorator('id', { 38 | rules: [ 39 | { 40 | required: true, 41 | whitespace: true, 42 | message: '请输入设备 ID', 43 | }, 44 | ], 45 | })()} 46 | 47 | 48 | 49 | 50 | 51 | 52 | {getFieldDecorator('url', { 53 | rules: [ 54 | { 55 | required: true, 56 | message: '请输入脚本链接', 57 | }, 58 | { 59 | type: 'url', 60 | message: '请输入有效的网址', 61 | } 62 | ], 63 | })()} 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | handleConfirm = e => { 72 | e.preventDefault(); 73 | this.props.form.validateFields((errors, values) => { 74 | if (errors) { 75 | this.setState({showResult: false}) 76 | } else { 77 | const id = values.id.trim(); 78 | const url = values.url.trim(); 79 | const password = values.password; 80 | 81 | let result = `${document.location.origin}/api/QuantumultXScriptAddDeviceID?id=${encodeURIComponent(id)}&src=${encodeURIComponent(url)}`; 82 | 83 | if (password) { 84 | result += `&pwd=${password.trim()}`; 85 | } 86 | 87 | this.setState({ 88 | showResult: true, 89 | result 90 | }); 91 | 92 | copy(result); 93 | message.info('新脚本网址已生成并复制到剪贴板'); 94 | } 95 | }); 96 | }; 97 | 98 | handleReset = () => { 99 | this.props.form.resetFields(); 100 | }; 101 | 102 | render() { 103 | const grids = {xs: 24, sm: 18, md:12, lg:9, xl:9, xxl: 9} 104 | 105 | return ( 106 | <> 107 | 108 | 109 |
110 | {this.getFields()} 111 | 112 | 113 | 114 |
115 | 116 |
117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | ); 129 | } 130 | } 131 | 132 | const WrappedQuantumultXScriptAddDeviceIDGenerateForm = Form.create({ name: 'generate' })(QuantumultXScriptAddDeviceIDGenerateForm); 133 | 134 | export default function() { 135 | return ( 136 |
137 | 138 | Generator 139 | QuantumultXScriptAddDeviceID 140 | 141 |

QuantumultX 脚本订阅批量生成设备 ID

142 |
具体使用方式请参考 https://t.me/singee_daily
143 | 144 |
145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /src/pages/generator/QuantumultXScriptSubscriptionAddDeviceID.css: -------------------------------------------------------------------------------- 1 | .form { 2 | padding: 24px; 3 | background: #fbfbfb; 4 | border: 1px solid #d9d9d9; 5 | border-radius: 6px; 6 | } 7 | 8 | .normal { 9 | margin: 1em 0; 10 | } -------------------------------------------------------------------------------- /src/pages/generator/QuantumultXScriptSubscriptionAddDeviceID.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { Breadcrumb, Form, Row, Col, Input, Button, message } from 'antd'; 4 | import styles from './QuantumultXScriptSubscriptionAddDeviceID.css'; 5 | import URLSafeBase64 from 'urlsafe-base64'; 6 | import copy from 'copy-to-clipboard'; 7 | 8 | class QuantumultXScriptSubscriptionAddDeviceIDGenerateForm extends React.Component { 9 | state = { 10 | showResult: false, 11 | result: '' 12 | }; 13 | 14 | getFields() { 15 | const { getFieldDecorator } = this.props.form; 16 | 17 | return ( 18 | <> 19 | 20 | 21 | 22 | {getFieldDecorator('password', { 23 | required: false, 24 | rules: [ 25 | { 26 | whitespace: true, 27 | message: '密码不能为纯空格', 28 | }, 29 | ] 30 | })()} 31 | 32 | 33 | 34 | 35 | 36 | 37 | {getFieldDecorator('id', { 38 | rules: [ 39 | { 40 | required: true, 41 | whitespace: true, 42 | message: '请输入设备 ID', 43 | }, 44 | ], 45 | })()} 46 | 47 | 48 | 49 | 50 | 51 | 52 | {getFieldDecorator('url', { 53 | rules: [ 54 | { 55 | required: true, 56 | message: '请输入订阅链接', 57 | }, 58 | { 59 | type: 'url', 60 | message: '请输入有效的网址', 61 | } 62 | ], 63 | })()} 64 | 65 | 66 | 67 | 68 | ); 69 | } 70 | 71 | handleConfirm = e => { 72 | e.preventDefault(); 73 | this.props.form.validateFields((errors, values) => { 74 | if (errors) { 75 | console.log(errors); 76 | this.setState({showResult: false}) 77 | } else { 78 | const id = values.id.trim(); 79 | const url = values.url; 80 | const password = values.password; 81 | 82 | const paramsB64 = URLSafeBase64.encode(Buffer.from(`id=${encodeURIComponent(id)}&src=${encodeURIComponent(url)}`)); 83 | let result = `${document.location.origin}/api/QuantumultXScriptSubscriptionAddDeviceID?b64=${paramsB64}`; 84 | 85 | if (password) { 86 | result += `&pwd=${password.trim()}`; 87 | } 88 | 89 | this.setState({ 90 | showResult: true, 91 | result 92 | }); 93 | 94 | copy(result); 95 | message.info('订阅网址已生成并复制到剪贴板'); 96 | } 97 | }); 98 | }; 99 | 100 | handleReset = () => { 101 | this.props.form.resetFields(); 102 | }; 103 | 104 | render() { 105 | const grids = {xs: 24, sm: 18, md:12, lg:9, xl:9, xxl: 9} 106 | 107 | return ( 108 | <> 109 | 110 | 111 |
112 | {this.getFields()} 113 | 114 | 115 | 116 |
117 | 118 |
119 |
120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | ); 131 | } 132 | } 133 | 134 | const WrappedQuantumultXScriptSubscriptionAddDeviceIDGenerateForm = Form.create({ name: 'generate' })(QuantumultXScriptSubscriptionAddDeviceIDGenerateForm); 135 | 136 | export default function() { 137 | return ( 138 |
139 | 140 | Generator 141 | QuantumultXScriptSubscriptionAddDeviceID 142 | 143 |

QuantumultX 脚本订阅批量生成设备 ID

144 |
具体使用方式请参考 https://t.me/singee_daily
145 | 146 |
147 | ); 148 | } 149 | -------------------------------------------------------------------------------- /src/pages/generator/QuantumultXScriptSubscriptionAddDeviceIDPreset.css: -------------------------------------------------------------------------------- 1 | .form { 2 | padding: 24px; 3 | background: #fbfbfb; 4 | border: 1px solid #d9d9d9; 5 | border-radius: 6px; 6 | } 7 | 8 | .normal { 9 | margin: 1em 0; 10 | } -------------------------------------------------------------------------------- /src/pages/generator/QuantumultXScriptSubscriptionAddDeviceIDPreset.js: -------------------------------------------------------------------------------- 1 | 2 | import React from 'react'; 3 | import { Breadcrumb, Form, Row, Col, Input, Button, message } from 'antd'; 4 | import styles from './QuantumultXScriptSubscriptionAddDeviceIDPreset.css'; 5 | import URLSafeBase64 from 'urlsafe-base64'; 6 | import copy from 'copy-to-clipboard'; 7 | 8 | class QuantumultXScriptSubscriptionAddDeviceIDPresetGenerateForm extends React.Component { 9 | state = { 10 | showResult: false, 11 | result: '' 12 | }; 13 | 14 | getFields() { 15 | const { getFieldDecorator } = this.props.form; 16 | 17 | return ( 18 | <> 19 | 20 | 21 | 22 | {getFieldDecorator('id', { 23 | rules: [ 24 | { 25 | required: true, 26 | whitespace: true, 27 | message: '请输入设备 ID', 28 | }, 29 | ], 30 | })()} 31 | 32 | 33 | 34 | 35 | 36 | 37 | {getFieldDecorator('url', { 38 | rules: [ 39 | { 40 | required: true, 41 | message: '请输入订阅链接', 42 | }, 43 | { 44 | type: 'url', 45 | message: '请输入有效的网址', 46 | } 47 | ], 48 | })()} 49 | 50 | 51 | 52 | 53 | ); 54 | } 55 | 56 | handleConfirm = e => { 57 | e.preventDefault(); 58 | this.props.form.validateFields((errors, values) => { 59 | if (errors) { 60 | this.setState({showResult: false}) 61 | } else { 62 | const id = values.id.trim(); 63 | const url = values.url.trim(); 64 | 65 | const params = `id=${encodeURIComponent(id)}&src=${encodeURIComponent(url)}`; 66 | 67 | this.setState({ 68 | showResult: true, 69 | result: params 70 | }); 71 | 72 | copy(params); 73 | message.info('预设参数结果已生成并复制到剪贴板'); 74 | } 75 | }); 76 | }; 77 | 78 | handleReset = () => { 79 | this.props.form.resetFields(); 80 | }; 81 | 82 | render() { 83 | const grids = {xs: 24, sm: 18, md:12, lg:9, xl:9, xxl: 9} 84 | 85 | return ( 86 | <> 87 | 88 | 89 |
90 | {this.getFields()} 91 | 92 | 93 | 94 |
95 | 96 |
97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | ); 109 | } 110 | } 111 | 112 | const WrappedQuantumultXScriptSubscriptionAddDeviceIDPresetGenerateForm = Form.create({ name: 'generate' })(QuantumultXScriptSubscriptionAddDeviceIDPresetGenerateForm); 113 | 114 | export default function() { 115 | return ( 116 |
117 | 118 | Generator 119 | QuantumultXScriptSubscriptionAddDeviceID 120 | Preset 121 | 122 |

QuantumultX 脚本订阅批量生成设备 ID 预设

123 |
具体使用方式请参考 https://t.me/singee_daily
124 | 125 |
126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /src/pages/index.css: -------------------------------------------------------------------------------- 1 | 2 | .normal { 3 | font-family: Georgia, sans-serif; 4 | margin-top: 4em; 5 | text-align: center; 6 | font-size: 1.2em; 7 | margin: 40vh 0 0; 8 | line-height: 1.5em; 9 | } -------------------------------------------------------------------------------- /src/pages/index.js: -------------------------------------------------------------------------------- 1 | import styles from './index.css'; 2 | 3 | export default function() { 4 | return ( 5 |
6 |
The home page is developing.
7 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 不是真实的 webpack 配置,仅为兼容 webstorm 和 intellij idea 代码跳转 3 | * ref: https://github.com/umijs/umi/issues/1109#issuecomment-423380125 4 | */ 5 | 6 | module.exports = { 7 | resolve: { 8 | alias: { 9 | '@': require('path').resolve(__dirname, 'src'), 10 | }, 11 | }, 12 | }; 13 | --------------------------------------------------------------------------------