config/
dev.env.js
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
API_ROOT: '"http://api.youkangapp.cn:9998"'
})
prod.env.js
module.exports = {
NODE_ENV: '"production"',
API_ROOT: '"http://api.youkangapp.cn"'
}
.gitignore
.DS_Store
node_modules/
dist/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
src/ asset components fetch page router utils vuex
components/common/dialog-patient
<template>
<el-dialog title="患者信息编辑" class="patient" :visible.sync="dialogVisible">
<el-row class="patient-edit" :gutter="20">
<el-col :span="12">
<el-input disabled placeholder="请输入内容" v-model="patient.pid">
<template slot="prepend">本院患者编号</template>
</el-input>
</el-col>
<el-col :span="12">
<el-input placeholder="请输入姓名" v-model="patient.name" :disabled="false">
<template slot="prepend">姓 名</template>
</el-input>
</el-col>
<el-col :span="12">
<div class="span_select">
<span class="labelx">性 别</span>
<el-select class="select" v-model="patient.gender" @change="genderChanged">
<el-option v-for="item in genderList" :key="item.label" :label="item.label" :value="item.value"></el-option>
</el-select>
</div>
</el-col>
<el-col :span="12">
<el-input placeholder="请输入身份证" v-model="patient.id_number">
<template slot="prepend">身份证</template>
</el-input>
</el-col>
<el-col :span="12">
<el-input placeholder="请输入手机" v-model="patient.phone">
<template slot="prepend">手 机</template>
</el-input>
</el-col>
<el-col :span="12">
<el-input placeholder="请输入住址" v-model="patient.address">
<template slot="prepend">住 址</template>
</el-input>
</el-col>
</el-row>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitInfo">保 存</el-button>
<el-button @click="dialogVisible = false">取 消</el-button>
</div>
</el-dialog>
</template>
<script>
import { mapGetters } from 'vuex'
import { mapActions } from 'vuex'
import Bus from '../../utils/bus.js'
export default{
name:"dialog-patient",
data(){
return {
dialogVisible:false,
patient:{},
genderList:[{value: 'M',label: '男'}, {value: 'F',label: '女'}, {value: 'O',label: '其它'}],
}
},
computed:{
...mapGetters([
'patientInfo',
]),
},
watch:{
patientInfo(val,old){
this.patient = val.format
},
},
methods:{
...mapActions({
updatePatientInfo:'updatePatientInfo',
getPatientInfo:'getPatientInfo',
}),
genderChanged(item){
this.patient.gender = item
},
//数据确认
submitInfo(){
//姓名、年龄、性别、检查项目、检查方式、详情不能为 空
if(this.utils.needEdit(this.patient.name)){
this.$message.error("姓名未填写");
return;
}
if(this.utils.needEdit(this.patient.gender)){
this.$message.error("性别未填写");
return;
}
// 提交申请信息
var info = {
phone: this.patient.phone,
id_number: this.patient.id_number,
address: this.patient.address,
patient_uuid: this.patient.uuid,
gender: this.patient.gender,
name: this.patient.name,
}
this.updatePatientInfo(info)
this.dialogVisible = false;//数据隐藏
},
editPatient(uuid){
this.getPatientInfo(uuid)
this.dialogVisible = true;
},
},
created() {
this.$bus.on('editPatient', this.editPatient);
},
beforeDestroy() {
this.$bus.off('editPatient', this.editPatient);
},
}
</script>
<style lang="less">
.patient{
.el-dialog{
width:680px;
}
.el-dialog__header{
text-align: center;
}
.el-dialog__body{
padding-top: 10px !important;
padding-bottom: 10px !important;
}
.el-col{
margin-bottom:20px;
}
}
.span_select{
position: relative;
display:inline-block;
width: 100%;
.labelx{
padding: 0 20px;
padding-left: 20px;
height: 40px;
color: #909399;
width: 81px;
line-height: 40px;
box-sizing: border-box;
border: 1px solid #dcdfe6;
border-right-width:0px;
border-radius: 4px;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
float:left;
overflow:hidden;
background: #f5f7fa;
}
.el-select{
width: calc(100% - 81px);
div input{
border-radius: 4px;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
}
</style>
######### fetch/api.js
import Request from './net-axios'
import qiniu from './qiniu'
import utils from '../utils/utils'
import Vue from 'vue'
//私有方法,外部不能调用
function get(url, params) {
return Request.get(url,params)
}
//私有方法,外部不能调用
function post(url, params) {
return Request.post(url,params)
}
//私有方法,外部不能调用
function post(url, params,config) {
return Request.post(url, params,config)
}
//私有方法,外部不能调用
function del(url) {
return Request.del(url)
}
//私有方法,外部不能调用
function put(url, params) {
return Request.put(url, params)
}
export default {
/**
* 用户登录
*/
login(p){
var params = new URLSearchParams();
params.append('username', p.username);
params.append('password', p.password);
return post('/login/',params)
},
}
################# fetch/net-axios
import Vue from 'vue'
import axios from 'axios'
import store from '../vuex/store'
// import qs from 'qs'
// axios 配置
axios.defaults.withCredentials = true
axios.defaults.timeout = 5000
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8';
// axios.defaults.headers.common['X-App-Version'] = '4.0';
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
// axios.defaults.headers.common['X-CSRFToken'] = '110';
axios.defaults.baseURL = process.env.API_ROOT;
//POST传参序列化
axios.interceptors.request.use((config) => {
// if(config.params == undefined){
// config.params = new URLSearchParams()
// }
// if(config.method === 'post'){
// config.data = qs.stringify(config.data);
// }
// console.log('request',config)
return config;
},(error) =>{
console.log("错误的传参", 'fail');
return Promise.reject(error);
});
//返回状态判断
axios.interceptors.response.use((res) =>{
if(res.config.url.indexOf('login') != -1){
// console.log("返回状态",res)
}
if(res.status >=200 && res.status < 300){
return res.data;
}
}, (error) => {
var res = error.response;
if(res.status >=400 && res.status < 500){
var action = res.data.action
var msg = res.data.args.toString()
error = {status:res.status,action:res.data.action,msg:msg}
}
return Promise.reject(error);
});
export default {
get(url,params) {
return new Promise((resolve, reject)=>{
axios.get(url, {params:params})
.then(response => {
resolve(response);
}, err => {
reject(err);
})
.catch((error) => {
reject(error)
})
})
},
post(url,params) {
return new Promise((resolve, reject)=>{
axios.post(url, params)
.then(response => {
resolve(response);
}, err => {
reject(err);
})
.catch((error) => {
reject(error)
})
})
},
post(url,params,config) {
return new Promise((resolve, reject)=>{
axios.post(url, params,config)
.then(response => {
resolve(response);
}, err => {
reject(err);
})
.catch((error) => {
reject(error)
})
})
},
del(url) {
return new Promise((resolve, reject)=>{
axios.delete(url)
.then(response => {
resolve(response);
}, err => {
reject(err);
})
.catch((error) => {
reject(error)
})
})
},
put(url,params) {
return new Promise((resolve, reject)=>{
axios.put(url,params)
.then(response => {
resolve(response);
}, err => {
reject(err);
})
.catch((error) => {
reject(error)
})
})
},
// axios 下载
download(url){
// var testurl = 'http://p6hiqyjcl.bkt.clouddn.com/DSC_0021.mp4'
axios({
method: 'get',
url: url,
timeout: 1000000,
withCredentials: true,
onDownloadProgress (event){
console.log('----',event)
}
// responseType: 'blob'
}).then(data => {
if(!data) return
let url = window.URL.createObjectURL(new Blob([data]))
let link = document.createElement('a')
link.style.display = 'none'
link.href = url
// link.setAttribute('download', 'excel.xlsx')
document.body.appendChild(link)
link.click()
}).catch((error) => {
})
},
}
############ page/index.vue
<template>
<div style="padding-bottom: 80px;">
<!-- 首页头部 -->
<v-index-header></v-index-header>
<div class="center">
<!-- 申请列表 -->
<v-index-table :requestList="requestList.list"></v-index-table>
<!-- 分页器 -->
<el-pagination background @size-change="handleSizeChange" class="pagination" @current-change="handleCurrentChange" :current-page="pagination.page" :page-sizes="[10, 15, 20, 25]" :page-size="requestPageLimit" layout="total, sizes, prev, pager, next" :total="pagination.count">
</el-pagination>
</div>
<!-- 申请阅片 -->
<v-dialog-request></v-dialog-request>
<!-- 编辑患者信息 -->
<v-dialog-patient></v-dialog-patient>
<router-view></router-view>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { mapActions } from 'vuex'
export default {
name: "index",
data() {
return {
pagination: {
page: 1,
limit: 10,
count: 0
}
}
},
computed: {
...mapGetters([
'requestList',
'requestPageLimit',
'loginStatus',
'userInfo',
]),
},
filters: {
reportUrl(url) {
var urlName = url.split("/")
return urlName[urlName.length - 1]
}
},
watch: {
requestList(val, old) {
this.pagination.count = this.requestList.count
},
},
methods: {
...mapActions({
getRequestList: 'getRequestList',
saveRequestPageLimit: 'saveRequestPageLimit',
}),
// 当前页的显示数变化
handleSizeChange(val) {
this.pagination.limit = val
this.pagination.page = 1
this.saveRequestPageLimit(val)
this.getRequestList(this.pagination)
},
// 当前页码发生变化
handleCurrentChange(val) {
this.pagination.page = val
this.getRequestList(this.pagination)
},
},
mounted() {
this.pagination.limit = this.requestPageLimit
this.getRequestList(this.pagination);
var test = {"action": "prompt", "args": ["Username or password error"]}
console.log(test.args.toString())
}
}
</script>
<style lang="less">
.center {
width: 960px;
}
.el-pagination {
margin-top: 30px;
text-align: center;
}
</style>
#################### router/index.js
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [
{
path:"/",
redirect:"/login"
},
{//登录
path: '/login',
name: 'login',
component: resolve => require(['../page/login'], resolve)
},
{//首页
path: '/index',
name: 'index',
component: resolve => require(['../page/index'], resolve)
},
{//医生
path: '/expert',
name: 'expert',
component: resolve => require(['../page/expert'], resolve)
},
{//医生
path: '/expert/detail/:uuid',
name: 'expert-detail',
component: resolve => require(['../page/expert-detail'], resolve)
},
{//报告
path: '/expert/detail/:uuid/diagnosis',
name: 'diagnosis',
component: resolve => require(['../page/diagnosis'], resolve)
},
]
})
########## utils/global.js
/**
* Created by qinzh on 2018/5/18.
*/
import Vue from 'vue'
function capitalizeFirstLetter(string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
const requireComponent = require.context(
'../components', true, /\.vue$/
//找到components文件夹下以.vue命名的文件
)
requireComponent.keys().forEach(fileName => {
const componentConfig = requireComponent(fileName);
const componentName = capitalizeFirstLetter(
fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')
//因为得到的filename格式是: './baseButton.vue', 所以这里我们去掉头和尾,只保留真正的文件名
)
var arr = componentName.split("/");
const cname = "v-" + arr[arr.length-1];//组件名称
Vue.component(cname, componentConfig.default || componentConfig)
})
################utils/utils.js
import * as keys from '../vuex/keys'
import axios from '../fetch/net-axios'
export default {
// 判断浏览器
browser(){
return browser()
},
// 格式化日期
parseTime(isoDate,format){
return isEmpty(isoDate) ? '未知' : formatDate(new Date(isoDate),format)
},
// 格式化日期
formatDate(date, format) {
return formatDate(date, format)
},
// 检查电话
testPhone(phone){
var PHONE_PATTERN = /^1[34578]\d{9}$/
return PHONE_PATTERN.test(phone)
},
// 获取对象的某条属性 不存在返回空字符串
getField(obj,key){
return key in obj ? obj[key] : ''
},
isField(obj,key){
return key in obj && !isEmpty(obj[key])
},
// 数据判空
isEmpty(value){
return isEmpty(value)
},
needEdit(value){
return isEmpty(value) || value == '未知'
},
deepCopy(obj){
return deepCopy(obj)
},
// 是否在数组中
inArray(item,arr){
return indexOf(arr,item) >= 0
},
// 兼容ie8之前的版本
indexOf(arr,item){
return indexOf(arr,item)
},
// 处理空字符串
formatStr(str){
if(isEmpty(str)){
return '未知'
}
return str.replace(/[\^]/ig,'')
},
// 处理患者性别
formatGender(gender){
if(isEmpty(gender)){
return '未知'
}
//兼容错误数据
gender = (gender == 'F' || gender == 'M' || gender == 'O' || gender == '男' || gender == '女' || gender == '其它') ? gender : '未知'
gender = gender.replace('F','女').replace('M','男').replace('O','其它')
return gender
},
// 处理患者年龄
formatAge(age){
if(isEmpty(age)){
return '未知'
}
var number = parseInt(age.replace(/[^0-9]/ig,""))
var unit = age.replace(/[^a-z]/ig,"").toUpperCase().replace('Y','岁').replace('M','月').replace('W','周').replace('D','天')
unit = isEmpty(unit) ? '岁' : unit
return {number:number,unit:unit,age:number + unit}
},
formatStatus(request){
var en = request.status
if(en == 'waiting' && request.lock && request.lock.time_left > 0){
en = 'composing'
}
// 申请状态
var status = {
waiting:'待查看',
composing:'报告中',
recomposing:'待修改',
checking:'待审核',
discusing:'疑难讨论',
confirming:'待确认',
downloadable:'待确认',
downloaded:'已下载',
rejected:'已拒绝',
refused:'已退回',
finished:'已完成',
}
return isEmpty(status[en]) ? en : status[en]
},
formatRequestStatus(en){
if(en == null) en = 'request'
var status = {
request:'申请阅片',
waiting:'等待阅片',
uploading:'上传中',
downloadable:'下载报告'
}
return isEmpty(status[en]) ? '异常' : status[en]
},
isEditStatus(request){
var status = request.status
// 别人锁定中
if(status == 'waiting' && request.lock && request.lock.time_left > 0 && !request.lock.is_holding){
return false
}
var editable = ['waiting','recomposing','discusing']
return indexOf(editable,status) >= 0
},
//将00000--转化为--;
chnageParseInt(number){
return parseInt(number);
},
//下载
downloadReport(url){
if(!url){
alert("地址为空")
return false;
}
//ie下载
if (browser()==="IE" || browser()==="Edge"){
window.open(url,"","width=1, height=1, top=5000, left=5000");
}else{
//非ie
var reportName = url.split("/")
reportName = reportName[reportName.length-1]
fetch(url).then(res => res.blob().then(blob => {
var a = document.createElement('a');
var url = window.URL.createObjectURL(blob);
var filename = reportName;
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.style.display='none';
a.click();
window.URL.revokeObjectURL(url);
}))
}
},
}
function deepCopy(obj){
if(typeof obj != 'object') return obj
if(obj === null) return null
var newobj = typeof obj === Array ? [] : {}
for ( var attr in obj) {
if(obj[attr] === newobj) continue
newobj[attr] = deepCopy(obj[attr])
}
return newobj
}
//浏览器判断
function browser(){
var agent = navigator.userAgent;
if(agent.indexOf("Opera") >= 0) return "Opera"
if(agent.indexOf("Firefox") >= 0) return "Firefox"
if(agent.indexOf("Chrome") >= 0) return "Chrome"
if(agent.indexOf("Safari") >= 0) return "Safari"
if(agent.indexOf("Trident") >= 0) return "Edge"
if(agent.indexOf("compatible") >= 0 && agent.indexOf("MSIE") >= 0 && agent.indexOf("Opera") < 0) return "Opera"
}
// 格式化日期
function formatDate(date, format) {
return format.replace(/yyyy|MM|dd|HH|mm|ss/g, function(a){
switch(a){
case 'yyyy':
return pad(date.getFullYear());
break;
case 'MM':
return pad(date.getMonth() + 1);
break;
case 'mm':
return pad(date.getMinutes());
break;
case 'dd':
return pad(date.getDate());
break;
case 'HH':
return pad(date.getHours());
break;
case 'ss':
return pad(date.getSeconds());
break;
}
})
}
// 日期补全0
function pad(i){
return (i < 10 ? '0' : '') + i
};
// 数据判空
function isEmpty(value){
switch (typeof value) {
case 'undefined':
return true;
case 'string':
if (value.replace(/(^[ \t\n\r]*)|([ \t\n\r]*$)/g, '').length == 0) return true;
break;
case 'boolean':
if (!value) return true;
break;
case 'number':
if (0 === value || isNaN(value)) return true;
break;
case 'object':
if (null === value || value.length === 0) return true;
for (var i in value) {
return false;
}
return true;
break;
}
return false;
}
// 兼容ie8之前的版本
function indexOf(arr,item){
if(Array.prototype.indexOf){
return arr.indexOf(item);
}else{
for( var i=0;i<arr.length;i++){
if(arr[i]===item)
return i;
else return -1;
}
}
}
##############vuex/modules/user.js
import * as keys from '../keys'
import api from '../../fetch/api'
import qiniu from '../../fetch/qiniu'
import utils from '../../utils/utils'
const state = {
//登录状态
loginStatus: JSON.parse(localStorage.getItem(keys.LOGIN_STATUS)) || false,
//登录提示状态
loginTipStatus: JSON.parse(localStorage.getItem(keys.LOGIN_TIP_STATUS)) || false,
//用户信息
userInfo : JSON.parse(localStorage.getItem(keys.USER_INFO)) || {},
//异常信息
errorInfo : JSON.parse(localStorage.getItem(keys.ERROR_INFO)) || {},
}
const actions = {
/**
* 登录
*/
login({ commit }, params){
api.login(params)
.then(res => {
keys.save({commit}, keys.LOGIN_STATUS, true)
keys.save({commit}, keys.USER_INFO, res)
})
.catch(err =>{
keys.save({commit}, keys.ERROR_INFO, err)
})
},
logout({ commit }){
api.logout()
keys.save({commit}, keys.LOGIN_STATUS, false)
},
}
const getters = {
loginStatus:state => state.loginStatus,
loginTipStatus:state => state.loginTipStatus,
userInfo : state => state.userInfo,
errorInfo: state => state.errorInfo,
}
const mutations = {
[keys.LOGIN_STATUS](state, res) {
state.loginStatus = res
},
[keys.LOGIN_TIP_STATUS](state, res) {
state.loginStatus = res
},
[keys.USER_INFO](state, res) {
state.userInfo = res
},
[keys.ERROR_INFO](state, res) {
state.errorInfo = res
},
}
export default {
state,
actions,
getters,
mutations
}
################vuex/keys.js
//USER模块
//登录状态
export const LOGIN_STATUS = 'LOGIN_STATUS'
export const LOGIN_TIP_STATUS = 'LOGIN_TIP_STATUS'
export const IS_LOGIN = 'IS_LOGIN'
//用户信息
export const USER_INFO = 'USER_INFO'
//提示信息
export const SUCCESS_INFO = 'SUCCESS_INFO'
//异常信息
export const ERROR_INFO = 'ERROR_INFO'
//检查项目-检查部位 公共信息
export const CHECK_PROJECT_POSITION = 'CHECK_PROJECT_POSITION'
// 患者信息
export const PATIENT_INFO = 'PATIENT_INFO'
// 患者端
// 申请列表
export const REQUEST_LIST = 'REQUEST_LIST'
// 单页数
export const REQUEST_PAGE_LIMIT = 'REQUEST_PAGE_LIMIT'
// 申请详情
export const REQUEST_DETAIL = 'REQUEST_DETAIL'
// 专家端
// 阅片列表
export const DIAGNOSIS_LIST = 'DIAGNOSIS_LIST'
// 阅片列表单页数
export const DIAGNOSIS_PAGE_LIMIT = 'DIAGNOSIS_PAGE_LIMIT'
// 阅片详情
export const DIAGNOSIS_DETAIL = 'DIAGNOSIS_DETAIL'
export function save({ commit }, key, value){
if(key == undefined || value == undefined){
console.log("params of function named save is undefined!")
return
}
if(key == ERROR_INFO){
if(value.action == "login"){
localStorage.setItem(LOGIN_STATUS, false)
commit(LOGIN_STATUS, false)
}
}
localStorage.setItem(key, JSON.stringify(value))
commit(key, value)
}
export function clear({ commit }, key, value){
if(!key){
console.log("params of function named clear is undefined!")
return
}
localStorage.removeItem(key)
commit(key, '')
}
#############vuex/store.js
import Vue from 'vue'
import Vuex from 'vuex'
import user from './modules/user'
import commonData from './modules/commonData'
import index from './modules/index'
import expert from './modules/expert'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user,
commonData,
index,
expert,
}
})
main.js
import Vue from 'vue'
// import Cube from 'cube-ui' // 一般直接放在这个位置
import App from './App'
import router from './router'
import "../static/css/reset.css"
// import $ from 'jquery'
//全局一劳永逸的住注册组件
import './utils/global.js'
// 路由拦截
import './utils/permission'
require('pdfjs-dist')
// 导出页面为PDF格式
import htmlToPdf from './utils/htmlToPdf'
Vue.use(htmlToPdf)
import Vuex from 'vuex'
Vue.use(Vuex)
import Bus from 'vue-bus'
Vue.use(Bus)
import VueCookie from 'vue-cookie'
Vue.use(VueCookie)
import utils from './utils/utils.js'
Vue.prototype.utils = utils
Vue.prototype.$vm = new Vue();
import store from './vuex/store'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
//动画组件库
import animate from 'animate.css'
Vue.use(animate)
Vue.use(ElementUI);
Vue.config.productionTip = false;
new Vue({
el: '#app',
router,
store,
template: '<App/>',
components: { App }
})
APP.vue
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default{
name:"App",
computed:{
...mapGetters(['errorInfo','loginStatus','userInfo']),
},
watch:{
errorInfo(val,old){
// console.log('watch errorInfo',val)
if(val.action == "login"){
this.showTip()
}else if(val.action == 'prompt'){
this.$notify({
title: '提示',
message: val.msg,
duration: 0
});
}
},
userInfo(val,old){
// console.log('watch userInfo',val)
if(this.loginStatus){
this.$router.push("index")
}else{
this.$router.push("login")
}
}
},
methods:{
showTip(){
this.$alert('您的安全认证已经失效,请重新登录系统。', '请重新登录', {
confirmButtonText: '登录',
callback: action => {
if(action == 'confirm'){
this.$router.push("login")
}
}
})
},
}
}
</script>
<style lang="less">
</style>