Repository: senghoo/wordai
Branch: master
Commit: 2210cc558409
Files: 61
Total size: 92.3 KB
Directory structure:
gitextract_fuu3vrhz/
├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.py
├── commands/
│ └── __init__.py
├── config/
│ ├── __init__.py
│ └── wsgi.ini
├── data/
│ ├── __init__.py
│ └── dict.py
├── jsondict/
│ ├── anki.py
│ └── search.py
├── manage.py
├── nginx-server.conf
├── nginx.conf
├── requirements.txt
├── start.sh
├── ui/
│ ├── .babelrc
│ ├── .editorconfig
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .postcssrc.js
│ ├── README.md
│ ├── build/
│ │ ├── build.js
│ │ ├── check-versions.js
│ │ ├── utils.js
│ │ ├── vue-loader.conf.js
│ │ ├── webpack.base.conf.js
│ │ ├── webpack.dev.conf.js
│ │ └── webpack.prod.conf.js
│ ├── config/
│ │ ├── dev.env.js
│ │ ├── index.js
│ │ └── prod.env.js
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── App.vue
│ │ ├── components/
│ │ │ ├── Card.vue
│ │ │ ├── Dictionary.vue
│ │ │ ├── Login.vue
│ │ │ ├── Main.vue
│ │ │ ├── statistic/
│ │ │ │ ├── DayChart.vue
│ │ │ │ └── index.vue
│ │ │ └── wordlist/
│ │ │ ├── Form.vue
│ │ │ ├── Learned.vue
│ │ │ ├── List.vue
│ │ │ ├── Setting.vue
│ │ │ ├── ToLearn.vue
│ │ │ └── index.vue
│ │ ├── main.js
│ │ ├── permission.js
│ │ ├── request.js
│ │ ├── router/
│ │ │ └── index.js
│ │ └── store.js
│ └── static/
│ └── .gitkeep
└── wordai/
├── __init__.py
├── api/
│ ├── __init__.py
│ └── apis.py
└── models/
├── __init__.py
└── models.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
ui/node_modules
================================================
FILE: .gitignore
================================================
### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Global/Emacs.gitignore
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/
dist/
# Flycheck
flycheck_*.el
# server auth directory
/server/
# projectiles files
.projectile
# directory configuration
.dir-locals.el
# network security
/network-security.data
### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Python.gitignore
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
/data/data/
.DS_Store
*/.DS_Store
================================================
FILE: Dockerfile
================================================
FROM python:3.7
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash -
RUN apt-get update && apt-get install -y\
build-essential libpq-dev nodejs nginx-extras
ENV NODE_PATH /usr/lib/node_modules
RUN pip install uwsgi
WORKDIR /usr/src/app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY ui/package.json ./ui/
WORKDIR /usr/src/app/ui
RUN npm install
COPY . /usr/src/app
ENV NODE_ENV production
RUN npm run build
WORKDIR /usr/src/app
COPY nginx.conf /etc/nginx/
COPY nginx-server.conf /etc/nginx/sites-enabled/default
EXPOSE 80
CMD './start.sh'
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2019 Senghoo Kim
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
================================================
FILE: README.md
================================================
# Word AI
一款使用自然语言处理辅助背单词的程序。
通过导入TMX翻译语料库,进行分词、词素分析、单词还原等操作自动生成各种不同语态、情态下的英语填空来进行单词学习。
*PS: 因为版权原因,不提供词典文件和TMX语料文件。需要自行下载导入。业余项目,分享出来供学习和交流,提供有限的技术支持,如有疑问欢迎Issue。*
## 功能特色
* 根据中文填写英文句子中的单词。
* 根据语料库自动生成题目,*拒绝重复*
* 根据艾宾浩斯记忆曲线重复出现需要复习的单词。
* 对于错误单词自动弹出词典,进行学习。
* 支持自定义词库。
* B/S架构,兼容移动端和PC端。
## 个人对背单词的一点看法
1. 背单词不能只针对独立的单词,需要放到语境中学习。
2. 单词需要自己完整的拼写出来,不能是只进行选择题。不然实际应用中容易出现拼写错误的情况。
3. 单词记忆时要关注时态、语态。
4. 学习的内容不能重复,也就是每次学习的上下文要发生改变,不能是固定的句子,学习固定的单词。这种情况下容易脱离了当前上下文,还是想不起单词。
## Examples
### 单词学习
如遇到不会的单词,可以长按空格查看答案。

对于错误单词自动弹出词典以供学习。

### 自定义单词列表
文本框输入单词列表。一行一个,自动提示词典查询结果,以检查是否添加正确。

### 学习统计
提供简单的统计分析功能。

