├── .travis.yml ├── .flowconfig ├── assets ├── style.css ├── icons │ ├── icon16.png │ ├── icon48.png │ └── icon128.png ├── words.html ├── popup.html ├── options.html ├── dev.html └── manifest.json ├── src ├── __tests__ │ ├── index.spec.js │ ├── parse.spec.js │ └── pages │ │ ├── noresponse.js │ │ ├── sentence.js │ │ ├── deficits.js │ │ └── newest.js ├── words_page.js ├── options_page.js ├── components │ ├── index.js │ ├── style.js │ ├── audio.js │ ├── words_app.js │ ├── add_word.js │ ├── tips.js │ ├── popup_app.js │ ├── content_app.js │ ├── searcher.js │ ├── options_app.js │ ├── detail.js │ └── icons.js ├── popup.js ├── utils.js ├── lib │ ├── config.js │ └── shanbay_oauth2.js ├── options.js ├── message.js ├── words.js ├── dev.js ├── event_page.js ├── content_script.js └── parse.js ├── .gitignore ├── CHANGELOG.md ├── configs └── build │ ├── cp_assets.js │ ├── webpack.dev.js │ └── webpack.prod.js ├── LICENSE.md ├── README.md └── package.json /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "6" 5 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | .ycce { 2 | width: 480px; 3 | font-size: 14px; 4 | } 5 | -------------------------------------------------------------------------------- /src/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | describe('test', () => { 2 | it('should', () => { 3 | 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /assets/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyyd/youdao-collins-chrome-extension/HEAD/assets/icons/icon16.png -------------------------------------------------------------------------------- /assets/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyyd/youdao-collins-chrome-extension/HEAD/assets/icons/icon48.png -------------------------------------------------------------------------------- /assets/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oyyd/youdao-collins-chrome-extension/HEAD/assets/icons/icon128.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | WORK.md 4 | /build 5 | dev 6 | npm-debug.log 7 | configs/crx.pem 8 | build.crx 9 | build.zip 10 | .history 11 | .package-lock.json -------------------------------------------------------------------------------- /src/words_page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './components/words_app' 4 | 5 | ReactDOM.render(, document.getElementById('main')) 6 | -------------------------------------------------------------------------------- /src/options_page.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './components/options_app' 4 | 5 | ReactDOM.render(, document.getElementById('main')) 6 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | import Detail from './detail' 2 | import Searcher from './searcher' 3 | 4 | const components = { 5 | Detail, 6 | Searcher, 7 | } 8 | 9 | export default components 10 | -------------------------------------------------------------------------------- /assets/words.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /assets/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/popup.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import Popup from './components/popup_app' 4 | import { styleContainer } from './utils' 5 | 6 | styleContainer(document.querySelector('body')) 7 | 8 | ReactDOM.render( 9 | , 10 | document.getElementById('main'), 11 | ) 12 | -------------------------------------------------------------------------------- /assets/dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dev 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | const style = 2 | "font-family: -apple-system,system-ui,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;font-size: 14px;font-weight: 400;line-height: 1.5;color: #292b2c;background-color: #fff;margin: 0;" 3 | 4 | export function styleContainer(containerEle) { 5 | containerEle.setAttribute('style', style) 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Custom your app config 3 | * include this script before lib/shanbay_oauth2.js 4 | */ 5 | 6 | export const AppConf = { 7 | client_id: 'f9d934fce99e367145c7', 8 | } 9 | 10 | export const ShanbayConf = { 11 | api_version: '1.0', 12 | api_root: 'https://api.shanbay.com', 13 | auth_url: '/oauth2/authorize/', 14 | token_url: '/oauth2/token/', 15 | auth_success_url: '/oauth2/auth/success/', 16 | } 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.2.0 4 | - 特性:支持在popup界面上停用划词翻译 5 | - 特性:popup搜索后自动选中全部文本,便于删除 6 | - 修复:划词弹层无法被选中 7 | 8 | ## v1.1.3 9 | - 修复:错误使用token导致的无法添加单词 10 | 11 | ## v1.1.2 12 | - 修复:window及linux下打开窗口时聚焦到input上 13 | 14 | ## v1.1.1 15 | - 修复:滚动样式 16 | 17 | ## v1.1.0 18 | - 特性:接入扇贝词库 19 | 20 | ## v1.0.8 21 | - 修复:触发事件错误 22 | 23 | ## v1.0.7 24 | - 修复:静态文件图标命名错误 25 | 26 | ## v1.0.6 27 | - 修复:静态文件图标命名错误 28 | 29 | ## v1.0.5 30 | - 修复:多选择项被当成单选择项 31 | - 特性:双击后划词翻译(拖拽选中不翻译) 32 | 33 | ## v1.0.4 34 | 35 | - 特性:添加通过Ctrl+Q打开popup page的快捷键 36 | - 特性:修复部分同义单词无任何反馈的问题,会跳转去搜索对应的同义单词解释 37 | - 构建:uglify缩小代码体积 38 | 39 | ## v1.0.3 40 | 41 | - bug修复 42 | - 特性:添加反馈链接 43 | 44 | ## v1.0.2 45 | -------------------------------------------------------------------------------- /configs/build/cp_assets.js: -------------------------------------------------------------------------------- 1 | const { readFileSync, writeFileSync } = require('fs') 2 | const { resolve } = require('path') 3 | const { version } = require('../../package.json') 4 | 5 | const MANIFEST_PATH = resolve(__dirname, '../../assets/manifest.json') 6 | const TARGET_PATH = resolve(__dirname, '../../build/manifest.json') 7 | 8 | function getContent() { 9 | const manifestJSON = JSON.parse(readFileSync(MANIFEST_PATH)) 10 | 11 | manifestJSON.version = version 12 | manifestJSON.background.persistent = false 13 | 14 | return JSON.stringify(manifestJSON, null, 2) 15 | } 16 | 17 | function writeManifest() { 18 | const content = getContent() 19 | 20 | writeFileSync(TARGET_PATH, content) 21 | } 22 | 23 | if (require.main === module) { 24 | writeManifest() 25 | } 26 | -------------------------------------------------------------------------------- /configs/build/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | function relative(p) { 4 | return path.resolve(__dirname, p) 5 | } 6 | 7 | function appendDeps(file) { 8 | return [ 9 | 'babel-polyfill', 10 | file, 11 | ] 12 | } 13 | 14 | module.exports = { 15 | devtool: 'inline-source-map', 16 | entry: { 17 | popup: appendDeps(relative('../../src/popup')), 18 | '../dev/dev': appendDeps(relative('../../src/dev')), 19 | event_page: appendDeps(relative('../../src/event_page')), 20 | content_script: appendDeps(relative('../../src/content_script')), 21 | options_page: appendDeps(relative('../../src/options_page')), 22 | words_page: appendDeps(relative('../../src/words_page')), 23 | }, 24 | output: { 25 | path: relative('../../build'), 26 | filename: '[name].js', 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.js$/, 32 | include: [ 33 | relative('../../src'), 34 | ], 35 | loader: 'babel-loader', 36 | }, 37 | ], 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | export const SHOW_NOTEBOOK_OPTIONS = false 2 | 3 | const DEFAULT_ACTIVE_TYPE = 'ALWAYS' 4 | 5 | const DEFAULT_SHOW_NOTEBOOK = true 6 | 7 | export const ACTIVE_TYPES = { 8 | ALWAYS: '划词即翻译', 9 | KEY_DOWN: '按住(meta/ctrl)键 + 划词时翻译', 10 | DOUBLE_CLICK: '双击单词后翻译', 11 | NEVER: '禁用划词翻译', 12 | } 13 | 14 | const DEFAULT_OPTIONS = { 15 | activeType: DEFAULT_ACTIVE_TYPE, 16 | showNotebook: DEFAULT_SHOW_NOTEBOOK, 17 | tempDisabled: false, 18 | } 19 | 20 | export function getOptions() { 21 | return new Promise((resolve) => { 22 | chrome.storage.sync.get(null, (data) => { 23 | let options = DEFAULT_OPTIONS 24 | 25 | if (Object.hasOwnProperty.call(data, 'options')) { 26 | options = Object.assign(options, data.options) 27 | } 28 | 29 | resolve(options) 30 | }) 31 | }) 32 | } 33 | 34 | export function setOptions(options) { 35 | return new Promise((resolve) => { 36 | chrome.storage.sync.set({ 37 | options, 38 | }, () => { 39 | resolve(options) 40 | }) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2015-present Ouyang Yadong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "划词翻译(柯林斯词典)+扇贝生词本", 5 | "description": "查询单词柯林斯释义,支持划词翻译,数据来源于有道词典。接入扇贝生词本,快速记录新单词,方便未来学习。", 6 | "version": "1.0", 7 | 8 | "icons": { 9 | "16": "./icons/icon16.png", 10 | "48": "./icons/icon48.png", 11 | "128": "./icons/icon128.png" 12 | }, 13 | 14 | "browser_action": { 15 | "default_icon": "./icons/icon16.png", 16 | "default_popup": "popup.html" 17 | }, 18 | 19 | "background": { 20 | "scripts": ["event_page.js"], 21 | "persistent": true 22 | }, 23 | 24 | "content_scripts": [{ 25 | "matches": [""], 26 | "js": ["content_script.js"] 27 | }], 28 | 29 | "options_ui": { 30 | "page": "options.html", 31 | "chrome_style": true 32 | }, 33 | 34 | "commands": { 35 | "_execute_browser_action": { 36 | "suggested_key": { 37 | "default": "Ctrl+Q", 38 | "mac": "MacCtrl+Q" 39 | } 40 | } 41 | }, 42 | 43 | "permissions": [ 44 | "storage", 45 | "activeTab", 46 | "tabs", 47 | "http://dict.youdao.com/", 48 | "https://api.shanbay.com/" 49 | ] 50 | } 51 | -------------------------------------------------------------------------------- /src/components/style.js: -------------------------------------------------------------------------------- 1 | export const fontS = 12 2 | export const fontN = 14 3 | export const fontL = 18 4 | export const gapL = 10 5 | export const gapM = 8 6 | export const gapS = 4 7 | export const mainBG = '#eff5f8' 8 | export const colorDanger = '#d9534f' 9 | export const colorMuted = '#636c72' 10 | export const colorWarning = '#f0ad4e' 11 | export const colorPrimary = '#0275d8' 12 | export const colorSuccess = '#5cb85c' 13 | export const colorInfo = '#5bc0de' 14 | export const colorBorder = '#D4D4D5' 15 | 16 | export const btn = { 17 | width: 40, 18 | boxSizing: 'border-box', 19 | cursor: 'pointer', 20 | padding: '.5rem .75rem', 21 | marginBottom: '0', 22 | fontSize: '1rem', 23 | fontWeight: '400', 24 | lineHeight: '1.25', 25 | color: '#464a4c', 26 | textAlign: 'center', 27 | backgroundColor: '#eceeef', 28 | borderTop: '1px solid rgba(0,0,0,.15)', 29 | borderBottom: '1px solid rgba(0,0,0,.15)', 30 | borderRight: '1px solid rgba(0,0,0,.15)', 31 | borderLeft: '1px solid rgba(0,0,0,.15)', 32 | borderTopRightRadius: '.25rem', 33 | borderTopLeftRadius: '.25rem', 34 | borderBottomLeftRadius: '.25rem', 35 | borderBottomRightRadius: '.25rem', 36 | display: 'flex', 37 | flexDirection: 'column', 38 | justifyContent: 'center', 39 | } 40 | -------------------------------------------------------------------------------- /src/message.js: -------------------------------------------------------------------------------- 1 | export const EVENTS = { 2 | SEARCH_WORD: 'SEARCH_WORD', 3 | OPEN_NEW_TAB: 'OPEN_NEW_TAB', 4 | ADD_WORD_SHANBAY: 'ADD_WORD_SHANBAY', 5 | CLEAR_SHANBAY_TOKEN: 'CLEAR_SHANBAY_TOKEN', 6 | } 7 | 8 | // event page receives an event 9 | export function onMessage(eventName, handler) { 10 | chrome.runtime.onMessage.addListener((req, sender, sendResponse) => { 11 | const { eventName: e, data } = req 12 | 13 | if (e !== eventName) { 14 | return false 15 | } 16 | 17 | handler(data, sendResponse) 18 | 19 | return true 20 | }) 21 | } 22 | 23 | // send message to event page 24 | export function sendMessage(eventName, data) { 25 | return new Promise((resolve) => { 26 | chrome.runtime.sendMessage({ 27 | eventName, 28 | data, 29 | }, (res) => { 30 | resolve(res) 31 | }) 32 | }) 33 | } 34 | 35 | export function openLink(word) { 36 | sendMessage(EVENTS.OPEN_NEW_TAB, word) 37 | } 38 | 39 | export async function searchWord(word) { 40 | const res = await sendMessage(EVENTS.SEARCH_WORD, word) 41 | 42 | return res 43 | } 44 | 45 | export async function addNotebookWord(word) { 46 | return sendMessage(EVENTS.ADD_WORD_SHANBAY, word) 47 | } 48 | 49 | export async function clearShanbayToken() { 50 | return sendMessage(EVENTS.CLEAR_SHANBAY_TOKEN) 51 | } 52 | -------------------------------------------------------------------------------- /src/words.js: -------------------------------------------------------------------------------- 1 | const SEARCH_PREFIX = 'http://dict.youdao.com/w/eng/' 2 | 3 | export function getWordURL(word) { 4 | return `${SEARCH_PREFIX}${word}` 5 | } 6 | 7 | export function getWordsPage() { 8 | return chrome.runtime.getURL('words.html') 9 | } 10 | 11 | export function get() { 12 | return new Promise((resolve) => { 13 | chrome.storage.sync.get(null, (data) => { 14 | let words = [] 15 | 16 | if (Object.hasOwnProperty.call(data, 'words')) { 17 | words = data.words 18 | } 19 | 20 | resolve(words) 21 | }) 22 | }) 23 | } 24 | 25 | export function have(word) { 26 | return get().then(words => words.indexOf(word) > -1) 27 | } 28 | 29 | function set(words) { 30 | return new Promise((resolve) => { 31 | chrome.storage.sync.set({ 32 | words, 33 | }, () => { 34 | resolve(words) 35 | }) 36 | }) 37 | } 38 | 39 | export function add(word) { 40 | return get().then((words) => { 41 | if (words.indexOf(word) >= 0) { 42 | return Promise.resolve(words) 43 | } 44 | 45 | words.push(word) 46 | 47 | return set(words) 48 | }) 49 | } 50 | 51 | export function remove(word) { 52 | return get().then((_words) => { 53 | let words = _words.slice() 54 | const index = words.indexOf(word) 55 | 56 | if (index < 0) { 57 | return Promise.resolve(words) 58 | } 59 | 60 | words = words.slice(0, index).concat(words.slice(index + 1)) 61 | 62 | return set(words) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /configs/build/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const webpack = require('webpack') 3 | 4 | function relative(p) { 5 | return path.resolve(__dirname, p) 6 | } 7 | 8 | function appendDeps(file) { 9 | return [ 10 | 'babel-polyfill', 11 | file, 12 | ] 13 | } 14 | 15 | module.exports = { 16 | entry: { 17 | popup: appendDeps(relative('../../src/popup')), 18 | '../dev/dev': appendDeps(relative('../../src/dev')), 19 | event_page: appendDeps(relative('../../src/event_page')), 20 | content_script: appendDeps(relative('../../src/content_script')), 21 | options_page: appendDeps(relative('../../src/options_page')), 22 | words_page: appendDeps(relative('../../src/words_page')), 23 | }, 24 | output: { 25 | path: relative('../../build'), 26 | filename: '[name].js', 27 | }, 28 | module: { 29 | rules: [ 30 | { 31 | test: /\.js$/, 32 | include: [ 33 | relative('../../src'), 34 | ], 35 | loader: 'babel-loader', 36 | }, 37 | ], 38 | }, 39 | plugins: [ 40 | new webpack.DefinePlugin({ 41 | 'process.env': { 42 | NODE_ENV: JSON.stringify('production'), 43 | }, 44 | }), 45 | new webpack.LoaderOptionsPlugin({ 46 | minimize: true, 47 | debug: false, 48 | }), 49 | new webpack.optimize.UglifyJsPlugin({ 50 | compress: { 51 | warnings: false, 52 | drop_console: false, 53 | }, 54 | }), 55 | ], 56 | } 57 | -------------------------------------------------------------------------------- /src/components/audio.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import icons from './icons' 4 | 5 | const { string } = PropTypes 6 | 7 | function getAudioURL(word, type = 1) { 8 | return `http://dict.youdao.com/dictvoice?audio=${word}&type=${type}` 9 | } 10 | 11 | class Audio extends Component { 12 | constructor(props) { 13 | super(props) 14 | 15 | this.playAudio = this.playAudio.bind(this) 16 | 17 | this.refers = {} 18 | this.state = { 19 | hide: false, 20 | } 21 | } 22 | 23 | playAudio() { 24 | this.refers.audio.play() 25 | } 26 | 27 | render() { 28 | const { playAudio } = this 29 | const { word } = this.props 30 | const { hide } = this.state 31 | const url = getAudioURL(word) 32 | 33 | const style = { 34 | cursor: 'pointer', 35 | verticalAlign: 'middle', 36 | display: hide ? 'none' : 'inline-block', 37 | } 38 | 39 | return ( 40 | 41 | horn 47 | 56 | ) 57 | } 58 | } 59 | 60 | Audio.propTypes = { 61 | word: string.isRequired, 62 | } 63 | 64 | 65 | export default Audio 66 | -------------------------------------------------------------------------------- /src/dev.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import React from 'react' 3 | import ReactDOM from 'react-dom' 4 | import components from './components' 5 | 6 | function renderDetail() { 7 | const { Detail } = components 8 | const mock = { 9 | type: 'explain', 10 | response: {"word":"perform","pronunciation":"/pəˈfɔːm/","frequence":4,"rank":"CET4 TEM4","additionalPattern":"(\n performing,\n performed,\n \t\t\t\t\t\t\t\t\t\t\t\tperforms\n )","meanings":[{"explain":{"type":"V-T","typeDesc":"及物动词","engExplain":"When you perform a task or action, especially a complicated one, you do it. 做; 执行 (尤指复杂的任务或行动)"},"example":{"eng":" We're looking for people of all ages who have performed outstanding acts of bravery, kindness, or courage. ","ch":"我们正在寻找各个年龄的、曾有过无畏、善良或英勇之举的杰出人士。"}},{"explain":{"type":"V-T","typeDesc":"及物动词","engExplain":"If something performs a particular function, it has that function. 行使 (某种功能)"},"example":{"eng":" An engine has many parts, each performing a different function. ","ch":"一部发动机有很多部件,各自行使不同的功能。"}},{"explain":{"type":"V-T","typeDesc":"及物动词","engExplain":"If you perform a play, a piece of music, or a dance, you do it in front of an audience. 演出; 演奏"},"example":{"eng":" Gardiner has pursued relentlessly high standards in performing classical music. ","ch":"加德纳在演奏古典音乐方面始终不懈地追求高标准。"}},{"explain":{"type":"V-I","typeDesc":"不及物动词","engExplain":"If someone or something performs well, they work well or achieve a good result. If they perform badly, they work badly or achieve a poor result. 表现 (好/不好)"},"example":{"eng":" He had not performed well in his exams. ","ch":"过去考试他都考得不好。"}}]} 11 | } 12 | // const mock = {"type":"choices","response":{"choices":[{"words":["choice","option"],"wordType":"n."},{"words":["select","choose","elect"],"wordType":"vt."}]}} 13 | 14 | ReactDOM.render(, document.getElementById('main')) 15 | } 16 | 17 | renderDetail() 18 | -------------------------------------------------------------------------------- /src/components/words_app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { get, remove, getWordURL } from '../words' 3 | import { colorPrimary } from './style' 4 | 5 | const styles = { 6 | word: { 7 | marginRight: 10, 8 | padding: '2px 4px', 9 | backgroundColor: colorPrimary, 10 | borderRadius: '4px', 11 | color: '#FFF', 12 | display: 'inline-block', 13 | }, 14 | text: { 15 | color: '#FFF', 16 | textDecoration: 'none', 17 | }, 18 | removeBtn: { 19 | textAlign: 'center', 20 | lineHeight: '12px', 21 | verticalAlign: 'middle', 22 | display: 'inline-block', 23 | backgroundColor: '#FFF', 24 | borderRadius: '50%', 25 | width: 12, 26 | height: 12, 27 | color: colorPrimary, 28 | marginLeft: 6, 29 | cursor: 'pointer', 30 | }, 31 | } 32 | 33 | class App extends Component { 34 | constructor(props) { 35 | super(props) 36 | 37 | this.removeWord = this.removeWord.bind(this) 38 | 39 | this.state = { 40 | words: null, 41 | } 42 | } 43 | 44 | componentDidMount() { 45 | get().then((words) => { 46 | this.setState({ 47 | words, 48 | }) 49 | }) 50 | } 51 | 52 | removeWord(word) { 53 | remove(word).then((words) => { 54 | this.setState({ 55 | words, 56 | }) 57 | }) 58 | } 59 | 60 | render() { 61 | const { removeWord } = this 62 | const { words } = this.state 63 | 64 | if (!words) { 65 | return null 66 | } 67 | 68 | return ( 69 |
70 |
生词:
71 |
72 | {words.map(word => ( 73 |
74 | 79 | {word} 80 | 81 |
removeWord(word)}> 82 | x 83 |
84 |
85 | ))} 86 |
87 |
88 | ) 89 | } 90 | } 91 | 92 | export default App 93 | -------------------------------------------------------------------------------- /src/lib/shanbay_oauth2.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { AppConf, ShanbayConf } from './config' 3 | 4 | let OAuth = null 5 | 6 | export default function ShanbayOauth(client_id, conf) { 7 | this.client_id = client_id 8 | this.conf = conf 9 | } 10 | 11 | /* 12 | * include lib/config.js and then call initPage in your background page 13 | */ 14 | ShanbayOauth.initPage = function() { 15 | OAuth = ShanbayOauth.fromConfig(AppConf, ShanbayConf) 16 | return OAuth 17 | } 18 | 19 | ShanbayOauth.fromConfig = function(app_conf, shanbay_conf) { 20 | return new ShanbayOauth(app_conf['client_id'], shanbay_conf) 21 | } 22 | 23 | ShanbayOauth.prototype.authorize = function(callback) { 24 | var authorize_url = this.conf.api_root + this.conf.auth_url 25 | var authorize_url = 26 | authorize_url + '?response_type=token&client_id=' + this.client_id 27 | chrome.tabs.create({ url: authorize_url }, function(tab) { 28 | OAuth.tabId = tab.id 29 | }) 30 | 31 | this.callback = callback 32 | chrome.tabs.onUpdated.addListener(this.onAuthorize) 33 | } 34 | 35 | ShanbayOauth.prototype.onAuthorize = function(tabId, changeInfo, tab) { 36 | if (OAuth.tabId != tabId) return false 37 | 38 | if (tab.url.indexOf(OAuth.conf.auth_success_url) !== -1) { 39 | // auth success, store access_token to localStorage and close this tab 40 | var index = tab.url.indexOf('#') + 1 41 | var hash = tab.url.slice(index, tab.url.length) 42 | hash = JSON.parse( 43 | '{"' + decodeURI(hash).replace(/&/g, '","').replace(/=/g, '":"') + '"}', 44 | ) 45 | localStorage.access_token = hash.access_token 46 | localStorage.expired_at = new Date( 47 | new Date().getTime() + hash.expires_in * 1000, 48 | ) 49 | chrome.tabs.remove(tabId) 50 | if (OAuth.callback) { 51 | OAuth.callback() 52 | } 53 | } 54 | } 55 | 56 | ShanbayOauth.prototype.checkToken = function(force) { 57 | if (!this.has_token()) { 58 | if (force) { 59 | this.authorize() // force authorize 60 | } else return 2 // no token 61 | } 62 | 63 | if (this.expired()) return 1 // token expired 64 | 65 | return 0 // token valid 66 | } 67 | 68 | ShanbayOauth.prototype.access_token = function() { 69 | return localStorage.access_token 70 | } 71 | 72 | ShanbayOauth.prototype.has_token = function() { 73 | return this.access_token != undefined 74 | } 75 | 76 | ShanbayOauth.prototype.expired = function() { 77 | var expired_at = localStorage.expired_at 78 | return expired_at == undefined || expired_at < new Date() 79 | } 80 | 81 | ShanbayOauth.prototype.token_valid = function() { 82 | return this.has_token() && !this.expired() 83 | } 84 | 85 | ShanbayOauth.prototype.clearToken = function() { 86 | delete localStorage.access_token 87 | delete localStorage.expired_at 88 | } 89 | -------------------------------------------------------------------------------- /src/components/add_word.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import icons from './icons' 4 | import { addNotebookWord } from '../message' 5 | 6 | const SHANBAY_URL = 'https://www.shanbay.com/bdc/learnings/library/' 7 | 8 | const styles = { 9 | container: { 10 | cursor: 'pointer', 11 | position: 'relative', 12 | display: 'inline-block', 13 | verticalAlign: 'top', 14 | }, 15 | book: { 16 | width: 16, 17 | height: 16, 18 | verticalAlign: 'middle', 19 | }, 20 | plus: { 21 | position: 'absolute', 22 | left: 0, 23 | bottom: 0, 24 | width: 8, 25 | height: 8, 26 | }, 27 | } 28 | 29 | class AddWord extends Component { 30 | constructor(props) { 31 | super(props) 32 | 33 | const { defaultAdded } = props 34 | 35 | this.addWord = this.addWord.bind(this) 36 | 37 | this.state = { 38 | added: defaultAdded, 39 | } 40 | } 41 | 42 | componentWillMount() { 43 | 44 | } 45 | 46 | addWord() { 47 | const { word } = this.props 48 | 49 | addNotebookWord(word).then((response) => { 50 | const { success, msg } = response 51 | 52 | if (!success) { 53 | throw new Error(msg) 54 | } 55 | 56 | this.setState({ 57 | added: true, 58 | }) 59 | }).catch((err) => { 60 | this.props.flash(err.message) 61 | }) 62 | } 63 | 64 | render() { 65 | const { addWord } = this 66 | const { word, showWordsPage } = this.props 67 | const { added } = this.state 68 | 69 | if (!word || !showWordsPage) { 70 | return null 71 | } 72 | 73 | const content = ( 74 |
75 | book 80 | {!added ? ( 81 | plus 86 | ) : null} 87 |
88 | ) 89 | 90 | return added ? ( 91 | 97 | {content} 98 | 99 | ) : ( 100 |
105 | {content} 106 |
107 | ) 108 | } 109 | } 110 | 111 | const { string, bool, func } = PropTypes 112 | 113 | AddWord.propTypes = { 114 | showWordsPage: bool.isRequired, 115 | word: string, 116 | defaultAdded: bool, 117 | flash: func.isRequired, 118 | } 119 | 120 | AddWord.defaultProps = { 121 | word: '', 122 | defaultAdded: false, 123 | } 124 | 125 | export default AddWord 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # youdao-collins-chrome-extension 2 | 3 | [![build](https://api.travis-ci.org/oyyd/youdao-collins-chrome-extension.svg?branch=master)](https://travis-ci.org/oyyd/youdao-collins-chrome-extension) 4 | 5 | 查询英文单词的[柯林斯](https://www.collinsdictionary.com/)释义的chrome扩展应用。支持划词翻译,数据来源于有道词典。接入扇贝生词本,快速记录新单词,方便未来复习。 6 | 7 | ![intro](https://oyyd.github.io/youdao-collins-chrome-extension/pics/intro.webp) 8 | 9 | 去chrome web store上[下载](https://chrome.google.com/webstore/detail/mkohdjbfagmlcaclajmadgkojelkbbfj/) 10 | 11 | ## 使用说明 12 | 13 | - 在配置页面可以设置划词翻译的模式 14 | - 划词即翻译 15 | - 按住(meta/ctrl)键 + 划词时翻译 16 | - 双击划词翻译 17 | - 快捷键`ctrl+q`打开右上角浮层,用于搜索单词 18 | - 搜索成功的单词可以快速加入到扇贝生词本(词库,oauth接入,需要你有扇贝的帐号)中,用于复习 19 | 20 | ## 功能介绍 21 | 22 | ### 1. 为什么是柯林斯词典? 23 | 24 | chrome store上已经有很多其他词典来满足一般的英文词语意义查询。同时我在自己的单词查询过程中发现,相比于两三个中文字的单词意思解释,柯林斯词典提供了一定的英文语境可以帮我 **更准确地** 理解一个单词的意思,并加深记忆。 25 | 26 | 当一个单词搜不到对应的柯林斯释义的时候,这个扩展应用会使用有道词典上找得到的解释。 27 | 28 | ### 2. 划词翻译 29 | 30 | 划词翻译是提高查询效率的重要一环,这个扩展提供: 31 | 32 | - 划词即翻译 33 | - 按住(meta/ctrl)键 + 划词时翻译 34 | - 双击划词翻译 35 | 36 | 两种选择。当然也可以关掉划词翻译。 37 | 38 | ### 3. 有道词典数据源 39 | 40 | 改扩展的数据源来源于[有道词典](http://dict.youdao.com/),但并没有通过api访问,而是直接获取页面内容再加工,理论上也就不会被api访问上线次数限制。 41 | 42 | ### 4. 接入扇贝生词本(词库) 43 | 44 | 市面上英语学习的软件不少,扇贝是其中之一。但我个人觉得扇贝是少数在探索如何将软件技术和语言学习有效地结合起来的产品之一,也是这个应用最后选择接入扇贝生词本的重要原因(虽然扇贝的“清空词库”功能是已经实现的功能,但却严格限制用户使用这一点,会让我这样只使用其中部分功能的用户非常费解)。通过生词本,我们每天多花一点时间复习今天碰到的单词。这让这个软件在教育、学习的层面上多了不少价值。 45 | 46 | ## 实现简介 47 | 48 | 本着一个应用的开发应验总是能帮助到其他踩到坑而又找不到有效信息的开发者的原则,这里简单介绍下这个应用的一些特点。 49 | 50 | ### 1. 用React构建UI,全内联样式 51 | 52 | React组件的组合和复用能力非常优秀。 53 | 54 | Chrome extension中的[content scripts](https://developer.chrome.com/extensions/content_scripts)中加入的css样式文件会影响当前页面的样式,然而我们往往只是想把这些样式用在自己的扩展上。所以这个扩展直接全部使用了内联样式来进行处理。但即便不是在content scripts这样特殊的运行环境上,我觉得使用react加内联样式也是个好主意。 55 | 56 | jsx和内联样式意味着一个应用的绝大多数内容都储存在了js文件中,通过我们构建使用到的js模块机制统一管理。这样的应用的代码会非常容易复用和移植。 57 | 58 | ### 2. 客户端直接爬取页面获得数据 59 | 60 | 因为安全原因而产生的浏览器跨域限制导致了一般web页面不能随意拉取跨域页面的信息,然而对于同是客户端性质的Chrome Extension(content scripts仍然会受到限制)和React Native等环境而言,虽然都是js,但却不受跨域限制,并且这一点很容易被忽略。 61 | 62 | 所有这个应用的数据来源是通过[event page](https://developer.chrome.com/extensions/event_pages)爬去页面解析而成,也正是因为这个原因,我们可以不用api,只是提取加工以下原有页面的数据即可。 63 | 64 | 而js社区中好用的、css selectors形式的静态页面解析工具非[cheerio](https://github.com/cheeriojs/cheerio)莫属。但cheerio依赖node native模块,没办法直接用在Chrome Extension或React Native上。不过没关系,我稍微修改了cheerio的代码和它的依赖的代码 - [cheerio-without-node-native](https://github.com/oyyd/cheerio-without-node-native)。你可以直接利用npm安装并用在非node环境上。 65 | 66 | ### 3. chrome extension中的event page和content scripts 67 | 68 | 像前面所说,event page不受跨域等限制,而且可以充分利用`chrome.*`上的api,具有各种丰富而强大的特性。我们在content scripts上(近似一般web页面)不能做的事情都可以放到event page上,并通过chrome api将数据发送到content scripts上。 69 | 70 | event page为了节省性能消耗会在没有唤起消息的时候进入idle状态。这点对于开发来说很麻烦,可以在开发时设置`persistent`为`true`,这样我们就可以在任何时候打开“背景”进行调试。 71 | 72 | ## 已知问题 73 | 74 | - 对iframe中的内容不生效 75 | 76 | ## 意见反馈 77 | 78 | [issues](https://github.com/oyyd/youdao-collins-chrome-extension/issues) 79 | 80 | ## LICENSE 81 | 82 | [MIT](./LICENSE.md) 83 | -------------------------------------------------------------------------------- /src/components/tips.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { number } from 'prop-types' 3 | import { Motion, spring } from 'react-motion' 4 | import { gapM, gapS, colorBorder } from './style' 5 | 6 | const CONTAINER_HEIGHT = 32 7 | const ANIMATION_TIME = 2000 8 | 9 | const styles = { 10 | popup: { 11 | position: 'absolute', 12 | left: 0, 13 | zIndex: 100, 14 | width: '100%', 15 | }, 16 | container: { 17 | textAlign: 'center', 18 | margin: '0 auto', 19 | boxSizing: 'border-box', 20 | backgroundColor: '#fff', 21 | width: '200px', 22 | overflow: 'hidden', 23 | border: `1px solid ${colorBorder}`, 24 | boxShadow: '0 2px 4px 0 rgba(34,36,38,.12), 0 2px 10px 0 rgba(34,36,38,.15)', 25 | }, 26 | } 27 | 28 | function createPaddingElement(contentString) { 29 | return ( 30 |
31 | {contentString} 32 |
33 | ) 34 | } 35 | 36 | class Tips extends Component { 37 | constructor(props) { 38 | super(props) 39 | 40 | this.flash = this.flash.bind(this) 41 | 42 | this.animationID = 0 43 | 44 | this.state = { 45 | show: false, 46 | content: '', 47 | } 48 | } 49 | 50 | flash(content) { 51 | this.animationID += 1 52 | 53 | const { animationID } = this 54 | 55 | new Promise((resolve) => { 56 | this.setState({ 57 | show: true, 58 | content, 59 | }, () => { 60 | setTimeout(() => { 61 | resolve() 62 | }, ANIMATION_TIME) 63 | }) 64 | }).then(() => { 65 | if (this.animationID !== animationID) { 66 | return 67 | } 68 | 69 | // hide 70 | this.setState({ 71 | show: false, 72 | }) 73 | }) 74 | } 75 | 76 | render() { 77 | const { top } = this.props 78 | const { show, content } = this.state 79 | const height = show ? CONTAINER_HEIGHT : spring(0) 80 | 81 | const popupStyle = Object.assign({}, styles.popup, { 82 | top, 83 | }) 84 | 85 | return ( 86 |
89 | 93 | {(interpolatingStyle) => { 94 | // eslint-disable-next-line 95 | const { height } = interpolatingStyle 96 | const display = height < 2 ? 'none' : 'block' 97 | 98 | return ( 99 |
102 | {typeof content === 'string' ? createPaddingElement(content) : content} 103 |
104 | ) 105 | }} 106 |
107 |
108 | ) 109 | } 110 | } 111 | 112 | Tips.propTypes = { 113 | top: number, 114 | } 115 | 116 | Tips.defaultProps = { 117 | top: 40, 118 | } 119 | 120 | export default Tips 121 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "youdao-collins-chrome-extension", 3 | "version": "1.2.2", 4 | "description": "A chrome extension to help you search english words in collins dict.", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "rm -rf build && cp -r assets build && node configs/build/cp_assets.js && webpack --config ./configs/build/webpack.prod.js && zip -r -X build.zip build", 8 | "dev": "rm -rf build && cp -r assets build && webpack -w --config ./configs/build/webpack.dev.js", 9 | "dev:static": "rm -rf dev && mkdir dev && cp assets/dev.html dev/index.html && cp -r assets/fonts dev/fonts && webpack -w --config ./configs/build/webpack.dev.js & http-server dev", 10 | "flow": "flow check", 11 | "test": "jest", 12 | "test:watch": "jest --watch" 13 | }, 14 | "keywords": [ 15 | "collins", 16 | "dictionary" 17 | ], 18 | "author": "oyyd ", 19 | "license": "MIT", 20 | "devDependencies": { 21 | "babel-core": "^6.24.0", 22 | "babel-eslint": "^7.2.1", 23 | "babel-jest": "^19.0.0", 24 | "babel-loader": "^6.4.1", 25 | "babel-plugin-transform-async-to-generator": "^6.22.0", 26 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 27 | "babel-polyfill": "^6.23.0", 28 | "babel-preset-es2015": "^6.24.0", 29 | "babel-preset-react": "^6.23.0", 30 | "cheerio-without-node-native": "^0.20.2", 31 | "eslint": "^3.19.0", 32 | "eslint-config-airbnb": "^14.1.0", 33 | "eslint-plugin-flowtype": "^2.30.4", 34 | "eslint-plugin-flowtype-errors": "^3.0.3", 35 | "eslint-plugin-import": "^2.2.0", 36 | "eslint-plugin-jsx-a11y": "^4.0.0", 37 | "eslint-plugin-react": "^6.10.3", 38 | "flow-bin": "^0.43.1", 39 | "http-server": "^0.11.1", 40 | "jest": "^19.0.2", 41 | "prop-types": "^15.5.8", 42 | "react": "^15.4.2", 43 | "react-dom": "^15.4.2", 44 | "react-motion": "^0.5.0", 45 | "textarea-caret": "^3.1.0", 46 | "webpack": "^2.3.2" 47 | }, 48 | "babel": { 49 | "presets": [ 50 | "es2015", 51 | "react" 52 | ], 53 | "plugins": [ 54 | "transform-async-to-generator", 55 | "transform-flow-strip-types" 56 | ] 57 | }, 58 | "eslintConfig": { 59 | "parser": "babel-eslint", 60 | "extends": [ 61 | "airbnb", 62 | "plugin:flowtype/recommended" 63 | ], 64 | "plugins": [ 65 | "flowtype", 66 | "flowtype-errors" 67 | ], 68 | "rules": { 69 | "no-mixed-operators": 0, 70 | "flowtype-errors/show-errors": 2, 71 | "import/no-extraneous-dependencies": 0, 72 | "react/jsx-filename-extension": 0, 73 | "react/prefer-stateless-function": 0, 74 | "react/no-array-index-key": 0, 75 | "react/forbid-prop-types": 0, 76 | "react/sort-comp": 0, 77 | "react/no-unescaped-entities": 0, 78 | "react/jsx-no-target-blank": 0, 79 | "jsx-a11y/no-static-element-interactions": 0, 80 | "import/prefer-default-export": 0, 81 | "semi": [ 82 | 2, 83 | "never" 84 | ] 85 | }, 86 | "env": { 87 | "browser": true, 88 | "jest": true 89 | }, 90 | "globals": { 91 | "chrome": true 92 | } 93 | }, 94 | "jest": { 95 | "testRegex": "/src/__tests__/.+spec\\.js$" 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/components/popup_app.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react' 3 | import Searcher from './searcher' 4 | import Detail from './detail' 5 | import { searchWord, openLink } from '../message' 6 | import type { WordResponseType } from '../parse' 7 | 8 | const styles = { 9 | detailContainer: { 10 | padding: '6px 13px', 11 | }, 12 | container: { 13 | position: 'relative', 14 | }, 15 | panelPlaceholder: { 16 | height: 38, 17 | }, 18 | panel: { 19 | width: '100%', 20 | position: 'fixed', 21 | top: 0, 22 | left: 0, 23 | zIndex: 10, 24 | display: 'flex', 25 | }, 26 | } 27 | 28 | function shouldPush(history, newItem) { 29 | if (!Array.isArray(history) || history.length < 1) { 30 | return true 31 | } 32 | 33 | const lastItem = history[history.length - 1] 34 | 35 | return !(lastItem.type === 'explain' && newItem.type === 'explain' 36 | && lastItem.response.wordInfo.word === newItem.response.wordInfo.word) 37 | } 38 | 39 | class Popup extends Component { 40 | state: { 41 | explain: ?WordResponseType, 42 | history: Array, 43 | currentWord: string, 44 | } 45 | 46 | constructor(props: any) { 47 | super(props); 48 | 49 | // awful 50 | (this: any).search = this.search.bind(this); 51 | (this: any).jumpBack = this.jumpBack.bind(this) 52 | 53 | this.state = { 54 | explain: null, 55 | history: [], 56 | currentWord: '', 57 | } 58 | } 59 | 60 | jumpBack() { 61 | const { history: oriHistory } = this.state 62 | const history = oriHistory.slice(0, oriHistory.length - 1) 63 | 64 | this.setState({ 65 | explain: history[history.length - 1], 66 | history, 67 | }) 68 | } 69 | 70 | search(word: string) { 71 | if (!word) { 72 | return 73 | } 74 | 75 | searchWord(word).then((res) => { 76 | let { history } = this.state 77 | const push = shouldPush(history, res) 78 | 79 | if (push) { 80 | history = history.slice() 81 | history.push(res) 82 | } 83 | 84 | this.setState({ 85 | currentWord: word, 86 | explain: res, 87 | history, 88 | }) 89 | }).catch((/* err */) => { 90 | // TODO: 91 | }) 92 | } 93 | 94 | render() { 95 | const { search, jumpBack } = this 96 | const { explain, history, currentWord } = this.state 97 | 98 | return ( 99 |
100 |
101 |
102 | 107 |
108 | {explain ? ( 109 |
110 | 118 |
119 | ) : null} 120 |
121 | ) 122 | } 123 | } 124 | 125 | export default Popup 126 | -------------------------------------------------------------------------------- /src/event_page.js: -------------------------------------------------------------------------------- 1 | import { EVENTS, onMessage } from './message' 2 | import { parse } from './parse' 3 | import { getWordURL, have } from './words' 4 | import ShanbayOauth from './lib/shanbay_oauth2' 5 | 6 | const { CLEAR_SHANBAY_TOKEN, SEARCH_WORD, OPEN_NEW_TAB, ADD_WORD_SHANBAY } = EVENTS 7 | let oauth = null 8 | 9 | async function getWordExplain(body) { 10 | const explain = parse(body) 11 | 12 | if (explain && explain.wordInfo && explain.wordInfo.word) { 13 | const word = explain.wordInfo.word 14 | const added = await have(word) 15 | explain.added = added 16 | } 17 | 18 | return explain 19 | } 20 | 21 | async function getWords(word, sendRes) { 22 | const url = getWordURL(word) 23 | const body = await fetch(url).then(res => res.text()) 24 | const explain = await getWordExplain(body) 25 | 26 | sendRes(explain) 27 | } 28 | 29 | function authorize() { 30 | return new Promise((resolve) => { 31 | oauth.authorize(() => { 32 | resolve() 33 | }) 34 | }) 35 | } 36 | 37 | const ADD_WORD_URL = 'https://api.shanbay.com/bdc/learning/' 38 | const SEARCH_WORD_URL = 'https://api.shanbay.com/bdc/search/' 39 | 40 | function getWord(url) { 41 | return fetch(url).then(res => res.json()).then((res) => { 42 | const { data, msg } = res 43 | 44 | if (msg !== 'SUCCESS') { 45 | throw new Error(msg) 46 | } 47 | 48 | return data 49 | }) 50 | } 51 | 52 | function clearShanbayToken() { 53 | oauth.clearToken() 54 | } 55 | 56 | async function addWordToShanbay(data, sendRes) { 57 | // redirect 58 | if (!oauth.token_valid()) { 59 | await authorize() 60 | } 61 | 62 | const token = oauth.access_token() 63 | 64 | const searchURL = `${SEARCH_WORD_URL}?word=${data}&access_token=${token}` 65 | 66 | let response = null 67 | 68 | try { 69 | response = await getWord(searchURL) 70 | } catch (err) { 71 | // 如果token失效,清除token并重新添加 72 | if (err.message === 'Invalid token') { 73 | clearShanbayToken() 74 | await addWordToShanbay(data, sendRes) 75 | return 76 | } 77 | 78 | sendRes({ 79 | success: false, 80 | msg: err.message, 81 | }) 82 | 83 | return 84 | } 85 | 86 | const { id } = response 87 | 88 | const addWordURL = `${ADD_WORD_URL}?access_token=${token}` 89 | 90 | try { 91 | response = await fetch(addWordURL, { 92 | method: 'POST', 93 | headers: { 94 | 'Content-Type': 'application/json', 95 | }, 96 | body: JSON.stringify({ 97 | id, 98 | access_token: token, 99 | }), 100 | }).then(res => res.json()).then((res) => { 101 | const { data: d, msg } = res 102 | 103 | if (msg !== 'SUCCESS') { 104 | throw new Error(msg) 105 | } 106 | 107 | return d 108 | }) 109 | } catch (err) { 110 | sendRes({ 111 | success: false, 112 | msg: err.message, 113 | }) 114 | 115 | return 116 | } 117 | 118 | sendRes({ 119 | success: true, 120 | }) 121 | } 122 | 123 | function openNewTab(word) { 124 | chrome.tabs.create({ url: getWordURL(word) }) 125 | } 126 | 127 | function init() { 128 | oauth = ShanbayOauth.initPage() 129 | 130 | onMessage(SEARCH_WORD, (data, sendRes) => { 131 | getWords(data, sendRes) 132 | }) 133 | 134 | onMessage(OPEN_NEW_TAB, (data) => { 135 | openNewTab(data) 136 | }) 137 | 138 | onMessage(ADD_WORD_SHANBAY, (data, sendRes) => { 139 | addWordToShanbay(data, sendRes) 140 | }) 141 | 142 | onMessage(CLEAR_SHANBAY_TOKEN, () => { 143 | clearShanbayToken() 144 | }) 145 | } 146 | 147 | init() 148 | -------------------------------------------------------------------------------- /src/__tests__/parse.spec.js: -------------------------------------------------------------------------------- 1 | import { parse } from '../parse' 2 | import pagePage from './pages/page' 3 | import performPage from './pages/perform' 4 | import noresponsePage from './pages/noresponse' 5 | import choicesPage from './pages/choices' 6 | import newestPage from './pages/newest' 7 | import deficitsPage from './pages/deficits' 8 | import openPage from './pages/open' 9 | import sentencePage from './pages/sentence' 10 | import favorablePage from './pages/favorable' 11 | import dimensionalPage from './pages/dimensional' 12 | 13 | const EXPECTED_KEYS = [ 14 | 'word', 15 | 'pronunciation', 16 | 'frequence', 17 | 'rank', 18 | 'additionalPattern', 19 | ] 20 | 21 | describe('parse', () => { 22 | describe('explain', () => { 23 | it('should have a "type" and a "meanings" and all expected keys in "wordInfo"', () => { 24 | const { type, response: { wordInfo, meanings } } = parse(pagePage) 25 | 26 | expect(type).toBe('explain') 27 | 28 | EXPECTED_KEYS.forEach((key) => { 29 | expect(wordInfo[key]).toBeTruthy() 30 | }) 31 | 32 | expect(Array.isArray(meanings)).toBe(true) 33 | }) 34 | 35 | it('should have all expected keys', () => { 36 | const { response: { wordInfo, meanings } } = parse(performPage) 37 | 38 | EXPECTED_KEYS.forEach((key) => { 39 | expect(wordInfo[key]).toBeTruthy() 40 | }) 41 | 42 | expect(Array.isArray(meanings)).toBe(true) 43 | }) 44 | 45 | it('should parse multiple explains', () => { 46 | const { response: { wordInfo, meanings } } = parse(openPage) 47 | 48 | EXPECTED_KEYS.forEach((key) => { 49 | expect(wordInfo[key]).toBeTruthy() 50 | }) 51 | 52 | expect(Array.isArray(meanings)).toBe(true) 53 | }) 54 | }) 55 | 56 | describe('noresponse', () => { 57 | it('should return "error" type', () => { 58 | const { type } = parse(noresponsePage) 59 | expect(type).toBe('error') 60 | }) 61 | }) 62 | 63 | describe('choices', () => { 64 | it('should return "choices" type and choices response', () => { 65 | const { type, response: { choices } } = parse(choicesPage) 66 | 67 | expect(type).toBe('choices') 68 | expect(Array.isArray(choices)).toBe(true) 69 | expect(choices[0].words[0].indexOf('tear down')).toBe(0) 70 | expect(choices[0].wordType).toBe('v.') 71 | expect(choices[1].words[0].indexOf('dismantle')).toBe(0) 72 | expect(choices[1].wordType).toBe('vt.') 73 | }) 74 | }) 75 | 76 | describe('non_collins_explain', () => { 77 | it('should return "non_collins_explain" type', () => { 78 | const { type, response: { wordInfo, explains } } = parse(newestPage) 79 | const { word, pronunciation } = wordInfo 80 | 81 | expect(word).toBe('newest') 82 | expect(pronunciation).toBe('[nju:ɪst]') 83 | expect(type).toBe('non_collins_explain') 84 | expect(Array.isArray(explains)).toBe(true) 85 | expect(explains[0].type).toBe('') 86 | expect(explains[0].explain).toBe('最新') 87 | }) 88 | 89 | it('should return "non_collins_explain" type too', () => { 90 | const { type, response: { wordInfo, explains } } = parse(deficitsPage) 91 | const { word, pronunciation } = wordInfo 92 | 93 | expect(word).toBe('deficits') 94 | expect(pronunciation).toBe("['defɪsɪts]") 95 | expect(type).toBe('non_collins_explain') 96 | expect(Array.isArray(explains)).toBe(true) 97 | expect(explains[0].type).toBe('n') 98 | expect(explains[0].explain).toBe('[财政] 赤字,亏损(deficit的复数形式)') 99 | }) 100 | 101 | it('should return "machine_translation" type and the correspond response', () => { 102 | const { type, response } = parse(sentencePage) 103 | 104 | expect(type).toBe('machine_translation') 105 | expect(response.translation).toBe('可视化工具的目的是构建可视化。') 106 | }) 107 | }) 108 | 109 | describe('synonyms', () => { 110 | it('should get synonyms info if there is', () => { 111 | const { type, response } = parse(favorablePage) 112 | 113 | expect(type).toBe('explain') 114 | expect(response.synonyms.type).toBe('[美国英语]') 115 | expect(response.synonyms.words[0]).toBe('favourable') 116 | expect(response.synonyms.hrefs[0]).toBe('/w/favourable/?keyfrom=dict.collins') 117 | }) 118 | }) 119 | 120 | describe('dimensional', () => { 121 | it('should get synonyms with multiple words and hrefs', () => { 122 | const { type, response } = parse(dimensionalPage) 123 | 124 | expect(type).toBe('explain') 125 | expect(response.synonyms.type).toBe('') 126 | expect(response.synonyms.words).toEqual([ 127 | 'two-dimensional', 128 | 'three-dimensional', 129 | ]) 130 | expect(response.synonyms.hrefs).toEqual([ 131 | '/w/two-dimensional/?keyfrom=dict.collins', 132 | '/w/three-dimensional/?keyfrom=dict.collins', 133 | ]) 134 | }) 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /src/components/content_app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import Detail from './detail' 4 | import { searchWord, openLink } from '../message' 5 | import { gapS, colorMuted } from './style' 6 | 7 | const WIDTH = 400 8 | const MAX_HEIGHT = 300 9 | const PADDING_LEFT = 20 10 | const ASSUME_LINE_HEIGHT = 20 11 | const LEFT_PAD_PERCENTAGE = 1 / 3 12 | 13 | const styles = { 14 | container: { 15 | padding: `${gapS}px`, 16 | border: `1px solid ${colorMuted}`, 17 | position: 'absolute', 18 | boxSizing: 'border-box', 19 | zIndex: 1000000, 20 | width: WIDTH, 21 | maxHeight: MAX_HEIGHT, 22 | overflow: 'auto', 23 | backgroundColor: '#fff', 24 | }, 25 | } 26 | 27 | function getOffsets() { 28 | const body = document.querySelector('body') 29 | const scrollTop = window.pageYOffset || document.scrollTop || body.scrollTop 30 | const scrollLeft = window.pageXOffset || document.scrollLeft || body.scrollLeft 31 | 32 | return { top: Math.round(scrollTop), left: Math.round(scrollLeft) } 33 | } 34 | 35 | function getViewportSize() { 36 | return { 37 | width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0), 38 | height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0), 39 | } 40 | } 41 | 42 | function getPositionAdjustment(position) { 43 | const { left } = position 44 | const { width } = getViewportSize() 45 | const { left: offsetLeft } = getOffsets() 46 | 47 | // const maxHeight = offsetTop + height 48 | // const elementHeight = top + containerHeight 49 | const allowLeftAdjustment = width > WIDTH 50 | const shouldLeftAdjustment = left + (WIDTH * LEFT_PAD_PERCENTAGE) > width + offsetLeft 51 | 52 | let leftAdjustment = 0 53 | 54 | if (allowLeftAdjustment && shouldLeftAdjustment) { 55 | leftAdjustment = left + (WIDTH * LEFT_PAD_PERCENTAGE) - width - offsetLeft 56 | } 57 | 58 | return { 59 | atTop: false, 60 | leftAdjustment, 61 | } 62 | } 63 | 64 | function getLayoutPosition(position, lineHeight) { 65 | const padHeight = (typeof lineHeight === 'number') 66 | ? Math.max(ASSUME_LINE_HEIGHT, lineHeight) : ASSUME_LINE_HEIGHT 67 | const { leftAdjustment } = getPositionAdjustment(position) 68 | 69 | let { top, left } = position 70 | const originLeftPos = left - (WIDTH * LEFT_PAD_PERCENTAGE) - leftAdjustment 71 | 72 | left = Math.max(originLeftPos, PADDING_LEFT) 73 | top += padHeight 74 | 75 | return { 76 | top, left, 77 | } 78 | } 79 | 80 | class ContentApp extends Component { 81 | constructor(props) { 82 | super(props) 83 | 84 | const { content } = props 85 | 86 | this.search = this.search.bind(this) 87 | 88 | this.state = { 89 | containerHeight: MAX_HEIGHT, 90 | display: false, 91 | isLoading: false, 92 | explain: null, 93 | currentWord: content, 94 | } 95 | } 96 | 97 | componentDidMount() { 98 | this.search(this.state.currentWord) 99 | } 100 | 101 | componentWillReceiveProps(newProps) { 102 | const { content } = newProps 103 | 104 | if (content !== this.state.currentWord) { 105 | this.search(content) 106 | } 107 | } 108 | 109 | search(word) { 110 | if (!word) { 111 | return 112 | } 113 | 114 | let fullfilled = false 115 | 116 | this.setState({ 117 | currentWord: word, 118 | display: false, 119 | isLoading: true, 120 | }) 121 | 122 | // try to not change screen too frequently 123 | setTimeout(() => { 124 | if (!fullfilled) { 125 | this.setState({ 126 | display: true, 127 | }) 128 | } 129 | }, 1000) 130 | 131 | searchWord(word).then((res) => { 132 | if (word !== this.state.currentWord) { 133 | return 134 | } 135 | 136 | this.setState({ 137 | display: true, 138 | isLoading: false, 139 | currentWord: word, 140 | explain: res, 141 | }) 142 | 143 | fullfilled = true 144 | }).catch((err) => { // eslint-disable-line 145 | // TODO: 146 | fullfilled = true 147 | }) 148 | } 149 | 150 | render() { 151 | const { search } = this 152 | const { options, lineHeight, hide, position } = this.props 153 | const { explain, isLoading, display, currentWord } = this.state 154 | 155 | if (hide) { 156 | return null 157 | } 158 | 159 | const containerStyle = Object.assign({}, 160 | styles.container, 161 | getLayoutPosition(position, lineHeight), 162 | !display ? { 163 | display: 'none', 164 | } : null, 165 | ) 166 | 167 | return ( 168 |
171 | {isLoading ? ( 172 |
正在加载 "{currentWord}" ...
173 | ) : ( 174 | 182 | )} 183 |
184 | ) 185 | } 186 | } 187 | 188 | const { object, string, bool, number } = PropTypes 189 | 190 | ContentApp.propTypes = { 191 | lineHeight: number, 192 | hide: bool.isRequired, 193 | position: object.isRequired, 194 | content: string.isRequired, 195 | options: object.isRequired, 196 | } 197 | 198 | ContentApp.defaultProps = { 199 | lineHeight: ASSUME_LINE_HEIGHT, 200 | } 201 | 202 | export default ContentApp 203 | -------------------------------------------------------------------------------- /src/content_script.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import getCaretCoordinates from 'textarea-caret' 4 | import ContentApp from './components/content_app' 5 | import { styleContainer } from './utils' 6 | import { getOptions } from './options' 7 | 8 | const CONTAINER_ID = 'ycce-container' 9 | 10 | function getShouldDisplay( 11 | activeType, 12 | activeKeyPressed, 13 | isDBClick, 14 | tempDisabled, 15 | ) { 16 | if (tempDisabled) { 17 | return false 18 | } 19 | 20 | if (activeType === 'NEVER') { 21 | return false 22 | } 23 | 24 | if (activeType === 'ALWAYS') { 25 | return true 26 | } 27 | 28 | if (activeType === 'DOUBLE_CLICK') { 29 | return isDBClick 30 | } 31 | 32 | return activeKeyPressed 33 | } 34 | 35 | function createContainer() { 36 | const containerEle = document.createElement('div') 37 | containerEle.id = CONTAINER_ID 38 | containerEle.style.fontSize = '14px' 39 | 40 | styleContainer(containerEle) 41 | 42 | return containerEle 43 | } 44 | 45 | function getContainer() { 46 | let containerEle = document.querySelector(`#${CONTAINER_ID}`) 47 | 48 | if (containerEle) { 49 | return containerEle 50 | } 51 | 52 | containerEle = createContainer() 53 | document.querySelector('body').appendChild(containerEle) 54 | 55 | return containerEle 56 | } 57 | 58 | function getPosition(selection) { 59 | let range = null 60 | let rect 61 | 62 | try { 63 | range = selection.getRangeAt(0) 64 | } catch (err) { 65 | return null 66 | } 67 | 68 | const elem = range.startContainer.firstElementChild 69 | if (elem !== undefined) { 70 | if (elem.nodeName === 'INPUT' || elem.nodeName === 'TEXTAREA') { 71 | const { top, left } = elem.getBoundingClientRect() 72 | const rectStart = getCaretCoordinates(elem, elem.selectionStart) 73 | const rectEnd = getCaretCoordinates(elem, elem.selectionEnd) 74 | if (!rectEnd) { 75 | return null 76 | } 77 | rect = { 78 | top: top + rectEnd.top, 79 | left: left + rectEnd.left, 80 | width: rectEnd.left - rectStart.left, 81 | } 82 | } 83 | } else { 84 | rect = range.getBoundingClientRect() 85 | } 86 | 87 | const { top, left, width } = rect 88 | 89 | return { 90 | left: left + window.pageXOffset + width / 2, 91 | top: top + window.pageYOffset, 92 | } 93 | } 94 | 95 | function getActiveKeyPressed(event) { 96 | const { metaKey, ctrlKey } = event 97 | const activeKeyPressed = metaKey || ctrlKey 98 | 99 | return activeKeyPressed 100 | } 101 | 102 | function getElementLineHeight(node) { 103 | const ele = node.parentElement 104 | const text = window 105 | .getComputedStyle(ele, null) 106 | .getPropertyValue('line-height') 107 | const res = /(.+)px/.exec(text) 108 | 109 | if (res === null) { 110 | return null 111 | } 112 | 113 | return parseFloat(res[1]) 114 | } 115 | 116 | function isClickContainer(event) { 117 | const containerEle = getContainer() 118 | 119 | return containerEle.contains(event.target) 120 | } 121 | 122 | function createSelectionStream(next, options) { 123 | const { activeType } = options 124 | let isSelecting = false 125 | 126 | document.addEventListener( 127 | 'selectionchange', 128 | () => { 129 | const containerEle = getContainer() 130 | const selection = window.getSelection() 131 | 132 | if (containerEle.contains(selection.baseNode)) { 133 | return 134 | } 135 | 136 | const content = selection.toString().trim() 137 | 138 | if (content) { 139 | isSelecting = true 140 | return 141 | } 142 | 143 | isSelecting = false 144 | }, 145 | false, 146 | ) 147 | 148 | // Handle mouse event to decide should we show popup. 149 | const handler = (event, isDBClick) => { 150 | let shouldDisplay = false 151 | let activeKeyPressed = null 152 | 153 | if (!isDBClick) { 154 | activeKeyPressed = getActiveKeyPressed(event) 155 | } 156 | 157 | shouldDisplay = getShouldDisplay( 158 | activeType, 159 | activeKeyPressed, 160 | isDBClick, 161 | options.tempDisabled, 162 | ) 163 | 164 | if (!isSelecting || !shouldDisplay) { 165 | return 166 | } 167 | 168 | isSelecting = false 169 | 170 | next(options, false) 171 | } 172 | 173 | document.addEventListener('dblclick', (event) => { 174 | if (activeType !== 'DOUBLE_CLICK' || isClickContainer(event)) { 175 | return 176 | } 177 | 178 | handler(event, true) 179 | }) 180 | 181 | document.addEventListener('mouseup', (event) => { 182 | const clickContainer = isClickContainer(event) 183 | 184 | if (!isSelecting && !clickContainer) { 185 | next(options, true) 186 | return 187 | } 188 | 189 | if (activeType === 'DOUBLE_CLICK' || clickContainer) { 190 | return 191 | } 192 | 193 | handler(event, false) 194 | }) 195 | } 196 | 197 | function hasChinese(text) { 198 | if (/.*[\u4e00-\u9fa5]+.*$/.test(text)) { 199 | // 包含汉字 200 | return true 201 | } 202 | return false 203 | } 204 | 205 | // Render popup 206 | function render(options, hide) { 207 | const selection = window.getSelection() 208 | const position = getPosition(selection) 209 | 210 | if (!position) { 211 | return 212 | } 213 | 214 | const content = selection.toString().trim() 215 | 216 | if (!options.showContainChinese && hasChinese(content)) { 217 | return 218 | } 219 | 220 | const containerEle = getContainer() 221 | const node = selection.baseNode 222 | const lineHeight = getElementLineHeight(node) 223 | ReactDOM.render( 224 | , 231 | containerEle, 232 | ) 233 | } 234 | 235 | function main() { 236 | getOptions().then((options) => { 237 | chrome.runtime.onMessage.addListener((msg) => { 238 | if (msg.type !== 'ycce') { 239 | return 240 | } 241 | options.tempDisabled = msg.tempDisabled 242 | }) 243 | createSelectionStream(render, options) 244 | }) 245 | } 246 | 247 | main() 248 | -------------------------------------------------------------------------------- /src/components/searcher.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import icons from './icons' 4 | import { btn } from './style' 5 | import { setOptions, getOptions } from '../options' 6 | 7 | const { func, array } = PropTypes 8 | 9 | const POINT_SIZE = 16 10 | const activeColor = '#76E15F' 11 | const inactiveColor = '#000' 12 | 13 | const styles = { 14 | activeBtnLight: { 15 | display: 'inline-block', 16 | width: POINT_SIZE, 17 | height: POINT_SIZE, 18 | borderRadius: Math.floor(POINT_SIZE / 2), 19 | }, 20 | activeBtn: { 21 | width: 30, 22 | height: 30, 23 | boxSizing: 'border-box', 24 | padding: '12px 0 0 6px', 25 | cursor: 'pointer', 26 | }, 27 | gearIcon: { 28 | width: 30, 29 | height: 30, 30 | }, 31 | searchIcon: { 32 | width: 18, 33 | height: 18, 34 | marginLeft: 11, 35 | }, 36 | backIcon: { 37 | width: 26, 38 | height: 26, 39 | marginLeft: 6, 40 | }, 41 | inputGroup: { 42 | position: 'relative', 43 | width: '100%', 44 | display: 'flex', 45 | flex: 1, 46 | }, 47 | input: { 48 | boxShadow: 0, 49 | outlineWidth: 0, 50 | display: 'flex', 51 | flex: 1, 52 | flexDirection: 'column', 53 | justifyContent: 'center', 54 | padding: '.5rem .75rem', 55 | fontSize: '1rem', 56 | lineHeight: 1.25, 57 | color: '#464a4c', 58 | backgroundColor: '#fff', 59 | backgroundImage: 'none', 60 | backgroundClip: 'padding-box', 61 | border: '1px solid rgba(0,0,0,.15)', 62 | borderRadius: '.25rem', 63 | transition: 64 | 'border-color ease-in-out .15s,box-shadow ease-in-out .15s,-webkit-box-shadow ease-in-out .15s', 65 | borderBottomRightRadius: 0, 66 | borderTopRightRadius: 0, 67 | }, 68 | searchBtn: Object.assign({}, btn, { 69 | padding: '0px', 70 | borderLeft: '0px', 71 | borderTopLeftRadius: '0px', 72 | borderBottomLeftRadius: '0px', 73 | }), 74 | backBtn: Object.assign({}, btn, { 75 | padding: '0px', 76 | borderLeft: '0px', 77 | borderTopLeftRadius: '0px', 78 | borderBottomLeftRadius: '0px', 79 | borderTopRightRadius: '0px', 80 | borderBottomRightRadius: '0px', 81 | }), 82 | } 83 | 84 | function openOptionsPage() { 85 | if (chrome.runtime.openOptionsPage) { 86 | // New way to open options pages, if supported (Chrome 42+). 87 | chrome.runtime.openOptionsPage() 88 | } else { 89 | // Reasonable fallback. 90 | window.open(chrome.runtime.getURL('options.html')) 91 | } 92 | } 93 | 94 | class Searcher extends Component { 95 | constructor(props) { 96 | super(props) 97 | 98 | this.onInputKey = this.onInputKey.bind(this) 99 | this.triggerSearch = this.triggerSearch.bind(this) 100 | this.shouldSearch = this.shouldSearch.bind(this) 101 | this.changeTempDisabled = this.changeTempDisabled.bind(this) 102 | 103 | this.refers = {} 104 | 105 | this.options = null 106 | 107 | this.state = { 108 | inputContent: '', 109 | tempDisabled: false, 110 | } 111 | } 112 | 113 | componentDidMount() { 114 | this.refers.input.focus() 115 | 116 | getOptions().then(options => { 117 | const { tempDisabled = false } = options 118 | 119 | this.setState({ tempDisabled }) 120 | this.options = options 121 | }) 122 | } 123 | 124 | changeTempDisabled() { 125 | const { tempDisabled } = this.state 126 | 127 | this.setState({ 128 | tempDisabled: !tempDisabled, 129 | }) 130 | 131 | setOptions( 132 | Object.assign({}, this.options, { 133 | tempDisabled: !tempDisabled, 134 | }), 135 | ) 136 | 137 | // Tell content_script about that should it disable translating or not. 138 | chrome.tabs.query({}, (tabs) => { 139 | tabs.forEach((tab) => { 140 | chrome.tabs.sendMessage(tab.id, { 141 | type: 'ycce', 142 | tempDisabled: !tempDisabled, 143 | }) 144 | }) 145 | }) 146 | } 147 | 148 | onInputKey(e) { 149 | this.setState({ inputContent: e.target.value }) 150 | } 151 | 152 | shouldSearch(e) { 153 | const { key } = e 154 | 155 | if (key === 'Enter') { 156 | this.triggerSearch() 157 | } 158 | } 159 | 160 | triggerSearch() { 161 | const { search } = this.props 162 | const { inputContent } = this.state 163 | 164 | search(inputContent) 165 | 166 | // Select all text after triggering search. 167 | const { input } = this.refers 168 | 169 | if (!input) { 170 | return 171 | } 172 | 173 | input.setSelectionRange(0, input.value.length) 174 | } 175 | 176 | render() { 177 | const { inputContent, tempDisabled } = this.state 178 | const { onInputKey, triggerSearch, shouldSearch } = this 179 | const { history, jumpBack } = this.props 180 | const activeBtnTitle = tempDisabled 181 | ? '划词翻译已经关闭' 182 | : '划词翻译已经启用' 183 | 184 | return ( 185 |
186 | (this.refers.input = input)} 188 | type="text" 189 | style={styles.input} 190 | placeholder="请输入单词" 191 | aria-describedby="basic-addon2" 192 | value={inputContent} 193 | onChange={onInputKey} 194 | onKeyPress={shouldSearch} 195 | /> 196 | {history.length > 1 ? ( 197 | 198 | back 199 | 200 | ) : null} 201 | 202 | search 203 | 204 | 205 | gear 206 | 207 |
208 | 214 |
215 |
216 | ) 217 | } 218 | } 219 | 220 | Searcher.propTypes = { 221 | search: func.isRequired, 222 | jumpBack: func.isRequired, 223 | history: array.isRequired, 224 | } 225 | 226 | export default Searcher 227 | -------------------------------------------------------------------------------- /src/components/options_app.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { getOptions, setOptions, 3 | ACTIVE_TYPES, SHOW_NOTEBOOK_OPTIONS } from '../options' 4 | import { getWordsPage } from '../words' 5 | import { colorPrimary } from './style' 6 | import { clearShanbayToken } from '../message' 7 | 8 | const FONT_SIZE = 14 9 | const SIZE = 12 10 | 11 | const styles = { 12 | container: { 13 | fontSize: FONT_SIZE, 14 | }, 15 | adviseLink: { 16 | fontSize: SIZE, 17 | textDecoration: 'none', 18 | float: 'right', 19 | marginTop: 16, 20 | }, 21 | item: { 22 | marginTop: 10, 23 | }, 24 | itemTitle: { 25 | 26 | }, 27 | activeTypeContainer: { 28 | marginTop: 8, 29 | fontSize: SIZE, 30 | }, 31 | activeTypeItem: { 32 | cursor: 'pointer', 33 | margin: '4px 0 4px 0', 34 | }, 35 | label: { 36 | marginLeft: 6, 37 | }, 38 | radio: { 39 | verticalAlign: 'top', 40 | width: SIZE, 41 | height: SIZE, 42 | }, 43 | saveBtn: { 44 | marginTop: 8, 45 | fontSize: 12, 46 | padding: 0, 47 | cursor: 'pointer', 48 | }, 49 | saveTips: { 50 | marginTop: 6, 51 | fontSize: 12, 52 | color: colorPrimary, 53 | }, 54 | } 55 | 56 | class App extends Component { 57 | constructor(props) { 58 | super(props) 59 | 60 | this.wordsPage = getWordsPage() 61 | 62 | this.saveOptions = this.saveOptions.bind(this) 63 | this.clearToken = this.clearToken.bind(this) 64 | 65 | this.state = { 66 | hasClearToken: false, 67 | saveTips: false, 68 | inited: false, 69 | options: null, 70 | } 71 | } 72 | 73 | componentDidMount() { 74 | getOptions().then((options) => { 75 | this.setState({ 76 | inited: true, 77 | options, 78 | }) 79 | }) 80 | } 81 | 82 | saveOptions() { 83 | const { options } = this.state 84 | 85 | this.setState({ 86 | saveTips: false, 87 | }) 88 | 89 | setOptions(options).then(() => { 90 | this.setState({ 91 | saveTips: true, 92 | }) 93 | }) 94 | } 95 | 96 | changeOptions(type, value) { 97 | const { options } = this.state 98 | 99 | const nextOptions = Object.assign({}, options, { 100 | [type]: value, 101 | }) 102 | 103 | this.setState({ 104 | options: nextOptions, 105 | }) 106 | } 107 | 108 | clearToken() { 109 | this.setState({ 110 | hasClearToken: false, 111 | }) 112 | 113 | clearShanbayToken() 114 | 115 | this.setState({ 116 | hasClearToken: true, 117 | }) 118 | } 119 | 120 | render() { 121 | const { clearToken, saveOptions, wordsPage } = this 122 | const { saveTips, options, inited, hasClearToken } = this.state 123 | 124 | // eslint-disable-next-line 125 | const changeActiveType = this.changeOptions.bind(this, 'activeType') 126 | // eslint-disable-next-line 127 | const changeShowNotebook = this.changeOptions.bind(this, 'showNotebook') 128 | // eslint-disable-next-line 129 | const changeShowContainChinese = this.changeOptions.bind(this, 'showContainChinese') 130 | 131 | if (!inited) { 132 | return null 133 | } 134 | 135 | const showNotebook = options.showNotebook 136 | const activeType = options.activeType 137 | const showContainChinese = options.showContainChinese 138 | 139 | return ( 140 |
141 |
142 |
扇贝单词本设置:
143 | {hasClearToken ? ( 144 |
145 | 清除成功 146 |
147 | ) : null} 148 |
149 | 152 |
153 |
154 |
159 |
164 | 打开 165 | 174 | 单词本 175 | 176 |
177 |
开启生词本:
178 |
179 |
changeShowNotebook(true)} 182 | > 183 | 189 | 开启 190 |
191 |
changeShowNotebook(false)} 194 | > 195 | 201 | 关闭 202 |
203 |
204 |
205 |
206 |
207 | 划词翻译设置: 208 |
209 |
210 | {Object.keys(ACTIVE_TYPES).map(type => ( 211 |
changeActiveType(type)} 215 | > 216 | 222 | {ACTIVE_TYPES[type]} 223 |
224 | ))} 225 |
226 |
227 |
228 |
229 | 中文翻译设置: 230 |
231 |
232 |
changeShowContainChinese(!showContainChinese)} 235 | > 236 | 242 | 包含中文时显示翻译 243 |
244 |
245 |
246 |
247 |
保存后重新刷新页面生效
248 | {saveTips ? ( 249 |
250 | 保存成功 251 |
252 | ) : null} 253 |
254 | 260 | 265 | 意见/bug反馈 266 | 267 |
268 |
269 |
270 | ) 271 | } 272 | } 273 | 274 | export default App 275 | -------------------------------------------------------------------------------- /src/parse.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import cheerio from 'cheerio-without-node-native' 3 | 4 | type MeaningExplainType = { 5 | type: string, 6 | typeDesc: string, 7 | engExplain: string, 8 | } 9 | 10 | type MeaningType = { 11 | explain: MeaningExplainType, 12 | example: { 13 | ch: string, 14 | eng: string, 15 | } 16 | } 17 | 18 | export type WordInfoType = {| 19 | word: string, 20 | pronunciation: string, 21 | frequence: number | null, 22 | rank: string, 23 | additionalPattern: string, 24 | |} 25 | 26 | export type MeaningsType = Array 27 | 28 | export type SynonymsType = {| 29 | type: string, 30 | hrefs: Array, 31 | words: Array, 32 | |} 33 | 34 | export type ExplainResponseType = { 35 | wordInfo: WordInfoType, 36 | synonyms: SynonymsType, 37 | meanings: MeaningsType, 38 | } 39 | 40 | type ChoiceType = { 41 | wordType: string, 42 | words: Array, 43 | } 44 | 45 | export type ChoiceResponseType = {| 46 | choices: Array 47 | |} 48 | 49 | export type NonCollinsExplainType = {| 50 | type: string, 51 | explain: string, 52 | |} 53 | 54 | export type NonCollinsExplainsType = Array 55 | 56 | export type NonCollinsExplainsResponseType = { 57 | wordInfo: WordInfoType, 58 | explains: NonCollinsExplainsType, 59 | } 60 | 61 | export type MachineTranslationResponseType = {| 62 | translation: string, 63 | |} 64 | 65 | export type WordResponseType = {| 66 | type: 'explain', 67 | response: ExplainResponseType, 68 | |} | {| 69 | type: 'choices', 70 | response: ChoiceResponseType, 71 | |} | {| 72 | type: 'error', 73 | |} | {| 74 | type: 'non_collins_explain', 75 | response: NonCollinsExplainsResponseType, 76 | |} | {| 77 | type: 'machine_translation', 78 | response: MachineTranslationResponseType, 79 | |} 80 | 81 | export type ResponseType = 'explain' | 'choices' | 'error' 82 | | 'non_collins_explain' | 'machine_translation' 83 | 84 | const LINK_REGEXP = /href="(.+)"/ 85 | 86 | function replaceJumpLink(content) { 87 | if (typeof content !== 'string') { 88 | return content 89 | } 90 | 91 | return content.replace(LINK_REGEXP, (match, m1) => `href="http://dict.youdao.com/${m1}"`) 92 | } 93 | 94 | function getFrequency(className) { 95 | const res = /star(\d)/.exec(className) 96 | 97 | return res ? parseInt(res[1], 10) : null 98 | } 99 | 100 | function getInfo($container) { 101 | const word = $container.find('.title').text() 102 | const $star = $container.find('.star') 103 | const pronunciation = $container.find('.spell').text() 104 | const frequence = $star.length > 0 ? getFrequency($star.attr('class')) : null 105 | const rank = $container.find('.rank').text() 106 | const additionalPattern = $container.find('.pattern').text().trim() 107 | 108 | return { 109 | word, 110 | pronunciation, 111 | frequence, 112 | rank, 113 | additionalPattern, 114 | } 115 | } 116 | 117 | function getExplain($explain) { 118 | const $type = $explain.find('.additional') 119 | const type = $type.text() 120 | const typeDesc = $type.attr('title') 121 | 122 | const $p = $explain.find('p') 123 | 124 | $p.find('span').remove() 125 | 126 | const engExplain = replaceJumpLink($p.html().trim()) 127 | 128 | return { 129 | type, 130 | typeDesc, 131 | engExplain, 132 | } 133 | } 134 | 135 | function getExample($example) { 136 | const $examples = $example.find('.examples p') 137 | const eng = $examples.eq(0).text() 138 | const ch = $examples.eq(1).text() 139 | 140 | return { 141 | eng, 142 | ch, 143 | } 144 | } 145 | 146 | function getMeanings($, $items) { 147 | const meanings = [] 148 | 149 | $items.each((index, itemEle) => { 150 | const $item = $(itemEle) 151 | const $exampleLists = $item.find('.exampleLists') 152 | const $explain = $item.find('.collinsMajorTrans') 153 | 154 | if ($explain.length === 0) { 155 | return 156 | } 157 | 158 | const meaning = { 159 | explain: getExplain($explain), 160 | example: getExample($exampleLists), 161 | } 162 | 163 | meanings.push(meaning) 164 | }) 165 | 166 | return { 167 | meanings, 168 | } 169 | } 170 | 171 | function getType($) { 172 | if ($('.collinsToggle').length > 0) { 173 | return 'explain' 174 | } else if ($('#phrsListTab .wordGroup').length > 0) { 175 | return 'choices' 176 | } else if ($('#phrsListTab .trans-container').length > 0) { 177 | return 'non_collins_explain' 178 | } else if ($('#ydTrans .trans-container').length > 0) { 179 | return 'machine_translation' 180 | } 181 | 182 | return 'error' 183 | } 184 | 185 | function getChoices($): ChoiceResponseType { 186 | const $container = $('#phrsListTab') 187 | const $wordGroup = $container.find('.wordGroup') 188 | const choices = [] 189 | 190 | $wordGroup.each((index, ele) => { 191 | const $spans = $(ele).find('span') 192 | const $firstSpan = $spans.eq(0) 193 | 194 | let wordType = '' 195 | 196 | if (!$firstSpan.hasClass('contentTitle')) { 197 | wordType = $spans.eq(0).text().trim() 198 | } 199 | 200 | const $words = $(ele).find('.contentTitle') 201 | const words = [] 202 | 203 | $words.each((i, e) => { 204 | const $ele = $(e) 205 | words.push($ele.find('.search-js').text().trim()) 206 | }) 207 | 208 | choices.push({ 209 | words, 210 | wordType, 211 | }) 212 | }) 213 | 214 | return { 215 | choices, 216 | } 217 | } 218 | 219 | function getSynonyms($) { 220 | const $type = $.find('.wt-container>.additional') 221 | const type = $type.length > 0 ? $type.text() : '' 222 | const $anchor = $.find('.wt-container>a') 223 | const hrefs = [] 224 | const words = [] 225 | 226 | $anchor.each((index) => { 227 | const $ele = $anchor.eq(index) 228 | hrefs.push($ele.attr('href') || '') 229 | words.push($ele.text() || '') 230 | }) 231 | 232 | return { 233 | type, hrefs, words, 234 | } 235 | } 236 | 237 | function getExplainResponse($): ExplainResponseType { 238 | const $collinsContainer = $('.collinsToggle') 239 | 240 | const { 241 | meanings, 242 | } = getMeanings($, $collinsContainer.find('li')) 243 | 244 | return { 245 | wordInfo: getInfo($collinsContainer.find('h4').eq(0)), 246 | synonyms: getSynonyms($collinsContainer), 247 | meanings, 248 | } 249 | } 250 | 251 | function getTitleInfo($): WordInfoType { 252 | const $title = $('.wordbook-js') 253 | const word = $title.find('.keyword').text().trim() 254 | const pronunciation = $title.find('.pronounce .phonetic').eq(0).text().trim() 255 | 256 | return { 257 | word, 258 | pronunciation, 259 | frequence: null, 260 | rank: '', 261 | additionalPattern: '', 262 | } 263 | } 264 | 265 | function getNonCollinsExplain($): NonCollinsExplainsResponseType { 266 | const explains = [] 267 | 268 | $('#phrsListTab .trans-container li').each((i, ele) => { 269 | const rawString = $(ele).text() 270 | const index = rawString.indexOf('. ') 271 | 272 | if (index > -1) { 273 | const type = rawString.slice(0, index) 274 | const explain = rawString.slice(index + 2) 275 | 276 | explains.push({ 277 | type, explain, 278 | }) 279 | return 280 | } 281 | 282 | explains.push({ 283 | type: '', 284 | explain: rawString, 285 | }) 286 | }) 287 | 288 | return { 289 | wordInfo: getTitleInfo($), 290 | explains, 291 | } 292 | } 293 | 294 | function getMachineTranslation($): MachineTranslationResponseType { 295 | const $container = $('#ydTrans .trans-container p') 296 | 297 | return { 298 | translation: $container.eq(1).text(), 299 | } 300 | } 301 | 302 | export function parse(html: string): WordResponseType { 303 | const $ = cheerio.load(html) 304 | 305 | const type = getType($) 306 | 307 | if (type === 'explain') { 308 | return { 309 | // it's possible for flow to know 310 | // what type is this without literals 311 | type: 'explain', 312 | response: getExplainResponse($), 313 | } 314 | } else if (type === 'choices') { 315 | return { 316 | type: 'choices', 317 | response: getChoices($), 318 | } 319 | } else if (type === 'non_collins_explain') { 320 | return { 321 | type: 'non_collins_explain', 322 | response: getNonCollinsExplain($), 323 | } 324 | } else if (type === 'machine_translation') { 325 | return { 326 | type: 'machine_translation', 327 | response: getMachineTranslation($), 328 | } 329 | } 330 | 331 | return { 332 | type: 'error', 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /src/components/detail.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import React, { Component } from 'react' 3 | import PropTypes from 'prop-types' 4 | import Audio from './audio' 5 | import AddWord from './add_word' 6 | import icons from './icons' 7 | import Tips from './tips' 8 | import { mainBG, fontS, gapL, gapM, gapS, colorDanger, 9 | colorMuted, colorWarning, colorPrimary } from './style' 10 | 11 | import type { 12 | ChoiceResponseType, ExplainResponseType, 13 | NonCollinsExplainsResponseType, MachineTranslationResponseType, 14 | SynonymsType, 15 | } from '../parse' 16 | 17 | const SMALL_FONT = 12 18 | 19 | const styles = { 20 | container: { 21 | transform: 'matrix(1, 0, 0, 1, 1, 1)', 22 | position: 'relative', 23 | }, 24 | synonymsContainer: { 25 | marginTop: gapS, 26 | fontSize: SMALL_FONT, 27 | }, 28 | errorP: { 29 | fontSize: SMALL_FONT, 30 | margin: `0 0 ${gapS}px 0`, 31 | }, 32 | link: { 33 | fontSize: SMALL_FONT, 34 | color: colorPrimary, 35 | cursor: 'pointer', 36 | }, 37 | info: { 38 | marginBottom: gapL, 39 | }, 40 | infoItem: { 41 | marginRight: gapL, 42 | }, 43 | wordType: { 44 | fontSize: fontS, 45 | marginRight: gapS, 46 | color: colorMuted, 47 | }, 48 | meaningItem: { 49 | marginBottom: gapL, 50 | }, 51 | explain: { 52 | padding: gapS, 53 | backgroundColor: mainBG, 54 | }, 55 | exampleItem: { 56 | marginTop: gapM, 57 | paddingLeft: 20, 58 | }, 59 | star: { 60 | width: 14, 61 | height: 14, 62 | verticalAlign: 'top', 63 | position: 'relative', 64 | top: 3, 65 | }, 66 | choiceItem: { 67 | backgroundColor: mainBG, 68 | padding: gapS, 69 | marginBottom: gapM, 70 | }, 71 | nonCollinsTips: { 72 | marginTop: gapS, 73 | marginBottom: gapM, 74 | }, 75 | warnItems: { 76 | backgroundColor: colorWarning, 77 | marginBottom: gapM, 78 | paddingLeft: 10, 79 | color: '#FFF', 80 | }, 81 | } 82 | 83 | function renderSentence(sentence) { 84 | return ( 85 | // eslint-disable-next-line 86 | 87 | ) 88 | } 89 | 90 | function renderFrequence(frequence) { 91 | return ( 92 |
93 | {[...Array(frequence).keys()].map((_, index) => ( 94 | star 100 | ))} 101 |
102 | ) 103 | } 104 | 105 | function renderMeaning(meaning, index) { 106 | const { 107 | example: { eng, ch }, 108 | explain: { type, typeDesc, engExplain }, 109 | } = meaning 110 | 111 | return ( 112 |
113 |
114 | {type} 115 | {typeDesc} 116 | {renderSentence(engExplain)} 117 |
118 |
119 |
{eng}
120 |
{ch}
121 |
122 |
123 | ) 124 | } 125 | 126 | function renderWordBasic( 127 | wordInfo, 128 | synonyms: ?SynonymsType, 129 | search: ?(word: string) => void, 130 | showWordsPage: boolean, 131 | showNotebook: boolean, 132 | flash: () => {}, 133 | ) { 134 | const { word, pronunciation, frequence, rank, additionalPattern } = wordInfo 135 | let synonymsEle = null 136 | 137 | if (synonyms && Array.isArray(synonyms.words) && synonyms.words.length > 0) { 138 | const { type, words: synonymsWords } = synonyms 139 | 140 | synonymsEle = ( 141 |
142 | 143 | {type || ''} → 144 | 145 | 搜索 146 | {synonymsWords.map(synonymsWord => ( 147 | { 151 | if (typeof search === 'function') { 152 | search(synonymsWord) 153 | } 154 | }} 155 | > 156 | "{synonymsWord}" 157 | 158 | ))} 159 |
160 | ) 161 | } 162 | 163 | return ( 164 |
165 |
166 | 167 | {word} 168 | 169 | {pronunciation ? ( 170 | 171 | {pronunciation} 172 | 173 | ) : null} 174 | 175 | 177 | {showNotebook ? ( 178 | 179 | 184 | 185 | ) : null} 186 | {frequence ? ( 187 | 188 | {renderFrequence(frequence)} 189 | 190 | ) : null} 191 | {rank ? ( 192 | 193 | {rank} 194 | 195 | ) : null} 196 | {additionalPattern ? ( 197 | 198 | {additionalPattern} 199 | 200 | ) : null} 201 |
202 | {synonymsEle} 203 |
204 | ) 205 | } 206 | 207 | function renderExplain( 208 | response: ExplainResponseType, 209 | showWordsPage, 210 | showNotebook, 211 | search, 212 | flash, 213 | ) { 214 | const { meanings, synonyms, wordInfo } = response 215 | 216 | return ( 217 |
218 | {renderWordBasic( 219 | wordInfo, synonyms, search, 220 | showWordsPage, showNotebook, flash, 221 | )} 222 |
223 | {meanings.map(renderMeaning)} 224 |
225 |
226 | ) 227 | } 228 | 229 | function renderChoices(response: ChoiceResponseType, searchWord) { 230 | const { choices } = response 231 | 232 | return ( 233 |
234 |
请选择单词:
235 | {choices.map((choice) => { 236 | const { wordType, words } = choice 237 | 238 | return ( 239 |
240 | {wordType} 241 | 242 | {words.map(word => ( 243 | searchWord(word)} 247 | > 248 | {word} 249 | 250 | ))} 251 | 252 |
253 | ) 254 | })} 255 |
256 | ) 257 | } 258 | 259 | function renderNonCollins( 260 | currentWord, navigate, 261 | response?: NonCollinsExplainsResponseType, 262 | showWordsPage?: boolean, showNotebook?: boolean, 263 | flash: () => {}, 264 | ) { 265 | const wordBasic = (response && response) 266 | ? renderWordBasic( 267 | response.wordInfo, 268 | null, 269 | null, 270 | Boolean(showWordsPage), 271 | Boolean(showNotebook), 272 | flash, 273 | ) : null 274 | 275 | const responseElement = response ? ( 276 |
277 | {response.explains.map(item => ( 278 |
279 | {item.type ? ( 280 | {item.type}. 281 | ) : null} 282 | {item.explain} 283 |
284 | ))} 285 |
286 | ) : null 287 | 288 | return ( 289 |
290 | {wordBasic} 291 | {responseElement} 292 |

293 | 未搜索到柯林斯释义。 294 | {currentWord ? ( 295 | 296 | 去有道搜索"{currentWord}" 297 | 298 | ) : null} 299 |

300 |
301 | ) 302 | } 303 | 304 | function renderMachineTranslation( 305 | response: MachineTranslationResponseType, 306 | ) { 307 | const { translation } = response 308 | 309 | return ( 310 |
311 | (机翻) {translation} 312 |
313 | ) 314 | } 315 | 316 | class Detail extends Component { 317 | defaultProps: { 318 | currentWord: string, 319 | } 320 | 321 | refers: any 322 | flash: () => {} 323 | 324 | constructor(props: any) { 325 | super(props) 326 | 327 | this.flash = this.flash.bind(this) 328 | 329 | this.refers = {} 330 | } 331 | 332 | flash(msg: string) { 333 | this.refers.tips.flash(msg) 334 | } 335 | 336 | renderContent() { 337 | const { flash } = this 338 | const { search, currentWord, explain: wordResponse, 339 | openLink, showWordsPage, showNotebook } = this.props 340 | const openCurrentWord = openLink.bind(null, currentWord) 341 | const renderErr = renderNonCollins.bind(null, currentWord, 342 | openCurrentWord, undefined, showWordsPage, 343 | showNotebook, flash, 344 | ) 345 | 346 | if (!wordResponse) { 347 | return renderErr() 348 | } 349 | 350 | const { response, type } = wordResponse 351 | 352 | if (type === 'explain') { 353 | return renderExplain(response, showWordsPage, showNotebook, search, flash) 354 | } else if (type === 'choices') { 355 | return renderChoices(response, search) 356 | } else if (type === 'non_collins_explain') { 357 | return renderNonCollins( 358 | currentWord, openCurrentWord, response, 359 | showWordsPage, showNotebook, flash, 360 | ) 361 | } else if (type === 'machine_translation') { 362 | return renderMachineTranslation(response) 363 | } 364 | 365 | return renderErr() 366 | } 367 | 368 | render() { 369 | const element = this.renderContent() 370 | 371 | return ( 372 |
373 | (this.refers.tips = tips)} 375 | /> 376 | {element} 377 |
378 | ) 379 | } 380 | } 381 | 382 | const { func, object, string, bool } = PropTypes 383 | 384 | Detail.propTypes = { 385 | currentWord: string, 386 | explain: object, 387 | search: func.isRequired, 388 | openLink: func.isRequired, 389 | showWordsPage: bool.isRequired, 390 | showNotebook: bool.isRequired, 391 | } 392 | 393 | // $FlowFixMe 394 | Detail.defaultProps = { 395 | currentWord: '', 396 | explain: null, 397 | } 398 | 399 | export default Detail 400 | -------------------------------------------------------------------------------- /src/__tests__/pages/noresponse.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = ` 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 【asdfasbadfasdfasdf】什么意思_英语asdfasbadfasdfasdf的翻译_音标_读音_用法_例句_在线翻译_有道词典 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 | 34 |
35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 | 中英 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | 当前查询结果是否对您有帮助 69 |
70 |
71 | 75 | 78 |
79 |
80 |
81 | 82 |
83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 |
91 |
92 | 93 | 94 | 95 | 102 |
103 |
104 | 105 | 106 |
107 |
108 |
109 |
110 | 111 | 117 |
118 | 119 | 128 |
129 |
130 |
131 | $firstVoiceSent 132 |
- 来自原声例句
133 |
134 |
135 |
136 |
137 |
138 |
139 | 140 | 141 | 154 |
155 |
156 | 162 |
163 | 164 | 171 | 172 |
173 |
小调查
174 |
175 | 请问您想要如何调整此模块? 176 |

