no message

This commit is contained in:
Calvin 2023-06-22 12:14:08 +08:00
parent 3cf2db8692
commit 9456045db4
253 changed files with 0 additions and 17054 deletions

View File

@ -1,200 +0,0 @@
### Download the model and place it in the models directory
- Link 1: https://github.com/mymagicpower/AIAS/releases/download/apps/CLIP-ViT-B-32-IMAGE.zip
- Link 2: https://github.com/mymagicpower/AIAS/releases/download/apps/M-BERT-Base-ViT-B.zip
### Cross-modal similarity comparison and retrieval for images and text [supports 40 languages]
This example demonstrates the ability to search for images through text (the model itself also supports searching for text through images, or mixed search).
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/arc.png)
### Main Features
- Uses feature vector similarity search at the bottom level
- Millisecond-level search for billions of data on a single server
- Near-real-time search, supports distributed deployment
- Can insert, delete, search, update, and other operations on data at any time
### Background introduction
OpenAI has released two new neural networks: CLIP and DALL·E. They combine NLP (natural language recognition) with image recognition and have a better understanding of images and language in daily life.
Previously, text was used to search for text, and images were used to search for images. Now, with the CLIP model, text can be used to search for images, and images can be used to search for text. The implementation idea is to map images and text to the same vector space. In this way, cross-modal similarity comparison and retrieval of images and text can be realized.
- Feature vector space (composed of images and text)
![img](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/nlp_sdks/clip_Imagesearch.png)
### CLIP - "Alternative" Image Recognition
Currently, most models learn to recognize images from labeled examples in labeled datasets, while CLIP learns to recognize images and their descriptions obtained from the Internet, thus understanding images through a description rather than word labels such as "cat" and "dog."
To do this, CLIP learns to associate a large number of objects with their names and descriptions, and can thus recognize objects outside the training set.
![img](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/nlp_sdks/clip.png)
As shown in the figure above, the CLIP network workflow: pre-trains the image encoder and text encoder to predict which images are paired with which text in the dataset.
Then, CLIP is converted to a zero-shot classifier. In addition, all classifications in the dataset are converted into labels such as "a photo of a dog" and the best match image is predicted.
CLIP model address:
https://github.com/openai/CLIP/blob/main/README.md
#### Supported language list:
![img](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/languages.png)
### 1. Front-end deployment
### 1.1 Run directly:
```bash
npm run dev
```
#### 1.2 Build the dist installation package:
```bash
npm run build:prod
```
#### 1.3 Nginx deployment and operation (mac environment as an example):
```bash
cd /usr/local/etc/nginx/
vi /usr/local/etc/nginx/nginx.conf
# edit nginx.conf
server {
listen 8080;
server_name localhost;
location / {
root /Users/calvin/Documents/image_text_search/dist/;
index index.html index.htm;
}
......
# Reload the configuration:
sudo nginx -s reload
# After deploying the application, restart it:
cd /usr/local/Cellar/nginx/1.19.6/bin
# Fast stop
sudo nginx -s stop
# Start
sudo nginx
```
### 2. Back-end jar deployment
### 2.1 Environment requirements:
- System JDK 1.8+
- application.yml
1). Edit the image upload root path rootPath according to your needs
```bash
# 文件存储路径
file:
mac:
path: ~/file/
# folder for unzip files
rootPath: ~/file/data_root/
linux:
path: /home/aias/file/
rootPath: /home/aias/file/data_root/
windows:
path: file:/D:/aias/file/
rootPath: file:/D:/aias/file/data_root/
...
```
2). Edit the image baseurl according to your needs
```bash
image:
#baseurl is the address prefix of the image
baseurl: <http://127.0.0.1:8089/files/>
```
#### 2.2 Running the program:
```bash
# run the program
java -jar image-text-search-0.1.0.jar
```
### 3. Backend vector engine deployment
#### 3.1 Environmental requirements:
-Need to install docker operating environment, Mac environment can use Docker Desktop
#### 3.2 Pull Milvus vector engine image (used to calculate feature vector similarity)
Download the milvus-standalone-docker-compose.yml configuration file and save it as docker-compose.yml
[Standalone Installation Document](https://milvus.io/docs/install_standalone-docker.md)
[Configure Milvus](https://milvus.io/docs/install_standalone-docker.md)
##### Please refer to the latest version of the official website
- Milvus vector engine reference link
[Milvus vector engine official website](https://milvus.io/)
[Milvus vector engine Github](https://github.com/milvus-io)
```bash
# example - v2.2.4
wget https://github.com/milvus-io/milvus/releases/download/v2.2.4/milvus-standalone-docker-compose.yml -O docker-compose.yml
```
#### 3.3 Start Docker container
```bash
sudo docker-compose up -d
```
#### 3.4 Edit vector engine connection configuration information
- application.yml
- Edit the vector engine connection IP address 127.0.0.1 to the IP of the host where the container is located as needed
```bash
################## Vector engine ################
search:
host: 127.0.0.1
port: 19530
```
### 4. Open the browser
- Enter the address: [http://localhost:8090](http://localhost:8090/)
### 4.1 Image upload
1). Click the upload button to upload the file.
[Test image data](https://github.com/mymagicpower/AIAS/releases/download/apps/images.zip)
2). Click the feature extraction button.
Wait for the image feature extraction and store it in the vector engine. The progress information can be seen through the console.
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/storage.png)
### 4.2 Cross-modal search-text search image
Enter the text description and click the query button to see the returned list of images sorted by similarity.
- Example 1, enter text: car
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/search1.png)
- Example 2, enter text: two dogs on the snow
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/search2.png)
#### 4.3 Cross-modal search-image search image
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/search3.png)
### 5. Help information
- swagger interface document:
http://localhost:8089/swagger-ui.html
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/video_search/swagger.png)
- Initialize the vector engine (clear data):
```bash
me.aias.tools.MilvusInit.java
...
```
- Milvus vector engine reference link
[Milvus vector engine official website](https://milvus.io/)
[Milvus vector engine Github](https://github.com/milvus-io)

View File

@ -1,203 +0,0 @@
### 目录:
http://aias.top/
### 下载模型放置于models目录
- 链接: https://pan.baidu.com/s/1PdJ_mn7JnJf7UFJK6orEdA?pwd=t49j
### 图像&文本的跨模态相似性比对检索【支持40种语言】
本例子提供了通过文本搜图片的能力展示(模型本身当然也支持图片搜文字,或者混合搜索)。
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/arc.png)
#### 主要特性
- 底层使用特征向量相似度搜索
- 单台服务器十亿级数据的毫秒级搜索
- 近实时搜索,支持分布式部署
- 随时对数据进行插入、删除、搜索、更新等操作
#### 背景介绍
OpenAI 发布了两个新的神经网络CLIP 和 DALL·E。它们将 NLP自然语言识别与 图像识别结合在一起,对日常生活中的图像和语言有了更好的理解。
之前都是用文字搜文字图片搜图片现在通过CLIP这个模型可是实现文字搜图片图片搜文字。其实现思路就是将图片跟文本映射到同一个向量空间。如此就可以实现图片跟文本的跨模态相似性比对检索。
- 特征向量空间(由图片 & 文本组成)
![img](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/nlp_sdks/clip_Imagesearch.png)
#### CLIP - “另类”的图像识别
目前,大多数模型学习从标注好的数据集的带标签的示例中识别图像,而 CLIP 则是学习从互联网获取的图像及其描述, 即通过一段描述而不是“猫”、“狗”这样的单词标签来认识图像。
为了做到这一点CLIP 学习将大量的对象与它们的名字和描述联系起来,并由此可以识别训练集以外的对象。
![img](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/nlp_sdks/clip.png)
如上图所示CLIP网络工作流程 预训练图编码器和文本编码器,以预测数据集中哪些图像与哪些文本配对。
然后将CLIP转换为zero-shot分类器。此外将数据集的所有分类转换为诸如“一只狗的照片”之类的标签并预测最佳配对的图像。
CLIP模型地址
https://github.com/openai/CLIP/blob/main/README.md
#### 支持的语言列表:
![img](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/languages.png)
### 1. 前端部署
#### 1.1 直接运行:
```bash
npm run dev
```
#### 1.2 构建dist安装包
```bash
npm run build:prod
```
#### 1.3 nginx部署运行(mac环境为例)
```bash
cd /usr/local/etc/nginx/
vi /usr/local/etc/nginx/nginx.conf
# 编辑nginx.conf
server {
listen 8080;
server_name localhost;
location / {
root /Users/calvin/Documents/image_text_search/dist/;
index index.html index.htm;
}
......
# 重新加载配置:
sudo nginx -s reload
# 部署应用后,重启:
cd /usr/local/Cellar/nginx/1.19.6/bin
# 快速停止
sudo nginx -s stop
# 启动
sudo nginx
```
### 2. 后端jar部署
#### 2.1 环境要求:
- 系统JDK 1.8+
- application.yml
1). 根据需要编辑图片上传根路径rootPath
```bash
# 文件存储路径
file:
mac:
path: ~/file/
# folder for unzip files
rootPath: ~/file/data_root/
linux:
path: /home/aias/file/
rootPath: /home/aias/file/data_root/
windows:
path: file:/D:/aias/file/
rootPath: file:/D:/aias/file/data_root/
...
```
2). 根据需要编辑图片baseurl
```bash
image:
#baseurl是图片的地址前缀
baseurl: http://127.0.0.1:8089/files/
```
#### 2.2 运行程序:
```bash
# 运行程序
java -jar image-text-search-0.1.0.jar
```
### 3. 后端向量引擎部署
#### 3.1 环境要求:
- 需要安装docker运行环境Mac环境可以使用Docker Desktop
#### 3.2 拉取Milvus向量引擎镜像用于计算特征值向量相似度
下载 milvus-standalone-docker-compose.yml 配置文件并保存为 docker-compose.yml
[单机版安装文档](https://milvus.io/docs/install_standalone-docker.md)
[引擎配置文档](https://milvus.io/docs/configure-docker.md)
```bash
# 例子v2.2.4,请根据官方文档,选择合适的版本
wget https://github.com/milvus-io/milvus/releases/download/v2.2.4/milvus-standalone-docker-compose.yml -O docker-compose.yml
```
#### 3.3 启动 Docker 容器
```bash
sudo docker-compose up -d
```
#### 3.5 编辑向量引擎连接配置信息
- application.yml
- 根据需要编辑向量引擎连接ip地址127.0.0.1为容器所在的主机ip
```bash
################## 向量引擎 ################
search:
host: 127.0.0.1
port: 19530
```
### 4. 打开浏览器
- 输入地址: http://localhost:8090
#### 4.1 图片上传
1). 点击上传按钮上传文件.
[测试图片数据](https://pan.baidu.com/s/1QtF6syNUKS5qkf4OKAcuLA?pwd=wfd8)
2). 点击特征提取按钮.
等待图片特征提取特征存入向量引擎。通过console可以看到进度信息。
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/storage.png)
#### 4.2 跨模态搜索 - 文本搜图
输入文字描述,点击查询,可以看到返回的图片清单,根据相似度排序。
- 例子1输入文本
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/search1.png)
- 例子2输入文本雪地上两只狗
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/search2.png)
#### 4.3 跨模态搜索 - 以图搜图
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/7_engine_hub/image_text_search/search3.png)
### 5. 帮助信息
- swagger接口文档:
http://localhost:8089/swagger-ui.html
![Screenshot](https://aias-home.oss-cn-beijing.aliyuncs.com/AIAS/video_search/swagger.png)
- 初始化向量引擎(清空数据):
```bash
me.aias.tools.MilvusInit.java
...
```
- Milvus向量引擎参考链接
[Milvus向量引擎官网](https://milvus.io/cn/docs/overview.md)
[Milvus向量引擎Github](https://github.com/milvus-io)
### 官网:
[官网链接](http://www.aias.top/)
### Git地址
[Github链接](https://github.com/mymagicpower/AIAS)
[Gitee链接](https://gitee.com/mymagicpower/AIAS)
#### 帮助文档:
- http://aias.top/guides.html
- 1.性能优化常见问题:
- http://aias.top/AIAS/guides/performance.html
- 2.引擎配置包括CPUGPU在线自动加载及本地配置:
- http://aias.top/AIAS/guides/engine_config.html
- 3.模型加载方式(在线自动加载,及本地配置):
- http://aias.top/AIAS/guides/load_model.html
- 4.Windows环境常见问题:
- http://aias.top/AIAS/guides/windows.html

View File

@ -1,14 +0,0 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@ -1,6 +0,0 @@
# just a flag
ENV = 'development'
# base api
VUE_APP_BASE_API = 'http://127.0.0.1:8089'

View File

@ -1,6 +0,0 @@
# just a flag
ENV = 'production'
# base api
VUE_APP_BASE_API = 'http://127.0.0.1:8089'

View File

@ -1,8 +0,0 @@
NODE_ENV = production
# just a flag
ENV = 'staging'
# base api
VUE_APP_BASE_API = '/stage-api'

View File

@ -1,4 +0,0 @@
build/*.js
src/assets
public
dist

View File

@ -1,198 +0,0 @@
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true,
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
// add your custom rules here
//it is base on https://github.com/vuejs/eslint-config-vue
rules: {
"vue/max-attributes-per-line": [2, {
"singleline": 10,
"multiline": {
"max": 1,
"allowFirstLine": false
}
}],
"vue/singleline-html-element-content-newline": "off",
"vue/multiline-html-element-content-newline":"off",
"vue/name-property-casing": ["error", "PascalCase"],
"vue/no-v-html": "off",
'accessor-pairs': 2,
'arrow-spacing': [2, {
'before': true,
'after': true
}],
'block-spacing': [2, 'always'],
'brace-style': [2, '1tbs', {
'allowSingleLine': true
}],
'camelcase': [0, {
'properties': 'always'
}],
'comma-dangle': [2, 'never'],
'comma-spacing': [2, {
'before': false,
'after': true
}],
'comma-style': [2, 'last'],
'constructor-super': 2,
'curly': [2, 'multi-line'],
'dot-location': [2, 'property'],
'eol-last': 2,
'eqeqeq': ["error", "always", {"null": "ignore"}],
'generator-star-spacing': [2, {
'before': true,
'after': true
}],
'handle-callback-err': [2, '^(err|error)$'],
'indent': [2, 2, {
'SwitchCase': 1
}],
'jsx-quotes': [2, 'prefer-single'],
'key-spacing': [2, {
'beforeColon': false,
'afterColon': true
}],
'keyword-spacing': [2, {
'before': true,
'after': true
}],
'new-cap': [2, {
'newIsCap': true,
'capIsNew': false
}],
'new-parens': 2,
'no-array-constructor': 2,
'no-caller': 2,
'no-console': 'off',
'no-class-assign': 2,
'no-cond-assign': 2,
'no-const-assign': 2,
'no-control-regex': 0,
'no-delete-var': 2,
'no-dupe-args': 2,
'no-dupe-class-members': 2,
'no-dupe-keys': 2,
'no-duplicate-case': 2,
'no-empty-character-class': 2,
'no-empty-pattern': 2,
'no-eval': 2,
'no-ex-assign': 2,
'no-extend-native': 2,
'no-extra-bind': 2,
'no-extra-boolean-cast': 2,
'no-extra-parens': [2, 'functions'],
'no-fallthrough': 2,
'no-floating-decimal': 2,
'no-func-assign': 2,
'no-implied-eval': 2,
'no-inner-declarations': [2, 'functions'],
'no-invalid-regexp': 2,
'no-irregular-whitespace': 2,
'no-iterator': 2,
'no-label-var': 2,
'no-labels': [2, {
'allowLoop': false,
'allowSwitch': false
}],
'no-lone-blocks': 2,
'no-mixed-spaces-and-tabs': 2,
'no-multi-spaces': 2,
'no-multi-str': 2,
'no-multiple-empty-lines': [2, {
'max': 1
}],
'no-native-reassign': 2,
'no-negated-in-lhs': 2,
'no-new-object': 2,
'no-new-require': 2,
'no-new-symbol': 2,
'no-new-wrappers': 2,
'no-obj-calls': 2,
'no-octal': 2,
'no-octal-escape': 2,
'no-path-concat': 2,
'no-proto': 2,
'no-redeclare': 2,
'no-regex-spaces': 2,
'no-return-assign': [2, 'except-parens'],
'no-self-assign': 2,
'no-self-compare': 2,
'no-sequences': 2,
'no-shadow-restricted-names': 2,
'no-spaced-func': 2,
'no-sparse-arrays': 2,
'no-this-before-super': 2,
'no-throw-literal': 2,
'no-trailing-spaces': 2,
'no-undef': 2,
'no-undef-init': 2,
'no-unexpected-multiline': 2,
'no-unmodified-loop-condition': 2,
'no-unneeded-ternary': [2, {
'defaultAssignment': false
}],
'no-unreachable': 2,
'no-unsafe-finally': 2,
'no-unused-vars': [2, {
'vars': 'all',
'args': 'none'
}],
'no-useless-call': 2,
'no-useless-computed-key': 2,
'no-useless-constructor': 2,
'no-useless-escape': 0,
'no-whitespace-before-property': 2,
'no-with': 2,
'one-var': [2, {
'initialized': 'never'
}],
'operator-linebreak': [2, 'after', {
'overrides': {
'?': 'before',
':': 'before'
}
}],
'padded-blocks': [2, 'never'],
'quotes': [2, 'single', {
'avoidEscape': true,
'allowTemplateLiterals': true
}],
'semi': [2, 'never'],
'semi-spacing': [2, {
'before': false,
'after': true
}],
'space-before-blocks': [2, 'always'],
'space-before-function-paren': [2, 'never'],
'space-in-parens': [2, 'never'],
'space-infix-ops': 2,
'space-unary-ops': [2, {
'words': true,
'nonwords': false
}],
'spaced-comment': [2, 'always', {
'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
}],
'template-curly-spacing': [2, 'never'],
'use-isnan': 2,
'valid-typeof': 2,
'wrap-iife': [2, 'any'],
'yield-star-spacing': [2, 'both'],
'yoda': [2, 'never'],
'prefer-const': 2,
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
'object-curly-spacing': [2, 'always', {
objectsInObjects: false
}],
'array-bracket-spacing': [2, 'never']
}
}

View File

@ -1,5 +0,0 @@
language: node_js
node_js: 10
script: npm run test
notifications:
email: false

View File

@ -1,14 +0,0 @@
module.exports = {
presets: [
// https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
'@vue/cli-plugin-babel/preset'
],
'env': {
'development': {
// babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
// This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
// https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html
'plugins': ['dynamic-import-node']
}
}
}

View File

@ -1,35 +0,0 @@
const { run } = require('runjs')
const chalk = require('chalk')
const config = require('../vue.config.js')
const rawArgv = process.argv.slice(2)
const args = rawArgv.join(' ')
if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
const report = rawArgv.includes('--report')
run(`vue-cli-service build ${args}`)
const port = 9526
const publicPath = config.publicPath
var connect = require('connect')
var serveStatic = require('serve-static')
const app = connect()
app.use(
publicPath,
serveStatic('./dist', {
index: ['index.html', '/']
})
)
app.listen(port, function () {
console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`))
if (report) {
console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`))
}
})
} else {
run(`vue-cli-service build ${args}`)
}

View File

@ -1,24 +0,0 @@
module.exports = {
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'],
coverageDirectory: '<rootDir>/tests/unit/coverage',
// 'collectCoverage': true,
'coverageReporters': [
'lcov',
'text-summary'
],
testURL: 'http://localhost/'
}

View File

@ -1,9 +0,0 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

View File

@ -1,65 +0,0 @@
{
"name": "voiceprint-search-ui",
"version": "1.0.0",
"description": "Image text Search UI Demo",
"author": "Calvin <179209347@qq.com>",
"scripts": {
"dev": "vue-cli-service serve",
"build:prod": "vue-cli-service build",
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit"
},
"dependencies": {
"crypto-js": "^3.1.9-1",
"axios": "0.18.1",
"core-js": "3.21.0",
"easy-circular-progress": "1.0.4",
"echarts": "^4.2.1",
"element-ui": "2.13.2",
"js-cookie": "2.2.0",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vertx3-eventbus-client": "^3.9.4",
"vue": "2.6.10",
"vue-count-to": "^1.0.13",
"vue-json-viewer": "^2.2.18",
"vue-router": "3.0.6",
"vuex": "3.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "4.4.4",
"@vue/cli-plugin-eslint": "4.4.4",
"@vue/cli-plugin-unit-jest": "4.4.4",
"@vue/cli-service": "4.4.4",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "9.5.1",
"babel-eslint": "10.1.0",
"babel-jest": "23.6.0",
"babel-plugin-dynamic-import-node": "2.3.3",
"chalk": "2.4.2",
"connect": "3.6.6",
"eslint": "6.7.2",
"eslint-plugin-vue": "6.2.2",
"html-webpack-plugin": "3.2.0",
"mockjs": "1.0.1-beta3",
"runjs": "4.3.2",
"sass": "1.26.8",
"sass-loader": "8.0.2",
"script-ext-html-webpack-plugin": "2.1.3",
"serve-static": "1.13.2",
"vue-template-compiler": "2.6.10"
},
"browserslist": [
"> 1%",
"last 2 versions"
],
"engines": {
"node": ">=8.9",
"npm": ">= 3.0.0"
},
"license": "MIT"
}

View File

@ -1,8 +0,0 @@
// https://github.com/michael-ciniawsky/postcss-load-config
module.exports = {
'plugins': {
// to edit target browsers: use "browserslist" field in package.json
'autoprefixer': {}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,11 +0,0 @@
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App'
}
</script>

View File

@ -1,61 +0,0 @@
import CryptoJS from 'crypto-js/crypto-js'
// 默认的 KEY 与 iv 如果没有给
const KEY = CryptoJS.enc.Utf8.parse("1234567890123456");
const IV = CryptoJS.enc.Utf8.parse('1234567890123456');
/**
* AES加密 字符串 key iv 返回base64
*/
export function encrypt(word, keyStr, ivStr) {
let key = KEY
let iv = IV
if (keyStr) {
key = CryptoJS.enc.Utf8.parse(keyStr);
iv = CryptoJS.enc.Utf8.parse(ivStr);
}
let srcs = CryptoJS.enc.Utf8.parse(word);
var encrypted = CryptoJS.AES.encrypt(srcs, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
// console.log("-=-=-=-", encrypted.ciphertext)
return CryptoJS.enc.Base64.stringify(encrypted.ciphertext);
}
/**
* AES 解密 字符串 key iv 返回base64
*
*/
export function decrypt(word, keyStr, ivStr) {
let key = KEY
let iv = IV
if (keyStr) {
key = CryptoJS.enc.Utf8.parse(keyStr);
iv = CryptoJS.enc.Utf8.parse(ivStr);
}
let base64 = CryptoJS.enc.Base64.parse(word);
let src = CryptoJS.enc.Base64.stringify(base64);
var decrypt = CryptoJS.AES.decrypt(src, key, {
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.ZeroPadding
});
var decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);
return decryptedStr.toString();
}
//判断字符是否为空的方法
export function isEmpty(obj){
if(typeof obj == "undefined" || obj == null || obj == ""){
return true;
}else{
return false;
}
}

View File

@ -1,46 +0,0 @@
import request from '@/utils/request'
export function getStorageList() {
return request({
url: 'api/localStorage/list',
method: 'get'
})
}
export function add(data) {
return request({
url: 'api/localStorage',
method: 'post',
data
})
}
export function del(id) {
return request({
url: 'api/localStorage/',
method: 'delete',
data: {
id: id
}
})
}
export function edit(data) {
return request({
url: 'api/localStorage',
method: 'put',
data
})
}
export function extract(id) {
return request({
url: 'api/data/extractFeatures',
method: 'get',
params: {
id: id
}
})
}
export default { getStorageList, add, edit, del, extract }

View File

@ -1,12 +0,0 @@
import request from '@/utils/request'
export function search(data) {
return request({
url: 'api/search/text',
method: 'get',
params: {
topK: data.topK,
text: data.text
}
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,55 +0,0 @@
// flex row
@mixin flex-row {
display: flex;
flex-direction: row;
}
@mixin flex-row-between {
@include flex-row();
justify-content: space-between;
}
@mixin flex-row-between-center {
@include flex-row-between();
align-items: center
}
@mixin flex-row-center {
@include flex-row();
justify-content: center
}
@mixin flex-row-all-center {
@include flex-row-center;
align-items: center
}
@mixin all-height($height) {
height: $height;
line-height: $height
}
@mixin ellipsis($width) {
width: $width;
display: inline-block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis
}
// flex column
@mixin flex-column {
display: flex;
flex-direction: column
}
@mixin flex-column-center {
@include flex-column();
justify-content: center
}
@mixin flex-column-all-center {
@include flex-column-center;
align-items: center
}

View File

@ -1,40 +0,0 @@
export default {
data () {
return {
tableInit: false,
emptyTable: false,
page: {
pageNum: 1,
pageSize: 8,
total: 0,
},
}
},
mounted () {
},
computed: {
// emptyTable () {
// return this.page.total === 0 && this.page.pageNum === 1 && this.emptyParam
// },
},
watch: {
'page.total' () {
if (this.page.total > 0) {
this.emptyTable = false
}
},
},
methods: {
isObjectEmpty (data = {}) {
return Object.values(data).filter(a => !!a).length === 0
},
setEmptyTable () {
this.tableInit = true
console.log(this.page.total, this.page.total == 0)
this.emptyTable = this.page.total == 0
},
clearPage () {
this.page.pageNum = 1
},
},
}

View File

@ -1,78 +0,0 @@
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect==='noRedirect'||index==levelList.length-1" class="no-redirect">{{ item.meta.title }}</span>
<a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script>
import pathToRegexp from 'path-to-regexp'
export default {
data() {
return {
levelList: null
}
},
watch: {
$route() {
this.getBreadcrumb()
}
},
created() {
this.getBreadcrumb()
},
methods: {
getBreadcrumb() {
// only show routes with meta.title
let matched = this.$route.matched.filter(item => item.meta && item.meta.title)
const first = matched[0]
if (!this.isDashboard(first)) {
matched = [{ path: '/dashboard', meta: { title: 'Dashboard' }}].concat(matched)
}
this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
},
isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim().toLocaleLowerCase() === 'Dashboard'.toLocaleLowerCase()
},
pathCompile(path) {
// To solve this problem https://github.com/PanJiaChen/vue-element-admin/issues/561
const { params } = this.$route
var toPath = pathToRegexp.compile(path)
return toPath(params)
},
handleLink(item) {
const { redirect, path } = item
if (redirect) {
this.$router.push(redirect)
return
}
this.$router.push(this.pathCompile(path))
}
}
}
</script>
<style lang="scss" scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>

View File

@ -1,44 +0,0 @@
<template>
<div style="padding: 0 15px;" @click="toggleClick">
<svg
:class="{'is-active':isActive}"
class="hamburger"
viewBox="0 0 1024 1024"
xmlns="http://www.w3.org/2000/svg"
width="64"
height="64"
>
<path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
</svg>
</div>
</template>
<script>
export default {
name: 'Hamburger',
props: {
isActive: {
type: Boolean,
default: false
}
},
methods: {
toggleClick() {
this.$emit('toggleClick')
}
}
}
</script>
<style scoped>
.hamburger {
display: inline-block;
vertical-align: middle;
width: 20px;
height: 20px;
}
.hamburger.is-active {
transform: rotate(180deg);
}
</style>

View File

@ -1,62 +0,0 @@
<template>
<div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
<svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
// doc: https://panjiachen.github.io/vue-element-admin-site/feature/component/svg-icon.html#usage
import { isExternal } from '@/utils/validate'
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
isExternal() {
return isExternal(this.iconClass)
},
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return 'svg-icon ' + this.className
} else {
return 'svg-icon'
}
},
styleExternalIcon() {
return {
mask: `url(${this.iconClass}) no-repeat 50% 50%`,
'-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
}
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.svg-external-icon {
background-color: currentColor;
mask-size: cover!important;
display: inline-block;
}
</style>

View File

@ -1,49 +0,0 @@
<template>
<div class="empty-data-wrapper" :style="{height: height}">
<div class="empty-data-data">
<img :src="imgPath">
<div v-if="title" class="empty-data-tips">{{ title }}</div>
<div class="empty-data-operate">
<slot/>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'EmptyData',
props: {
imgUrl: { prop: String },
title: { prop: String },
height: { prop: String, default: 'calc( 100vh - 300px)' }
},
methods: {}
}
</script>
<style lang='scss' scoped>
@import "~@/assets/styles/base";
.empty-data-wrapper {
@include flex-column-all-center;
.empty-data-data {
@include flex-column-all-center;
width: 400px;
.empty-data-tips {
margin: 10px 0;
font-size: 12px;
font-weight: 400;
color: rgba(131, 138, 145, 1);
}
.img {
width: 114px;
height: 95px;
}
}
}
</style>

View File

@ -1,40 +0,0 @@
<template>
<section class="app-main">
<transition name="fade-transform" mode="out-in">
<router-view :key="key" />
</transition>
</section>
</template>
<script>
export default {
name: 'AppMain',
computed: {
key() {
return this.$route.path
}
}
}
</script>
<style scoped>
.app-main {
/*50 = navbar */
min-height: calc(100vh - 50px);
width: 100%;
position: relative;
overflow: hidden;
}
.fixed-header+.app-main {
padding-top: 50px;
}
</style>
<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
.fixed-header {
padding-right: 15px;
}
}
</style>

View File

@ -1,111 +0,0 @@
<template>
<div class="navbar">
<hamburger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb class="breadcrumb-container" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
export default {
components: {
Breadcrumb,
Hamburger
},
computed: {
...mapGetters([
'sidebar',
'avatar'
])
},
methods: {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
}
}
}
</script>
<style lang="scss" scoped>
.navbar {
height: 50px;
overflow: hidden;
position: relative;
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
.hamburger-container {
line-height: 46px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container {
float: left;
}
.right-menu {
float: right;
height: 100%;
line-height: 50px;
&:focus {
outline: none;
}
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background .3s;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
}
.avatar-container {
margin-right: 30px;
.avatar-wrapper {
margin-top: 5px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
}
.el-icon-caret-bottom {
cursor: pointer;
position: absolute;
right: -20px;
top: 25px;
font-size: 12px;
}
}
}
}
}
</style>

View File

@ -1,26 +0,0 @@
export default {
computed: {
device() {
return this.$store.state.app.device
}
},
mounted() {
// In order to fix the click on menu on the ios device will trigger the mouseleave bug
// https://github.com/PanJiaChen/vue-element-admin/issues/1135
this.fixBugIniOS()
},
methods: {
fixBugIniOS() {
const $subMenu = this.$refs.subMenu
if ($subMenu) {
const handleMouseleave = $subMenu.handleMouseleave
$subMenu.handleMouseleave = (e) => {
if (this.device === 'mobile') {
return
}
handleMouseleave(e)
}
}
}
}
}

View File

@ -1,41 +0,0 @@
<script>
export default {
name: 'MenuItem',
functional: true,
props: {
icon: {
type: String,
default: ''
},
title: {
type: String,
default: ''
}
},
render(h, context) {
const { icon, title } = context.props
const vnodes = []
if (icon) {
if (icon.includes('el-icon')) {
vnodes.push(<i class={[icon, 'sub-el-icon']} />)
} else {
vnodes.push(<svg-icon icon-class={icon}/>)
}
}
if (title) {
vnodes.push(<span slot='title'>{(title)}</span>)
}
return vnodes
}
}
</script>
<style scoped>
.sub-el-icon {
color: currentColor;
width: 1em;
height: 1em;
}
</style>

View File

@ -1,43 +0,0 @@
<template>
<component :is="type" v-bind="linkProps(to)">
<slot />
</component>
</template>
<script>
import { isExternal } from '@/utils/validate'
export default {
props: {
to: {
type: String,
required: true
}
},
computed: {
isExternal() {
return isExternal(this.to)
},
type() {
if (this.isExternal) {
return 'a'
}
return 'router-link'
}
},
methods: {
linkProps(to) {
if (this.isExternal) {
return {
href: to,
target: '_blank',
rel: 'noopener'
}
}
return {
to: to
}
}
}
}
</script>

View File

@ -1,82 +0,0 @@
<template>
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 v-else class="sidebar-title">{{ title }} </h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo">
<h1 class="sidebar-title">{{ title }} </h1>
</router-link>
</transition>
</div>
</template>
<script>
export default {
name: 'SidebarLogo',
props: {
collapse: {
type: Boolean,
required: true
}
},
data() {
return {
title: 'OCR'
// logo: 'https://djl.ai/website/img/djl-middle.png'
}
}
}
</script>
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
opacity: 0;
}
.sidebar-logo-container {
position: relative;
width: 100%;
height: 50px;
line-height: 50px;
background: #2b2f3a;
text-align: center;
overflow: hidden;
& .sidebar-logo-link {
height: 100%;
width: 100%;
& .sidebar-logo {
width: 32px;
height: 32px;
vertical-align: middle;
margin-right: 12px;
}
& .sidebar-title {
display: inline-block;
margin: 0;
color: #fff;
font-weight: 600;
line-height: 50px;
font-size: 14px;
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
vertical-align: middle;
}
}
&.collapse {
.sidebar-logo {
margin-right: 0px;
}
}
}
</style>

View File

@ -1,95 +0,0 @@
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
<item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
</el-menu-item>
</app-link>
</template>
<el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
<template slot="title">
<item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-submenu>
</div>
</template>
<script>
import path from 'path'
import { isExternal } from '@/utils/validate'
import Item from './Item'
import AppLink from './Link'
import FixiOSBug from './FixiOSBug'
export default {
name: 'SidebarItem',
components: { Item, AppLink },
mixins: [FixiOSBug],
props: {
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
},
data() {
// To fix https://github.com/PanJiaChen/vue-admin-template/issues/237
// TODO: refactor with render function
this.onlyOneChild = null
return {}
},
methods: {
hasOneShowingChild(children = [], parent) {
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
this.onlyOneChild = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
return true
}
return false
},
resolvePath(routePath) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(this.basePath)) {
return this.basePath
}
return path.resolve(this.basePath, routePath)
}
}
}
</script>

