This commit is contained in:
fanyq 2022-10-22 11:01:52 +08:00
commit 200117b921
105 changed files with 26944 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
*/__pycache__
*.pyc
*.po
*.mo
*.log
.DS_Store
test

8
Makefile Normal file
View File

@ -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

31
docker-compose.yaml Normal file
View File

@ -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

BIN
online_version.sqlite3 Normal file

Binary file not shown.

6
osdict_front/Dockerfile Normal file
View File

@ -0,0 +1,6 @@
FROM nginx:stable
COPY ./*.conf /etc/nginx/conf.d
COPY . /var/local/web/
CMD [ "nginx", "-g", "daemon off;" ]

17
osdict_front/Makefile Normal file
View File

@ -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

View File

@ -0,0 +1 @@
.localhistory-container{border:1px solid #ccc;border-radius:4px;background:#fff}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Binary file not shown.

17
osdict_front/osdict.conf Normal file
View File

@ -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;
}
}

4
osdictvue/.eslintignore Normal file
View File

@ -0,0 +1,4 @@
/build/
/config/
/dist/
/*.js

29
osdictvue/.eslintrc.js Normal file
View File

@ -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'
}
}

23
osdictvue/.gitignore vendored Normal file
View File

@ -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?

24
osdictvue/README.md Normal file
View File

@ -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/).

View File

@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

41
osdictvue/build/build.js Normal file
View File

@ -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'
))
})
})

View File

@ -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)
}
}

BIN
osdictvue/build/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

101
osdictvue/build/utils.js Normal file
View File

@ -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')
})
}
}

View File

@ -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'
}
}

View File

@ -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'
}
}

View File

@ -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)
}
})
})

View File

@ -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

12062
osdictvue/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

46
osdictvue/package.json Normal file
View File

@ -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"
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -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>

15
osdictvue/src/App.vue Normal file
View File

@ -0,0 +1,15 @@
<template>
<background>
<router-view />
</background>
</template>
<script>
import Background from './components/Background.vue'
export default {
name: 'App',
components: {
Background
}
}
</script>

26
osdictvue/src/api.js Normal file
View File

@ -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

BIN
osdictvue/src/assets/g1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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: '后缀' }
]

View File

@ -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}

View File

@ -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>

View File

@ -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">&times;</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>

View File

@ -0,0 +1,12 @@
<template>
<login-form> </login-form>
</template>
<script>
import LoginForm from './LoginForm'
export default {
components: { LoginForm: LoginForm }
}
</script>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">&times;</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>

View File

@ -0,0 +1,14 @@
<template>
<passwordreset-form></passwordreset-form>
</template>
<script>
import PasswordresetForm from './PasswordresetForm'
export default {
components: {
PasswordresetForm
},
data: function () {
return {}
}
}
</script>

View File

@ -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">&times;</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>

View File

@ -0,0 +1,14 @@
<template>
<registration-form></registration-form>
</template>
<script>
import RegistrationForm from './RegistrationForm'
export default {
components: {
RegistrationForm
},
data: function () {
return {}
}
}
</script>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

26
osdictvue/src/main.js Normal file
View File

@ -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')

View File

@ -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
}
]
})

View File

@ -0,0 +1,7 @@
export default function (dict) {
let answer = ''
for (var key in dict) {
answer = answer + key + ':' + String(dict[key]) + '\n'
}
return answer
}

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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()

File diff suppressed because it is too large Load Diff

10
spider/httpchoices.py Normal file
View File

@ -0,0 +1,10 @@
from normalutils.choices import BaseChoices
class HttpMethod(BaseChoices):
GET = "GET"
POST = "POST"
PUT = "PUT"
PATCH = "PATCH"
DELETE = "DELETE"
OPTIONS = "OPOTIONS"

86
spider/models.py Normal file
View File

@ -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()

View File

View File

@ -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"))

View File

@ -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"

View File

@ -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

View File

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
)

56
spider/omodels.py Normal file
View File

@ -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)

View File

@ -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()

71
spider/serializers.py Normal file
View File

@ -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)

BIN
spider/spider.sqlite3 Normal file

Binary file not shown.

View File

@ -0,0 +1,5 @@
__version__ = '0.0.8'
from .validator import Validator, create_validator
from .fields import *
from .exceptions import *

View File

@ -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')

781
spider/validator/fields.py Normal file
View File

@ -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))

Some files were not shown because too many files have changed in this diff Show More