mirror of
https://gitee.com/mymagicpower/AIAS.git
synced 2024-12-02 04:08:21 +08:00
no message
This commit is contained in:
parent
3cf2db8692
commit
9456045db4
@ -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)
|
||||
|
@ -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.引擎配置(包括CPU,GPU在线自动加载,及本地配置):
|
||||
- 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
|
@ -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
|
@ -1,6 +0,0 @@
|
||||
# just a flag
|
||||
ENV = 'development'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = 'http://127.0.0.1:8089'
|
||||
|
@ -1,6 +0,0 @@
|
||||
# just a flag
|
||||
ENV = 'production'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = 'http://127.0.0.1:8089'
|
||||
|
@ -1,8 +0,0 @@
|
||||
NODE_ENV = production
|
||||
|
||||
# just a flag
|
||||
ENV = 'staging'
|
||||
|
||||
# base api
|
||||
VUE_APP_BASE_API = '/stage-api'
|
||||
|
@ -1,4 +0,0 @@
|
||||
build/*.js
|
||||
src/assets
|
||||
public
|
||||
dist
|
@ -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']
|
||||
}
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
language: node_js
|
||||
node_js: 10
|
||||
script: npm run test
|
||||
notifications:
|
||||
email: false
|
@ -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']
|
||||
}
|
||||
}
|
||||
}
|
@ -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}`)
|
||||
}
|
@ -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/'
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
@ -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"
|
||||
}
|
@ -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 |
@ -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>
|
@ -1,11 +0,0 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'App'
|
||||
}
|
||||
</script>
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 }
|
@ -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 |
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
},
|
||||
},
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -1,3 +0,0 @@
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as Sidebar } from './Sidebar'
|
||||
export { default as AppMain } from './AppMain'
|
@ -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>
|
@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
@ -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()
|
||||
})
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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%;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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}`
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
||||
}
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
}
|
@ -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)
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
@ -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')
|
||||
})
|
||||
})
|
@ -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: '测试'
|
||||
})
|
||||
})
|
||||
})
|
@ -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()
|
||||
})
|
||||
})
|
@ -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)
|
||||
})
|
||||
})
|
@ -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')
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -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();
|
||||
```
|
||||
|
||||
|
@ -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>
|
@ -1,3 +0,0 @@
|
||||
Manifest-Version: 1.0
|
||||
Main-Class: me.aias.MainApplication
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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";
|
||||
}
|
@ -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";
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
};
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得图片的后缀,例如:JPEG、GIF等
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -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("-", "");
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user