View File

@ -1,56 +0,0 @@
<template>
<div :class="{'has-logo':showLogo}">
<logo v-if="showLogo" :collapse="isCollapse" />
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:unique-opened="false"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
mode="vertical"
>
<sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebar'
]),
routes() {
return this.$router.options.routes
},
activeMenu() {
const route = this.$route
const { meta, path } = route
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened
}
}
}
</script>

View File

@ -1,3 +0,0 @@
export { default as Navbar } from './Navbar'
export { default as Sidebar } from './Sidebar'
export { default as AppMain } from './AppMain'

View File

@ -1,93 +0,0 @@
<template>
<div :class="classObj" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" />
<div class="main-container">
<div :class="{'fixed-header':fixedHeader}">
<navbar />
</div>
<app-main />
</div>
</div>
</template>
<script>
import { Navbar, Sidebar, AppMain } from './components'
import ResizeMixin from './mixin/ResizeHandler'
export default {
name: 'Layout',
components: {
Navbar,
Sidebar,
AppMain
},
mixins: [ResizeMixin],
computed: {
sidebar() {
return this.$store.state.app.sidebar
},
device() {
return this.$store.state.app.device
},
fixedHeader() {
return this.$store.state.settings.fixedHeader
},
classObj() {
return {
hideSidebar: !this.sidebar.opened,
openSidebar: this.sidebar.opened,
withoutAnimation: this.sidebar.withoutAnimation,
mobile: this.device === 'mobile'
}
}
},
methods: {
handleClickOutside() {
this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/styles/mixin.scss";
@import "~@/styles/variables.scss";
.app-wrapper {
@include clearfix;
position: relative;
height: 100%;
width: 100%;
&.mobile.openSidebar{
position: fixed;
top: 0;
}
}
.drawer-bg {
background: #000;
opacity: 0.3;
width: 100%;
top: 0;
height: 100%;
position: absolute;
z-index: 999;
}
.fixed-header {
position: fixed;
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$sideBarWidth});
transition: width 0.28s;
}
.hideSidebar .fixed-header {
width: calc(100% - 54px)
}
.mobile .fixed-header {
width: 100%;
}
</style>

View File

@ -1,45 +0,0 @@
import store from '@/store'
const { body } = document
const WIDTH = 992 // refer to Bootstrap's responsive design
export default {
watch: {
$route(route) {
if (this.device === 'mobile' && this.sidebar.opened) {
store.dispatch('app/closeSideBar', { withoutAnimation: false })
}
}
},
beforeMount() {
window.addEventListener('resize', this.$_resizeHandler)
},
beforeDestroy() {
window.removeEventListener('resize', this.$_resizeHandler)
},
mounted() {
const isMobile = this.$_isMobile()
if (isMobile) {
store.dispatch('app/toggleDevice', 'mobile')
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
},
methods: {
// use $_ for mixins properties
// https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
$_isMobile() {
const rect = body.getBoundingClientRect()
return rect.width - 1 < WIDTH
},
$_resizeHandler() {
if (!document.hidden) {
const isMobile = this.$_isMobile()
store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
if (isMobile) {
store.dispatch('app/closeSideBar', { withoutAnimation: true })
}
}
}
}
}

View File

@ -1,30 +0,0 @@
import Vue from 'vue'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/en' // lang i18n
import '@/styles/index.scss' // global css
import App from './App'
import store from './store'
import router from './router'
import '@/permission' // permission control
// set ElementUI lang to EN
// Vue.use(ElementUI, { locale })
// 如果想要中文版 element-ui按如下方式声明
// To use the Chinese version of element-ui, declare as follows
Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})

