├── .gitattributes ├── README.md ├── backend ├── __pycache__ │ ├── demo.cpython-37.pyc │ └── utils.cpython-37.pyc ├── demo.py ├── main.py ├── outputs │ └── sentiment_results.json ├── textresource │ ├── test_hotel.txt │ └── test_hotel_small.txt └── utils.py ├── frontend ├── .editorconfig ├── .env.development ├── .env.production ├── .env.staging ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .travis.yml ├── LICENSE ├── README-zh.md ├── README.md ├── babel.config.js ├── build │ └── index.js ├── jest.config.js ├── jsconfig.json ├── mock │ ├── index.js │ ├── mock-server.js │ ├── table.js │ ├── user.js │ └── utils.js ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ ├── table.js │ │ └── user.js │ ├── assets │ │ ├── 404_images │ │ │ ├── 404.png │ │ │ └── 404_cloud.png │ │ └── img │ │ │ ├── image1.jpg │ │ │ └── image2.jpg │ ├── components │ │ ├── Breadcrumb │ │ │ └── index.vue │ │ ├── Hamburger │ │ │ └── index.vue │ │ └── SvgIcon │ │ │ └── index.vue │ ├── icons │ │ ├── index.js │ │ ├── svg │ │ │ ├── dashboard.svg │ │ │ ├── example.svg │ │ │ ├── eye-open.svg │ │ │ ├── eye.svg │ │ │ ├── form.svg │ │ │ ├── link.svg │ │ │ ├── nested.svg │ │ │ ├── password.svg │ │ │ ├── table.svg │ │ │ ├── tree.svg │ │ │ └── user.svg │ │ └── svgo.yml │ ├── layout │ │ ├── components │ │ │ ├── AppMain.vue │ │ │ ├── Navbar.vue │ │ │ ├── Sidebar │ │ │ │ ├── FixiOSBug.js │ │ │ │ ├── Item.vue │ │ │ │ ├── Link.vue │ │ │ │ ├── Logo.vue │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.vue │ │ │ └── index.js │ │ ├── index.vue │ │ └── mixin │ │ │ └── ResizeHandler.js │ ├── main.js │ ├── permission.js │ ├── router │ │ └── index.js │ ├── settings.js │ ├── store │ │ ├── getters.js │ │ ├── index.js │ │ └── modules │ │ │ ├── app.js │ │ │ ├── settings.js │ │ │ └── user.js │ ├── styles │ │ ├── element-ui.scss │ │ ├── index.scss │ │ ├── mixin.scss │ │ ├── sidebar.scss │ │ ├── transition.scss │ │ └── variables.scss │ ├── utils │ │ ├── Excel.js │ │ ├── auth.js │ │ ├── get-page-title.js │ │ ├── index.js │ │ ├── request.js │ │ └── validate.js │ └── views │ │ ├── 404.vue │ │ ├── batchAnalysis │ │ └── index.vue │ │ ├── dashboard │ │ └── index.vue │ │ ├── login │ │ └── index.vue │ │ └── singleAnalysis │ │ └── index.vue ├── tests │ └── unit │ │ ├── .eslintrc.js │ │ ├── components │ │ ├── Breadcrumb.spec.js │ │ ├── Hamburger.spec.js │ │ └── SvgIcon.spec.js │ │ └── utils │ │ ├── formatTime.spec.js │ │ ├── param2Obj.spec.js │ │ ├── parseTime.spec.js │ │ └── validate.spec.js └── vue.config.js └── pic ├── pic1.png ├── pic10.png ├── pic2.png ├── pic3.png ├── pic4.png ├── pic5.png ├── pic6.png ├── pic7.png ├── pic8.png └── pic9.png /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 项目名称:基于UIE的舆论情感分析Web系统 2 | 3 | ![pic1](./pic/pic1.png) 4 | 5 | 项目教程:https://aistudio.baidu.com/aistudio/projectdetail/5857236 6 | 7 | Github项目地址:https://github.com/JIANG-HS/UIE-SentimentAnalysisWeb 8 | 9 | 演示视频传送门: https://www.bilibili.com/video/BV1ug4y1g71L/?spm_id_from=333.999.0.0&vd_source=18c1d4290ab8e6f5101b518491603fc3 10 | 11 | # 一. 项目简介 12 | 基于UIE的舆论情感分析Web系统,前后端分离式架构部署,支持单文本属性级情感分析及上传txt文件进行批量情感分析,并支持分析结果的可视化展示。 13 | 技术栈:后端:FastAPI + UIE;前端:Vue + ElementUI + Echarts。 14 | 15 | # 二. 项目目录结构 16 | 17 | 项目采用前后端分离式架构,分为frontend和backend两个文件夹 18 | 19 | * backend文件夹为后端接口服务模块,demo.py为模型预测演示程序,main.py为后端接口服务主程序,utils.py定义一些工具函数。 20 | * frontend文件夹为情感分析系统前端界面模块,基于 vue-admin-template进行开发。 核心关注src/router/index.js和src/views/两大模块,router中定义了界面路由即侧边栏选项框及映射关系,views文件夹下为搭建的新Web界面,包含欢迎页、单文本情感分析界面和批量文本情感分析界面。 21 | 22 | # 三. 项目环境配置 23 | ## 3.1 后端服务环境配置 24 | 25 | 首先需要下载安装Python包管理器Anaconda:https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/ 26 | 访问镜像下载网站,根据自己电脑系统(win64或Linux等)选择合适的版本,建议选择较新的版本。 27 | 28 | 配置清华源镜像加速 29 | 30 | ```bash 31 | conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/ 32 | conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/ 33 | conda config --set show_channel_urls yes 34 | ``` 35 | 36 | 创建新虚拟环境便于隔离,环境名为paddlepaddle,python版本为3.7 37 | 38 | ```bash 39 | conda create -n paddlepaddle python=3.7 40 | ``` 41 | 42 | 进入刚才创建的虚拟环境paddlepaddle,需注意后续环境配置操作都将在该环境中进行配置!!! 43 | 44 | ```bash 45 | activate paddlepaddle 46 | ``` 47 | 48 | 下载paddle,建议安装GPU版本性能更优。简化配置的话也可以选择下载CPU版本 49 | paddle官网下载地址(根据型号等进行选择): 50 | https://www.paddlepaddle.org.cn/install/quick?docurl=/documentation/docs/zh/install/pip/linux-pip.html 51 | 若安装GPU版本需要先配置cuda和cudnn,参考教程: 52 | https://aistudio.baidu.com/aistudio/projectdetail/696822?channelType=0&channel=0 53 | 下面给出PaddlePaddle 2.3 CPU版本 Windows下pip的下载命令(具体建议以官网提供的为准): 54 | 55 | ```bash 56 | python -m pip install paddlepaddle==2.3.2 -i https://pypi.tuna.tsinghua.edu.cn/simple 57 | ``` 58 | 59 | paddle测试是否安装成功,在命令行中依次输入: 60 | 61 | ```python 62 | python 63 | import paddle 64 | paddle.utils.run_check() 65 | ``` 66 | 67 | 若提示 “PaddlePaddle is installed successfully!” 则安装成功! 68 | 69 | 下载最新版PaddleNLP,若出现不兼容问题可以考虑指定版本降级 70 | 71 | ```bash 72 | pip install --upgrade paddlenlp 73 | ``` 74 | 75 | 下载后端依赖Web框架FastAPI 76 | 77 | ```bash 78 | pip install fastapi 79 | pip install "uvicorn[standard]" 80 | pip install python-multipart 81 | ``` 82 | 83 | 下载pandas读取excel文件依赖库 84 | ```bash 85 | pip install openpyxl 86 | ``` 87 | 88 | 启动后端项目: 89 | 通过cd命令进行项目backend文件夹,运行demo演示程序进行测试 90 | ```bash 91 | python demo.py 92 | ``` 93 | 94 | 通过cd命令进行项目backend文件夹,启动后端接口服务! 95 | ```bash 96 | python main.py 97 | ``` 98 | ps: 初次启动会进行一次模型预测操作进行预热,时间会稍久些但可有效提高后续接口访问的性能。看到“Application startup complete”和“Uvicorn ruuning on http:127.0.0.1:8000”代表后端项目启动成功 99 | 100 | 接口调试可以下载安装Postman软件便于后端Restful API接口的访问测试。 101 | Postman使用参考:https://mp.weixin.qq.com/s/IoseF-2Ma8mH2gdQLn1rUA 102 | 103 | ## 3.2 前端项目环境配置: 104 | 105 | 建议下载个前端IDE便于调试,推荐使用VS Code!由于项目添加了eslint代码标准化审查,建议在VS Code插件市场下载vue和eslint插件。 106 | 107 | 安装node.js,因项目需要使用到npm管理包!!! 108 | 参考:https://m.php.cn/article/483528.html 109 | 110 | 通过cd命令进行项目frontend文件夹,安装项目所需依赖 111 | ```bash 112 | npm install 113 | ``` 114 | 115 | 启动前端项目 116 | ```bash 117 | npm run dev 118 | ``` 119 | ps:看到App running at:Local: http://localhost:9528代表项目启动成功。此时访问http://localhost:9528即可进入情感分析Web系统 120 | 121 | ## 3.3 项目使用说明 122 | 123 | 特别注意要完整访问项目的话,前端和后端项目都要启动哦!!! 124 | 125 | 1. 单文本情感分析:单文本情感分析界面输入框内输入要进行情感分析的文本,点击情感分析按钮进行情感分析预测。 126 | 127 | ![pic2](./pic/pic2.png) 128 | 129 | 2. 批量文本情感分析:批量文本情感分析界面选择要上传的txt文件。 选择上传文件后点击情感分析按钮进行批量情感分析预测。 130 | 131 | **注意:!!!!!!!!!!!!! 批量文本分析的txt文本的末尾不能有空行 !!!!!!!** 132 | 133 | ![pic2](./pic/pic3.png) 134 | 135 | 3. 分析结果可视化:一共有六种可视化图形,分别为:“属性频率词云图”、”属性频率柱状图”、”属性+观点词云图”、”属性+观点柱状图”、”属性+情感词云图”和”属性+情感柱状图” 136 | 137 | ![pic2](./pic/pic4.png) 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | # 四.参考学习资料: 146 | 147 | 1. FastAPI官方文档:https://fastapi.tiangolo.com/zh/ 148 | 2. Postman使用教程:https://mp.weixin.qq.com/s/IoseF-2Ma8mH2gdQLn1rUA 149 | 3. Vue官方文档:https://v3.cn.vuejs.org/ 150 | 4. ElementUI文档:https://v3.cn.vuejs.org/ 151 | 5. vue-admin-template:https://github.com/PanJiaChen/vue-admin-template 152 | 6. ECharts:https://echarts.apache.org/zh/index.html 153 | 154 | # 五.项目反馈: 155 | 项目运行过程中遇到问题欢迎在项目评论区(https://aistudio.baidu.com/aistudio/projectdetail/5857236)留言反馈。 156 | 157 | 联系作者: 若遇到较难解决问题可以添加qq:1090272686联系作者,注意提供完整报错信息和截图便于定位和解决问题。 158 | 159 | 160 | 161 | 参考项目: 162 | 1. [基于PaddleNLP的属性级情感分析Web系统](https://aistudio.baidu.com/aistudio/projectdetail/5060618) 163 | 2. [基于UIE的情感分析技术开源,小样本能力强悍!助力评论洞察与舆情分析](https://aistudio.baidu.com/aistudio/projectdetail/5318177) 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /backend/__pycache__/demo.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/backend/__pycache__/demo.cpython-37.pyc -------------------------------------------------------------------------------- /backend/__pycache__/utils.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/backend/__pycache__/utils.cpython-37.pyc -------------------------------------------------------------------------------- /backend/demo.py: -------------------------------------------------------------------------------- 1 | """ 2 | demo演示程序: 3 | 使用Taskflow进行属性级情感分析 4 | 单文本情感分析:针对输入的语句进行单文本情感分析 5 | 批量文本情感分析:读取txt文件内容后进行批量情感分析 6 | """ 7 | 8 | # 导入所需依赖 9 | import os 10 | import paddle 11 | from paddlenlp import Taskflow 12 | from utils import write_json_file 13 | from utils import format_results,format_print,load_txt 14 | 15 | # 单条文本情感分析预测函数 16 | def predict(input_text,schema): 17 | """ 18 | Predict based on Taskflow. 19 | """ 20 | # 单条文本情感分析 21 | senta = Taskflow("sentiment_analysis", model="uie-senta-base", schema=schema) 22 | # predict with Taskflow 23 | results = senta(input_text) 24 | # 如果语句中没有属性词,只有情感词,则调用语句级情感分析 25 | if results==[{}]: 26 | schema2 = ['情感倾向[正向,负向]'] 27 | senta2 = Taskflow("sentiment_analysis", model="uie-senta-base", schema=schema2) 28 | results = senta2(input_text) 29 | sentiment = results[0]['情感倾向[正向,负向]'][0]['text'] 30 | results = [{"aspect": 'None', "opinions": input_text, "sentiment": sentiment}] 31 | else: 32 | # 将结果输出,并以list形式保存到consequence中 33 | results = format_results(results) 34 | # 返回预测结果 35 | return results 36 | 37 | # 批量情感分析预测函数 38 | def batchPredict(file_path,schema): 39 | """ 40 | Predict based on Taskflow. 41 | """ 42 | # read file 43 | if not os.path.exists(file_path): 44 | raise ValueError("something with wrong for your file_path, it may be not exists.") 45 | examples = load_txt(file_path) 46 | 47 | # 批量情感分析 48 | senta = Taskflow("sentiment_analysis", model="uie-senta-base", schema=schema, 49 | batch_size=4, max_seq_len=512) 50 | # predict with Taskflow 51 | results = senta(examples) 52 | # 保存结果 53 | save_path = os.path.join('./outputs', "sentiment_results.json") 54 | write_json_file(results, save_path) 55 | print("The results of sentiment analysis has been saved to: {}".format(save_path)) 56 | # 将结果输出并以list形式保存到consequence中 57 | results = format_results(results) 58 | # 返回预测结果 59 | return results 60 | 61 | if __name__== "__main__" : 62 | # 定义schema 63 | schema = [{"评价维度":["观点词", "情感倾向[正向,负向,未提及]"]}] 64 | 65 | # 单条文本情感分析 66 | input_text_1 = "环境装修不错,也很干净,前台服务非常好" 67 | result_text_1 = predict(input_text_1,schema) 68 | format_print(result_text_1) 69 | 70 | input_text_2 = "蛋糕味道不错,很好吃,店家很耐心,服务也很好,很棒" 71 | result_text_2 = predict(input_text_2,schema) 72 | format_print(result_text_2) 73 | 74 | # 读取txt文件内容进行批量情感分析 75 | file_path = './textresource/test_hotel_small.txt' 76 | # 批量文本情感分析 77 | result_batch = batchPredict(file_path,schema) 78 | format_print(result_batch) 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /backend/main.py: -------------------------------------------------------------------------------- 1 | """ 2 | 基于FastAPI的属性级情感分析后端模块 3 | 先加载观点抽取和情感分析模型预热后再启动后端接口服务 4 | """ 5 | from fastapi import FastAPI, HTTPException, UploadFile 6 | from fastapi.middleware.cors import CORSMiddleware 7 | from pydantic import BaseModel 8 | import uvicorn 9 | import time 10 | 11 | import paddle 12 | from paddlenlp import Taskflow 13 | from utils import format_print 14 | from demo import predict,batchPredict 15 | from collections import defaultdict 16 | from utils import load_json_file 17 | 18 | schema = [{"评价维度":["观点词", "情感倾向[正向,负向,未提及]"]}] 19 | 20 | # 模型预热,属性级情感分析 21 | input_text = "环境装修不错,也很干净,前台服务非常好" 22 | result_text = predict(input_text,schema) 23 | format_print(result_text) 24 | 25 | class SentimentResult: 26 | """ 27 | load and analyze result of sentiment analysis. 28 | """ 29 | def __init__(self, file_path, sentiment_name="情感倾向[正向,负向,未提及]", opinion_name="观点词"): 30 | self.file_path = file_path 31 | self.sentiment_name = sentiment_name 32 | self.opinion_name = opinion_name 33 | self.examples = load_json_file(file_path) 34 | self.read_and_count_result(self.examples) 35 | 36 | def read_and_count_result(self, examples): 37 | sentiment_name = self.sentiment_name 38 | opinion_name = self.opinion_name 39 | aspect_frequency = defaultdict(int) 40 | opinion_frequency = defaultdict(int) 41 | aspect_opinion_positives = defaultdict(int) 42 | aspect_opinion_negatives = defaultdict(int) 43 | 44 | aspect_sentiment = defaultdict(dict) 45 | aspect_opinion = defaultdict(dict) 46 | for example in examples: 47 | if not example: 48 | continue 49 | for aspect in example["评价维度"]: 50 | aspect_name = aspect["text"] 51 | if "relations" not in aspect: 52 | continue 53 | if sentiment_name not in aspect["relations"] or opinion_name not in aspect["relations"]: 54 | continue 55 | sentiment = aspect["relations"][sentiment_name][0] 56 | if sentiment["text"] == "未提及": 57 | continue 58 | aspect_frequency[aspect_name] += 1 59 | if sentiment["text"] not in aspect_sentiment[aspect_name]: 60 | aspect_sentiment[aspect_name][sentiment["text"]] = 1 61 | else: 62 | aspect_sentiment[aspect_name][sentiment["text"]] += 1 63 | 64 | opinions = aspect["relations"][opinion_name] 65 | for opinion in opinions: 66 | opinion_text = opinion["text"] 67 | opinion_frequency[opinion_text] += 1 68 | if opinion_text not in aspect_opinion[aspect_name]: 69 | aspect_opinion[aspect_name][opinion_text] = 1 70 | else: 71 | aspect_opinion[aspect_name][opinion_text] += 1 72 | 73 | aspect_opinion_text = aspect_name + opinion_text 74 | if sentiment["text"] == "正向": 75 | aspect_opinion_positives[aspect_opinion_text] += 1 76 | else: 77 | aspect_opinion_negatives[aspect_opinion_text] += 1 78 | 79 | aspect_freq_items = sorted(aspect_frequency.items(), key=lambda x: x[1], reverse=True) 80 | descend_aspects = [item[0] for item in aspect_freq_items] 81 | 82 | self.aspect_frequency = aspect_frequency # 每个属性出现的频率 83 | #{'酒店': 1, '手艺': 1, '服务': 4, '房间': 8, '设施': 1, '早餐': 1} 84 | self.opinion_frequency = opinion_frequency # 每种观点词出现的频率 85 | #{'非常好': 1, '棒': 1, '好': 2, '干净': 4, '热情': 1, '大': 1, '小': 2} 86 | self.aspect_sentiment = aspect_sentiment # 各属性的情感倾向统计 87 | #{'酒店': {'正向': 1}, '手艺': {'正向': 1}, '服务': {'正向': 3, '负向': 1}} 88 | self.aspect_opinion = aspect_opinion # 各属性的观点词统计 89 | #{'酒店': {'非常好': 1}, '手艺': {'棒': 1}, '服务': {'好': 1, '热情': 1, '差': 1, '服务': 1}, '房间': {'干净': 4, '大': 1, '小': 2, '热': 1, '一般': 1, '漂亮': 1}} 90 | self.aspect_opinion_positives = aspect_opinion_positives # 正向属性观点词统计 91 | #{'酒店非常好': 1, '手艺棒': 1, '服务好': 1, '房间干净': 4, '服务热 情': 1} 92 | self.aspect_opinion_negatives = aspect_opinion_negatives # 负向属性观点词统计 93 | #{'房间小': 2, '房间热': 1, '服务差': 1, '房间一般': 1} 94 | self.descend_aspects = descend_aspects #将各属性出现次数降序排序 95 | #['房间', '服务', '性价比', '酒店', '手艺', '设施', '早餐', '风格', '环境'] 96 | 97 | # 创建一个 FastAPI「实例」,名字为app 98 | app = FastAPI() 99 | 100 | # 设置允许跨域请求,解决跨域问题 101 | app.add_middleware( 102 | CORSMiddleware, 103 | allow_origins=['*'], 104 | allow_credentials=True, 105 | allow_methods=["*"], 106 | allow_headers=["*"], 107 | ) 108 | 109 | # 定义请求体数据类型:text 用户输入的要进行属性级情感分析的文本 110 | class Document(BaseModel): 111 | text: str 112 | 113 | # 定义路径操作装饰器:POST方法 + API接口路径 114 | # 单文本情感分析接口 115 | @app.post("/v1/singleEmotionAnalysis/", status_code=200) 116 | # 定义路径操作函数,当接口被访问将调用该函数 117 | async def SingleEmotionAnalysis(document: Document): 118 | try: 119 | # 获取用户输入的要进行属性级情感分析的文本内容 120 | input_text = document.text 121 | # 调用加载好的模型进行属性级情感分析 122 | singleAnalysisResult = predict(input_text,schema) 123 | # 接口结果返回 124 | results = {"message": "success", "inputText": document.text, "singleAnalysisResult": singleAnalysisResult} 125 | return results 126 | # 异常处理 127 | except Exception as e: 128 | print("异常信息:", e) 129 | raise HTTPException(status_code=500, detail=str("请求失败,服务器端发生异常!异常信息提示:" + str(e))) 130 | 131 | # 批量情感分析接口 132 | @app.post("/v1/batchEmotionAnalysis/", status_code=200) 133 | # 定义路径操作函数,当接口被访问将调用该函数 134 | async def BatchEmotionAnalysis(file: UploadFile): 135 | # 读取上传的文件 136 | fileBytes = file.file.read() 137 | fileName = file.filename 138 | # 判断上传文件类型 139 | fileType = fileName.split(".")[-1] 140 | if fileType != "txt": 141 | raise HTTPException(status_code=406, detail=str("请求失败,上传文件格式不正确!请上传txt文件!")) 142 | try: 143 | # 将添加时间标记重命名避免重复 144 | now_time = int(time.mktime(time.localtime(time.time()))) 145 | filePath = "./textresource/" + str(now_time) + "_" + fileName 146 | # 将用户上传的文件保存到本地 147 | fout = open(filePath, 'wb') 148 | fout.write(fileBytes) 149 | fout.close() 150 | # 批量文本情感分析 151 | batchAnalysisResults = batchPredict(filePath,schema) 152 | sr = SentimentResult('./outputs/sentiment_results.json') 153 | # 属性频率词云图数据 154 | aspect_wc_data = [] 155 | for item in sr.aspect_frequency: 156 | text = {} 157 | text['name'] = item 158 | text['value'] = sr.aspect_frequency[item] 159 | aspect_wc_data.append(text) 160 | # 属性频率柱状图数据 161 | content_freq_items = sr.aspect_frequency.items() 162 | content_freq_items = sorted(content_freq_items, key=lambda x: x[1], reverse=True) 163 | content_freq_items = content_freq_items[:15] 164 | aspect_hist_x_data = [item[0] for item in content_freq_items] 165 | aspect_hist_y_data = [item[1] for item in content_freq_items] 166 | # 属性+观点数据 167 | new_aspect_opinion = {} 168 | for aspect in sr.aspect_opinion: 169 | for opinion in sr.aspect_opinion[aspect]: 170 | key = aspect + opinion 171 | new_aspect_opinion[key] = sr.aspect_opinion[aspect][opinion] 172 | aspect_opinion_data = new_aspect_opinion 173 | # 属性+观点词云图数据 174 | aspect_opinion_wc_data = [] 175 | for item in aspect_opinion_data: 176 | text = {} 177 | text['name'] = item 178 | text['value'] = aspect_opinion_data[item] 179 | aspect_opinion_wc_data.append(text) 180 | # 属性+观点柱状图数据 181 | content_freq_items = aspect_opinion_data.items() 182 | content_freq_items = sorted(content_freq_items, key=lambda x: x[1], reverse=True) 183 | content_freq_items = content_freq_items[:15] 184 | aspect_opinion_hist_x_data = [item[0] for item in content_freq_items] 185 | aspect_opinion_hist_y_data = [item[1] for item in content_freq_items] 186 | # 属性+情感词云图数据 187 | new_aspect_opinion = {} 188 | for aspect in sr.aspect_sentiment: 189 | for sentiment in sr.aspect_sentiment[aspect]: 190 | key = aspect + sentiment 191 | new_aspect_opinion[key] = sr.aspect_sentiment[aspect][sentiment] 192 | aspect_sentiment_wc_data = [] 193 | for item in new_aspect_opinion: 194 | text = {} 195 | text['name'] = item 196 | text['value'] = new_aspect_opinion[item] 197 | aspect_sentiment_wc_data.append(text) 198 | # 属性+情感柱状图数据 199 | aspect_sentiment_hist_x_data = [] 200 | aspect_sentiment_positives = [] 201 | aspect_sentiment_negatives = [] 202 | keep_aspects = set(sr.descend_aspects[:15]) 203 | for aspect, sentiment in sr.aspect_sentiment.items(): 204 | if aspect not in keep_aspects: 205 | continue 206 | aspect_sentiment_hist_x_data.append(aspect) 207 | if "正向" in sentiment: 208 | aspect_sentiment_positives.append(sentiment["正向"]) 209 | else: 210 | aspect_sentiment_positives.append(0) 211 | if "负向" in sentiment: 212 | aspect_sentiment_negatives.append(sentiment["负向"]) 213 | else: 214 | aspect_sentiment_negatives.append(0) 215 | 216 | # 接口结果返回 217 | results = {"message": "success", "batchAnalysisResults": batchAnalysisResults, 218 | "aspect_wc_data": aspect_wc_data, 219 | "aspect_hist_x_data": aspect_hist_x_data, 220 | "aspect_hist_y_data": aspect_hist_y_data, 221 | "aspect_opinion_wc_data": aspect_opinion_wc_data, 222 | "aspect_opinion_hist_x_data": aspect_opinion_hist_x_data, 223 | "aspect_opinion_hist_y_data": aspect_opinion_hist_y_data, 224 | "aspect_sentiment_wc_data": aspect_sentiment_wc_data, 225 | "aspect_sentiment_hist_x_data": aspect_sentiment_hist_x_data, 226 | "aspect_sentiment_positives": aspect_sentiment_positives, 227 | "aspect_sentiment_negatives": aspect_sentiment_negatives} 228 | return results 229 | # 异常处理 230 | except Exception as e: 231 | print("异常信息:", e) 232 | raise HTTPException(status_code=500, detail=str("请求失败,服务器端发生异常!异常信息提示:" + str(e))) 233 | 234 | # 启动创建的实例app,设置启动ip和端口号 235 | uvicorn.run(app, host="127.0.0.1", port=8000) 236 | -------------------------------------------------------------------------------- /backend/outputs/sentiment_results.json: -------------------------------------------------------------------------------- 1 | {"评价维度": [{"text": "酒店", "start": 4, "end": 6, "probability": 0.9805555948078251, "relations": {"观点词": [{"text": "非常好", "start": 0, "end": 3, "probability": 0.9979143946517546}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999283561718961}]}}, {"text": "手艺", "start": 26, "end": 28, "probability": 0.9886985550091936, "relations": {"观点词": [{"text": "棒", "start": 30, "end": 31, "probability": 0.8958608472553422}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999203098546658}]}}]} 2 | {"评价维度": [{"text": "服务", "start": 0, "end": 2, "probability": 0.9988774645833729, "relations": {"观点词": [{"text": "好", "start": 5, "end": 6, "probability": 0.9998978996967764}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999292502484849}]}}, {"text": "房间", "start": 7, "end": 9, "probability": 0.9998807343369762, "relations": {"观点词": [{"text": "干净", "start": 11, "end": 13, "probability": 0.9999251975044814}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999386079448982}]}}]} 3 | {"评价维度": [{"text": "服务", "start": 13, "end": 15, "probability": 0.9988338575307836, "relations": {"观点词": [{"text": "热情", "start": 15, "end": 17, "probability": 0.9989774953388064}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999371775908266}]}}, {"text": "房间", "start": 3, "end": 5, "probability": 0.9997860879553215, "relations": {"观点词": [{"text": "干净", "start": 8, "end": 10, "probability": 0.9892090958331323}, {"text": "大", "start": 6, "end": 7, "probability": 0.9998239946333243}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999372966937123}]}}]} 4 | {"评价维度": [{"text": "房间", "start": 15, "end": 17, "probability": 0.9975641146361909, "relations": {"观点词": [{"text": "小", "start": 42, "end": 43, "probability": 0.9997207628257243}, {"text": "热", "start": 18, "end": 19, "probability": 0.9400050764378491}], "情感倾向[正向,负向,未提及]": [{"text": "负向", "probability": 0.9998598147799242}]}}]} 5 | {"评价维度": [{"text": "服务", "start": 2, "end": 4, "probability": 0.9995068323763334, "relations": {"观点词": [{"text": "差", "start": 5, "end": 6, "probability": 0.9998059300148405}], "情感倾向[正向,负向,未提及]": [{"text": "负向", "probability": 0.9998630331728293}]}}, {"text": "房间", "start": 7, "end": 9, "probability": 0.9998169003884207, "relations": {"观点词": [{"text": "一般", "start": 9, "end": 11, "probability": 0.9999566082311162}], "情感倾向[正向,负向,未提及]": [{"text": "负向", "probability": 0.9999223366510819}]}}]} 6 | {"评价维度": [{"text": "房间", "start": 2, "end": 4, "probability": 0.9790792638602568, "relations": {"观点词": [{"text": "小", "start": 6, "end": 7, "probability": 0.9994983500711854}], "情感倾向[正向,负向,未提及]": [{"text": "负向", "probability": 0.9998062346148515}]}}]} 7 | {"评价维度": [{"text": "设施", "start": 2, "end": 4, "probability": 0.9976909427381528, "relations": {"观点词": [{"text": "高", "start": 4, "end": 5, "probability": 0.5535427083728166}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999151246361038}]}}, {"text": "服务", "start": 14, "end": 16, "probability": 0.398364268557458, "relations": {"观点词": [{"text": "服务", "start": 14, "end": 16, "probability": 0.9900867145553605}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9998200589804611}]}}, {"text": "早餐", "start": 19, "end": 21, "probability": 0.9940091894402485, "relations": {"观点词": [{"text": "不错", "start": 21, "end": 23, "probability": 0.999834901391754}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999446875777629}]}}]} 8 | {"评价维度": [{"text": "性价比", "start": 35, "end": 38, "probability": 0.4583181917686545, "relations": {"观点词": [{"text": "高", "start": 19, "end": 20, "probability": 0.9400142179951843}, {"text": "高", "start": 39, "end": 40, "probability": 0.9991386025219811}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999169127873309}]}}, {"text": "房间", "start": 0, "end": 2, "probability": 0.9990997906955101, "relations": {"观点词": [{"text": "干净", "start": 3, "end": 5, "probability": 0.9998067709801717}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999247204827455}]}}, {"text": "风格", "start": 9, "end": 11, "probability": 0.417759185511386, "relations": {"观点词": [{"text": "不错", "start": 12, "end": 14, "probability": 0.999420786284702}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.999890508922249}]}}, {"text": "性价比", "start": 15, "end": 18, "probability": 0.9978598909092113, "relations": {"观点词": [{"text": "高", "start": 19, "end": 20, "probability": 0.9400142179951843}, {"text": "高", "start": 39, "end": 40, "probability": 0.9991386025219811}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999169127873309}]}}, {"text": "环境", "start": 21, "end": 23, "probability": 0.9662384187973494, "relations": {"观点词": [{"text": "好", "start": 27, "end": 28, "probability": 0.9977891464072641}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9998949790928577}]}}]} 9 | {"评价维度": [{"text": "房间", "start": 0, "end": 2, "probability": 0.9999567274422816, "relations": {"观点词": [{"text": "干净", "start": 3, "end": 5, "probability": 0.9998881846355623}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999423630417184}]}}]} 10 | {"评价维度": [{"text": "房间", "start": 0, "end": 2, "probability": 0.9999476081119667, "relations": {"观点词": [{"text": "漂亮", "start": 3, "end": 5, "probability": 0.999757185390564}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999175682170502}]}}]} 11 | {"评价维度": [{"text": "房间", "start": 6, "end": 8, "probability": 0.9998633304919977, "relations": {"观点词": [{"text": "一般", "start": 8, "end": 10, "probability": 0.9999404558364802}], "情感倾向[正向,负向,未提及]": [{"text": "负向", "probability": 0.9999018335582477}]}}, {"text": "院子", "start": 0, "end": 2, "probability": 0.9994853151361127, "relations": {"观点词": [{"text": "漂亮", "start": 3, "end": 5, "probability": 0.9998504575166223}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999381311542272}]}}]} 12 | {"评价维度": [{"text": "环境", "start": 0, "end": 2, "probability": 0.9999068990115099, "relations": {"观点词": [{"text": "挺好", "start": 2, "end": 4, "probability": 0.9998769194749073}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999407537455376}]}}, {"text": "房间", "start": 5, "end": 7, "probability": 0.9998673832792875, "relations": {"观点词": [{"text": "舒服", "start": 9, "end": 11, "probability": 0.9998691716623469}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999305019988469}]}}]} 13 | {"评价维度": [{"text": "环境", "start": 8, "end": 10, "probability": 0.9997608792372539, "relations": {"观点词": [{"text": "不错", "start": 10, "end": 12, "probability": 0.997209851082097}, {"text": "新", "start": 3, "end": 4, "probability": 0.879978359405893}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999315749257676}]}}, {"text": "交通", "start": 15, "end": 17, "probability": 0.8003588012371665, "relations": {"观点词": [{"text": "方便", "start": 19, "end": 21, "probability": 0.9991306639517035}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.913080269236815}]}}]} 14 | {"评价维度": [{"text": "位置", "start": 0, "end": 2, "probability": 0.9998990910343437, "relations": {"观点词": [{"text": "好", "start": 3, "end": 4, "probability": 0.9996382621538018}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999340782290496}]}}, {"text": "隔音", "start": 7, "end": 9, "probability": 0.9995043786374964, "relations": {"观点词": [{"text": "一般", "start": 9, "end": 11, "probability": 0.9998404441234641}], "情感倾向[正向,负向,未提及]": [{"text": "负向", "probability": 0.9998415771222575}]}}]} 15 | {"评价维度": [{"text": "房间", "start": 6, "end": 8, "probability": 0.9998020615649992, "relations": {"观点词": [{"text": "值", "start": 18, "end": 19, "probability": 0.7554655664512069}, {"text": "宽敞", "start": 9, "end": 11, "probability": 0.9998114787175112}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999306808024144}]}}, {"text": "环境", "start": 0, "end": 2, "probability": 0.9999158991904267, "relations": {"观点词": [{"text": "挺好", "start": 2, "end": 4, "probability": 0.9999057670625433}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999234091814913}]}}]} 16 | {"评价维度": [{"text": "房间", "start": 0, "end": 2, "probability": 0.999943555186178, "relations": {"观点词": [{"text": "小", "start": 4, "end": 5, "probability": 0.9998123715074598}], "情感倾向[正向,负向,未提及]": [{"text": "负向", "probability": 0.9999153035765502}]}}]} 17 | {"评价维度": [{"text": "房间", "start": 0, "end": 2, "probability": 0.999684356294491, "relations": {"观点词": [{"text": "大", "start": 2, "end": 3, "probability": 0.9998984958208865}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999324091134554}]}}, {"text": "设施", "start": 28, "end": 30, "probability": 0.46528325679116733, "relations": {"观点词": [{"text": "温馨", "start": 37, "end": 39, "probability": 0.9767321127870652}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9982318254744804}]}}, {"text": "床", "start": 14, "end": 15, "probability": 0.7730360220500359, "relations": {"观点词": [{"text": "舒服", "start": 16, "end": 18, "probability": 0.9993581447129429}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9997745862411875}]}}, {"text": "服务", "start": 19, "end": 21, "probability": 0.9921678435571124, "relations": {"观点词": [{"text": "好", "start": 24, "end": 25, "probability": 0.9998435438716129}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999318728786477}]}}]} 18 | {"评价维度": [{"text": "服务", "start": 0, "end": 2, "probability": 0.9988774645833729, "relations": {"观点词": [{"text": "好", "start": 5, "end": 6, "probability": 0.9998978996967764}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999292502484849}]}}, {"text": "房间", "start": 7, "end": 9, "probability": 0.9998807343369762, "relations": {"观点词": [{"text": "干净", "start": 11, "end": 13, "probability": 0.9999251975044814}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999386079448982}]}}]} 19 | {"评价维度": [{"text": "房间", "start": 0, "end": 2, "probability": 0.99996292619727, "relations": {"观点词": [{"text": "好", "start": 3, "end": 4, "probability": 0.999664016605891}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999336608725784}]}}]} 20 | {"评价维度": [{"text": "感觉", "start": 22, "end": 24, "probability": 0.9263640702036895, "relations": {"观点词": [{"text": "不错", "start": 25, "end": 27, "probability": 0.9994820976945711}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9999405153112662}]}}, {"text": "房间", "start": 8, "end": 10, "probability": 0.9993937924510696, "relations": {"观点词": [{"text": "不大", "start": 10, "end": 12, "probability": 0.930644064673622}, {"text": "整洁", "start": 16, "end": 18, "probability": 0.9996797065859653}, {"text": "舒服", "start": 19, "end": 21, "probability": 0.9995551586521856}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9995580801959818}]}}, {"text": "环境", "start": 3, "end": 5, "probability": 0.999899865768608, "relations": {"观点词": [{"text": "不错", "start": 5, "end": 7, "probability": 0.9997724409942954}], "情感倾向[正向,负向,未提及]": [{"text": "正向", "probability": 0.9998413368569068}]}}]} 21 | -------------------------------------------------------------------------------- /backend/textresource/test_hotel.txt: -------------------------------------------------------------------------------- 1 | 非常好的酒店 不枉我们爬了近一个小时的山,另外 大厨手艺非常棒 竹筒饭 竹筒鸡推荐入住的客人必须要点, 2 | 房间隔音效果不好,楼下KTV好吵的 3 | 酒店的房间很大,干净舒适,服务热情 4 | 怎么说呢,早上办理入住的,一进房间闷热的一股怪味,很臭,不能开热风,好多了,虽然房间小,但是合理范围 5 | 总台服务很差,房间一般 6 | 大床房间比较小,卫生情况还可以,超划算 7 | 酒店设施高大上,前台工作人员服务到位,早餐不错,总体五星 8 | 房间挺干净的,装修风格很不错,性价比很高,环境卫生都很好,这个价位来讲性价比很高 9 | 房间很干净 从泰山脚下打的到酒店门口只要13块, 10 | 房间挺漂亮的,就是小了点 11 | 院子挺漂亮,房间一般~ 12 | 环境挺好,房间也很舒服 13 | 酒店是新装修的,环境不错,饮食交通都算方便 14 | 位置很好,房间隔音一般,设施尚可 15 | 环境挺好的,房间很宽敞,只能说物超所值 16 | 房间比较小,属于住一晚就不想住的那种 17 | 房间大,只是不好停车,特别是床特舒服,服务态度很好,房间设施感觉像在家一样温馨 18 | 服务态度超好,房间也很干净,会定点再来的 19 | 房间很好呢, 20 | 朋友说环境不错,房间不大,但是很整洁,舒服,感觉很不错,只要有房间 21 | 交通比较方便,高速路过.环境还可以,房间比较小,勉强居住 22 | 大厅装修得温馨而精致,房间灯光温暖而明亮,惬意 23 | 房间很不错,空间够大,服务态度很好 24 | 海滩很一般,海韵酒店过街就是海滩 25 | 环境不错,房间干净宽敞, 26 | 地点还算方便,酒店不高,但是光线还好,房间不大,五脏俱全,前台服务也不错,性价比高 27 | 房间不错,服务人员很好 28 | 房间干净整洁,布置得很温馨,位置非常棒 29 | 房间有点异味,酒店环境很不错,房间面积一般般,其他设施一应俱全,空调叶子坏了声音有点大,总体感觉还不错,服务很周到 30 | 房间很大,环境也不错就是空调噪音有点大,总体还行 31 | 不过房间相对较小,店主服务态度是很好的 32 | 酒店位置很好,服务态度很好 33 | 非常好的酒店,硬件措施挺不错的 34 | 老板服务很周到,房间硬件有待提高 35 | 别选靠近公路的房间就好,性价比高哦 36 | 续两周定了一样的房型一样的价格 第二次去房间变小很多 前台说就是这样的 说携程上写错了 我也是觉得很搞笑 给好评是因为第一次住时感觉还是不错的 37 | 房间不错,服务也不错,蛮舒服的 38 | 无空调无wife房间旧有老鼠~至少温泉还可以泡泡 39 | 算干净卫生吧,就是房间不够大 40 | 除房间设备旧点,服务赞, 41 | 性价比比较高,房间干净,暖气很好 42 | 房间挺大的,还算干净,但服务态度有点冷淡, 43 | 房间挺大的,工作人员很热心帮我找到,其他的隔音效果都挺好, 44 | 服务,房间收拾的特别干净,床品很干净舒适 45 | 春节里我们住的证大喜玛拉雅酒店就在壹城的江山门那里,一直不喜欢人太多的地方,人少又干净,不过倒也惟妙惟肖 46 | 环境一般,主要房间隔音太差,太吵了 47 | 前台服务是我住过所有酒店最差的,房间设施陈旧 48 | 房间大,其他还好 49 | 酒店味道大,隔音差 50 | 毛巾要钱也就是洗脸都要掏钱睡觉的时候头上的镜子让我很奇怪这是要干嘛?睡觉前照镜子?房间略小,前台服务不错 WiFi速度较慢 性价比不高 51 | 房间挺好的,服务员态度也好 52 | 房间垃圾都没倒,第一次住, 53 | 感觉卫生一般,房间比较旧,设施还是齐全的,性价比一般, 54 | 很一般的酒店,房间外边好吵跟打仗似的,隔音效果好差, 55 | 酒店很好,房间很大,设施齐全,涨价好多 56 | 房间小了点,隔音差 57 | 房间整洁,服务态度好,还算舒适,但是我住的房间电话是坏的,不能用,向前台反映了也只是提供换房而不是修理电话;宾馆没有专门的停车场,希望房间做好合适的通风处理 58 | 接送机服务不错,房间挺干净 59 | 环境不错,房间风格让人舒服 60 | 给的房间不是很好 61 | 房间宽畅干净,服务也不错 62 | 环境优雅,安静,房间干净,整洁 63 | 房间设施齐全还有洗衣机,最主要光线好,住着很安静 64 | 交通很便利,开车打车都挺方便的,酒店的基本设施和配套设施也都可以,床很大,是大型的酒店双人床,卫生环境也不错 65 | 房间太小太小 66 | 酒店很好,房间大又干净 67 | 酒店地理位置不错,吃饭很方便,前台工作人员很热情周到,之前携程订房的信息有误 68 | 环境还不错,服务态度也不错,房间很大 69 | 不错的酒店,逛街都方便,酒店卫生,房内设施设备都不错 70 | 房间好大呀,而且这边还挺安静的,不过整体还是很好的,很宽敞,就是洗手台的水龙头应该要找人来修下咯,走的时候男盆友落了手机在床上 71 | 房间也很干净,楼上养了很多花花草草,环境很赞 72 | 环境美,房间不大,但舒适温馨,老板服务周到贴心 73 | 酒店还不错,住着还可以,床很舒服 74 | 这次来荆州给我的房间小的无语了 75 | 干净的房间,视野很好,服务很亲切, 76 | 房间环境和交通都特别好 77 | 酒店不大,一楼大概四五个大桌子,楼上是包厢 78 | 半面墙都是玻璃,房间也还干净,床也舒服,挺喜欢的 79 | 房间挺好,就是隔音不够好 80 | 和老公一起来的,立刻被外景吸引了,私密性非常好,但是私秘性好大堂服务人员很热情,大堂像个婚纱礼服馆,还有很好看的婚纱哦,很有再拍一次婚纱照的欲望,大堂人员都很热情,入住的房间很有感觉,简约干净而且具有小清新的意境,我最喜欢木质屋顶和那个风扇,很有住在海边小木屋的感觉,设施齐全,酒店特别贴心,酒店内许多景致特别美好,适合拍婚纱照 81 | 酒店离虞山景点较近,房间不大,设施齐全,吃饭方便,客房清洁阿姨的服务态度蛮好的 82 | 确实性价比高,房间很干净,感觉心情都变得超好 83 | 酒店不错,房间面积大,住的也舒适 84 | 经济实惠,空调不给力,跟房间大有关系 85 | 只能说一分价钱一分货~房间还是很不错的很干净,但是牙膏牙刷拖鞋这类的都需要自己带哦~船员846名,非欧洲籍船员合同期8个月,都是男性服务员~第一顿和最后一顿我们在2楼吃的点餐~平时基本都在9楼自助餐厅解决了~品种不多但是能吃饱~岸上活动时间太短了~小朋友可以去思高俱乐部玩,提香餐厅~~~邮轮好不好 86 | 酒店设施略显陈旧,灯光还需要再亮点,有些暗 87 | 周边环境不是很好,但是服务不错,酒店内部还可以 88 | 房间非常宽敞,这点如家就比不上了 89 | 房间干净卫生,满舒服的 90 | 酒店还可以,网络还可以.总体不错 91 | 房间不错,虽小但温馨,网络不给力 92 | 没拍照片,房间很小,但干净整洁 93 | 房间不大但是很干净也很安静 94 | 环境还可以,就是酒店位置不太好找, 95 | 房间还不错,有免费的矿泉水,wifi信号弱 96 | 房间整洁,设施完好,客服人员不错 97 | 房间太脏了,地上也有,明明拍了照片还被我删了,起码地面应该干净吧 98 | 不过这次这个房间确实小了点 99 | 周边吃饭不方便,但是房间大,价格合适,发票开的不及时 100 | 订的有窗户的,房间就比较紧凑了,卫生挺好的 101 | 酒店没有窗户,气味很臭,卫生也不干净 102 | 到了楼上服务台也是破破的整体的感觉就是破旧房间装修的很一般 103 | 房间破旧 104 | 房间挺小的,因为在高铁站附近,步行15到20分钟可达高铁站 105 | 房间不错,服务也很棒,mm也很漂亮 106 | 房间很大,店家也挺热情,很棒,下次还会选择 107 | 房间很干净,简洁,就是隔音太差 108 | 房间还可以,就是周边环境不好 109 | 酒店的地理位置给了我一个惊喜,吃饭也方便,升级为商务大床房,总体是不错的,这个价钱 110 | 房间超级漂亮 111 | 环境还不错,房间很干净 112 | 房间比较小,设施还是挺齐全卫生的 113 | 房间很大,有两扇平开窗,无异味,床比较舒服,wifi 速度快,希望改进 114 | 酒店设施非常差,房间内设施差 115 | 性价比很高,店家服务也很到位,但是房间很温馨 116 | 房间好干净,环境蛮好,服务人员很热情 117 | 虽然在顶楼,不过比较安静蛮好的,环境也不错,房间面积挺大的,比较舒适,连锁酒店还是不错的选择 118 | 一般,酒店服务不敢恭维,有点忽悠成分 119 | 酒店环境很好,非常干净,工作人员服务非常好 120 | 交通位置比较好,生活交通都比较便利,酒店前台比较热情,行李可以寄存,环境卫生比较干净,性价比比较高, 121 | 房间很宽敞,毛巾、浴巾太陈旧 122 | 地理位置好,但是房间隔音不好 123 | 房间新装修的,比较舒适,住的是8楼,虽然前台已经帮安排了较安静的房间, 124 | 酒店还不错,性价比还可以 125 | 环境好,位置佳,房间大,服务态度也很好 126 | 房间大,阳光充足 127 | 99元标间 房间很好 温馨 128 | 说电视怎么看不了啊,前台直接说你不住可以退房,我忍,刚下火车到目的地太累了,房间太热了,服务超差 129 | 有点出乎意料的干净,觉得这样酒店应该没那么整洁,房间很温馨,还可以做饭,房间价格这样比下来还是很划算的 130 | 酒店环境不错,安静舒适, 131 | 房间设施有点陈旧了,温泉还不错 132 | 交通方便,房间干净,下次还来 133 | 酒店硬件很好,位置很不错 134 | 房间空调不行 135 | 设施设施比较齐全,美女服务不错,房间挺大,嗯 136 | 房间很不错,交通很方便,服务很好 137 | 性价比还行,酒店设施老,服务还好 138 | 房间不是很干净的说,而且设施比较简陋 139 | 酒店环境非常不错,床特别的舒服 140 | 房间很干净,不错的团购 141 | 位置很好,离地铁2号线很近,房间很干净, 142 | 附近人垃圾场早上很吵喔,去得不是时候… 酒店的员工都好nice,早餐很好,够营养 143 | 只能睡后面的房间,房间很大,浴室也超大,干净卫生,老板自己家的别墅,川菜味道很好,第一二张是别墅外景,视野很好很漂亮 144 | 青年旅社两个人都是差不多100多元,这边安静而且房间大宽敞,卫浴是公共 145 | 酒店挺好的 服务也很好 房间很大 就是枕头太软了 146 | 地理位置好,到西湖也还好,房间有点小 147 | 环境舒适,服务贴心,房间干净安静,早餐丰富 148 | 房间够大床垫很舒适、也比较干净 149 | 房间干净,设施齐全 150 | 房间很旧但还算干净,隔音效果非常差隔壁房间说话可以听的很清楚 151 | 房间还算干净整洁,整体环境还不错,就是位置不太好,我们是晚上到的 152 | 位置有点偏了,房间很好,房间很大,电视小,空调很给力,环境也挺好,干净舒适 153 | 酒店不错,前台美女更不错,下次为美女在来 154 | 房间空气不怎么好'味道不好' 155 | 我们的房间还可以,算是偏海景吧,一个大房间,旁边酒店的餐厅吃饭性价比不错 156 | 地理位置不错,房间设施齐全,太暗了 157 | 房间很小,环境还可以 158 | 但房间很不错,就是一直没有双人房, 159 | 距离永福有一个小时车程 建议多人包车进金钟山划得来 酒店硬件条件一般 但是金钟山环境真心好 空气清新 下午到直接去泡温泉 人很少因为淡季吧 开的池子很少 水温也不是很烫 金钟山的饭菜还是很实惠的 总体给个75分 160 | 环境不错,房间干净整洁,设施齐全,周边小吃超市方便,交通便利 161 | 位罟非常好,房间也很干净 162 | 房间宽敞,干净舒适,退房时与前台沟通确定两天后的房间 163 | 服务超级好,麻烦老板加了一床被子和浴巾都很快送来,但是房间好小被子好小 164 | 房间挺大,装修设施比较老 165 | 房间不错,卫生挺干净,设施也很完善 166 | 由于是新开的,所以环境很好,设施都很新,也没有装修的异味,特别是大床非常舒服,不足之处是酒店周围还在装修施工 167 | 服务态度一般,交通不太方便,房间隔音效果不好,晚上隔壁说话都能听到 168 | 环境赞,房间挺宽敞的,服务特别好,美中不足,走廊隔音差了点,开关门的声音有点吵 169 | 老板服务很好,房间也算是干净 170 | 名字取自诗词,房间内的摆设和其他的差别不大 171 | 房间挺大的~床很舒服,但是wifi有点不给力啊,网线也不管用 172 | 房间很大,物有所值 173 | 环境好,房间舒适,服务态度也不错 174 | 这家店位置在岛上很偏僻,附近环境很差,住了一天都没在店里看到任何服务人员,房价还比附近环境更好的酒店高百分之五十,在酒店内并未看到任何营业许可,连押金单都没有 175 | 做的房间很安静,但翘的效果还是不错的,最后我错给了微信支付码闹了个乌龙… 176 | 酒店人员服务态度很好,进门就会递上热毛巾,房间设施也不错,市中心的位置, 177 | 房间奇小, 178 | 房间很大,性价比很好 179 | 没有Wifi 房间设施老旧 可能靠近马路的原因吧 太吵了 180 | 酒店太差了,热水慢,绝对不会住第二次 181 | 服务到位,立刻免费给我升级了房间,态度很好,赞 182 | 因为所住的铜锣湾怡东酒店就在这附近,早起无聊,这里空气清新 183 | 房间很不错服务员服务的态度也很好,晚上出门等车很方便 184 | 楼层烟味比较大,房间设施开始有点旧 185 | 房间有好有坏 186 | 交通非常方便;中山图书馆、北京路步行街也很近,房间小 187 | 环境很温馨,房间很舒适 188 | 酒店空间很大,房间也不错, 189 | 房间视野非常好,看到梯田很美,老板是东北人 190 | 酒店干净整洁,老板热情,服务很周到 191 | 房间连厕所都没有,太差了 192 | 酒店位置很好,隔音不是很好,有些吵了,酒店工作人员也都服务挺好, 193 | 房间比较大,位置好,去哪儿都比较近,交通便利 194 | 服务态度差,房间很差 195 | 服务员挺热情的,房间太小,有点潮湿 196 | 房间非常狭小,窗户小且偏,不透光 197 | 房间很不错,就是灯有点坏了,位置还蛮方便的,居然没人了 198 | 服务态度很好 来回接送 选择的这个房间没有沿河 可惜了一点 199 | 酒店环境真差 不值这个价格 200 | 房间挺大,也挺干净,就是隔音差了点 201 | 房间干净 舒服 设施都很棒 特价房没有窗户 朋友住128的也没有 只是床变大了一些 有点遗憾 周围交通便利 吃的东西也多 202 | 本酒店位置很好,闹中取静,也算是市中心的位置,视野较好,公园和街景一览无余,房间足够大,在床和窗户之间有足够大的空间,还能垂直床放一个双人沙发,卫生间是一个长条形,设施一般,总体感觉不错 203 | 房间不大,空调很给力, 204 | 酒店不大,但很温馨 205 | 很安静,还有浴袍很方便,房间很大很宽敞,茶叶很好喝,很棒的团购 206 | 评分1,服务态度差,房间异味大 207 | 这家酒店不错,就像回到家一样,感觉很好 208 | 房间安静 挺好的 就是去了又加钱了, 209 | 房间还可以,服务周到,性价比较高, 210 | 非常成功的一次团购,房间很干净,设施也很齐全,服务很好,好的服务态度让人住的舒心、温馨 211 | 酒店的地理位置还是很好的,酒店周边的配套设施非常的齐全,酒店的价格和硬件和软件服务都还不错 ,发表于2015-01-29 00:13整体印象:酒店地理位置极佳,非常方便,交通也很便利,选择的凌晨房性价比不错,客房服务也还OK,步行200米就到了自己开车的话也比较方面(PS:酒店提供免费停车位哦~再一次蹭车,晚上基本没客人哦,但精气神稍微差点(这点可以原谅啦,无线网络在6楼信号还可以,采光.通风.热水.隔音都还不错.我入住的这间刚好临街,直径大约2米吧(感觉圆床还是适合滚床单啊,而且柔软度一般哦 212 | 房间小是正常的 213 | 经济型酒店不错 214 | 酒店前台服务很好,房间很不错,拍照起来很好 215 | 比如家酒店要很多,服务态度就不是同一个档次 216 | 和同学在网上团了门票,无语,不过算了现场也就差2元,基本构造就是中间二楼可看到传统戏台,左右两边的房间都是和海事有关的展品,但总的来说地方不大,也没有什么出彩的地方,周末参观的人几乎没有 217 | 房间很干净,服务员很热情 218 | 房间还不错,性价比高 219 | 新开的酒店,中央空调效果给力、简直有星级酒店那么好 220 | 酒店不错,有中央空调,酒店温度27度,服务人员态度也挺好,去哪儿网定便宜 221 | 此次来北京开会找到这酒店真的很不错交通也很方便一进酒店前台人员服务真的很热情这点做的很到位,客房很干净房间大小不错对的起这价格 222 | 服务还可以,房间好像比现金订房的房间小,没有窗户的有点闷有点潮, 223 | 位置不错,离长沙车站很近、购物方便、附近有电影院、交通便利,但停车好难、车位少.服务态度好、总台、楼层服务员可帮忙加被、暖气.卫生都干净、但房间太靠近路边、汽车声、商场声太杂、隔着不好.房间设施中规中矩 224 | 酒店环境很好,安静,周边餐饮很方便 225 | 房间不错,住的挺舒服, 226 | 位置离乾州铁索桥很近,房间层高比较矮,空调这些都好用 227 | 酒店周围环境还是不错的,距离各大超市都很近,房间里充电的地方一会有电一会没电, 228 | 青岛威斯汀酒店环境美,服务热心贴心 229 | 服务还可以,位置不错,酒店环境着实一般 230 | 房间干净舒适,价格合适 231 | 房间很大,住的很舒服 232 | 海景房的窗玻璃脏的什麽也看不到,要求服务员更换到隔壁干净窗户的房间,真是无语、、、、一个晚上装修的声音就在吵,没有做好准备就不要营业嘛 233 | 服务很好,客服很热情,房间干净,空间大,小区位置很好 234 | 酒店很干净,正赶上隔壁酒店在拍夜场电影有点吵杂,地理位置也很好 235 | 房间太小了, 236 | 服务员态度极限的差 以后找不到房间也不会住这里了 237 | 酒店老板服务态度很好~,房间很干净 238 | 服务不错,房间一般般 239 | 房间小,过年期间人生 240 | 房间干净,房间较大,停车方便,真的可以 241 | 距离市中心比较近,出门交通还算方便,酒店也还不错,服务挺好 242 | 房间很好,就是卫生间太一般了,房间设施有点老旧, 243 | 环境可以停车还算方便,就是房间太小了 244 | 房间干净,服务员态度也不错,只是隔音一般吧 245 | 住的感觉还不错,现在已经不属于七天,房间天花板发霉、走廊异味较重、房间很吵、无线网络不好……下次应该不会再来了,PS:此宾馆不能多开发票 246 | 环境不好房间还有异味 247 | 房间够宽敞,淋浴很舒服,很适合我们出差住 248 | 一大排的土特产,产品特别丰富,旁边的“流行美”,已经“倒闭”,距离洪崖洞大酒店, 249 | 位置相对比较偏僻,房间比较小,但是, 250 | 房间不大,窗户小 251 | 房间还可以,服务也可以吧,环境也不错,就是房间里面比较简单没有柜子,短期住一天两天还是不错的 252 | 挺好的,服务态度好,房间很大、服务很好、性价比很高、房间干净、,位置挺好找的,服务态度很好,设施很好,环境还可以,交通便利 253 | 服务一般,房间还可以,一分价钱一份货 254 | 周边环境非常糟糕,有建筑工地、集市;设施不好,新装修的房间气味特浓,结果到了之后却说没有了 255 | 酒店很好,服务意识还有待提高 256 | 环境赞,就是房间的隔音不太好,无线网的信号不是很好,服务态度很好, 257 | 公司附近~多次入住了~~~周围的酒店都比较贵 258 | 房间窄 空气不好 有的房间还没窗户 259 | 酒店不错,基本条件还好 260 | 酒店大堂很漂亮,房间面积宽,早上吃的酒店自助餐有云南米粉,味道不错,总体来讲性价比很高 261 | 房间是新装修的,设施不错,在两条路的交汇处 262 | 不错,房间大,装修的有点风格,下次再来 263 | 生活也方便,酒店服务很好 264 | 房间很干净,服务挺好 265 | 酒店地理位置很好,西藏北路地铁站出来走3分钟就到,房间不大,在上海属于性价比比较高的了,但是隔音很差,走廊里的人声听的清清楚楚,毕竟价格实惠 266 | 一般的交通位置方便的酒店 267 | 房间不隔音,能听到麻将的声音,技师按摩还是挺不错的 268 | 服务很好,房间很干净 269 | 位置属于闹中取静,离景点都不远,但不算太嘈杂,酒店一看就是老牌,设施较陈旧,靠服务弥补了设施的减分,味道不错,质量也不错,关键是含在房费里,手快不细看很容易就选了...... 总体上酒店值得推荐 270 | 酒店服务态度很好,但周边环境一般,房间隔音效果差,由于隔音效果差,但硬件差也的确是硬伤, 271 | 房间很干净也很高级,下次一定还来 272 | 位置还不错,交通也便利,房间设施简单实用,性价比还行 273 | 房间太小没有窗,隔音差 274 | 酒店设施挺完善的,装修很新,环境舒适,老公工地长期这边出差了 275 | 房间也很旧 276 | 酒店整体感觉还行,老板守信用, 277 | 位置差着好几公里,有点骗人的感觉,好在环境不错,房间条件也还行,免费的, 278 | 房间很不错,空调也很给力,淋浴特别棒, 279 | 非常干净卫生,老板娘人非常好,还有房间里面画的画但是她们亲自设计的,有图为证 280 | 设施较好,住过比较多的酒店 281 | 性价比高,性价比高,性价比高,房间有点小,干净整洁,设施齐全,椅子也很舒服,到绵阳了 282 | 酒店很不错,设施齐全,总体给人的感觉不错 283 | 房间进去很干净,住的很舒服 284 | 房间环境很好,服务态度也很好 285 | 团购的房间比较小 不太舒服 286 | 房间好小~不过这个价格,也就这样了 287 | 酒店一般,第一是服务一般,第二是卫生一般 288 | 甘肃天水酒店宾馆、快捷酒店、快接宾馆、商务宾馆?回复:天水100左右的宾馆都还不错,如家等,价高条件差 289 | 酒店确实不错 290 | 服务很棒啊,房间卫生挺干净的,老板的服务态度也很好,位置好找,床也不错 291 | 酒店位于火车站附近,交通便利,酒店服务人员热情 292 | 房间比较小,就是只能看一场电影,就是感觉价格太贵了点 -------------------------------------------------------------------------------- /backend/textresource/test_hotel_small.txt: -------------------------------------------------------------------------------- 1 | 非常好的酒店 不枉我们爬了近一个小时的山,另外 大厨手艺非常棒 竹筒饭 竹筒鸡推荐入住的客人必须要点, 2 | 服务态度超好,房间也很干净,会定点再来的 3 | 酒店的房间很大,干净舒适,服务热情 4 | 怎么说呢,早上办理入住的,一进房间闷热的一股怪味,很臭,不能开热风,好多了,虽然房间小,但是合理范围 5 | 总台服务很差,房间一般 6 | 大床房间比较小,卫生情况还可以,超划算 7 | 酒店设施高大上,前台工作人员服务到位,早餐不错,总体五星 8 | 房间挺干净的,装修风格很不错,性价比很高,环境卫生都很好,这个价位来讲性价比很高 9 | 房间很干净 从泰山脚下打的到酒店门口只要13块, 10 | 房间挺漂亮的,就是小了点 11 | 院子挺漂亮,房间一般~ 12 | 环境挺好,房间也很舒服 13 | 酒店是新装修的,环境不错,饮食交通都算方便 14 | 位置很好,房间隔音一般,设施尚可 15 | 环境挺好的,房间很宽敞,只能说物超所值 16 | 房间比较小,属于住一晚就不想住的那种 17 | 房间大,只是不好停车,特别是床特舒服,服务态度很好,房间设施感觉像在家一样温馨 18 | 服务态度超好,房间也很干净,会定点再来的 19 | 房间很好呢, 20 | 朋友说环境不错,房间不大,但是很整洁,舒服,感觉很不错,只要有房间 -------------------------------------------------------------------------------- /backend/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | def format_results(results): 4 | result = [] 5 | for res in results: 6 | for dimension in res['评价维度']: 7 | aspect = dimension['text'] 8 | if('观点词' in dimension['relations']): 9 | opinions = [opinion['text'] for opinion in dimension['relations']['观点词']] 10 | else: 11 | opinions = None 12 | sentiment = dimension['relations']['情感倾向[正向,负向,未提及]'][0]['text'] 13 | con = {"aspect": aspect, "opinions": str(opinions), "sentiment": sentiment} 14 | result.append(con) 15 | return result 16 | 17 | def format_print(results): 18 | for res in results: 19 | print(f"aspect: {res['aspect']}, opinions: {res['opinions']}, sentiment: {res['sentiment']}") 20 | print() 21 | 22 | def load_txt(file_path): 23 | texts = [] 24 | with open(file_path, "r", encoding="utf-8") as f: 25 | for line in f.readlines(): 26 | texts.append(line.strip()) 27 | return texts 28 | 29 | def write_json_file(examples, save_path): 30 | with open(save_path, "w", encoding="utf-8") as f: 31 | for example in examples: 32 | line = json.dumps(example, ensure_ascii=False) 33 | f.write(line + "\n") 34 | 35 | def load_json_file(path): 36 | exmaples = [] 37 | with open(path, "r", encoding="utf-8") as f: 38 | for line in f.readlines(): 39 | example = json.loads(line) 40 | exmaples.append(example) 41 | return exmaples 42 | -------------------------------------------------------------------------------- /frontend/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | insert_final_newline = false 14 | trim_trailing_whitespace = false 15 | -------------------------------------------------------------------------------- /frontend/.env.development: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'development' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/dev-api' 6 | -------------------------------------------------------------------------------- /frontend/.env.production: -------------------------------------------------------------------------------- 1 | # just a flag 2 | ENV = 'production' 3 | 4 | # base api 5 | VUE_APP_BASE_API = '/prod-api' 6 | 7 | -------------------------------------------------------------------------------- /frontend/.env.staging: -------------------------------------------------------------------------------- 1 | NODE_ENV = production 2 | 3 | # just a flag 4 | ENV = 'staging' 5 | 6 | # base api 7 | VUE_APP_BASE_API = '/stage-api' 8 | 9 | -------------------------------------------------------------------------------- /frontend/.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | src/assets 3 | public 4 | dist 5 | -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parserOptions: { 4 | parser: 'babel-eslint', 5 | sourceType: 'module' 6 | }, 7 | env: { 8 | browser: true, 9 | node: true, 10 | es6: true, 11 | }, 12 | extends: ['plugin:vue/recommended', 'eslint:recommended'], 13 | 14 | // add your custom rules here 15 | //it is base on https://github.com/vuejs/eslint-config-vue 16 | rules: { 17 | "vue/max-attributes-per-line": [2, { 18 | "singleline": 10, 19 | "multiline": { 20 | "max": 1, 21 | "allowFirstLine": false 22 | } 23 | }], 24 | "vue/singleline-html-element-content-newline": "off", 25 | "vue/multiline-html-element-content-newline":"off", 26 | "vue/name-property-casing": ["error", "PascalCase"], 27 | "vue/no-v-html": "off", 28 | 'accessor-pairs': 2, 29 | 'arrow-spacing': [2, { 30 | 'before': true, 31 | 'after': true 32 | }], 33 | 'block-spacing': [2, 'always'], 34 | 'brace-style': [2, '1tbs', { 35 | 'allowSingleLine': true 36 | }], 37 | 'camelcase': [0, { 38 | 'properties': 'always' 39 | }], 40 | 'comma-dangle': [2, 'never'], 41 | 'comma-spacing': [2, { 42 | 'before': false, 43 | 'after': true 44 | }], 45 | 'comma-style': [2, 'last'], 46 | 'constructor-super': 2, 47 | 'curly': [2, 'multi-line'], 48 | 'dot-location': [2, 'property'], 49 | 'eol-last': 2, 50 | 'eqeqeq': ["error", "always", {"null": "ignore"}], 51 | 'generator-star-spacing': [2, { 52 | 'before': true, 53 | 'after': true 54 | }], 55 | 'handle-callback-err': [2, '^(err|error)$'], 56 | 'indent': [2, 2, { 57 | 'SwitchCase': 1 58 | }], 59 | 'jsx-quotes': [2, 'prefer-single'], 60 | 'key-spacing': [2, { 61 | 'beforeColon': false, 62 | 'afterColon': true 63 | }], 64 | 'keyword-spacing': [2, { 65 | 'before': true, 66 | 'after': true 67 | }], 68 | 'new-cap': [2, { 69 | 'newIsCap': true, 70 | 'capIsNew': false 71 | }], 72 | 'new-parens': 2, 73 | 'no-array-constructor': 2, 74 | 'no-caller': 2, 75 | 'no-console': 'off', 76 | 'no-class-assign': 2, 77 | 'no-cond-assign': 2, 78 | 'no-const-assign': 2, 79 | 'no-control-regex': 0, 80 | 'no-delete-var': 2, 81 | 'no-dupe-args': 2, 82 | 'no-dupe-class-members': 2, 83 | 'no-dupe-keys': 2, 84 | 'no-duplicate-case': 2, 85 | 'no-empty-character-class': 2, 86 | 'no-empty-pattern': 2, 87 | 'no-eval': 2, 88 | 'no-ex-assign': 2, 89 | 'no-extend-native': 2, 90 | 'no-extra-bind': 2, 91 | 'no-extra-boolean-cast': 2, 92 | 'no-extra-parens': [2, 'functions'], 93 | 'no-fallthrough': 2, 94 | 'no-floating-decimal': 2, 95 | 'no-func-assign': 2, 96 | 'no-implied-eval': 2, 97 | 'no-inner-declarations': [2, 'functions'], 98 | 'no-invalid-regexp': 2, 99 | 'no-irregular-whitespace': 2, 100 | 'no-iterator': 2, 101 | 'no-label-var': 2, 102 | 'no-labels': [2, { 103 | 'allowLoop': false, 104 | 'allowSwitch': false 105 | }], 106 | 'no-lone-blocks': 2, 107 | 'no-mixed-spaces-and-tabs': 2, 108 | 'no-multi-spaces': 2, 109 | 'no-multi-str': 2, 110 | 'no-multiple-empty-lines': [2, { 111 | 'max': 1 112 | }], 113 | 'no-native-reassign': 2, 114 | 'no-negated-in-lhs': 2, 115 | 'no-new-object': 2, 116 | 'no-new-require': 2, 117 | 'no-new-symbol': 2, 118 | 'no-new-wrappers': 2, 119 | 'no-obj-calls': 2, 120 | 'no-octal': 2, 121 | 'no-octal-escape': 2, 122 | 'no-path-concat': 2, 123 | 'no-proto': 2, 124 | 'no-redeclare': 2, 125 | 'no-regex-spaces': 2, 126 | 'no-return-assign': [2, 'except-parens'], 127 | 'no-self-assign': 2, 128 | 'no-self-compare': 2, 129 | 'no-sequences': 2, 130 | 'no-shadow-restricted-names': 2, 131 | 'no-spaced-func': 2, 132 | 'no-sparse-arrays': 2, 133 | 'no-this-before-super': 2, 134 | 'no-throw-literal': 2, 135 | 'no-trailing-spaces': 2, 136 | 'no-undef': 2, 137 | 'no-undef-init': 2, 138 | 'no-unexpected-multiline': 2, 139 | 'no-unmodified-loop-condition': 2, 140 | 'no-unneeded-ternary': [2, { 141 | 'defaultAssignment': false 142 | }], 143 | 'no-unreachable': 2, 144 | 'no-unsafe-finally': 2, 145 | 'no-unused-vars': [2, { 146 | 'vars': 'all', 147 | 'args': 'none' 148 | }], 149 | 'no-useless-call': 2, 150 | 'no-useless-computed-key': 2, 151 | 'no-useless-constructor': 2, 152 | 'no-useless-escape': 0, 153 | 'no-whitespace-before-property': 2, 154 | 'no-with': 2, 155 | 'one-var': [2, { 156 | 'initialized': 'never' 157 | }], 158 | 'operator-linebreak': [2, 'after', { 159 | 'overrides': { 160 | '?': 'before', 161 | ':': 'before' 162 | } 163 | }], 164 | 'padded-blocks': [2, 'never'], 165 | 'quotes': [2, 'single', { 166 | 'avoidEscape': true, 167 | 'allowTemplateLiterals': true 168 | }], 169 | 'semi': [2, 'never'], 170 | 'semi-spacing': [2, { 171 | 'before': false, 172 | 'after': true 173 | }], 174 | 'space-before-blocks': [2, 'always'], 175 | 'space-before-function-paren': [2, 'never'], 176 | 'space-in-parens': [2, 'never'], 177 | 'space-infix-ops': 2, 178 | 'space-unary-ops': [2, { 179 | 'words': true, 180 | 'nonwords': false 181 | }], 182 | 'spaced-comment': [2, 'always', { 183 | 'markers': ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ','] 184 | }], 185 | 'template-curly-spacing': [2, 'never'], 186 | 'use-isnan': 2, 187 | 'valid-typeof': 2, 188 | 'wrap-iife': [2, 'any'], 189 | 'yield-star-spacing': [2, 'both'], 190 | 'yoda': [2, 'never'], 191 | 'prefer-const': 2, 192 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 193 | 'object-curly-spacing': [2, 'always', { 194 | objectsInObjects: false 195 | }], 196 | 'array-bracket-spacing': [2, 'never'] 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | package-lock.json 8 | tests/**/coverage/ 9 | 10 | # Editor directories and files 11 | .idea 12 | .vscode 13 | *.suo 14 | *.ntvs* 15 | *.njsproj 16 | *.sln 17 | -------------------------------------------------------------------------------- /frontend/.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 10 3 | script: npm run test 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /frontend/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-present PanJiaChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /frontend/README-zh.md: -------------------------------------------------------------------------------- 1 | # vue-admin-template 2 | 3 | > 这是一个极简的 vue admin 管理后台。它只包含了 Element UI & axios & iconfont & permission control & lint,这些搭建后台必要的东西。 4 | 5 | [线上地址](http://panjiachen.github.io/vue-admin-template) 6 | 7 | [国内访问](https://panjiachen.gitee.io/vue-admin-template) 8 | 9 | 目前版本为 `v4.0+` 基于 `vue-cli` 进行构建,若你想使用旧版本,可以切换分支到[tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0),它不依赖 `vue-cli`。 10 | 11 |