177 |

178 |

179 |
感谢您的反馈,我们会尽快进行适当修改!
180 | 进来说说原因吧 181 | 确定 182 |
183 |
184 | 185 |
186 |
小调查
187 |
188 | 请问您想要如何调整此模块? 189 |

190 |

191 |

192 |
感谢您的反馈,我们会尽快进行适当修改!
193 | 进来说说原因吧 194 | 确定 195 |
196 |
197 | 198 | 215 | 216 | 217 | 242 | 243 | 244 | 250 | 251 | 252 | 253 | 257 | 258 | 259 | ` 260 | -------------------------------------------------------------------------------- /src/__tests__/pages/sentence.js: -------------------------------------------------------------------------------- 1 | // eslint-disable 2 | module.exports = ` 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 【the_purpose_of_a_visualization_tool_is_to_construct_visualizations.】什么意思_英语the_purpose_of_a_visualization_tool_is_to_construct_visualizations.的翻译_音标_读音_用法_例句_在线翻译_有道词典 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 | 34 |
35 | 36 | 37 | 38 |
39 |
40 |
41 | 42 |
43 |
44 | 中英 45 | 46 |
47 | 48 | 49 | 50 | 51 | 52 | 53 |
54 |
55 |
56 | 57 | 58 | 59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 | 72 |
73 |
74 | 当前查询结果是否对您有帮助 75 |
76 |
77 | 81 | 84 |
85 |
86 | go top 87 |
88 | 89 |
90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 | 100 | 101 |
102 |