View File

@ -1,27 +0,0 @@
import router from './router'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import getPageTitle from '@/utils/get-page-title'
NProgress.configure({ showSpinner: false }) // NProgress Configuration
router.beforeEach(async(to, from, next) => {
// start progress bar
NProgress.start()
// set page title
document.title = getPageTitle(to.meta.title)
if (to.path === '/login') {
// if is logged in, redirect to the home page
next({ path: '/' })
NProgress.done()
} else {
next()
}
})
router.afterEach(() => {
// finish progress bar
NProgress.done()
})

View File

@ -1,93 +0,0 @@
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
/* Layout */
import Layout from '@/layout'
/**
* Note: sub-menu only appear when route children.length >= 1
* Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html
*
* hidden: true if set true, item will not show in the sidebar(default is false)
* alwaysShow: true if set true, will always show the root menu
* if not set alwaysShow, when item has more than one children route,
* it will becomes nested mode, otherwise not show the root menu
* redirect: noRedirect if set noRedirect will no redirect in the breadcrumb
* name:'router-name' the name is used by <keep-alive> (must set!!!)
* meta : {
roles: ['admin','editor'] control the page roles (you can set multiple roles)
title: 'title' the name show in sidebar and breadcrumb (recommend set)
icon: 'svg-name'/'el-icon-x' the icon show in the sidebar
breadcrumb: false if set false, the item will hidden in breadcrumb(default is true)
activeMenu: '/example/list' if set path, the sidebar will highlight the path you set
}
*/
/**
* constantRoutes
* a base page that does not have permission requirements
* all roles can be accessed
*/
export const constantRoutes = [
{
path: '/404',
component: () => import('@/views/404'),
hidden: true
},
{
path: '/',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/textsearch/index'),
name: 'search',
meta: { title: 'Text Search', icon: 'el-icon-search' }
}
]
},
{
path: '/image',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/imagesearch/index'),
name: 'storage',
meta: { title: 'Image Search', icon: 'el-icon-search' }
}
]
},
{
path: '/storage',
component: Layout,
children: [
{
path: 'index',
component: () => import('@/views/storage/index'),
name: 'storage',
meta: { title: 'Data Management', icon: 'el-icon-upload' }
}
]
},
// 404 page must be placed at the end !!!
{ path: '*', redirect: '/404', hidden: true }
]
const createRouter = () => new Router({
// mode: 'history', // require service support
scrollBehavior: () => ({ y: 0 }),
routes: constantRoutes
})
const router = createRouter()
// Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465
export function resetRouter() {
const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router
}
export default router

View File

@ -1,16 +0,0 @@
module.exports = {
title: '跨模态搜索 UI',
/**
* @type {boolean} true | false
* @description Whether fix the header
*/
fixedHeader: false,
/**
* @type {boolean} true | false
* @description Whether show the logo in sidebar
*/
sidebarLogo: false
}

View File

@ -1,8 +0,0 @@
const getters = {
sidebar: state => state.app.sidebar,
device: state => state.app.device,
token: state => state.user.token,
avatar: state => state.user.avatar,
name: state => state.user.name
}
export default getters

View File

@ -1,17 +0,0 @@
import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import app from './modules/app'
import settings from './modules/settings'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
app,
settings
},
getters
})
export default store

View File

@ -1,48 +0,0 @@
import Cookies from 'js-cookie'
const state = {
sidebar: {
opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
withoutAnimation: false
},
device: 'desktop'
}
const mutations = {
TOGGLE_SIDEBAR: state => {
state.sidebar.opened = !state.sidebar.opened
state.sidebar.withoutAnimation = false
if (state.sidebar.opened) {
Cookies.set('sidebarStatus', 1)
} else {
Cookies.set('sidebarStatus', 0)
}
},
CLOSE_SIDEBAR: (state, withoutAnimation) => {
Cookies.set('sidebarStatus', 0)
state.sidebar.opened = false
state.sidebar.withoutAnimation = withoutAnimation
},
TOGGLE_DEVICE: (state, device) => {
state.device = device
}
}
const actions = {
toggleSideBar({ commit }) {
commit('TOGGLE_SIDEBAR')
},
closeSideBar({ commit }, { withoutAnimation }) {
commit('CLOSE_SIDEBAR', withoutAnimation)
},
toggleDevice({ commit }, device) {
commit('TOGGLE_DEVICE', device)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -1,32 +0,0 @@
import defaultSettings from '@/settings'
const { showSettings, fixedHeader, sidebarLogo } = defaultSettings
const state = {
showSettings: showSettings,
fixedHeader: fixedHeader,
sidebarLogo: sidebarLogo
}
const mutations = {
CHANGE_SETTING: (state, { key, value }) => {
// eslint-disable-next-line no-prototype-builtins
if (state.hasOwnProperty(key)) {
state[key] = value
}
}
}
const actions = {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
}
}
export default {
namespaced: true,
state,
mutations,
actions
}

View File

@ -1,49 +0,0 @@
// cover some element-ui styles
.el-breadcrumb__inner,
.el-breadcrumb__inner a {
font-weight: 400 !important;
}
.el-upload {
input[type="file"] {
display: none !important;
}
}
.el-upload__input {
display: none;
}
// to fixed https://github.com/ElemeFE/element/issues/2461
.el-dialog {
transform: none;
left: 0;
position: relative;
margin: 0 auto;
}
// refine element ui upload
.upload-container {
.el-upload {
width: 100%;
.el-upload-dragger {
width: 100%;
height: 200px;
}
}
}
// dropdown
.el-dropdown-menu {
a {
display: block
}
}
// to fix el-date-picker css style
.el-range-separator {
box-sizing: content-box;
}

View File

@ -1,65 +0,0 @@
@import './variables.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
body {
height: 100%;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
}
label {
font-weight: 700;
}
html {
height: 100%;
box-sizing: border-box;
}
#app {
height: 100%;
}
*,
*:before,
*:after {
box-sizing: inherit;
}
a:focus,
a:active {
outline: none;
}
a,
a:focus,
a:hover {
cursor: pointer;
color: inherit;
text-decoration: none;
}
div:focus {
outline: none;
}
.clearfix {
&:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
}
// main-container global css
.app-container {
padding: 20px;
}

View File

@ -1,28 +0,0 @@
@mixin clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@mixin scrollBar {
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
@mixin relative {
position: relative;
width: 100%;
height: 100%;
}

View File

@ -1,226 +0,0 @@
#app {
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: $sideBarWidth;
position: relative;
}
.sidebar-container {
transition: width 0.28s;
width: $sideBarWidth !important;
background-color: $menuBg;
height: 100%;
position: fixed;
font-size: 0px;
top: 0;
bottom: 0;
left: 0;
z-index: 1001;
overflow: hidden;
// reset element-ui css
.horizontal-collapse-transition {
transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
}
.scrollbar-wrapper {
overflow-x: hidden !important;
}
.el-scrollbar__bar.is-vertical {
right: 0px;
}
.el-scrollbar {
height: 100%;
}
&.has-logo {
.el-scrollbar {
height: calc(100% - 50px);
}
}
.is-horizontal {
display: none;
}
a {
display: inline-block;
width: 100%;
overflow: hidden;
}
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
.el-menu {
border: none;
height: 100%;
width: 100% !important;
}
// menu hover
.submenu-title-noDropdown,
.el-submenu__title {
&:hover {
background-color: $menuHover !important;
}
}
.is-active>.el-submenu__title {
color: $subMenuActiveText !important;
}
& .nest-menu .el-submenu>.el-submenu__title,
& .el-submenu .el-menu-item {
min-width: $sideBarWidth !important;
background-color: $subMenuBg !important;
&:hover {
background-color: $subMenuHover !important;
}
}
}
.hideSidebar {
.sidebar-container {
width: 54px !important;
}
.main-container {
margin-left: 54px;
}
.submenu-title-noDropdown {
padding: 0 !important;
position: relative;
.el-tooltip {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
}
}
.el-submenu {
overflow: hidden;
&>.el-submenu__title {
padding: 0 !important;
.svg-icon {
margin-left: 20px;
}
.sub-el-icon {
margin-left: 19px;
}
.el-submenu__icon-arrow {
display: none;
}
}
}
.el-menu--collapse {
.el-submenu {
&>.el-submenu__title {
&>span {
height: 0;
width: 0;
overflow: hidden;
visibility: hidden;
display: inline-block;
}
}
}
}
}
.el-menu--collapse .el-menu .el-submenu {
min-width: $sideBarWidth !important;
}
// mobile responsive
.mobile {
.main-container {
margin-left: 0px;
}
.sidebar-container {
transition: transform .28s;
width: $sideBarWidth !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$sideBarWidth, 0, 0);
}
}
}
.withoutAnimation {
.main-container,
.sidebar-container {
transition: none;
}
}
}
// when menu collapsed
.el-menu--vertical {
&>.el-menu {
.svg-icon {
margin-right: 16px;
}
.sub-el-icon {
margin-right: 12px;
margin-left: -2px;
}
}
.nest-menu .el-submenu>.el-submenu__title,
.el-menu-item {
&:hover {
// you can use $subMenuHover
background-color: $menuHover !important;
}
}
// the scroll bar appears when the subMenu is too long
>.el-menu--popup {
max-height: 100vh;
overflow-y: auto;
&::-webkit-scrollbar-track-piece {
background: #d3dce6;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #99a9bf;
border-radius: 20px;
}
}
}

View File

@ -1,48 +0,0 @@
// global transition css
/* fade */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.28s;
}
.fade-enter,
.fade-leave-active {
opacity: 0;
}
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all .5s;
}
.fade-transform-enter {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* breadcrumb transition */
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-move {
transition: all .5s;
}
.breadcrumb-leave-active {
position: absolute;
}

View File

@ -1,25 +0,0 @@
// sidebar
$menuText:#bfcbd9;
$menuActiveText:#409EFF;
$subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951
$menuBg:#304156;
$menuHover:#263445;
$subMenuBg:#1f2d3d;
$subMenuHover:#001528;
$sideBarWidth: 210px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
menuText: $menuText;
menuActiveText: $menuActiveText;
subMenuActiveText: $subMenuActiveText;
menuBg: $menuBg;
menuHover: $menuHover;
subMenuBg: $subMenuBg;
subMenuHover: $subMenuHover;
sideBarWidth: $sideBarWidth;
}

View File

@ -1,10 +0,0 @@
import defaultSettings from '@/settings'
const title = defaultSettings.title || 'Vue Admin Template'
export default function getPageTitle(pageTitle) {
if (pageTitle) {
return `${pageTitle} - ${title}`
}
return `${title}`
}

View File

