├── .editorconfig ├── .gitignore ├── README.md ├── assets └── style.css ├── components ├── Aa.vue ├── Editor.vue └── Nav.vue ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages ├── index.vue └── index │ ├── 2407 │ ├── 1.vue │ ├── 2.vue │ ├── 3.vue │ └── 4.vue │ ├── 2408 │ ├── 1.vue │ ├── 2.vue │ ├── 3.vue │ └── 4.vue │ ├── 2409 │ ├── 1.vue │ ├── 2.vue │ ├── 3.vue │ └── 4.vue │ ├── about.vue │ └── index.vue ├── plugins └── analytics.client.js ├── static └── favicon.ico ├── store └── README.md ├── util └── dbHelper.js └── vercel.json /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 英语外刊阅读 2 | 3 | #### 在线访问   4 | - 国内:[ppenglish.tech](https://www.ppenglish.tech/) 5 | - 国际:[https://ppenglish.vercel.app](https://ppenglish.vercel.app/) 6 | 7 |
8 | 9 | #### 演示           10 | ![image](https://github.com/user-attachments/assets/1280bb7b-837e-4b14-b958-3fe62f9bf528) 11 | 12 | ![image](https://github.com/user-attachments/assets/1cd94160-a87e-4d36-aa30-cd09a2697109) 13 | 14 | ![image](https://github.com/user-attachments/assets/b671e4c0-6826-4882-b216-61206d08326b) 15 | 16 | ![image](https://github.com/user-attachments/assets/cc248191-656c-4cc1-8682-4f6a789e3d9d) 17 | 18 |
19 | 20 | #### 特点 21 | 22 | - 千问大模型AI翻译 23 | - 主流外刊文章,适配四六级、考研等英语学习者需求 24 | - 用户自定义布局 25 | - 字体样式 fontFamily 26 | - 字体大小 fontSize 27 | - 行间距 lineHeight    28 | - 单双列布局 29 | - 主题 30 | - markdown笔记 31 | 32 |
33 | 34 | #### 数据来源 35 | 36 | 文章来源于主流期刊杂志, 翻译由qwen-plus提供:    37 | ```python 38 | # 阿里千问api_key 39 | dashscope.api_key = "" 40 | 41 | def AITranslate(content): 42 | messages = [ 43 | { 44 | "role": "system", 45 | "content": "You will be provided with statements, and your task is to translate them to standard Chinese.", 46 | }, 47 | { 48 | "role": "user", 49 | "content": content, 50 | }, 51 | ] 52 | response = Generation.call( 53 | model="qwen-plus", 54 | messages=messages, 55 | seed=random.randint(1, 10000), 56 | result_format="message", 57 | ) 58 | if response.status_code == HTTPStatus.OK: 59 | return response.output.choices[0].message.content 60 | else: 61 | return "" 62 | ``` 63 | 64 | 65 |
66 | 67 | #### 查词功能 68 | 69 | 网站不提供查词功能,可安装 [欧路翻译 - 网页划词翻译工具 (google.com)](https://chromewebstore.google.com/detail/欧路翻译-网页划词翻译工具/djbfechcnkppbknmlhfcaoifgnicolin)等插件实现。 70 | 71 |
72 | 73 | #### 反馈            74 | 网站开源且长期更新,希望得到你的帮助。
75 | 如果你有好的想法或者文章数据源,请到 [github](https://github.com/wushanglang/ppenglish)上提交issue。   76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | /* Reset CSS */ 2 | *, 3 | *::before, 4 | *::after { 5 | box-sizing: border-box; 6 | margin: 0; 7 | padding: 0; 8 | } 9 | 10 | html { 11 | font-family: Helvetica, Arial, sans-serif; 12 | transition: 250ms; 13 | box-sizing: border-box; 14 | } 15 | 16 | body { 17 | padding: 36px 24px; 18 | line-height: 1.6; 19 | } 20 | 21 | body::before { 22 | content: ''; 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | right: 0; 27 | bottom: 0; 28 | background-color: inherit; 29 | z-index: -1; 30 | } 31 | 32 | .parag { 33 | display: grid; 34 | column-gap: 20px; 35 | } 36 | 37 | h1, 38 | h2, 39 | h3, 40 | h4, 41 | h5, 42 | h6 { 43 | margin: 1rem 0; 44 | font-weight: 600; 45 | line-height: 1.2; 46 | } 47 | 48 | .parag>p { 49 | margin: 1rem 0; 50 | } 51 | p { 52 | margin: 4px 0; 53 | } 54 | a { 55 | color: #007bff; 56 | text-decoration: none; 57 | } 58 | 59 | a:hover { 60 | text-decoration: underline; 61 | } 62 | 63 | ul, 64 | li { 65 | list-style: none; 66 | } 67 | 68 | button:hover { 69 | background-color: #0056b3; 70 | } 71 | 72 | /* Responsive Design */ 73 | @media (max-width: 768px) { 74 | .container { 75 | padding: 0 0.5rem; 76 | } 77 | 78 | h1 { 79 | font-size: 2rem; 80 | } 81 | 82 | h2 { 83 | font-size: 1.75rem; 84 | } 85 | 86 | h3 { 87 | font-size: 1.5rem; 88 | } 89 | } 90 | 91 | 92 | /* 93 | * tooltip 94 | */ 95 | .tooltip { 96 | cursor: pointer; 97 | display: flex; 98 | align-items: center; 99 | justify-content: center; 100 | } 101 | 102 | .tooltip { 103 | position: relative; 104 | display: inline-block; 105 | } 106 | 107 | .tooltip .tooltiptext { 108 | background-color: #2a2a2a; 109 | color: #f9f9f9; 110 | visibility: hidden; 111 | text-align: center; 112 | border-radius: 4px; 113 | padding: 8px; 114 | position: absolute; 115 | z-index: 999; 116 | transform: translate(-0%, -100%); 117 | font-size: 16px; 118 | } 119 | 120 | .tooltip:hover .tooltiptext { 121 | visibility: visible; 122 | } 123 | 124 | 125 | 126 | /* tiptap */ 127 | .tiptap:first-child { 128 | width: 46rem; 129 | max-width: 46rem; 130 | height: 24rem; 131 | padding: 0.5rem 1rem; 132 | font-size: 1rem; 133 | overflow-y: auto; 134 | overflow-wrap: break-word; 135 | outline: none; 136 | border-color: none; 137 | box-shadow: 0 0 0 1px #a7a7a7; 138 | border-radius: 8px; 139 | } 140 | 141 | ul li { 142 | list-style: circle; 143 | } 144 | 145 | .tiptap ul, 146 | .tiptap ol { 147 | padding: 0 8px; 148 | margin-left: 8px; 149 | } 150 | 151 | .tiptap h1, 152 | .tiptap h2, 153 | .tiptap h3, 154 | .tiptap h4, 155 | .tiptap h5, 156 | .tiptap h6 { 157 | line-height: 1; 158 | margin-top: 0; 159 | } 160 | 161 | .tiptap h1, 162 | .tiptap h2 { 163 | margin: 12px 0; 164 | } 165 | 166 | .tiptap h1, 167 | h2 { 168 | font-size: 24px; 169 | } 170 | 171 | 172 | 173 | .tiptap code { 174 | background: rgb(53, 42, 42); 175 | color: #f2f2f2; 176 | border-radius: 0.4rem; 177 | font-size: 0.85rem; 178 | padding: 0.25em 0.3em; 179 | } 180 | 181 | .tiptap pre { 182 | background: rgb(53, 42, 42); 183 | color: #f2f2f2; 184 | border-radius: 0.5rem; 185 | font-family: "JetBrainsMono", monospace; 186 | margin: 1.5rem 0; 187 | padding: 0.75rem 1rem; 188 | } 189 | 190 | .tiptap pre code { 191 | background: none; 192 | color: inherit; 193 | font-size: 0.8rem; 194 | padding: 0; 195 | } 196 | 197 | .tiptap blockquote { 198 | border-left: 3px solid #696969; 199 | margin: 1rem 0; 200 | padding-left: 1rem; 201 | } 202 | 203 | .tiptap hr { 204 | border: none; 205 | border-top: 1px solid #696969; 206 | margin: 2rem 0; 207 | } -------------------------------------------------------------------------------- /components/Aa.vue: -------------------------------------------------------------------------------- 1 | 107 | 108 | 215 | 216 | 286 | -------------------------------------------------------------------------------- /components/Editor.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 54 | 55 | 66 | -------------------------------------------------------------------------------- /components/Nav.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 47 | 48 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode 3 | ssr: false, 4 | 5 | // Target: https://go.nuxtjs.dev/config-target 6 | target: 'static', 7 | 8 | // Global page headers: https://go.nuxtjs.dev/config-head 9 | head: { 10 | title: 'bubble', 11 | htmlAttrs: { 12 | lang: 'en' 13 | }, 14 | meta: [ 15 | { charset: 'utf-8' }, 16 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 17 | { hid: 'description', name: 'description', content: '' }, 18 | { name: 'format-detection', content: 'telephone=no' } 19 | ], 20 | link: [ 21 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 22 | ] 23 | }, 24 | 25 | // Global CSS: https://go.nuxtjs.dev/config-css 26 | css: [ 27 | { src: '~/assets/style.css' } 28 | ], 29 | 30 | // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins 31 | plugins: [ 32 | ], 33 | 34 | // Auto import components: https://go.nuxtjs.dev/config-components 35 | components: true, 36 | 37 | // Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules 38 | buildModules: [ 39 | ], 40 | 41 | // Modules: https://go.nuxtjs.dev/config-modules 42 | modules: [ 43 | ], 44 | 45 | // Build Configuration: https://go.nuxtjs.dev/config-build 46 | build: { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bubble", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxt", 7 | "build": "nuxt build", 8 | "start": "nuxt start", 9 | "generate": "nuxt generate" 10 | }, 11 | "dependencies": { 12 | "@tiptap/extension-color": "^2.4.0", 13 | "@tiptap/extension-text-style": "^2.4.0", 14 | "@tiptap/pm": "^2.4.0", 15 | "@tiptap/starter-kit": "^2.4.0", 16 | "@tiptap/vue-2": "^2.4.0", 17 | "@vercel/analytics": "^1.3.1", 18 | "core-js": "^3.25.3", 19 | "fs": "^0.0.1-security", 20 | "nuxt": "^2.15.8", 21 | "vue": "^2.7.10", 22 | "vue-server-renderer": "^2.7.10", 23 | "vue-template-compiler": "^2.7.10" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 69 | 70 | 86 | -------------------------------------------------------------------------------- /pages/index/2407/1.vue: -------------------------------------------------------------------------------- 1 | 45 | -------------------------------------------------------------------------------- /pages/index/2407/2.vue: -------------------------------------------------------------------------------- 1 | 65 | -------------------------------------------------------------------------------- /pages/index/2407/3.vue: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /pages/index/2407/4.vue: -------------------------------------------------------------------------------- 1 | 37 | -------------------------------------------------------------------------------- /pages/index/2408/1.vue: -------------------------------------------------------------------------------- 1 | 53 | -------------------------------------------------------------------------------- /pages/index/2408/2.vue: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /pages/index/2408/3.vue: -------------------------------------------------------------------------------- 1 | 61 | -------------------------------------------------------------------------------- /pages/index/2408/4.vue: -------------------------------------------------------------------------------- 1 | 81 | -------------------------------------------------------------------------------- /pages/index/2409/1.vue: -------------------------------------------------------------------------------- 1 | 109 | -------------------------------------------------------------------------------- /pages/index/2409/2.vue: -------------------------------------------------------------------------------- 1 | 325 | -------------------------------------------------------------------------------- /pages/index/2409/3.vue: -------------------------------------------------------------------------------- 1 | 49 | -------------------------------------------------------------------------------- /pages/index/2409/4.vue: -------------------------------------------------------------------------------- 1 | 81 | -------------------------------------------------------------------------------- /pages/index/about.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 58 | 59 | 82 | -------------------------------------------------------------------------------- /pages/index/index.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 138 | 139 | 197 | 198 | -------------------------------------------------------------------------------- /plugins/analytics.client.js: -------------------------------------------------------------------------------- 1 | import { inject } from '@vercel/analytics'; 2 | 3 | export default defineNuxtPlugin(() => { 4 | inject(); 5 | }); -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wushanglang/ppenglish/d1873310dae3160b03b0c66e46467c402f49f222/static/favicon.ico -------------------------------------------------------------------------------- /store/README.md: -------------------------------------------------------------------------------- 1 | # STORE 2 | 3 | **This directory is not required, you can delete it if you don't want to use it.** 4 | 5 | This directory contains your Vuex Store files. 6 | Vuex Store option is implemented in the Nuxt.js framework. 7 | 8 | Creating a file in this directory automatically activates the option in the framework. 9 | 10 | More information about the usage of this directory in [the documentation](https://nuxtjs.org/guide/vuex-store). 11 | -------------------------------------------------------------------------------- /util/dbHelper.js: -------------------------------------------------------------------------------- 1 | // indexedDBHelper.js 2 | export function openDatabase(dbName, storeName) { 3 | return new Promise((resolve, reject) => { 4 | const request = indexedDB.open(dbName, 1); 5 | 6 | request.onupgradeneeded = function (event) { 7 | const db = event.target.result; 8 | if (!db.objectStoreNames.contains(storeName)) { 9 | db.createObjectStore(storeName, { keyPath: 'id' }); 10 | } 11 | }; 12 | 13 | request.onsuccess = function (event) { 14 | resolve(event.target.result); 15 | }; 16 | 17 | request.onerror = function (event) { 18 | reject(event.target.error); 19 | }; 20 | }); 21 | } 22 | 23 | export function saveContent(db, storeName, content, key) { 24 | return new Promise((resolve, reject) => { 25 | const transaction = db.transaction([storeName], 'readwrite'); 26 | const store = transaction.objectStore(storeName); 27 | const request = store.put({ id: key, content }); 28 | 29 | request.onsuccess = function () { 30 | resolve(); 31 | }; 32 | 33 | request.onerror = function (event) { 34 | reject(event.target.error); 35 | }; 36 | }); 37 | } 38 | 39 | export function getContent(db, storeName, key) { 40 | return new Promise((resolve, reject) => { 41 | const transaction = db.transaction([storeName], 'readonly'); 42 | const store = transaction.objectStore(storeName); 43 | const request = store.get(key); 44 | 45 | request.onsuccess = function (event) { 46 | resolve(event.target.result ? event.target.result.content : null); 47 | }; 48 | 49 | request.onerror = function (event) { 50 | reject(event.target.error); 51 | }; 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "builds": [ 3 | { 4 | "src": "nuxt.config.js", 5 | "use": "@nuxtjs/vercel-builder", 6 | "config": {} 7 | } 8 | ] 9 | } --------------------------------------------------------------------------------