├── static └── favicon.ico ├── assets ├── images │ ├── demo.jpg │ └── gefei.png └── icons │ ├── close.svg │ └── drogdown.svg ├── plugins ├── axios.js └── filters.js ├── .prettierrc ├── jsconfig.json ├── .editorconfig ├── vercel.json ├── .eslintrc.js ├── README.md ├── nuxt.config.js ├── .gitignore ├── package.json ├── .prettierignore ├── tailwind.config.js ├── pages └── index.vue ├── layouts └── default.vue └── components └── BaseTable.vue /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kunsect/roi-calculator/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /assets/images/demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kunsect/roi-calculator/HEAD/assets/images/demo.jpg -------------------------------------------------------------------------------- /assets/images/gefei.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kunsect/roi-calculator/HEAD/assets/images/gefei.png -------------------------------------------------------------------------------- /plugins/axios.js: -------------------------------------------------------------------------------- 1 | export default function ({ $axios }) { 2 | $axios.onRequest(config => { 3 | console.log('Making request to ' + config.url) 4 | }) 5 | } -------------------------------------------------------------------------------- /plugins/filters.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | const formatNumber = (value) => { 4 | if (!value) return '0' 5 | return parseInt(value).toLocaleString('en-US') 6 | } 7 | 8 | Vue.filter('formatNumber', formatNumber) 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": true, 6 | "semi": false, 7 | "trailingComma": "none", 8 | "endOfLine": "auto", 9 | "bracketSameLine": false 10 | } 11 | -------------------------------------------------------------------------------- /assets/icons/close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "~/*": ["./*"], 6 | "@/*": ["./*"], 7 | "~~/*": ["./*"], 8 | "@@/*": ["./*"] 9 | } 10 | }, 11 | "exclude": ["node_modules", ".nuxt", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # 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 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "nuxt.config.js", 6 | "use": "@nuxtjs/vercel-builder", 7 | "config": { 8 | "serverFiles": ["package.json", "./.nuxt/dist/sitemap-routes.json"] 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /assets/icons/drogdown.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true, 6 | }, 7 | parserOptions: { 8 | parser: '@babel/eslint-parser', 9 | requireConfigFile: false, 10 | 11 | }, 12 | extends: ['@nuxtjs', 'plugin:nuxt/recommended', 'prettier'], 13 | plugins: [], 14 | // add your custom rules here 15 | rules: { 16 | 'vue/no-v-html': 'off', 17 | 'no-console': 'off' 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keywords ROI Calculator - 导入 Semrush Excel 自动计算关键词优化回报率 2 | 3 | 在 Semrush 搜索关键词并导出 xlsx 之后,可以导入到该项目,会通过公式自动计算每个关键词对应的优化回报率。Excel 均在本地设备处理,不会上传到任何远程服务器。 4 | 5 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/Kunsect/roi-calculator) 6 | 7 | [![ROI Calculator Demo](/assets/images/demo.jpg)](https://roi-calculator-flame.vercel.app/) 8 | 9 | ## 贡献 10 | 11 | 欢迎您参与项目的优化和改进。如果你想为本项目做出贡献,或者有任何问题和建议,请在 Issues 中提出。 12 | 13 | ## 版权和许可 14 | 15 | 本项目采用 [MIT License](https://choosealicense.com/licenses/mit/). 16 | 17 | ## 致谢 18 | 19 | 感谢哥飞提出的建议和 ROI 计算逻辑,哥飞的社群主要面向技术开发者、产品经理、设计师等人群,大家一起讨论独立开发、出海产品、流量获取、流量变现等话题,欢迎关注: 20 | 21 | gefei wechat -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | export default { 4 | mode: 'universal', 5 | 6 | // Global page headers: https://go.nuxtjs.dev/config-head 7 | head: { 8 | title: 'roi-calculator-nuxt', 9 | htmlAttrs: { 10 | lang: 'en', 11 | }, 12 | meta: [ 13 | { charset: 'utf-8' }, 14 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 15 | { hid: 'description', name: 'description', content: '' }, 16 | { name: 'format-detection', content: 'telephone=no' }, 17 | ], 18 | link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }], 19 | }, 20 | 21 | // Global CSS: https://go.nuxtjs.dev/config-css 22 | css: [], 23 | 24 | // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins 25 | plugins: [ 26 | '~/plugins/axios', 27 | '~/plugins/filters.js' 28 | ], 29 | 30 | // Auto import components: https://go.nuxtjs.dev/config-components 31 | components: true, 32 | 33 | // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules 34 | buildModules: [ 35 | '@nuxtjs/eslint-module', 36 | '@nuxtjs/tailwindcss', 37 | '@nuxtjs/google-gtag', 38 | ...(process.env.NODE_ENV !== 'production' ? ['@nuxtjs/proxy'] : []), 39 | ], 40 | 41 | // Modules: https://go.nuxtjs.dev/config-modules 42 | modules: [ 43 | '@nuxtjs/axios', 44 | '@nuxtjs/sitemap' 45 | ], 46 | 47 | build: {}, 48 | 49 | proxy: { 50 | '/api/': { 51 | target: process.env.DEV_URL, 52 | pathRewrite: { '^/api/': '/' } 53 | }, 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Node template 3 | # Logs 4 | /logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | *.pid.lock 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # nyc test coverage 23 | .nyc_output 24 | 25 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 26 | .grunt 27 | 28 | # Bower dependency directory (https://bower.io/) 29 | bower_components 30 | 31 | # node-waf configuration 32 | .lock-wscript 33 | 34 | # Compiled binary addons (https://nodejs.org/api/addons.html) 35 | build/Release 36 | 37 | # Dependency directories 38 | node_modules/ 39 | jspm_packages/ 40 | 41 | # TypeScript v1 declaration files 42 | typings/ 43 | 44 | # Optional npm cache directory 45 | .npm 46 | 47 | # Optional eslint cache 48 | .eslintcache 49 | 50 | # Optional REPL history 51 | .node_repl_history 52 | 53 | # Output of 'npm pack' 54 | *.tgz 55 | 56 | # Yarn Integrity file 57 | .yarn-integrity 58 | 59 | # dotenv environment variables file 60 | .env 61 | 62 | # parcel-bundler cache (https://parceljs.org/) 63 | .cache 64 | 65 | # next.js build output 66 | .next 67 | 68 | # nuxt.js build output 69 | .nuxt 70 | 71 | # Nuxt generate 72 | dist 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless 79 | 80 | # IDE / Editor 81 | .idea 82 | 83 | # Service worker 84 | sw.* 85 | 86 | # macOS 87 | .DS_Store 88 | 89 | # Vim swap files 90 | *.swp 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roi-calculator-nuxt", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt generate", 8 | "start": "nuxt start", 9 | "lint:js": "eslint --ext \".js,.vue\" --ignore-path .gitignore .", 10 | "lint:style": "stylelit", 11 | "generate": "nuxt gent \"**/*.{css,scss,sass,html,vue}\" --ignore-path .gitignore", 12 | "lint:prettier": "prettier --check .", 13 | "lint": "npm run lint:js && npm run lint:style && npm run lint:prettier", 14 | "lintfix": "prettier --write --list-different . && npm run lint:js -- --fix && npm run lint:style -- --fix" 15 | }, 16 | "dependencies": { 17 | "@nuxtjs/axios": "^5.13.6", 18 | "@nuxtjs/proxy": "^2.1.0", 19 | "@nuxtjs/sitemap": "^2.4.0", 20 | "@tailwindcss/aspect-ratio": "^0.4.2", 21 | "core-js": "^3.25.3", 22 | "dotenv": "^16.3.1", 23 | "node-fetch-native": "^1.4.0", 24 | "nuxt": "^2.15.8", 25 | "vue": "^2.7.10", 26 | "vue-server-renderer": "^2.7.10", 27 | "vue-template-compiler": "^2.7.10", 28 | "xlsx": "^0.18.5" 29 | }, 30 | "devDependencies": { 31 | "@babel/eslint-parser": "^7.19.1", 32 | "@nuxtjs/eslint-config": "^11.0.0", 33 | "@nuxtjs/eslint-module": "^3.1.0", 34 | "@nuxtjs/google-gtag": "^1.0.4", 35 | "@nuxtjs/tailwindcss": "^5.3.3", 36 | "@nuxtjs/vercel-builder": "^0.24.0", 37 | "eslint": "^8.24.0", 38 | "eslint-config-prettier": "^8.5.0", 39 | "eslint-plugin-nuxt": "^4.0.0", 40 | "eslint-plugin-vue": "^9.5.1", 41 | "file-saver": "^2.0.5", 42 | "postcss": "^8.4.17", 43 | "postcss-html": "^1.5.0", 44 | "prettier": "^2.7.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | ### 2 | # Place your Prettier ignore content here 3 | 4 | ### 5 | # .gitignore content is duplicated here due to https://github.com/prettier/prettier/issues/8506 6 | 7 | # Created by .ignore support plugin (hsz.mobi) 8 | ### Node template 9 | # Logs 10 | /logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # parcel-bundler cache (https://parceljs.org/) 69 | .cache 70 | 71 | # next.js build output 72 | .next 73 | 74 | # nuxt.js build output 75 | .nuxt 76 | 77 | # Nuxt generate 78 | dist 79 | 80 | # vuepress build output 81 | .vuepress/dist 82 | 83 | # Serverless directories 84 | .serverless 85 | 86 | # IDE / Editor 87 | .idea 88 | 89 | # Service worker 90 | sw.* 91 | 92 | # macOS 93 | .DS_Store 94 | 95 | # Vim swap files 96 | *.swp 97 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["*.{html,js}"], 3 | 4 | theme: { 5 | extend: { 6 | colors: { 7 | 'dark-blue': '#12151e', // 这是 dark-blue 的基础色值,你可以根据实际情况进行调整。 8 | }, 9 | backgroundColor: { 10 | 'dark-blue': '#12151e', 11 | 'medium-blue': '#2c3349', 12 | 'medium-blueHover': '#303850', 13 | 'dark-medium-blue': '#222839', 14 | 'orange': '#cc5514', 15 | 'orangeHover': '#df5c16', 16 | 'grey': '#8f94a3' 17 | }, 18 | gradientColorStops: theme => ({ 19 | 'from-dark-blue-0': theme('colors.dark-blue.0', 'rgba(#12151e, 0)'), 20 | 'to-dark-blue-80': theme('colors.dark-blue.80', 'rgba(#12151e, 0.8)') 21 | }), 22 | borderColor: { 23 | 'grey': '#8f94a3', 24 | 'orange': '#cc5514', 25 | 'medium-blue': '#2c3349' 26 | }, 27 | textColor: { 28 | 'grey': '#8f94a3', 29 | 'orange': '#cc5514' 30 | }, 31 | boxShadow: { 32 | 'button-secondary': '0px 2px 3px 0px rgba(0,0,0,.16), inset 0px 1px 2px 0px rgba(242,242,242,.16), inset 0px -1px 2px 0px rgba(0,0,0,.2)', 33 | 'button-primary': 'inset 0px 1px 2px 0px rgba(242,176,140,1), 0px 2px 3px 0px rgba(0,0,0,.16), inset 0px -1px 2px 0px rgba(0,0,0,.4)', 34 | }, 35 | }, 36 | }, 37 | variants: { 38 | extend: { 39 | opacity: ['disabled'], 40 | cursor: ['disabled'], 41 | }, 42 | }, 43 | plugins: [ 44 | require('@tailwindcss/aspect-ratio'), 45 | function ({ addBase }) { 46 | addBase({ 47 | 'input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button': { 48 | '-webkit-appearance': 'none', 49 | margin: '0', 50 | }, 51 | 'input[type="number"]': { 52 | '-moz-appearance': 'textfield', 53 | }, 54 | }); 55 | }, 56 | ], 57 | } -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 92 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 77 | -------------------------------------------------------------------------------- /components/BaseTable.vue: -------------------------------------------------------------------------------- 1 | 179 | 180 | 327 | --------------------------------------------------------------------------------