103 | 有道翻译 104 | 105 |

106 |
107 |
108 |

The purpose of a visualization tool is to construct visualizations.

109 |

可视化工具的目的是构建可视化。

110 |

以上为机器翻译结果,长、整句建议使用 人工翻译

111 |
112 |
113 |
114 | 115 |
116 |
117 | 118 | 119 |
120 |
121 |
122 |
123 | 124 | 130 |
131 | 132 | 135 |
136 |
137 |
138 | $firstVoiceSent 139 |
- 来自原声例句
140 |
141 |
142 |
143 |
144 |
145 |
146 | 147 | 148 | 161 |
162 |
163 | 169 |
170 | 171 | 178 | 179 |
180 |
小调查
181 |
182 | 请问您想要如何调整此模块? 183 |

184 |

185 |

186 |
感谢您的反馈,我们会尽快进行适当修改!
187 | 进来说说原因吧 188 | 确定 189 |
190 |
191 | 192 |
193 |
小调查
194 |
195 | 请问您想要如何调整此模块? 196 |

197 |

198 |

199 |
感谢您的反馈,我们会尽快进行适当修改!
200 | 进来说说原因吧 201 | 确定 202 |
203 |
204 | 205 | 222 | 223 | 224 | 249 | 250 | 251 | 257 | 258 | 259 | 260 | 264 | 265 | 266 | ` 267 | -------------------------------------------------------------------------------- /src/components/icons.js: -------------------------------------------------------------------------------- 1 | const icons = { 2 | search: 'data:image/png;base64,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', 3 | horn: 'data:image/png;base64,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', 4 | back: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAASFBMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACrhKybAAAAF3RSTlMAFRYxMzQ1Njc4OVRYam2o0tXv8PHy+C9C0HYAAADESURBVHja7ZTZDQMhDAWdc1lykAAJ/Xca/ijAih5IMw3MSJafAQAAAACsToxi/6dGrb+1umv9o0DjHwUyf6cGqb9TgsY/KGetv70O+PHjx48fP378+PHjx48f///93+fdR/T5/SSxvyWxvyWxvyWvXx+w/gnEBfo3lA+Rd4ofNx/BnAXvozmhYM2CSgEFFFBAwTwFOwUUUEABBRMVBAqmK8gnkxbki5myIF/NlAWl+1UFw68qGH5VQdlMStgMAAAAAGBxfih9rI9ED1BkAAAAAElFTkSuQmCC', 5 | star: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAC0FBMVEUAAAD/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yij/yiimdGeYAAAA73RSTlMAAAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERkdISUpLTE1PUFFSU1RVVldYWVpbXF1eX2BhY2RlZmdoaWprbG1ucHFyc3R1dnd5e3x+gIGCg4SFh4iJiouMjY6PkJGSk5SVlpeYmZqcnp+goaKjpKWmqKqrra6vsLGys7S1t7i5uru8vb6/wMHCxMXGx8jJysvMzc7P0NHS09TV1tfY2drb3N3f4OHi4+Tl5ufo6err7O3u7/Dx8vP09fb3+Pn6+/z9/qXbrCAAAAePSURBVHgB7djrWxTXAcfxzs4CK3dYNEDwIpqYRLDWC7UEiTEmBkypURMbmhowalKwNlINbTRe6j22XmrSFNRUjSZYTdCWijE0RC3BqhgELXeRIrvLzsz5/QvtusW9MDvMnjM7+6af5/GNvDjfnZlzZs75zgMcFXNeAkfFNTBbwJRTmUENiFqJ0vhgBkw7heqsIAYYXrUAywzBC4jbBmBfSvACnq8BUL8oeAFbLQCEPcYgBfCjz8KhOs0YnADTigY4NK0ZFpyAmE/64WA/mxCUgLDsZji15gwLRsDD2y1wsr4/JhgBmVcFOEm3Zxv0DzAsFTBAWhWhf0DaYbiczNA/oLAVLt1FugdE7YW7D2P1DnjuHNx9vUjvgI29cGfbrW8An3QWnqpTjXoGRBTcgqeWVVF6BoyqsMBTf1WqngHZrfDWnWPQLyBxfT+8CTtH6xewsIHAm9T4rG4BEZshpyRcr4C0Q5BzfLpeAUsaIafjDZ0CQvaKkPVhlD4BmV9A3sW5ugSEbu3EA0QieKBnf5geAcnn3cYXReJ+CVINgQ+IyGuCCyFwczs/JvAByQd64YvlT6MCH/BkswhfSMczhkAHRK+GknXDAx0w4ySU/G1WoAMKe6HEUhTggIR9UFaWEtAAfvElKLu+zBiIAEOIKSImPnnS08dEDOHknMkj42MiTSG8lgHmic+/uf1I9fV2K4Zi62i4cGxX8bz0BIMWAeGjp+cVbSo7ff7yt51WqGXravzHF5+Vb175o++PjaQMCEtKy8rJL9lZ9umFmxYMhRDZjqaaivL31ryaOzM92aQ2wBAel5iaNnP+8nfLKuu6MSTiIIkSgU8935w5uOGNBU9NSk2Ki+B9BvC8MSQ0LGHSD19fd6im1Q41HEM7CIIjQJnQVnv43RV5k4eHhYYYeX5wwGPT5/98c9nZK82tXb2qhpcEx9gSuQ8q2O91td66VnVwyy9ezHh8cMDq03WNHTaoR5xjw1/9nY11lb8aHPDSOeintmBwADe3SoQ+yN/ny80C48s3oBYhyv+lrKXAKDsNYwq71d17SRRkAgRRklRFWIvjfKwDMSWdyjNekiRRvP8Pgw38yQEKet5J8LkQhW/pUlhsRMk5AiE+E50RipPz7u9iFVbCuP1WuR8uCv+b8yrnp3NxEiWZBlt5MqcQYPjuR/Ag2gVJUry3Ch3/JdhFeKj4Aa8UwPGzP/d+tOD3auO6dhAFCe6qc0OHeBmF5NYSz0eLgB7xelzrF5iGfB0bfvy15HEJJNCTPOdr/VJezfdAcSPxvJWg5fXwtKxV90ES947da4KDjuh1+7aa1QVwo3Yo3kjaB2BfqupPsnGHBFA9BwqPLzn+hB/fhI/+pV/pWqpcjeDGfmEy50dA2OyvBk1p/96SNpsEd3V5Jn8CuGH5dfBAVFxyUXCt1nf64O768kg/P8uji5r8nnOi5HpRWQS4aSsx+70xGV7aCb8Q+HR3UxLFzsi8qx3auPN+IkcRwCd9JEITFWN4ur3hxE+hhTPTqDens6rAriaXYXeccw2sGhdyDAHhi1oIWJCOgmiWAC6huI3i69+l6+1ExhMS84YO0Lu7I4n5iGbse/8GLeuBxzQ4I5p4yEpolj8Q2/EpnAYB/NQqG2jYa57ktQjgope3g0ZHUSynSUDU4hbQ7JvbfhqjTUBiaZcfARIGdG9K0SZg9J4e0Lh3cLw2AY9+3Ef1gWw9M1GbgElf9mOA5EeAWD9Vm4DMZsp9wZ1ntAl4oQ90pMUGTQIKQKvYpEVA1GrQ+nWCFgEpW0Br3yNaBKSXQ4aqY9oTGVoEZFfKDO08iBqqoOY5LQJeqPM8eBIFxwGYpGa7eHORFgGvtLuGdwztObaivkItAorsrovvGBx+WKNBgGEd6G0LYQ8I3wN6HwxnDxhZDnpHnmAPmFrh36uQuJ/rnZnFHjDvvPuxExk6wP1w8OJi9oCl19x/nJoAggeaV7EHvN0Oer0b2QO22UBP2s8cwCtPghuH/wklx0yMAYbY0/Ct82hB+k8Ot8K3qod5toCQCRfgS1/j1nGOhWL9Nd/b19ppoWwBYVkX4YPl6JxI3nGRwmeW34MP3+QOYwuIeOkq5J0rGD9wf0PH5ldC3rfLotgColc3Qc7VnU+HcS7GrM2XIad1fSxbQPyuTgx2+5PCGM5TeP4RudSeP5jZAkYcscCb5UppIjeY+a3LfQRe+j97iC0guUqEF+GPWVG83IyNzPi9FV7IV2PYAiYMWmf+uvxx3uc2dskpeLn5PaYAQ1Y7PNTvmMNzCrJ/cwkeuucaWQJMC3vh5l+fLzFxyviXK5oJXPpei2IJiP+Za4khQkNpCje0EW/VC8QVsDaRJSBlvWsS2A5kmQ2cCnEZu13Xzbo7lSVgwgc2OJFTyyZwao177YQdTv1H01kCplba4UAubc/m/DFjY60AB+HLGSwB2VcEAFLHyRc5f837uE0EIN56liUgp0MCcOOXI42cv4zJKx1rCOlbwBLwigj07n1qBEfDnLmjCyCvMwSErIXw5zcf4WiNLTxhxYZw+oCHNjT8NpNjMWXb1e2p9AHjV5TEcmwii1emKwX8338AK3HjPCyNu4gAAAAASUVORK5CYII=', 6 | gear: 'data:image/png;base64,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', 7 | book: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAFV0lEQVR4nO2dTWgdVRTHf69pY21ra6oi1Ur9QE2xC0EtxSxqLeIXWFeKrnSn6MKF4EZEUBHduNKFGxEFwUWtqAtBip+IUl2UQMWiC9HWUmyrNmBNX+PiBpy85KUvyTlz5875/2Bg5iXv3MOb38y798x9Mx3qYQi4GdgJbAMuA9YDwzW13xa6wHbggFXA5VaB+jAEPAw8Atzg3FYUTPeZpwA3Aa8AY45tROM0MGUZ0EuAh4BXgVVO8YURHgI8QTry+3ES+Ak4AkwCHYccSqZL6isVefDcRzpFzbV8SjozbEI7fT7uBk4x92c4CWzJl9r8XA0cZ3bSvwEPZsyrJNYCP9P/IGqsAB3gA2YnPA5ckzGv0nicmZ/fvxQiwG3M3vmHgatyJlUYQ8A+/v/8fgR24yzAMqM4j83x2pOkzp4YjM3A9ZXtN0lnUFcsBNhI6rVW2Qe8YxA7EttIZwFIR/seYIV3oxYCjAFrel57CzhjEDsS1VP7UeAHaiiVWwhwY8/2FPC5QdxoXFpZP0yqB3iX6k0EuKJn+w/gV4O40RiprB+rq1ELAUZ6tidIhQyxeEzr/fNhIUBvVW8VcK5B3Gj8VVnvPajcsBoGVlkDrHSI23aOVtbXkw6srnejHgKsBFY7xG07ByvrG4ALSJd/XfEQoAOsc4jbdvZX1oeBraRSsCseAsDMIY0YjP3An5Xt2+to1EuArU5x28zvwNeV7btIV1hd8RLgHmooY7aQdyvrVwJ3eDfoJcAosMspdpvZQ5opBWnfrPVu0EsAgBepcTzbEo4Dr+VOYqHspf8MlvdRTWChXAgcoqAZQfMJMAV8QrrWLQbnAfoLMGrZkPvVJtJcgW9I8wM+JA13TqCJofPxEak/cO8cfxsBzp9e75CkOLnYhix2wl5gxwL+f4J0xdCz/1E6XVIxaMMcfztCKhB1ppevgPsX21AdZ4BeVqNS8VK4uGf7kqUE01FYPpNLebMECI4ECI4ECI4ECE6OUUAvXeDv3Ek0mCnShbXeqfcmNEGAA8CduZNoMFPALcDbHsGbIMApNI38bBzyCtyEPoBKwmfHbW5FEwQQGZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwZEAwWnCpNBBGSLdHKEtD5vskCZ7Zp0QW5IAK0i/mb88cx6WPAM8lzOB0r4CNIPYmNIEaNtDKGq7K3g/ShNAGCMBgiMBglOaAC6/kM3IObkTKGkY2AWeJd0irQ2dwSHgs9xJlCTAJAXeRrXplPYVIIyRAMGRAMGRAMEpqRO4jHTv3LZI2yE9K/BEziRKEmAY+BjYlDsRQ54HXsqZQEkCAJxHu4pB2QtBpZ1O3Z+kWTPZC1qlCSCMkQDBkQDBKU2ANnUAoQGdwJJGAV3gdeAiGtB5MmA5Mx8Vmy2JUpgEns6dRNso7StAGCMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgiMBgmMhgJ7lVzAWAmR/9p1YPBYCjBvEEJmwEOA9gxgiExYCfAF8bxBHZMBCgNPACwZxRH/cboxpNQzcPb0slo6WvgukZwy6YHmn0EeB64BrF/i+UeA70mhCQ8rZnAHW5U5iULYAv5B2ppZ6lr0D7Zk+WFcCx4GdwLfGcYUTHqXgg8CtwMvAPw7xhSFe1wImgKeAMeAN4JhTO2KJ/bi6Ol0bgR3AdmAz6fl/xXRsGs6XwK7Fvvk/B0ctcIj3nmUAAAAASUVORK5CYII=', 8 | plus: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABP0lEQVRoge3ZMUoEQRBA0TezI7qsgWioYCB6Ag/gTQTvYL7gEQQvZWhgoJmmBgriKrprMBOJwfZ2Qyn0h4EJpqr6Q9NU1zSmSnCAS5zgbYnv13GDM9zmFO5ygn/k2cJ4eJZhu0T9NjfBwALzxJivIS6LUgJhVIFoqkA0VSCaKhBNFYimCkRTSmCGz8SYOd5zC3f6y8goI8cC+9hIjFvD0fDerFq8wxUmqybQ9/UTHCbG7eECLzIuNo1p/qUikhZP0YvIoZWx//4C9RiNpgpEUwWiaRWYjkXSYid6ETl0uMZmRo65ftq8a/nBLnzgUT/NXnkrdziV143Sd6NTHCfEPOAcdzmFO5nz+YFnvCbGzPT/CO5zCpc6hcbSW+JW+h3i1yT/mioQTRWIpgpEUwWiqQLRVIFoSgk0K+QaKTAV/AazHCMmFbpFagAAAABJRU5ErkJggg==', 9 | } 10 | 11 | export default icons 12 | -------------------------------------------------------------------------------- /src/__tests__/pages/deficits.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = ` 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 【deficits】什么意思_英语deficits的翻译_音标_读音_用法_例句_在线翻译_有道词典 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 33 |
34 | 35 | 36 | 37 |
38 |
39 |
40 | 41 |
42 |
43 | 中英 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 | 74 |
75 |
76 | 当前查询结果是否对您有帮助 77 |
78 |
79 | 83 | 86 |
87 |
88 | go top 89 |
90 | 91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 104 | 105 | 184 | 185 | 186 |
187 | 188 | 189 | 190 |
191 |
192 |
193 | 194 | 199 | 200 |