@ -1,147 +0,0 @@
/**
* Created by PanJiaChen on 16/11/18.
*/
/**
* Parse the time to string
* @param {(Object|string|number)} time
* @param {string} cFormat
* @returns {string | null}
*/
export function parseTime(time, cFormat) {
if (arguments.length === 0 || !time) {
return null
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
let date
if (typeof time === 'object') {
date = time
} else {
if ((typeof time === 'string')) {
if ((/^[0-9]+$/.test(time))) {
// support "1548221490638"
time = parseInt(time)
} else {
// support safari
// https://stackoverflow.com/questions/4310953/invalid-date-in-safari
time = time.replace(new RegExp(/-/gm), '/')
}
}
if ((typeof time === 'number') && (time.toString().length === 10)) {
time = time * 1000
}
date = new Date(time)
}
const formatObj = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => {
const value = formatObj[key]
// Note: getDay() returns 0 on Sunday
if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] }
return value.toString().padStart(2, '0')
})
return time_str
}
/**
* @param {number} time
* @param {string} option
* @returns {string}
*/
export function formatTime(time, option) {
if (('' + time).length === 10) {
time = parseInt(time) * 1000
} else {
time = +time
}
const d = new Date(time)
const now = Date.now()
const diff = (now - d) / 1000
if (diff < 30) {
return '刚刚'
} else if (diff < 3600) {
// less 1 hour
return Math.ceil(diff / 60) + '分钟前'
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + '小时前'
} else if (diff < 3600 * 24 * 2) {
return '1天前'
}
if (option) {
return parseTime(time, option)
} else {
return (
d.getMonth() +
1 +
'月' +
d.getDate() +
'日' +
d.getHours() +
'时' +
d.getMinutes() +
'分'
)
}
}
/**
* @param {string} url
* @returns {Object}
*/
export function param2Obj(url) {
const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ')
if (!search) {
return {}
}
const obj = {}
const searchArr = search.split('&')
searchArr.forEach(v => {
const index = v.indexOf('=')
if (index !== -1) {
const name = v.substring(0, index)
const val = v.substring(index + 1, v.length)
obj[name] = val
}
})
return obj
}
export function debounce(func, wait, immediate) {
let timeout, args, context, timestamp, result
const later = function() {
const last = +new Date() - timestamp
if (last < wait && last > 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
if (!immediate) {
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
}
return function(...args) {
context = this
timestamp = +new Date()
const callNow = immediate && !timeout
if (!timeout) timeout = setTimeout(later, wait)
if (callNow) {
result = func.apply(context, args)
context = args = null
}
return result
}
}

View File

@ -1,78 +0,0 @@
import axios from 'axios'
import { MessageBox, Message } from 'element-ui'
import store from '@/store'
// import { BaseURL } from '../../public/config'
// create an axios instance
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
// timeout: 5000 // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
const res = response.data
// if the custom code is not 0000, it is judged as an error.
if (res.code !== '0000') {
Message({
message: res.message || 'Error',
type: 'error',
duration: 5 * 1000
})
// 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
// to re-login
MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', {
confirmButtonText: 'Re-Login',
cancelButtonText: 'Cancel',
type: 'warning'
}).then(() => {
store.dispatch('user/resetToken').then(() => {
location.reload()
})
})
}
return Promise.reject(new Error(res.message || 'Error'))
} else {
return res
}
},
error => {
console.log('err' + error) // for debug
Message({
message: error.message,
type: 'error',
duration: 5 * 1000
})
return Promise.reject(error)
}
)
export default service

View File

@ -1,16 +0,0 @@
/**
* @param {string} path
* @returns {Boolean}
*/
export function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
/**
* @param {string} str
* @returns {Boolean}
*/
export function validUsername(str) {
const valid_map = ['admin', 'editor']
return valid_map.indexOf(str.trim()) >= 0
}

View File

@ -1,228 +0,0 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__info">All rights reserved
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
<a href="" class="bullshit__return-home">Back to home</a>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message() {
return 'The webmaster said that you can not enter this page...'
}
}
}
</script>
<style lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
position: absolute;
top: 40%;
left: 50%;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
}
</style>

View File

@ -1,89 +0,0 @@
<template>
<div class="group-card">
<div class="card-wrapper">
<el-image
:src="data.url | img"
:preview-src-list="[data.url]"
fit="contain"
lazy
class="card-img"
>
<div slot="error">
<i class="el-icon-document" />
</div>
</el-image>
<div class="score">Dis {{ data.score }}</div>
<div class="footer">
<div class="title">{{ data.id }}</div>
<!-- <div class="date-time">{{ data.createTime }}</div>-->
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ImageCard',
props: {
data: {
type: Object,
required: true
}
},
data() {
return {}
}
}
</script>
<style lang='scss' scoped>
@import "~@/assets/styles/base";
.card-wrapper {
box-sizing: border-box;
width: 120px;
height: 180px;
position: relative;
.card-img {
width: 120px;
height: 120px;
}
.score {
top: 120px;
width: 120px;
@include all-height(24px);
position: absolute;
background: #42b983;
color: white;
text-align: center;
font-size: 14px;
font-weight: 500;
}
.footer {
margin-top: 24px;
@include flex-column;
font-size: 12px;
.title {
@include ellipsis(120px);
@include all-height(18px);
}
.date-time {
@include all-height(18px);
@include ellipsis(120px);
}
}
}
.group-card {
box-sizing: border-box;
margin: 16px 12px;
}
</style>

View File

@ -1,265 +0,0 @@
<template>
<div class="person-image-list">
<div class="head">
<div class="bts">
TopK<el-input v-model="topK" type="primary" style="margin:0 5px;width: 100px;" placeholder="TopK" />
<el-upload
ref="imageUploader"
name="image"
:on-change="handleFileChange"
:before-upload="handleUploadBefore"
:on-success="handleSuccess"
:on-error="removeField"
drag
:auto-upload="false"
:action="upload()"
class="avatar-uploader"
:limit="0"
list-type="picture"
accept="image/jpeg, image/jpg, image/png"
:show-file-list="false"
>
<el-input
style="margin-right: 5px;"
placeholder="Please upload an image or drag in an image."
>
<i slot="prefix" class="el-icon-search el-input__icon" />
</el-input>
</el-upload>
<div class="img-upload">
<el-image
v-if="imgUrl"
:src="imgUrl"
:preview-src-list="[imgUrl]"
fit="contain"
lazy
class="el-avatar"
/>
<div v-else style=" width:0px;border: none" />
</div>
<el-button
ref="queryButton"
icon="el-icon-search"
class="filter-item"
size="medium"
type="primary"
:disabled="!imgUrl"
@click="doFaceQuery"
>Query</el-button>
<el-button size="medium" type="primary" @click="restQuery">Reset</el-button>
</div>
</div>
<template v-if="page.total !== 0 ">
<div class="cards">
<image-card
v-for="(data, index) of pageData"
:key="index"
:data="data"
:class="{'left': index%5 === 0, 'right': index%5 === 4}"
/>
</div>
<el-pagination
style="text-align: center; position: absolute; bottom: 10px;width:100%"
:current-page.sync="page.pageNum"
:page-sizes="[20, 40, 80, 160]"
:page-size.sync="page.pageSize"
layout=" total, prev, pager,next, jumper, sizes "
:total="page.total"
/>
</template>
<empty-data
v-else-if="!imgFile || !imgFile.size"
title="You have not uploaded an image yet. Please drag an image or click to upload."
/>
<empty-data v-else title="No information found" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import EmptyData from '@/components/empty-data/EmptyData'
import tableMixin from '@/common/mixin/table-mixin'
import ImageCard from '@/views/imagesearch/component/image-card'
export default {
name: 'PersonImageTable',
components: { ImageCard, EmptyData },
mixins: [tableMixin],
data() {
return {
query: {
},
imgFile: '',
topK: '5',
tableData: [],
page: {
pageNum: 1,
pageSize: 20,
total: 0
}
}
},
computed: {
...mapGetters([
'baseApi',
'imageSearchApi'
]),
emptyParam() {
return !this.imgFile && (!this.topK || !this.topK.length)
},
pageData() {
return this.tableData.slice(
(this.page.pageNum - 1) * this.page.pageSize,
this.page.pageNum * this.page.pageSize
)
},
imgUrl() {
return this.imgFile ? this.imgFile.url : ''
}
},
async mounted() {
this.setEmptyTable()
},
methods: {
upload() {
// return this.imageSearchApi + '?topK=' + this.topK + '&type=1'
return `${process.env.VUE_APP_BASE_API}/api/search/image` + '?topK=' + this.topK
},
restQuery() {
this.imgFile = {}
this.tableData = []
},
doFaceQuery() {
this.page.pageNum = 1
this.type = 1
this.$refs.imageUploader.submit()
},
handleSuccess(response, file, fileList) {
if (response.success && response.data) {
if (response.total) {
const data = response.data
data.forEach(a => {
// a.score = (new Number(a.score) * 100).toFixed(0)
a.score = (Number(a.score)).toFixed(2)
})
this.tableData = data.sort((a, b) => {
return a.score - b.score
})
this.page.total = response.total
} else {
this.tableData = []
this.page.total = 0
}
} else {
this.tableData = []
this.page.total = 0
}
fileList[fileList.length - 1].status = 'ready'
},
handleFileChange(file, fileList) {
if (fileList[0] && fileList[0].size > 2097152) {
this.$message.warning('Please upload an image smaller than 2MB in size.')
this.$refs.imageUploader.clearFiles()
return false
}
this.imgFile = file
fileList[fileList.length - 1].status = 'ready'
},
handleUploadBefore(file) {
if (file.size > 2097152) {
this.$message.warning('Please upload an image smaller than 2MB in size.')
return false
}
},
removeField() {
this.$refs.imageUploader.clearFiles()
}
}
}
</script>
<style lang='scss'>
@import "~@/assets/styles/base";
.left {
margin-left: 0 !important;
}
.right {
margin-right: 0 !important;
}
.cards {
@include flex-row;
max-height: calc(100vh - 300px);
overflow-y: auto;
flex-wrap: wrap;
}
.el-image {
display: inline-block;
text-align: center;
background: #ccc;
color: #fff;
white-space: nowrap;
position: relative;
overflow: hidden;
vertical-align: middle;
width: 32px;
height: 32px;
line-height: 32px;
border-radius: 16px;
}
.person-image-list {
position: relative;
top: 15px;
box-sizing: border-box;
width: 800px;
height: calc(100vh - 150px);
}
.el-upload-dragger {
border: none;
border-radius: 0;
box-sizing: border-box;
width: 200px;
height: 40px;
cursor: pointer;
position: relative;
overflow: hidden;
}
.el-radio-button__inner{
color: rgba(14, 37, 60, 1);
font-weight: 400;
.iconfont {
color: rgba(14, 37, 60, 1);
font-weight: 400;
}
}
.el-upload{
border: none;
display: inherit;
}
.img-upload {
@include all-height(36px);
@include flex-row-between-center;
width: 36px;
margin: 0 0 0 8px;
img {
width: 36px;
height: 36px;
}
}
.head {
margin: 10px 0;
@include flex-row-between-center;
.bts {
@include flex-row-all-center;
}
}
</style>

View File

@ -1,30 +0,0 @@
<template>
<div class="person-image-main">
<person-image-table />
</div>
</template>
<script>
import PersonImageTable from '@/views/imagesearch/component/image-table'
export default {
name: 'ImageSearch',
components: { PersonImageTable },
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/base";
.person-image-main {
box-sizing: border-box;
height: 100%;
padding: 32px 56px;
position: relative;
}
</style>

View File

@ -1,145 +0,0 @@
<template>
<div class="app-container">
<el-row>
<el-upload
ref="upload"
name="file"
class="upload"
:action="upload()"
:on-preview="handlePreview"
:on-change="handleChange"
:on-remove="handleRemove"
:on-success="handleSuccess"
:on-error="handleError"
::limit="1"
:show-file-list="false"
:auto-upload="false"
>
<el-button slot="trigger" size="small" type="primary">Select</el-button>
<el-button
v-loading.fullscreen.lock="fullscreenLoading"
style="margin-left: 10px;"
type="success"
size="small"
element-loading-text="loading"
@click="submitUpload"
>Upload</el-button>
<div slot="tip" class="el-upload__tip">File type: zip</div>
</el-upload>
</el-row>
<el-table
v-loading="listLoading"
:data="list"
element-loading-text="Loading"
border
fit
highlight-current-row
>
<el-table-column label="ID" align="center">
<template slot-scope="scope">
{{ scope.row.id }}
</template>
</el-table-column>
<el-table-column label="Name" align="center">
<template slot-scope="scope">
{{ scope.row.name }}
</template>
</el-table-column>
<el-table-column label="Type" align="center">
<template slot-scope="scope">
{{ scope.row.suffix }}
</template>
</el-table-column>
<el-table-column label="Size" align="center">
<template slot-scope="scope">
{{ scope.row.size }}
</template>
</el-table-column>
<el-table-column label="Action" align="center">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click="extract(scope.row)">Feature</el-button>
<el-button size="mini" type="danger" @click="handleDelete(scope.row)">Delete</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
import { getStorageList, del, extract } from '@/api/localStorage'
export default {
data() {
return {
fullscreenLoading: false,
list: null,
listLoading: true
}
},
created() {
this.fetchData()
},
methods: {
upload() {
return `${process.env.VUE_APP_BASE_API}/api/localStorage/file`
},
submitUpload() {
this.fullscreenLoading = true
this.$refs.upload.submit()
},
handleRemove(file, fileList) {
console.log(file, fileList)
},
handleChange(file) {
console.log(file)
},
handlePreview(file) {
console.log(file)
},
handleSuccess() {
this.fullscreenLoading = false
this.fetchData()
},
handleError(file) {
this.fullscreenLoading = false
},
fetchData() {
this.listLoading = true
getStorageList().then(response => {
this.list = response.data
this.listLoading = false
}).catch(function(response) {
console.log(response)
})
},
handleDelete(row) {
this.$confirm('This action will delete the setting, do you want to continue?', 'Confirm', {
confirmButtonText: 'Yes',
cancelButtonText: 'No',
type: 'warning'
}).then(() => {
const id = row.id
del(id).then(response => {
this.$message({
type: 'success',
message: 'Scuccess!'
})
this.fetchData()
})
}).catch(() => {
console.log('Cancel')
})
},
extract(row) {
const id = row.id
extract(id).then(response => {
this.$message({
type: 'success',
message: 'Started, refresh the page to check the progress!'
})
})
}
}
}
</script>

View File

@ -1,89 +0,0 @@
<template>
<div class="group-card">
<div class="card-wrapper">
<el-image
:src="data.url | img"
:preview-src-list="[data.url]"
fit="contain"
lazy
class="card-img"
>
<div slot="error">
<i class="el-icon-document" />
</div>
</el-image>
<div class="score">Dis {{ data.score }}</div>
<div class="footer">
<div class="title">{{ data.id }}</div>
<!-- <div class="date-time">{{ data.createTime }}</div>-->
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ImageCard',
props: {
data: {
type: Object,
required: true
}
},
data() {
return {}
}
}
</script>
<style lang='scss' scoped>
@import "~@/assets/styles/base";
.card-wrapper {
box-sizing: border-box;
width: 120px;
height: 180px;
position: relative;
.card-img {
width: 120px;
height: 120px;
}
.score {
top: 120px;
width: 120px;
@include all-height(24px);
position: absolute;
background: #42b983;
color: white;
text-align: center;
font-size: 14px;
font-weight: 500;
}
.footer {
margin-top: 24px;
@include flex-column;
font-size: 12px;
.title {
@include ellipsis(120px);
@include all-height(18px);
}
.date-time {
@include all-height(18px);
@include ellipsis(120px);
}
}
}
.group-card {
box-sizing: border-box;
margin: 16px 12px;
}
</style>

View File

