init
|
@ -0,0 +1,7 @@
|
||||||
|
*/__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.po
|
||||||
|
*.mo
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
test
|
|
@ -0,0 +1,8 @@
|
||||||
|
# scp docker compose
|
||||||
|
PROJECT_NAME = osdict
|
||||||
|
|
||||||
|
REMOTE_USER = fyq@www.osdict.cn
|
||||||
|
REMOTE_PATH = ~/docker/${PROJECT_NAME}
|
||||||
|
|
||||||
|
scp:
|
||||||
|
scp docker-compose.yaml ${REMOTE_USER}:${REMOTE_PATH}/docker-compose.yaml
|
|
@ -0,0 +1,31 @@
|
||||||
|
version: '3.9'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
osdict_net:
|
||||||
|
name: osdict_net
|
||||||
|
attachable: true
|
||||||
|
ipam:
|
||||||
|
driver: default
|
||||||
|
config:
|
||||||
|
- subnet: 192.168.88.0/24
|
||||||
|
|
||||||
|
services:
|
||||||
|
front:
|
||||||
|
container_name: osdict_front
|
||||||
|
image: registry.cn-beijing.aliyuncs.com/cypress-boat/osdict_front:latest
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:12000:80"
|
||||||
|
networks:
|
||||||
|
osdict_net:
|
||||||
|
ipv4_address: 192.168.88.200
|
||||||
|
|
||||||
|
osdict:
|
||||||
|
container_name: osdict_backend
|
||||||
|
image: registry.cn-beijing.aliyuncs.com/cypress-boat/osdict:latest
|
||||||
|
restart: always
|
||||||
|
expose:
|
||||||
|
- "8000"
|
||||||
|
networks:
|
||||||
|
osdict_net:
|
||||||
|
ipv4_address: 192.168.88.100
|
|
@ -0,0 +1,6 @@
|
||||||
|
FROM nginx:stable
|
||||||
|
|
||||||
|
COPY ./*.conf /etc/nginx/conf.d
|
||||||
|
COPY . /var/local/web/
|
||||||
|
|
||||||
|
CMD [ "nginx", "-g", "daemon off;" ]
|
|
@ -0,0 +1,17 @@
|
||||||
|
WORKDIR = $(shell pwd)
|
||||||
|
PROJECT_NAME = osdict_front
|
||||||
|
DOCKER_HUB_ADDR = registry.cn-beijing.aliyuncs.com/cypress-boat
|
||||||
|
IMAGE_REPOSITORY = ${DOCKER_HUB_ADDR}/${PROJECT_NAME}
|
||||||
|
TAG ?= latest
|
||||||
|
IMAGE_REPOSITORY_TAG = ${IMAGE_REPOSITORY}:${TAG}
|
||||||
|
|
||||||
|
.PHONY: image publish
|
||||||
|
|
||||||
|
image:
|
||||||
|
docker build --platform=amd64 -t ${IMAGE_REPOSITORY_TAG} .
|
||||||
|
|
||||||
|
publish:
|
||||||
|
@echo "push ${IMAGE_REPOSITORY_TAG}"
|
||||||
|
docker push ${IMAGE_REPOSITORY_TAG}
|
||||||
|
|
||||||
|
all: image publish
|
|
@ -0,0 +1 @@
|
||||||
|
.localhistory-container{border:1px solid #ccc;border-radius:4px;background:#fff}
|
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 45 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>osdictvue</title><style>html,body{height: 100%;width: 100%;margin: 0px;overflow-y: auto;}
|
||||||
|
.botton-link{display:inline-block;text-decoration:none;height:20px;line-height:20px;}
|
||||||
|
.botton-text{float:left;height:20px;line-height:20px;margin: 0px 0px 0px 5px; color:#939393;}</style><link href="/css/app.26a77fe3.css" rel="preload" as="style"><link href="/css/chunk-vendors.815ddbf8.css" rel="preload" as="style"><link href="/js/app.172fd3fb.js" rel="preload" as="script"><link href="/js/chunk-vendors.c967b810.js" rel="preload" as="script"><link href="/css/chunk-vendors.815ddbf8.css" rel="stylesheet"><link href="/css/app.26a77fe3.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but osdictvue2 doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><div style="width:300px;margin:0 auto; padding:20px 0;"><a href="https://beian.miit.gov.cn/" target="_blank" class="botton-link"><span class="botton-text">渝ICP备2021011178号</span></a> <a target="_blank" href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11010802037748" class="botton-link"><img src="http://www.beian.gov.cn/img/new/gongan.png" style="float:left;"><p class="botton-text">京公网安备 11010802037748号</p></a></div><script src="/js/chunk-vendors.c967b810.js"></script><script src="/js/app.172fd3fb.js"></script></body></html>
|
|
@ -0,0 +1,17 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
#你的域名
|
||||||
|
server_name 127.0.0.1;
|
||||||
|
|
||||||
|
location /api/ {
|
||||||
|
rewrite /api/(.+)$ /$1 break;
|
||||||
|
proxy_pass http://192.168.88.100:8000;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
root /var/local/web/osdict-front/dist;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
/build/
|
||||||
|
/config/
|
||||||
|
/dist/
|
||||||
|
/*.js
|
|
@ -0,0 +1,29 @@
|
||||||
|
// 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'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
|
@ -0,0 +1,24 @@
|
||||||
|
# osdictvue2
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
```
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
```
|
||||||
|
npm run serve
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
```
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/).
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
'@vue/cli-plugin-babel/preset'
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
'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'
|
||||||
|
))
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,54 @@
|
||||||
|
'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)
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 6.7 KiB |
|
@ -0,0 +1,101 @@
|
||||||
|
'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')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
'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'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
'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: /\.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'
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
'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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,145 @@
|
||||||
|
'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
|
|
@ -0,0 +1,46 @@
|
||||||
|
{
|
||||||
|
"name": "osdictvue2",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"serve": "vue-cli-service serve",
|
||||||
|
"build": "vue-cli-service build",
|
||||||
|
"lint": "vue-cli-service lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.21.4",
|
||||||
|
"bootstrap": "^4.6.0",
|
||||||
|
"bootstrap-icons": "^1.7.0",
|
||||||
|
"core-js": "^3.6.5",
|
||||||
|
"vue": "^2.6.11",
|
||||||
|
"vue-router": "^3.5.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/cli-plugin-babel": "~4.5.0",
|
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0",
|
||||||
|
"@vue/cli-service": "~4.5.0",
|
||||||
|
"babel-eslint": "^10.1.0",
|
||||||
|
"eslint": "^6.7.2",
|
||||||
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"vue-template-compiler": "^2.6.11"
|
||||||
|
},
|
||||||
|
"eslintConfig": {
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"plugin:vue/essential",
|
||||||
|
"eslint:recommended"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"parser": "babel-eslint"
|
||||||
|
},
|
||||||
|
"rules": {}
|
||||||
|
},
|
||||||
|
"browserslist": [
|
||||||
|
"> 1%",
|
||||||
|
"last 2 versions",
|
||||||
|
"not dead"
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 4.2 KiB |
|
@ -0,0 +1,26 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
<title>osdictvue</title>
|
||||||
|
<style>
|
||||||
|
html,body{height: 100%;width: 100%;margin: 0px;overflow-y: auto;}
|
||||||
|
.botton-link{display:inline-block;text-decoration:none;height:20px;line-height:20px;}
|
||||||
|
.botton-text{float:left;height:20px;line-height:20px;margin: 0px 0px 0px 5px; color:#939393;}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
<!-- built files will be auto injected -->
|
||||||
|
<div style="width:300px;margin:0 auto; padding:20px 0;">
|
||||||
|
<a href="https://beian.miit.gov.cn/" target="_blank" class="botton-link"><span class="botton-text">渝ICP备2021011178号</span></a>
|
||||||
|
<a target="_blank" href="http://www.beian.gov.cn/portal/registerSystemInfo?recordcode=11010802037748" class="botton-link"><img src="http://www.beian.gov.cn/img/new/gongan.png" style="float:left;"/><p class="botton-text">京公网安备 11010802037748号</p></a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<background>
|
||||||
|
<router-view />
|
||||||
|
</background>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Background from './components/Background.vue'
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
Background
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
// const apiroot = 'http://192.168.0.100/'
|
||||||
|
// const apiroot = 'http://localhost:8000/'
|
||||||
|
const apiroot = 'http://182.92.72.205/api/'
|
||||||
|
|
||||||
|
const api = {
|
||||||
|
registration: apiroot + 'user/registration',
|
||||||
|
registrationVerifyEmail: apiroot + 'user/registration/verify-email',
|
||||||
|
passwordReset: apiroot + 'user/password-reset',
|
||||||
|
passwordResetVerifyEmail: apiroot + 'user/password-reset/verify-email',
|
||||||
|
wordSearch: apiroot + 'word/search',
|
||||||
|
userCheck: apiroot + 'user/check',
|
||||||
|
login: apiroot + 'user/login',
|
||||||
|
word: apiroot + 'word/',
|
||||||
|
meaningList: apiroot + 'meaning/list/', // meaningfield_id
|
||||||
|
meaningCreate: apiroot + 'meaning/create/', // word_id
|
||||||
|
meaningAppend: apiroot + 'meaning/append/', // meaningfield_id
|
||||||
|
manageWords: apiroot + 'management/words',
|
||||||
|
manageMeanings: apiroot + 'management/meanings',
|
||||||
|
manageStaffs: apiroot + 'management/staffs',
|
||||||
|
manageDeleteRefuse: apiroot + 'management/delete-refuse',
|
||||||
|
// get: obtain user's collected word; post: to collect word
|
||||||
|
// fields: word_id collected (get: add_time)
|
||||||
|
collectedWord: apiroot + 'user/collected-word',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api
|
After Width: | Height: | Size: 45 KiB |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M3.612 15.443c-.386.198-.824-.149-.746-.592l.83-4.73L.173 6.765c-.329-.314-.158-.888.283-.95l4.898-.696L7.538.792c.197-.39.73-.39.927 0l2.184 4.327 4.898.696c.441.062.612.636.282.95l-3.522 3.356.83 4.73c.078.443-.36.79-.746.592L8 13.187l-4.389 2.256z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 399 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-star" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.866 14.85c-.078.444.36.791.746.593l4.39-2.256 4.389 2.256c.386.198.824-.149.746-.592l-.83-4.73 3.522-3.356c.33-.314.16-.888-.282-.95l-4.898-.696L8.465.792a.513.513 0 0 0-.927 0L5.354 5.12l-4.898.696c-.441.062-.612.636-.283.95l3.523 3.356-.83 4.73zm4.905-2.767-3.686 1.894.694-3.957a.565.565 0 0 0-.163-.505L1.71 6.745l4.052-.576a.525.525 0 0 0 .393-.288L8 2.223l1.847 3.658a.525.525 0 0 0 .393.288l4.052.575-2.906 2.77a.565.565 0 0 0-.163.506l.694 3.957-3.686-1.894a.503.503 0 0 0-.461 0z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 635 B |
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash-fill" viewBox="0 0 16 16">
|
||||||
|
<path d="M2.5 1a1 1 0 0 0-1 1v1a1 1 0 0 0 1 1H3v9a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V4h.5a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H10a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1H2.5zm3 4a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 .5-.5zM8 5a.5.5 0 0 1 .5.5v7a.5.5 0 0 1-1 0v-7A.5.5 0 0 1 8 5zm3 .5v7a.5.5 0 0 1-1 0v-7a.5.5 0 0 1 1 0z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 448 B |
|
@ -0,0 +1,4 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16">
|
||||||
|
<path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/>
|
||||||
|
<path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 573 B |
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
background: 'url(' + bgpic + ') repeat',
|
||||||
|
//'background-size': 'cover',
|
||||||
|
height: '100%',
|
||||||
|
overflow: 'auto'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<osdict-header v-if="headerIsActive"></osdict-header>
|
||||||
|
<div class="container mt-3">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import OsdictHeader from './OsdictHeader.vue'
|
||||||
|
import bgpic from '../assets/g1.jpg'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
OsdictHeader
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
bgpic: bgpic,
|
||||||
|
headerIsActive: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route.name': function (newName, oldName) {
|
||||||
|
document.querySelector('title').textContent = this.$route.name + " osdict"
|
||||||
|
if (oldName === 'Login') {
|
||||||
|
this.headerIsActive = false
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.headerIsActive = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created:function(){
|
||||||
|
document.querySelector('title').textContent = this.$route.name + " osdict"
|
||||||
|
//document.querySelector('body').setAttribute("style","background:#555555")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div class="hello">
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
<p>
|
||||||
|
For a guide and recipes on how to configure / customize this project,<br>
|
||||||
|
check out the
|
||||||
|
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
||||||
|
</p>
|
||||||
|
<h3>Installed CLI Plugins</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
||||||
|
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
|
||||||
|
</ul>
|
||||||
|
<h3>Essential Links</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
||||||
|
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
||||||
|
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
||||||
|
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
||||||
|
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
||||||
|
</ul>
|
||||||
|
<h3>Ecosystem</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
||||||
|
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
||||||
|
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
||||||
|
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
||||||
|
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'HelloWorld',
|
||||||
|
props: {
|
||||||
|
msg: String
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||||
|
<style scoped>
|
||||||
|
h3 {
|
||||||
|
margin: 40px 0 0;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #42b983;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<header class="navbar navbar-expand-md navbar-dark bg-dark">
|
||||||
|
<router-link class="navbar-brand" one-link-mark="yes" :to="{ name: 'Main' }"
|
||||||
|
>OSDICT</router-link
|
||||||
|
>
|
||||||
|
<button class="navbar-toggler collapsed" type="button" v-on:click="classbind">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" :class="{show:expanded}" id="navbarCollapse">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<router-link class="nav-link" one-link-mark="yes" :to="{ name: 'WordAdd' }">添加新单词</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="navbar-nav ml-auto">
|
||||||
|
<template v-if="user.is_authenticated">
|
||||||
|
<li class="nav-item">
|
||||||
|
<router-link class="nav-link" one-link-mark="yes" :to="{ name: 'ManagementMeanings' }">ManagementMeanings</router-link>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<router-link class="nav-link" one-link-mark="yes" :to="{ name: 'ManagementWords' }">ManagementWords</router-link>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<router-link class="nav-link" one-link-mark="yes" :to="{ name: 'CollectedWords'}">{{ user.nickname }}</router-link>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" @click="logout">
|
||||||
|
<a class="nav-link" href="#" one-link-mark="yes">登出</a>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
<li class="nav-item" v-else>
|
||||||
|
<router-link :to="{ name: 'Login' }" class="nav-link" one-link-mark="yes">登录</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/*
|
||||||
|
* Author: 范袁侨
|
||||||
|
*/
|
||||||
|
import OsdictLocalStorage from '@/utils/OsdictLocalStorage'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
user: {
|
||||||
|
is_authenticated: false,
|
||||||
|
username: null
|
||||||
|
},
|
||||||
|
expanded:false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getData: function () {
|
||||||
|
const url = Api.userCheck
|
||||||
|
const this_ = this
|
||||||
|
const token = OsdictLocalStorage.getJWTlocalStorage()
|
||||||
|
if (token != null) {
|
||||||
|
axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({})
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
const data = response.data
|
||||||
|
this_.user = data.user
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
this_.user = {
|
||||||
|
is_authenticated: false,
|
||||||
|
username: null
|
||||||
|
}
|
||||||
|
OsdictLocalStorage.removeJWTlocalStorage()
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.user = {
|
||||||
|
is_authenticated: false,
|
||||||
|
username: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
logout: function () {
|
||||||
|
OsdictLocalStorage.removeJWTlocalStorage()
|
||||||
|
this.user.is_authenticated = false
|
||||||
|
this.$router.push({ name: 'Main' })
|
||||||
|
},
|
||||||
|
classbind:function(){
|
||||||
|
this.expanded = !this.expanded
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.getData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,15 @@
|
||||||
|
export default [
|
||||||
|
{ value: 'n.', property: '名词' },
|
||||||
|
{ value: 'adj.', property: '形容词' },
|
||||||
|
{ value: 'v.', property: '动词' },
|
||||||
|
{ value: 'adv.', property: '副词' },
|
||||||
|
{ value: 'num.', property: '数词' },
|
||||||
|
{ value: 'pron.', property: '代词' },
|
||||||
|
{ value: 'art.', property: '冠词' },
|
||||||
|
{ value: 'prep.', property: '节词' },
|
||||||
|
{ value: 'conj.', property: '连词' },
|
||||||
|
{ value: 'interj.', property: '感叹词' },
|
||||||
|
{ value: 'abbr.', property: '缩写词' },
|
||||||
|
{ value: 'comb.', property: '合成词' },
|
||||||
|
{ value: 'suff.', property: '后缀' }
|
||||||
|
]
|
|
@ -0,0 +1,31 @@
|
||||||
|
var local="localuser"
|
||||||
|
function read(key){
|
||||||
|
try{
|
||||||
|
const value=JSON.parse(localStorage.getItem(key))
|
||||||
|
if(value==null){
|
||||||
|
return []
|
||||||
|
}else{
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}catch{
|
||||||
|
remove()
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function readlocalhistory(){
|
||||||
|
var readhistory=read(local)
|
||||||
|
if(readhistory==null){
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return readhistory
|
||||||
|
}
|
||||||
|
function addlocalhistory(historyitem){
|
||||||
|
var historyarray=readlocalhistory()
|
||||||
|
historyarray.push(historyitem)
|
||||||
|
historyarray = JSON.stringify(historyarray)
|
||||||
|
return localStorage.setItem(local,historyarray)
|
||||||
|
}
|
||||||
|
function remove(){
|
||||||
|
return localStorage.removeItem(local);
|
||||||
|
}
|
||||||
|
export {read,readlocalhistory,addlocalhistory,remove}
|
|
@ -0,0 +1,66 @@
|
||||||
|
<template>
|
||||||
|
<div class="localhistory-container m-3" :style="{ width: length + 'px' }">
|
||||||
|
<table class="table table-borderless ">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>历史记录</th>
|
||||||
|
<th>
|
||||||
|
<a href="#">
|
||||||
|
<i
|
||||||
|
class="bi-trash"
|
||||||
|
style="color:black;font-size:1.2em;float:right"
|
||||||
|
@click="remove_history"
|
||||||
|
></i>
|
||||||
|
</a>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="his in history_display" :key="his.spelling">
|
||||||
|
<td>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'word', query: { word_id: his.word_id } }"
|
||||||
|
>{{ his.spelling }}</router-link
|
||||||
|
>
|
||||||
|
</td>
|
||||||
|
<td style="float:right">{{ his.time }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
read,
|
||||||
|
readlocalhistory,
|
||||||
|
addlocalhistory,
|
||||||
|
remove,
|
||||||
|
} from "./LocalHistory.js";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
length: Number,
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
var history_ = readlocalhistory();
|
||||||
|
const len = history_.length;
|
||||||
|
const history_display = len > 10 ? history_.slice(len - 10, len) : history_;
|
||||||
|
return {
|
||||||
|
history_display,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
remove_history: function() {
|
||||||
|
remove();
|
||||||
|
this.history_display = [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.localhistory-container {
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,84 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="邮箱"
|
||||||
|
v-model="data_.username"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="密码"
|
||||||
|
v-model="data_.password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<button v-on:click="send" type="submit" class="btn btn-outline-secondary">
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
<router-link :to="{name: 'Registation'}" class="mx-4 btn btn-outline-secondary">
|
||||||
|
注册
|
||||||
|
</router-link>
|
||||||
|
<router-link :to="{name: 'PasswordReset'}" class="btn btn-outline-secondary">
|
||||||
|
重设密码
|
||||||
|
</router-link>
|
||||||
|
<div
|
||||||
|
class="alert alert-warning alert-dismissible fade show"
|
||||||
|
role="alert"
|
||||||
|
v-if="veHasError"
|
||||||
|
>
|
||||||
|
<strong>信息错误</strong> <br />
|
||||||
|
<ul>
|
||||||
|
<li v-for="(item, key) in errorData" :key="key">
|
||||||
|
{{ key }}:{{ item }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button class="close" v-on:click="veHasError = false">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
import OsdictLocalStorage from '@/utils/OsdictLocalStorage.js'
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
data_: {
|
||||||
|
username: null,
|
||||||
|
password: null
|
||||||
|
},
|
||||||
|
errorData: null,
|
||||||
|
veHasError: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
send: function () {
|
||||||
|
let url = Api.login
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
data: this_.data_
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
const token = response.data.token
|
||||||
|
OsdictLocalStorage.setJWTlocalStorage(token)
|
||||||
|
// this_.$router.push({name: 'Main'})
|
||||||
|
this_.$router.go(-1)
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
this_.veHasError = true
|
||||||
|
this_.errorData = error.response.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,12 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<login-form> </login-form>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import LoginForm from './LoginForm'
|
||||||
|
export default {
|
||||||
|
components: { LoginForm: LoginForm }
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,101 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<search-form @reload="refreshData"></search-form>
|
||||||
|
<words-table
|
||||||
|
@reload="refreshData"
|
||||||
|
:data_words="WordsTableData"
|
||||||
|
:thead_="thead_"
|
||||||
|
:change-state="changeState"
|
||||||
|
v-if="tableState"
|
||||||
|
></words-table>
|
||||||
|
<page-nav @reload="refreshData" v-bind="page_"></page-nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import OsdictLocalStorage from '@/utils/OsdictLocalStorage.js'
|
||||||
|
import SearchForm from './SearchForm.vue'
|
||||||
|
import WordsTable from './WordsTable'
|
||||||
|
import Axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
import PageNav from './PageNav.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
WordsTable,
|
||||||
|
SearchForm,
|
||||||
|
PageNav
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
WordsTableData: [],
|
||||||
|
thead_: [
|
||||||
|
{ thead_: '单词', key: 'word' },
|
||||||
|
{ thead_: '词性', key: 'word_property' },
|
||||||
|
{ thead_: '含义', key: 'meaning' },
|
||||||
|
{ thead_: '领域', key: 'field' },
|
||||||
|
{ thead_: '例句', key: 'sentence' },
|
||||||
|
{ thead_: '状态', key: 'state' }
|
||||||
|
],
|
||||||
|
page_: { count: null, previous: null, next: null, currentpage: 1 },
|
||||||
|
tableState:true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getData: function () {
|
||||||
|
const url = Api.manageMeanings
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
params: this.$route.query,
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({})
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
this_.WordsTableData = response.data.results
|
||||||
|
let currentpage = 1
|
||||||
|
if (this_.$route.query.page !== undefined) {
|
||||||
|
currentpage = Number(this_.$route.query.page)
|
||||||
|
}
|
||||||
|
this_.page_ = {
|
||||||
|
count: response.data.count,
|
||||||
|
previous: response.data.previous,
|
||||||
|
next: response.data.next,
|
||||||
|
currentpage: currentpage
|
||||||
|
}
|
||||||
|
// this_.noAnswer = false
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
this_.WordsTableData = null
|
||||||
|
// this_.noAnswer = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshData:function(){
|
||||||
|
this.tableState=false
|
||||||
|
this.getData()
|
||||||
|
this.tableState=true
|
||||||
|
},
|
||||||
|
changeState: function (st, checkState) {
|
||||||
|
const url = Api.manageMeanings
|
||||||
|
for (var i = 0; i < checkState.length; i++) {
|
||||||
|
if (checkState[i].__state === true) {
|
||||||
|
Axios({
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({}),
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
params: { meaning_id: checkState[i].meaning_id },
|
||||||
|
data: { state: st }
|
||||||
|
})
|
||||||
|
.then(function () {})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log(error.response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.getData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<nav aria-label="..." class="mt-3">
|
||||||
|
<ul class="pagination">
|
||||||
|
<!-- previous -->
|
||||||
|
<li class="page-item disabled" v-if="!previous">
|
||||||
|
<span class="page-link">上一页</span>
|
||||||
|
</li>
|
||||||
|
<li class="page-item" v-else>
|
||||||
|
<a class="page-link" @click="pagePrevious">上一页</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<span class="page-link">第{{ currentpage }}页</span>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<span class="page-link">共{{ maxpage }}页</span>
|
||||||
|
</li>
|
||||||
|
<li class="page-item disabled" v-if="!next">
|
||||||
|
<span class="page-link">下一页</span>
|
||||||
|
</li>
|
||||||
|
<li class="page-item" v-else>
|
||||||
|
<a class="page-link" @click="pageNext">下一页</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
count: null,
|
||||||
|
next: null,
|
||||||
|
previous: null,
|
||||||
|
currentpage: Number
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
pageChange: function (page) {
|
||||||
|
var qy = Object(this.$route.query)
|
||||||
|
qy.page = page
|
||||||
|
// console.log(qy)
|
||||||
|
this.$router.push({
|
||||||
|
name: this.$route.name,
|
||||||
|
query: qy
|
||||||
|
})
|
||||||
|
this.$emit('reload')
|
||||||
|
},
|
||||||
|
pagePrevious: function () {
|
||||||
|
this.pageChange(this.currentpage - 1)
|
||||||
|
},
|
||||||
|
pageNext: function () {
|
||||||
|
this.pageChange(this.currentpage + 1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
maxpage: function () {
|
||||||
|
return Math.ceil(this.count / 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="word"
|
||||||
|
v-model="spelling"
|
||||||
|
/>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
type="button"
|
||||||
|
@click="search"
|
||||||
|
>
|
||||||
|
Search
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mt-3">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<label class="input-group-text" for="inputGroupSelect01"
|
||||||
|
>状态筛选</label
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<select class="custom-select" id="inputGroupSelect01" v-model="state">
|
||||||
|
<option value="" selected>无</option>
|
||||||
|
<option value="rf">已拒绝</option>
|
||||||
|
<option value="ck">待发布</option>
|
||||||
|
<option value="pb">已发布</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: String,
|
||||||
|
search_word: String
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
spelling: null,
|
||||||
|
state: null,
|
||||||
|
noAnswer: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
search: function () {
|
||||||
|
let sn = this.$route.query
|
||||||
|
sn.spelling = this.spelling
|
||||||
|
sn.page = 1
|
||||||
|
this.$router.push({
|
||||||
|
name: this.$route.name,
|
||||||
|
query: sn
|
||||||
|
})
|
||||||
|
this.$emit('reload')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
state: function (newState, oldState) {
|
||||||
|
if (newState !== oldState) {
|
||||||
|
let st = this.$route.query
|
||||||
|
st.state = newState
|
||||||
|
st.page = 1
|
||||||
|
this.$router.push({
|
||||||
|
name: this.$route.name,
|
||||||
|
query: st
|
||||||
|
})
|
||||||
|
this.$emit('reload')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<table class="table table-light table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col"></th>
|
||||||
|
<th scope="col" v-for="th in thead_" :key="th.key">
|
||||||
|
{{ th.thead_ }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template v-for="word in dataArray">
|
||||||
|
<tr v-bind:key="Object.keys(word)[0]">
|
||||||
|
<th scope="row">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="defaultCheck1"
|
||||||
|
v-model="word.__state"
|
||||||
|
/>
|
||||||
|
</th>
|
||||||
|
<td v-for="th in thead_" :key="th.key">{{ word[th.key] }}</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm mx-3" @click="refuse()">
|
||||||
|
拒绝
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" @click="publish()">
|
||||||
|
发布
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
data_words: Array,
|
||||||
|
thead_: Array,
|
||||||
|
changeState: Function
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data_words: function (NewDataWords, OldDataWords) {
|
||||||
|
if (NewDataWords !== OldDataWords) {
|
||||||
|
let dataArray_ = NewDataWords
|
||||||
|
/* for(var i=0; i<dataArray_.length;i++){
|
||||||
|
dataArray_[i].__state=false
|
||||||
|
} */
|
||||||
|
// console.log(dataArray_)
|
||||||
|
this.dataArray = dataArray_
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
var dataArray_ = this.data_words.slice()
|
||||||
|
// console.log(dataArray_)
|
||||||
|
for (var i = 0; i < dataArray_.length; i++) {
|
||||||
|
dataArray_[i].__state = false
|
||||||
|
}
|
||||||
|
// console.log(dataArray_)
|
||||||
|
return {
|
||||||
|
dataArray: dataArray_
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
refuse: function () {
|
||||||
|
this.changeState('rf', this.dataArray)
|
||||||
|
setTimeout(()=>{this.$emit('reload'),500})
|
||||||
|
},
|
||||||
|
publish: function () {
|
||||||
|
this.changeState('pb', this.dataArray)
|
||||||
|
setTimeout(()=>{this.$emit('reload'),500})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,97 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<search-form @reload="refreshData"></search-form>
|
||||||
|
<words-table
|
||||||
|
@reload="refreshData"
|
||||||
|
:data_words="WordsTableData"
|
||||||
|
:thead_="thead_"
|
||||||
|
:change-state="changeState"
|
||||||
|
v-if="tableState"
|
||||||
|
></words-table>
|
||||||
|
<page-nav @reload="refreshData" v-bind="page_"></page-nav>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import OsdictLocalStorage from '@/utils/OsdictLocalStorage.js'
|
||||||
|
import SearchForm from './SearchForm.vue'
|
||||||
|
import WordsTable from './WordsTable'
|
||||||
|
import Axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
import PageNav from './PageNav.vue'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
WordsTable,
|
||||||
|
SearchForm,
|
||||||
|
PageNav
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
WordsTableData: [],
|
||||||
|
thead_: [
|
||||||
|
{ thead_: '单词', key: 'spelling' },
|
||||||
|
{ thead_: '状态', key: 'state' }
|
||||||
|
],
|
||||||
|
page_: { count: null, previous: null, next: null, currentpage: 1 },
|
||||||
|
tableState:true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getData: function () {
|
||||||
|
const url = Api.manageWords
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
params: this.$route.query,
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({})
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
this_.WordsTableData = response.data.results
|
||||||
|
let currentpage = 1
|
||||||
|
if (this_.$route.query.page !== undefined) {
|
||||||
|
currentpage = Number(this_.$route.query.page)
|
||||||
|
}
|
||||||
|
this_.page_ = {
|
||||||
|
count: response.data.count,
|
||||||
|
previous: response.data.previous,
|
||||||
|
next: response.data.next,
|
||||||
|
currentpage: currentpage
|
||||||
|
}
|
||||||
|
// this_.noAnswer = false
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
this_.WordsTableData = null
|
||||||
|
// this_.noAnswer = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
refreshData:function(){
|
||||||
|
this.tableState=false
|
||||||
|
this.getData()
|
||||||
|
this.tableState=true
|
||||||
|
},
|
||||||
|
changeState: function (st, dataArray) {
|
||||||
|
const url = Api.manageWords
|
||||||
|
for (var i = 0; i < dataArray.length; i++) {
|
||||||
|
if (dataArray[i].__state === true) {
|
||||||
|
Axios({
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({}),
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
params: { word: dataArray[i].spelling },
|
||||||
|
data: { state: st }
|
||||||
|
})
|
||||||
|
.then(function (response) {})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log(error.response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
this.getData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<div class="inputmeaning">
|
||||||
|
<label class="input-group-text" for="InputGroupSelect">词性</label>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
class="custom-select"
|
||||||
|
id="inputGroupSelect01"
|
||||||
|
v-model="data_.word_property"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="choice in choices"
|
||||||
|
v-bind:key="choice.value"
|
||||||
|
v-bind:value="choice.value"
|
||||||
|
>
|
||||||
|
{{ choice.property }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="含义"
|
||||||
|
v-model="data_.meaning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="例句(选填)"
|
||||||
|
v-model="data_.sentence"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="作者名(选填)"
|
||||||
|
v-model="data_.author_name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" @click="send" >修改</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from 'axios'
|
||||||
|
import DictConcat from '@/utils/DictConcat'
|
||||||
|
import WordProperty from '@/components/WordProperty'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
urladd: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
usedurl:{
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
data_: {
|
||||||
|
meaning: null,
|
||||||
|
sentence: null,
|
||||||
|
author_name: null,
|
||||||
|
Word_Property: null
|
||||||
|
},
|
||||||
|
choices: WordProperty
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
send: function () {
|
||||||
|
let data1 = this.data_
|
||||||
|
let urlSend = this.urladd
|
||||||
|
let urlReturn = this.usedurl
|
||||||
|
Axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: urlSend,
|
||||||
|
data: data1
|
||||||
|
}).then(function(){
|
||||||
|
alert("添加成功")
|
||||||
|
location.href=urlReturn
|
||||||
|
}).catch(function (error) {
|
||||||
|
alert("错误\n" + DictConcat(error.response.data))
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>{{ spell }}</h1>
|
||||||
|
<meaningadd-form :urladd="sendplace" :usedurl="fomerurl"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MeaningaddForm from './meaningAddForm.vue'
|
||||||
|
import Api from '@/api'
|
||||||
|
import Axios from 'axios'
|
||||||
|
export default {
|
||||||
|
components: { MeaningaddForm },
|
||||||
|
data: function () {
|
||||||
|
const parameter = this.$route.query
|
||||||
|
let ID = Number(parameter.word_id)
|
||||||
|
|
||||||
|
return {
|
||||||
|
sendplace: Api.meaningCreate + ID,
|
||||||
|
spell: null,
|
||||||
|
fomerurl: "word?word_id=" +ID
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
let url = Api.wordSearch
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
params: this.$route.query
|
||||||
|
}).then(function (response) {
|
||||||
|
this_.spell = response.data[0].spelling
|
||||||
|
}).catch(function (error) {
|
||||||
|
console.log(error.response)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<label class="input-group-text" for="inputGroupSelect01">词性</label>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
class="custom-select"
|
||||||
|
id="inputGroupSelect01"
|
||||||
|
v-model="data_.word_property"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="choice in choices"
|
||||||
|
v-bind:key="choice.value"
|
||||||
|
v-bind:value="choice.value"
|
||||||
|
>
|
||||||
|
{{ choice.property }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="含义"
|
||||||
|
v-model="data_.meaning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="例句(选填)"
|
||||||
|
v-model="data_.sentence"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="作者名(选填)"
|
||||||
|
v-model="data_.author_name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-secondary" type="button" v-on:click="send">添加</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from 'axios'
|
||||||
|
import WordProperty from '@/components/WordProperty'
|
||||||
|
import DictConcat from '@/utils/DictConcat'
|
||||||
|
// import Api from '@/api'
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url1: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
word_id:{
|
||||||
|
type: null,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
data_: {
|
||||||
|
meaning: null,
|
||||||
|
sentence: null,
|
||||||
|
author_name: null,
|
||||||
|
word_property: null
|
||||||
|
},
|
||||||
|
choices: WordProperty,
|
||||||
|
spell: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
send: function () {
|
||||||
|
const this_ = this
|
||||||
|
let url = this.url1
|
||||||
|
let lastUrl = "word?word_id=" + this.word_id
|
||||||
|
Axios({
|
||||||
|
method: 'PATCH',
|
||||||
|
url: url,
|
||||||
|
data: this.data_
|
||||||
|
}).then(function(){
|
||||||
|
alert("添加成功")
|
||||||
|
this_.$router.push({path:lastUrl})
|
||||||
|
}).catch(function (error) {
|
||||||
|
alert("错误\n" + DictConcat(error.response.data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>{{ spell }}</h1>
|
||||||
|
<meaning-append-form :url1="sendplace" :word_id="returnurl"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MeaningAppendForm from './meaningAppendForm.vue'
|
||||||
|
import Api from '@/api'
|
||||||
|
import Axios from 'axios'
|
||||||
|
export default {
|
||||||
|
components: { MeaningAppendForm },
|
||||||
|
data: function () {
|
||||||
|
const parameter = this.$route.query
|
||||||
|
let ID = Number(parameter.meaningfield_id)
|
||||||
|
let Api2 = Api.meaningAppend + ID
|
||||||
|
return {
|
||||||
|
sendplace: Api2,
|
||||||
|
spell: null,
|
||||||
|
returnurl : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
let url = Api.wordSearch
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: url,
|
||||||
|
params: this.$route.query
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
console.log(error.response)
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
this_.spell = response.data[0].spelling,
|
||||||
|
this_.returnurl = response.data[0].word_id
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// name:word query:{word_id=this.word_id}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="邮箱"
|
||||||
|
v-model="data_.email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="验证码"
|
||||||
|
v-model="data_.code"
|
||||||
|
/>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text" v-if="veHasSend">已发送</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
v-on:click="sendVerifyCode"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="输入密码"
|
||||||
|
v-model="data_.password1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="确认密码"
|
||||||
|
v-model="data_.password2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-toggle="button"
|
||||||
|
aria-pressed="false"
|
||||||
|
v-on:click="send"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</button>
|
||||||
|
<!--error info-->
|
||||||
|
<div
|
||||||
|
class="alert alert-warning alert-dismissible fade show"
|
||||||
|
role="alert"
|
||||||
|
v-if="veHasError"
|
||||||
|
>
|
||||||
|
<strong>信息错误</strong> <br />
|
||||||
|
<ul>
|
||||||
|
<li v-for="(item, key) in errorData" :key="key">
|
||||||
|
{{ key }}:{{ item }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button class="close" v-on:click="veHasError = false">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
data_: {
|
||||||
|
email: null,
|
||||||
|
password1: null,
|
||||||
|
password2: null,
|
||||||
|
code: null
|
||||||
|
},
|
||||||
|
veHasSend: false,
|
||||||
|
veHasError: false,
|
||||||
|
errorData: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
send: function () {
|
||||||
|
let url = Api.passwordReset
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
data: this_.data_
|
||||||
|
}).catch(function (error) {
|
||||||
|
this_.veHasError = true
|
||||||
|
this_.errorData = error.response.data
|
||||||
|
}).then(function () {
|
||||||
|
this_.$router.push({ name:'Login'})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendVerifyCode: function () {
|
||||||
|
let url = Api.passwordResetVerifyEmail
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
data: { email: this_.data_.email }
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
this_.veHasSend = true
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
this_.veHasError = true
|
||||||
|
console.log(error.response)
|
||||||
|
this_.errorData = error.response.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<passwordreset-form></passwordreset-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import PasswordresetForm from './PasswordresetForm'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
PasswordresetForm
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,126 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="邮箱"
|
||||||
|
v-model="data_.email"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="输入密码"
|
||||||
|
v-model="data_.password1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="确认密码"
|
||||||
|
v-model="data_.password2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mb-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="验证码"
|
||||||
|
v-model="data_.code"
|
||||||
|
/>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text" v-if="veHasSend">已发送</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
v-on:click="sendVerifyCode"
|
||||||
|
v-else
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-toggle="button"
|
||||||
|
aria-pressed="false"
|
||||||
|
v-on:click="send"
|
||||||
|
>
|
||||||
|
注册
|
||||||
|
</button>
|
||||||
|
<!--error info-->
|
||||||
|
<div
|
||||||
|
class="alert alert-warning alert-dismissible fade show"
|
||||||
|
role="alert"
|
||||||
|
v-if="veHasError"
|
||||||
|
>
|
||||||
|
<strong>信息错误</strong> <br />
|
||||||
|
<ul>
|
||||||
|
<li v-for="(item, key) in errorData" :key="key">
|
||||||
|
{{ key }}:{{ item }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<button class="close" v-on:click="veHasError = false">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
data_: {
|
||||||
|
email: null,
|
||||||
|
password1: null,
|
||||||
|
password2: null,
|
||||||
|
code: null
|
||||||
|
},
|
||||||
|
veHasSend: false,
|
||||||
|
veHasError: false,
|
||||||
|
errorData: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
send: function () {
|
||||||
|
let url = Api.registration
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
data: this_.data_
|
||||||
|
}).catch(function (error) {
|
||||||
|
this_.veHasError = true
|
||||||
|
this_.errorData = error.response.data
|
||||||
|
}).then(function () {
|
||||||
|
this_.$router.push({ name:'Login'})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
sendVerifyCode: function () {
|
||||||
|
let url = Api.registrationVerifyEmail
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
data: { email: this_.data_.email }
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
this_.veHasSend = true
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
this_.veHasError = true
|
||||||
|
this_.errorData = error.response.data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<registration-form></registration-form>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import RegistrationForm from './RegistrationForm'
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
RegistrationForm
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,102 @@
|
||||||
|
<template>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="input-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="word"
|
||||||
|
v-model="spelling"
|
||||||
|
id="search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" v-for="word in data" :key="word.word_id">
|
||||||
|
<a href="#"
|
||||||
|
@click="history(word)"
|
||||||
|
>{{ word.spelling }}</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li v-if="noAnswer" class="list-group-item">
|
||||||
|
<span class="disabled">No Answer</span>
|
||||||
|
<br />
|
||||||
|
<router-link :to="{ name: 'WordAdd' }">Add Word</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
import {addlocalhistory} from'@/components/history/LocalHistory.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
spelling: '',
|
||||||
|
data: [],
|
||||||
|
noAnswer: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
name: 'search-input-group',
|
||||||
|
methods: {
|
||||||
|
search: function () {
|
||||||
|
const url = Api.wordSearch
|
||||||
|
const this_ = this
|
||||||
|
const data = { spelling: this_.spelling }
|
||||||
|
Axios({
|
||||||
|
methods: 'POST',
|
||||||
|
url: url,
|
||||||
|
params: data
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
|
// this_.$router.push()
|
||||||
|
this_.data = response.data
|
||||||
|
this_.noAnswer = false
|
||||||
|
if (this_.spelling !== '' && this_.data.length === 0) {
|
||||||
|
this_.noAnswer = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function (error) {
|
||||||
|
if (error.response.status === 404) {
|
||||||
|
this_.data = []
|
||||||
|
this_.noAnswer = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
history:function(word){
|
||||||
|
var word_=word
|
||||||
|
var key="time"
|
||||||
|
var stdtime = new Date()
|
||||||
|
if(stdtime.getMinutes()<10){
|
||||||
|
var localtimeminute = '0' + stdtime.getMinutes()
|
||||||
|
}else{
|
||||||
|
var localtimeminute =stdtime.getMinutes()
|
||||||
|
}
|
||||||
|
const localdatetime=stdtime.getFullYear() + '/' + (stdtime.getMonth()+1) + '/' +stdtime.getDate()
|
||||||
|
const localtime=localdatetime + " " + stdtime.getHours() + ":" + localtimeminute
|
||||||
|
word_[key]=localtime
|
||||||
|
addlocalhistory(word_)
|
||||||
|
this.$router.push({ name: 'word', query: { word_id: word.word_id }})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
spelling: function (value, oldvar) {
|
||||||
|
if (value !== oldvar && value !== '') {
|
||||||
|
this.search()
|
||||||
|
} else if (value === '') {
|
||||||
|
this.data = []
|
||||||
|
this.noAnswer = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'$route.query.word_id': function (newwordid, oldwordid) {
|
||||||
|
if (newwordid !== oldwordid) {
|
||||||
|
this.spelling = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,32 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1 class="my-3" :style="{'font-size':fontSize,'text-align': 'center'}">OSDICT</h1>
|
||||||
|
<searchinputgroup />
|
||||||
|
<local-history-list :length="length"></local-history-list>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import searchinputgroup from './SearchInputGroup.vue'
|
||||||
|
import LocalHistoryList from '../history/LocalHistoryList.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
searchinputgroup,
|
||||||
|
LocalHistoryList
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return { fontSize: '7.5em', length: 1078}
|
||||||
|
},
|
||||||
|
created: function () {
|
||||||
|
let screenWidth = document.body.clientWidth || document.documentElement.clientWidth
|
||||||
|
if (screenWidth <= 768){
|
||||||
|
this.fontSize = parseInt(screenWidth*0.8/5) + "px"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted(){
|
||||||
|
const e = document.getElementById("search-input")
|
||||||
|
this.length = e.clientWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,63 @@
|
||||||
|
<template>
|
||||||
|
<p v-if="items == null || items.length == 0">当前无收藏记录</p>
|
||||||
|
<div v-else>
|
||||||
|
<ul class="list-group">
|
||||||
|
<li class="list-group-item" v-for="item in items" :key="item.spelling">
|
||||||
|
<router-link :to="{ name: 'word', query: { word_id: item.word_id } }">
|
||||||
|
{{ item.spelling }} </router-link
|
||||||
|
><a href="#">
|
||||||
|
<i
|
||||||
|
class="bi-x-circle"
|
||||||
|
style="float:right;"
|
||||||
|
@click="cancelCollect(item)"
|
||||||
|
></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from "axios";
|
||||||
|
import Api from "@/api";
|
||||||
|
import OsdictLocalStorage from "@/utils/OsdictLocalStorage.js";
|
||||||
|
export default {
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
items: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
created: function() {
|
||||||
|
const this_ = this;
|
||||||
|
let url = Api.collectedWord;
|
||||||
|
Axios({
|
||||||
|
method: "GET",
|
||||||
|
url: url,
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({}),
|
||||||
|
})
|
||||||
|
.catch(function(error) {
|
||||||
|
console.log(error.response);
|
||||||
|
})
|
||||||
|
.then(function(response) {
|
||||||
|
this_.items = response.data;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancelCollect: function(item) {
|
||||||
|
const this_ = this;
|
||||||
|
let url = Api.collectedWord;
|
||||||
|
Axios({
|
||||||
|
method: "POST",
|
||||||
|
url: url,
|
||||||
|
data: { word_id: item.word_id, collected: false },
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({}),
|
||||||
|
})
|
||||||
|
.then(function() {
|
||||||
|
this_.items.splice(this_.items.indexOf(item), 1);
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
this_.$router.push({ name: "Login" });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,14 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h1>收藏</h1>
|
||||||
|
<collect-card-form> </collect-card-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import CollectCardForm from "./CollectCardForm";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
CollectCardForm,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,99 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="card mb-3">
|
||||||
|
<ul class="list-group list-group-flush">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-left" v-if="field != null">
|
||||||
|
{{ word_property }} {{ meaning }},[{{ field }}]
|
||||||
|
</h5>
|
||||||
|
<h5 class="card-title text-left" v-else>
|
||||||
|
{{ word_property }} {{ meaning }}
|
||||||
|
</h5>
|
||||||
|
<p class="card-text text-left">{{ sentence }}</p>
|
||||||
|
<p class="card-text text-left">{{ localtime }}</p>
|
||||||
|
<router-link :to="{name: 'MeaningAppend',query:{meaningfield_id:meaningfield_id}}">修改含义</router-link>
|
||||||
|
<a
|
||||||
|
style="display:block" href="#"
|
||||||
|
v-if="count > 1 && dataList == null"
|
||||||
|
@click="getdata"
|
||||||
|
>
|
||||||
|
浏览历史版本
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<template v-if="dataList != null">
|
||||||
|
<li
|
||||||
|
class="list-group-item"
|
||||||
|
v-for="datas in dataList"
|
||||||
|
v-bind:key="datas.version"
|
||||||
|
>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-left" v-if="datas.field != null">
|
||||||
|
{{ datas.word_property }} {{ datas.meaning }},[{{
|
||||||
|
datas.field
|
||||||
|
}}]
|
||||||
|
</h5>
|
||||||
|
<h5 class="card-title text-left" v-else>
|
||||||
|
{{ datas.word_property }} {{ datas.meaning }}
|
||||||
|
</h5>
|
||||||
|
<p class="card-text text-left">{{ datas.sentence }}</p>
|
||||||
|
<p class="card-text text-left">{{ datas.localtime }}</p>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
field: String,
|
||||||
|
word_property: String,
|
||||||
|
meaning: String,
|
||||||
|
sentence: String,
|
||||||
|
meaningfield_id: Number,
|
||||||
|
count: Number,
|
||||||
|
add_time:String
|
||||||
|
},
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
dataList: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
transform:function(add_time){
|
||||||
|
var stdtime = new Date(add_time)
|
||||||
|
if(stdtime.getMinutes()<10){
|
||||||
|
var localtimeminute = '0' + stdtime.getMinutes()
|
||||||
|
}else{
|
||||||
|
var localtimeminute =stdtime.getMinutes()
|
||||||
|
}
|
||||||
|
const localdatetime=stdtime.getFullYear() + '/' + (stdtime.getMonth()+1) + '/' +stdtime.getDate()
|
||||||
|
const localtime=localdatetime + " " + stdtime.getHours() + ":" + localtimeminute
|
||||||
|
return localtime
|
||||||
|
},
|
||||||
|
getdata: function () {
|
||||||
|
let url = Api.meaningList + this.meaningfield_id
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'GET',
|
||||||
|
url: url
|
||||||
|
}).then(function (response) {
|
||||||
|
this_.dataList = response.data
|
||||||
|
for(var i = 0; i < this_.dataList.length; i++) {
|
||||||
|
this_.dataList[i].localtime=this_.transform(this_.dataList[i].add_time)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed:{
|
||||||
|
localtime:function(){
|
||||||
|
return this.transform(this.add_time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div class="card my-3">
|
||||||
|
<div class="card-body">
|
||||||
|
<a href="#">
|
||||||
|
<i
|
||||||
|
class="bi-star"
|
||||||
|
@click="toCollect"
|
||||||
|
v-if="data_.collected == false"
|
||||||
|
></i>
|
||||||
|
</a>
|
||||||
|
<a href="#">
|
||||||
|
<i
|
||||||
|
class="bi-star-fill"
|
||||||
|
@click="toCollect"
|
||||||
|
v-if="data_.collected == true"
|
||||||
|
></i>
|
||||||
|
</a>
|
||||||
|
<h5 class="card-title text-center">{{ spelling }}</h5>
|
||||||
|
<h6
|
||||||
|
class="card-subtitle mb-2 text-muted text-center"
|
||||||
|
v-if="importance != null"
|
||||||
|
>
|
||||||
|
{{ importance }}
|
||||||
|
</h6>
|
||||||
|
<router-link :to="{ name: 'MeaningAdd', query: { word_id: word_id } }"
|
||||||
|
>添加新含义</router-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from "axios";
|
||||||
|
import Api from "@/api";
|
||||||
|
import OsdictLocalStorage from "@/utils/OsdictLocalStorage.js";
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
spelling: String,
|
||||||
|
importance: String,
|
||||||
|
word_id: Number,
|
||||||
|
iscollected: Boolean,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
data_: {
|
||||||
|
word_id: this.word_id,
|
||||||
|
collected: this.iscollected,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
toCollect: function() {
|
||||||
|
const this_ = this;
|
||||||
|
let url = Api.collectedWord;
|
||||||
|
this.data_.collected = !this_.data_.collected;
|
||||||
|
Axios({
|
||||||
|
method: "POST",
|
||||||
|
url: url,
|
||||||
|
data: this.data_,
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({}),
|
||||||
|
})
|
||||||
|
.then(function() {})
|
||||||
|
.catch(function() {
|
||||||
|
this_.$router.push({ name: "Login" });
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
word_id(newdata, olddata) {
|
||||||
|
this.data_.word_id = this.word_id;
|
||||||
|
this.data_.collected = this.iscollected;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,69 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<searchinputgroup />
|
||||||
|
<div class="container">
|
||||||
|
<word-card
|
||||||
|
v-bind:spelling="data.spelling"
|
||||||
|
v-bind:importance="data.importance"
|
||||||
|
v-bind:word_id="$route.query.word_id"
|
||||||
|
:iscollected="collected"
|
||||||
|
v-if="wordCardState"
|
||||||
|
></word-card>
|
||||||
|
<div v-for="meaning in data.meanings" v-bind:key="meaning.add_time">
|
||||||
|
<meanings-card v-bind="meaning"></meanings-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import MeaningsCard from "./MeaningsCard.vue";
|
||||||
|
import WordCard from "./WordCard.vue";
|
||||||
|
import searchinputgroup from "../search/SearchInputGroup.vue";
|
||||||
|
import Axios from "axios";
|
||||||
|
import Api from "@/api";
|
||||||
|
import OsdictLocalStorage from "@/utils/OsdictLocalStorage.js";
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MeaningsCard,
|
||||||
|
WordCard,
|
||||||
|
searchinputgroup,
|
||||||
|
},
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
data: null,
|
||||||
|
collected: false,
|
||||||
|
wordCardState: true
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getData: function() {
|
||||||
|
const wordid = this.$route.query.word_id;
|
||||||
|
let url = Api.word + wordid;
|
||||||
|
const this_ = this;
|
||||||
|
|
||||||
|
Axios({
|
||||||
|
url: url,
|
||||||
|
method: "GET",
|
||||||
|
headers: OsdictLocalStorage.headerAuthorization({}),
|
||||||
|
}).then(function(response) {
|
||||||
|
this_.wordCardState = false
|
||||||
|
this_.data = response.data;
|
||||||
|
this_.collected = response.data.collected;
|
||||||
|
this_.$nextTick(() => {
|
||||||
|
this_.wordCardState = true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created: function() {
|
||||||
|
this.getData();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
"$route.query.word_id": function(newwordid, oldwordid) {
|
||||||
|
if (newwordid !== oldwordid) {
|
||||||
|
this.getData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -0,0 +1,111 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="拼写"
|
||||||
|
v-model="data_.spelling"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group mb-3">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<label class="input-group-text" for="inputGroupSelect01">词性</label>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
class="custom-select"
|
||||||
|
id="inputGroupSelect01"
|
||||||
|
v-model="data_.word_property"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="choice in choices"
|
||||||
|
v-bind:key="choice.value"
|
||||||
|
v-bind:value="choice.value"
|
||||||
|
>{{ choice.property }}</option
|
||||||
|
>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="适用领域(选填)"
|
||||||
|
v-model="data_.field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="含义"
|
||||||
|
v-model="data_.meaning"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="例句(选填)"
|
||||||
|
v-model="data_.sentance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="form-control"
|
||||||
|
placeholder="作者名(选填)"
|
||||||
|
v-model="data_.author_name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
data-toggle="button"
|
||||||
|
aria-pressed="false"
|
||||||
|
v-on:click="send"
|
||||||
|
>
|
||||||
|
添加
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import Axios from 'axios'
|
||||||
|
import Api from '@/api'
|
||||||
|
import WordProperty from '@/components/WordProperty'
|
||||||
|
import DictConcat from '@/utils/DictConcat'
|
||||||
|
export default {
|
||||||
|
data: function () {
|
||||||
|
return {
|
||||||
|
data_: {
|
||||||
|
spelling: null,
|
||||||
|
field: null,
|
||||||
|
meaning: null,
|
||||||
|
sentance: null,
|
||||||
|
author_name: null,
|
||||||
|
word_property: null
|
||||||
|
},
|
||||||
|
choices: WordProperty
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
send: function () {
|
||||||
|
let url = Api.word
|
||||||
|
const this_ = this
|
||||||
|
Axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: url,
|
||||||
|
data: this_.data_
|
||||||
|
}).then(function(){
|
||||||
|
alert("添加成功,即将返回上一页")
|
||||||
|
this_.$router.go(-1)
|
||||||
|
}).catch(function (error) {
|
||||||
|
alert("错误\n" + DictConcat(error.response.data))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,26 @@
|
||||||
|
// 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 Bootstrap an BootstrapVue CSS files (order is important)
|
||||||
|
// import '@popperjs/core'
|
||||||
|
import 'bootstrap/dist/css/bootstrap.css'
|
||||||
|
import 'bootstrap-icons/font/bootstrap-icons.css'
|
||||||
|
// import 'https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.slim.min.js'
|
||||||
|
// import 'bootstrap/dist/js/bootstrap.js'
|
||||||
|
|
||||||
|
import App from './App'
|
||||||
|
import router from './router'
|
||||||
|
|
||||||
|
Vue.config.productionTip = false
|
||||||
|
|
||||||
|
// Vue.use(BootstrapVue)
|
||||||
|
// Optionally install the BootstrapVue icon components plugin
|
||||||
|
// Vue.use(IconsPlugin)
|
||||||
|
|
||||||
|
/* eslint-disable no-new */
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
render: h => h(App),
|
||||||
|
router: router
|
||||||
|
}).$mount('#app')
|
|
@ -0,0 +1,77 @@
|
||||||
|
import Vue from 'vue'
|
||||||
|
import Router from 'vue-router'
|
||||||
|
import SearchView from '@/components/search/SearchView'
|
||||||
|
import Registration from '@/components/registration/RegistrationView'
|
||||||
|
import LoginView from '@/components/login/LoginView'
|
||||||
|
import PasswordResetView from '@/components/passwordreset/PasswordresetView'
|
||||||
|
import WordView from '@/components/words/WordView'
|
||||||
|
import WordAdd from '@/components/wordsadd/WordAdd'
|
||||||
|
import ManagementWords from '@/components/management/WordsView'
|
||||||
|
import ManagementMeanings from '@/components/management/MeaningsView'
|
||||||
|
import MeaningAppend from '@/components/meaningappend/meaningAppendView'
|
||||||
|
import MeaningAdd from '@/components/meaningadd/meaningAddView'
|
||||||
|
import CollectCard from '@/components/words/CollectCardView'
|
||||||
|
// test
|
||||||
|
|
||||||
|
Vue.use(Router)
|
||||||
|
|
||||||
|
export default new Router({
|
||||||
|
mode: 'history',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/registration',
|
||||||
|
name: 'Registation',
|
||||||
|
component: Registration
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Main',
|
||||||
|
component: SearchView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: LoginView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/password-reset',
|
||||||
|
name: 'PasswordReset',
|
||||||
|
component: PasswordResetView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/word',
|
||||||
|
name: 'word',
|
||||||
|
component: WordView
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/word-add',
|
||||||
|
name: 'WordAdd',
|
||||||
|
component: WordAdd
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/management/words',
|
||||||
|
name: 'ManagementWords',
|
||||||
|
component: ManagementWords
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/meaningappend',
|
||||||
|
name: 'MeaningAppend',
|
||||||
|
component: MeaningAppend
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/meaningadd',
|
||||||
|
name: 'MeaningAdd',
|
||||||
|
component: MeaningAdd
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/management/meanings',
|
||||||
|
name: 'ManagementMeanings',
|
||||||
|
component: ManagementMeanings
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/collected-words',
|
||||||
|
name: 'CollectedWords',
|
||||||
|
component: CollectCard
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
|
@ -0,0 +1,7 @@
|
||||||
|
export default function (dict) {
|
||||||
|
let answer = ''
|
||||||
|
for (var key in dict) {
|
||||||
|
answer = answer + key + ':' + String(dict[key]) + '\n'
|
||||||
|
}
|
||||||
|
return answer
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
const jwtKeyName = 'osdict_jwt'
|
||||||
|
|
||||||
|
function prependJWT (str) {
|
||||||
|
return 'JWT ' + str
|
||||||
|
}
|
||||||
|
|
||||||
|
var OsdictLocalStorage = function () {
|
||||||
|
var setJWTlocalStorage = function (str) {
|
||||||
|
str = prependJWT(str)
|
||||||
|
localStorage.setItem(jwtKeyName, str)
|
||||||
|
}
|
||||||
|
var getJWTlocalStorage = function () {
|
||||||
|
var jwt = ''
|
||||||
|
try {
|
||||||
|
jwt = localStorage.getItem(jwtKeyName)
|
||||||
|
} catch (err) {
|
||||||
|
jwt = null
|
||||||
|
}
|
||||||
|
return jwt
|
||||||
|
}
|
||||||
|
var removeJWTlocalStorage = function () {
|
||||||
|
try {
|
||||||
|
localStorage.removeItem(jwtKeyName)
|
||||||
|
} catch (err) {
|
||||||
|
// regardless
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var headerAuthorization = function (data) {
|
||||||
|
var token = getJWTlocalStorage()
|
||||||
|
data.Authorization = token
|
||||||
|
data['Content-Type'] = 'application/json'
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
setJWTlocalStorage,
|
||||||
|
getJWTlocalStorage,
|
||||||
|
removeJWTlocalStorage,
|
||||||
|
headerAuthorization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OsdictLocalStorage()
|
|
@ -0,0 +1,54 @@
|
||||||
|
from typing import Sequence
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from sqlalchemy.sql.expression import update
|
||||||
|
|
||||||
|
from wordspider import WordSpider
|
||||||
|
from models import WordData
|
||||||
|
import models
|
||||||
|
|
||||||
|
|
||||||
|
AMOUNT = 9500
|
||||||
|
|
||||||
|
|
||||||
|
def get_not_retrieve_word_list(session: Session) -> Sequence:
|
||||||
|
queryset = session.query(WordData).filter_by(has_retrieve=False)
|
||||||
|
return queryset
|
||||||
|
|
||||||
|
|
||||||
|
def get_data(session: Session, word_list: Sequence) -> None:
|
||||||
|
for word in word_list:
|
||||||
|
spider = WordSpider(word)
|
||||||
|
text = spider.parse_page()
|
||||||
|
if spider.success:
|
||||||
|
print("*", end="")
|
||||||
|
else:
|
||||||
|
print("F", end="")
|
||||||
|
continue
|
||||||
|
session.execute(
|
||||||
|
update(WordData)
|
||||||
|
.where(WordData.word == word)
|
||||||
|
.values(html=text, has_retrieve=True)
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
starttime = time.time()
|
||||||
|
logging.basicConfig(filename="spider.log")
|
||||||
|
with models.Session() as session:
|
||||||
|
ans = get_not_retrieve_word_list(session)
|
||||||
|
|
||||||
|
if ans.count() < AMOUNT:
|
||||||
|
queryset = ans
|
||||||
|
else:
|
||||||
|
queryset = ans[0:AMOUNT]
|
||||||
|
|
||||||
|
word_list = map(lambda item: item.word, queryset)
|
||||||
|
|
||||||
|
get_data(session, word_list)
|
||||||
|
|
||||||
|
endtime = time.time()
|
||||||
|
print(endtime - starttime)
|
|
@ -0,0 +1,43 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from normalutils.choices import StateType
|
||||||
|
import omodels as om
|
||||||
|
import models as sm
|
||||||
|
|
||||||
|
|
||||||
|
word_fn = lambda word: {
|
||||||
|
"spelling": word.spelling,
|
||||||
|
"importance": word.importance,
|
||||||
|
"state": StateType.PUBLISHED.value,
|
||||||
|
}
|
||||||
|
meaning_fn = lambda meaning: {
|
||||||
|
"meaning": meaning.meaning,
|
||||||
|
"word_property": meaning.word_property,
|
||||||
|
"state": StateType.PUBLISHED.value
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_word_meanings(o_session, word: sm.Word):
|
||||||
|
if word.spelling is None:
|
||||||
|
logging.error("word is None. word id is {}".format(word.id))
|
||||||
|
return
|
||||||
|
meanings = word.meanings
|
||||||
|
o_word_dict = word_fn(word)
|
||||||
|
o_word = om.OWord(**o_word_dict)
|
||||||
|
o_session.add(o_word)
|
||||||
|
|
||||||
|
for meaning in meanings:
|
||||||
|
meaningfield = om.OMeaningField(word=o_word)
|
||||||
|
o_session.add(meaningfield)
|
||||||
|
meaning_dict = meaning_fn(meaning)
|
||||||
|
meaning = om.OMeaning(meaningfield=meaningfield, **meaning_dict)
|
||||||
|
o_session.add(meaning)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with sm.Session() as s_session:
|
||||||
|
words = s_session.query(sm.Word)
|
||||||
|
with om.OSession() as o_session:
|
||||||
|
for word in words:
|
||||||
|
migrate_word_meanings(o_session, word)
|
||||||
|
o_session.commit()
|
|
@ -0,0 +1,14 @@
|
||||||
|
from models import WordData, Session
|
||||||
|
|
||||||
|
|
||||||
|
words = []
|
||||||
|
with open("./google-10000-english.txt", "r") as f:
|
||||||
|
words = f.readlines()
|
||||||
|
|
||||||
|
with Session() as session:
|
||||||
|
for word in words:
|
||||||
|
newword = WordData(word=word.strip())
|
||||||
|
# newword = WordData(spelling=word.strip())
|
||||||
|
session.add(newword)
|
||||||
|
|
||||||
|
session.commit()
|
|
@ -0,0 +1,46 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from models import Session, WordData, Word
|
||||||
|
from serializers import WordAddSerializer, MeaningAddSerializer
|
||||||
|
from renderercontents import renderer_word, renderer_meaningslist, has_value_to_render
|
||||||
|
|
||||||
|
|
||||||
|
def create_word_meaning(text, session):
|
||||||
|
word_dict = renderer_word(text)
|
||||||
|
word_serializer = WordAddSerializer(word_dict, session)
|
||||||
|
flag = False
|
||||||
|
try:
|
||||||
|
flag = word_serializer.is_valid(True)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("msg: {} data: {}".format(e, word_dict))
|
||||||
|
if not flag:
|
||||||
|
return # fail validation
|
||||||
|
if session.query(Word).filter_by(spelling=word_dict['spelling']).count() > 0:
|
||||||
|
return # repeat
|
||||||
|
word = word_serializer.save()
|
||||||
|
if word.spelling is None: # word is null
|
||||||
|
logging.error("word spelling is null word_dict: {}\n{}".format(word_dict, text))
|
||||||
|
try:
|
||||||
|
meaning_list = renderer_meaningslist(text)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("msg: {} word: {}\n{}".format(e, word.spelling, text))
|
||||||
|
raise e
|
||||||
|
for meaning in meaning_list:
|
||||||
|
meaning_serializer = MeaningAddSerializer(meaning, session, word)
|
||||||
|
flag = False
|
||||||
|
try:
|
||||||
|
flag = meaning_serializer.is_valid(True)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("msg: {} word: {} data: {}".format(e, word.spelling, meaning))
|
||||||
|
meaning_serializer.save()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
logging.basicConfig(filename="spider.log")
|
||||||
|
with Session() as session:
|
||||||
|
queryset = session.query(WordData).filter_by(has_retrieve=True)
|
||||||
|
texts = map(lambda word: word.html, queryset)
|
||||||
|
for text in texts:
|
||||||
|
if has_value_to_render(text):
|
||||||
|
create_word_meaning(text, session)
|
||||||
|
session.commit()
|
|
@ -0,0 +1,10 @@
|
||||||
|
from normalutils.choices import BaseChoices
|
||||||
|
|
||||||
|
|
||||||
|
class HttpMethod(BaseChoices):
|
||||||
|
GET = "GET"
|
||||||
|
POST = "POST"
|
||||||
|
PUT = "PUT"
|
||||||
|
PATCH = "PATCH"
|
||||||
|
DELETE = "DELETE"
|
||||||
|
OPTIONS = "OPOTIONS"
|
|
@ -0,0 +1,86 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine, Column
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import relationship, sessionmaker
|
||||||
|
from sqlalchemy.sql.schema import ForeignKey
|
||||||
|
from sqlalchemy.sql.sqltypes import Boolean, DateTime, String, Text, Integer
|
||||||
|
|
||||||
|
|
||||||
|
engine = create_engine("sqlite:///spider.sqlite3", future=True)
|
||||||
|
Base = declarative_base()
|
||||||
|
Session = sessionmaker(engine, future=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Word(Base):
|
||||||
|
__tablename__ = "word"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
spelling = Column(String(64), unique=True)
|
||||||
|
importance = Column("importance", String(32), nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return self.spelling
|
||||||
|
|
||||||
|
|
||||||
|
class Meaning(Base):
|
||||||
|
__tablename__ = "meaning"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
meaning = Column(String(128))
|
||||||
|
word_property = Column(String(8))
|
||||||
|
sentance = Column(String(128), nullable=True)
|
||||||
|
add_time = Column(DateTime, default=datetime.utcnow)
|
||||||
|
word_id = Column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("{0}.id".format(Word.__tablename__), ondelete="CASCADE"),
|
||||||
|
)
|
||||||
|
|
||||||
|
word = relationship("Word", backref="meanings")
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "{} {} {}".format(self.word.spelling, self.word_property, self.meaning)
|
||||||
|
|
||||||
|
|
||||||
|
class WordData(Base):
|
||||||
|
__tablename__ = "word_data"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
word = Column(String(64), unique=True, nullable=False)
|
||||||
|
has_retrieve = Column(Boolean, default=False, nullable=False)
|
||||||
|
url = Column(String(256), nullable=True)
|
||||||
|
html = Column(Text, nullable=True)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return "'{}' {}".format(self.word, self.has_retrieve)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_word_null():
|
||||||
|
with Session() as session:
|
||||||
|
queryset = session.query(Word).filter_by(spelling=None)
|
||||||
|
for item in queryset:
|
||||||
|
session.delete(item)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Base.metadata.create_all(engine)
|
||||||
|
def testcase():
|
||||||
|
with Session() as session:
|
||||||
|
the = session.query(Word).filter_by(spelling="the").one()
|
||||||
|
print(the.meanings)
|
||||||
|
|
||||||
|
# testcase()
|
||||||
|
def testcase2():
|
||||||
|
with Session() as session:
|
||||||
|
nonecase = session.query(Word).filter_by(spelling=None)
|
||||||
|
print(nonecase.count())
|
||||||
|
|
||||||
|
def testcase3():
|
||||||
|
with Session() as session:
|
||||||
|
john = session.query(Word).filter_by(spelling="john").one()
|
||||||
|
print(john.importance)
|
||||||
|
print(type(john.importance)) # => str something is wrong
|
||||||
|
|
||||||
|
# clear_word_null()
|
||||||
|
testcase3()
|
|
@ -0,0 +1,59 @@
|
||||||
|
# from userscontent.models import ContentType
|
||||||
|
import enum
|
||||||
|
from typing import Iterator, Union
|
||||||
|
|
||||||
|
|
||||||
|
class BaseChoices(enum.Enum):
|
||||||
|
@classmethod
|
||||||
|
def is_valid(cls, value: str, raise_exception: bool = False) -> bool:
|
||||||
|
answer = isinstance(value, str)
|
||||||
|
if not answer:
|
||||||
|
if raise_exception:
|
||||||
|
raise TypeError("The type of 'value' is wrong.")
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
answer = value in cls.choices()
|
||||||
|
if not raise_exception or answer:
|
||||||
|
return answer
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"The class '{}' does not have {}".format(cls.__name__, value)
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def choices(cls, iter=False) -> Union[tuple, Iterator]:
|
||||||
|
choices = tuple(cls)
|
||||||
|
iterator_obj = map(lambda choice: choice.value, choices)
|
||||||
|
if iter:
|
||||||
|
return iterator_obj
|
||||||
|
else:
|
||||||
|
return tuple(iterator_obj)
|
||||||
|
|
||||||
|
|
||||||
|
class WordPropertyType(BaseChoices):
|
||||||
|
NOUN = "n."
|
||||||
|
PRONOUN = "pron." # 代词
|
||||||
|
ADJECTIVE = "adj."
|
||||||
|
ADVERB = "adv."
|
||||||
|
VERB = "v."
|
||||||
|
NUMBERAL = "num."
|
||||||
|
ARTICLE = "art."
|
||||||
|
PREPOTION = "prep."
|
||||||
|
CONJUNCTION = "conj."
|
||||||
|
INTERJECTION = "interj."
|
||||||
|
ABBREVIATION = "abbr."
|
||||||
|
COMBINATION = "comb."
|
||||||
|
SUFFIX = "suff." # 后缀
|
||||||
|
|
||||||
|
|
||||||
|
class StateType(BaseChoices):
|
||||||
|
REFUSED = "rf"
|
||||||
|
CHECKING = "ck"
|
||||||
|
PUBLISHED = "pb"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(list(StateType.choices()))
|
||||||
|
print(StateType.is_valid(1))
|
||||||
|
print(StateType.is_valid("refuse"))
|
||||||
|
print(StateType.is_valid("rf"))
|
|
@ -0,0 +1,17 @@
|
||||||
|
from normalutils.choices import BaseChoices
|
||||||
|
|
||||||
|
|
||||||
|
class HttpMethod(BaseChoices):
|
||||||
|
GET = "GET"
|
||||||
|
POST = "POST"
|
||||||
|
PUT = "PUT"
|
||||||
|
PATCH = "PATCH"
|
||||||
|
DELETE = "DELETE"
|
||||||
|
OPTIONS = "OPOTIONS"
|
||||||
|
|
||||||
|
|
||||||
|
class HtmlContentType(BaseChoices):
|
||||||
|
TEXT_PLAIN = "text/plain"
|
||||||
|
TEXT_HTML = "text/html"
|
||||||
|
TEXT_MARKDOWN = "text/markdown"
|
||||||
|
APPLICATION_JSON = "application/json"
|
|
@ -0,0 +1,79 @@
|
||||||
|
from typing import Callable, Optional
|
||||||
|
|
||||||
|
from fake_useragent import UserAgent
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from validator import Validator
|
||||||
|
import httpchoices
|
||||||
|
|
||||||
|
|
||||||
|
class NoValidator(Validator):
|
||||||
|
def is_valid(self, raise_error):
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Spider:
|
||||||
|
validator_class: Validator = NoValidator
|
||||||
|
parser: Callable[[str], dict] = None
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
url,
|
||||||
|
method="GET",
|
||||||
|
request_data: Optional[dict] = None,
|
||||||
|
params: Optional[dict] = None,
|
||||||
|
) -> None:
|
||||||
|
self.useragent = UserAgent()
|
||||||
|
self.headers = {"User-Agent": self.useragent.random}
|
||||||
|
self.__data = {}
|
||||||
|
self.__html = ""
|
||||||
|
self.url = url
|
||||||
|
self.method = method
|
||||||
|
self.has_verified = False
|
||||||
|
httpchoices.HttpMethod.is_valid(method, True)
|
||||||
|
self.__request_parameters = {
|
||||||
|
"data": request_data,
|
||||||
|
"params": params,
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_parser(self):
|
||||||
|
assert self.parser is not None
|
||||||
|
return self.__class__.parser
|
||||||
|
|
||||||
|
async def __get_html(self) -> str:
|
||||||
|
if self.__html != "":
|
||||||
|
return self.__html
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
try:
|
||||||
|
response = await client.request(
|
||||||
|
self.method,
|
||||||
|
self.url,
|
||||||
|
headers=self.headers,
|
||||||
|
**self.__request_parameters
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
self.__html = response.text
|
||||||
|
except httpx.HTTPStatusError:
|
||||||
|
pass
|
||||||
|
return self.__html
|
||||||
|
|
||||||
|
async def __get_data(self) -> dict:
|
||||||
|
if self.__data == {} or self.__data == []:
|
||||||
|
html = await self.__get_html()
|
||||||
|
self.__data = self.get_parser()(html)
|
||||||
|
return self.__data
|
||||||
|
|
||||||
|
async def is_valid(self, raise_exception=False) -> bool:
|
||||||
|
data = await self.__get_data()
|
||||||
|
validator_class = self.validator_class(data)
|
||||||
|
ans = validator_class.is_valid(raise_exception)
|
||||||
|
if ans:
|
||||||
|
self.has_verified = True
|
||||||
|
return ans
|
||||||
|
|
||||||
|
async def data(self) -> dict:
|
||||||
|
if self.has_verified:
|
||||||
|
return self.__data
|
||||||
|
else:
|
||||||
|
await self.is_valid(True)
|
||||||
|
return self.__data
|
|
@ -0,0 +1,21 @@
|
||||||
|
from markdown import markdown
|
||||||
|
import html
|
||||||
|
|
||||||
|
from normalutils.choices.htmlchoices import HtmlContentType
|
||||||
|
|
||||||
|
|
||||||
|
def content_to_html(content: str, content_type=HtmlContentType.TEXT_MARKDOWN, title=None):
|
||||||
|
if content_type == HtmlContentType.TEXT_MARKDOWN:
|
||||||
|
content = markdown(content)
|
||||||
|
return content
|
||||||
|
elif content_type == HtmlContentType.TEXT_PLAIN:
|
||||||
|
content = html.escape(content)
|
||||||
|
content = content.split('\n')
|
||||||
|
ret = ''
|
||||||
|
for sentence in content:
|
||||||
|
ret += ''.join(["<p>", sentence, "</p>\n"])
|
||||||
|
# add title
|
||||||
|
if title is not None:
|
||||||
|
title = html.escape(title)
|
||||||
|
return ''.join(['<h1>', title, '</h1>\n', ret])
|
||||||
|
return ret
|
|
@ -0,0 +1,72 @@
|
||||||
|
from typing import Callable
|
||||||
|
import random
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
def random_str(typename: str, randomlength: int = 16) -> Callable[[None], str]:
|
||||||
|
"""Parameter:
|
||||||
|
----------
|
||||||
|
type: 'common' [A-Za-z0-9]; 'lower' [a-z0-9]"""
|
||||||
|
common = "AaBbCcDdEeFfGgHhJjKkLlMmNnOoPpQqRrSsTtUuVvWwXxYyZz0123456789"
|
||||||
|
lower = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||||
|
|
||||||
|
if typename == 'common':
|
||||||
|
chars = common
|
||||||
|
elif typename == 'lower':
|
||||||
|
chars = lower
|
||||||
|
else:
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
def _do() -> str:
|
||||||
|
length = len(chars) - 1
|
||||||
|
ret = "".join([chars[random.randint(0, length)] for _ in range(randomlength)])
|
||||||
|
return ret
|
||||||
|
|
||||||
|
return _do
|
||||||
|
|
||||||
|
|
||||||
|
def create_random_unique_str(rand_func: Callable[[None], str]):
|
||||||
|
time_string = None
|
||||||
|
list_string = []
|
||||||
|
|
||||||
|
def create_random(rand_func: Callable[[None], str]):
|
||||||
|
timestr = timezone.now().timestamp()
|
||||||
|
timestr = str(int(timestr))
|
||||||
|
ranstr = rand_func()
|
||||||
|
ret = timestr + ranstr
|
||||||
|
return ret, timestr
|
||||||
|
|
||||||
|
def get_unique_str():
|
||||||
|
nonlocal time_string
|
||||||
|
nonlocal list_string
|
||||||
|
while True:
|
||||||
|
ret, timestr = create_random(rand_func)
|
||||||
|
if time_string != timestr:
|
||||||
|
time_string = timestr
|
||||||
|
list_string = [ret]
|
||||||
|
return ret
|
||||||
|
else:
|
||||||
|
if ret not in list_string:
|
||||||
|
list_string.append(ret)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def decrator_func(func):
|
||||||
|
@wraps(func)
|
||||||
|
def _do():
|
||||||
|
return get_unique_str()
|
||||||
|
return _do
|
||||||
|
|
||||||
|
return decrator_func
|
||||||
|
# return get_unique_str
|
||||||
|
|
||||||
|
|
||||||
|
@create_random_unique_str(random_str('common', 2))
|
||||||
|
def default_nickname() -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@create_random_unique_str(random_str('lower', 1))
|
||||||
|
def default_version_unique_id() -> str:
|
||||||
|
pass
|
|
@ -0,0 +1,14 @@
|
||||||
|
from time import time
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
|
||||||
|
def timeit(func):
|
||||||
|
@wraps(func)
|
||||||
|
def _totime(*args, **kwargs):
|
||||||
|
st = time()
|
||||||
|
ans = func(*args, **kwargs)
|
||||||
|
end = time()
|
||||||
|
print("'{}' use time: {}".format(func.__name__, end - st))
|
||||||
|
return ans
|
||||||
|
|
||||||
|
return _totime
|
|
@ -0,0 +1,13 @@
|
||||||
|
from validator import ValidationError
|
||||||
|
|
||||||
|
|
||||||
|
def validate_lenth(value: str, max_length: int, min_length: int = 4):
|
||||||
|
length = len(value)
|
||||||
|
if length > max_length:
|
||||||
|
raise ValidationError(
|
||||||
|
"Length is {}. It is longer than {}".format(length, max_length)
|
||||||
|
)
|
||||||
|
elif length < min_length:
|
||||||
|
raise ValidationError(
|
||||||
|
"Length is {}. It is shorter than {}".format(length, min_length)
|
||||||
|
)
|
|
@ -0,0 +1,56 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlalchemy import create_engine, Column
|
||||||
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import sessionmaker, relationship
|
||||||
|
from sqlalchemy.sql.schema import ForeignKey
|
||||||
|
from sqlalchemy.sql.sqltypes import Boolean, DateTime, Integer, String
|
||||||
|
|
||||||
|
from normalutils.choices import StateType
|
||||||
|
|
||||||
|
|
||||||
|
oengine = create_engine("sqlite:///db.sqlite3", future=True)
|
||||||
|
Base = declarative_base()
|
||||||
|
OSession = sessionmaker(oengine, future=True)
|
||||||
|
|
||||||
|
|
||||||
|
class OWord(Base):
|
||||||
|
__tablename__ = "word_word"
|
||||||
|
id = Column("word_id", Integer, primary_key=True)
|
||||||
|
spelling = Column(String(64), nullable=False, unique=True)
|
||||||
|
importance = Column(String(32), nullable=True)
|
||||||
|
state = Column(String(2), default=StateType.CHECKING.value, nullable=False)
|
||||||
|
|
||||||
|
|
||||||
|
class OMeaningField(Base):
|
||||||
|
__tablename__ = "meaning_meaningfield"
|
||||||
|
id = Column("meaningfield_id", Integer, primary_key=True)
|
||||||
|
current_version = Column(Integer, default=1)
|
||||||
|
has_many = Column(Boolean, default=False)
|
||||||
|
word_id = Column(Integer, ForeignKey("word_word.word_id", ondelete="CASCADE"))
|
||||||
|
|
||||||
|
word = relationship("OWord", backref="meaningfields")
|
||||||
|
|
||||||
|
|
||||||
|
class OMeaning(Base):
|
||||||
|
__tablename__ = "meaning_meaning"
|
||||||
|
id = Column("meaning_id", Integer, primary_key=True)
|
||||||
|
meaningfield_id = Column(
|
||||||
|
Integer, ForeignKey("meaning_meaningfield.meaningfield_id", ondelete="CASCADE")
|
||||||
|
)
|
||||||
|
author_id = Column(Integer, nullable=True)
|
||||||
|
author_name = Column(String(64), nullable=True)
|
||||||
|
state = Column(String(2), default=StateType.CHECKING.value, nullable=False)
|
||||||
|
word_property = Column(String(8), nullable=False)
|
||||||
|
field = Column(String(64), nullable=True)
|
||||||
|
version = Column(Integer, default=1, nullable=False)
|
||||||
|
meaning = Column(String(128), nullable=False)
|
||||||
|
sentence = Column(String(256), nullable=True)
|
||||||
|
add_time = Column(DateTime, default=datetime.utcnow)
|
||||||
|
|
||||||
|
meaningfield = relationship("OMeaningField", backref="meanings")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
with OSession() as session:
|
||||||
|
meaning = session.query(OMeaning).first()
|
||||||
|
print(meaning.meaningfield.word.id)
|
|
@ -0,0 +1,96 @@
|
||||||
|
import re
|
||||||
|
from wordpropertyconversion import word_property_conversion
|
||||||
|
from parsel import Selector
|
||||||
|
|
||||||
|
from models import Session, WordData
|
||||||
|
|
||||||
|
|
||||||
|
def renderer_word(txt) -> dict:
|
||||||
|
sel = Selector(txt)
|
||||||
|
# word
|
||||||
|
spelling = sel.css(".keyword::text").get()
|
||||||
|
importance = sel.xpath("//span[@class='via rank']/text()").get()
|
||||||
|
word = {
|
||||||
|
"spelling": spelling,
|
||||||
|
"importance": importance
|
||||||
|
}
|
||||||
|
return word
|
||||||
|
|
||||||
|
|
||||||
|
def renderer_meaningslist(txt) -> list:
|
||||||
|
# meanings
|
||||||
|
sel = Selector(txt)
|
||||||
|
sel.css("#synonyms").remove()
|
||||||
|
meanings_list = sel.css(".trans-container")
|
||||||
|
if meanings_list == []:
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
meanings_list = meanings_list[0].xpath("//div/ul/li/text()").getall()
|
||||||
|
# meanings_list = sel.xpath("//div[@class='trans-container'][1]").xpath("//div/ul/li/text()").getall()
|
||||||
|
meanings = map(renderer_meaning, meanings_list)
|
||||||
|
meanings = list(meanings)
|
||||||
|
while None in meanings:
|
||||||
|
meanings.remove(None)
|
||||||
|
return meanings
|
||||||
|
|
||||||
|
|
||||||
|
def renderer_meaning(text):
|
||||||
|
word_property = re.match(r"[a-z]{1,8}\.", text)
|
||||||
|
if word_property is None:
|
||||||
|
return None
|
||||||
|
word_property = word_property.group()
|
||||||
|
word_property = word_property_conversion(word_property)
|
||||||
|
length = len(word_property)
|
||||||
|
meaning = text[length+1:]
|
||||||
|
return {
|
||||||
|
"word_property": word_property,
|
||||||
|
"meaning": meaning
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def has_value_to_render(text):
|
||||||
|
sel = Selector(text)
|
||||||
|
return sel.css(".error-typo") == []
|
||||||
|
|
||||||
|
|
||||||
|
def testcase1():
|
||||||
|
with Session() as session:
|
||||||
|
# data = session.query(WordData).first()
|
||||||
|
data = session.query(WordData).filter_by(word="ob").first() # the
|
||||||
|
text = data.html
|
||||||
|
# print(parser_worddict(text))
|
||||||
|
# print(renderer_meaningslist(text))
|
||||||
|
# print(has_value_to_render(text))
|
||||||
|
# astr = "[ 过去式 researched 过去分词 researched 现在分词 researching ]"
|
||||||
|
astr = "linux下的桌面环境"
|
||||||
|
ans = renderer_meaning(astr)
|
||||||
|
print(ans)
|
||||||
|
|
||||||
|
|
||||||
|
def testcase3():
|
||||||
|
with Session() as session:
|
||||||
|
data = session.query(WordData).filter_by(word="search").one()
|
||||||
|
text = data.html
|
||||||
|
ans = renderer_meaningslist(text)
|
||||||
|
print(ans)
|
||||||
|
|
||||||
|
|
||||||
|
def testcase4():
|
||||||
|
"test word importance None"
|
||||||
|
with Session() as session:
|
||||||
|
data = session.query(WordData).filter_by(word="john").one()
|
||||||
|
text = data.html
|
||||||
|
ans = renderer_word(text)
|
||||||
|
print(ans)
|
||||||
|
print(type(ans["importance"]))
|
||||||
|
|
||||||
|
|
||||||
|
def testcase2():
|
||||||
|
txt = """
|
||||||
|
<div id="results-contents" class="results-content"><div class="trans-wrapper" id="phrsListTab"><h2 class="wordbook-js"><span class="keyword">hentai</span></h2></div><div id="wordArticle" class="trans-wrapper trans-tab"><h3><span class="tabs"></span></h3><div id="wordArticleToggle"></div></div></div>
|
||||||
|
"""
|
||||||
|
renderer_meaningslist(txt)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
testcase4()
|
|
@ -0,0 +1,71 @@
|
||||||
|
from validator import Validator, fields, FieldValidationError
|
||||||
|
from normalutils.choices import WordPropertyType
|
||||||
|
|
||||||
|
from models import Word, Meaning, Session
|
||||||
|
|
||||||
|
|
||||||
|
class WordAddSerializer(Validator):
|
||||||
|
spelling = fields.StringField(1, 64, allow_null=False)
|
||||||
|
importance = fields.StringField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, raw_data, session: Session, *args, **kwargs):
|
||||||
|
super().__init__(raw_data)
|
||||||
|
self.__session = session
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
assert self.errors == {}
|
||||||
|
return self.create(self.validated_data)
|
||||||
|
|
||||||
|
def create(self, data):
|
||||||
|
session = self.__session
|
||||||
|
word = Word(**data)
|
||||||
|
session.add(word)
|
||||||
|
return word
|
||||||
|
|
||||||
|
|
||||||
|
class MeaningAddSerializer(Validator):
|
||||||
|
meaning = fields.StringField()
|
||||||
|
word_property = fields.StringField()
|
||||||
|
sentence = fields.StringField(required=False)
|
||||||
|
|
||||||
|
def __init__(self, raw_data, session: Session, word, *args, **kwargs):
|
||||||
|
super().__init__(raw_data)
|
||||||
|
self.__session = session
|
||||||
|
self.__word = word
|
||||||
|
|
||||||
|
def validate_word_property(self, data):
|
||||||
|
try:
|
||||||
|
WordPropertyType.is_valid(data, True)
|
||||||
|
except Exception as e:
|
||||||
|
raise FieldValidationError(e)
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
assert self.errors == {}
|
||||||
|
return self.create(self.validated_data)
|
||||||
|
|
||||||
|
def create(self, data):
|
||||||
|
session = self.__session
|
||||||
|
meaning = Meaning(word=self.__word, **data)
|
||||||
|
session.add(meaning)
|
||||||
|
return meaning
|
||||||
|
|
||||||
|
|
||||||
|
def testcase1():
|
||||||
|
data = {
|
||||||
|
"spelling": "a",
|
||||||
|
"meaning": "haha",
|
||||||
|
"word_property": "n."
|
||||||
|
}
|
||||||
|
with Session() as session:
|
||||||
|
serializer = WordAddSerializer(data, session)
|
||||||
|
serializer.is_valid()
|
||||||
|
serializer.save()
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
data = {'spelling': "None", 'importance': None}
|
||||||
|
with Session() as session:
|
||||||
|
serializer = WordAddSerializer(data, session)
|
||||||
|
flag = serializer.is_valid()
|
||||||
|
print(serializer.validated_data)
|
|
@ -0,0 +1,5 @@
|
||||||
|
__version__ = '0.0.8'
|
||||||
|
|
||||||
|
from .validator import Validator, create_validator
|
||||||
|
from .fields import *
|
||||||
|
from .exceptions import *
|
|
@ -0,0 +1,69 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import six
|
||||||
|
from .utils import force_text, force_str
|
||||||
|
from .translation import gettext as _
|
||||||
|
|
||||||
|
|
||||||
|
def _flat_error_detail(detail):
|
||||||
|
if isinstance(detail, list):
|
||||||
|
return [_flat_error_detail(item) for item in detail]
|
||||||
|
elif isinstance(detail, dict):
|
||||||
|
return {
|
||||||
|
key: _flat_error_detail(value)
|
||||||
|
for key, value in six.iteritems(detail)
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return force_text(detail)
|
||||||
|
|
||||||
|
|
||||||
|
class BaseValidationError(Exception):
|
||||||
|
|
||||||
|
default_detail = _('Base validation error')
|
||||||
|
default_code = _('error')
|
||||||
|
|
||||||
|
def __init__(self, detail=None, code=None):
|
||||||
|
"""
|
||||||
|
:param detail: `detail` maybe a string, a dict or a list.
|
||||||
|
:param code: error code, it not used for now.
|
||||||
|
"""
|
||||||
|
if detail is None:
|
||||||
|
detail = self.default_detail
|
||||||
|
if code is None:
|
||||||
|
code = self.default_code
|
||||||
|
|
||||||
|
self.detail = _flat_error_detail(detail)
|
||||||
|
self.code = code
|
||||||
|
|
||||||
|
def get_detail(self):
|
||||||
|
return self.detail
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return force_str(self.detail)
|
||||||
|
|
||||||
|
def __unicode__(self):
|
||||||
|
return force_text(self.detail)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
detail = self.detail
|
||||||
|
if len(detail) > 103:
|
||||||
|
detail = detail[:100] + '...'
|
||||||
|
return '{0}(detail={1!r})'.format(self.__class__.__name__, detail)
|
||||||
|
|
||||||
|
|
||||||
|
class FieldRequiredError(BaseValidationError):
|
||||||
|
|
||||||
|
default_detail = _('Field is required')
|
||||||
|
default_code = _('error')
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(BaseValidationError):
|
||||||
|
|
||||||
|
default_detail = _('Validation error')
|
||||||
|
default_code = _('error')
|
||||||
|
|
||||||
|
|
||||||
|
class FieldValidationError(BaseValidationError):
|
||||||
|
|
||||||
|
default_detail = _('field Validation error')
|
||||||
|
default_code = _('error')
|
|
@ -0,0 +1,781 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import six
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
import re
|
||||||
|
import copy
|
||||||
|
import datetime
|
||||||
|
from collections import OrderedDict
|
||||||
|
from six.moves import urllib_parse as urlparse, range
|
||||||
|
from IPy import IP, MAX_IPV4_ADDRESS, MAX_IPV6_ADDRESS
|
||||||
|
from . import exceptions
|
||||||
|
from .utils import force_text
|
||||||
|
from .translation import gettext as _
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
# Don't need to add field to here by hand,
|
||||||
|
# BaseFieldMetaClass will auto add field to here.
|
||||||
|
]
|
||||||
|
|
||||||
|
FIELDS_NAME_MAP = {
|
||||||
|
# Don't need to add field to here by hand,
|
||||||
|
# BaseFieldMetaClass will auto add field to here.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def create_field(field_info):
|
||||||
|
"""
|
||||||
|
Create a field by field info dict.
|
||||||
|
"""
|
||||||
|
field_type = field_info.get('type')
|
||||||
|
if field_type not in FIELDS_NAME_MAP:
|
||||||
|
raise ValueError(_('not support this field: {}').format(field_type))
|
||||||
|
field_class = FIELDS_NAME_MAP.get(field_type)
|
||||||
|
params = dict(field_info)
|
||||||
|
params.pop('type')
|
||||||
|
return field_class.from_dict(params)
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyValue(object):
|
||||||
|
"""
|
||||||
|
a data type replace None
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return '__empty_value__'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<{}>'.format(self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
EMPTY_VALUE = EmptyValue()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseFieldMetaClass(type):
|
||||||
|
|
||||||
|
def __new__(cls, name, bases, attrs):
|
||||||
|
__all__.append(name)
|
||||||
|
clazz = super(BaseFieldMetaClass, cls).__new__(cls, name, bases, attrs)
|
||||||
|
field_name = attrs.get('FIELD_TYPE_NAME')
|
||||||
|
if field_name is not None and field_name != 'object':
|
||||||
|
FIELDS_NAME_MAP[field_name] = clazz
|
||||||
|
return clazz
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(BaseFieldMetaClass)
|
||||||
|
class BaseField(object):
|
||||||
|
"""
|
||||||
|
BaseField
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
INTERNAL_TYPE is the type of the field in python internal, like str, int, list, dict
|
||||||
|
INTERNAL_TYPE can be a type list, such as [int, long]
|
||||||
|
INTERNAL_TYPE used to validate field's type by isinstance(value, INTERNAL_TYPE)
|
||||||
|
"""
|
||||||
|
INTERNAL_TYPE = object
|
||||||
|
|
||||||
|
FIELD_TYPE_NAME = 'object'
|
||||||
|
|
||||||
|
PARAMS = [
|
||||||
|
'strict', 'default', 'validators', 'required'
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, strict=True, default=EMPTY_VALUE, validators=None, required=True, allow_null=False, **kwargs):
|
||||||
|
"""
|
||||||
|
:param strict: bool, if strict is True, value must be an instance of INTERVAL_TYPE,
|
||||||
|
otherwise, value should be convert to INTERNAL_TYPE
|
||||||
|
:param default: default value, defaults to EMPTY_VALUE
|
||||||
|
:param validators: a validator list, validator can be function, other callable object or object that have method named validate
|
||||||
|
:param required: bool, indicate that this field is whether required
|
||||||
|
"""
|
||||||
|
self.strict = strict
|
||||||
|
self.default = default
|
||||||
|
self.allow_null = allow_null
|
||||||
|
|
||||||
|
if validators is None:
|
||||||
|
validators = []
|
||||||
|
elif not isinstance(validators, (tuple, list)):
|
||||||
|
validators = [validators]
|
||||||
|
self.validators = validators
|
||||||
|
|
||||||
|
self.required = required
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _check_value_range(cls, min_value, max_value):
|
||||||
|
if max_value is not None and max_value < min_value:
|
||||||
|
raise ValueError(_('the max value must greater than or equals the min value, got min value={min}, max value={max}').format(
|
||||||
|
min=min_value, max=max_value))
|
||||||
|
|
||||||
|
def _convert_type(self, value):
|
||||||
|
if isinstance(self.INTERNAL_TYPE, (tuple, list)):
|
||||||
|
for t in self.INTERNAL_TYPE:
|
||||||
|
try:
|
||||||
|
value = t(value)
|
||||||
|
break
|
||||||
|
except TypeError as e:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
else:
|
||||||
|
value = self.INTERNAL_TYPE(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_all_params(cls):
|
||||||
|
"""
|
||||||
|
Collect all PARAMS from this class and its parent class.
|
||||||
|
"""
|
||||||
|
params = list(cls.PARAMS)
|
||||||
|
bases = cls.__bases__
|
||||||
|
for base in bases:
|
||||||
|
if issubclass(base, BaseField):
|
||||||
|
params.extend(base._get_all_params())
|
||||||
|
return params
|
||||||
|
|
||||||
|
def validate(self, value):
|
||||||
|
"""
|
||||||
|
return validated value or raise FieldValidationError.
|
||||||
|
"""
|
||||||
|
if not self.required:
|
||||||
|
return value
|
||||||
|
if not self.allow_null and value is None:
|
||||||
|
raise exceptions.FieldValidationError(_("value can't be 'null'."))
|
||||||
|
|
||||||
|
value = self._validate(value)
|
||||||
|
for v in self.validators:
|
||||||
|
v(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
"""
|
||||||
|
return validated value or raise FieldValidationError.
|
||||||
|
sub-class should override this method.
|
||||||
|
"""
|
||||||
|
return self._validate_type(value)
|
||||||
|
|
||||||
|
def _validate_type(self, value):
|
||||||
|
"""
|
||||||
|
validate the type of value
|
||||||
|
"""
|
||||||
|
if not isinstance(value, self.INTERNAL_TYPE):
|
||||||
|
if self.strict:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('got a wrong type: {0}, expect {1}').format(type(value).__name__, self.FIELD_TYPE_NAME))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
value = self._convert_type(value)
|
||||||
|
except (ValueError, TypeError) as e:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('type convertion({0} -> {1}) is failed: {2}').format(type(value).__name__, self.FIELD_TYPE_NAME, str(e)))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def is_required(self):
|
||||||
|
return self.required
|
||||||
|
|
||||||
|
def get_default(self):
|
||||||
|
"""
|
||||||
|
return default value
|
||||||
|
"""
|
||||||
|
if callable(self.default):
|
||||||
|
return self.default()
|
||||||
|
else:
|
||||||
|
return self.default
|
||||||
|
|
||||||
|
def to_presentation(self, value):
|
||||||
|
"""
|
||||||
|
value: must be a internal value
|
||||||
|
"""
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_internal(self, value):
|
||||||
|
"""
|
||||||
|
value: must be a validated value
|
||||||
|
"""
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
to dict presentation
|
||||||
|
"""
|
||||||
|
d = {
|
||||||
|
'type': self.FIELD_TYPE_NAME,
|
||||||
|
}
|
||||||
|
params = self._get_all_params()
|
||||||
|
for name in params:
|
||||||
|
if hasattr(self, name):
|
||||||
|
value = getattr(self, name)
|
||||||
|
# 处理特殊值
|
||||||
|
if value is EMPTY_VALUE:
|
||||||
|
value = '__empty__'
|
||||||
|
d[name] = value
|
||||||
|
return d
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, params):
|
||||||
|
"""
|
||||||
|
Create a field from params.
|
||||||
|
sub-class can override this method.
|
||||||
|
"""
|
||||||
|
if params.get('default') == '__empty__':
|
||||||
|
params['default'] = EMPTY_VALUE
|
||||||
|
return cls(**params)
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
"""
|
||||||
|
reutrn mocking data
|
||||||
|
sub-class should override this method
|
||||||
|
"""
|
||||||
|
return 'this field doesnt implement mock_data method'
|
||||||
|
|
||||||
|
|
||||||
|
class StringField(BaseField):
|
||||||
|
"""
|
||||||
|
StringField
|
||||||
|
internal: six.string_types
|
||||||
|
presentation: string
|
||||||
|
"""
|
||||||
|
if six.PY2:
|
||||||
|
INTERNAL_TYPE = (unicode, str)
|
||||||
|
else:
|
||||||
|
INTERNAL_TYPE = str
|
||||||
|
FIELD_TYPE_NAME = 'string'
|
||||||
|
PARAMS = ['min_length', 'max_length', 'regex']
|
||||||
|
|
||||||
|
def __init__(self, min_length=0, max_length=None, regex=None, **kwargs):
|
||||||
|
if min_length < 0:
|
||||||
|
min_length = 0
|
||||||
|
self._check_value_range(min_length, max_length)
|
||||||
|
self.min_length = min_length
|
||||||
|
self.max_length = max_length
|
||||||
|
|
||||||
|
if isinstance(regex, six.string_types):
|
||||||
|
regex = re.compile(regex)
|
||||||
|
self.regex = regex
|
||||||
|
|
||||||
|
super(StringField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
value = self._validate_type(value)
|
||||||
|
|
||||||
|
if len(value) < self.min_length:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('string is too short, min-length is {}').format(self.min_length))
|
||||||
|
if self.max_length and len(value) > self.max_length:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('string is too long, max-length is {}').format(self.max_length))
|
||||||
|
|
||||||
|
if not self._match(value):
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('{0} not match {1}').format(self.regex.pattern, value))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _match(self, value):
|
||||||
|
if self.regex is None:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return self.regex.match(value) is not None
|
||||||
|
|
||||||
|
def to_internal(self, value):
|
||||||
|
if value is None:
|
||||||
|
return value
|
||||||
|
return six.text_type(value)
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
min_ = self.min_length
|
||||||
|
max_ = self.max_length
|
||||||
|
if max_ is None:
|
||||||
|
max_ = min_ + 100
|
||||||
|
size = random.randint(min_, max_)
|
||||||
|
random_str = ''.join(
|
||||||
|
[random.choice(string.ascii_letters + string.digits) for _ in range(size)])
|
||||||
|
random_str = self.to_internal(random_str)
|
||||||
|
return random_str
|
||||||
|
|
||||||
|
|
||||||
|
class NumberField(BaseField):
|
||||||
|
if six.PY2:
|
||||||
|
INTERNAL_TYPE = (int, long, float)
|
||||||
|
else:
|
||||||
|
INTERNAL_TYPE = (int, float)
|
||||||
|
FIELD_TYPE_NAME = 'number'
|
||||||
|
PARAMS = ['min_value', 'max_value']
|
||||||
|
|
||||||
|
def __init__(self, min_value=None, max_value=None, **kwargs):
|
||||||
|
self._check_value_range(min_value, max_value)
|
||||||
|
self.min_value = min_value
|
||||||
|
self.max_value = max_value
|
||||||
|
|
||||||
|
super(NumberField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
value = self._validate_type(value)
|
||||||
|
|
||||||
|
if self.min_value is not None and value < self.min_value:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('value is too small, min-value is {}').format(self.min_value))
|
||||||
|
|
||||||
|
if self.max_value is not None and value > self.max_value:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('value is too big, max-value is {}').format(self.max_value))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
min_ = self.min_value
|
||||||
|
if min_ is None:
|
||||||
|
min_ = 0
|
||||||
|
max_ = self.max_value
|
||||||
|
if max_ is None:
|
||||||
|
max_ = min_ + 1000
|
||||||
|
return random.uniform(min_, max_)
|
||||||
|
|
||||||
|
|
||||||
|
class IntegerField(NumberField):
|
||||||
|
INTERNAL_TYPE = int
|
||||||
|
FIELD_TYPE_NAME = 'integer'
|
||||||
|
PARAMS = []
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
d = super(IntegerField, self).mock_data()
|
||||||
|
return int(d)
|
||||||
|
|
||||||
|
|
||||||
|
class FloatField(NumberField):
|
||||||
|
INTERNAL_TYPE = float
|
||||||
|
FIELD_TYPE_NAME = 'float'
|
||||||
|
PARAMS = []
|
||||||
|
|
||||||
|
|
||||||
|
class BoolField(BaseField):
|
||||||
|
INTERNAL_TYPE = bool
|
||||||
|
FIELD_TYPE_NAME = 'bool'
|
||||||
|
PARAMS = []
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
return random.choice([True, False])
|
||||||
|
|
||||||
|
|
||||||
|
class UUIDField(BaseField):
|
||||||
|
INTERNAL_TYPE = uuid.UUID
|
||||||
|
FIELD_TYPE_NAME = 'UUID'
|
||||||
|
PARAMS = ['format']
|
||||||
|
SUPPORT_FORMATS = {
|
||||||
|
'hex': 'hex',
|
||||||
|
'str': '__str__',
|
||||||
|
'int': 'int',
|
||||||
|
'bytes': 'bytes',
|
||||||
|
'bytes_le': 'bytes_le'
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, format='hex', **kwargs):
|
||||||
|
"""
|
||||||
|
format: what format used when to_presentation, supports 'hex', 'str', 'int', 'bytes', 'bytes_le'
|
||||||
|
"""
|
||||||
|
if format not in self.SUPPORT_FORMATS:
|
||||||
|
raise ValueError(_('not supports format: {}').format(format))
|
||||||
|
self.format = format
|
||||||
|
|
||||||
|
kwargs.setdefault('strict', False)
|
||||||
|
super(UUIDField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
value = self._validate_type(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_presentation(self, value):
|
||||||
|
assert isinstance(value, self.INTERNAL_TYPE)
|
||||||
|
attr = getattr(value, self.SUPPORT_FORMATS[self.format])
|
||||||
|
if callable(attr):
|
||||||
|
return attr()
|
||||||
|
return attr
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
return uuid.uuid4()
|
||||||
|
|
||||||
|
|
||||||
|
class MD5Field(StringField):
|
||||||
|
FIELD_TYPE_NAME = 'md5'
|
||||||
|
PARAMS = []
|
||||||
|
REGEX = r'[\da-fA-F]{32}'
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs['strict'] = True
|
||||||
|
super(MD5Field, self).__init__(min_length=32,
|
||||||
|
max_length=32,
|
||||||
|
regex=self.REGEX,
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
try:
|
||||||
|
return super(MD5Field, self)._validate(value)
|
||||||
|
except exceptions.FieldValidationError as e:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('Got wrong md5 value: {}').format(value))
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
return ''.join([random.choice(string.hexdigits) for i in range(32)])
|
||||||
|
|
||||||
|
|
||||||
|
class SHAField(StringField):
|
||||||
|
FIELD_TYPE_NAME = 'sha'
|
||||||
|
SUPPORT_VERSION = [1, 224, 256, 384, 512]
|
||||||
|
PARAMS = ['version']
|
||||||
|
|
||||||
|
def __init__(self, version=256, **kwargs):
|
||||||
|
if version not in self.SUPPORT_VERSION:
|
||||||
|
raise ValueError(_('{0} not support, support versions are: {1}').format(
|
||||||
|
version, self.SUPPORT_VERSION))
|
||||||
|
if version == 1:
|
||||||
|
length = 40
|
||||||
|
else:
|
||||||
|
length = int(version / 8 * 2)
|
||||||
|
self.version = version
|
||||||
|
self.length = length
|
||||||
|
kwargs['strict'] = True
|
||||||
|
super(SHAField, self).__init__(min_length=length,
|
||||||
|
max_length=length,
|
||||||
|
regex=r'[\da-fA-F]{' +
|
||||||
|
str(length) + '}',
|
||||||
|
**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
try:
|
||||||
|
return super(SHAField, self)._validate(value)
|
||||||
|
except exceptions.FieldValidationError as e:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('Got wrong sha{0} value: {1}').format(self.version, value))
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
return ''.join([random.choice(string.hexdigits) for i in range(self.length)])
|
||||||
|
|
||||||
|
|
||||||
|
class EmailField(StringField):
|
||||||
|
FIELD_TYPE_NAME = 'email'
|
||||||
|
REGEX = r'^[a-zA-Z0-9.!#$%&\'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
|
||||||
|
PARAMS = []
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs['strict'] = True
|
||||||
|
super(EmailField, self).__init__(regex=self.REGEX, **kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
try:
|
||||||
|
return super(EmailField, self)._validate(value)
|
||||||
|
except exceptions.FieldValidationError as e:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('Got wrong email value: {}').format(value))
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
name = ''.join(random.sample(string.ascii_lowercase, 5))
|
||||||
|
domain = '{0}.com'.format(
|
||||||
|
''.join(random.sample(string.ascii_lowercase, 3)))
|
||||||
|
return '{0}@{1}'.format(name, domain)
|
||||||
|
|
||||||
|
|
||||||
|
class IPAddressField(BaseField):
|
||||||
|
INTERNAL_TYPE = IP
|
||||||
|
FIELD_TYPE_NAME = 'ip_address'
|
||||||
|
PARAMS = ['version']
|
||||||
|
SUPPORT_VERSIONS = ['ipv4', 'ipv6', 'both']
|
||||||
|
|
||||||
|
def __init__(self, version='both', **kwargs):
|
||||||
|
if version not in self.SUPPORT_VERSIONS:
|
||||||
|
raise ValueError(_('{} version is not supported').format(version))
|
||||||
|
self.version = version
|
||||||
|
|
||||||
|
kwargs.setdefault('strict', False)
|
||||||
|
super(IPAddressField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
try:
|
||||||
|
value = IP(value)
|
||||||
|
except ValueError as e:
|
||||||
|
raise exceptions.FieldValidationError(str(e))
|
||||||
|
if self.version == 'ipv4' and value.version() != 4:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('expected an ipv4 address, got {}').format(value.strNormal()))
|
||||||
|
if self.version == 'ipv6' and value.version() != 6:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
-('expected an ipv6 address, got {}').format(value.strNormal()))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_presentation(self, value):
|
||||||
|
return value.strNormal()
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
v = self.version
|
||||||
|
if v == 'both':
|
||||||
|
v = random.choice(['ipv4', 'ipv6'])
|
||||||
|
|
||||||
|
if v == 'ipv4':
|
||||||
|
ip = random.randint(0, MAX_IPV4_ADDRESS)
|
||||||
|
return IP(ip)
|
||||||
|
else:
|
||||||
|
ip = random.randint(0, MAX_IPV6_ADDRESS)
|
||||||
|
return IP(ip)
|
||||||
|
|
||||||
|
|
||||||
|
class URLField(StringField):
|
||||||
|
FIELD_TYPE_NAME = 'url'
|
||||||
|
PARAMS = []
|
||||||
|
SCHEMAS = ('http', 'https')
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
kwargs['strict'] = True
|
||||||
|
super(URLField, self).__init__(min_length=0, **kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
value = self._validate_type(value)
|
||||||
|
url = urlparse.urlparse(value)
|
||||||
|
if url.scheme not in self.SCHEMAS:
|
||||||
|
raise exceptions.FieldValidationError(_('schema is lost'))
|
||||||
|
if url.hostname == '':
|
||||||
|
raise exceptions.FieldValidationError(_('hostname is lost'))
|
||||||
|
return url.geturl()
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
return 'http://www.example.com/media/image/demo.jpg'
|
||||||
|
|
||||||
|
|
||||||
|
class EnumField(BaseField):
|
||||||
|
INTERNAL_TYPE = object
|
||||||
|
FIELD_TYPE_NAME = 'enum'
|
||||||
|
PARAMS = ['choices']
|
||||||
|
|
||||||
|
def __init__(self, choices=None, **kwargs):
|
||||||
|
if choices is None or len(choices) == 0:
|
||||||
|
raise ValueError('choices cant be empty or None')
|
||||||
|
self.choices = choices
|
||||||
|
|
||||||
|
super(EnumField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
if value not in self.choices:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('{!r} not in the choices').format(value))
|
||||||
|
return value
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
return random.choice(self.choices)
|
||||||
|
|
||||||
|
|
||||||
|
class DictField(BaseField):
|
||||||
|
INTERNAL_TYPE = dict
|
||||||
|
FIELD_TYPE_NAME = 'dict'
|
||||||
|
PARAMS = ['validator']
|
||||||
|
|
||||||
|
def __init__(self, validator=None, **kwargs):
|
||||||
|
"""
|
||||||
|
:param validator: Validator object
|
||||||
|
"""
|
||||||
|
self.validator = validator
|
||||||
|
super(DictField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
value = self._validate_type(value)
|
||||||
|
|
||||||
|
if self.validator:
|
||||||
|
v = self.validator(value)
|
||||||
|
if v.is_valid():
|
||||||
|
value = v.validated_data
|
||||||
|
else:
|
||||||
|
raise exceptions.FieldValidationError(v.errors)
|
||||||
|
else:
|
||||||
|
value = copy.deepcopy(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
d = super(DictField, self).to_dict()
|
||||||
|
if d['validator'] is not None:
|
||||||
|
d['validator'] = d['validator'].to_dict()
|
||||||
|
return d
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
if self.validator:
|
||||||
|
return self.validator.mock_data()
|
||||||
|
else:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class ListField(BaseField):
|
||||||
|
INTERNAL_TYPE = (list, tuple)
|
||||||
|
FIELD_TYPE_NAME = 'list'
|
||||||
|
PARAMS = ['field', 'min_length', 'max_length']
|
||||||
|
|
||||||
|
def __init__(self, field=None, min_length=0, max_length=None, **kwargs):
|
||||||
|
if field is not None and not isinstance(field, BaseField):
|
||||||
|
raise ValueError(
|
||||||
|
_('field param expect a instance of BaseField, but got {!r}').format(field))
|
||||||
|
self.field = field
|
||||||
|
|
||||||
|
self._check_value_range(min_length, max_length)
|
||||||
|
self.min_length = min_length
|
||||||
|
self.max_length = max_length
|
||||||
|
|
||||||
|
super(ListField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
value = self._validate_type(value)
|
||||||
|
if self.min_length is not None and len(value) < self.min_length:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('this list has too few elements, min length is {}').format(self.min_length))
|
||||||
|
|
||||||
|
if self.max_length is not None and len(value) > self.max_length:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('this list has too many elements, max length is {}').format(self.max_length))
|
||||||
|
|
||||||
|
if self.field:
|
||||||
|
new_value = []
|
||||||
|
for item in value:
|
||||||
|
new_item = self.field.validate(item)
|
||||||
|
new_value.append(new_item)
|
||||||
|
value = new_value
|
||||||
|
else:
|
||||||
|
value = copy.deepcopy(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
d = super(ListField, self).to_dict()
|
||||||
|
if d['field'] is not None:
|
||||||
|
d['field'] = d['field'].to_dict()
|
||||||
|
return d
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, params):
|
||||||
|
if 'field' in params and isinstance(params['field'], dict):
|
||||||
|
params['field'] = create_field(params['field'])
|
||||||
|
return super(ListField, cls).from_dict(params)
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
min_ = self.min_length
|
||||||
|
if min_ is None:
|
||||||
|
min_ = 0
|
||||||
|
max_ = self.max_length
|
||||||
|
if max_ is None:
|
||||||
|
max_ = 10
|
||||||
|
length = random.choice(range(min_, max_))
|
||||||
|
|
||||||
|
data = [None] * length
|
||||||
|
if self.field:
|
||||||
|
for i in range(length):
|
||||||
|
data[i] = self.field.mock_data()
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class TimestampField(IntegerField):
|
||||||
|
FIELD_TYPE_NAME = 'timestamp'
|
||||||
|
PARAMS = []
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
super(TimestampField, self).__init__(
|
||||||
|
min_value=0, max_value=2 ** 32 - 1, **kwargs)
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
try:
|
||||||
|
return super(TimestampField, self)._validate(value)
|
||||||
|
except exceptions.FieldValidationError as e:
|
||||||
|
raise exceptions.FieldValidationError(
|
||||||
|
_('Got wrong timestamp: {}').format(value))
|
||||||
|
|
||||||
|
|
||||||
|
class DatetimeField(BaseField):
|
||||||
|
INTERNAL_TYPE = datetime.datetime
|
||||||
|
FIELD_TYPE_NAME = 'datetime'
|
||||||
|
PARAMS = ['dt_format', 'tzinfo']
|
||||||
|
DEFAULT_FORMAT = '%Y/%m/%d %H:%M:%S'
|
||||||
|
|
||||||
|
def __init__(self, dt_format=None, tzinfo=None, **kwargs):
|
||||||
|
if dt_format is None:
|
||||||
|
dt_format = self.DEFAULT_FORMAT
|
||||||
|
self.dt_format = dt_format
|
||||||
|
if isinstance(tzinfo, six.string_types):
|
||||||
|
try:
|
||||||
|
import pytz
|
||||||
|
except ImportError as e:
|
||||||
|
raise ValueError(
|
||||||
|
_('Cant create DatetimeField instance with tzinfo {}, please install pytz and try again').format(params['tzinfo']))
|
||||||
|
tzinfo = pytz.timezone(tzinfo)
|
||||||
|
self.tzinfo = tzinfo
|
||||||
|
kwargs.setdefault('strict', False)
|
||||||
|
super(DatetimeField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _convert_type(self, value):
|
||||||
|
# override
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
if value.isdigit():
|
||||||
|
value = int(value)
|
||||||
|
return self.INTERNAL_TYPE.fromtimestamp(value, tz=self.tzinfo)
|
||||||
|
else:
|
||||||
|
dt = self.INTERNAL_TYPE.strptime(value, self.dt_format)
|
||||||
|
if self.tzinfo:
|
||||||
|
dt = dt.replace(tzinfo=self.tzinfo)
|
||||||
|
return dt
|
||||||
|
elif isinstance(value, six.integer_types):
|
||||||
|
return self.INTERNAL_TYPE.fromtimestamp(value, tz=self.tzinfo)
|
||||||
|
else:
|
||||||
|
raise ValueError(_('Got wrong datetime value: {}').format(value))
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
value = self._validate_type(value)
|
||||||
|
return copy.copy(value)
|
||||||
|
|
||||||
|
def to_presentation(self, value):
|
||||||
|
return value.strftime(self.dt_format)
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
d = super(DatetimeField, self).to_dict()
|
||||||
|
if d['tzinfo'] is not None:
|
||||||
|
d['tzinfo'] = force_text(d['tzinfo'])
|
||||||
|
return d
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
return self.INTERNAL_TYPE.fromtimestamp(random.randint(0, 2 ** 32 - 1))
|
||||||
|
|
||||||
|
|
||||||
|
class DateField(BaseField):
|
||||||
|
INTERNAL_TYPE = datetime.date
|
||||||
|
FIELD_TYPE_NAME = 'date'
|
||||||
|
PARAMS = ['dt_format']
|
||||||
|
DEFAULT_FORMAT = '%Y/%m/%d'
|
||||||
|
|
||||||
|
def __init__(self, dt_format=None, **kwargs):
|
||||||
|
if dt_format is None:
|
||||||
|
dt_format = self.DEFAULT_FORMAT
|
||||||
|
self.dt_format = dt_format
|
||||||
|
kwargs.setdefault('strict', False)
|
||||||
|
super(DateField, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def _convert_type(self, value):
|
||||||
|
# override
|
||||||
|
if isinstance(value, six.string_types):
|
||||||
|
if value.isdigit():
|
||||||
|
value = int(value)
|
||||||
|
return self.INTERNAL_TYPE.fromtimestamp(value)
|
||||||
|
else:
|
||||||
|
dt = datetime.datetime.strptime(value, self.dt_format)
|
||||||
|
return dt.date()
|
||||||
|
elif isinstance(value, six.integer_types):
|
||||||
|
return self.INTERNAL_TYPE.fromtimestamp(value)
|
||||||
|
else:
|
||||||
|
raise ValueError()
|
||||||
|
|
||||||
|
def _validate(self, value):
|
||||||
|
value = self._validate_type(value)
|
||||||
|
return copy.copy(value)
|
||||||
|
|
||||||
|
def to_presentation(self, value):
|
||||||
|
return value.strftime(self.dt_format)
|
||||||
|
|
||||||
|
def mock_data(self):
|
||||||
|
return self.INTERNAL_TYPE.fromtimestamp(random.randint(0, 2 ** 32 - 1))
|