201 | deficits 202 |
203 | 英 204 | ['defɪsɪts] 205 | 206 | 207 | 美 208 | ['dɛfəsɪts] 209 | 210 | 211 |
212 |

213 |
214 | 215 |
    216 |
  • n. [财政] 赤字,亏损(deficit的复数形式)
  • 217 |
218 |
219 |
220 | 221 | 222 | 223 | 224 | 225 |
226 |

227 | 228 | 229 | 230 | 231 | 网络释义 232 | 233 | 234 |

235 | 236 | 237 |
238 |
239 | 240 | 241 |
242 |
243 |   244 | 245 | 赤字 246 |
247 |

248 | olyuhong的听写 - 听写酷 - 我的沪江 ... 249 | Democrat 民主党人 250 | deficits 赤字 251 | caucus 核心会议 ...

252 |

基于11个网页-相关网页

253 |
254 |
255 |
256 |   257 | 258 | 财政赤字 259 |
260 |

261 | 求翻译:Doh_'baby DL 是什么意思? ... 262 | Others think that it was built to celebrate a great victory over an enemy » 另一些人认为它是建立一个伟大胜利,庆祝一个敌人 263 | deficits » 财政赤字 264 | component being inspected. » 被检查的组件。 ...

265 |

基于4个网页-相关网页