@ -1,176 +0,0 @@
<template>
<div class="image-table">
<div class="head">
<div class="bts">
<el-form ref="form" :model="form">
<el-input v-model="form.text" placeholder="Please input" class="input-with-select">
<el-select slot="prepend" v-model="form.topK" style="width:80px;" placeholder="Select">
<el-option label="Top 5" value="5" />
<el-option label="Top 10" value="10" />
<el-option label="Top 20" value="20" />
<el-option label="Top 50" value="50" />
<el-option label="Top 100" value="100" />
<el-option label="Top 200" value="200" />
</el-select>
<el-button slot="append" icon="el-icon-search" element-loading-text="loading" @click="onSubmit" />
</el-input>
</el-form>
</div>
</div>
<template v-if="page.total !== 0 ">
<div class="cards">
<image-card
v-for="(data, index) of pageData"
:key="index"
:data="data"
:class="{'left': index%5 === 0, 'right': index%5 === 4}"
/>
</div>
<el-pagination
style="text-align: center; position: absolute; bottom: 10px;width:100%"
:current-page.sync="page.pageNum"
:page-sizes="[20, 40, 80, 160]"
:page-size.sync="page.pageSize"
layout=" total, prev, pager,next, jumper, sizes "
:total="page.total"
/>
</template>
<empty-data
v-else-if="!imgFile || !imgFile.size"
title="Please enter the search text information."
/>
<empty-data v-else title="No information found" />
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { search } from '@/api/search'
import EmptyData from '@/components/empty-data/EmptyData'
import tableMixin from '@/common/mixin/table-mixin'
import ImageCard from '@/views/textsearch/component/image-card'
export default {
name: 'ImageTable',
components: { ImageCard, EmptyData },
mixins: [tableMixin],
data() {
return {
query: {
},
fullscreenLoading: false,
form: {
topK: 5,
text: '',
result: ''
},
imgFile: '',
tableData: [],
page: {
pageNum: 1,
pageSize: 20,
total: 0
}
}
},
computed: {
...mapGetters([
'baseApi',
'imageSearchApi'
]),
emptyParam() {
return !this.imgFile && (!this.topK || !this.topK.length)
},
pageData() {
return this.tableData.slice(
(this.page.pageNum - 1) * this.page.pageSize,
this.page.pageNum * this.page.pageSize
)
}
},
async mounted() {
this.setEmptyTable()
},
methods: {
onSubmit() {
this.fullscreenLoading = true
this.page.pageNum = 1
this.type = 1
search(this.form).then(response => {
this.fullscreenLoading = false
if (response.success && response.data) {
if (response.total) {
const data = response.data
data.forEach(a => {
// a.score = (new Number(a.score) * 100).toFixed(0)
a.score = (Number(a.score)).toFixed(2)
})
this.tableData = data.sort((a, b) => {
return a.score - b.score
})
this.page.total = response.total
} else {
this.tableData = []
this.page.total = 0
}
} else {
this.tableData = []
this.page.total = 0
}
})
}
}
}
</script>
<style lang='scss' scoped>
@import "~@/assets/styles/base";
.left {
margin-left: 0 !important;
}
.right {
margin-right: 0 !important;
}
.cards {
@include flex-row;
max-height: calc(100vh - 300px);
overflow-y: auto;
flex-wrap: wrap;
}
.image-table {
position: relative;
box-sizing: border-box;
width: 800px;
height: calc(100vh - 150px);
:global(.el-radio-button__inner) {
color: rgba(14, 37, 60, 1);
font-weight: 400;
.iconfont {
color: rgba(14, 37, 60, 1);
font-weight: 400;
}
}
:global(.el-upload) {
border: none;
display: inherit;
}
}
.head {
margin: 10px 0;
@include flex-row-between-center;
.bts {
@include flex-row-all-center;
}
}
</style>

View File

@ -1,30 +0,0 @@
<template>
<div class="image-main">
<image-table />
</div>
</template>
<script>
import ImageTable from '@/views/textsearch/component/image-table'
export default {
name: 'Audio',
components: { ImageTable },
data() {
return {
}
}
}
</script>
<style lang="scss" scoped>
@import "~@/assets/styles/base";
.image-main {
box-sizing: border-box;
height: 100%;
padding: 32px 56px;
position: relative;
}
</style>

View File

@ -1,5 +0,0 @@
module.exports = {
env: {
jest: true
}
}

View File

@ -1,98 +0,0 @@
import { mount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
import ElementUI from 'element-ui'
import Breadcrumb from '@/components/Breadcrumb/index.vue'
const localVue = createLocalVue()
localVue.use(VueRouter)
localVue.use(ElementUI)
const routes = [
{
path: '/',
name: 'home',
children: [{
path: 'dashboard',
name: 'dashboard'
}]
},
{
path: '/menu',
name: 'menu',
children: [{
path: 'menu1',
name: 'menu1',
meta: { title: 'menu1' },
children: [{
path: 'menu1-1',
name: 'menu1-1',
meta: { title: 'menu1-1' }
},
{
path: 'menu1-2',
name: 'menu1-2',
redirect: 'noredirect',
meta: { title: 'menu1-2' },
children: [{
path: 'menu1-2-1',
name: 'menu1-2-1',
meta: { title: 'menu1-2-1' }
},
{
path: 'menu1-2-2',
name: 'menu1-2-2'
}]
}]
}]
}]
const router = new VueRouter({
routes
})
describe('Breadcrumb.vue', () => {
const wrapper = mount(Breadcrumb, {
localVue,
router
})
it('dashboard', () => {
router.push('/dashboard')
const len = wrapper.findAll('.el-breadcrumb__inner').length
expect(len).toBe(1)
})
it('normal route', () => {
router.push('/menu/menu1')
const len = wrapper.findAll('.el-breadcrumb__inner').length
expect(len).toBe(2)
})
it('nested route', () => {
router.push('/menu/menu1/menu1-2/menu1-2-1')
const len = wrapper.findAll('.el-breadcrumb__inner').length
expect(len).toBe(4)
})
it('no meta.title', () => {
router.push('/menu/menu1/menu1-2/menu1-2-2')
const len = wrapper.findAll('.el-breadcrumb__inner').length
expect(len).toBe(3)
})
// it('click link', () => {
// router.push('/menu/menu1/menu1-2/menu1-2-2')
// const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
// const second = breadcrumbArray.at(1)
// console.log(breadcrumbArray)
// const href = second.find('a').attributes().href
// expect(href).toBe('#/menu/menu1')
// })
// it('noRedirect', () => {
// router.push('/menu/menu1/menu1-2/menu1-2-1')
// const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
// const redirectBreadcrumb = breadcrumbArray.at(2)
// expect(redirectBreadcrumb.contains('a')).toBe(false)
// })
it('last breadcrumb', () => {
router.push('/menu/menu1/menu1-2/menu1-2-1')
const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner')
const redirectBreadcrumb = breadcrumbArray.at(3)
expect(redirectBreadcrumb.contains('a')).toBe(false)
})
})

View File

@ -1,18 +0,0 @@
import { shallowMount } from '@vue/test-utils'
import Hamburger from '@/components/Hamburger/index.vue'
describe('Hamburger.vue', () => {
it('toggle click', () => {
const wrapper = shallowMount(Hamburger)
const mockFn = jest.fn()
wrapper.vm.$on('toggleClick', mockFn)
wrapper.find('.hamburger').trigger('click')
expect(mockFn).toBeCalled()
})
it('prop isActive', () => {
const wrapper = shallowMount(Hamburger)
wrapper.setProps({ isActive: true })
expect(wrapper.contains('.is-active')).toBe(true)
wrapper.setProps({ isActive: false })
expect(wrapper.contains('.is-active')).toBe(false)
})
})

View File

@ -1,22 +0,0 @@
import { shallowMount } from '@vue/test-utils'
import SvgIcon from '@/components/SvgIcon/index.vue'
describe('SvgIcon.vue', () => {
it('iconClass', () => {
const wrapper = shallowMount(SvgIcon, {
propsData: {
iconClass: 'test'
}
})
expect(wrapper.find('use').attributes().href).toBe('#icon-test')
})
it('className', () => {
const wrapper = shallowMount(SvgIcon, {
propsData: {
iconClass: 'test'
}
})
expect(wrapper.classes().length).toBe(1)
wrapper.setProps({ className: 'test' })
expect(wrapper.classes().includes('test')).toBe(true)
})
})

View File

@ -1,30 +0,0 @@
import { formatTime } from '@/utils/index.js'
describe('Utils:formatTime', () => {
const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
const retrofit = 5 * 1000
it('ten digits timestamp', () => {
expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分')
})
it('test now', () => {
expect(formatTime(+new Date() - 1)).toBe('刚刚')
})
it('less two minute', () => {
expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前')
})
it('less two hour', () => {
expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前')
})
it('less one day', () => {
expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前')
})
it('more than one day', () => {
expect(formatTime(d)).toBe('7月13日17时54分')
})
it('format', () => {
expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
})
})

View File

@ -1,14 +0,0 @@
import { param2Obj } from '@/utils/index.js'
describe('Utils:param2Obj', () => {
const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95'
it('param2Obj test', () => {
expect(param2Obj(url)).toEqual({
name: 'bill',
age: '29',
sex: '1',
field: window.btoa('test'),
key: '测试'
})
})
})

View File

@ -1,35 +0,0 @@
import { parseTime } from '@/utils/index.js'
describe('Utils:parseTime', () => {
const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01"
it('timestamp', () => {
expect(parseTime(d)).toBe('2018-07-13 17:54:01')
})
it('timestamp string', () => {
expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01')
})
it('ten digits timestamp', () => {
expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01')
})
it('new Date', () => {
expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01')
})
it('format', () => {
expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54')
expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13')
expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54')
})
it('get the day of the week', () => {
expect(parseTime(d, '{a}')).toBe('五') // 星期五
})
it('get the day of the week', () => {
expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日
})
it('empty argument', () => {
expect(parseTime()).toBeNull()
})
it('null', () => {
expect(parseTime(null)).toBeNull()
})
})

View File

@ -1,17 +0,0 @@
import { validUsername, isExternal } from '@/utils/validate.js'
describe('Utils:validate', () => {
it('validUsername', () => {
expect(validUsername('admin')).toBe(true)
expect(validUsername('editor')).toBe(true)
expect(validUsername('xxxx')).toBe(false)
})
it('isExternal', () => {
expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true)
expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true)
expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false)
expect(isExternal('/dashboard')).toBe(false)
expect(isExternal('./dashboard')).toBe(false)
expect(isExternal('dashboard')).toBe(false)
})
})

View File