12 | SPONSORED BY 13 |

14 |

15 | 16 | 17 | 18 |

19 | 20 | ## Extra 21 | 22 | 如果你想要根据用户角色来动态生成侧边栏和 router,你可以使用该分支[permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) 23 | 24 | ## 相关项目 25 | 26 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 27 | 28 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) 29 | 30 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) 31 | 32 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) 33 | 34 | 写了一个系列的教程配套文章,如何从零构建后一个完整的后台项目: 35 | 36 | - [手摸手,带你用 vue 撸后台 系列一(基础篇)](https://juejin.im/post/59097cd7a22b9d0065fb61d2) 37 | - [手摸手,带你用 vue 撸后台 系列二(登录权限篇)](https://juejin.im/post/591aa14f570c35006961acac) 38 | - [手摸手,带你用 vue 撸后台 系列三 (实战篇)](https://juejin.im/post/593121aa0ce4630057f70d35) 39 | - [手摸手,带你用 vue 撸后台 系列四(vueAdmin 一个极简的后台基础模板,专门针对本项目的文章,算作是一篇文档)](https://juejin.im/post/595b4d776fb9a06bbe7dba56) 40 | - [手摸手,带你封装一个 vue component](https://segmentfault.com/a/1190000009090836) 41 | 42 | ## Build Setup 43 | 44 | ```bash 45 | # 克隆项目 46 | git clone https://github.com/PanJiaChen/vue-admin-template.git 47 | 48 | # 进入项目目录 49 | cd vue-admin-template 50 | 51 | # 安装依赖 52 | npm install 53 | 54 | # 建议不要直接使用 cnpm 安装以来,会有各种诡异的 bug。可以通过如下操作解决 npm 下载速度慢的问题 55 | npm install --registry=https://registry.npm.taobao.org 56 | 57 | # 启动服务 58 | npm run dev 59 | ``` 60 | 61 | 浏览器访问 [http://localhost:9528](http://localhost:9528) 62 | 63 | ## 发布 64 | 65 | ```bash 66 | # 构建测试环境 67 | npm run build:stage 68 | 69 | # 构建生产环境 70 | npm run build:prod 71 | ``` 72 | 73 | ## 其它 74 | 75 | ```bash 76 | # 预览发布环境效果 77 | npm run preview 78 | 79 | # 预览发布环境效果 + 静态资源分析 80 | npm run preview -- --report 81 | 82 | # 代码格式检查 83 | npm run lint 84 | 85 | # 代码格式检查并自动修复 86 | npm run lint -- --fix 87 | ``` 88 | 89 | 更多信息请参考 [使用文档](https://panjiachen.github.io/vue-element-admin-site/zh/) 90 | 91 | ## 购买贴纸 92 | 93 | 你也可以通过 购买[官方授权的贴纸](https://smallsticker.com/product/vue-element-admin) 的方式来支持 vue-element-admin - 每售出一张贴纸,我们将获得 2 元的捐赠。 94 | 95 | ## Demo 96 | 97 | ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) 98 | 99 | ## Browsers support 100 | 101 | Modern browsers and Internet Explorer 10+. 102 | 103 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 104 | | --------- | --------- | --------- | --------- | 105 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 106 | 107 | ## License 108 | 109 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. 110 | 111 | Copyright (c) 2017-present PanJiaChen 112 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # vue-admin-template 2 | 3 | English | [简体中文](./README-zh.md) 4 | 5 | > A minimal vue admin template with Element UI & axios & iconfont & permission control & lint 6 | 7 | **Live demo:** http://panjiachen.github.io/vue-admin-template 8 | 9 | 10 | **The current version is `v4.0+` build on `vue-cli`. If you want to use the old version , you can switch branch to [tag/3.11.0](https://github.com/PanJiaChen/vue-admin-template/tree/tag/3.11.0), it does not rely on `vue-cli`** 11 | 12 |