266 |
267 |
268 |
269 |   270 | 271 | 不足额 272 |
273 |

274 | 求翻译:听话的女儿 是什么意思? ... 275 | recipient shall provide a written certificate to xx regarding destruction within ten days theteafter » 收件人须提供关于销毁十天 theteafter xx 的书面的证书 276 | deficits » (名) 赤字; 不足额 277 | lifetime history of conduct » 一生的历史行为 ...

278 |

基于3个网页-相关网页

279 |
280 |
281 |
282 |   283 | 284 | 缺陷 285 |
286 |

287 | 缺陷

288 |

基于1个网页-相关网页

289 |
290 |
291 |
短语
292 |

293 | 294 | curb deficits 295 | 防止财政状况恶化 296 |

297 |

298 | 299 | Budget deficits 300 | 预算赤字 301 | ; 302 | 赤字预算 303 |

304 |

305 | 306 | fiscal deficits 307 | 财政赤字 308 |

309 |

310 | 311 | Pastoral Deficits 312 | 教牧赤字 313 |

314 |

315 | 316 | Government deficits 317 | 财政赤字 318 |

319 |

320 | 321 | neurologic deficits 322 | 神经功能缺损 323 | ; 324 | 神经功能缺损程度 325 | ; 326 | 神经功能 327 |