@ -1,121 +0,0 @@
'use strict'
const path = require('path')
const defaultSettings = require('./src/settings.js')
function resolve(dir) {
return path.join(__dirname, dir)
}
const name = defaultSettings.title || 'vue Admin Template' // page title
// If your port is set to 80,
// use administrator privileges to execute the command line.
// For example, Mac: sudo npm run
// You can change the port by the following methods:
// port = 8090 npm run dev OR npm run dev --port = 8090
const port = process.env.port || process.env.npm_config_port || 8090 // dev port
// All configuration item explanations can be find in https://cli.vuejs.org/config/
module.exports = {
/**
* You will need to set publicPath if you plan to deploy your site under a sub path,
* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then publicPath should be set to "/bar/".
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
publicPath: '/',
outputDir: 'dist',
assetsDir: 'static',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: false,
devServer: {
port: port,
open: true,
overlay: {
warnings: false,
errors: true
},
proxy: {
'/api': {
target: process.env.VUE_APP_BASE_API,
changeOrigin: true,
pathRewrite: {
'^/api': 'api'
}
},
'/auth': {
target: process.env.VUE_APP_BASE_API,
changeOrigin: true,
pathRewrite: {
'^/auth': 'auth'
}
}
}
},
configureWebpack: {
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
name: name,
resolve: {
alias: {
'@': resolve('src')
}
}
},
chainWebpack(config) {
// it can improve the speed of the first screen, it is recommended to turn on preload
config.plugin('preload').tap(() => [
{
rel: 'preload',
// to ignore runtime.js
// https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
include: 'initial'
}
])
// when there are many pages, it will cause too many meaningless requests
config.plugins.delete('prefetch')
config
.when(process.env.NODE_ENV !== 'development',
config => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime\..*\.js$/
}])
.end()
config
.optimization.splitChunks({
chunks: 'all',
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial' // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI', // split elementUI into a single package
priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app
test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm
},
commons: {
name: 'chunk-commons',
test: resolve('src/components'), // can customize your rules
minChunks: 3, // minimum common number
priority: 5,
reuseExistingChunk: true
}
}
})
// https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk
config.optimization.runtimeChunk('single')
}
)
}
}

View File

@ -1,60 +0,0 @@
#### Common Model Loading Methods
1. How to load a model online via URL?
```text
# Use optModelUrls to load a model via URL
Criteria<Image, DetectedObjects> criteria =
Criteria.builder()
.optEngine("PaddlePaddle")
.setTypes(Image.class, DetectedObjects.class)
.optModelUrls("https://aias-home.oss-cn-beijing.aliyuncs.com/models/ocr_models/ch_ppocr_mobile_v2.0_det_infer.zip")
.optTranslator(new PpWordDetectionTranslator(new ConcurrentHashMap<String, String>()))
.optProgress(new ProgressBar())
.build();
```
2. How to load a model locally?
```text
# Use optModelPath to load a model from a zipped file
Path modelPath = Paths.get("src/test/resources/ch_ppocr_mobile_v2.0_det_infer.zip");
Criteria<Image, DetectedObjects> criteria =
Criteria.builder()
.optEngine("PaddlePaddle")
.setTypes(Image.class, DetectedObjects.class)
.optModelPath(modelPath)
.optTranslator(new PpWordDetectionTranslator(new ConcurrentHashMap<String, String>()))
.optProgress(new ProgressBar())
.build();
# Use optModelPath to load a model from a local directory
Path modelPath = Paths.get("src/test/resources/ch_ppocr_mobile_v2.0_det_infer/");
Criteria<Image, DetectedObjects> criteria =
Criteria.builder()
.optEngine("PaddlePaddle")
.setTypes(Image.class, DetectedObjects.class)
.optModelPath(modelPath)
.optTranslator(new PpWordDetectionTranslator(new ConcurrentHashMap<String, String>()))
.optProgress(new ProgressBar())
.build();
```
3. How to load a model packed into a JAR file?
```text
# Use optModelUrls to load a model
# Assuming the model is located in the JAR file at:
# BOOT-INF/classes/ch_ppocr_mobile_v2.0_det_infer.zip
Criteria<Image, DetectedObjects> criteria =
Criteria.builder()
.optEngine("PaddlePaddle")
.setTypes(Image.class, DetectedObjects.class)
.optModelUrls("jar:///ch_ppocr_mobile_v2.0_det_infer.zip")
.optTranslator(new PpWordDetectionTranslator(new ConcurrentHashMap<String, String>()))
.optProgress(new ProgressBar())
.build();
```

View File

@ -1,198 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.9.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>aias</groupId>
<artifactId>image-text-search</artifactId>
<version>0.17.0</version>
<description>AIAS Image-Text Search Project</description>
<properties>
<jna.version>5.6.0</jna.version>
<djl.version>0.17.0</djl.version>
<fastjson.version>1.2.70</fastjson.version>
<swagger.version>2.9.2</swagger.version>
<hutool.version>5.3.4</hutool.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
<!-- apache commons -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.17.2</version>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<!-- 服务器端推理引擎 -->
<dependency>
<groupId>ai.djl</groupId>
<artifactId>api</artifactId>
<version>${djl.version}</version>
</dependency>
<dependency>
<groupId>ai.djl</groupId>
<artifactId>basicdataset</artifactId>
<version>${djl.version}</version>
</dependency>
<dependency>
<groupId>ai.djl</groupId>
<artifactId>model-zoo</artifactId>
<version>${djl.version}</version>
</dependency>
<!-- Pytorch -->
<dependency>
<groupId>ai.djl.pytorch</groupId>
<artifactId>pytorch-engine</artifactId>
<version>${djl.version}</version>
</dependency>
<dependency>
<groupId>ai.djl.pytorch</groupId>
<artifactId>pytorch-model-zoo</artifactId>
<version>${djl.version}</version>
</dependency>
<!-- 向量引擎 -->
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>${jna.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.http-client</groupId>
<artifactId>google-http-client</artifactId>
<version>1.19.0</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- Swagger UI -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<!--工具包-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- zip解压缩 -->
<dependency>
<groupId>org.apache.ant</groupId>
<artifactId>ant</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>commons-cli</groupId>
<artifactId>commons-cli</artifactId>
<version>1.4</version>
</dependency>
<!-- 解析客户端操作系统、浏览器信息 -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>me.aias.MainApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -1,3 +0,0 @@
Manifest-Version: 1.0
Main-Class: me.aias.MainApplication

View File

@ -1,14 +0,0 @@
package me.aias;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}

View File

@ -1,17 +0,0 @@
package me.aias.common.constant;
/**
* @author Calvin
* @date 2021-12-12
**/
public class Constant {
// 上传压缩文件类型
// zip file type
public static String ZIP_FILE_TYPE = "zip";
// 判断window系统
// window type
public static String PC_WINDOW_TYPE = "WINDOWS";
// 判断苹果系统
// apple type
public static String PC_APPLE_TYPE = "MACOSX";
}

View File

@ -1,20 +0,0 @@
package me.aias.common.constant;
/**
* 常用静态常量
* Constants
*
* @author Calvin
* @date 2021-12-12
**/
public class Constants {
/**
* win
*/
public static final String WIN = "win";
/**
* mac
*/
public static final String MAC = "mac";
}

View File

@ -1,25 +0,0 @@
package me.aias.common.enums;
import lombok.Getter;
/**
* 状态枚举
* Status enumeration
*
* @author Calvin
* @date 2021-06-20
**/
@Getter
public enum ResEnum {
SUCCESS("0000", "success"),
ZIP_FILE_FAIL("0001", "Incorrect compression package type"),
DECOMPRESSION_FAIL("0002", "Exception occurred during decompression of the compression package"),
SYSTEM_ERROR("1001", "Internal system error");
public String KEY;
public String VALUE;
private ResEnum(String key, String value) {
this.KEY = key;
this.VALUE = value;
}
}

View File

@ -1,25 +0,0 @@
package me.aias.common.exception;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
/**
* @author Calvin
* @date 2021-12-12
**/
@Getter
public class BadRequestException extends RuntimeException{
private Integer status = BAD_REQUEST.value();
public BadRequestException(String msg){
super(msg);
}
public BadRequestException(HttpStatus status, String msg){
super(msg);
this.status = status.value();
}
}

View File

@ -1,24 +0,0 @@
package me.aias.common.exception;
import lombok.Data;
/**
* @author Calvin
* @date 2021-12-12
**/
@Data
public class BusinessException extends RuntimeException {
private String code;
private String msg;
public BusinessException(String code, String msg) {
this.code = code;
this.msg = msg;
}
public BusinessException(String msg) {
this.msg = msg;
}
}

View File

@ -1,280 +0,0 @@
package me.aias.common.milvus;
import io.milvus.client.MilvusClient;
import io.milvus.client.MilvusServiceClient;
import io.milvus.param.ConnectParam;
import java.util.Enumeration;
import java.util.Vector;
public class ConnectionPool {
private String host = ""; // Milvus 主机 - Milvus host
private int port; // Milvus 端口号 - Milvus port
private static volatile ConnectionPool uniqueInstance;
private int initialConnections = 10; // 连接池的初始大小 - initial size of the connection pool
private int incrementalConnections = 5; // 连接池自动增加的大小 - automatic increase in connection pool size
private int maxConnections = 50; // 连接池最大的大小 - maximum size of the connection pool
private Vector connections = null; // 存放连接池中连接的向量, 存放的对象为 PooledConnection
// vector that stores the connections in the connection pool, objects stored are of type PooledConnection
private ConnectionPool(String host, int port) {
this.host = host;
this.port = port;
}
public static ConnectionPool getInstance(String host, String port, boolean refresh) {
if (uniqueInstance == null || refresh) {
synchronized (ConnectionPool.class) {
if (uniqueInstance == null || refresh) {
uniqueInstance = new ConnectionPool(host, Integer.parseInt(port));
try {
uniqueInstance.createPool();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
return uniqueInstance;
}
private void createPool() { // synchronized
if (connections != null) {
return; // 假如己经创建则返回
// return if the connection pool already exists
}
// 创建保存连接的向量 , 初始时有 0 个元素
// create a vector to store the connections, initially with 0 elements
connections = new Vector();
// 根据 initialConnections 中设置的值创建连接
// create connections based on the value set in initialConnections
createConnections(this.initialConnections);
System.out.println(" Milvus连接池创建成功");
System.out.println("Milvus connection pool created successfully!");
}
private void createConnections(int numConnections) {
// 循环创建指定数目的数据库连接
// loop to create the specified number of database connections
for (int x = 0; x < numConnections; x++) {
// 是否连接池中的Milvus连接数量己经达到最大最大值由类成员 maxConnections
// if the number of Milvus connections in the connection pool has reached the maximum, break the loop. The maximum value is set by the class member maxConnections.
if (this.maxConnections > 0 && this.connections.size() >= this.maxConnections) {
break;
}
// 增加一个连接到连接池中Vector connections
// add a connection to the connection pool (Vector connections)
connections.addElement(new PooledConnection(newConnection()));
System.out.println(" Milvus连接己创建 ......");
System.out.println("Milvus connection created......");
}
}
private MilvusClient newConnection() {
// 创建一个 Milvus 客户端
// create a Milvus client
ConnectParam connectParam = ConnectParam.newBuilder()
.withHost(host)
.withPort(port)
.build();
MilvusServiceClient milvusClient = new MilvusServiceClient(connectParam);
// 返回创建的新的Milvus连接
// return the newly created Milvus connection
return milvusClient;
}
public synchronized MilvusClient getConnection() {
// 确保连接池己被创建
// ensure that the connection pool has been created
if (connections == null) {
return null; // 连接池还没创建则返回 null
// if the connection pool has not been created, return null
}
MilvusClient client = getFreeConnection(); // 获得一个可用的数据库连接
// obtain a available database connection
// 假如目前没有可以使用的连接即所有的连接都在使用中
// if there are no available connections, i.e., all connections are in use
while (client == null) {
// 等一会再试 250 ms
// wait for a while and try again in 250 ms
wait(250);
client = getFreeConnection(); // 重新再试直到获得可用的连接假如
// getFreeConnection() 返回的为 null
// 则表明创建一批连接后也不可获得可用连接
// try again until an available connection is obtained, if getFreeConnection() returns null, it means that even after creating a batch of connections, no available connections can be obtained.
}
return client; // 返回获得的可用的连接
// return the available connection obtained
}
private MilvusClient getFreeConnection() {
// 从连接池中获得一个可用的Milvus连接
// obtain an available Milvus connection from the connection pool
MilvusClient client = findFreeConnection();
if (client == null) {
// 假如目前连接池中没有可用的连接
// 创建一些连接
// if there are no available connections in the connection pool, create some connections
createConnections(incrementalConnections);
// 重新从池中查找是否有可用连接
// search again in the pool for available connections
client = findFreeConnection();
if (client == null) {
// 假如创建连接后仍获得不到可用的连接则返回 null
// if no available connections are obtained after creating new connections, return null
return null;
}
}
return client;
}
private MilvusClient findFreeConnection() {
MilvusClient client = null;
PooledConnection pConn = null;
// 获得连接池中所有的对象
// obtain all objects in the connection pool
Enumeration enumerate = connections.elements();
// 遍历所有的对象看是否有可用的连接
// traverse all the objects to see if there are any available connections
while (enumerate.hasMoreElements()) {
pConn = (PooledConnection) enumerate.nextElement();
if (!pConn.isBusy()) {
// 假如此对象不忙则获得它的数据库连接并把它设为忙
// if this object is not busy, obtain its database connection and set it as busy
client = pConn.getConnection();
pConn.setBusy(true);
break; // 己经找到一个可用的连接退出
// an available connection has been found, exit
}
}
return client; // 返回找到到的可用连接
// return the found available connection
}
public void returnConnection(MilvusClient client) {
// 确保连接池存在假如连接没有创建不存在直接返回
if (connections == null) {
System.out.println(" 连接池不存在,无法返回此连接到连接池中 !");
return;
}
PooledConnection pConn = null;
Enumeration enumerate = connections.elements();
// 遍历连接池中的所有连接找到这个要返回的连接对象
// ensure that the connection pool exists, if the connection has not been created (does not exist), return directly
while (enumerate.hasMoreElements()) {
pConn = (PooledConnection) enumerate.nextElement();
// 先找到连接池中的要返回的连接对象
// traverse all connections in the connection pool to find the connection object to be returned
if (client == pConn.getConnection()) {
// 找到了 , 设置此连接为空闲状态
// if found, set the connection to idle status
pConn.setBusy(false);
break;
}
}
}
public synchronized void refreshConnections() {
// 确保连接池己创新存在
// ensure that the connection pool has been created
if (connections == null) {
System.out.println(" 连接池不存在,无法刷新 !");
System.out.println("The connection pool does not exist, cannot refresh!");
return;
}
PooledConnection pConn = null;
Enumeration enumerate = connections.elements();
while (enumerate.hasMoreElements()) {
// 获得一个连接对象
// obtain a connection object
pConn = (PooledConnection) enumerate.nextElement();
// 假如对象忙则等 5 ,5 秒后直接刷新
// if the object is busy, wait for 5 seconds and then refresh it directly
if (pConn.isBusy()) {
wait(5000); // 5
}
// 关闭此连接用一个新的连接代替它
// close this connection and replace it with a new one.
closeConnection(pConn.getConnection());
pConn.setConnection(newConnection());
pConn.setBusy(false);
}
}
public synchronized void closeConnectionPool() {
// 确保连接池存在假如不存在返回
// ensure that the connection pool exists, if it does not exist, return
if (connections == null) {
System.out.println(" 连接池不存在,无法关闭 !");
System.out.println("The connection pool does not exist, cannot close!");
return;
}
PooledConnection pConn = null;
Enumeration enumerate = connections.elements();
while (enumerate.hasMoreElements()) {
pConn = (PooledConnection) enumerate.nextElement();
// 假如忙 5
// if busy, wait for 5 seconds
if (pConn.isBusy()) {
wait(5000); // 5
}
// 5 秒后直接关闭它
// close it directly after 5 seconds
closeConnection(pConn.getConnection());
// 从连接池向量中删除它
// remove it from the connection pool vector
connections.removeElement(pConn);
}
// 置连接池为空
// set the connection pool to null
connections = null;
}
private void closeConnection(MilvusClient client) {
client.close();
}
private void wait(int mSeconds) {
try {
Thread.sleep(mSeconds);
} catch (InterruptedException e) {
}
}
class PooledConnection {
MilvusClient client = null; // Milvus连接
// Milvus connection
boolean busy = false; // 此连接是否正在使用的标志默认没有正在使用
// flag indicating whether this connection is currently in use, default is not in use
// 构造函数根据一个 Connection 构告一个 PooledConnection 对象
// constructor that constructs a PooledConnection object based on a Connection
public PooledConnection(MilvusClient client) {
this.client = client;
}
// 返回此对象中的连接
// return the connection in this object
public MilvusClient getConnection() {
return client;
}
// 设置此对象的连接
// set the connection in this object
public void setConnection(MilvusClient client) {
this.client = client;
}
// 获得对象连接是否忙
// obtain the status of if the connection in the object is busy
public boolean isBusy() {
return busy;
}
// 设置对象的连接正在忙
// set the connection in the object as busy
public void setBusy(boolean busy) {
this.busy = busy;
}
}
}

View File

@ -1,41 +0,0 @@
package me.aias.common.utils;
import java.text.SimpleDateFormat;
/**
* 日期类
* Date Util
*
* @author Calvin
* @date 2021-12-12
**/
public class DateUtil {
public static final ThreadLocal<SimpleDateFormat> YYYY_MM_dd_HH_mm_ss =
new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static final ThreadLocal<SimpleDateFormat> YYYY_MM_dd =
new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy/MM/dd/");
}
};
public static final ThreadLocal<SimpleDateFormat> YYYYMMdd =
new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd");
}
};
public static final ThreadLocal<SimpleDateFormat> YYYYMMdd_HH_mm_ss =
new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyyMMdd_HH:mm:ss");
}
};
}

View File

@ -1,24 +0,0 @@
package me.aias.common.utils;
/**
* 特征计算类
* Feature Utils
*
* @author Calvin
* @date 2021-12-12
**/
public final class FeatureUtils {
public static float calculSimilar(float[] feature1, float[] feature2) {
float ret = 0.0f;
float mod1 = 0.0f;
float mod2 = 0.0f;
int length = feature1.length;
for (int i = 0; i < length; ++i) {
ret += feature1[i] * feature2[i];
mod1 += feature1[i] * feature1[i];
mod2 += feature2[i] * feature2[i];
}
return (float) (ret / Math.sqrt(mod1) / Math.sqrt(mod2));
}
}

View File