13 | SPONSORED BY 14 |

15 |

16 | 17 | 18 | 19 |

20 | 21 | ## Build Setup 22 | 23 | ```bash 24 | # clone the project 25 | git clone https://github.com/PanJiaChen/vue-admin-template.git 26 | 27 | # enter the project directory 28 | cd vue-admin-template 29 | 30 | # install dependency 31 | npm install 32 | 33 | # develop 34 | npm run dev 35 | ``` 36 | 37 | This will automatically open http://localhost:9528 38 | 39 | ## Build 40 | 41 | ```bash 42 | # build for test environment 43 | npm run build:stage 44 | 45 | # build for production environment 46 | npm run build:prod 47 | ``` 48 | 49 | ## Advanced 50 | 51 | ```bash 52 | # preview the release environment effect 53 | npm run preview 54 | 55 | # preview the release environment effect + static resource analysis 56 | npm run preview -- --report 57 | 58 | # code format check 59 | npm run lint 60 | 61 | # code format check and auto fix 62 | npm run lint -- --fix 63 | ``` 64 | 65 | Refer to [Documentation](https://panjiachen.github.io/vue-element-admin-site/guide/essentials/deploy.html) for more information 66 | 67 | ## Demo 68 | 69 | ![demo](https://github.com/PanJiaChen/PanJiaChen.github.io/blob/master/images/demo.gif) 70 | 71 | ## Extra 72 | 73 | If you want router permission && generate menu by user roles , you can use this branch [permission-control](https://github.com/PanJiaChen/vue-admin-template/tree/permission-control) 74 | 75 | For `typescript` version, you can use [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) (Credits: [@Armour](https://github.com/Armour)) 76 | 77 | ## Related Project 78 | 79 | - [vue-element-admin](https://github.com/PanJiaChen/vue-element-admin) 80 | 81 | - [electron-vue-admin](https://github.com/PanJiaChen/electron-vue-admin) 82 | 83 | - [vue-typescript-admin-template](https://github.com/Armour/vue-typescript-admin-template) 84 | 85 | - [awesome-project](https://github.com/PanJiaChen/vue-element-admin/issues/2312) 86 | 87 | ## Browsers support 88 | 89 | Modern browsers and Internet Explorer 10+. 90 | 91 | | [IE / Edge](http://godban.github.io/browsers-support-badges/)
IE / Edge | [Firefox](http://godban.github.io/browsers-support-badges/)
Firefox | [Chrome](http://godban.github.io/browsers-support-badges/)
Chrome | [Safari](http://godban.github.io/browsers-support-badges/)
Safari | 92 | | --------- | --------- | --------- | --------- | 93 | | IE10, IE11, Edge| last 2 versions| last 2 versions| last 2 versions 94 | 95 | ## License 96 | 97 | [MIT](https://github.com/PanJiaChen/vue-admin-template/blob/master/LICENSE) license. 98 | 99 | Copyright (c) 2017-present PanJiaChen 100 | -------------------------------------------------------------------------------- /frontend/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app 4 | '@vue/cli-plugin-babel/preset' 5 | ], 6 | 'env': { 7 | 'development': { 8 | // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require(). 9 | // This plugin can significantly increase the speed of hot updates, when you have a large number of pages. 10 | // https://panjiachen.github.io/vue-element-admin-site/guide/advanced/lazy-loading.html 11 | 'plugins': ['dynamic-import-node'] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /frontend/build/index.js: -------------------------------------------------------------------------------- 1 | const { run } = require('runjs') 2 | const chalk = require('chalk') 3 | const config = require('../vue.config.js') 4 | const rawArgv = process.argv.slice(2) 5 | const args = rawArgv.join(' ') 6 | 7 | if (process.env.npm_config_preview || rawArgv.includes('--preview')) { 8 | const report = rawArgv.includes('--report') 9 | 10 | run(`vue-cli-service build ${args}`) 11 | 12 | const port = 9526 13 | const publicPath = config.publicPath 14 | 15 | var connect = require('connect') 16 | var serveStatic = require('serve-static') 17 | const app = connect() 18 | 19 | app.use( 20 | publicPath, 21 | serveStatic('./dist', { 22 | index: ['index.html', '/'] 23 | }) 24 | ) 25 | 26 | app.listen(port, function () { 27 | console.log(chalk.green(`> Preview at http://localhost:${port}${publicPath}`)) 28 | if (report) { 29 | console.log(chalk.green(`> Report at http://localhost:${port}${publicPath}report.html`)) 30 | } 31 | 32 | }) 33 | } else { 34 | run(`vue-cli-service build ${args}`) 35 | } 36 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: ['js', 'jsx', 'json', 'vue'], 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest', 5 | '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 6 | 'jest-transform-stub', 7 | '^.+\\.jsx?$': 'babel-jest' 8 | }, 9 | moduleNameMapper: { 10 | '^@/(.*)$': '/src/$1' 11 | }, 12 | snapshotSerializers: ['jest-serializer-vue'], 13 | testMatch: [ 14 | '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)' 15 | ], 16 | collectCoverageFrom: ['src/utils/**/*.{js,vue}', '!src/utils/auth.js', '!src/utils/request.js', 'src/components/**/*.{js,vue}'], 17 | coverageDirectory: '/tests/unit/coverage', 18 | // 'collectCoverage': true, 19 | 'coverageReporters': [ 20 | 'lcov', 21 | 'text-summary' 22 | ], 23 | testURL: 'http://localhost/' 24 | } 25 | -------------------------------------------------------------------------------- /frontend/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": "./", 4 | "paths": { 5 | "@/*": ["src/*"] 6 | } 7 | }, 8 | "exclude": ["node_modules", "dist"] 9 | } 10 | -------------------------------------------------------------------------------- /frontend/mock/index.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | const { param2Obj } = require('./utils') 3 | 4 | const user = require('./user') 5 | const table = require('./table') 6 | 7 | const mocks = [ 8 | ...user, 9 | ...table 10 | ] 11 | 12 | // for front mock 13 | // please use it cautiously, it will redefine XMLHttpRequest, 14 | // which will cause many of your third-party libraries to be invalidated(like progress event). 15 | function mockXHR() { 16 | // mock patch 17 | // https://github.com/nuysoft/Mock/issues/300 18 | Mock.XHR.prototype.proxy_send = Mock.XHR.prototype.send 19 | Mock.XHR.prototype.send = function() { 20 | if (this.custom.xhr) { 21 | this.custom.xhr.withCredentials = this.withCredentials || false 22 | 23 | if (this.responseType) { 24 | this.custom.xhr.responseType = this.responseType 25 | } 26 | } 27 | this.proxy_send(...arguments) 28 | } 29 | 30 | function XHR2ExpressReqWrap(respond) { 31 | return function(options) { 32 | let result = null 33 | if (respond instanceof Function) { 34 | const { body, type, url } = options 35 | // https://expressjs.com/en/4x/api.html#req 36 | result = respond({ 37 | method: type, 38 | body: JSON.parse(body), 39 | query: param2Obj(url) 40 | }) 41 | } else { 42 | result = respond 43 | } 44 | return Mock.mock(result) 45 | } 46 | } 47 | 48 | for (const i of mocks) { 49 | Mock.mock(new RegExp(i.url), i.type || 'get', XHR2ExpressReqWrap(i.response)) 50 | } 51 | } 52 | 53 | module.exports = { 54 | mocks, 55 | mockXHR 56 | } 57 | 58 | -------------------------------------------------------------------------------- /frontend/mock/mock-server.js: -------------------------------------------------------------------------------- 1 | const chokidar = require('chokidar') 2 | const bodyParser = require('body-parser') 3 | const chalk = require('chalk') 4 | const path = require('path') 5 | const Mock = require('mockjs') 6 | 7 | const mockDir = path.join(process.cwd(), 'mock') 8 | 9 | function registerRoutes(app) { 10 | let mockLastIndex 11 | const { mocks } = require('./index.js') 12 | const mocksForServer = mocks.map(route => { 13 | return responseFake(route.url, route.type, route.response) 14 | }) 15 | for (const mock of mocksForServer) { 16 | app[mock.type](mock.url, mock.response) 17 | mockLastIndex = app._router.stack.length 18 | } 19 | const mockRoutesLength = Object.keys(mocksForServer).length 20 | return { 21 | mockRoutesLength: mockRoutesLength, 22 | mockStartIndex: mockLastIndex - mockRoutesLength 23 | } 24 | } 25 | 26 | function unregisterRoutes() { 27 | Object.keys(require.cache).forEach(i => { 28 | if (i.includes(mockDir)) { 29 | delete require.cache[require.resolve(i)] 30 | } 31 | }) 32 | } 33 | 34 | // for mock server 35 | const responseFake = (url, type, respond) => { 36 | return { 37 | url: new RegExp(`${process.env.VUE_APP_BASE_API}${url}`), 38 | type: type || 'get', 39 | response(req, res) { 40 | console.log('request invoke:' + req.path) 41 | res.json(Mock.mock(respond instanceof Function ? respond(req, res) : respond)) 42 | } 43 | } 44 | } 45 | 46 | module.exports = app => { 47 | // parse app.body 48 | // https://expressjs.com/en/4x/api.html#req.body 49 | app.use(bodyParser.json()) 50 | app.use(bodyParser.urlencoded({ 51 | extended: true 52 | })) 53 | 54 | const mockRoutes = registerRoutes(app) 55 | var mockRoutesLength = mockRoutes.mockRoutesLength 56 | var mockStartIndex = mockRoutes.mockStartIndex 57 | 58 | // watch files, hot reload mock server 59 | chokidar.watch(mockDir, { 60 | ignored: /mock-server/, 61 | ignoreInitial: true 62 | }).on('all', (event, path) => { 63 | if (event === 'change' || event === 'add') { 64 | try { 65 | // remove mock routes stack 66 | app._router.stack.splice(mockStartIndex, mockRoutesLength) 67 | 68 | // clear routes cache 69 | unregisterRoutes() 70 | 71 | const mockRoutes = registerRoutes(app) 72 | mockRoutesLength = mockRoutes.mockRoutesLength 73 | mockStartIndex = mockRoutes.mockStartIndex 74 | 75 | console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed ${path}`)) 76 | } catch (error) { 77 | console.log(chalk.redBright(error)) 78 | } 79 | } 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /frontend/mock/table.js: -------------------------------------------------------------------------------- 1 | const Mock = require('mockjs') 2 | 3 | const data = Mock.mock({ 4 | 'items|30': [{ 5 | id: '@id', 6 | title: '@sentence(10, 20)', 7 | 'status|1': ['published', 'draft', 'deleted'], 8 | author: 'name', 9 | display_time: '@datetime', 10 | pageviews: '@integer(300, 5000)' 11 | }] 12 | }) 13 | 14 | module.exports = [ 15 | { 16 | url: '/vue-admin-template/table/list', 17 | type: 'get', 18 | response: config => { 19 | const items = data.items 20 | return { 21 | code: 20000, 22 | data: { 23 | total: items.length, 24 | items: items 25 | } 26 | } 27 | } 28 | } 29 | ] 30 | -------------------------------------------------------------------------------- /frontend/mock/user.js: -------------------------------------------------------------------------------- 1 | 2 | const tokens = { 3 | admin: { 4 | token: 'admin-token' 5 | }, 6 | editor: { 7 | token: 'editor-token' 8 | } 9 | } 10 | 11 | const users = { 12 | 'admin-token': { 13 | roles: ['admin'], 14 | introduction: 'I am a super administrator', 15 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 16 | name: 'Super Admin' 17 | }, 18 | 'editor-token': { 19 | roles: ['editor'], 20 | introduction: 'I am an editor', 21 | avatar: 'https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif', 22 | name: 'Normal Editor' 23 | } 24 | } 25 | 26 | module.exports = [ 27 | // user login 28 | { 29 | url: '/vue-admin-template/user/login', 30 | type: 'post', 31 | response: config => { 32 | const { username } = config.body 33 | const token = tokens[username] 34 | 35 | // mock error 36 | if (!token) { 37 | return { 38 | code: 60204, 39 | message: 'Account and password are incorrect.' 40 | } 41 | } 42 | 43 | return { 44 | code: 20000, 45 | data: token 46 | } 47 | } 48 | }, 49 | 50 | // get user info 51 | { 52 | url: '/vue-admin-template/user/info\.*', 53 | type: 'get', 54 | response: config => { 55 | const { token } = config.query 56 | const info = users[token] 57 | 58 | // mock error 59 | if (!info) { 60 | return { 61 | code: 50008, 62 | message: 'Login failed, unable to get user details.' 63 | } 64 | } 65 | 66 | return { 67 | code: 20000, 68 | data: info 69 | } 70 | } 71 | }, 72 | 73 | // user logout 74 | { 75 | url: '/vue-admin-template/user/logout', 76 | type: 'post', 77 | response: _ => { 78 | return { 79 | code: 20000, 80 | data: 'success' 81 | } 82 | } 83 | } 84 | ] 85 | -------------------------------------------------------------------------------- /frontend/mock/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @param {string} url 3 | * @returns {Object} 4 | */ 5 | function param2Obj(url) { 6 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 7 | if (!search) { 8 | return {} 9 | } 10 | const obj = {} 11 | const searchArr = search.split('&') 12 | searchArr.forEach(v => { 13 | const index = v.indexOf('=') 14 | if (index !== -1) { 15 | const name = v.substring(0, index) 16 | const val = v.substring(index + 1, v.length) 17 | obj[name] = val 18 | } 19 | }) 20 | return obj 21 | } 22 | 23 | module.exports = { 24 | param2Obj 25 | } 26 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-admin-template", 3 | "version": "4.4.0", 4 | "description": "A vue admin template with Element UI & axios & iconfont & permission control & lint", 5 | "author": "Pan ", 6 | "scripts": { 7 | "dev": "vue-cli-service serve", 8 | "build:prod": "vue-cli-service build", 9 | "build:stage": "vue-cli-service build --mode staging", 10 | "preview": "node build/index.js --preview", 11 | "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.yml", 12 | "lint": "eslint --ext .js,.vue src", 13 | "test:unit": "jest --clearCache && vue-cli-service test:unit", 14 | "test:ci": "npm run lint && npm run test:unit" 15 | }, 16 | "dependencies": { 17 | "axios": "^0.18.1", 18 | "core-js": "3.6.5", 19 | "echarts": "^5.4.2", 20 | "echarts-wordcloud": "^2.1.0", 21 | "element-ui": "2.13.2", 22 | "file-saver": "^2.0.5", 23 | "js-cookie": "2.2.0", 24 | "mammoth": "^1.4.21", 25 | "normalize.css": "7.0.0", 26 | "nprogress": "0.2.0", 27 | "path-to-regexp": "2.4.0", 28 | "vue": "2.6.10", 29 | "vue-axios": "^3.4.1", 30 | "vue-router": "3.0.6", 31 | "vuex": "3.1.0", 32 | "xlsx": "^0.18.5" 33 | }, 34 | "devDependencies": { 35 | "@vue/cli-plugin-babel": "4.4.4", 36 | "@vue/cli-plugin-eslint": "4.4.4", 37 | "@vue/cli-plugin-unit-jest": "4.4.4", 38 | "@vue/cli-service": "4.4.4", 39 | "@vue/test-utils": "1.0.0-beta.29", 40 | "autoprefixer": "9.5.1", 41 | "babel-eslint": "10.1.0", 42 | "babel-jest": "23.6.0", 43 | "babel-plugin-dynamic-import-node": "2.3.3", 44 | "chalk": "2.4.2", 45 | "connect": "3.6.6", 46 | "eslint": "6.7.2", 47 | "eslint-plugin-vue": "6.2.2", 48 | "html-webpack-plugin": "3.2.0", 49 | "mockjs": "1.0.1-beta3", 50 | "runjs": "4.3.2", 51 | "sass": "1.26.8", 52 | "sass-loader": "8.0.2", 53 | "script-ext-html-webpack-plugin": "2.1.3", 54 | "serve-static": "1.13.2", 55 | "svg-sprite-loader": "4.1.3", 56 | "svgo": "1.2.2", 57 | "vue-template-compiler": "2.6.10" 58 | }, 59 | "browserslist": [ 60 | "> 1%", 61 | "last 2 versions" 62 | ], 63 | "engines": { 64 | "node": ">=8.9", 65 | "npm": ">= 3.0.0" 66 | }, 67 | "license": "MIT" 68 | } 69 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | 'plugins': { 5 | // to edit target browsers: use "browserslist" field in package.json 6 | 'autoprefixer': {} 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= webpackConfig.name %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /frontend/src/api/table.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function getList(params) { 4 | return request({ 5 | url: '/vue-admin-template/table/list', 6 | method: 'get', 7 | params 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/api/user.js: -------------------------------------------------------------------------------- 1 | import request from '@/utils/request' 2 | 3 | export function login(data) { 4 | return request({ 5 | url: '/vue-admin-template/user/login', 6 | method: 'post', 7 | data 8 | }) 9 | } 10 | 11 | export function getInfo(token) { 12 | return request({ 13 | url: '/vue-admin-template/user/info', 14 | method: 'get', 15 | params: { token } 16 | }) 17 | } 18 | 19 | export function logout() { 20 | return request({ 21 | url: '/vue-admin-template/user/logout', 22 | method: 'post' 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/frontend/src/assets/404_images/404.png -------------------------------------------------------------------------------- /frontend/src/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/frontend/src/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /frontend/src/assets/img/image1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/frontend/src/assets/img/image1.jpg -------------------------------------------------------------------------------- /frontend/src/assets/img/image2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/frontend/src/assets/img/image2.jpg -------------------------------------------------------------------------------- /frontend/src/components/Breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 65 | 66 | 79 | -------------------------------------------------------------------------------- /frontend/src/components/Hamburger/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 32 | 33 | 45 | -------------------------------------------------------------------------------- /frontend/src/components/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 47 | 48 | 63 | -------------------------------------------------------------------------------- /frontend/src/icons/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SvgIcon from '@/components/SvgIcon'// svg component 3 | 4 | // register globally 5 | Vue.component('svg-icon', SvgIcon) 6 | 7 | const req = require.context('./svg', false, /\.svg$/) 8 | const requireAll = requireContext => requireContext.keys().map(requireContext) 9 | requireAll(req) 10 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/example.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye-open.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/eye.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/nested.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/password.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/table.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/tree.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/icons/svgo.yml: -------------------------------------------------------------------------------- 1 | # replace default config 2 | 3 | # multipass: true 4 | # full: true 5 | 6 | plugins: 7 | 8 | # - name 9 | # 10 | # or: 11 | # - name: false 12 | # - name: true 13 | # 14 | # or: 15 | # - name: 16 | # param1: 1 17 | # param2: 2 18 | 19 | - removeAttrs: 20 | attrs: 21 | - 'fill' 22 | - 'fill-rule' 23 | -------------------------------------------------------------------------------- /frontend/src/layout/components/AppMain.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 32 | 33 | 41 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Navbar.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 58 | 59 | 137 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/FixiOSBug.js: -------------------------------------------------------------------------------- 1 | export default { 2 | computed: { 3 | device() { 4 | return this.$store.state.app.device 5 | } 6 | }, 7 | mounted() { 8 | // In order to fix the click on menu on the ios device will trigger the mouseleave bug 9 | // https://github.com/PanJiaChen/vue-element-admin/issues/1135 10 | this.fixBugIniOS() 11 | }, 12 | methods: { 13 | fixBugIniOS() { 14 | const $subMenu = this.$refs.subMenu 15 | if ($subMenu) { 16 | const handleMouseleave = $subMenu.handleMouseleave 17 | $subMenu.handleMouseleave = (e) => { 18 | if (this.device === 'mobile') { 19 | return 20 | } 21 | handleMouseleave(e) 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Item.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 42 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Link.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/Logo.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 33 | 34 | 83 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /frontend/src/layout/components/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 57 | -------------------------------------------------------------------------------- /frontend/src/layout/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Navbar } from './Navbar' 2 | export { default as Sidebar } from './Sidebar' 3 | export { default as AppMain } from './AppMain' 4 | -------------------------------------------------------------------------------- /frontend/src/layout/index.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 52 | 53 | 94 | -------------------------------------------------------------------------------- /frontend/src/layout/mixin/ResizeHandler.js: -------------------------------------------------------------------------------- 1 | import store from '@/store' 2 | 3 | const { body } = document 4 | const WIDTH = 992 // refer to Bootstrap's responsive design 5 | 6 | export default { 7 | watch: { 8 | $route(route) { 9 | if (this.device === 'mobile' && this.sidebar.opened) { 10 | store.dispatch('app/closeSideBar', { withoutAnimation: false }) 11 | } 12 | } 13 | }, 14 | beforeMount() { 15 | window.addEventListener('resize', this.$_resizeHandler) 16 | }, 17 | beforeDestroy() { 18 | window.removeEventListener('resize', this.$_resizeHandler) 19 | }, 20 | mounted() { 21 | const isMobile = this.$_isMobile() 22 | if (isMobile) { 23 | store.dispatch('app/toggleDevice', 'mobile') 24 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 25 | } 26 | }, 27 | methods: { 28 | // use $_ for mixins properties 29 | // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential 30 | $_isMobile() { 31 | const rect = body.getBoundingClientRect() 32 | return rect.width - 1 < WIDTH 33 | }, 34 | $_resizeHandler() { 35 | if (!document.hidden) { 36 | const isMobile = this.$_isMobile() 37 | store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') 38 | 39 | if (isMobile) { 40 | store.dispatch('app/closeSideBar', { withoutAnimation: true }) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import 'normalize.css/normalize.css' // A modern alternative to CSS resets 4 | 5 | import ElementUI from 'element-ui' 6 | import 'element-ui/lib/theme-chalk/index.css' 7 | // import locale from 'element-ui/lib/locale/lang/en' // lang i18n 8 | 9 | import '@/styles/index.scss' // global css 10 | 11 | import App from './App' 12 | import store from './store' 13 | import router from './router' 14 | 15 | import '@/icons' // icon 16 | import '@/permission' // permission control 17 | 18 | import exportToExcel from './utils/Excel' // Excel导出 19 | Vue.prototype.Excels = exportToExcel 20 | 21 | /** 22 | * If you don't want to use mock-server 23 | * you want to use MockJs for mock api 24 | * you can execute: mockXHR() 25 | * 26 | * Currently MockJs will be used in the production environment, 27 | * please remove it before going online ! ! ! 28 | */ 29 | if (process.env.NODE_ENV === 'production') { 30 | const { mockXHR } = require('../mock') 31 | mockXHR() 32 | } 33 | 34 | // set ElementUI lang to EN 35 | // Vue.use(ElementUI, { locale }) 36 | // 如果想要中文版 element-ui,按如下方式声明 37 | Vue.use(ElementUI) 38 | 39 | Vue.config.productionTip = false 40 | 41 | new Vue({ 42 | el: '#app', 43 | router, 44 | store, 45 | render: h => h(App) 46 | }) 47 | -------------------------------------------------------------------------------- /frontend/src/permission.js: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import store from './store' 3 | import { Message } from 'element-ui' 4 | import NProgress from 'nprogress' // progress bar 5 | import 'nprogress/nprogress.css' // progress bar style 6 | import { getToken } from '@/utils/auth' // get token from cookie 7 | import getPageTitle from '@/utils/get-page-title' 8 | 9 | NProgress.configure({ showSpinner: false }) // NProgress Configuration 10 | 11 | const whiteList = ['/login'] // no redirect whitelist 12 | 13 | router.beforeEach(async(to, from, next) => { 14 | // start progress bar 15 | NProgress.start() 16 | 17 | // set page title 18 | document.title = getPageTitle(to.meta.title) 19 | 20 | // determine whether the user has logged in 21 | const hasToken = getToken() 22 | 23 | if (hasToken) { 24 | if (to.path === '/login') { 25 | // if is logged in, redirect to the home page 26 | next({ path: '/' }) 27 | NProgress.done() 28 | } else { 29 | const hasGetUserInfo = store.getters.name 30 | if (hasGetUserInfo) { 31 | next() 32 | } else { 33 | try { 34 | // get user info 35 | await store.dispatch('user/getInfo') 36 | 37 | next() 38 | } catch (error) { 39 | // remove token and go to login page to re-login 40 | await store.dispatch('user/resetToken') 41 | Message.error(error || 'Has Error') 42 | next(`/login?redirect=${to.path}`) 43 | NProgress.done() 44 | } 45 | } 46 | } 47 | } else { 48 | /* has no token*/ 49 | 50 | if (whiteList.indexOf(to.path) !== -1) { 51 | // in the free login whitelist, go directly 52 | next() 53 | } else { 54 | // other pages that do not have permission to access are redirected to the login page. 55 | next(`/login?redirect=${to.path}`) 56 | NProgress.done() 57 | } 58 | } 59 | }) 60 | 61 | router.afterEach(() => { 62 | // finish progress bar 63 | NProgress.done() 64 | }) 65 | -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | Vue.use(Router) 5 | 6 | /* Layout */ 7 | import Layout from '@/layout' 8 | 9 | /** 10 | * Note: sub-menu only appear when route children.length >= 1 11 | * Detail see: https://panjiachen.github.io/vue-element-admin-site/guide/essentials/router-and-nav.html 12 | * 13 | * hidden: true if set true, item will not show in the sidebar(default is false) 14 | * alwaysShow: true if set true, will always show the root menu 15 | * if not set alwaysShow, when item has more than one children route, 16 | * it will becomes nested mode, otherwise not show the root menu 17 | * redirect: noRedirect if set noRedirect will no redirect in the breadcrumb 18 | * name:'router-name' the name is used by (must set!!!) 19 | * meta : { 20 | roles: ['admin','editor'] control the page roles (you can set multiple roles) 21 | title: 'title' the name show in sidebar and breadcrumb (recommend set) 22 | icon: 'svg-name'/'el-icon-x' the icon show in the sidebar 23 | breadcrumb: false if set false, the item will hidden in breadcrumb(default is true) 24 | activeMenu: '/example/list' if set path, the sidebar will highlight the path you set 25 | } 26 | */ 27 | 28 | /** 29 | * constantRoutes 30 | * a base page that does not have permission requirements 31 | * all roles can be accessed 32 | */ 33 | export const constantRoutes = [ 34 | { 35 | path: '/login', 36 | component: () => import('@/views/login/index'), 37 | hidden: true 38 | }, 39 | 40 | { 41 | path: '/404', 42 | component: () => import('@/views/404'), 43 | hidden: true 44 | }, 45 | 46 | { 47 | path: '/', 48 | component: Layout, 49 | redirect: '/dashboard', 50 | children: [{ 51 | path: 'dashboard', 52 | name: 'Dashboard', 53 | component: () => import('@/views/dashboard/index'), 54 | meta: { title: '主页', icon: 'el-icon-s-home' } 55 | }] 56 | }, 57 | 58 | { 59 | path: '/analysis', 60 | component: Layout, 61 | redirect: '/analysis/singleAnalysis', 62 | name: 'Analysis', 63 | meta: { title: '属性级情感分析', icon: 'el-icon-s-help' }, 64 | children: [ 65 | { 66 | path: 'singleAnalysis', 67 | name: 'singleAnalysis', 68 | component: () => import('@/views/singleAnalysis/index'), 69 | meta: { title: '单条文本分析', icon: 'el-icon-search' } 70 | }, 71 | { 72 | path: 'batchAnalysis', 73 | name: 'batchAnalysis', 74 | component: () => import('@/views/batchAnalysis/index'), 75 | meta: { title: '批量文本分析', icon: 'el-icon-document-copy' } 76 | } 77 | ] 78 | }, 79 | 80 | { 81 | path: 'external-link', 82 | component: Layout, 83 | children: [ 84 | { 85 | path: 'https://github.com/JIANG-HS/UIE-SentimentAnalysisWeb', 86 | meta: { title: '跳转Github', icon: 'link' } 87 | } 88 | ] 89 | }, 90 | 91 | // 404 page must be placed at the end !!! 92 | { path: '*', redirect: '/404', hidden: true } 93 | ] 94 | 95 | const createRouter = () => new Router({ 96 | // mode: 'history', // require service support 97 | scrollBehavior: () => ({ y: 0 }), 98 | routes: constantRoutes 99 | }) 100 | 101 | const router = createRouter() 102 | 103 | // Detail see: https://github.com/vuejs/vue-router/issues/1234#issuecomment-357941465 104 | export function resetRouter() { 105 | const newRouter = createRouter() 106 | router.matcher = newRouter.matcher // reset router 107 | } 108 | 109 | export default router 110 | -------------------------------------------------------------------------------- /frontend/src/settings.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | title: '基于UIE的舆情情感分析系统', 4 | 5 | /** 6 | * @type {boolean} true | false 7 | * @description Whether fix the header 8 | */ 9 | fixedHeader: false, 10 | 11 | /** 12 | * @type {boolean} true | false 13 | * @description Whether show the logo in sidebar 14 | */ 15 | sidebarLogo: false 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/store/getters.js: -------------------------------------------------------------------------------- 1 | const getters = { 2 | sidebar: state => state.app.sidebar, 3 | device: state => state.app.device, 4 | token: state => state.user.token, 5 | avatar: state => state.user.avatar, 6 | name: state => state.user.name 7 | } 8 | export default getters 9 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import getters from './getters' 4 | import app from './modules/app' 5 | import settings from './modules/settings' 6 | import user from './modules/user' 7 | 8 | Vue.use(Vuex) 9 | 10 | const store = new Vuex.Store({ 11 | modules: { 12 | app, 13 | settings, 14 | user 15 | }, 16 | getters 17 | }) 18 | 19 | export default store 20 | -------------------------------------------------------------------------------- /frontend/src/store/modules/app.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const state = { 4 | sidebar: { 5 | opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true, 6 | withoutAnimation: false 7 | }, 8 | device: 'desktop' 9 | } 10 | 11 | const mutations = { 12 | TOGGLE_SIDEBAR: state => { 13 | state.sidebar.opened = !state.sidebar.opened 14 | state.sidebar.withoutAnimation = false 15 | if (state.sidebar.opened) { 16 | Cookies.set('sidebarStatus', 1) 17 | } else { 18 | Cookies.set('sidebarStatus', 0) 19 | } 20 | }, 21 | CLOSE_SIDEBAR: (state, withoutAnimation) => { 22 | Cookies.set('sidebarStatus', 0) 23 | state.sidebar.opened = false 24 | state.sidebar.withoutAnimation = withoutAnimation 25 | }, 26 | TOGGLE_DEVICE: (state, device) => { 27 | state.device = device 28 | } 29 | } 30 | 31 | const actions = { 32 | toggleSideBar({ commit }) { 33 | commit('TOGGLE_SIDEBAR') 34 | }, 35 | closeSideBar({ commit }, { withoutAnimation }) { 36 | commit('CLOSE_SIDEBAR', withoutAnimation) 37 | }, 38 | toggleDevice({ commit }, device) { 39 | commit('TOGGLE_DEVICE', device) 40 | } 41 | } 42 | 43 | export default { 44 | namespaced: true, 45 | state, 46 | mutations, 47 | actions 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const { showSettings, fixedHeader, sidebarLogo } = defaultSettings 4 | 5 | const state = { 6 | showSettings: showSettings, 7 | fixedHeader: fixedHeader, 8 | sidebarLogo: sidebarLogo 9 | } 10 | 11 | const mutations = { 12 | CHANGE_SETTING: (state, { key, value }) => { 13 | // eslint-disable-next-line no-prototype-builtins 14 | if (state.hasOwnProperty(key)) { 15 | state[key] = value 16 | } 17 | } 18 | } 19 | 20 | const actions = { 21 | changeSetting({ commit }, data) { 22 | commit('CHANGE_SETTING', data) 23 | } 24 | } 25 | 26 | export default { 27 | namespaced: true, 28 | state, 29 | mutations, 30 | actions 31 | } 32 | 33 | -------------------------------------------------------------------------------- /frontend/src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { login, logout, getInfo } from '@/api/user' 2 | import { getToken, setToken, removeToken } from '@/utils/auth' 3 | import { resetRouter } from '@/router' 4 | 5 | const getDefaultState = () => { 6 | return { 7 | token: getToken(), 8 | name: '', 9 | avatar: '' 10 | } 11 | } 12 | 13 | const state = getDefaultState() 14 | 15 | const mutations = { 16 | RESET_STATE: (state) => { 17 | Object.assign(state, getDefaultState()) 18 | }, 19 | SET_TOKEN: (state, token) => { 20 | state.token = token 21 | }, 22 | SET_NAME: (state, name) => { 23 | state.name = name 24 | }, 25 | SET_AVATAR: (state, avatar) => { 26 | state.avatar = avatar 27 | } 28 | } 29 | 30 | const actions = { 31 | // user login 32 | login({ commit }, userInfo) { 33 | const { username, password } = userInfo 34 | return new Promise((resolve, reject) => { 35 | login({ username: username.trim(), password: password }).then(response => { 36 | const { data } = response 37 | commit('SET_TOKEN', data.token) 38 | setToken(data.token) 39 | resolve() 40 | }).catch(error => { 41 | reject(error) 42 | }) 43 | }) 44 | }, 45 | 46 | // get user info 47 | getInfo({ commit, state }) { 48 | return new Promise((resolve, reject) => { 49 | getInfo(state.token).then(response => { 50 | const { data } = response 51 | 52 | if (!data) { 53 | return reject('Verification failed, please Login again.') 54 | } 55 | 56 | const { name, avatar } = data 57 | 58 | commit('SET_NAME', name) 59 | commit('SET_AVATAR', avatar) 60 | resolve(data) 61 | }).catch(error => { 62 | reject(error) 63 | }) 64 | }) 65 | }, 66 | 67 | // user logout 68 | logout({ commit, state }) { 69 | return new Promise((resolve, reject) => { 70 | logout(state.token).then(() => { 71 | removeToken() // must remove token first 72 | resetRouter() 73 | commit('RESET_STATE') 74 | resolve() 75 | }).catch(error => { 76 | reject(error) 77 | }) 78 | }) 79 | }, 80 | 81 | // remove token 82 | resetToken({ commit }) { 83 | return new Promise(resolve => { 84 | removeToken() // must remove token first 85 | commit('RESET_STATE') 86 | resolve() 87 | }) 88 | } 89 | } 90 | 91 | export default { 92 | namespaced: true, 93 | state, 94 | mutations, 95 | actions 96 | } 97 | 98 | -------------------------------------------------------------------------------- /frontend/src/styles/element-ui.scss: -------------------------------------------------------------------------------- 1 | // cover some element-ui styles 2 | 3 | .el-breadcrumb__inner, 4 | .el-breadcrumb__inner a { 5 | font-weight: 400 !important; 6 | } 7 | 8 | .el-upload { 9 | input[type="file"] { 10 | display: none !important; 11 | } 12 | } 13 | 14 | .el-upload__input { 15 | display: none; 16 | } 17 | 18 | 19 | // to fixed https://github.com/ElemeFE/element/issues/2461 20 | .el-dialog { 21 | transform: none; 22 | left: 0; 23 | position: relative; 24 | margin: 0 auto; 25 | } 26 | 27 | // refine element ui upload 28 | .upload-container { 29 | .el-upload { 30 | width: 100%; 31 | 32 | .el-upload-dragger { 33 | width: 100%; 34 | height: 200px; 35 | } 36 | } 37 | } 38 | 39 | // dropdown 40 | .el-dropdown-menu { 41 | a { 42 | display: block 43 | } 44 | } 45 | 46 | // to fix el-date-picker css style 47 | .el-range-separator { 48 | box-sizing: content-box; 49 | } 50 | -------------------------------------------------------------------------------- /frontend/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './variables.scss'; 2 | @import './mixin.scss'; 3 | @import './transition.scss'; 4 | @import './element-ui.scss'; 5 | @import './sidebar.scss'; 6 | 7 | body { 8 | height: 100%; 9 | -moz-osx-font-smoothing: grayscale; 10 | -webkit-font-smoothing: antialiased; 11 | text-rendering: optimizeLegibility; 12 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 13 | } 14 | 15 | label { 16 | font-weight: 700; 17 | } 18 | 19 | html { 20 | height: 100%; 21 | box-sizing: border-box; 22 | } 23 | 24 | #app { 25 | height: 100%; 26 | } 27 | 28 | *, 29 | *:before, 30 | *:after { 31 | box-sizing: inherit; 32 | } 33 | 34 | a:focus, 35 | a:active { 36 | outline: none; 37 | } 38 | 39 | a, 40 | a:focus, 41 | a:hover { 42 | cursor: pointer; 43 | color: inherit; 44 | text-decoration: none; 45 | } 46 | 47 | div:focus { 48 | outline: none; 49 | } 50 | 51 | .clearfix { 52 | &:after { 53 | visibility: hidden; 54 | display: block; 55 | font-size: 0; 56 | content: " "; 57 | clear: both; 58 | height: 0; 59 | } 60 | } 61 | 62 | // main-container global css 63 | .app-container { 64 | padding: 20px; 65 | } 66 | -------------------------------------------------------------------------------- /frontend/src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin clearfix { 2 | &:after { 3 | content: ""; 4 | display: table; 5 | clear: both; 6 | } 7 | } 8 | 9 | @mixin scrollBar { 10 | &::-webkit-scrollbar-track-piece { 11 | background: #d3dce6; 12 | } 13 | 14 | &::-webkit-scrollbar { 15 | width: 6px; 16 | } 17 | 18 | &::-webkit-scrollbar-thumb { 19 | background: #99a9bf; 20 | border-radius: 20px; 21 | } 22 | } 23 | 24 | @mixin relative { 25 | position: relative; 26 | width: 100%; 27 | height: 100%; 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/styles/sidebar.scss: -------------------------------------------------------------------------------- 1 | #app { 2 | 3 | .main-container { 4 | min-height: 100%; 5 | transition: margin-left .28s; 6 | margin-left: $sideBarWidth; 7 | position: relative; 8 | } 9 | 10 | .sidebar-container { 11 | transition: width 0.28s; 12 | width: $sideBarWidth !important; 13 | background-color: $menuBg; 14 | height: 100%; 15 | position: fixed; 16 | font-size: 0px; 17 | top: 0; 18 | bottom: 0; 19 | left: 0; 20 | z-index: 1001; 21 | overflow: hidden; 22 | 23 | // reset element-ui css 24 | .horizontal-collapse-transition { 25 | transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out; 26 | } 27 | 28 | .scrollbar-wrapper { 29 | overflow-x: hidden !important; 30 | } 31 | 32 | .el-scrollbar__bar.is-vertical { 33 | right: 0px; 34 | } 35 | 36 | .el-scrollbar { 37 | height: 100%; 38 | } 39 | 40 | &.has-logo { 41 | .el-scrollbar { 42 | height: calc(100% - 50px); 43 | } 44 | } 45 | 46 | .is-horizontal { 47 | display: none; 48 | } 49 | 50 | a { 51 | display: inline-block; 52 | width: 100%; 53 | overflow: hidden; 54 | } 55 | 56 | .svg-icon { 57 | margin-right: 16px; 58 | } 59 | 60 | .sub-el-icon { 61 | margin-right: 12px; 62 | margin-left: -2px; 63 | } 64 | 65 | .el-menu { 66 | border: none; 67 | height: 100%; 68 | width: 100% !important; 69 | } 70 | 71 | // menu hover 72 | .submenu-title-noDropdown, 73 | .el-submenu__title { 74 | &:hover { 75 | background-color: $menuHover !important; 76 | } 77 | } 78 | 79 | .is-active>.el-submenu__title { 80 | color: $subMenuActiveText !important; 81 | } 82 | 83 | & .nest-menu .el-submenu>.el-submenu__title, 84 | & .el-submenu .el-menu-item { 85 | min-width: $sideBarWidth !important; 86 | background-color: $subMenuBg !important; 87 | 88 | &:hover { 89 | background-color: $subMenuHover !important; 90 | } 91 | } 92 | } 93 | 94 | .hideSidebar { 95 | .sidebar-container { 96 | width: 54px !important; 97 | } 98 | 99 | .main-container { 100 | margin-left: 54px; 101 | } 102 | 103 | .submenu-title-noDropdown { 104 | padding: 0 !important; 105 | position: relative; 106 | 107 | .el-tooltip { 108 | padding: 0 !important; 109 | 110 | .svg-icon { 111 | margin-left: 20px; 112 | } 113 | 114 | .sub-el-icon { 115 | margin-left: 19px; 116 | } 117 | } 118 | } 119 | 120 | .el-submenu { 121 | overflow: hidden; 122 | 123 | &>.el-submenu__title { 124 | padding: 0 !important; 125 | 126 | .svg-icon { 127 | margin-left: 20px; 128 | } 129 | 130 | .sub-el-icon { 131 | margin-left: 19px; 132 | } 133 | 134 | .el-submenu__icon-arrow { 135 | display: none; 136 | } 137 | } 138 | } 139 | 140 | .el-menu--collapse { 141 | .el-submenu { 142 | &>.el-submenu__title { 143 | &>span { 144 | height: 0; 145 | width: 0; 146 | overflow: hidden; 147 | visibility: hidden; 148 | display: inline-block; 149 | } 150 | } 151 | } 152 | } 153 | } 154 | 155 | .el-menu--collapse .el-menu .el-submenu { 156 | min-width: $sideBarWidth !important; 157 | } 158 | 159 | // mobile responsive 160 | .mobile { 161 | .main-container { 162 | margin-left: 0px; 163 | } 164 | 165 | .sidebar-container { 166 | transition: transform .28s; 167 | width: $sideBarWidth !important; 168 | } 169 | 170 | &.hideSidebar { 171 | .sidebar-container { 172 | pointer-events: none; 173 | transition-duration: 0.3s; 174 | transform: translate3d(-$sideBarWidth, 0, 0); 175 | } 176 | } 177 | } 178 | 179 | .withoutAnimation { 180 | 181 | .main-container, 182 | .sidebar-container { 183 | transition: none; 184 | } 185 | } 186 | } 187 | 188 | // when menu collapsed 189 | .el-menu--vertical { 190 | &>.el-menu { 191 | .svg-icon { 192 | margin-right: 16px; 193 | } 194 | .sub-el-icon { 195 | margin-right: 12px; 196 | margin-left: -2px; 197 | } 198 | } 199 | 200 | .nest-menu .el-submenu>.el-submenu__title, 201 | .el-menu-item { 202 | &:hover { 203 | // you can use $subMenuHover 204 | background-color: $menuHover !important; 205 | } 206 | } 207 | 208 | // the scroll bar appears when the subMenu is too long 209 | >.el-menu--popup { 210 | max-height: 100vh; 211 | overflow-y: auto; 212 | 213 | &::-webkit-scrollbar-track-piece { 214 | background: #d3dce6; 215 | } 216 | 217 | &::-webkit-scrollbar { 218 | width: 6px; 219 | } 220 | 221 | &::-webkit-scrollbar-thumb { 222 | background: #99a9bf; 223 | border-radius: 20px; 224 | } 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /frontend/src/styles/transition.scss: -------------------------------------------------------------------------------- 1 | // global transition css 2 | 3 | /* fade */ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /* fade-transform */ 15 | .fade-transform-leave-active, 16 | .fade-transform-enter-active { 17 | transition: all .5s; 18 | } 19 | 20 | .fade-transform-enter { 21 | opacity: 0; 22 | transform: translateX(-30px); 23 | } 24 | 25 | .fade-transform-leave-to { 26 | opacity: 0; 27 | transform: translateX(30px); 28 | } 29 | 30 | /* breadcrumb transition */ 31 | .breadcrumb-enter-active, 32 | .breadcrumb-leave-active { 33 | transition: all .5s; 34 | } 35 | 36 | .breadcrumb-enter, 37 | .breadcrumb-leave-active { 38 | opacity: 0; 39 | transform: translateX(20px); 40 | } 41 | 42 | .breadcrumb-move { 43 | transition: all .5s; 44 | } 45 | 46 | .breadcrumb-leave-active { 47 | position: absolute; 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText:#bfcbd9; 3 | $menuActiveText:#409EFF; 4 | $subMenuActiveText:#f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg:#304156; 7 | $menuHover:#263445; 8 | 9 | $subMenuBg:#1f2d3d; 10 | $subMenuHover:#001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // the :export directive is the magic sauce for webpack 15 | // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass 16 | :export { 17 | menuText: $menuText; 18 | menuActiveText: $menuActiveText; 19 | subMenuActiveText: $subMenuActiveText; 20 | menuBg: $menuBg; 21 | menuHover: $menuHover; 22 | subMenuBg: $subMenuBg; 23 | subMenuHover: $subMenuHover; 24 | sideBarWidth: $sideBarWidth; 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/utils/Excel.js: -------------------------------------------------------------------------------- 1 | import FileSaver from 'file-saver' 2 | import * as XLSX from 'xlsx/xlsx.mjs' 3 | export default { 4 | // 导出Excel表格 5 | exportExcel(name, tableName) { 6 | // name表示生成excel的文件名 tableName表示表格的id 7 | var sel = XLSX.utils.table_to_book(document.querySelector(tableName)) 8 | var selIn = XLSX.write(sel, { 9 | bookType: 'xlsx', 10 | bookSST: true, 11 | type: 'array' 12 | }) 13 | try { 14 | FileSaver.saveAs( 15 | new Blob([selIn], { type: 'application/octet-stream' }), 16 | name 17 | ) 18 | } catch (e) { 19 | if (typeof console !== 'undefined') console.log(e, selIn) 20 | } 21 | return selIn 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookies from 'js-cookie' 2 | 3 | const TokenKey = 'vue_admin_template_token' 4 | 5 | export function getToken() { 6 | return Cookies.get(TokenKey) 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookies.set(TokenKey, token) 11 | } 12 | 13 | export function removeToken() { 14 | return Cookies.remove(TokenKey) 15 | } 16 | -------------------------------------------------------------------------------- /frontend/src/utils/get-page-title.js: -------------------------------------------------------------------------------- 1 | import defaultSettings from '@/settings' 2 | 3 | const title = defaultSettings.title || '文本纠错系统' 4 | 5 | export default function getPageTitle(pageTitle) { 6 | if (pageTitle) { 7 | return `${pageTitle} - ${title}` 8 | } 9 | return `${title}` 10 | } 11 | -------------------------------------------------------------------------------- /frontend/src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * Parse the time to string 7 | * @param {(Object|string|number)} time 8 | * @param {string} cFormat 9 | * @returns {string | null} 10 | */ 11 | export function parseTime(time, cFormat) { 12 | if (arguments.length === 0 || !time) { 13 | return null 14 | } 15 | const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}' 16 | let date 17 | if (typeof time === 'object') { 18 | date = time 19 | } else { 20 | if ((typeof time === 'string')) { 21 | if ((/^[0-9]+$/.test(time))) { 22 | // support "1548221490638" 23 | time = parseInt(time) 24 | } else { 25 | // support safari 26 | // https://stackoverflow.com/questions/4310953/invalid-date-in-safari 27 | time = time.replace(new RegExp(/-/gm), '/') 28 | } 29 | } 30 | 31 | if ((typeof time === 'number') && (time.toString().length === 10)) { 32 | time = time * 1000 33 | } 34 | date = new Date(time) 35 | } 36 | const formatObj = { 37 | y: date.getFullYear(), 38 | m: date.getMonth() + 1, 39 | d: date.getDate(), 40 | h: date.getHours(), 41 | i: date.getMinutes(), 42 | s: date.getSeconds(), 43 | a: date.getDay() 44 | } 45 | const time_str = format.replace(/{([ymdhisa])+}/g, (result, key) => { 46 | const value = formatObj[key] 47 | // Note: getDay() returns 0 on Sunday 48 | if (key === 'a') { return ['日', '一', '二', '三', '四', '五', '六'][value ] } 49 | return value.toString().padStart(2, '0') 50 | }) 51 | return time_str 52 | } 53 | 54 | /** 55 | * @param {number} time 56 | * @param {string} option 57 | * @returns {string} 58 | */ 59 | export function formatTime(time, option) { 60 | if (('' + time).length === 10) { 61 | time = parseInt(time) * 1000 62 | } else { 63 | time = +time 64 | } 65 | const d = new Date(time) 66 | const now = Date.now() 67 | 68 | const diff = (now - d) / 1000 69 | 70 | if (diff < 30) { 71 | return '刚刚' 72 | } else if (diff < 3600) { 73 | // less 1 hour 74 | return Math.ceil(diff / 60) + '分钟前' 75 | } else if (diff < 3600 * 24) { 76 | return Math.ceil(diff / 3600) + '小时前' 77 | } else if (diff < 3600 * 24 * 2) { 78 | return '1天前' 79 | } 80 | if (option) { 81 | return parseTime(time, option) 82 | } else { 83 | return ( 84 | d.getMonth() + 85 | 1 + 86 | '月' + 87 | d.getDate() + 88 | '日' + 89 | d.getHours() + 90 | '时' + 91 | d.getMinutes() + 92 | '分' 93 | ) 94 | } 95 | } 96 | 97 | /** 98 | * @param {string} url 99 | * @returns {Object} 100 | */ 101 | export function param2Obj(url) { 102 | const search = decodeURIComponent(url.split('?')[1]).replace(/\+/g, ' ') 103 | if (!search) { 104 | return {} 105 | } 106 | const obj = {} 107 | const searchArr = search.split('&') 108 | searchArr.forEach(v => { 109 | const index = v.indexOf('=') 110 | if (index !== -1) { 111 | const name = v.substring(0, index) 112 | const val = v.substring(index + 1, v.length) 113 | obj[name] = val 114 | } 115 | }) 116 | return obj 117 | } 118 | -------------------------------------------------------------------------------- /frontend/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { MessageBox, Message } from 'element-ui' 3 | import store from '@/store' 4 | import { getToken } from '@/utils/auth' 5 | 6 | // create an axios instance 7 | const service = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url 9 | // withCredentials: true, // send cookies when cross-domain requests 10 | timeout: 5000 // request timeout 11 | }) 12 | 13 | // request interceptor 14 | service.interceptors.request.use( 15 | config => { 16 | // do something before request is sent 17 | 18 | if (store.getters.token) { 19 | // let each request carry token 20 | // ['X-Token'] is a custom headers key 21 | // please modify it according to the actual situation 22 | config.headers['X-Token'] = getToken() 23 | } 24 | return config 25 | }, 26 | error => { 27 | // do something with request error 28 | console.log(error) // for debug 29 | return Promise.reject(error) 30 | } 31 | ) 32 | 33 | // response interceptor 34 | service.interceptors.response.use( 35 | /** 36 | * If you want to get http information such as headers or status 37 | * Please return response => response 38 | */ 39 | 40 | /** 41 | * Determine the request status by custom code 42 | * Here is just an example 43 | * You can also judge the status by HTTP Status Code 44 | */ 45 | response => { 46 | const res = response.data 47 | 48 | // if the custom code is not 20000, it is judged as an error. 49 | if (res.code !== 20000) { 50 | Message({ 51 | message: res.message || 'Error', 52 | type: 'error', 53 | duration: 5 * 1000 54 | }) 55 | 56 | // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired; 57 | if (res.code === 50008 || res.code === 50012 || res.code === 50014) { 58 | // to re-login 59 | MessageBox.confirm('You have been logged out, you can cancel to stay on this page, or log in again', 'Confirm logout', { 60 | confirmButtonText: 'Re-Login', 61 | cancelButtonText: 'Cancel', 62 | type: 'warning' 63 | }).then(() => { 64 | store.dispatch('user/resetToken').then(() => { 65 | location.reload() 66 | }) 67 | }) 68 | } 69 | return Promise.reject(new Error(res.message || 'Error')) 70 | } else { 71 | return res 72 | } 73 | }, 74 | error => { 75 | console.log('err' + error) // for debug 76 | Message({ 77 | message: error.message, 78 | type: 'error', 79 | duration: 5 * 1000 80 | }) 81 | return Promise.reject(error) 82 | } 83 | ) 84 | 85 | export default service 86 | -------------------------------------------------------------------------------- /frontend/src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by PanJiaChen on 16/11/18. 3 | */ 4 | 5 | /** 6 | * @param {string} path 7 | * @returns {Boolean} 8 | */ 9 | export function isExternal(path) { 10 | return /^(https?:|mailto:|tel:)/.test(path) 11 | } 12 | 13 | /** 14 | * @param {string} str 15 | * @returns {Boolean} 16 | */ 17 | export function validUsername(str) { 18 | const valid_map = ['admin', 'editor'] 19 | return valid_map.indexOf(str.trim()) >= 0 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/views/404.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 34 | 35 | 229 | -------------------------------------------------------------------------------- /frontend/src/views/batchAnalysis/index.vue: -------------------------------------------------------------------------------- 1 | 83 | 84 | 483 | 484 | 499 | -------------------------------------------------------------------------------- /frontend/src/views/dashboard/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 84 | 85 | 94 | -------------------------------------------------------------------------------- /frontend/src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 127 | 128 | 174 | 175 | 238 | -------------------------------------------------------------------------------- /frontend/src/views/singleAnalysis/index.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 108 | 109 | 117 | -------------------------------------------------------------------------------- /frontend/tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/tests/unit/components/Breadcrumb.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, createLocalVue } from '@vue/test-utils' 2 | import VueRouter from 'vue-router' 3 | import ElementUI from 'element-ui' 4 | import Breadcrumb from '@/components/Breadcrumb/index.vue' 5 | 6 | const localVue = createLocalVue() 7 | localVue.use(VueRouter) 8 | localVue.use(ElementUI) 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | name: 'home', 14 | children: [{ 15 | path: 'dashboard', 16 | name: 'dashboard' 17 | }] 18 | }, 19 | { 20 | path: '/menu', 21 | name: 'menu', 22 | children: [{ 23 | path: 'menu1', 24 | name: 'menu1', 25 | meta: { title: 'menu1' }, 26 | children: [{ 27 | path: 'menu1-1', 28 | name: 'menu1-1', 29 | meta: { title: 'menu1-1' } 30 | }, 31 | { 32 | path: 'menu1-2', 33 | name: 'menu1-2', 34 | redirect: 'noredirect', 35 | meta: { title: 'menu1-2' }, 36 | children: [{ 37 | path: 'menu1-2-1', 38 | name: 'menu1-2-1', 39 | meta: { title: 'menu1-2-1' } 40 | }, 41 | { 42 | path: 'menu1-2-2', 43 | name: 'menu1-2-2' 44 | }] 45 | }] 46 | }] 47 | }] 48 | 49 | const router = new VueRouter({ 50 | routes 51 | }) 52 | 53 | describe('Breadcrumb.vue', () => { 54 | const wrapper = mount(Breadcrumb, { 55 | localVue, 56 | router 57 | }) 58 | it('dashboard', () => { 59 | router.push('/dashboard') 60 | const len = wrapper.findAll('.el-breadcrumb__inner').length 61 | expect(len).toBe(1) 62 | }) 63 | it('normal route', () => { 64 | router.push('/menu/menu1') 65 | const len = wrapper.findAll('.el-breadcrumb__inner').length 66 | expect(len).toBe(2) 67 | }) 68 | it('nested route', () => { 69 | router.push('/menu/menu1/menu1-2/menu1-2-1') 70 | const len = wrapper.findAll('.el-breadcrumb__inner').length 71 | expect(len).toBe(4) 72 | }) 73 | it('no meta.title', () => { 74 | router.push('/menu/menu1/menu1-2/menu1-2-2') 75 | const len = wrapper.findAll('.el-breadcrumb__inner').length 76 | expect(len).toBe(3) 77 | }) 78 | // it('click link', () => { 79 | // router.push('/menu/menu1/menu1-2/menu1-2-2') 80 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 81 | // const second = breadcrumbArray.at(1) 82 | // console.log(breadcrumbArray) 83 | // const href = second.find('a').attributes().href 84 | // expect(href).toBe('#/menu/menu1') 85 | // }) 86 | // it('noRedirect', () => { 87 | // router.push('/menu/menu1/menu1-2/menu1-2-1') 88 | // const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 89 | // const redirectBreadcrumb = breadcrumbArray.at(2) 90 | // expect(redirectBreadcrumb.contains('a')).toBe(false) 91 | // }) 92 | it('last breadcrumb', () => { 93 | router.push('/menu/menu1/menu1-2/menu1-2-1') 94 | const breadcrumbArray = wrapper.findAll('.el-breadcrumb__inner') 95 | const redirectBreadcrumb = breadcrumbArray.at(3) 96 | expect(redirectBreadcrumb.contains('a')).toBe(false) 97 | }) 98 | }) 99 | -------------------------------------------------------------------------------- /frontend/tests/unit/components/Hamburger.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import Hamburger from '@/components/Hamburger/index.vue' 3 | describe('Hamburger.vue', () => { 4 | it('toggle click', () => { 5 | const wrapper = shallowMount(Hamburger) 6 | const mockFn = jest.fn() 7 | wrapper.vm.$on('toggleClick', mockFn) 8 | wrapper.find('.hamburger').trigger('click') 9 | expect(mockFn).toBeCalled() 10 | }) 11 | it('prop isActive', () => { 12 | const wrapper = shallowMount(Hamburger) 13 | wrapper.setProps({ isActive: true }) 14 | expect(wrapper.contains('.is-active')).toBe(true) 15 | wrapper.setProps({ isActive: false }) 16 | expect(wrapper.contains('.is-active')).toBe(false) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /frontend/tests/unit/components/SvgIcon.spec.js: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import SvgIcon from '@/components/SvgIcon/index.vue' 3 | describe('SvgIcon.vue', () => { 4 | it('iconClass', () => { 5 | const wrapper = shallowMount(SvgIcon, { 6 | propsData: { 7 | iconClass: 'test' 8 | } 9 | }) 10 | expect(wrapper.find('use').attributes().href).toBe('#icon-test') 11 | }) 12 | it('className', () => { 13 | const wrapper = shallowMount(SvgIcon, { 14 | propsData: { 15 | iconClass: 'test' 16 | } 17 | }) 18 | expect(wrapper.classes().length).toBe(1) 19 | wrapper.setProps({ className: 'test' }) 20 | expect(wrapper.classes().includes('test')).toBe(true) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /frontend/tests/unit/utils/formatTime.spec.js: -------------------------------------------------------------------------------- 1 | import { formatTime } from '@/utils/index.js' 2 | 3 | describe('Utils:formatTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | const retrofit = 5 * 1000 6 | 7 | it('ten digits timestamp', () => { 8 | expect(formatTime((d / 1000).toFixed(0))).toBe('7月13日17时54分') 9 | }) 10 | it('test now', () => { 11 | expect(formatTime(+new Date() - 1)).toBe('刚刚') 12 | }) 13 | it('less two minute', () => { 14 | expect(formatTime(+new Date() - 60 * 2 * 1000 + retrofit)).toBe('2分钟前') 15 | }) 16 | it('less two hour', () => { 17 | expect(formatTime(+new Date() - 60 * 60 * 2 * 1000 + retrofit)).toBe('2小时前') 18 | }) 19 | it('less one day', () => { 20 | expect(formatTime(+new Date() - 60 * 60 * 24 * 1 * 1000)).toBe('1天前') 21 | }) 22 | it('more than one day', () => { 23 | expect(formatTime(d)).toBe('7月13日17时54分') 24 | }) 25 | it('format', () => { 26 | expect(formatTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 27 | expect(formatTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 28 | expect(formatTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /frontend/tests/unit/utils/param2Obj.spec.js: -------------------------------------------------------------------------------- 1 | import { param2Obj } from '@/utils/index.js' 2 | describe('Utils:param2Obj', () => { 3 | const url = 'https://github.com/PanJiaChen/vue-element-admin?name=bill&age=29&sex=1&field=dGVzdA==&key=%E6%B5%8B%E8%AF%95' 4 | 5 | it('param2Obj test', () => { 6 | expect(param2Obj(url)).toEqual({ 7 | name: 'bill', 8 | age: '29', 9 | sex: '1', 10 | field: window.btoa('test'), 11 | key: '测试' 12 | }) 13 | }) 14 | }) 15 | -------------------------------------------------------------------------------- /frontend/tests/unit/utils/parseTime.spec.js: -------------------------------------------------------------------------------- 1 | import { parseTime } from '@/utils/index.js' 2 | 3 | describe('Utils:parseTime', () => { 4 | const d = new Date('2018-07-13 17:54:01') // "2018-07-13 17:54:01" 5 | it('timestamp', () => { 6 | expect(parseTime(d)).toBe('2018-07-13 17:54:01') 7 | }) 8 | it('timestamp string', () => { 9 | expect(parseTime((d + ''))).toBe('2018-07-13 17:54:01') 10 | }) 11 | it('ten digits timestamp', () => { 12 | expect(parseTime((d / 1000).toFixed(0))).toBe('2018-07-13 17:54:01') 13 | }) 14 | it('new Date', () => { 15 | expect(parseTime(new Date(d))).toBe('2018-07-13 17:54:01') 16 | }) 17 | it('format', () => { 18 | expect(parseTime(d, '{y}-{m}-{d} {h}:{i}')).toBe('2018-07-13 17:54') 19 | expect(parseTime(d, '{y}-{m}-{d}')).toBe('2018-07-13') 20 | expect(parseTime(d, '{y}/{m}/{d} {h}-{i}')).toBe('2018/07/13 17-54') 21 | }) 22 | it('get the day of the week', () => { 23 | expect(parseTime(d, '{a}')).toBe('五') // 星期五 24 | }) 25 | it('get the day of the week', () => { 26 | expect(parseTime(+d + 1000 * 60 * 60 * 24 * 2, '{a}')).toBe('日') // 星期日 27 | }) 28 | it('empty argument', () => { 29 | expect(parseTime()).toBeNull() 30 | }) 31 | 32 | it('null', () => { 33 | expect(parseTime(null)).toBeNull() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /frontend/tests/unit/utils/validate.spec.js: -------------------------------------------------------------------------------- 1 | import { validUsername, isExternal } from '@/utils/validate.js' 2 | 3 | describe('Utils:validate', () => { 4 | it('validUsername', () => { 5 | expect(validUsername('admin')).toBe(true) 6 | expect(validUsername('editor')).toBe(true) 7 | expect(validUsername('xxxx')).toBe(false) 8 | }) 9 | it('isExternal', () => { 10 | expect(isExternal('https://github.com/PanJiaChen/vue-element-admin')).toBe(true) 11 | expect(isExternal('http://github.com/PanJiaChen/vue-element-admin')).toBe(true) 12 | expect(isExternal('github.com/PanJiaChen/vue-element-admin')).toBe(false) 13 | expect(isExternal('/dashboard')).toBe(false) 14 | expect(isExternal('./dashboard')).toBe(false) 15 | expect(isExternal('dashboard')).toBe(false) 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const defaultSettings = require('./src/settings.js') 4 | 5 | function resolve(dir) { 6 | return path.join(__dirname, dir) 7 | } 8 | 9 | const name = defaultSettings.title || 'vue Admin Template' // page title 10 | 11 | // If your port is set to 80, 12 | // use administrator privileges to execute the command line. 13 | // For example, Mac: sudo npm run 14 | // You can change the port by the following methods: 15 | // port = 9528 npm run dev OR npm run dev --port = 9528 16 | const port = process.env.port || process.env.npm_config_port || 9528 // dev port 17 | 18 | // All configuration item explanations can be find in https://cli.vuejs.org/config/ 19 | module.exports = { 20 | /** 21 | * You will need to set publicPath if you plan to deploy your site under a sub path, 22 | * for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/, 23 | * then publicPath should be set to "/bar/". 24 | * In most cases please use '/' !!! 25 | * Detail: https://cli.vuejs.org/config/#publicpath 26 | */ 27 | publicPath: '/', 28 | outputDir: 'dist', 29 | assetsDir: 'static', 30 | lintOnSave: process.env.NODE_ENV === 'development', 31 | productionSourceMap: false, 32 | devServer: { 33 | port: port, 34 | open: true, // 默认打开浏览器 35 | overlay: { 36 | warnings: false, 37 | errors: true 38 | }, 39 | before: require('./mock/mock-server.js') 40 | }, 41 | configureWebpack: { 42 | // provide the app's title in webpack's name field, so that 43 | // it can be accessed in index.html to inject the correct title. 44 | name: name, 45 | resolve: { 46 | alias: { 47 | '@': resolve('src') 48 | } 49 | } 50 | }, 51 | chainWebpack(config) { 52 | // it can improve the speed of the first screen, it is recommended to turn on preload 53 | config.plugin('preload').tap(() => [ 54 | { 55 | rel: 'preload', 56 | // to ignore runtime.js 57 | // https://github.com/vuejs/vue-cli/blob/dev/packages/@vue/cli-service/lib/config/app.js#L171 58 | fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/], 59 | include: 'initial' 60 | } 61 | ]) 62 | 63 | // when there are many pages, it will cause too many meaningless requests 64 | config.plugins.delete('prefetch') 65 | 66 | // set svg-sprite-loader 67 | config.module 68 | .rule('svg') 69 | .exclude.add(resolve('src/icons')) 70 | .end() 71 | config.module 72 | .rule('icons') 73 | .test(/\.svg$/) 74 | .include.add(resolve('src/icons')) 75 | .end() 76 | .use('svg-sprite-loader') 77 | .loader('svg-sprite-loader') 78 | .options({ 79 | symbolId: 'icon-[name]' 80 | }) 81 | .end() 82 | 83 | config 84 | .when(process.env.NODE_ENV !== 'development', 85 | config => { 86 | config 87 | .plugin('ScriptExtHtmlWebpackPlugin') 88 | .after('html') 89 | .use('script-ext-html-webpack-plugin', [{ 90 | // `runtime` must same as runtimeChunk name. default is `runtime` 91 | inline: /runtime\..*\.js$/ 92 | }]) 93 | .end() 94 | config 95 | .optimization.splitChunks({ 96 | chunks: 'all', 97 | cacheGroups: { 98 | libs: { 99 | name: 'chunk-libs', 100 | test: /[\\/]node_modules[\\/]/, 101 | priority: 10, 102 | chunks: 'initial' // only package third parties that are initially dependent 103 | }, 104 | elementUI: { 105 | name: 'chunk-elementUI', // split elementUI into a single package 106 | priority: 20, // the weight needs to be larger than libs and app or it will be packaged into libs or app 107 | test: /[\\/]node_modules[\\/]_?element-ui(.*)/ // in order to adapt to cnpm 108 | }, 109 | commons: { 110 | name: 'chunk-commons', 111 | test: resolve('src/components'), // can customize your rules 112 | minChunks: 3, // minimum common number 113 | priority: 5, 114 | reuseExistingChunk: true 115 | } 116 | } 117 | }) 118 | // https:// webpack.js.org/configuration/optimization/#optimizationruntimechunk 119 | config.optimization.runtimeChunk('single') 120 | } 121 | ) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /pic/pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic1.png -------------------------------------------------------------------------------- /pic/pic10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic10.png -------------------------------------------------------------------------------- /pic/pic2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic2.png -------------------------------------------------------------------------------- /pic/pic3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic3.png -------------------------------------------------------------------------------- /pic/pic4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic4.png -------------------------------------------------------------------------------- /pic/pic5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic5.png -------------------------------------------------------------------------------- /pic/pic6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic6.png -------------------------------------------------------------------------------- /pic/pic7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic7.png -------------------------------------------------------------------------------- /pic/pic8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic8.png -------------------------------------------------------------------------------- /pic/pic9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JIANG-HS/UIE-SentimentAnalysisWeb/0e66287ca6fefa8b911ea8f5af9c070fabeec9ca/pic/pic9.png --------------------------------------------------------------------------------