328 |

329 | 330 | democratic deficits 331 | 民主赤字 332 |

333 |

334 | 335 | Perceptual deficits 336 | 知觉缺陷 337 |

338 |

339 | 340 | Deficits And 341 | 不足和 342 |

343 | 344 |
 更多收起网络短语
346 |
347 |
348 |
349 |
350 | 351 |
352 |
353 | 354 |
355 |

356 | 357 | 21世纪大英汉词典 358 | 359 | 360 |

361 |
362 | 363 | 364 |
365 |

366 | deficit 367 | ['defisit] 368 |

369 | 370 | 371 |
    372 | 373 | 374 | 375 |
  • 376 | n.
      377 | 378 |
    • 379 | 逆差;亏损,亏空;赤字 380 | 381 | 382 | 383 | 384 |
    • 385 | 缺少,缺乏,欠缺,不足 386 | 387 | 388 | 389 | 390 |
    • 391 | 不足额,短缺额 392 | 393 | 394 | 395 | 396 |
    • 397 | 缺陷 398 | 399 | 400 | 401 | 402 |
    • 403 | 失利,落后 404 | 405 | 406 | 407 | 408 |
    • 409 | 障碍,不利条件 410 | 411 | 412 | 413 |
    • 414 |
  • 415 | 416 | 417 | 418 |
419 | 420 | 421 | 422 |
423 | 以上来源于:《21世纪大英汉词典》 424 |
425 |
426 |
427 |
428 | 429 | 430 |
431 |

432 | 433 | 双语例句原声例句权威例句 434 | 435 | 436 |

437 |
438 |
439 |
    440 | 441 | 442 |
  • 443 |

    And why do we have these deficits? 444 | 445 |

    446 | 447 |

    为什么我们这些赤字呢? 448 |

    449 |

    450 | article.yeeyan.org 451 |

    452 |
  • 453 | 454 | 455 |
  • 456 |

    They have undertaken to underwrite a large proportion of the supermarket's deficits. 457 | 458 |

    459 | 460 |

    他们承诺支付超级市场赤字大部分 461 |

    462 |

    463 | 《21世纪大英汉词典》 464 |

    465 |
  • 466 | 467 | 468 |
  • 469 |

    As I said, deficits saved the world. 470 | 471 |

    472 | 473 |

    说过的那样,赤字拯救世界 474 |

    475 |

    476 | article.yeeyan.org 477 |

    478 |
  • 479 |
480 |
481 | 482 | 483 |
484 | 485 |
    486 | 487 |
  • 488 |

    One of the big issues to discuss is how and when to reduce deficits and economic growth measures as conditions improve. 489 | 490 |

    491 |

    492 | VOA: special.2010.06.25 493 |

    494 |
  • 495 | 496 | 497 | 498 |
  • 499 |

    And if parts of these brains--if parts -if these parts of the brain are damaged you get language deficits or aphasias where you might lose the ability to understand or create language.

    500 |

    如果这些脑区,如果这些脑区受到损伤,那就会患上各种语言障碍或失语症,就是说可能会失去理解或创造语言的能力

    501 | 502 |
    503 | 耶鲁公开课中出现Deficits的视频截图 504 | 505 | 506 | 507 |
    508 |

    耶鲁公开课 - 心理学导论课程节选

    509 |
  • 510 | 511 | 512 |
  • 513 |

    It means that we supplement our diets with all sorts of things to fix perceived deficits in this mineral, in this vitamin, in this thing that somebody tells us is good for us.

    514 |

    也就是说,我们只要听说哪些是有益的,就用各种各样的东西来进补,这种矿物质,那种维生素,诸如此类

    515 | 516 |
    517 | 耶鲁公开课中出现Deficits的视频截图 518 | 519 | 520 | 521 |
    522 |

    耶鲁公开课 - 关于食物的心理学、生物学和政治学课程节选

    523 |
  • 524 |
525 | 更多原声例句 526 |
527 | 528 | 529 |
530 | 550 | 更多权威例句 551 |
552 |
553 |
554 | 555 | 556 |
557 |
558 | 559 | 560 |
561 |
562 |
563 |
564 | 565 | 571 |
572 | 573 | 582 |
583 |
584 |
585 | $firstVoiceSent 586 |
- 来自原声例句
587 |
588 |
589 |
590 |
591 |
592 |
593 | 594 | 595 | 608 |
609 |
610 | 616 |
617 | 618 | 625 | 626 |
627 |
小调查
628 |
629 | 请问您想要如何调整此模块? 630 |

631 |

632 |

633 |
感谢您的反馈,我们会尽快进行适当修改!
634 | 进来说说原因吧 635 | 确定 636 |
637 |
638 | 639 |
640 |
小调查
641 |
642 | 请问您想要如何调整此模块? 643 |

644 |

645 |

646 |
感谢您的反馈,我们会尽快进行适当修改!
647 | 进来说说原因吧 648 | 确定 649 |
650 |
651 | 652 | 669 | 670 | 671 | 696 | 697 | 698 | 704 | 705 | 706 | 707 | 711 | 712 | 713 | ` 714 | -------------------------------------------------------------------------------- /src/__tests__/pages/newest.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | module.exports = ` 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 【newest】什么意思_英语newest的翻译_音标_读音_用法_例句_在线翻译_有道词典 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 |
29 |
30 | 33 |
34 | 35 | 36 | 37 |
38 |
39 |
40 | 41 |
42 |
43 | 中英 44 | 45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 | 57 | 58 | 59 |
60 |
61 |
62 |
63 |
64 |
65 | 74 |
75 |
76 | 当前查询结果是否对您有帮助 77 |
78 |
79 | 83 | 86 |
87 |
88 | go top 89 |
90 | 91 |
92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 104 | 105 | 184 | 185 | 186 |
187 | 188 | 189 | 190 |
191 |
192 |
193 | 194 | 199 | 200 |

201 | newest 202 |
203 | 英 204 | [nju:ɪst] 205 | 206 | 207 | 美 208 | [njuɪst] 209 | 210 | 211 |
212 |

213 |
214 | 215 |
    216 |
  • 最新
  • 217 |
218 |
219 |
220 | 221 | 222 | 223 | 224 | 225 |
226 |

227 | 228 | 229 | 230 | 231 | 网络释义 232 | 233 | 234 |

235 | 236 | 237 |
238 |
239 | 240 | 241 |
242 |
243 |   244 | 245 | 最新 246 |
247 |

248 | 行业专业常用英语单词第419页 ... 249 | 最先 n(the) very first 250 | 最新 nlatest; newest 251 | 最终 nfinal; ultimate ...

252 |

基于15个网页-相关网页

253 |
254 |
255 |
256 |   257 | 258 | 至新 259 |
260 |

261 | 99至新Newest)W1301粉色尼龙材质休闲卡通趣味益智儿童围棋书包 262 | 去京东商城购买