@ -1,421 +0,0 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.aias.common.utils;
import cn.hutool.core.util.IdUtil;
import me.aias.common.exception.BadRequestException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import java.io.*;
import java.security.MessageDigest;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* File工具类扩展 hutool 工具包
* File tool class, extending hutool tool package
*
* @author Zheng Jie
* @date 2018-12-27
*/
public class FileUtil extends cn.hutool.core.io.FileUtil {
private static final Logger log = LoggerFactory.getLogger(FileUtil.class);
/**
* 系统临时目录
* System temporary directory
* <br>
* windows 包含路径分割符但Linux 不包含,
* 在windows \\==\ 前提下
* 为安全起见 同意拼装 路径分割符
* - Windows contains path separators, but Linux does not,
* - under the premise of windows \\==\,
* - for safety, agree to assemble path separators,
* <pre>
* java.io.tmpdir
* windows : C:\Users/xxx\AppData\Local\Temp\
* linux: /temp
* </pre>
*/
public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator;
/**
* 定义GB的计算常量
* Define GB calculation constants
*/
private static final int GB = 1024 * 1024 * 1024;
/**
* 定义MB的计算常量
* Define MB calculation constants
*/
private static final int MB = 1024 * 1024;
/**
* 定义KB的计算常量
* Define KB calculation constants
*/
private static final int KB = 1024;
/**
* 格式化小数
* Format decimal
*/
private static final DecimalFormat DF = new DecimalFormat("0.00");
public static final String IMAGE = "IMAGE";
public static final String TXT = "TXT";
public static final String MUSIC = "MUSIC";
public static final String VIDEO = "VIDEO";
public static final String OTHER = "OTHER";
/**
* 根据日期生成本地图片相对保存路径
* Generate local image relative storage path based on date
*/
public static String generatePath(String fileRoot) {
Date date = new Date();
// yyyy/MM/dd/"
String relativePath = DateUtil.YYYY_MM_dd.get().format(date);
String filePath = fileRoot + relativePath;
// 如果不存在,创建文件夹
// If it does not exist, create a folder
File f = new File(filePath);
if (!f.exists()) {
f.mkdirs();
}
return relativePath;
}
/**
* MultipartFile转File
*/
public static File toFile(MultipartFile multipartFile) {
// 获取文件名
// Get file name
String fileName = multipartFile.getOriginalFilename();
// 获取文件后缀
// Get file extension
String prefix = "." + getExtensionName(fileName);
File file = null;
try {
// 用uuid作为文件名防止生成的临时文件重复
// Use uuid as file name to prevent duplicate temporary files from being generated
file = new File(SYS_TEM_DIR + IdUtil.simpleUUID() + prefix);
// MultipartFile to File
multipartFile.transferTo(file);
} catch (IOException e) {
log.error(e.getMessage(), e);
}
return file;
}
/**
* 获取文件扩展名不带 .
* Get file extension name without .
*/
public static String getExtensionName(String filename) {
if ((filename != null) && (filename.length() > 0)) {
int dot = filename.lastIndexOf('.');
if ((dot > -1) && (dot < (filename.length() - 1))) {
return filename.substring(dot + 1);
}
}
return filename;
}
/**
* Java文件操作 获取不带扩展名的文件名
* Java file operation, get the file name without extension
*/
public static String getFileNameNoEx(String filename) {
if ((filename != null) && (filename.length() > 0)) {
int dot = filename.lastIndexOf('.');
if ((dot > -1) && (dot < (filename.length()))) {
return filename.substring(0, dot);
}
}
return filename;
}
/**
* 文件大小转换
* File size conversion
*/
public static String getSize(long size) {
String resultSize;
if (size / GB >= 1) {
//如果当前Byte的值大于等于1GB
// If the current Byte value is greater than or equal to 1GB
resultSize = DF.format(size / (float) GB) + "GB ";
} else if (size / MB >= 1) {
//如果当前Byte的值大于等于1MB
// If the current Byte value is greater than or equal to 1MB
resultSize = DF.format(size / (float) MB) + "MB ";
} else if (size / KB >= 1) {
//如果当前Byte的值大于等于1KB
// If the current Byte value is greater than or equal to 1KB
resultSize = DF.format(size / (float) KB) + "KB ";
} else {
resultSize = size + "B ";
}
return resultSize;
}
/**
* inputStream File
* InputStream to File
*/
static File inputStreamToFile(InputStream ins, String name) throws Exception {
File file = new File(SYS_TEM_DIR + name);
if (file.exists()) {
return file;
}
OutputStream os = new FileOutputStream(file);
int bytesRead;
int len = 8192;
byte[] buffer = new byte[len];
while ((bytesRead = ins.read(buffer, 0, len)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
return file;
}
/**
* 将文件名解析成文件的上传路径
* Parse the file name into the upload path of the file
*/
public static File upload(MultipartFile file, String filePath) {
Date date = new Date();
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddhhmmssS");
String name = getFileNameNoEx(file.getOriginalFilename());
String suffix = getExtensionName(file.getOriginalFilename());
String nowStr = "-" + format.format(date);
try {
String fileName = name + nowStr + "." + suffix;
String path = filePath + fileName;
// getCanonicalFile 可解析正确各种路径
// getCanonicalFile can correctly parse various paths
File dest = new File(path).getCanonicalFile();
// 检测是否存在目录
// Check if the directory exists
if (!dest.getParentFile().exists()) {
if (!dest.getParentFile().mkdirs()) {
System.out.println("was not successful.");
}
}
// 文件写入
// File write
file.transferTo(dest);
return dest;
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
public static String getFileType(String type) {
String documents = "txt doc pdf ppt pps xlsx xls docx";
String music = "mp3 wav wma mpa ram ra aac aif m4a";
String video = "avi mpg mpe mpeg asf wmv mov qt rm mp4 flv m4v webm ogv ogg";
String image = "bmp dib pcp dif wmf gif jpg tif eps psd cdr iff tga pcd mpt png jpeg";
if (image.contains(type)) {
return IMAGE;
} else if (documents.contains(type)) {
return TXT;
} else if (music.contains(type)) {
return MUSIC;
} else if (video.contains(type)) {
return VIDEO;
} else {
return OTHER;
}
}
public static void checkSize(long maxSize, long size) {
// 1M
int len = 1024 * 1024;
if (size > (maxSize * len)) {
throw new BadRequestException("max size limitted");
}
}
/**
* 判断两个文件是否相同
* Determine whether two files are the same
*/
public static boolean check(File file1, File file2) {
String img1Md5 = getMd5(file1);
String img2Md5 = getMd5(file2);
return img1Md5.equals(img2Md5);
}
/**
* 判断两个文件是否相同
* Determine whether two files are the same
*/
public static boolean check(String file1Md5, String file2Md5) {
return file1Md5.equals(file2Md5);
}
public static byte[] getByte(File file) {
// 得到文件长度
// Get file length
byte[] b = new byte[(int) file.length()];
try {
InputStream in = new FileInputStream(file);
try {
System.out.println(in.read(b));
} catch (IOException e) {
log.error(e.getMessage(), e);
}
} catch (FileNotFoundException e) {
log.error(e.getMessage(), e);
return null;
}
return b;
}
/**
* 保存字节数组图片到指定path
* Save byte array picture to specified path
*/
public static void bytesToFile(byte[] bs, String filePath) throws IOException {
FileOutputStream os = new FileOutputStream(filePath);
os.write(bs);
os.close();
}
private static String getMd5(byte[] bytes) {
// 16进制字符
// 16 hexadecimal characters
char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
try {
MessageDigest mdTemp = MessageDigest.getInstance("MD5");
mdTemp.update(bytes);
byte[] md = mdTemp.digest();
int j = md.length;
char[] str = new char[j * 2];
int k = 0;
// 移位 输出字符串
// Move output string
for (byte byte0 : md) {
str[k++] = hexDigits[byte0 >>> 4 & 0xf];
str[k++] = hexDigits[byte0 & 0xf];
}
return new String(str);
} catch (Exception e) {
log.error(e.getMessage(), e);
}
return null;
}
public static String getMd5(File file) {
return getMd5(getByte(file));
}
public static boolean delete(String path) {
boolean flag = false;
File file = new File(path);
// 判断目录或文件是否存在
// Check if the directory or file exists
if (!file.exists()) { // 不存在返回 false
//Does not exist returns false
return flag;
} else {
// 判断是否为文件
// If it is a file, call the delete file method
if (file.isFile()) { // 为文件时调用删除文件方法
// If it is a file, call the delete file method
return deleteFile(path);
} else { // 为目录时调用删除目录方法
// If it is a directory, call the delete directory method
return deleteDirectory(path);
}
}
}
/**
* 删除单个文件
*Delete single file
*
* @param path 被删除文件的文件名 - path file name to be deleted
* @return 单个文件删除成功返回true否则返回false - Return true if the single file is deleted successfully, otherwise return false
*/
public static boolean deleteFile(String path) {
boolean flag = false;
File file = new File(path);
// 路径为文件且不为空则进行删除
// If the path is a file and is not empty, delete it
if (file.isFile() && file.exists()) {
file.delete();
flag = true;
}
return flag;
}
/**
* 删除目录文件夹以及目录下的文件
* Delete directory (file) and files under the directory
*
* @param path 被删除目录的文件路径 - path file path of the directory to be deleted
* @return 目录删除成功返回true否则返回false
* Return true if the directory is deleted successfully, otherwise return false
*/
public static boolean deleteDirectory(String path) {
// 如果sPath不以文件分隔符结尾自动添加文件分隔符
// If sPath does not end with a file separator, automatically add a file separator
if (!path.endsWith(File.separator)) {
path = path + File.separator;
}
File dirFile = new File(path);
// 如果dir对应的文件不存在或者不是一个目录则退出
// If dir does not exist or is not a directory, exit
if (!dirFile.exists() || !dirFile.isDirectory()) {
return false;
}
boolean flag = true;
// 删除文件夹下的所有文件(包括子目录)
// Delete all files in the folder (including subdirectories)
File[] files = dirFile.listFiles();
for (int i = 0; i < files.length; i++) {
// 删除子文件
// Delete sub files
if (files[i].isFile()) {
flag = deleteFile(files[i].getAbsolutePath());
if (!flag) break;
} // 删除子目录
// Delete subdirectories
else {
flag = deleteDirectory(files[i].getAbsolutePath());
if (!flag) break;
}
}
if (!flag) return false;
// 删除当前目录
// Delete the current directory
if (dirFile.delete()) {
return true;
} else {
return false;
}
}
}

View File

@ -1,192 +0,0 @@
package me.aias.common.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Date;
import java.util.Iterator;
/**
* 图片操作类
* Image utility class
*
* @author Calvin
* @date 2021-12-12
**/
public class ImageUtil {
private static final Logger log = LoggerFactory.getLogger(ImageUtil.class);
/**
* multipartFile 编码转换为 BufferedImage
* Convert multipartFile encoding to BufferedImage
*/
public static BufferedImage multipartFileToBufImage(MultipartFile imageFile) {
try {
InputStream ins = imageFile.getInputStream();
byte[] bytes = ImageUtil.readInputStream(ins);
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
return ImageIO.read(bais);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* MultipartFile对象转字节数组
* Convert MultipartFile object to byte array
*/
public static byte[] multipartFileToBytes(MultipartFile file) {
InputStream ins = null;
try {
ins = file.getInputStream();
byte[] bytes = ImageUtil.readInputStream(ins);
return bytes;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (ins != null) {
try {
ins.close();
} catch (Exception e) {
log.error(e.getMessage());
e.printStackTrace();
}
}
}
return null;
}
/**
* file byte数组
* Convert file to byte array
*/
public static byte[] file2Byte(File file) throws IOException {
byte[] buffer = null;
FileInputStream fis = new FileInputStream(file);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int n;
while ((n = fis.read(b)) != -1) {
bos.write(b, 0, n);
}
fis.close();
bos.close();
buffer = bos.toByteArray();
return buffer;
}
/**
* 根据地址获得数据的字节流
* Get the byte stream of the data based on the address
*/
public static byte[] getImageByUrl(String strUrl) {
try {
URL url = new URL(strUrl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5 * 1000);
InputStream inStream = conn.getInputStream(); // 通过输入流获取图片数据
// get image data through input stream
byte[] bytes = readInputStream(inStream); // 得到图片的二进制数据
// get binary data of the image
return bytes;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 从输入流中获取数据
* Get data from input stream
*/
public static byte[] readInputStream(InputStream inStream) throws Exception {
ByteArrayOutputStream outStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outStream.write(buffer, 0, len);
}
inStream.close();
return outStream.toByteArray();
}
/**
* bytes 编码转换为 BufferedImage
* Convert bytes encoding to BufferedImage
*/
public static BufferedImage bytesToBufferedImage(byte[] bytes) {
try {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
return ImageIO.read(bais);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 保存字节数组图片到指定path
* Save byte array image to specified path
*/
public static void bytesToImageFile(byte[] bs, String filePath) throws IOException {
FileOutputStream os = new FileOutputStream(filePath);
os.write(bs);
os.close();
}
/**
* 根据日期生成本地图片相对保存路径
* Generate local image relative save path based on date
*/
public static String generatePath(String fileRoot) {
Date date = new Date();
// yyyy/MM/dd/"
String relativePath = DateUtil.YYYY_MM_dd.get().format(date);
String filePath = fileRoot + relativePath;
// 如果不存在,创建文件夹
File f = new File(filePath);
if (!f.exists()) {
f.mkdirs();
}
return relativePath;
}
/**
* 获得图片的后缀例如JPEGGIF等
* Get the suffix of the image, for example: JPEG, GIF, etc.
*/
public static String getImageFormat(Object obj) {
ImageInputStream iis = null;
try {
iis = ImageIO.createImageInputStream(obj);
Iterator<ImageReader> iterator = ImageIO.getImageReaders(iis);
while (iterator.hasNext()) {
ImageReader reader = (ImageReader) iterator.next();
String suffix = reader.getFormatName();
return suffix.toLowerCase();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (iis != null) {
try {
iis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}

View File

@ -1,17 +0,0 @@
package me.aias.common.utils;
import java.util.UUID;
/**
* 获取UUID
* Get UUID
*
* @author Calvin
* @date 2021-12-12
**/
public class UUIDUtil {
public static String getUUID() {
String uuid = UUID.randomUUID().toString();
return uuid.replaceAll("-", "");
}
}

View File

@ -1,40 +0,0 @@
package me.aias.common.utils;
import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import javax.servlet.http.HttpServletRequest;
import java.net.InetAddress;
import java.net.UnknownHostException;
/**
* 获取系统信息
* Get system info
*
* @author Calvin
* @date 2021-12-12
**/
@Slf4j
public class UserAgentUtil {
private UserAgent userAgent;
private String userAgentString;
private HttpServletRequest request;
public UserAgentUtil(HttpServletRequest request) {
this.request = request;
userAgentString = request.getHeader("User-Agent");
userAgent = UserAgent.parseUserAgentString(userAgentString);
}
/**
* 获取操作系统
* Get os info
*
*/
public String getOS() {
if (null == userAgent) {
return "Unknown";
}
return userAgent.getOperatingSystem().getName();
}
}

View File

@ -1,93 +0,0 @@
package me.aias.common.utils;
import lombok.extern.slf4j.Slf4j;
import me.aias.common.constant.Constant;
import me.aias.common.exception.BusinessException;
import me.aias.domain.ResEnum;
import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipFile;
import java.io.*;
import java.util.Enumeration;
@Slf4j
public class ZipUtil {
private static final int BUFFER = 2048;
/**
* 解压Zip文件
* unzip file
*/
public static void unZip(String receivedZipFile, String osName, String filePath) {
int count = -1;
InputStream is = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
ZipFile zipFile = null;
try {
if (osName.toUpperCase().contains(Constant.PC_WINDOW_TYPE)) {
zipFile = new ZipFile(receivedZipFile, "gbk");
} else {
zipFile = new ZipFile(receivedZipFile, "UTF8");
}
Enumeration<?> entries = zipFile.getEntries();
while (entries.hasMoreElements()) {
byte[] buf = new byte[BUFFER];
ZipEntry entry = (ZipEntry) entries.nextElement();
String filename = entry.getName();
boolean ismkdir = false;
if (!osName.toUpperCase().contains(Constant.PC_WINDOW_TYPE)) {
if (filename.toUpperCase().contains(Constant.PC_APPLE_TYPE)) {
continue;
}
}
if (filename.lastIndexOf("/") != -1) {
filename = filename.substring(filename.lastIndexOf("/"), filename.length());
ismkdir = false;
}
filename = filePath + File.separator + filename;
if (entry.isDirectory()) {
continue;
}
File file = new File(filename);
if (!file.exists()) {
if (ismkdir) {
new File(filename.substring(0, filename.lastIndexOf("/"))).mkdirs();
}
}
file.createNewFile();
is = zipFile.getInputStream(entry);
fos = new FileOutputStream(file);
bos = new BufferedOutputStream(fos, BUFFER);
while ((count = is.read(buf)) > -1) {
bos.write(buf, 0, count);
}
bos.flush();
}
} catch (IOException e) {
log.error("zip file exception {}", e.getMessage());
throw new BusinessException(
ResEnum.PACKAGE_DECOMPRESSION_FAIL.KEY, ResEnum.PACKAGE_DECOMPRESSION_FAIL.VALUE);
} finally {
try {
if (bos != null) {
bos.close();
}
if (fos != null) {
fos.close();
}
if (is != null) {
is.close();
}
if (zipFile != null) {
zipFile.close();
}
} catch (Exception e) {
log.error("zip file exception {}", e.getMessage());
throw new BusinessException(
ResEnum.PACKAGE_DECOMPRESSION_FAIL.KEY, ResEnum.PACKAGE_DECOMPRESSION_FAIL.VALUE);
}
}
}
}

View File

@ -1,77 +0,0 @@
package me.aias.config;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
/**
* @author Calvin
* @date 2021-12-12
**/
@Configuration
@EnableWebMvc
public class ConfigurerAdapter implements WebMvcConfigurer {
/**
* 文件配置
* File configuration.
* */
private final FileProperties properties;
public ConfigurerAdapter(FileProperties properties) {
this.properties = properties;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
FileProperties.ElPath path = properties.getPath();
String pathUtl = "file:" + path.getPath().replace("\\", "/");
String imageRootPath = "file:" + path.getRootPath().replace("\\", "/");
registry.addResourceHandler("/file/**").addResourceLocations(pathUtl).setCachePeriod(0);
registry.addResourceHandler("/files/**").addResourceLocations(imageRootPath).setCachePeriod(0);
registry.addResourceHandler("/**").addResourceLocations("classpath:/META-INF/resources/").setCachePeriod(0);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 使用 fastjson 序列化会导致 @JsonIgnore 失效可以使用 @JSONField(serialize = false) 替换
// Use fastjson serialization, which will cause @JsonIgnore to be invalid, can be replaced with @JSONField (serialize = false)
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
List<MediaType> supportMediaTypeList = new ArrayList<>();
supportMediaTypeList.add(MediaType.APPLICATION_JSON_UTF8);
FastJsonConfig config = new FastJsonConfig();
config.setDateFormat("yyyy-MM-dd HH:mm:ss");
config.setSerializerFeatures(SerializerFeature.DisableCircularReferenceDetect);
converter.setFastJsonConfig(config);
converter.setSupportedMediaTypes(supportMediaTypeList);
converter.setDefaultCharset(StandardCharsets.UTF_8);
converters.add(converter);
}
}

View File

@ -1,48 +0,0 @@
package me.aias.config;
import lombok.Data;
import me.aias.common.constant.Constants;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/**
* 文件类
* File Properties
*
* @author Calvin
* @date 2021-12-12
**/
@Data
@Configuration
@ConfigurationProperties(prefix = "file")
public class FileProperties {
// 文件大小限制
// File size limit
private Long maxSize;
private ElPath mac;
private ElPath linux;
private ElPath windows;
public ElPath getPath(){
String os = System.getProperty("os.name");
if(os.toLowerCase().startsWith(Constants.WIN)) {
return windows;
} else if(os.toLowerCase().startsWith(Constants.MAC)){
return mac;
}
return linux;
}
@Data
public static class ElPath{
private String path;
private String rootPath;
}
}

View File

@ -1,45 +0,0 @@
package me.aias.config;
import ai.djl.MalformedModelException;
import ai.djl.repository.zoo.ModelNotFoundException;
import me.aias.model.ImageEncoderModel;
import me.aias.model.TextEncoderModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
/**
* 模型实例
* Model Instance
*
* @author Calvin
* @date 2021-12-19
**/
@Configuration
public class ModelConfiguration {
// Text Model
@Value("${model.textModel}")
private String textModel;
@Value("${model.chinese}")
private boolean chinese;
// Image Model
@Value("${model.imageModel}")
private String imageModel;
@Bean
public TextEncoderModel textEncoderModel() throws IOException, ModelNotFoundException, MalformedModelException {
TextEncoderModel textEncoderModel = new TextEncoderModel();
textEncoderModel.init(textModel, chinese);
return textEncoderModel;
}
@Bean
public ImageEncoderModel imageEncoderModel() throws IOException, ModelNotFoundException, MalformedModelException {
ImageEncoderModel imageEncoderModel = new ImageEncoderModel();
imageEncoderModel.init(imageModel);
return imageEncoderModel;
}
}

View File

@ -1,96 +0,0 @@
package me.aias.config;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Predicates;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.data.domain.Pageable;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ParameterBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.schema.AlternateTypeRule;
import springfox.documentation.schema.AlternateTypeRuleConvention;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
import static com.google.common.collect.Lists.newArrayList;
import static springfox.documentation.schema.AlternateTypeRules.newRule;
/**
* @author Calvin
* @date 2021-06-20
**/
@Configuration
@EnableSwagger2
public class SwaggerConfig {
@Value("${swagger.enabled}")
private Boolean enabled;
@Bean
@SuppressWarnings("all")
public Docket createRestApi() {
ParameterBuilder ticketPar = new ParameterBuilder();
return new Docket(DocumentationType.SWAGGER_2)
.enable(enabled)
.apiInfo(apiInfo())
.select()
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.description("")
.title("API Doc")
.version("1.0")
.build();
}
}
/**
* 将Pageable转换展示在swagger中
* Convert Pageable for display in Swagger
*/
@Configuration
class SwaggerDataConfig {
@Bean
public AlternateTypeRuleConvention pageableConvention(final TypeResolver resolver) {
return new AlternateTypeRuleConvention() {
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public List<AlternateTypeRule> rules() {
return newArrayList(newRule(resolver.resolve(Pageable.class), resolver.resolve(Page.class)));
}
};
}
@ApiModel
@Data
private static class Page {
@ApiModelProperty("Page number (0..N)")
private Integer page;
@ApiModelProperty("Number of items per page")
private Integer size;
@ApiModelProperty("Sort criteria in the following format: property[,asc|desc]. Default sort order is ascending. Multiple sort conditions are supported, such as id,asc")
private List<String> sort;
}
}

View File

@ -1,142 +0,0 @@
package me.aias.controller;
import ai.djl.ModelException;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import ai.djl.translate.TranslateException;
import io.milvus.param.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.aias.common.constant.Constant;
import me.aias.common.utils.UUIDUtil;
import me.aias.common.utils.UserAgentUtil;
import me.aias.common.utils.ZipUtil;
import me.aias.config.FileProperties;
import me.aias.domain.DataInfo;
import me.aias.domain.LocalStorage;
import me.aias.domain.ResEnum;
import me.aias.domain.ResultRes;
import me.aias.service.DataService;
import me.aias.service.FeatureService;
import me.aias.service.LocalStorageService;
import me.aias.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 视频管理
* Video management
*
* @author Calvin
* @date 2021-12-12
**/
@Slf4j
@RestController
@RequiredArgsConstructor
@Api(tags = "数据管理 - Data Management")
@RequestMapping("/api/data")
public class DataController {
private final FileProperties properties;
@Autowired
private DataService dataService;
@Autowired
private FeatureService featureService;
@Autowired
private SearchService searchService;
@Autowired
private LocalStorageService localStorageService;
@ApiOperation(value = "提取特征 - Extract Features")
@GetMapping("/extractFeatures")
public ResponseEntity<Object> extractFeatures(@RequestParam(value = "id") String id, HttpServletRequest request) throws IOException {
LocalStorage localStorage = localStorageService.findById(Integer.parseInt(id));
String suffix = localStorage.getSuffix();
if (!Constant.ZIP_FILE_TYPE.equalsIgnoreCase(suffix.toUpperCase())) {
return new ResponseEntity<>(ResultRes.error(ResEnum.PACKAGE_FILE_FAIL.KEY, ResEnum.PACKAGE_FILE_FAIL.VALUE), HttpStatus.OK);
}
UserAgentUtil userAgentGetter = new UserAgentUtil(request);
String os = userAgentGetter.getOS();
if (!new File(properties.getPath().getRootPath()).exists()) {
new File(properties.getPath().getRootPath()).mkdirs();
}
//生成UUID作为解压缩的目录
// Generate UUID as the directory for unzipping
String UUID = UUIDUtil.getUUID();
String unZipFilePath = properties.getPath().getRootPath() + UUID;
if (!new File(unZipFilePath).exists()) {
new File(unZipFilePath).mkdirs();
}
ZipUtil.unZip(localStorage.getPath(), os, unZipFilePath);
//生成视频文件提取的图片帧目录
// Generate the directory for the extracted image frames
String imagesPath = properties.getPath().getRootPath();
if (!new File(imagesPath).exists()) {
new File(imagesPath).mkdirs();
}
List<DataInfo> dataInfoList = dataService.uploadData(properties.getPath().getRootPath(), UUID);
// 抓取图像画面
// Capture the image screen
try {
List<Long> vectorIds = new ArrayList<>();
List<List<Float>> vectors = new ArrayList<>();
for (DataInfo dataInfo : dataInfoList) {
Path imageFile = Paths.get(dataInfo.getFullPath());
Image image = ImageFactory.getInstance().fromFile(imageFile);
List<Float> feature = featureService.imageFeature(image);
ConcurrentHashMap<String, String> map = dataService.getMap();
int size = map.size();
dataService.addData(String.valueOf(size + 1), dataInfo.getRelativePath());
vectorIds.add(Long.valueOf(size + 1));
vectors.add(feature);
}
// 将向量插入Milvus向量引擎
// Insert the vectors into the Milvus vector engine
try {
R<Boolean> response = searchService.hasCollection();
if (!response.getData()) {
searchService.initSearchEngine();
}
searchService.insert(vectorIds, vectors);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
return new ResponseEntity<>(ResultRes.error(ResEnum.MILVUS_CONNECTION_ERROR.KEY, ResEnum.MILVUS_CONNECTION_ERROR.VALUE), HttpStatus.OK);
}
} catch (IOException e) {
e.printStackTrace();
} catch (ModelException e) {
e.printStackTrace();
} catch (TranslateException e) {
e.printStackTrace();
}
return new ResponseEntity<>(ResultRes.success(), HttpStatus.OK);
}
}

View File

@ -1,82 +0,0 @@
package me.aias.controller;
import cn.hutool.core.util.ObjectUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import me.aias.common.exception.BadRequestException;
import me.aias.common.utils.FileUtil;
import me.aias.config.FileProperties;
import me.aias.domain.LocalStorage;
import me.aias.domain.ResEnum;
import me.aias.domain.ResultBean;
import me.aias.domain.ResultRes;
import me.aias.service.LocalStorageService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.List;
/**
* 存储管理
* Storage management
*
* @author Calvin
* @date 2021-12-12
**/
@RestController
@RequiredArgsConstructor
@Api(tags = "存储管理 - Storage management")
@RequestMapping("/api/localStorage")
public class LocalStorageController {
private final LocalStorageService localStorageService;
private final FileProperties properties;
@ApiOperation("查询文件列表 - Query file list")
@GetMapping("/list")
public ResponseEntity<Object> getContact() {
List<LocalStorage> result = localStorageService.getStorageList();
return new ResponseEntity<>(ResultRes.success(result, result.size()), HttpStatus.OK);
}
@ApiOperation("上传文件 - Upload file")
@PostMapping("/file")
public ResponseEntity<Object> create(@RequestParam("file") MultipartFile multipartFile) {
FileUtil.checkSize(properties.getMaxSize(), multipartFile.getSize());
String suffix = FileUtil.getExtensionName(multipartFile.getOriginalFilename());
String type = FileUtil.getFileType(suffix);
File file = FileUtil.upload(multipartFile, properties.getPath().getPath() + type + File.separator);
if (ObjectUtil.isNull(file)) {
return new ResponseEntity<>(ResultRes.error(ResEnum.UPLOAD_FAIL.KEY, ResEnum.UPLOAD_FAIL.VALUE), HttpStatus.OK);
}
try {
LocalStorage localStorage = new LocalStorage(
file.getName(),
FileUtil.getFileNameNoEx(multipartFile.getOriginalFilename()),
suffix,
file.getPath(),
type,
FileUtil.getSize(multipartFile.getSize())
);
localStorageService.addStorageFile(localStorage);
} catch (Exception e) {
FileUtil.del(file);
return new ResponseEntity<>(ResultRes.error(ResEnum.UPLOAD_FAIL.KEY, ResEnum.UPLOAD_FAIL.VALUE), HttpStatus.OK);
}
return new ResponseEntity<>(ResultRes.success(), HttpStatus.OK);
}
@ApiOperation("删除文件 - Delete file")
@DeleteMapping
public ResponseEntity<Object> delete(@RequestBody LocalStorage localStorage) {
LocalStorage storage = localStorageService.findById(localStorage.getId());
FileUtil.del(storage.getPath());
localStorageService.delete(localStorage.getId());
return new ResponseEntity<>(ResultRes.success(), HttpStatus.OK);
}
}

View File

@ -1,150 +0,0 @@
package me.aias.controller;
import ai.djl.modality.cv.Image;
import ai.djl.modality.cv.ImageFactory;
import com.google.common.collect.Lists;
import io.milvus.Response.SearchResultsWrapper;
import io.milvus.grpc.SearchResults;
import io.milvus.param.R;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.aias.common.utils.ImageUtil;
import me.aias.domain.DataInfoRes;
import me.aias.domain.ResEnum;
import me.aias.domain.ResultRes;
import me.aias.service.DataService;
import me.aias.service.FeatureService;
import me.aias.service.SearchService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 搜索管理
* Search management
*
* @author Calvin
* @date 2021-12-19
**/
@Slf4j
@Api(tags = "搜索管理 - Search management")
@RequestMapping("/api/search")
@RequiredArgsConstructor
@RestController
public class SearchController {
@Autowired
private SearchService searchService;
@Autowired
private DataService imageService;
@Autowired
private FeatureService featureService;
@Value("${search.collectionName}")
String collectionName;
@Value("${image.baseUrl}")
String baseUrl;
@GetMapping("/text")
@ApiOperation(value = "search", nickname = "search")
public ResponseEntity<Object> searchImage(@RequestParam("text") String text, @RequestParam(value = "topK") String topk) {
// 生成向量
Integer topK = Integer.parseInt(topk);
List<Float> vectorToSearch;
try {
vectorToSearch = featureService.textFeature(text);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
return new ResponseEntity<>(ResultRes.error(ResEnum.MODEL_ERROR.KEY, ResEnum.MODEL_ERROR.VALUE), HttpStatus.OK);
}
List<List<Float>> vectorsToSearch = new ArrayList<>();
vectorsToSearch.add(vectorToSearch);
try {
// 根据向量搜索
// Search by vectors
R<SearchResults> searchResponse = searchService.search(topK, vectorsToSearch);
SearchResultsWrapper wrapper = new SearchResultsWrapper(searchResponse.getData().getResults());
List<SearchResultsWrapper.IDScore> scores = wrapper.getIDScore(0);
List<Long> idList = Lists.transform(scores, (entity) -> {
return entity.getLongID();
});
// 根据ID获取图片信息
// Get image information by ID
ConcurrentHashMap<String, String> map = imageService.getMap();
List<DataInfoRes> imageInfoResList = new ArrayList<>();
for (SearchResultsWrapper.IDScore score : scores) {
DataInfoRes imageInfoRes = new DataInfoRes();
imageInfoRes.setScore(score.getScore());
imageInfoRes.setId(score.getLongID());
imageInfoRes.setUrl(baseUrl + map.get("" + score.getLongID()));
imageInfoResList.add(imageInfoRes);
}
return new ResponseEntity<>(ResultRes.success(imageInfoResList, imageInfoResList.size()), HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
return new ResponseEntity<>(ResultRes.error(ResEnum.MILVUS_CONNECTION_ERROR.KEY, ResEnum.MILVUS_CONNECTION_ERROR.VALUE), HttpStatus.OK);
}
}
@PostMapping(value = "/image")
@ApiOperation(value = "searchImage", nickname = "searchImage")
public ResponseEntity<Object> searchImage(@RequestParam("image") MultipartFile imageFile, @RequestParam(value = "topK") String topk) {
BufferedImage bufferedImage = ImageUtil.multipartFileToBufImage(imageFile);
Integer topK = Integer.parseInt(topk);
List<Float> vectorToSearch;
try {
//特征提取
// Feature extraction
Image image = ImageFactory.getInstance().fromImage(bufferedImage);
vectorToSearch = featureService.imageFeature(image);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
return new ResponseEntity<>(ResultRes.error(ResEnum.MODEL_ERROR.KEY, ResEnum.MODEL_ERROR.VALUE), HttpStatus.OK);
}
List<List<Float>> vectorsToSearch = new ArrayList<>();
vectorsToSearch.add(vectorToSearch);
try {
// 根据图片向量搜索
// Search by image vectors
R<SearchResults> searchResponse = searchService.search(topK, vectorsToSearch);
SearchResultsWrapper wrapper = new SearchResultsWrapper(searchResponse.getData().getResults());
List<SearchResultsWrapper.IDScore> scores = wrapper.getIDScore(0);
// 根据ID获取图片信息
// Get image information by ID
ConcurrentHashMap<String, String> map = imageService.getMap();
List<DataInfoRes> imageInfoResList = new ArrayList<>();
for (SearchResultsWrapper.IDScore score : scores) {
DataInfoRes imageInfoRes = new DataInfoRes();
imageInfoRes.setScore(score.getScore());
imageInfoRes.setId(score.getLongID());
imageInfoRes.setUrl(baseUrl + map.get("" + score.getLongID()));
imageInfoResList.add(imageInfoRes);
}
return new ResponseEntity<>(ResultRes.success(imageInfoResList, imageInfoResList.size()), HttpStatus.OK);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage());
return new ResponseEntity<>(ResultRes.error(ResEnum.MILVUS_CONNECTION_ERROR.KEY, ResEnum.MILVUS_CONNECTION_ERROR.VALUE), HttpStatus.OK);
}
}
}

View File

@ -1,36 +0,0 @@
package me.aias.domain;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.Date;
/**
* 数据信息对象
* Data info object
*/
@Data
@ApiModel(value = "DataInfo", description = "DataInfo")
public class DataInfo {
@ApiModelProperty(value = "id", name = "id")
private Long id;
@ApiModelProperty(value = "score", name = "score")
private Float score;
@ApiModelProperty(value = "uuid", name = "uuid")
private String uuid;
@ApiModelProperty(value = "preName", name = "preName")
private String preName;
@ApiModelProperty(value = "fullPath", name = "fullPath")
private String fullPath;
@ApiModelProperty(value = "relativePath", name = "relativePath")
private String relativePath;
@ApiModelProperty(value = "createTime", name = "createTime")
private Date createTime;
}

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