## 管理命令
因为是简单自用为目的设计的软件,没有单独做管理页面。系统管理通过命令行进行。
添加用户
```
# 普通用户
python manage.py useradd <用户名> <口令>
# 管理员
python manage.py useradd <用户名> <口令> admin
```
导入字典
```
python manage.py sync_dict
```
导入例句
```
python manage.py sync_sentence
```
分析词典
```
python manage.py wordlist # 标注 单词星级
python manage.py dict_parse # 拆分中英文解释
```
================================================
FILE: app.py
================================================
from wordai.api import app
from config import api_env, flask_config
app.config.update(flask_config())
def run_dev():
app.run(**api_env())
================================================
FILE: commands/__init__.py
================================================
import string
from data import load_dict, load_sentence
from data.dict import Description
from wordai.models import Word, Sentence, User, WordList, DictDescInfo
def dict_to_mongo():
items = load_dict()
for word, item in items.items():
if Word.objects(word=word).count() == 0:
dbitem = Word(**item)
dbitem.save()
def sentence_to_mongo(*typ):
items = load_sentence(*typ)
print(items)
for k, v in items.items():
_sentence_to_mongo(k, v)
def _sentence_to_mongo(typ, items):
import nltk
from nltk.corpus import wordnet
def wordnet_pos(tag):
if tag.startswith('J'):
return wordnet.ADJ
elif tag.startswith('V'):
return wordnet.VERB
elif tag.startswith('N'):
return wordnet.NOUN
elif tag.startswith('R'):
return wordnet.ADV
else:
return wordnet.NOUN
# nltk.download('punkt')
nltk.download('averaged_perceptron_tagger')
nltk.download('stopwords')
nltk.download('wordnet')
nltk.download('punkt')
stop_words = set(nltk.corpus.stopwords.words('english'))
stemmer = nltk.stem.WordNetLemmatizer()
sentences = []
for trans in items:
eng, chn = trans.getsource(), trans.gettarget()
tokens = nltk.word_tokenize(eng)
pos_tag = [pos[1] for pos in nltk.pos_tag(tokens)]
roots = [stemmer.lemmatize(word, wordnet_pos(pos_tag[idx])) for idx, word in enumerate(tokens)]
cleanword = [token for token in roots if token.isalpha() and token not in stop_words and len(token) >= 3]
# remove duplicates
clean_word = list(dict.fromkeys(cleanword))
if len(clean_word) > 0:
score = Word.search_words(*clean_word).sum('star') / len(clean_word)
else:
score = -1
sentence = Sentence(eng=eng, chn=chn, words=tokens, pos_tag=pos_tag, roots=roots, score=score, typ=typ)
sentences.append(sentence)
if len(sentences) > 50:
Sentence.objects.insert(sentences)
sentences = []
def new_user(username, passwd, role='user', *args):
u = User(username=username, password=passwd, role=role)
u.save()
def dict_star_word_list():
for star in range(1, 6, 1):
words = [word.word for word in Word.objects(star=star).only('word')]
wl = WordList(name="word star {}".format(star), words=words)
wl.save()
def dict_parse():
for word in Word.objects:
for des in word.descriptions:
res = Description(des.description)
des.seq = res.seq
des.cn = res.cn
des.en = res.en
des.speech = res.speech
des.infos = [DictDescInfo(**info) for info in res.info]
word.save()
def run(method, *args):
if method == "sync_dict":
dict_to_mongo()
elif method == "sync_sentence":
sentence_to_mongo(*args)
elif method == "useradd":
new_user(*args)
elif method == "wordlist":
dict_star_word_list(*args)
elif method == "dict_parse":
dict_parse(*args)
else:
print("unknown command {0}".format(method))
================================================
FILE: config/__init__.py
================================================
import os
def c(name, default=None):
return os.environ.get(name.upper(), default)
def mongo_config():
return {
'host': c('mongo_host', "127.0.0.1"),
'port': int(c('mongo_port', "27017")),
'db': c('mongo_db', "wordai"),
'username': c('mongo_username', "root"),
'password': c('mongo_password', ""),
}
def api_env():
return {
'host': c('host', '127.0.0.1'),
'port': int(c('port', 8000)),
'debug': c('debug', "true") == "true"
}
def flask_config():
return {
'JWT_SECRET_KEY': c('secret', '__SOME_SECRET_KEYS_HERE__')
}
================================================
FILE: config/wsgi.ini
================================================
[uwsgi]
module = app:app
master = true
processes = 3
socket = /var/run/wordai.sock
logto = /var/log/wordai.log
chmod-socket = 660
vacuum = true
================================================
FILE: data/__init__.py
================================================
# -*- coding: utf-8 -*-
import os
import json
from itertools import chain
from translate.storage.tmx import tmxfile
DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
def data_file(filename):
return os.path.join(DATA_DIR, filename)
def load_dict():
with open(data_file("dict.json"), "r") as f:
return json.load(f)
def load_sentence(*typ):
files = {
'talks': ['日常口语_20190906111009_1.tmx', '日常口语_20190906111009_2.tmx', '日常口语_20190906111009_3.tmx'],
'dictexams': ['词典例句汇集1.tmx', '词典例句汇集3.tmx', '词典例句汇集5.tmx', '词典例句汇集7.tmx',
'词典例句汇集2.tmx', '词典例句汇集4.tmx', '词典例句汇集6.tmx', '词典例句汇集8.tmx']
}
iters = {}
for t, fs in files.items():
if len(typ) == 0 or t in typ:
type_iterns = []
for fname in fs:
with open(data_file(fname), 'rb') as fin:
tmx = tmxfile(fin, 'en', 'cn')
type_iterns.append(tmx.unit_iter())
iters[t] = chain(*type_iterns)
return iters
================================================
FILE: data/dict.py
================================================
# -*- coding: utf-8 -*-
import string
import re
class Description(object):
def __init__(self, des):
self.des = des
self.idx = 0
self.length = len(des)
self.info = []
if not self.read_seq():
self.seq = 0
self.en = self.des[self.idx:]
self.cn = self.des[self.idx:]
self.speech = 'OTHER'
return
if not self.read_word_speech():
self.en = self.des[self.idx:]
self.cn = self.des[self.idx:]
self.speech = 'OTHER'
return
if not self.read_cn_and_en():
self.en = self.des[self.idx:]
self.cn = self.des[self.idx:]
return
self.read_info()
def read_seq(self):
res = ''
while(self.idx < self.length):
if self.des[self.idx] <= '9' and self.des[self.idx] >= '0':
res += self.des[self.idx]
self.idx += 1
else:
break
if self.des[self.idx] == '.':
self.idx += 1
if len(res) > 0:
self.seq = int(res.strip())
return True
return False
def read_word_speech(self):
res = ''
while(self.idx < self.length):
if self.des[self.idx] in string.ascii_uppercase+"- ;":
res += self.des[self.idx]
self.idx += 1
else:
break
res = res.strip()
if self.des[self.idx] == '\t':
self.idx += 1
if len(res) > 0:
self.speech = res
return True
return False
def read_cn_and_en(self):
end = self.des.find('【', self.idx)
if end == -1:
end = len(self.des)
idx = end
while(idx >= self.idx):
idx -= 1
if self.des[idx] not in string.printable:
break
if idx == self.idx:
self.cn = ''
self.en = self.des[idx:end]
elif idx == end-1:
self.cn = self.des[idx:end]
self.en = self.des[idx:end]
else:
self.cn = self.des[self.idx:idx+1]
self.en = self.des[idx+1:end]
self.idx = end
return True
def read_info(self):
text = self.des[self.idx:]
res = re.findall(r'【([^【】]+)】:([^【】]+)', text, re.M)
for item in res:
self.info.append({
'name': item[0],
'value': item[1],
})
================================================
FILE: jsondict/anki.py
================================================
import sys
import csv
from search import search_string, description
res = []
def cloze_word(sentense, word, level="1"):
return sentense.lower().replace(word, "{{c"+level+"::"+word+"}}")
with open(sys.argv[1], 'r') as f:
contents = f.read()
items = contents.split("\n\n")
for item in items:
lines = item.split("\n")
word = lines[0]
exp = lines[1]
eg = lines[2:]
dct = search_string(word)
exp2 = cloze_word(description(word), word, "1")
for x in [{"sentence": cloze_word(x, word, "1"),"exp2": exp2, "exp": exp, "dict": dct } for x in eg]:
res.append(x)
with open('{0}.csv'.format(sys.argv[1]), 'w') as csv_file:
fieldnames = ['sentence','exp2', 'exp', 'dict']
writer = csv.DictWriter(csv_file, fieldnames=fieldnames)
for row in res:
print(row['sentence'])
writer.writerows(res)
================================================
FILE: jsondict/search.py
================================================
#!/usr/bin/python
# -*- coding: utf-8 -*-
import sys
import json
with open('dict.json', 'r') as f:
dictionary = json.load(f)
def search(word):
try:
return dictionary[word]
except:
return None
def format(item):
res = "{word}\tStar:{star}\n".format(**item)
for desc in item['descriptions']:
res += "\t{description}\n".format(**desc)
for ex in desc['examples']:
res += "\t\tEN:\t{en}\n\t\tCN:\t{cn}\n".format(**ex)
res +="\n"
return res
def description(word):
item = search(word)
if item is None:
return ""
return "\n".join([x['description'] for x in item['descriptions']])
def search_string(word):
res = search(word)
if res is None:
return ""
return format(res)
if __name__ == '__main__':
res = search(sys.argv[1])
if res is not None:
print(format(res))
================================================
FILE: manage.py
================================================
import os
import sys
ROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT_DIR)
import commands
import app
def command(method, *args):
if method == 'task':
commands.run(*args)
elif method == 'dev_server':
app.run_dev()
else:
print("unknown command {0}".format(method))
def usage():
print("{0} task".format( sys.argv[0]))
if __name__ == '__main__':
if len(sys.argv) > 1:
command(*sys.argv[1:])
else:
usage()
================================================
FILE: nginx-server.conf
================================================
server {
listen 80;
server_name 0.0.0.0;
location /api/ {
include uwsgi_params;
uwsgi_pass unix:/var/run/wordai.sock;
}
location / {
root /usr/src/app/ui/dist/;
}
}
================================================
FILE: nginx.conf
================================================
user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 768;
}
http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}
================================================
FILE: requirements.txt
================================================
mongoengine
mongoengine-goodjson
lxml
translate-toolkit
nltk
flask
flask-restful
flask-jwt-extended
flask-cors
bcrypt
jsonschema
================================================
FILE: start.sh
================================================
#!/bin/bash
service nginx start
uwsgi --ini config/wsgi.ini
================================================
FILE: ui/.babelrc
================================================
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"]
}
================================================
FILE: ui/.editorconfig
================================================
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: ui/.eslintignore
================================================
/build/
/config/
/dist/
/*.js
================================================
FILE: ui/.eslintrc.js
================================================
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}
================================================
FILE: ui/.gitignore
================================================
.DS_Store
node_modules/
/dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
================================================
FILE: ui/.postcssrc.js
================================================
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
"plugins": {
"postcss-import": {},
"postcss-url": {},
// to edit target browsers: use "browserslist" field in package.json
"autoprefixer": {}
}
}
================================================
FILE: ui/README.md
================================================
# wordai
> Word AI
## Build Setup
``` bash
# install dependencies
npm install
# serve with hot reload at localhost:8080
npm run dev
# build for production with minification
npm run build
# build for production and view the bundle analyzer report
npm run build --report
```
For a detailed explanation on how things work, check out the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader).
================================================
FILE: ui/build/build.js
================================================
'use strict'
require('./check-versions')()
process.env.NODE_ENV = 'production'
const ora = require('ora')
const rm = require('rimraf')
const path = require('path')
const chalk = require('chalk')
const webpack = require('webpack')
const config = require('../config')
const webpackConfig = require('./webpack.prod.conf')
const spinner = ora('building for production...')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, (err, stats) => {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build.
chunks: false,
chunkModules: false
}) + '\n\n')
if (stats.hasErrors()) {
console.log(chalk.red(' Build failed with errors.\n'))
process.exit(1)
}
console.log(chalk.cyan(' Build complete.\n'))
console.log(chalk.yellow(
' Tip: built files are meant to be served over an HTTP server.\n' +
' Opening index.html over file:// won\'t work.\n'
))
})
})
================================================
FILE: ui/build/check-versions.js
================================================
'use strict'
const chalk = require('chalk')
const semver = require('semver')
const packageConfig = require('../package.json')
const shell = require('shelljs')
function exec (cmd) {
return require('child_process').execSync(cmd).toString().trim()
}
const versionRequirements = [
{
name: 'node',
currentVersion: semver.clean(process.version),
versionRequirement: packageConfig.engines.node
}
]
if (shell.which('npm')) {
versionRequirements.push({
name: 'npm',
currentVersion: exec('npm --version'),
versionRequirement: packageConfig.engines.npm
})
}
module.exports = function () {
const warnings = []
for (let i = 0; i < versionRequirements.length; i++) {
const mod = versionRequirements[i]
if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) {
warnings.push(mod.name + ': ' +
chalk.red(mod.currentVersion) + ' should be ' +
chalk.green(mod.versionRequirement)
)
}
}
if (warnings.length) {
console.log('')
console.log(chalk.yellow('To use this template, you must update following to modules:'))
console.log()
for (let i = 0; i < warnings.length; i++) {
const warning = warnings[i]
console.log(' ' + warning)
}
console.log()
process.exit(1)
}
}
================================================
FILE: ui/build/utils.js
================================================
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
================================================
FILE: ui/build/vue-loader.conf.js
================================================
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
================================================
FILE: ui/build/webpack.base.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
const createLintingRule = () => ({
test: /\.(js|vue)$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter'),
emitWarning: !config.dev.showEslintErrorsInOverlay
}
})
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'@': resolve('src'),
}
},
module: {
rules: [
...(config.dev.useEslint ? [createLintingRule()] : []),
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.s[a|c]ss$/,
loader: 'style!css!sass'
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
================================================
FILE: ui/build/webpack.dev.conf.js
================================================
'use strict'
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const path = require('path')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
const portfinder = require('portfinder')
const HOST = process.env.HOST
const PORT = process.env.PORT && Number(process.env.PORT)
const devWebpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true })
},
// cheap-module-eval-source-map is faster for development
devtool: config.dev.devtool,
// these devServer options should be customized in /config/index.js
devServer: {
clientLogLevel: 'warning',
historyApiFallback: {
rewrites: [
{ from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
],
},
hot: true,
contentBase: false, // since we use CopyWebpackPlugin.
compress: true,
host: HOST || config.dev.host,
port: PORT || config.dev.port,
open: config.dev.autoOpenBrowser,
overlay: config.dev.errorOverlay
? { warnings: false, errors: true }
: false,
publicPath: config.dev.assetsPublicPath,
proxy: config.dev.proxyTable,
quiet: true, // necessary for FriendlyErrorsPlugin
watchOptions: {
poll: config.dev.poll,
}
},
plugins: [
new webpack.DefinePlugin({
'process.env': require('../config/dev.env')
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update.
new webpack.NoEmitOnErrorsPlugin(),
// https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.dev.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = process.env.PORT || config.dev.port
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
// publish the new Port, necessary for e2e tests
process.env.PORT = port
// add port to devServer config
devWebpackConfig.devServer.port = port
// Add FriendlyErrorsPlugin
devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${port}`],
},
onErrors: config.dev.notifyOnErrors
? utils.createNotifierCallback()
: undefined
}))
resolve(devWebpackConfig)
}
})
})
================================================
FILE: ui/build/webpack.prod.conf.js
================================================
'use strict'
const path = require('path')
const utils = require('./utils')
const webpack = require('webpack')
const config = require('../config')
const merge = require('webpack-merge')
const baseWebpackConfig = require('./webpack.base.conf')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const env = require('../config/prod.env')
const webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true,
usePostCSS: true
})
},
devtool: config.build.productionSourceMap ? config.build.devtool : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
// http://vuejs.github.io/vue-loader/en/workflow/production.html
new webpack.DefinePlugin({
'process.env': env
}),
new UglifyJsPlugin({
uglifyOptions: {
compress: {
warnings: false
}
},
sourceMap: config.build.productionSourceMap,
parallel: true
}),
// extract css into its own file
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css'),
// Setting the following option to `false` will not extract CSS from codesplit chunks.
// Their CSS will instead be inserted dynamically with style-loader when the codesplit chunk has been loaded by webpack.
// It's currently set to `true` because we are seeing that sourcemaps are included in the codesplit bundle as well when it's `false`,
// increasing file size: https://github.com/vuejs-templates/webpack/issues/1110
allChunks: true,
}),
// Compress extracted CSS. We are using this plugin so that possible
// duplicated CSS from different components can be deduped.
new OptimizeCSSPlugin({
cssProcessorOptions: config.build.productionSourceMap
? { safe: true, map: { inline: false } }
: { safe: true }
}),
// generate dist index.html with correct asset hash for caching.
// you can customize output by editing /index.html
// see https://github.com/ampedandwired/html-webpack-plugin
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true,
removeAttributeQuotes: true
// more options:
// https://github.com/kangax/html-minifier#options-quick-reference
},
// necessary to consistently work with multiple chunks via CommonsChunkPlugin
chunksSortMode: 'dependency'
}),
// keep module.id stable when vendor modules does not change
new webpack.HashedModuleIdsPlugin(),
// enable scope hoisting
new webpack.optimize.ModuleConcatenationPlugin(),
// split vendor js into its own file
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks (module) {
// any required modules inside node_modules are extracted to vendor
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
// extract webpack runtime and module manifest to its own file in order to
// prevent vendor hash from being updated whenever app bundle is updated
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
minChunks: Infinity
}),
// This instance extracts shared chunks from code splitted chunks and bundles them
// in a separate chunk, similar to the vendor chunk
// see: https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: 3
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
const CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig
================================================
FILE: ui/config/dev.env.js
================================================
'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"http://localhost:8000/api"',
})
================================================
FILE: ui/config/index.js
================================================
'use strict'
// Template version: 1.3.1
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
module.exports = {
dev: {
// Paths
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
// Various Dev Server settings
host: 'localhost', // can be overwritten by process.env.HOST
port: 8080, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined
autoOpenBrowser: false,
errorOverlay: true,
notifyOnErrors: true,
poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions-
// Use Eslint Loader?
// If true, your code will be linted during bundling and
// linting errors and warnings will be shown in the console.
useEslint: true,
// If true, eslint errors and warnings will also be shown in the error overlay
// in the browser.
showEslintErrorsInOverlay: false,
/**
* Source Maps
*/
// https://webpack.js.org/configuration/devtool/#development
devtool: 'cheap-module-eval-source-map',
// If you have problems debugging vue-files in devtools,
// set this to false - it *may* help
// https://vue-loader.vuejs.org/en/options.html#cachebusting
cacheBusting: true,
cssSourceMap: true
},
build: {
// Template for index.html
index: path.resolve(__dirname, '../dist/index.html'),
// Paths
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
/**
* Source Maps
*/
productionSourceMap: true,
// https://webpack.js.org/configuration/devtool/#production
devtool: '#source-map',
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
// Run the build command with an extra argument to
// View the bundle analyzer report after build finishes:
// `npm run build --report`
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: process.env.npm_config_report
}
}
================================================
FILE: ui/config/prod.env.js
================================================
'use strict'
module.exports = {
NODE_ENV: '"production"',
BASE_API: '"/api"',
}
================================================
FILE: ui/index.html
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1,minimum-scale=1, user-scalable=no">
<title>Word-AI</title>
</head>
<style>
body {
margin:0;
}
</style>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
================================================
FILE: ui/package.json
================================================
{
"name": "wordai",
"version": "1.0.0",
"description": "Word AI",
"author": "Senghoo Kim <shkdmb@gmail.com>",
"private": true,
"scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev",
"lint": "eslint --ext .js,.vue src",
"build": "node build/build.js"
},
"dependencies": {
"axios": "^0.19.0",
"element-ui": "^2.12.0",
"font-awesome": "^4.7.0",
"js-cookie": "^2.2.1",
"moment-timezone": "^0.5.26",
"vue": "^2.5.2",
"vue-axios": "^2.1.4",
"vue-cli": "^2.9.6",
"vue-moment": "^4.0.0",
"vue-router": "^3.0.1",
"vue-underscore": "^0.1.4",
"vuex": "^3.1.1"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
"babel-core": "^6.22.1",
"babel-eslint": "^8.2.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1",
"babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.11",
"echarts": "^4.2.1",
"eslint": "^4.15.0",
"eslint-config-standard": "^10.2.1",
"eslint-friendly-formatter": "^3.0.0",
"eslint-loader": "^1.7.1",
"eslint-plugin-import": "^2.7.0",
"eslint-plugin-node": "^5.2.0",
"eslint-plugin-promise": "^3.4.0",
"eslint-plugin-standard": "^3.0.1",
"eslint-plugin-vue": "^4.0.0",
"extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2",
"node-sass": "^4.12.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0",
"portfinder": "^1.0.13",
"postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1",
"rimraf": "^2.6.0",
"sass-loader": "^7.3.1",
"semver": "^5.3.0",
"shelljs": "^0.7.8",
"style-loader": "^1.0.0",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8",
"vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0",
"webpack-bundle-analyzer": "^3.3.2",
"webpack-dev-server": "^3.1.11",
"webpack-merge": "^4.1.0"
},
"engines": {
"node": ">= 6.0.0",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
================================================
FILE: ui/src/App.vue
================================================
<template>
<div id="app">
<router-view/>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
<style lang="scss">
#app {
direction: ltr;
color: #1a3b71;
background-color: #DFE6E5;
background-color: #DFE6E5;
flex-direction: column;
overflow: hidden;
height: 100vh;
}
</style>
================================================
FILE: ui/src/components/Card.vue
================================================
<template>
<div class="card" v-if="!alldone">
<div class="cn" >
<span>{{quiz.cn}}</span>
</div>
<div class="quiz">
<span class="en">{{quiz.cloze}}</span>
</div>
<el-form label-width="50px" class="answer">
<el-form-item v-for="(ans, idx) in answers" label="答案">
<el-input v-model="answers[idx]"
@keydown.enter.native="submitAnswer"
@keydown.tab.native="(event)=>{tabEvent(event, idx)}"
@keydown.space.native="(event)=>{spaceDownEvent(event, idx)}"
@keyup.space.native="(event)=>{spaceUpEvent(event, idx)}"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
:ref="'answer-'+idx" >
<div slot="suffix">
<el-tooltip v-model=showAns[idx] :content="quiz.answers[idx]" effect="light" placement="right">
<span> 查看答案</span>
</el-tooltip>
</div>
</el-input>
</el-form-item>
</el-form>
</div>
<div v-else>
<el-row>
<span>您已经完成所有字卡。</span>
</el-row>
</div>
</template>
<script>
import Dictionary from './Dictionary'
export default {
name: 'Main',
components: {
Dictionary
},
mounted (){
this.load_quiz()
},
data () {
return {
showAns:{},
showDict: false,
quiz: {},
answers: [],
alldone: false
}
},
methods: {
load_quiz (){
this.showDict = false
this.axios({
"method": "GET",
"url": "/learn/word"
}).then(res => {
this.quiz = res.data
this.cleanAnswer()
this.$nextTick(() => {
this.focusAnswer(0)
})
}, err=>{
if (err.response.status == 404) {
this.alldone = true
}
})
},
cleanAnswer(){
this.answers = []
for (var i = 0; i < this.quiz.check.length; i++) {
this.answers.push('')
}
},
focusAnswer(idx){
this.$refs["answer-"+idx][0].focus()
},
submitAnswer() {
this.axios({
'method': 'POST',
'url': '/learn/word',
'data': {
'check': this.quiz.check,
'id': this.quiz.id,
'sid': this.quiz.sid,
'answers': this.answers
}
}).then(res => {
if (res.data.result) {
this.load_quiz()
this.$emit('correct', this.quiz.word)
} else {
this.cleanAnswer()
this.$emit('wrong', this.quiz.word)
}
})
},
tabEvent(event, idx){
if (this.answers.length > idx+1){
this.focusAnswer(idx+1)
}else{
this.focusAnswer(0)
}
event.preventDefault()
},
spaceDownEvent(event, idx){
this.showAns[idx] = true
event.preventDefault()
},
spaceUpEvent(event, idx){
this.showAns[idx] = false
event.preventDefault()
}
}
}
</script>
<style lang="scss" scoped>
.cn{
padding:20px 20px 0px 20px;
}
.quiz{
padding:5px 20px 20px 20px;
font-size: 1.5em;
}
.answer{
padding: 0px 20px 20px 20px;
}
</style>
================================================
FILE: ui/src/components/Dictionary.vue
================================================
<template>
<div class="main">
<div class="dict-head">
<span>{{word}}</span>
</div>
<el-collapse v-model="activeName" accordion>
<el-collapse-item v-for="desc in define" :name="desc.seq">
<template slot="title">
<el-tag size="mini">{{desc.speech}}</el-tag>
<span class="en">{{desc.cn}}</span>
</template>
<div>{{desc.en}}</div>
<div v-for="eg in desc.examples">
<el-divider content-position="left">例句</el-divider>
<div>{{eg.en}}</div>
<div>{{eg.cn}}</div>
</div>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script>
export default {
name: 'Main',
mounted (){
this.load_dict()
},
computed: {
},
data () {
return {
start: 0,
define: [],
activeName: '1'
}
},
props: {
word: ''
},
watch: {
word () {
this.load_dict()
}
},
methods: {
load_dict (){
this.axios({
"method": "GET",
"url": "/dictionary/"+this.word
}).then(res => {
this.star = res.data.star
this.define = res.data.descriptions
}, err=>{
})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style rel="stylesheet/scss" lang="scss" scoped>
.dict-head{
text-align: left;
font-size: 2em;
background: #C1D5D6;
padding: 5px;
}
.answer{
margin: 30px;
}
.card {
margin:100px auto;
width:500px;
background-color: white;
min-height: 250px;
border-radius: .5rem;
box-shadow: 0 3px 0.5rem #d9d9d9;
.quiz {
margin: 10px 30px;
}
.en {
font-size: 2rem;
}
}
.main{
}
.el-row {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.en{
line-height: 13px;
padding: 5px 10px;
align-items: flex-start;
overflow: hidden;
}
</style>
================================================
FILE: ui/src/components/Login.vue
================================================
<template>
<div class="login">
<el-row>
<el-col :span="24">
<el-input id="name" v-model="name" placeholder="请输入帐号">
<template slot="prepend">帐号</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-input id="password" v-model="password" type="password" placeholder="请输入密码" @keyup.enter.native="login">
<template slot="prepend">密码</template>
</el-input>
</el-col>
</el-row>
<el-row>
<el-col :span="24">
<el-button id="login" v-on:click="login" style="width:100%" type="primary">登录</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
data() {
return {
name: '',
password: '',
passwordErr: false
}
},
computed: {
...mapGetters([
'username',
])
},
methods: {
...mapActions([
`fetchJWT`
]),
login() {
this.fetchJWT({
// #Security...
username: this.name,
password: this.password
}).then(()=>{
this.$router.push({ path: '/' })
}, (err)=>{
console.log(err)
this.$message({
message: '用户名密码错误',
type: 'warning'
});
})
}
},
mounted() {
}
}
</script>
<style rel="stylesheet/scss" lang="scss" scoped>
.login {
margin:20% auto;
width:300px;
}
.el-row {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
</style>
================================================
FILE: ui/src/components/Main.vue
================================================
<template>
<div class="main">
<div class="content">
<el-menu class="right-menu" :collapse="true" active-text-color="#303133">
<el-submenu index="1">
<template slot="title">
<i class="el-icon-notebook-2"></i>
<!-- <i class="el-icon-list-alt"></i> -->
<span slot="title">单词表</span>
</template>
<el-menu-item-group>
<span slot="title">单词表</span>
<el-menu-item index="1-1" @click="openLearned">已学单词</el-menu-item>
<el-menu-item index="1-2" @click="openToLearn">未学单词</el-menu-item>
</el-menu-item-group>
</el-submenu>
<el-menu-item index="2" @click="openStatistic">
<i class="el-icon-s-marketing"></i>
<span slot="title">统计数据</span>
</el-menu-item>
<el-menu-item index="3" >
<i class="el-icon-reading" @click="openWordlistList"></i>
<span slot="title">单词本</span>
</el-menu-item>
<el-menu-item index="4" @click="settingVisible = true">
<i class="el-icon-setting"></i>
<span slot="title">设置</span>
</el-menu-item>
</el-menu>
<el-row>
<el-col :sm="{span: 18, offset: 3}" :md="{span: 12, offset: 6}" class="card" >
<card ref="card" v-on:correct="onCorrect" v-on:wrong="onWrong"></card>
</el-col>
</el-row>
</div>
<el-dialog title="单词本" :visible.sync="wordlistListVisible" width="90%" class="wordlist-list" custom-class="wordlist-list-2">
<wordlist-list ref="wordlistList"> </wordlist-list>
</el-dialog>
<el-dialog title="设置" :visible.sync="settingVisible">
<wordlist-setting ref="wordlistSetting"> </wordlist-setting>
<div slot="footer" class="dialog-footer">
<el-button @click="settingVisible = false">取 消</el-button>
<el-button type="primary" @click="settingSubmit">确 定</el-button>
</div>
</el-dialog>
<el-dialog
title="统计信息"
:visible.sync="statisticVisible"
width="80%"
center>
<statistic ref="statistics"></statistic>
<span slot="footer" class="dialog-footer">
<el-button @click="statisticVisible = false">关 闭</el-button>
</span>
</el-dialog>
<el-drawer class="right-drawer"
title="单词表 "
custom-class="right-drawer"
:visible.sync="showWordlist"
size="250"
direction="rtl">
<wordlist ref="wordlist" class="wordlist"></wordlist>
</el-drawer>
<el-drawer class="bottom-drawer"
title="词典"
custom-class="dict-drawer"
:visible.sync="showDict"
:modal="false"
direction="btt">
<dictionary :word="word"></dictionary>
</el-drawer>
</div>
</template>
<script>
import 'element-ui/lib/theme-chalk/display.css';
import Dictionary from './Dictionary'
import Card from './Card'
import wordlist from './wordlist'
import WordlistSetting from './wordlist/Setting'
import WordlistList from './wordlist/List'
import statistic from './statistic'
export default {
name: 'Main',
components: {
Dictionary,
Card,
wordlist,
WordlistSetting,
WordlistList,
statistic
},
mounted (){},
data () {
return {
showDict: false,
word:"",
showWordlist: false,
settingVisible: false,
statisticVisible: false,
wordlistListVisible: false
}
},
methods: {
openWordlistList(){
this.wordlistListVisible = true
},
openStatistic(){
this.statisticVisible = true
this.$refs.statistics.load()
},
openLearned(){
this.showWordlist = true
this.$nextTick(() => {
this.$refs.wordlist.openLearned()
})
},
openToLearn(){
this.showWordlist = true
this.$nextTick(() => {
this.$refs.wordlist.openToLearn()
})
},
onWrong(word) {
this.word = word
this.showDict = true
this.$message({
message: '回答错误',
type: 'warning'
})
},
onCorrect() {
this.word = ""
this.showDict = false
this.$message({
message: '回答正确,继续努力!!',
type: 'success'
})
},
settingSubmit() {
var self = this
this.$refs.wordlistSetting.submit().then(()=>{
self.$refs.card.load_quiz()
self.settingVisible = false
self.$message({
message: '设置成功',
type: 'success'
})
})
}
}
}
</script>
<style lang="scss">
.dict-drawer{
overflow-y: scroll;
#el-drawer__title{
padding: 5px 10px;
margin: 0;
}
}
</style>
<style lang="scss" scoped>
.content{
margin: 5px;
}
.right-menu{
position: fixed;
bottom: 20px;
right: 5px;
}
.content{
padding-top: 20px;
}
@media(min-width: 768px){
.content{
padding-top: 20vh;
}
.right-menu{
position: absolute;
z-index:100;
bottom: initial;
right: initial;
}
}
.main{
height: 100%;
}
.top-padding{
height: 20%;
}
.card {
background-color: white;
min-height: 250px;
border-radius: .5rem;
box-shadow: 0 3px 0.5rem #d9d9d9;
}
.dict {
width: 800px;
margin: 0 auto;
overflow: hidden;
}
.right-drawer{
max-height: 100%;
.right-drawer-body{
max-height: 100%;
min-width:300px;
}
}
.wordlist{
height: 500px;
}
</style>
================================================
FILE: ui/src/components/statistic/DayChart.vue
================================================
<template>
<div>
<div id="day_chart_echart" style="height: 400px; width: 100%;"></div>
</div>
</template>
<script>
var echarts = require('echarts');
export default {
mounted () {
this.load_data();
},
data () {
return {
// now: new Date(),
data: {
exercise: {},
review: {}
}
}
},
computed:{
xaxis() {
var res = []
return this.$_.sortBy(
this.$_.uniq(this.$_.union(
this.$_.keys(this.data.exercise),
this.$_.keys(this.data.review))),
a=>{a}
)
return res
},
yaxis(){
return [
{name: '已学', type: 'line', data: this.$_.map(this.xaxis, date=>{
return this.data.exercise[date] ? this.data.exercise[date] : 0
})},
{name: '需要复习', type: 'line', data: this.$_.map(this.xaxis, date=>{
return this.data.review[date] ? this.data.review[date] : 0
})}
]
}
},
methods: {
load_data(){
this.axios({
"method": "GET",
"url": "/statistic/learn",
}).then(res=>{
this.data = res.data
this.init_echarts()
})
},
init_echarts () {
// 基于准备好的dom,初始化echarts实例
var echart_etl_stat = echarts.init(document.getElementById('day_chart_echart'));
// 设置option
var echart_etl_stat_option = {
title: {
text: '学习数量'
},
tooltip: {
trigger: 'axis'
},
legend: {
data:['已学', '需要复习' ]
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: this.xaxis
},
yAxis: {
type: 'value'
},
series: this.yaxis
};
// 绘制图表
echart_etl_stat.setOption(echart_etl_stat_option);
},
needReview(act){
var now = this.$moment()
var review = this.$moment(act.review)
return review.isBefore(now)
},
tp (act){
if (this.needReview(act)){
return 'danger'
} else {
return 'success'
}
},
ts (act) {
var prefix='预计复习: '
if (this.needReview(act)){
prefix = '记忆过期:'
}
return prefix + this.$moment(act.review).tz("Asia/Shanghai").fromNow()
}
}
}
</script>
<style lang="scss" scoped>
ul{
max-height: 500px;
}
</style>
================================================
FILE: ui/src/components/statistic/index.vue
================================================
<template>
<div>
<el-row>
<el-col :span="24">
<day-chart ref="dayChart"></day-chart>
</el-col>
</el-row>
</div>
</template>
<script>
import DayChart from './DayChart'
export default {
components: {
DayChart,
},
data() {
return {
};
},
methods: {
load(){
this.$refs.dayChart.load()
}
}
}
</script>
<style lang="scss" scoped>
</style>
================================================
FILE: ui/src/components/wordlist/Form.vue
================================================
<template>
<el-form :model="form" label-width="80px">
<el-form-item label="名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="描述">
<el-input v-model="form.description"></el-input>
</el-form-item>
<label class="wl-label" >单词列表:</label>
<div class="wl-border">
<div class="wl-container">
<div class="wl-backdrop" ref="backdrop">
<div class="wl-highlights" v-html="highlight"></div>
</div>
<textarea ref="textInput" class="wl-input" v-model="wordText"
autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
@keydown.enter="load_define"
@blur="load_define"
></textarea>
</div>
</div>
<br/>
<el-form-item>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="cancel">取消</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
mounted (){
this.load_word()
this.$refs.textInput.addEventListener('scroll', () => {
this.$refs.backdrop.scrollTop = this.$refs.textInput.scrollTop
}, false)
},
data () {
return {
wordText: '',
form: {
name: '',
description: '',
},
checkRes: {
defines: {},
not_dict: [],
not_sentence: []
}
}
},
props: {
value: ''
},
watch: {
value () {
this.load_word()
}
},
computed: {
words() {
return this.$_.uniq(this.$_.reject(
this.$_.map(
this.wordText.split("\n"),
w=>{
return w.trim()
}
), word=>{
return !word || word === ''
}))
},
highlight(){
var texts = this.wordText
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'").split("\n")
return this.$_.map(texts, l=>{
var w = l.trim()
var define = this.checkRes.defines[w]
if (define){
return '<div class="wl-line-div"><span class="highlight-word">' + l +'</span> <span class="desc">'+define.descriptions[0].description+'</span></div>'
} else if (w ==='') {
return l
} else {
return '<div class="wl-line-div"><span class="highlight-word-ne">' + l +'</span> <span class="desc">未找到定义或例句</span></div>'
}
}).join('')
}
},
methods: {
cancel() {
this.$emit('cancel')
},
load_word() {
this.clean()
if (this.value){
this.axios({
'method': 'GET',
'url': '/wordlist/'+this.value,
}).then(res=>{
this.form.name = res.data.name
this.form.description = res.data.description
this.wordText = res.data.words.join('\n')
this.load_define()
}, err=>{
this.$message({
message: '您无权访问此单词表',
type: 'warning'
})
this.cancel()
})
}
},
clean(){
this.form.name =''
this.form.description = ''
this.wordText = ''
this.checkRes = {
defines: {},
not_dict: [],
not_sentence: []
}
},
submit() {
if (!this.value){
this.axios({
'method': 'POST',
'url': '/wordlist',
'data': {
'name': this.form.name,
'description': this.form.description,
'words': this.words
}
}).then(res=>{
this.$emit('submit')
})
} else {
this.axios({
'method': 'PUT',
'url': '/wordlist/'+this.value,
'data': {
'name': this.form.name,
'description': this.form.description,
'words': this.words
}
}).then(res=>{
this.$emit('submit')
})
}
},
load_define(){
this.axios({
'method': 'PUT',
'url': '/wordlist',
'data': this.words
}).then(res=>{
this.checkRes = res.data
})
}
}
}
</script>
<style lang="scss">
.wl-container, .wl-backdrop, .wl-input {
width: 100%;
height: 180px;
}
.wl-highlights, .wl-input {
font-family: Consolas,Liberation Mono,Courier,monospace;
letter-spacing: 1px;
font-size: 14px; word-wrap: break-word;
}
.wl-border{
border: 1px solid;
}
.wl-container {
display: block;
margin: 0 auto;
transform: translateZ(0);
-webkit-text-size-adjust: none;
padding:1px;
}
.wl-backdrop {
position: absolute;
z-index: 1;
background-color: #fff;
overflow: auto;
pointer-events: none;
transition: transform 1s;
}
.wl-highlights {
white-space: pre-wrap;
word-wrap: break-word;
color: transparent;
/* color: red; */
}
.wl-input {
display: block;
position: absolute;
z-index: 2;
margin: 0;
border: 2px solid #74637f;
border-radius: 0;
color: #444;
background-color: transparent;
overflow: auto;
resize: none;
transition: transform 1s;
padding:0;
border-style: none;
}
.wl-line-div{
overflow: visible;
white-space: nowrap;
height:16px;
}
.highlight-word{
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.highlight-word-ne{
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border-radius: 3px;
color: transparent;
border-bottom:1px solid red;
}
.desc{
display: inline-block;
overflow: hidden;
text-overflow: ellipsis;
overflow-y: vi;
white-space: nowrap;
color: gray;
font-size: 12px;
}
mark {
color: transparent;
background-color: #d4e9ab; /* or whatever */
}
textarea {
margin: 0;
border-radius: 0;
}
textarea:focus, button:focus {
outline: none;
box-shadow: 0 0 0 2px #c6aada;
}
</style>
================================================
FILE: ui/src/components/wordlist/Learned.vue
================================================
<template>
<el-timeline>
<el-timeline-item
v-for="(activity, index) in learned"
:key="activity.wordname"
:type="tp(activity)"
size="large"
color="color"
:timestamp="ts(activity)">
{{activity.wordname}}
</el-timeline-item>
</el-timeline>
</template>
<script>
export default {
mounted () {
this.load()
},
data () {
return {
// now: new Date(),
learned: []
}
},
methods: {
load () {
this.axios({
'method': 'GET',
'url': '/wordlist/learned'
}).then(res=>{
this.learned = res.data
})
},
needReview(act){
var now = this.$moment()
var review = this.$moment(act.review)
return review.isBefore(now)
},
tp (act){
if (this.needReview(act)){
return 'danger'
} else {
return 'success'
}
},
ts (act) {
var prefix='预计复习: '
if (this.needReview(act)){
prefix = '记忆过期:'
}
return prefix + this.$moment(act.review).tz("Asia/Shanghai").fromNow()
}
}
}
</script>
<style lang="scss" scoped>
ul{
max-height: 500px;
}
</style>
================================================
FILE: ui/src/components/wordlist/List.vue
================================================
<template>
<div>
<div class="wordlist" v-if="show=='list'">
<el-button type="primary" icon="el-icon-edit" size="mini" @click="create">新建</el-button>
<el-table
:data="lists"
style="width: 100%">
<el-table-column
label="名称"
prop="name">
</el-table-column>
<el-table-column
label="单词数"
prop="words"
:formatter="formatWordCount">
</el-table-column>
<el-table-column
prop="user"
label="类型"
width="100">
<template slot-scope="scope">
<el-tag
:type="scope.row.user ? 'success' : 'primary'"
disable-transitions>{{scope.row.user ? '用户' : '系统'}}</el-tag>
</template>
</el-table-column>
<el-table-column
align="right">
<template slot-scope="scope">
<el-button
v-if="scope.row.user"
type="text"
size="mini"
@click="handleEdit(scope.$index, scope.row)">编辑</el-button>
<el-button
v-if="scope.row.user"
type="text danger"
size="mini"
@click="handleDelete(scope.$index, scope.row)">Delete</el-button>
</template>
</el-table-column>
</el-table>
</div>
<div class="wordlist" v-if="show=='form'">
<wordlist-form
v-model="formId"
@cancel="formCancel"
@submit="formSubmit"
>
</wordlist-form>
</div>
</div>
</template>
<script>
import WordlistForm from './Form'
export default {
components: {
WordlistForm
},
mounted (){
this.load_list()
},
data () {
return {
formId: null,
show: 'list',
lists: [],
selected: {}
}
},
methods: {
formCancel(){
this.formId = null
this.show = 'list'
this.load_list()
},
formSubmit(){
this.formId = null
this.show = 'list'
this.load_list()
},
handleEdit(idx, row){
this.formId = row.id
this.show = 'form'
},
handleDelete(idx, row){
this.axios({
'method': 'DELETE',
'url': '/wordlist/'+row.id,
}).then(res=>{
this.$message({
message: '删除成功',
type: 'success'
})
this.load_list()
}, err=>{
this.$message({
message: '您无权访问此单词表',
type: 'warning'
})
this.load_list()
})
},
create(){
this.selected = {}
this.show='form'
},
load_list(){
return this.axios({
'method': 'GET',
'url': '/wordlist'
}).then(res=>{
this.lists = res.data
})
},
formatWordCount(row){
return row.words.length
}
}
}
</script>
<style lang="scss" scoped>
ul{
max-height: 500px;
}
</style>
================================================
FILE: ui/src/components/wordlist/Setting.vue
================================================
<template>
<el-form :model="form">
<el-form-item label="单词本" :label-width="'70px'">
<el-select v-model="form.wordlist" placeholder="请选择活动区域">
<el-option v-for="item in lists" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-form-item>
</el-form>
</template>
<script>
export default {
mounted (){
this.load_current()
this.load_list()
},
data () {
return {
form: {
wordlist: '',
},
lists: []
}
},
methods: {
load_current(){
return this.axios({
'method': 'GET',
'url': '/user/wordlist',
}).then(res=>{
this.form.wordlist = res.data.wordlist
})
},
load_list(){
return this.axios({
'method': 'GET',
'url': '/wordlist'
}).then(res=>{
this.lists = res.data
})
},
submit(){
return this.axios({
'method': 'POST',
'url': 'http://127.0.0.1:8000/api/user/wordlist',
'data': this.form
})
}
}
}
</script>
<style lang="scss" scoped>
ul{
max-height: 500px;
}
</style>
================================================
FILE: ui/src/components/wordlist/ToLearn.vue
================================================
<template>
<div class="list-container">
<ul class="infinite-list" v-infinite-scroll="load" style="overflow:auto">
<li v-for="i in to_learn" class="infinite-list-item">
<span class="word">{{i}}</span>
</li>
</ul>
</div>
</template>
<script>
export default {
data () {
return {
now: new Date(),
to_learn: []
}
},
methods: {
load () {
this.axios({
'method': 'GET',
'url': '/wordlist/to_learn'
}).then(res=>{
this.to_learn = res.data
})
}
}
}
</script>
<style lang="scss" scoped>
ul{
max-height: 500px;
}
</style>
================================================
FILE: ui/src/components/wordlist/index.vue
================================================
<template>
<el-tabs v-model="activeName" class="tabs">
<el-tab-pane label="已学单词" name="learned">
<learned ref="learned" class="words"></learned>
</el-tab-pane>
<el-tab-pane label="未学单词" name="tolearn">
<to-learn ref="toLearn" class="words"></to-learn>
</el-tab-pane>
</el-tabs>
</template>
<script>
import Learned from './Learned'
import ToLearn from './ToLearn'
export default {
components: {
Learned,
ToLearn
},
data() {
return {
activeName: 'learned'
};
},
methods: {
openLearned(){
this.$refs.learned.load()
this.$refs.toLearn.load()
this.activeName = 'learned'
},
openToLearn(){
this.$refs.learned.load()
this.$refs.toLearn.load()
this.activeName = 'tolearn'
},
}
}
</script>
<style lang="scss" scoped>
.tabs {
margin: 0 20px;
}
</style>
================================================
FILE: ui/src/main.js
================================================
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import VueAxios from 'vue-axios'
import axios from 'axios'
import underscore from 'vue-underscore'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import App from './App'
import router from './router'
import store from './store'
import request from './request'
import '@/permission'
Vue.use(ElementUI)
Vue.use(VueAxios, request)
Vue.use(underscore)
import moment from 'moment-timezone'
moment.tz.setDefault("UTC")
require('moment/locale/zh-cn')
Vue.use(require('vue-moment'), {
moment
})
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
================================================
FILE: ui/src/permission.js
================================================
import router from './router'
import store from './store'
import {getToken} from './store'
import { Message } from 'element-ui'
const whiteList = ['/login']
router.beforeEach((to, from, next) => {
if (getToken()) {
if (to.path === '/login') {
next({ path: '/' })
} else {
store.dispatch('getUserInfo').then(res => {
next()
}).catch((err) => {
store.dispatch('logOut').then(() => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
})
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next()
} else {
next('/login')
}
}
})
================================================
FILE: ui/src/request.js
================================================
import axios from 'axios'
import {MessageBox} from 'element-ui'
import store from './store'
import { getToken } from './store'
const service = axios.create({
baseURL: process.env.BASE_API
})
var lastRefresh = (new Date()).getTime()
service.interceptors.request.use(config => {
if (getToken()) {
if (!config.headers['Authorization']){
config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
var now = (new Date()).getTime()
// debugger;
if (now - lastRefresh > 1 * 60 * 1000 && config.url !== '/token/refresh' && config.url !== '/login') {
store.dispatch('refreshJWT').then(() => {
lastRefresh = now
})
}
}
return config
}, error => {
// Do something with request error
console.log(error) // for debug
Promise.reject(error)
})
service.interceptors.response.use(
response => {
return response
},
error => {
const req = error.request
const res = error.response
const url = new URL(req.responseURL)
if (res.status === 401 && url.pathname !== '/login') {
MessageBox.confirm('你已被登出', '确定登出', {
confirmButtonText: '重新登录',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
store.dispatch('logOut').then(() => {
location.reload()// 为了重新实例化vue-router对象 避免bug
})
})
}
return Promise.reject(error)
}
)
export default service
================================================
FILE: ui/src/router/index.js
================================================
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path: '/',
name: 'HelloWorld',
component: () => import('@/components/Main')
},
{
path: '/login',
name: 'Login',
component: () => import('@/components/Login')
}
]
})
================================================
FILE: ui/src/store.js
================================================
import Vue from 'vue'
import vuex from 'vuex'
import service from './request'
//import Cookies from 'js-cookie'
// Vue.use(service)
Vue.use(vuex)
const TokenKey = 'accessToken'
const refreshKey = 'refreshToken'
export function getToken () {
return sessionStorage.getItem(TokenKey)
}
export function getRefreshToken () {
return sessionStorage.getItem(refreshKey)
}
export function setToken (token) {
return sessionStorage.setItem(TokenKey, token)
}
export function setRefreshTokens (token) {
return sessionStorage.setItem(refreshKey, token)
}
export function removeToken () {
sessionStorage.removeItem(refreshKey)
return sessionStorage.removeItem(TokenKey)
}
export default new vuex.Store({
state: {
accessToken: '',
refreshToken: '',
wordlist: ''
},
getters: {
accessToken: state => state.accessToken,
refreshToken: state => state.refreshToken,
username: (state, getters) => state.accessToken ? JSON.parse(atob(getters.accessToken.split('.')[1])) : null,
},
mutations: {
setTokens (state, access) {
// When this updates, the getters and anything bound to them updates as well.
state.accessToken = access
},
setRefreshTokens (state, refresh) {
// When this updates, the getters and anything bound to them updates as well.
state.refreshToken = refresh
},
setWordList (state, wordlist) {
state.wordList = wordlist
}
},
actions: {
fetchJWT ({ commit }, { username, password }) {
return service({
'method': 'POST',
'url': '/login',
'headers': {
'Content-Type': 'application/json; charset=utf-8'
},
'data': {
'username': username,
'password': password
}
}).then(res => {
commit('setTokens', res.data.access_token)
commit('setRefreshTokens', res.data.refresh_token)
setToken(res.data.access_token)
setRefreshTokens(res.data.refresh_token)
} )
},
refreshJWT ({ commit, state}) {
return service({
'method': 'POST',
'url': '/token/refresh',
'headers': {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': 'Bearer ' + getRefreshToken()
}
}).then(res => {
commit('setTokens', res.data.access_token)
// commit('setRefreshTokens', res.data.refresh_token)
setToken(res.data.access_token, res.data.refresh_token)
})
},
getUserInfo ({commit, state}) {
return service({
'method': 'GET',
'url': '/user/wordlist',
'headers': {
'Content-Type': 'application/json; charset=utf-8',
'Authorization': 'Bearer ' + getToken()
}
}).then(res => {
commit('setWordList', res.wordlist)
})
},
logOut ({commit, state}) {
return new Promise((resolve, reject) => {
commit('setTokens', '', '')
removeToken()
resolve()
})
}
}
})
================================================
FILE: ui/static/.gitkeep
================================================
================================================
FILE: wordai/__init__.py
================================================
================================================
FILE: wordai/api/__init__.py
================================================
from flask import Flask, jsonify
from flask_cors import CORS
from flask_jwt_extended import (JWTManager)
app = Flask(__name__)
CORS(app)
jwt = JWTManager(app)
from wordai.models import User
@app.route('/')
def index():
return jsonify({'message': 'Hello, World!'})
from wordai.api.apis import blueprint as api
app.register_blueprint(api, url_prefix='/api')
================================================
FILE: wordai/api/apis.py
================================================
import hashlib
import json
from datetime import datetime, timedelta
import wordai.models as models
from flask import Blueprint
from flask_jwt_extended import (JWTManager, create_access_token,
create_refresh_token, get_jwt_identity,
get_raw_jwt, jwt_refresh_token_required,
jwt_required)
from flask_restful import Api, Resource, abort, reqparse, request
from jsonschema import validate
blueprint = Blueprint('profile', __name__,
template_folder='templates',
static_folder='static')
api = Api(blueprint)
class api_register(object):
def __init__(self, path):
self.path = path
def __call__(self, cls):
api.add_resource(cls, self.path)
return cls
def admin_required(f):
def __inner__(self, *args, **kwargs):
identify = get_jwt_identity()
user = models.User.find_by_username(identify)
if user and user.role == 'admin':
return f(self, user, *args, **kwargs)
return {
'message': 'Not found',
}, 404
return jwt_required(__inner__)
def user_required(f):
def __inner__(self, *args, **kwargs):
identify = get_jwt_identity()
user = models.User.find_by_username(identify)
if user and user.role in ['admin', 'user'] :
return f(self, user, *args, **kwargs)
return {
'message': 'Not found',
}, 404
return jwt_required(__inner__)
user_parser = reqparse.RequestParser()
user_parser.add_argument('username', help='This username cannot be blank', required=True)
user_parser.add_argument('password', help='This password cannot be blank', required=True)
@api_register("/registration")
class UserRegistration(Resource):
def post(self):
return {'message': 'User registration'}
@api_register("/login")
class UserLogin(Resource):
def post(self):
data = user_parser.parse_args()
current_user = models.User.check_user(data['username'], data['password'])
if not current_user:
abort(401)
return {
'message': 'User {} doesn\'t exist'.format(data['username']),
}
access_token = create_access_token(identity=data['username'])
refresh_token = create_refresh_token(identity=data['username'])
return {
'message': 'Logged in as {}'.format(current_user.username),
'role': current_user.role,
'access_token': access_token,
'refresh_token': refresh_token
}
@api_register("/token/refresh")
class TokenRefresh(Resource):
@jwt_refresh_token_required
def post(self):
current_user = get_jwt_identity()
if current_user:
access_token = create_access_token(identity=current_user)
return {
'access_token': access_token}
abort(401)
return {'message': 'invalid refresh token'}
@api_register("/wordlist")
class WordListList(Resource):
@user_required
def get(self, user):
return [json.loads(x.to_json()) for x in user.wordlists()]
@user_required
def put(self, user):
schema = {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True
}
try:
body = request.json
validate(instance=body, schema=schema)
wordok, not_has, wnot_has = models.WordList.check_word(*body)
defines = models.Word.search_words(*wordok)
return {
"defines": {w['word']: w for w in json.loads(defines.to_json())},
"not_dict": wnot_has,
"not_sentence": not_has,
}
except Exception as err:
return {
"message": "invalid request body",
"error": str(err)
}, 422
@user_required
def post(self, user):
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"description": {"type": "string"},
"words": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True
}
}
}
try:
body = request.json
validate(instance=body, schema=schema)
wordok, not_has, wnot_has = models.WordList.check_word(*body['words'])
body['words'] = list(wordok)
wordlist = models.WordList(**body)
wordlist.user = user
wordlist.save()
return {
"message": "ok",
"has": list(wordok),
"not_dict": wnot_has,
"not_sentence": not_has,
}
except Exception as err:
return {
"message": "invalid request body",
"error": str(err)
}, 422
@api_register("/wordlist/<string:lid>")
class WordListItem(Resource):
@user_required
def get(self, user, lid):
print(lid)
return json.loads(user.wordlists().filter(id=lid).first().to_json())
@user_required
def put(self, user, lid):
wordlist = models.WordList.objects(user=user, id=lid).first()
if not wordlist:
return {
"message": "wordlist not exists",
}, 404
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"description": {"type": "string"},
"words": {
"type": "array",
"items": {"type": "string"},
"uniqueItems": True
}
}
}
try:
body = request.json
validate(instance=body, schema=schema)
wordok, not_has, wnot_has = models.WordList.check_word(*body['words'])
wordlist.words = wordok
wordlist.name = body['name']
wordlist.description = body['description']
wordlist.user = user
wordlist.save()
return {
"message": "ok",
"has": list(wordok),
"not_dict": wnot_has,
"not_sentence": not_has,
}
except Exception as err:
return {
"message": "invalid request body",
"error": str(err)
}, 422
@user_required
def delete(self, user, lid):
wordlist = models.WordList.objects(user=user, id=lid).first()
if not wordlist:
return {
"message": "wordlist not exists",
}, 404
wordlist.delete()
@api_register("/user/wordlist")
class UserWordList(Resource):
@user_required
def get(self, user):
if not user.wordlist:
return {
"message", "wordlist not set"
}, 404
data = json.loads(user.wordlist.to_json())
return {
"message": "ok",
"wordlist": data['id'],
"wordlist_name": data['name']
}
@user_required
def post(self, user):
parser = reqparse.RequestParser()
parser.add_argument('wordlist', help='This wordlist cannot be blank', required=True)
wordlist_id = parser.parse_args()
wordlist = models.WordList.objects(id=wordlist_id['wordlist']).first()
user.wordlist = wordlist
user.save()
return {
"message": "ok",
"wordlist": wordlist.name
}
@api_register("/learn/word")
class LearnNext(Resource):
@user_required
def get(self, user):
ex = user.next_exercise()
if ex:
sentence_id = json.loads(ex.sentence.to_json())['id']
word_id = json.loads(ex.word.to_json())['id']
return {
"id": word_id,
"word": ex.word.word,
"message": "ok",
"cloze": ex.cloze,
"cn": ex.sentence.chn,
"sid": sentence_id,
"answers": [a for a in ex.answers],
"check": [hashlib.sha1((a+sentence_id+word_id).encode()).hexdigest() for a in ex.answers]
}
else:
return {
"message": "no word need exercise"
}, 404
@user_required
def post(self, user):
parser = reqparse.RequestParser()
parser.add_argument('id', help='This answers cannot be blank', required=True)
parser.add_argument('sid', help='This answers cannot be blank', required=True)
parser.add_argument('answers', help='This answers cannot be blank', required=True,action='append')
parser.add_argument('check', help='This answer_check cannot be blank', required=True, action='append')
data = parser.parse_args()
word_id = data['id']
word = models.Word.objects(id=word_id).first()
if not word:
return {
"message": "word not exist"
}, 404
sentence_id = data['sid']
answers = data['answers']
check = data['check']
check_res = [hashlib.sha1((a+sentence_id+word_id).encode()).hexdigest() for a in answers]
result = check == check_res
slog = models.SentenceLog(sentence=sentence_id, result=result, time=datetime.utcnow())
models.ExerciseLog.objects(user=user, word=word).update_one(
push__sentences=slog, wordname=word.word,
upsert=True)
log = models.ExerciseLog.objects(user=user, word=word).first()
log.calucate_review()
log.save()
return {
"message": "ok",
"result": result,
}
@api_register("/dictionary/<string:word>")
class Dictionary(Resource):
@user_required
def get(self, user, word):
define = models.Word.objects(word=word).first()
if define:
return json.loads(define.to_json())
else:
return {"message": "not found"}, 404
@api_register("/wordlist/learned")
class WordlistLearned(Resource):
@user_required
def get(self, user):
words = user.wordlist.user_learned(user).only("wordname", "review")
return json.loads(words.to_json())
@api_register("/wordlist/to_learn")
class WordlistToLearn(Resource):
@user_required
def get(self, user):
words = user.wordlist.user_to_learn(user)
return words
@api_register("/statistic/learn")
class StatisticLearn(Resource):
@user_required
def get(self, user):
return {
'exercise': models.ExerciseLog.exercise_count(
user,
datetime.now()-timedelta(days=7),
datetime.now()+timedelta(days=7)
),
'review': models.ExerciseLog.review_count(
user,
datetime.now()-timedelta(days=7),
datetime.now()+timedelta(days=7)
)
}
================================================
FILE: wordai/models/__init__.py
================================================
from mongoengine import connect
from config import mongo_config
connect(**mongo_config())
from .models import *
================================================
FILE: wordai/models/models.py
================================================
from datetime import datetime, timedelta
import bcrypt
from mongoengine import (BooleanField, DateTimeField, EmbeddedDocumentField,
FloatField, IntField, ListField, ReferenceField,
SortedListField, StringField)
from mongoengine_goodjson import Document, EmbeddedDocument
class DictExample(EmbeddedDocument):
en = StringField(required=True)
cn = StringField(required=True)
class DictDescInfo(EmbeddedDocument):
name = StringField(required=True)
value = StringField(required=True)
class DictDescription(EmbeddedDocument):
description = StringField(required=True)
seq = IntField(required=True)
cn = StringField(required=True)
en = StringField(required=True)
speech = StringField(required=True)
infos = ListField(EmbeddedDocumentField(DictDescInfo))
examples = ListField(EmbeddedDocumentField(DictExample))
class Word(Document):
word = StringField(required=True)
star = IntField(max_value=5)
descriptions = ListField(EmbeddedDocumentField(DictDescription))
meta = {
'indexes': [
('word', '-star'),
]
}
@classmethod
def search_words(cls, *words):
return Word.objects(word__in=words)
@classmethod
def has(cls, *words):
has = []
not_has = []
for word in words:
if Word.objects(word=word).count() > 0:
has.append(word)
else:
not_has.append(word)
return has, not_has
@classmethod
def search_word(cls, word):
return Word.objects(word=word).first()
class Sentence(Document):
eng = StringField(required=True)
chn = StringField(required=True)
score = FloatField()
words = ListField(StringField())
pos_tag = ListField(StringField())
roots = ListField(StringField())
typ = StringField(required=True)
meta = {
'indexes': [
('roots', '-score'),
('typ', 'roots', '-score')
]
}
@classmethod
def has(cls, *words):
has = []
not_has = []
for word in words:
if cls.objects(roots=word).count() > 0:
has.append(word)
else:
not_has.append(word)
return has, not_has
@classmethod
def search_by_root(cls, word):
return cls.objects(roots=word, typ='dictexams').order_by('-score')
def cloze(self, word):
answers = []
cloz = self.eng
for idx, root in enumerate(self.roots):
if root == word:
answers.append(self.words[idx])
cloz = cloz.replace(self.words[idx], '[___]')
return cloz, answers
class User(Document):
username = StringField(required=True, unique=True)
encrypted_password = StringField(required=True)
salt = StringField(required=True, default=lambda: bcrypt.gensalt().decode())
role = StringField(required=True, default='user')
wordlist = ReferenceField('WordList', required=True)
meta = {
'indexes': [
'#username'
]
}
def __init__(self, *args, **kwargs):
passwd = kwargs.get("password")
if passwd:
del kwargs['password']
Document.__init__(self, *args, **kwargs)
if passwd:
self.password = passwd
@classmethod
def find_by_username(cls, username):
return cls.objects(username=username).first()
@classmethod
def check_user(cls, username, passwd):
user = cls.objects(username=username).first()
if user and user.check_password(passwd):
return user
return None
@property
def password(self):
return ""
@password.setter
def password(self, passwd):
passwd = passwd.encode()
salt = bcrypt.gensalt()
self.encrypted_password = bcrypt.hashpw(passwd, salt).decode()
def check_password(self, passwd):
passwd = passwd.encode()
if (not self.encrypted_password) or (not self.salt):
return False
# hashed = bcrypt.hashpw(passwd, self.salt)
return bcrypt.checkpw(passwd, self.encrypted_password.encode())
def wordlist_exercise_log(self):
list_words = self.wordlist.words
return ExerciseLog.objects(user=self, wordname__in=list_words)
def word_exercise_log(self, word):
return ExerciseLog.objects(user=self, wordname=word).first()
def new_words(self):
list_words = self.wordlist.words
learned = [l.wordname for l in self.wordlist_exercise_log().only('wordname')]
return list(set(list_words) - set(learned))
def due_words(self):
return [l.wordname for l in self.wordlist_exercise_log().filter(review__lt=datetime.utcnow()).only('wordname')]
def next_word(self):
words = self.due_words()
if words :
return words[0]
words = self.new_words()
if words :
return words[0]
return None
def next_exercise(self):
word = self.next_word()
if not word:
return None
word_item = Word.search_word(word)
if not word_item:
return None
now = datetime.utcnow()
exlog = self.word_exercise_log(word)
sentence_log = exlog.sentences if exlog else []
log = {l.sentence.id: score_w(now - l.time, l.result) for l in sentence_log}
sentences = Sentence.search_by_root(word).limit(100)
top = None
top_score = 0
for s in sentences:
score = s.score * log.get(s.id, 1)
if score > top_score:
top = s
top_score = score
if not top:
return None
class cloze:
def __init__(self, word, quiz, answers, sentence):
self.word = word
self.cloze = quiz
self.answers = answers
self.sentence = sentence
quiz, answer = top.cloze(word)
return cloze(word_item, quiz, answer, top)
def wordlists(self):
return WordList.objects(user__in=[None, self])
class WordList(Document):
name = StringField(required=True)
description = StringField(required=True)
words = ListField(StringField())
user = ReferenceField(User)
meta = {
'indexes': [
('user', 'name'),
]
}
@classmethod
def check_word(self, *words):
has, not_has = Sentence.has(*words)
whas, wnot_has = Word.has(*words)
has = set(has)
whas = set(whas)
ok = list(has.intersection(whas))
return ok, not_has, wnot_has
def user_learned(self, user):
return ExerciseLog.objects(user=user, wordname__in=list(self.words))
def user_to_learn(self, user):
learned = set([e.wordname for e in ExerciseLog.objects(user=user).only('wordname')])
words = set(self.words)
return list(words - learned)
def score_w(delta, result):
if delta < timedelta(minutes=20):
w = (1-.58)
elif delta < timedelta(hours=1):
w = (1-.44)
elif delta < timedelta(hours=9):
w = (1-.36)
elif delta < timedelta(days=1):
w = (1-.33)
elif delta < timedelta(days=2):
w = (1-.28)
elif delta < timedelta(days=6):
w = (1-.25)
elif delta < timedelta(days=31):
w = (1-.21)
elif delta < timedelta(days=60):
w = (1-.10)
else:
w = .99
return w if result else w * 1.5
class SentenceLog(EmbeddedDocument):
sentence = ReferenceField(Sentence, required=True)
result = BooleanField(required=True)
time = DateTimeField(required=True)
ebbinghaus = {
0: timedelta(minutes=1),
1: timedelta(minutes=5),
2: timedelta(minutes=30),
3: timedelta(hours=12),
4: timedelta(days=1),
5: timedelta(days=2),
6: timedelta(days=4),
7: timedelta(days=7),
8: timedelta(days=15),
}
class ExerciseLog(Document):
user = ReferenceField(User, required=True)
wordname = StringField(required=True)
word = ReferenceField(Word, required=True)
review = DateTimeField(required=True)
sentences = SortedListField(
EmbeddedDocumentField(SentenceLog),
ordering="time", reverse=True)
def calucate_review(self):
count = 0
for s in reversed(self.sentences):
if not s.result:
break
count += 1
delta = ebbinghaus.get(count, timedelta(days=15))
self.review = datetime.utcnow() + delta
@classmethod
def review_count(cls, user, start, end):
data = ExerciseLog.objects(user=user, review__gt=start, review__lt=end).\
aggregate(
{'$group' : {'_id' : {'$dateToString' : {
'date': "$review",
'format': "%Y/%m/%d",
} }, 'count' : { '$sum' : 1 }}},
{'$sort': {'_id': 1}}
)
return {i['_id']:i['count'] for i in list(data)}
@classmethod
def exercise_count(cls, user, start, end):
data = ExerciseLog.objects(user=user).\
aggregate(
{"$match": {"sentences.time": {"$gte": start, "$lte": end}}},
{"$project":{'_id':0, 'sentences.time': 1, 'sentences.result':1}},
{"$unwind":"$sentences"},
{"$match": {"sentences.time": {"$gte": start, "$lte": end}}},
{'$project': {'day': '$sentences.time',
'result': '$sentences.result'}},
{'$group': { '_id': {'$dateToString' : {
'date': "$day",
'format': "%Y/%m/%d",
} }, 'count': { '$sum': 1 } } },
{'$sort': {'_id': 1}}
)
return {i['_id']:i['count'] for i in list(data)}
gitextract_fuu3vrhz/
├── .dockerignore
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── app.py
├── commands/
│ └── __init__.py
├── config/
│ ├── __init__.py
│ └── wsgi.ini
├── data/
│ ├── __init__.py
│ └── dict.py
├── jsondict/
│ ├── anki.py
│ └── search.py
├── manage.py
├── nginx-server.conf
├── nginx.conf
├── requirements.txt
├── start.sh
├── ui/
│ ├── .babelrc
│ ├── .editorconfig
│ ├── .eslintignore
│ ├── .eslintrc.js
│ ├── .gitignore
│ ├── .postcssrc.js
│ ├── README.md
│ ├── build/
│ │ ├── build.js
│ │ ├── check-versions.js
│ │ ├── utils.js
│ │ ├── vue-loader.conf.js
│ │ ├── webpack.base.conf.js
│ │ ├── webpack.dev.conf.js
│ │ └── webpack.prod.conf.js
│ ├── config/
│ │ ├── dev.env.js
│ │ ├── index.js
│ │ └── prod.env.js
│ ├── index.html
│ ├── package.json
│ ├── src/
│ │ ├── App.vue
│ │ ├── components/
│ │ │ ├── Card.vue
│ │ │ ├── Dictionary.vue
│ │ │ ├── Login.vue
│ │ │ ├── Main.vue
│ │ │ ├── statistic/
│ │ │ │ ├── DayChart.vue
│ │ │ │ └── index.vue
│ │ │ └── wordlist/
│ │ │ ├── Form.vue
│ │ │ ├── Learned.vue
│ │ │ ├── List.vue
│ │ │ ├── Setting.vue
│ │ │ ├── ToLearn.vue
│ │ │ └── index.vue
│ │ ├── main.js
│ │ ├── permission.js
│ │ ├── request.js
│ │ ├── router/
│ │ │ └── index.js
│ │ └── store.js
│ └── static/
│ └── .gitkeep
└── wordai/
├── __init__.py
├── api/
│ ├── __init__.py
│ └── apis.py
└── models/
├── __init__.py
└── models.py
SYMBOL INDEX (115 symbols across 17 files)
FILE: app.py
function run_dev (line 6) | def run_dev():
FILE: commands/__init__.py
function dict_to_mongo (line 6) | def dict_to_mongo():
function sentence_to_mongo (line 14) | def sentence_to_mongo(*typ):
function _sentence_to_mongo (line 20) | def _sentence_to_mongo(typ, items):
function new_user (line 63) | def new_user(username, passwd, role='user', *args):
function dict_star_word_list (line 67) | def dict_star_word_list():
function dict_parse (line 73) | def dict_parse():
function run (line 84) | def run(method, *args):
FILE: config/__init__.py
function c (line 3) | def c(name, default=None):
function mongo_config (line 6) | def mongo_config():
function api_env (line 15) | def api_env():
function flask_config (line 22) | def flask_config():
FILE: data/__init__.py
function data_file (line 10) | def data_file(filename):
function load_dict (line 13) | def load_dict():
function load_sentence (line 18) | def load_sentence(*typ):
FILE: data/dict.py
class Description (line 5) | class Description(object):
method __init__ (line 6) | def __init__(self, des):
method read_seq (line 29) | def read_seq(self):
method read_word_speech (line 44) | def read_word_speech(self):
method read_cn_and_en (line 61) | def read_cn_and_en(self):
method read_info (line 82) | def read_info(self):
FILE: jsondict/anki.py
function cloze_word (line 7) | def cloze_word(sentense, word, level="1"):
FILE: jsondict/search.py
function search (line 11) | def search(word):
function format (line 17) | def format(item):
function description (line 26) | def description(word):
function search_string (line 32) | def search_string(word):
FILE: manage.py
function command (line 11) | def command(method, *args):
function usage (line 20) | def usage():
FILE: ui/build/check-versions.js
function exec (line 7) | function exec (cmd) {
FILE: ui/build/utils.js
function generateLoaders (line 33) | function generateLoaders (loader, loaderOptions) {
FILE: ui/build/webpack.base.conf.js
function resolve (line 7) | function resolve (dir) {
FILE: ui/build/webpack.dev.conf.js
constant HOST (line 13) | const HOST = process.env.HOST
constant PORT (line 14) | const PORT = process.env.PORT && Number(process.env.PORT)
FILE: ui/build/webpack.prod.conf.js
method minChunks (line 84) | minChunks (module) {
FILE: ui/src/store.js
function getToken (line 12) | function getToken () {
function getRefreshToken (line 15) | function getRefreshToken () {
function setToken (line 19) | function setToken (token) {
function setRefreshTokens (line 22) | function setRefreshTokens (token) {
function removeToken (line 26) | function removeToken () {
method setTokens (line 43) | setTokens (state, access) {
method setRefreshTokens (line 47) | setRefreshTokens (state, refresh) {
method setWordList (line 51) | setWordList (state, wordlist) {
method fetchJWT (line 56) | fetchJWT ({ commit }, { username, password }) {
method refreshJWT (line 74) | refreshJWT ({ commit, state}) {
method getUserInfo (line 88) | getUserInfo ({commit, state}) {
method logOut (line 100) | logOut ({commit, state}) {
FILE: wordai/api/__init__.py
function index (line 15) | def index():
FILE: wordai/api/apis.py
class api_register (line 20) | class api_register(object):
method __init__ (line 21) | def __init__(self, path):
method __call__ (line 24) | def __call__(self, cls):
function admin_required (line 28) | def admin_required(f):
function user_required (line 39) | def user_required(f):
class UserRegistration (line 55) | class UserRegistration(Resource):
method post (line 56) | def post(self):
class UserLogin (line 61) | class UserLogin(Resource):
method post (line 62) | def post(self):
class TokenRefresh (line 81) | class TokenRefresh(Resource):
method post (line 83) | def post(self):
class WordListList (line 94) | class WordListList(Resource):
method get (line 96) | def get(self, user):
method put (line 100) | def put(self, user):
method post (line 122) | def post(self, user):
class WordListItem (line 156) | class WordListItem(Resource):
method get (line 158) | def get(self, user, lid):
method put (line 163) | def put(self, user, lid):
method delete (line 203) | def delete(self, user, lid):
class UserWordList (line 212) | class UserWordList(Resource):
method get (line 214) | def get(self, user):
method post (line 226) | def post(self, user):
class LearnNext (line 240) | class LearnNext(Resource):
method get (line 242) | def get(self, user):
method post (line 262) | def post(self, user):
class Dictionary (line 294) | class Dictionary(Resource):
method get (line 296) | def get(self, user, word):
class WordlistLearned (line 305) | class WordlistLearned(Resource):
method get (line 307) | def get(self, user):
class WordlistToLearn (line 313) | class WordlistToLearn(Resource):
method get (line 315) | def get(self, user):
class StatisticLearn (line 320) | class StatisticLearn(Resource):
method get (line 322) | def get(self, user):
FILE: wordai/models/models.py
class DictExample (line 11) | class DictExample(EmbeddedDocument):
class DictDescInfo (line 16) | class DictDescInfo(EmbeddedDocument):
class DictDescription (line 20) | class DictDescription(EmbeddedDocument):
class Word (line 30) | class Word(Document):
method search_words (line 40) | def search_words(cls, *words):
method has (line 44) | def has(cls, *words):
method search_word (line 56) | def search_word(cls, word):
class Sentence (line 59) | class Sentence(Document):
method has (line 75) | def has(cls, *words):
method search_by_root (line 86) | def search_by_root(cls, word):
method cloze (line 89) | def cloze(self, word):
class User (line 99) | class User(Document):
method __init__ (line 110) | def __init__(self, *args, **kwargs):
method find_by_username (line 119) | def find_by_username(cls, username):
method check_user (line 123) | def check_user(cls, username, passwd):
method password (line 130) | def password(self):
method password (line 134) | def password(self, passwd):
method check_password (line 139) | def check_password(self, passwd):
method wordlist_exercise_log (line 146) | def wordlist_exercise_log(self):
method word_exercise_log (line 150) | def word_exercise_log(self, word):
method new_words (line 153) | def new_words(self):
method due_words (line 158) | def due_words(self):
method next_word (line 161) | def next_word(self):
method next_exercise (line 170) | def next_exercise(self):
method wordlists (line 199) | def wordlists(self):
class WordList (line 203) | class WordList(Document):
method check_word (line 215) | def check_word(self, *words):
method user_learned (line 224) | def user_learned(self, user):
method user_to_learn (line 227) | def user_to_learn(self, user):
function score_w (line 233) | def score_w(delta, result):
class SentenceLog (line 254) | class SentenceLog(EmbeddedDocument):
class ExerciseLog (line 271) | class ExerciseLog(Document):
method calucate_review (line 280) | def calucate_review(self):
method review_count (line 290) | def review_count(cls, user, start, end):
method exercise_count (line 302) | def exercise_count(cls, user, start, end):
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (104K chars).
[
{
"path": ".dockerignore",
"chars": 15,
"preview": "ui/node_modules"
},
{
"path": ".gitignore",
"chars": 2460,
"preview": "### https://raw.github.com/github/gitignore/f9291de89f5f7dc0d3d87f9eb111b839f81d5dbc/Global/Emacs.gitignore\n\n# -*- mode:"
},
{
"path": "Dockerfile",
"chars": 591,
"preview": "FROM python:3.7\n\nRUN curl -sL https://deb.nodesource.com/setup_12.x | bash -\n\nRUN apt-get update && apt-get install -y\\\n"
},
{
"path": "LICENSE",
"chars": 1068,
"preview": "MIT License\n\nCopyright (c) 2019 Senghoo Kim\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
},
{
"path": "README.md",
"chars": 1082,
"preview": "# Word AI\n\n一款使用自然语言处理辅助背单词的程序。\n\n通过导入TMX翻译语料库,进行分词、词素分析、单词还原等操作自动生成各种不同语态、情态下的英语填空来进行单词学习。\n\n*PS: 因为版权原因,不提供词典文件和TMX语料文件。需"
},
{
"path": "app.py",
"chars": 144,
"preview": "from wordai.api import app\nfrom config import api_env, flask_config\n\napp.config.update(flask_config())\n\ndef run_dev():\n "
},
{
"path": "commands/__init__.py",
"chars": 3173,
"preview": "import string\nfrom data import load_dict, load_sentence\nfrom data.dict import Description\nfrom wordai.models import Word"
},
{
"path": "config/__init__.py",
"chars": 619,
"preview": "import os\n\ndef c(name, default=None):\n return os.environ.get(name.upper(), default)\n\ndef mongo_config():\n return {"
},
{
"path": "config/wsgi.ini",
"chars": 144,
"preview": "[uwsgi]\nmodule = app:app\nmaster = true\nprocesses = 3\n\nsocket = /var/run/wordai.sock\nlogto = /var/log/wordai.log\nchmod-so"
},
{
"path": "data/__init__.py",
"chars": 1044,
"preview": "# -*- coding: utf-8 -*-\n\nimport os\nimport json\nfrom itertools import chain\nfrom translate.storage.tmx import tmxfile\n\nDA"
},
{
"path": "data/dict.py",
"chars": 2521,
"preview": "# -*- coding: utf-8 -*-\nimport string\nimport re\n\nclass Description(object):\n def __init__(self, des):\n self.de"
},
{
"path": "jsondict/anki.py",
"chars": 891,
"preview": "import sys\nimport csv\nfrom search import search_string, description\n\nres = []\n\ndef cloze_word(sentense, word, level=\"1\")"
},
{
"path": "jsondict/search.py",
"chars": 892,
"preview": "#!/usr/bin/python\n# -*- coding: utf-8 -*-\n\nimport sys\nimport json\n\n\nwith open('dict.json', 'r') as f:\n dictionary = j"
},
{
"path": "manage.py",
"chars": 517,
"preview": "import os\nimport sys\n\nROOT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)))\nsys.path.insert(0, ROOT_DIR)\n\n"
},
{
"path": "nginx-server.conf",
"chars": 213,
"preview": "server {\n listen 80;\n server_name 0.0.0.0;\n\n location /api/ {\n include uwsgi_params;\n uwsgi_pass "
},
{
"path": "nginx.conf",
"chars": 480,
"preview": "user root;\nworker_processes auto;\npid /run/nginx.pid;\ninclude /etc/nginx/modules-enabled/*.conf;\n\nevents {\n\tworker_conne"
},
{
"path": "requirements.txt",
"chars": 129,
"preview": "mongoengine\nmongoengine-goodjson\nlxml\ntranslate-toolkit\nnltk\nflask\nflask-restful\nflask-jwt-extended\nflask-cors\nbcrypt\njs"
},
{
"path": "start.sh",
"chars": 63,
"preview": "#!/bin/bash\n\nservice nginx start\nuwsgi --ini config/wsgi.ini \n\n"
},
{
"path": "ui/.babelrc",
"chars": 230,
"preview": "{\n \"presets\": [\n [\"env\", {\n \"modules\": false,\n \"targets\": {\n \"browsers\": [\"> 1%\", \"last 2 versions\""
},
{
"path": "ui/.editorconfig",
"chars": 147,
"preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
},
{
"path": "ui/.eslintignore",
"chars": 30,
"preview": "/build/\n/config/\n/dist/\n/*.js\n"
},
{
"path": "ui/.eslintrc.js",
"chars": 791,
"preview": "// https://eslint.org/docs/user-guide/configuring\n\nmodule.exports = {\n root: true,\n parserOptions: {\n parser: 'babe"
},
{
"path": "ui/.gitignore",
"chars": 154,
"preview": ".DS_Store\nnode_modules/\n/dist/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n.vsc"
},
{
"path": "ui/.postcssrc.js",
"chars": 246,
"preview": "// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n \"plugins\": {\n \"postcss-import\": {},"
},
{
"path": "ui/README.md",
"chars": 454,
"preview": "# wordai\n\n> Word AI\n\n## Build Setup\n\n``` bash\n# install dependencies\nnpm install\n\n# serve with hot reload at localhost:8"
},
{
"path": "ui/build/build.js",
"chars": 1198,
"preview": "'use strict'\nrequire('./check-versions')()\n\nprocess.env.NODE_ENV = 'production'\n\nconst ora = require('ora')\nconst rm = r"
},
{
"path": "ui/build/check-versions.js",
"chars": 1290,
"preview": "'use strict'\nconst chalk = require('chalk')\nconst semver = require('semver')\nconst packageConfig = require('../package.j"
},
{
"path": "ui/build/utils.js",
"chars": 2587,
"preview": "'use strict'\nconst path = require('path')\nconst config = require('../config')\nconst ExtractTextPlugin = require('extract"
},
{
"path": "ui/build/vue-loader.conf.js",
"chars": 553,
"preview": "'use strict'\nconst utils = require('./utils')\nconst config = require('../config')\nconst isProduction = process.env.NODE_"
},
{
"path": "ui/build/webpack.base.conf.js",
"chars": 2464,
"preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst config = require('../config')\nconst vue"
},
{
"path": "ui/build/webpack.dev.conf.js",
"chars": 3004,
"preview": "'use strict'\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst config = require('../config')\ncon"
},
{
"path": "ui/build/webpack.prod.conf.js",
"chars": 5055,
"preview": "'use strict'\nconst path = require('path')\nconst utils = require('./utils')\nconst webpack = require('webpack')\nconst conf"
},
{
"path": "ui/config/dev.env.js",
"chars": 200,
"preview": "'use strict'\nconst merge = require('webpack-merge')\nconst prodEnv = require('./prod.env')\n\nmodule.exports = merge(prodEn"
},
{
"path": "ui/config/index.js",
"chars": 2291,
"preview": "'use strict'\n// Template version: 1.3.1\n// see http://vuejs-templates.github.io/webpack for documentation.\n\nconst path ="
},
{
"path": "ui/config/prod.env.js",
"chars": 84,
"preview": "'use strict'\nmodule.exports = {\n NODE_ENV: '\"production\"',\n BASE_API: '\"/api\"',\n}\n"
},
{
"path": "ui/index.html",
"chars": 373,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-width, initia"
},
{
"path": "ui/package.json",
"chars": 2548,
"preview": "{\n \"name\": \"wordai\",\n \"version\": \"1.0.0\",\n \"description\": \"Word AI\",\n \"author\": \"Senghoo Kim <shkdmb@gmail.com>\",\n "
},
{
"path": "ui/src/App.vue",
"chars": 319,
"preview": "<template>\n <div id=\"app\">\n <router-view/>\n </div>\n</template>\n\n<script>\nexport default {\n name: 'App'\n}\n</script>"
},
{
"path": "ui/src/components/Card.vue",
"chars": 3159,
"preview": "<template>\n <div class=\"card\" v-if=\"!alldone\">\n <div class=\"cn\" >\n <span>{{quiz.cn}}</span>\n </div>\n <div"
},
{
"path": "ui/src/components/Dictionary.vue",
"chars": 1804,
"preview": "<template>\n<div class=\"main\">\n <div class=\"dict-head\">\n <span>{{word}}</span>\n </div>\n <el-collapse v-model=\"activ"
},
{
"path": "ui/src/components/Login.vue",
"chars": 1437,
"preview": "<template>\n<div class=\"login\">\n<el-row>\n <el-col :span=\"24\">\n <el-input id=\"name\" v-model=\"name\" placeholder=\"请输入帐号"
},
{
"path": "ui/src/components/Main.vue",
"chars": 5191,
"preview": "<template>\n<div class=\"main\">\n <div class=\"content\">\n <el-menu class=\"right-menu\" :collapse=\"true\" active-text-color="
},
{
"path": "ui/src/components/statistic/DayChart.vue",
"chars": 2525,
"preview": "<template>\n <div>\n <div id=\"day_chart_echart\" style=\"height: 400px; width: 100%;\"></div>\n </div>\n</template>\n<scrip"
},
{
"path": "ui/src/components/statistic/index.vue",
"chars": 391,
"preview": "<template>\n<div>\n <el-row>\n <el-col :span=\"24\">\n <day-chart ref=\"dayChart\"></day-chart>\n </el-col>\n </el-ro"
},
{
"path": "ui/src/components/wordlist/Form.vue",
"chars": 5824,
"preview": "<template>\n<el-form :model=\"form\" label-width=\"80px\">\n <el-form-item label=\"名称\">\n <el-input v-model=\"form.name\"></el"
},
{
"path": "ui/src/components/wordlist/Learned.vue",
"chars": 1155,
"preview": "<template>\n<el-timeline>\n <el-timeline-item\n v-for=\"(activity, index) in learned\"\n :key=\"activity.wordname\""
},
{
"path": "ui/src/components/wordlist/List.vue",
"chars": 2679,
"preview": "<template>\n <div>\n<div class=\"wordlist\" v-if=\"show=='list'\">\n <el-button type=\"primary\" icon=\"el-icon-edit\" size=\"mini"
},
{
"path": "ui/src/components/wordlist/Setting.vue",
"chars": 1082,
"preview": "<template>\n<el-form :model=\"form\">\n <el-form-item label=\"单词本\" :label-width=\"'70px'\">\n <el-select v-model=\"form.wordl"
},
{
"path": "ui/src/components/wordlist/ToLearn.vue",
"chars": 627,
"preview": "<template>\n <div class=\"list-container\">\n <ul class=\"infinite-list\" v-infinite-scroll=\"load\" style=\"overflow:auto\">\n"
},
{
"path": "ui/src/components/wordlist/index.vue",
"chars": 868,
"preview": "<template>\n <el-tabs v-model=\"activeName\" class=\"tabs\">\n <el-tab-pane label=\"已学单词\" name=\"learned\">\n <learned r"
},
{
"path": "ui/src/main.js",
"chars": 846,
"preview": "// The Vue build version to load with the `import` command\n// (runtime-only or standalone) has been set in webpack.base."
},
{
"path": "ui/src/permission.js",
"chars": 674,
"preview": "import router from './router'\nimport store from './store'\nimport {getToken} from './store'\n\nimport { Message } from 'ele"
},
{
"path": "ui/src/request.js",
"chars": 1426,
"preview": "import axios from 'axios'\nimport {MessageBox} from 'element-ui'\nimport store from './store'\nimport { getToken } from './"
},
{
"path": "ui/src/router/index.js",
"chars": 333,
"preview": "import Vue from 'vue'\nimport Router from 'vue-router'\n\nVue.use(Router)\n\nexport default new Router({\n routes: [\n {\n "
},
{
"path": "ui/src/store.js",
"chars": 2987,
"preview": "import Vue from 'vue'\nimport vuex from 'vuex'\nimport service from './request'\n//import Cookies from 'js-cookie'\n\n// Vue."
},
{
"path": "ui/static/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "wordai/__init__.py",
"chars": 0,
"preview": ""
},
{
"path": "wordai/api/__init__.py",
"chars": 369,
"preview": "from flask import Flask, jsonify\nfrom flask_cors import CORS\n\nfrom flask_jwt_extended import (JWTManager)\n\napp = Flask(_"
},
{
"path": "wordai/api/apis.py",
"chars": 11089,
"preview": "import hashlib\nimport json\nfrom datetime import datetime, timedelta\n\nimport wordai.models as models\nfrom flask import Bl"
},
{
"path": "wordai/models/__init__.py",
"chars": 115,
"preview": "from mongoengine import connect\n\nfrom config import mongo_config\n\nconnect(**mongo_config())\n\nfrom .models import *\n"
},
{
"path": "wordai/models/models.py",
"chars": 9862,
"preview": "\nfrom datetime import datetime, timedelta\n\nimport bcrypt\nfrom mongoengine import (BooleanField, DateTimeField, EmbeddedD"
}
]
About this extraction
This page contains the full source code of the senghoo/wordai GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (92.3 KB), approximately 26.0k tokens, and a symbol index with 115 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.