263 |

基于11个网页-相关网页

264 |
265 |
266 |
267 |   268 | 269 | 舞蹈公司 270 |
271 |

272 | ...Rain、大韩航空、Rain》S.E.S —、、崔完规、李云在2》、李秉宪》 李胜基—、朴慧京、郑多彬崔岷植、赵显宰、NEWEST舞蹈公司)、韩文、岛、金佰年(餐饮)《 Is it impossible、朴龙河、大长今、贝蕾尔儿童用品集团

273 |

基于10个网页-相关网页

274 |
275 |
276 |
277 |   278 | 279 | 新品速递 280 |
281 |

282 | 首页-GRACE-CITY- 淘宝网 ... 283 | NEWEST 新品速递 284 | UPPER WEAR 上装 285 | DRESS 连衣裙 ...

286 |

基于5个网页-相关网页

287 |
288 |
289 |
短语
290 |

291 | 292 | Newest visitor 293 | 最新访客 294 |

295 |

296 | 297 | NEWEST GROUP 298 | 最新团购 299 |

300 |

301 | 302 | newest activities 303 | 最新活动 304 | ; 305 | 来北京的N个理由 306 |

307 |

308 | 309 | Newest Server 310 | 最新服务器 311 | ; 312 | 最新的服务器 313 |

314 |

315 | 316 | todays newest 317 | 今天最新 318 | ; 319 | 当今最新 320 |

321 |

322 | 323 | Newest Wallpaper 324 | 最新桌布 325 |

326 |

327 | 328 | newest development 329 | 新发展 330 | ; 331 | 最新发展 332 | ; 333 | 最新进展 334 |

335 |

336 | 337 | newest hero 338 | 最新英雄 339 |

340 |

341 | 342 | newest generation 343 | 推出最新一代 344 |

345 | 346 |
 更多收起网络短语
348 |
349 |
350 |
351 |
352 | 353 |
354 |
355 | 356 |
357 |

358 | 359 | 21世纪大英汉词典 360 | 361 | 362 |

363 |
364 | 365 | 366 |
367 |

368 | NEW 369 |

370 | 371 | 372 |
    373 |
  • 374 | abbr. 375 | net economic welfare 净经济福利 376 |
  • 377 | 378 | 379 |
380 | 381 | 382 | 383 | 384 |

385 | new 386 | [nju:; nu:] 387 |

388 | 389 | 390 |
    391 | 392 | 393 | 394 |
  • 395 | adj.
      396 | 397 |
    • 398 | 新的 399 | 400 | 401 | 402 | 403 |
    • 404 | 更新的,最新的 405 | 406 | 407 | 408 | 409 |
    • 410 | 新鲜的,未用过的 411 | 412 | 413 | 414 | 415 |
    • 416 | 新型的,初次出现的;新奇的 417 | 418 | 419 | 420 | 421 |
    • 422 | 新发生的;时新的;流行的 423 | 424 | 425 | 426 | 427 |
    • 428 | 新发现的 429 | 430 | 431 | 432 | 433 |
    • 434 | 新有的,新兴的 435 | 436 | 437 | 438 | 439 |
    • 440 | 更多的,附加的 441 | 442 | 443 | 444 | 445 |
    • 446 | 新接触的,不熟悉的(常与 to 连用) 447 | 448 | 449 | 450 | 451 |
    • 452 | 不习惯的,无经验的(常与 to 连用) 453 | 454 | 455 | 456 | 457 |
    • 458 | (身体、精神状态)恢复了的,变好了的 459 | 460 | 461 | 462 | 463 |
    • 464 | (本季作物中)最早收获的 465 | 466 | 467 | 468 | 469 |
    • 470 | [N-](语言)新的,现代的 471 | 472 | 473 | 474 | 475 |
    • 476 |
  • 477 |
  • 478 | adv. 479 |
      480 | 481 |
    • 482 | 重新 483 | 484 | 485 | 486 | 487 |
    • 488 | [常用以构成复合词]新,新近,最近 489 | 490 | 491 | 492 | 493 |
    • 494 |
  • 495 |
  • 496 | n. 497 |
      498 |
    • [the n-]新东西,新事物(或特征、状态等)
    • 499 | 500 | 501 | 502 | 503 |
  • 504 | 505 | 506 |
  • 507 | 近义词: 508 |

    509 | fresh 510 | . modern 511 |

  • 512 |
  • 513 | 反义词: 514 |

    515 | old 516 |

  • 517 | 518 |
  • 519 | 短语: 520 |
      521 |
    • 522 | be a new one on (someone) 523 | 见one
    • 524 |
    • 525 | new at 526 | 不熟悉…的,对…生疏的
    • 527 |
    • 528 | new from 529 | 刚从…来的
    • 530 |
    • 531 | new to 532 | =new at
    • 533 |
    • 534 | What's new? 535 | [美国口语]你好吗?怎么样?
    • 536 |
  • 537 |
538 | 539 | 540 | 541 |
  542 | 更多收起结果 544 |
545 |
546 | 以上来源于:《21世纪大英汉词典》 547 |
548 |
549 |
550 |
551 | 552 | 553 |
554 |

555 | 556 | 双语例句原声例句权威例句 557 | 558 | 559 |

560 |
561 |
562 |
    563 | 564 | 565 |
  • 566 |

    Their newest truck was spec'd by a computer. 567 | 568 |

    569 | 570 |

    他们最新卡车电脑提供说明书 571 |

    572 |

    573 | 《21世纪大英汉词典》 574 |

    575 |
  • 576 |
577 |
578 | 579 | 580 |
581 | 582 |
    583 | 584 |
  • 585 |

    They are the retired Justice Sandra Day O'Connor, the current Justice Ruth Bader Ginsburg and the newest justice,Sonia Sotomayor. 586 | 587 |

    588 |

    589 | VOA: special.2009.08.24 590 |

    591 |
  • 592 | 593 | 594 | 595 |
  • 596 |

    Because if anything worked, there would be a solution that would have some value over time and people wouldn't have to be seeking out the newest miracle, getting every little thing they see on TV and the like, and I'll show you some amusing examples of those in the next class.

    597 |

    如果真有用,那就找到了长期有效的解决方法,人们就不会再三地寻找方法了,只要看到电视上有什么小妙招就要尝试,下堂课我会举几个有趣的例子

    598 | 599 |
    600 | 耶鲁公开课中出现newest的视频截图 601 | 602 | 603 | 604 |
    605 |

    耶鲁公开课 - 关于食物的心理学、生物学和政治学课程节选

    606 |
  • 607 | 608 | 609 |
  • 610 |

    Course assistants as we use the jargon are alumni of CS50 who return to the course for a couple of hours a week having taken it in prior years, to help you, the newest batch of students, one on one with office hours, with problem sets; and similarly do the CS function as they might in many courses.

    611 |

    课程助理用我们的行话说就是CS50校友0,他们在前几年一直都是一周回来几个小时,来帮助你们这些新生,在上机的时候以一对一的方式,帮助你们完成这些问题集,在很多其他课程中他们也是这么做的。

    612 | 613 |
    614 | 哈佛公开课中出现newest的视频截图 615 | 616 | 617 | 618 |
    619 |

    哈佛公开课 - 计算机科学课程节选

    620 |
  • 621 |
622 | 更多原声例句 623 |
624 | 625 | 626 |
627 | 647 | 更多权威例句 648 |
649 |
650 |
651 | 652 | 653 |
654 |
655 | 656 | 657 |
658 |
659 |
660 |
661 | 662 | 668 |
669 | 670 | 679 |
680 |
681 |
682 | $firstVoiceSent 683 |
- 来自原声例句
684 |
685 |
686 |
687 |
688 |
689 |
690 | 691 | 692 | 705 |
706 |
707 | 713 |
714 | 715 | 722 | 723 |
724 |
小调查
725 |
726 | 请问您想要如何调整此模块? 727 |

728 |

729 |

730 |
感谢您的反馈,我们会尽快进行适当修改!
731 | 进来说说原因吧 732 | 确定 733 |
734 |
735 | 736 |
737 |
小调查
738 |
739 | 请问您想要如何调整此模块? 740 |

741 |

742 |

743 |
感谢您的反馈,我们会尽快进行适当修改!
744 | 进来说说原因吧 745 | 确定 746 |
747 |
748 | 749 | 766 | 767 | 768 | 793 | 794 | 795 | 801 | 802 | 803 | 804 | 808 | 809 | 810 | ` 811 | --------------------------------------------------------------------------------