Commit e5c5c4f3 authored by 郭铭瑶's avatar 郭铭瑶 🤘

完善

parent ba2cfac5
......@@ -1628,6 +1628,14 @@
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"dependencies": {
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npm.taobao.org/qs/download/qs-6.7.0.tgz",
"integrity": "sha1-QdwaAV49WB8WIXdr4xr7KHapsbw=",
"dev": true
}
}
},
"bonjour": {
......@@ -4369,6 +4377,14 @@
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npm.taobao.org/qs/download/qs-6.7.0.tgz",
"integrity": "sha1-QdwaAV49WB8WIXdr4xr7KHapsbw=",
"dev": true
}
}
},
"ext": {
......@@ -6326,6 +6342,11 @@
"integrity": "sha1-Hvo57yxfeYC7F4St5KivLeMpESE=",
"dev": true
},
"js-cookie": {
"version": "2.2.1",
"resolved": "https://registry.npm.taobao.org/js-cookie/download/js-cookie-2.2.1.tgz",
"integrity": "sha1-aeEG3F1YBolFYpAqpbrsN0Tpsrg="
},
"js-tokens": {
"version": "3.0.2",
"resolved": "https://registry.npm.taobao.org/js-tokens/download/js-tokens-3.0.2.tgz",
......@@ -10201,10 +10222,9 @@
"dev": true
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npm.taobao.org/qs/download/qs-6.7.0.tgz",
"integrity": "sha1-QdwaAV49WB8WIXdr4xr7KHapsbw=",
"dev": true
"version": "6.9.0",
"resolved": "https://registry.npm.taobao.org/qs/download/qs-6.9.0.tgz",
"integrity": "sha1-0Sl+KgScUxGctJzKNmrbusyAtAk="
},
"query-string": {
"version": "4.3.4",
......@@ -12251,6 +12271,11 @@
"integrity": "sha1-HuO8mhbsv1EYvjNLsV+cRvgvWCU=",
"dev": true
},
"vuex": {
"version": "3.1.1",
"resolved": "https://registry.npm.taobao.org/vuex/download/vuex-3.1.1.tgz",
"integrity": "sha1-DCZL/jDNvM+Wq52zF30hGCilkQ4="
},
"warning": {
"version": "3.0.0",
"resolved": "https://registry.npm.taobao.org/warning/download/warning-3.0.0.tgz?cache=0&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fwarning%2Fdownload%2Fwarning-3.0.0.tgz",
......
......@@ -15,9 +15,12 @@
"axios": "^0.19.0",
"babel-polyfill": "^6.26.0",
"dhtmlx-gantt": "^6.2.7",
"js-cookie": "^2.2.1",
"moment": "^2.24.0",
"qs": "^6.9.0",
"vue": "^2.5.2",
"vue-router": "^3.0.1"
"vue-router": "^3.0.1",
"vuex": "^3.1.1"
},
"devDependencies": {
"autoprefixer": "^7.1.2",
......
<template>
<a-form :form="form">
<slot name="title"/>
<a-row v-for="(row, rowIndex) in layout" :key="rowIndex">
<a-col v-for="(item, key) in row" :key="key" :span="item.width">
<ActiveFormItem
:entry="key"
:model="model"
:item="item"
:labelWidth="labelWidth"
/>
</a-col>
</a-row>
<slot />
</a-form>
</template>
<script>
import moment from 'moment'
import ActiveFormItem from './ActiveFormItem'
export default {
name: 'ActiveForm',
components: {
ActiveFormItem
},
props: {
layout: {
type: Array,
required: true
},
model: {
type: Object,
default () {
return {}
}
},
labelWidth: {
type: Number,
default () {
return 5
}
}
},
created () {
this.filterDateItem()
},
data () {
return {
form: this.$form.createForm(this, {
// 当表单值变化时同时赋给model
onValuesChange: (e, val) => {
this.model = Object.assign(this.model, this.operateDateItem(val, true))
}
}),
dateItems: {}
}
},
mounted () {
const model = this.filterDataModel(this.model)
// 根据model初始化表单值
this.form.setFieldsValue(this.operateDateItem(model, false))
},
methods: {
// 表单验证,上级组件可以通过this.$refs来调用此函数
validate (callback) {
this.form.validateFields((err, values) => {
callback(err, values)
})
},
// 筛选layout中的日期组件
filterDateItem () {
const result = {}
this.layout.forEach(item => {
for (const key in item) {
if (item[key].type && (item[key].type == 'date' || item[key].type == 'daterange')) {
result[key] = item[key]
}
}
})
this.dateItems = result
},
// 由于antd的日期组件都是moment格式,这里进行了转化成字符串
operateDateItem (obj, isMomentToString) {
const model = { ...obj }
for (const key in model) {
const dateItem = this.dateItems[key]
if (dateItem) {
if (dateItem.type == 'date') {
if (isMomentToString) {
if (!moment.isMoment(model[key])) return model
model[key] = moment(model[key]).format(dateItem.format || 'YYYY-MM-DD')
} else {
model[key] = moment(model[key], dateItem.format || 'YYYY-MM-DD')
}
} else if (dateItem.type == 'daterange') {
if (isMomentToString) {
if (!moment.isMoment(model[key][0]) || !moment.isMoment(model[key][1])) return model
model[key] = [moment(model[key][0]).format(dateItem.format || 'YYYY-MM-DD'), moment(model[key][1]).format(dateItem.format || 'YYYY-MM-DD')]
} else {
model[key] = [moment(model[key][0], dateItem.format || 'YYYY-MM-DD'), moment(model[key][1], dateItem.format || 'YYYY-MM-DD')]
}
}
}
}
return model
},
// 过滤掉layout中不存在的字段,防止You cannot set a form field before rendering a field associated with the value.的错误
filterDataModel (model) {
const keys = Object.keys(model)
if (keys.length <= 0) return {}
const list = []
if (this.layout.length > 0) {
this.layout.forEach(item => {
list.push(...Object.keys(item))
})
}
const result = {}
keys.forEach(key => {
if (list.indexOf(key) >= 0) {
result[key] = model[key]
}
})
return result
}
},
watch: {
model (cur, past) {
// 通过监听model来判断reset表单或给表单重新设置值
const keys = Object.keys(cur)
const result = this.filterDataModel(cur)
if (keys.length <= 0) {
this.form.resetFields()
} else {
this.form.resetFields()
this.form.setFieldsValue(this.operateDateItem(result, false))
}
}
}
}
</script>
<template>
<a-form-item :label-col="labelCol" :wrapper-col="wrapperCol" :label="item.label">
<a-input
v-if="item.type == 'input'"
v-decorator="validate"
:placeholder="placeholder"
:disabled="item.disabled" />
<a-input-password
v-if="item.type == 'password'"
v-decorator="validate"
:placeholder="placeholder"
:disabled="item.disabled" />
<a-textarea
v-if="item.type == 'textarea'"
v-decorator="validate"
:placeholder="placeholder"
:disabled="item.disabled" />
<a-checkbox-group
v-if="item.type == 'checkbox'"
v-decorator="validate"
:options="item.options"
:disabled="item.disabled" />
<a-radio-group
v-if="item.type == 'radio'"
v-decorator="validate"
:options="item.options"
:disabled="item.disabled" />
<a-select
v-if="item.type == 'select'"
v-decorator="validate"
allowClear
:placeholder="placeholder"
:disabled="item.disabled">
<a-select-option
v-for="option in item.options"
:key="option.value"
:value="option.value">
{{option.label}}
</a-select-option>
</a-select>
<a-cascader
v-if="item.type == 'cascader'"
v-decorator="validate"
allowClear
:options="item.options"
:placeholder="placeholder"
:disabled="item.disabled" />
<a-date-picker
v-if="item.type == 'date'"
style="width: 100%"
v-decorator="validate"
:format="item.format"
allowClear
:placeholder="placeholder"
:disabled="item.disabled" />
<a-range-picker
v-if="item.type == 'daterange'"
v-decorator="validate"
:format="item.format"
allowClear
:placeholder="placeholder"
:disabled="item.disabled" />
<a-upload
v-if="item.type == 'upload'"
multiple
v-decorator="validate"
:customRequest="handleRequest"
@change="handleChange"
:remove="item.remove"
accept='.jpg,.jpeg,.png,.gif,.doc,.docx,.xlsx,.xls,.xlsm,.txt,.pdf'
:beforeUpload='item.beforeUpload'>
<a-button>
<a-icon type="upload" /> {{item.txt || '上传'}}
</a-button>
</a-upload>
<a-button class="ActiveForm-view-btn" v-if="item.type == 'view'" v-decorator="validate" @click="handleClick">
<a v-if="model[entry] && model[entry].fileType == 'file'" :href='model[entry].url' target="_blank">{{model[entry] && model[entry].name}}</a>
<span v-else-if="model[entry] && model[entry].fileType == 'pic'">{{model[entry] && model[entry].name}}</span>
<span v-else>{{model[entry] && model[entry].name}}</span>
</a-button>
<span v-if="item.type == 'text'" v-decorator="validate">
{{item.formatter ? item.formatter(model[entry]) : model[entry]}}
</span>
<template v-if="item.render">
<component :is="component" v-decorator="validate"/>
</template>
</a-form-item>
</template>
<script>
import Vue from 'vue'
export default {
name: 'ActiveFormItem',
props: {
entry: {
type: String,
required: true,
},
item: {
type: Object,
required: true,
},
model: {
type: Object,
default() {
return {}
}
},
labelWidth: {
type: Number,
default() {
return 5
}
},
},
data() {
return {
curData: null,
component: null,
}
},
created() {
if (this.item.render) {
this.component = Vue.component(this.entry, {
render: this.item.render,
props: this.item.props,
})
}
},
methods: {
handleRequest(obj) {
// 这里的obj是用来onProgress、onSuccess、onError的
this.$nextTick(() => {
this.item.customRequest(this.curData.file, obj)
})
},
handleChange(data, list) {
// 这里的data是用来响应改变model数据的
this.curData = data
},
// 点击查看文件按钮的回调
handleClick() {
this.item.onClick(this.model[this.entry].url)
},
// 上传文件的过滤,防止类型错误的报错
normFile(e) {
if (Array.isArray(e)) return e
return e && e.fileList
},
},
computed: {
// 默认表单验证
validate() {
if (this.item.type == 'checkbox') {
// 如果是CheckBox的话初始化要是个数组
return [this.entry, Object.assign(this.item.validate || {}, {initialValue: []})]
}
if (this.item.type == 'upload') {
return [this.entry, Object.assign(this.item.validate || {}, {valuePropName: 'fileList' , getValueFromEvent: this.normFile})]
}
return [this.entry, this.item.validate || {}]
},
// 默认placeholder
placeholder() {
const item = this.item
if (item.placeholder) {
return item.placeholder
}
if (item.type == 'input' || item.type == 'textarea') {
return '请输入'
}
if (item.type == 'daterange') {
return ['开始日期', '结束日期']
}
return '请选择'
},
labelCol() {
return {
style: {
width: `${this.labelWidth}px`
},
}
},
wrapperCol() {
return {
style: {
display: 'inline-block',
width: `calc(90% - ${this.labelWidth}px)`
}
}
}
}
}
</script>
<style>
.ActiveForm-view-btn {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ant-row.ant-form-item {
display: flex;
}
</style>
/** ActiveForm 使用示例 */
// 首先在入口文件引入并use组件
// import ActiveForm from '@/components/ActiveForm'
// Vue.use(ActiveForm)
<template>
<a-card>
<ActiveForm :layout="layout" :label-width="7" ref="exampleForm" :model="model">
<div slot="title">
<h2>ActiveForm Title(可选, 样式自己定)</h2>
</div>
<div style="text-align:right">
<a-button @click="handleSearch" type="primary">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</div>
</ActiveForm>
</a-card>
</template>
<script>
export default {
name: 'ExampleComponent',
data() {
return {
layout: [
{
example1: {
label: '输入',
type: 'input',
width: 8,
validate: {
rules: [{required: true, message: '请输入'}]
}
},
example2: {
label: '单选',
type: 'radio',
width: 8,
options: [
{label: '男', value: '1'},
{label: '女', value: '0'},
]
},
example3: {
label: '选择',
type: 'select',
width: 8,
options: [
{label: '苹果', value: '苹果'},
{label: '香蕉', value: '香蕉'},
],
disabled: true
},
example4: {
label: '日期',
type: 'date',
width: 8,
validate: {
rules: [{required: true, message: '请选择日期'}]
}
},
example5: {
label: '日期区间',
type: 'daterange',
width: 8,
validate: {
rules: [{required: true, message: '请选择'}]
}
},
example6: {
label: '级联',
type: 'cascader',
width: 8,
options: [{
value: 'zhejiang',
label: 'Zhejiang',
children: [{
value: 'hangzhou',
label: 'Hangzhou',
children: [{
value: 'xihu',
label: 'West Lake',
}],
}],
}, {
value: 'jiangsu',
label: 'Jiangsu',
children: [{
value: 'nanjing',
label: 'Nanjing',
children: [{
value: 'zhonghuamen',
label: 'Zhong Hua Men',
}],
}],
}]
},
example7: {
label: '多选框',
type: 'checkbox',
width: 8,
options: [
{label: '龙', value: '龙'},
{label: '蛇', value: '蛇'},
],
validate: {
rules: [{required: true, message: '请选择'}]
}
},
example8: {
label: '查看文件',
type: 'view',
width: 8,
onClick: this.handleClick, // 点击后触发的回调,返回对应的url
},
},
{
example9: {
label: '上传',
type: 'upload',
txt: '上传文件', // 上传按钮的名字,不传的话默认是‘上传’
width: 8,
remove: this.remove, // remove动作会自动完成,没有其他特殊要求的操作可以不用调用了
customRequest: this.customRequest, // 上传请求
beforeUpload: this.beforeUpload, // 上传前的校验,没有其他特殊要求的操作可以不用调用了
validate: {
rules: [{required: true, message: '请上传'}]
}
},
example10: {
label: '文字',
type: 'text',
width: 8,
formatter: (val) => val += '(进行格式化处理)'
},
example11: {
label: 'test',
width: 8,
render:(h) => {
// render自定义组件,会比较麻烦
return h('div', [
h('p', '测试render2个'),
h('a-select', {
props: {
placeholder: 'placeholder',
allowClear: true,
options: [{value: 'test', label: '测试render select'}],
},
on: {
select(val) {
console.log('select-change', val)
}
},
style: 'color: red',
})
])
},
}
}
],
model: {
example4: '2019/08/23',
example5: ['2019/08/23', '2019/08/24'],
example7: ['龙'],
example8: {
name: 'accounts.xlsx',
fileType: 'file', // file为新开页下载,pic根据你的event回调来定
url: 'http://iftp.omniview.pro/temp/1567069629359-634052a98b43d554976a440bf07a35af.xlsx',
},
example9: [
{
name: 'accounts.xlsx',
objId: null,
response: null,
status: null,
type: null,
uid: 'f58ba55c2ce847a0bc7ea0ca74fadcb9',
url: 'http://iftp.omniview.pro/temp/1567069629359-634052a98b43d554976a440bf07a35af.xlsx',
}
],
example10: '纯展示数据用',
},
}
},
methods: {
handleSearch() {
console.log('example-model', this.model)
this.$refs.exampleForm.validate(err => {
if (err) return
// TODO 这里做接下来的其他操作
this.model.example10 = JSON.stringify(this.model)
})
},
handleReset() {
this.model = {
example8: {
name: 'accounts.xlsx',
fileType: 'file',
url: 'http://iftp.omniview.pro/temp/1567069629359-634052a98b43d554976a440bf07a35af.xlsx',
},
}
},
// 上传的请求
customRequest(data, fn) { // data是数据,可修改data来同步更新model;fn用来调用onProgress、onSuccess、onError这些函数
setTimeout(() => { // 模拟请求完成后的操作
// 这里可以修改data的一些所需的字段值,比如插入url什么的
data.url = 'http://www.baidu.com'
// !!有一点不同的是没必要再手动插入model中了,model会自动更新
fn.onSuccess()
}, 500)
},
// remove动作会自动完成,如果没有其他特殊要求的操作可以去掉了
remove(data) {
console.log('remove', data)
},
// 上传前的校验,没有其他特殊要求的操作可以去掉了
beforeUpload(data) {
console.log('before', data)
},
handleClick(data) {
console.log('click', data)
},
}
}
</script>
import ActiveFormComponent from './ActiveForm'
export default (Vue) => {
Vue.component(ActiveFormComponent.name, ActiveFormComponent)
}
......@@ -2,15 +2,22 @@
<div class="container">
<div id="ganttChart" ref="gantt"/>
<div @mousedown="mouseDownHandler" class="move-bar" :style="`left: ${left - 2}px`"/>
<GanttModal v-model="showModal" @save="handleSave" @cancel="handleCancel" @delete="handleDelete" :task="curTask"/>
</div>
</template>
<script>
import moment from 'moment'
import 'dhtmlx-gantt'
import 'dhtmlx-gantt/codebase/ext/dhtmlxgantt_tooltip'
import GanttModal from './gantt-modal'
import './locale_cn'
import {config, template} from './gantt-config'
export default {
name: 'GanttChart',
components: {
GanttModal,
},
props: {
tasks: {
type: Object,
......@@ -24,57 +31,74 @@ export default {
},
data() {
return {
left: 450,
columns: [
{name:'text', label:'任务名', tree:true, align: 'left'},
{name:'name', label:'人员', align: 'center', max_width: 200},
{name:'start_date', label:'开始时间', align: 'center', max_width: 100},
{name:'end_date', label:'结束时间', align: 'center', max_width: 100},
{name:'duration', label:'周期', align: 'center', max_width: 50},
{name:'add', label:'', max_width: 50},
]
showModal: false,
left: config.grid_width,
curTask: {},
columns: config.columns,
}
},
mounted() {
gantt.config.date_format = '%Y-%m-%d'
gantt.config.work_time = true
gantt.config.columns = this.columns
gantt.config.grid_width = this.left
gantt.config.row_height = 24
gantt.config.date_scale = '%M%d日' // 列日期
gantt.config.drag_links = false
gantt.templates.task_text = (start, end, task) => {
return Math.round(task.progress * 100) + '%'
}
gantt.templates.tooltip_text = (start, end, task) => {
return `
<b>任务名:${task.text}</b><br/>
<b>姓名:${task.name}</b><br/>
<b>开始时间:${gantt.templates.tooltip_date_format(task.start_date)}</b><br/>
<b>结束时间:${gantt.templates.tooltip_date_format(task.end_date)}</b><br/>
<b>周期:${task.duration}</b><br/>
<b>当前进度:${Math.round(100 * task.progress)}%</b><br/>
`
}
gantt.templates.task_cell_class = (task, date) => {
if (!gantt.isWorkTime({task, date})) {
return 'weekend'
}
}
this.setHolidays()
gantt.init(this.$refs.gantt)
gantt.parse(this.tasks)
this.initGantt()
},
methods: {
init(config = {}){
if (gantt.$contianer) {
initGantt() {
for (let key in config) {
gantt.config[key] = config[key]
}
for (let key in template) {
gantt.templates[key] = template[key]
}
gantt.showLightbox = (id) => {
const task = gantt.getTask(id)
this.curTask = task
this.showModal = true
}
gantt.attachEvent('onAfterTaskDrag', (id, mode, e) => {
const task = gantt.getTask(id)
if (mode == 'progress') {
this.$emit('progressChange', task)
}
})
this.setHolidays()
gantt.init(this.$refs.gantt)
gantt.parse(this.tasks)
},
reset(config = {}){
if (gantt.$container) {
gantt.clearAll()
}
gantt.config.columns = config.columns || this.columns
gantt.init(this.gantt)
gantt.init(this.$refs.gantt)
gantt.parse(this.tasks)
gantt.render()
},
handleSave({id, text, name, date}) {
let task = gantt.getTask(id)
task = Object.assign(task, {
name,
text,
start_date: new Date(moment(date[0])),
end_date: new Date(moment(date[1])),
})
if(task.$new) {
delete task.$new
gantt.addTask(task, task.parent)
} else {
gantt.updateTask(id)
}
gantt.render()
},
handleDelete({id}) {
const task = gantt.getTask(id)
gantt.deleteTask(id)
this.showModal = false
},
handleCancel({id}) {
const task = gantt.getTask(id)
if (task.$new) {
gantt.deleteTask(id)
}
},
mouseDownHandler(){
document.body.addEventListener('mousemove',this.mouseMoveHandler)
document.body.addEventListener('mouseup',this.mouseUpHandler)
......@@ -85,10 +109,10 @@ export default {
},
mouseMoveHandler(e){
let left = e.clientX
if (left <= 300) {
left = 300
} else if (left >= 600) {
left = 600
if (left <= 400) {
left = 400
} else if (left >= 800) {
left = 800
}
this.left = left
gantt.config.grid_width = left
......
export const config = {
date_format: '%Y-%m-%d',
work_time: true,
columns: [
{name:'text', label:'任务名', tree:true, align: 'left'},
{name:'name', label:'人员', align: 'center', max_width: 200},
{name:'start_date', label:'开始日期', align: 'center', max_width: 100},
{name:'end_date', label:'结束日期', align: 'center', max_width: 100},
{name:'duration', label:'周期', align: 'center', max_width: 50},
{name:'add', label:'', max_width: 50},
],
grid_width: 600,
row_height: 24,
date_scale: '%M%d日', // 列日期
drag_links: false,
}
export const template = {
task_text: (start, end, task) => {
return Math.round(task.progress * 100) + '%'
},
tooltip_text: (start, end, task) => {
return `
<b>任务名:${task.text}</b><br/>
<b>姓名:${task.name}</b><br/>
<b>开始时间:${gantt.templates.tooltip_date_format(task.start_date)}</b><br/>
<b>结束时间:${gantt.templates.tooltip_date_format(task.end_date)}</b><br/>
<b>周期:${task.duration}</b><br/>
<b>当前进度:${Math.round(100 * task.progress)}%</b><br/>
`
},
timeline_cell_class: (task, date) => {
if (!gantt.isWorkTime({task, date})) {
return 'weekend'
}
},
}
<template>
<a-modal :title="title" :visible="value" :maskClosable="false" @cancel="handleClose" :footer="null">
<ActiveForm v-if="value" ref="form" :layout="layout" :model="model" :label-width="100">
<div style="text-align:right;">
<a-popconfirm title="确定删除此任务吗?" @confirm="handleDelete">
<a-button type="danger">删除</a-button>
</a-popconfirm>
<a-button @click="handleSave" type="primary">保存</a-button>
</div>
</ActiveForm>
</a-modal>
</template>
<script>
export default {
name: 'GanttModal',
props: {
value: {
type: Boolean,
default: false,
},
task: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
layout: [
{
text: {
label: '任务名',
type: 'input',
width: 24,
placeholder: '任务名',
},
name: {
label: '人员',
type: 'input',
width: 24,
placeholder: '人员名称',
},
date: {
label: '起止日期',
type: 'daterange',
width: 24,
validate: {
rules: [{required: true, message: '请选择任务起止日期'}]
}
},
}
],
}
},
methods: {
handleClose() {
this.$emit('input', false)
this.$emit('cancel', this.model)
},
handleDelete() {
this.$emit('delete', this.model)
},
handleSave() {
this.$refs.form.validate(err => {
if (err) return
this.$emit('save', this.model)
this.handleClose()
})
}
},
computed: {
title() {
const {text, name} = this.task
return `${text} ${name}`
},
model() {
const task = {...this.task}
task.date = [task.start_date, task.end_date]
return task
},
}
}
</script>
<template>
<div class="loader" :style="`position:${position}`" v-show="$store.state.showLoading">
<a-spin :tip="msg" :spinning="$store.state.showLoading" size="large"/>
</div>
</template>
<script>
export default {
name: 'Loader',
props: {
msg: {
type: String,
default: '加载中...',
},
position: {
type: String,
default: 'fixed',
}
}
}
</script>
<style scoped>
.loader {
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0,0,0,0.1);
display: flex;
align-items: center;
justify-content: center;
z-index: 999;
}
</style>
<template>
<div class="login-bg">
<div class="login-form">
<Loader msg="登录中..." position="absolute" />
<ActiveForm :layout="layout" :model="model" ref="form" :label-width="90">
<h2 class="login-title" slot="title">登录</h2>
<div class="login-btn">
<a-button @click="handleLogin" type="primary" block>登录</a-button>
</div>
</ActiveForm>
</div>
</div>
</template>
<script>
import Loader from '@/components/loader'
export default {
name: 'Login',
components: {
Loader,
},
data() {
return {
layout: [
{
username: {
label: '用户名',
type: 'input',
width: 24,
validate: {
rules: [{required: true, message: '请输入用户名'}]
}
},
password: {
label: '密码',
type: 'password',
width: 24,
validate: {
rules: [{required: true, message: '请输入密码'}]
}
}
}
],
model: {},
}
},
methods: {
handleLogin() {
this.$refs.form.validate(err => {
if (err) return
const params = {
...this.model,
client_id: 'plan',
client_secret: 'plan',
grant_type: 'password',
}
this.$ajax.post({
url: this.$api.POST_LOGIN,
params,
}).then(res => {
const {access_token, refresh_token} = res
this.$cookie.set('token', access_token)
this.$cookie.set('refresh_token', refresh_token)
this.$message.success('登录成功')
this.$nextTick(() => this.$router.replace({path: '/'}))
})
})
},
},
}
</script>
<style scoped>
.login-bg {
position: fixed;
top: 0;
left: 0;
bottom: 0;
right: 0;
overflow: hidden;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.login-form {
position: relative;
background: #fff;
width: 300px;
padding: 30px 0;
border-radius: 4px;
box-shadow: 0px 0px 8px 0px rgba(0,0,0,0.2);
}
.login-title {
text-align: center;
margin-bottom: 30px;
}
.login-btn {
text-align: center;
width: 90%;
margin: 20px auto 0;
}
</style>
<template>
<div class="main">
<a-tabs class="chart-tab" defaultActiveKey="1" @change="handleSelect">
<Loader />
<a-tabs class="chart-tab" defaultActiveKey="1" @change="getData">
<a-tab-pane tab="基于任务" key="1"/>
<a-tab-pane tab="基于人员" key="2"/>
<a-dropdown class="user" slot="tabBarExtraContent">
<span>
<a-icon type="user" /> <span class="name">{{info.username || '用户名'}}</span>
<a-icon type="down" />
</span>
<a-menu slot="overlay" @click="handleClick">
<a-menu-item key="logout">退出登录</a-menu-item>
</a-menu>
</a-dropdown>
</a-tabs>
<div class="container">
<GanttChart :tasks="tasks"/>
<GanttChart @progressChange="handleProgressChange" ref="gantt" :tasks="tasks"/>
</div>
</div>
</template>
<script>
import Loader from '@/components/loader'
export default {
name: 'Main',
components: {
Loader,
},
data() {
return {
info: {},
tasks: {
data: [
{id: 1, name: 'guo', text: 'Task #1', start_date: '2019-10-01', duration: 3, progress: 0.6},
{id: 2, name: 'guo', text: 'Task #2', start_date: '2019-10-05', duration: 3, progress: 0.4}
]
}
data: [],
},
}
},
mounted() {
this.getInfo()
this.getData()
},
methods: {
handleSelect(key) {
console.log(key)
getInfo() {
this.$ajax.get({
url: this.$api.GET_INFO,
}).then(res => {
this.info = this.$com.confirm(res, 'data.content', {})
})
},
getData(key = '1', id = '329795521284190208') {
if (key == '2') {
this.$ajax.get({
url: this.$api.GET_PLAN_BY_PERSON.replace('{id}', id),
}).then(res => {
const data = this.$com.confirm(res, 'data.content', [])
const result = []
data.forEach(item => {
if (item.task && item.task.length > 0) {
result.push({
name: item.resource || '',
id: item.id,
dataId: item.id,
text: '',
start_date: null,
end_date: null,
})
item.task.forEach(task => {
result.push({
id: task.tid,
dataId: task.id,
name: item.resource || '',
text: task.name || '',
start_date: this.$com.formatDate(task.start),
end_date: this.$com.formatDate(task.finish),
progress: task.speed ? Number(task.speed) : 0,
parent: item.id,
})
})
}
})
this.tasks.data = result
const columns = [
{name:'name', label:'人员', tree: true, align: 'left', max_width: 200},
{name:'text', label:'任务名', align: 'center'},
{name:'start_date', label:'开始日期', align: 'center', max_width: 100},
{name:'end_date', label:'结束日期', align: 'center', max_width: 100},
{name:'duration', label:'周期', align: 'center', max_width: 50},
{name:'add', label:'', max_width: 50},
]
this.$refs.gantt.reset({columns})
})
return
}
this.$ajax.get({
url: this.$api.GET_PLAN.replace('{id}', id)
}).then(res => {
const data = this.$com.confirm(res, 'data.content', [])
this.tasks.data = data.map(item => {
return {
id: item.tid,
dataId: item.id,
name: item.resource || '',
text: item.name || '',
start_date: this.$com.formatDate(item.start),
end_date: this.$com.formatDate(item.finish),
progress: item.speed ? Number(item.speed) : 0,
parent: item.fid == '0' ? null : item.fid
}
})
this.$refs.gantt.reset()
})
},
handleProgressChange({dataId, progress}) {
this.$ajax.post({
url: this.$api.POST_SPEED.replace('{id}', dataId),
params: {
tid: dataId,
speed: progress,
},
}).then(res => {
this.$message.success('调整进度成功')
})
},
handleClick({key}) {
switch (key) {
case 'logout':
this.$cookie.remove('token')
this.$cookie.remove('refresh_token')
this.$router.replace({name: 'login'})
break
default:
break
}
}
}
}
......@@ -48,4 +153,7 @@ export default {
width: 100%;
height: 96%;
}
.user {
cursor: pointer;
}
</style>
......@@ -4,24 +4,47 @@ import 'babel-polyfill'
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import ajax from './server/ajax'
import api from './server/api'
import cookie from './util/local-cookie'
import common from './util/common'
import GanttChart from '@/components/GanttChart'
import {LocaleProvider, Tabs, Button, message} from 'ant-design-vue'
import ActiveForm from '@/components/ActiveForm'
import {LocaleProvider, Tabs, Button, Modal, Form, Row, Col, Input, message, Spin, Dropdown, Icon, Menu, DatePicker, Popconfirm} from 'ant-design-vue'
import moment from 'moment'
import 'moment/locale/zh-cn'
moment.locale('zh-cn')
Vue.config.productionTip = false
Vue.prototype.$message = message
Vue.prototype.$ajax = ajax
Vue.prototype.$api = api
Vue.prototype.$cookie = cookie
Vue.prototype.$com = common
Vue.use(GanttChart)
Vue.use(ActiveForm)
Vue.use(LocaleProvider)
Vue.use(Modal)
Vue.use(Tabs)
Vue.use(Button)
Vue.use(Form)
Vue.use(Row)
Vue.use(Col)
Vue.use(Input)
Vue.use(Spin)
Vue.use(Dropdown)
Vue.use(Icon)
Vue.use(DatePicker)
Vue.use(Menu)
Vue.use(Popconfirm)
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
import Vue from 'vue'
import Router from 'vue-router'
import Main from '@/components/main'
import Login from '@/components/login'
Vue.use(Router)
......@@ -10,6 +11,11 @@ export default new Router({
path: '/',
name: 'main',
component: Main
}
},
{
path: '/login',
name: 'login',
component: Login
},
]
})
import axios from 'axios'
import qs from 'qs'
import api from './api'
import Store from '@/store'
import Cookie from '@/util/local-cookie'
import router from '@/router'
import Common from '@/util/common'
import { message } from 'ant-design-vue'
// 配置请求的根域名和超时时间
const Axios = axios.create({
baseURL: api.BASE_URL,
timeout: 15000,
})
const CancelToken = axios.CancelToken
let cancelRequest = null
// 处理请求状态码
const reponseCodeHandler = (res) => {
const code = res.data && res.data.code
if (code && code != '200') {
switch (code) {
case '911':
const params = {
grant_type: 'refresh_token',
client_id: 'house',
client_secret: 'house',
refreshToken: Cookie.get('refresh_token'),
}
request({
method: 'POST',
url: api.REFRESH_TOKEN_POST,
params,
contentType: 'application/x-www-form-urlencoded;charset=UTF-8',
}).then(res => {
const access_token = Common.confirm(res, 'data.content.access_token')
const refresh_token = Common.confirm(res, 'data.content.refresh_token')
Cookie.set('token', access_token)
Cookie.set('refresh_token', refresh_token)
})
break
default:
message.error(code)
break
}
}
}
// 根据报错的状态码进行错误处理
const errorHandler = (err) => {
const errStatus = (err.response && err.response.status) || (err.data && err.data.errcode)
message.error(errStatus)
}
Axios.interceptors.request.use(config => {
const token = Cookie.get('token')
if (token) {
config.headers.Authorization = token
}
return config
}, error => {
return Promise.reject(error)
})
Axios.interceptors.response.use(response => {
reponseCodeHandler(response)
return response.data
}, error => {
errorHandler(error)
return error.response
})
/**
* 请求
* @param {String} method [请求方法]
* @param {String} url [请求地址]
* @param {Object} params [请求参数]
* @param {String} contentType [请求头,默认为'application/json;charset=UTF-8']
* @param {Boolean} hideLoading [隐藏请求时的loading图,默认为false]
*/
const request = ({ method, url, params, contentType = 'application/json;charset=UTF-8', hideLoading = false }) => {
if (!url || typeof(url) != 'string') {
throw new Error('接口URL不正确')
}
if (!params || typeof(params) == 'string' || typeof(params) == 'number') {
params = {}
}
let config = {
method,
url,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Content-Type': contentType,
},
cancelToken: new CancelToken((c) => {
cancelRequest = c
}),
}
if (method === 'GET') {
config = Object.assign(config, { params })
} else {
if (contentType.toLowerCase().indexOf('x-www-form-urlencoded') >= 0) {
config = Object.assign(config, { data: qs.stringify(params) })
} else {
config = Object.assign(config, { data: params })
}
}
if (!hideLoading) {
Store.commit('SET_LOADING', true)
}
return new Promise((resolve, reject) => {
Axios(config)
.then(res => {
resolve(res)
Store.commit('SET_LOADING', false)
}).catch(err => {
reject(err)
Store.commit('SET_LOADING', false)
})
})
}
export default {
/**
* 取消请求
* @param {String} txt [取消请求时需要显示在控制台的提示信息]
*/
cancel(txt = '取消请求') {
Store.commit('SET_LOADING', false)
if (typeof(cancelRequest) === 'function') {
return cancelRequest(txt)
}
},
get(args) {
return request({ method: 'GET', ...args })
},
post(args) {
return request({ method: 'POST', ...args })
},
put(args) {
return request({ method: 'PUT', ...args })
},
delete(args) {
return request({ method: 'DELETE', ...args })
},
all(...ajaxs) {
return Promise.all(ajaxs)
},
}
let BASE_URL = ''
switch (process.env.NODE_ENV) {
case 'production':
BASE_URL = ''
break
default:
BASE_URL = 'http://47.100.45.230:30000/mock/277'
break
}
export default {
BASE_URL,
POST_LOGIN: '/oauth/token',
GET_INFO: '/info',
GET_PLAN: '/plan/{id}/format',
GET_PLAN_BY_PERSON: '/plan/{id}/resource',
POST_SPEED: '/plan/{id}/speed',
}
export default {}
import Vue from 'vue'
import Vuex from 'vuex'
import state from './state'
import actions from './actions'
import mutations from './mutations'
Vue.use(Vuex)
const isDev = process.env.NODE_ENV === 'development'
export default new Vuex.Store({
strict: isDev,
state,
actions,
mutations,
})
export default {
SET_LOADING(state, val) {
state.showLoading = val
},
}
export default {
showLoading: false,
}
/** 公共方法 */
import Cookie from '@/util/local-cookie'
import Store from '@/store'
import Router from '@/router'
import moment from 'moment'
export default {
/**
* 在深层数据结构中取值(为了替代类似 res && res.data && res.data.content这种写法)
* @param {Object} obj [必填-需要取值的目标对象(例:res)]
* @param {String} path [必填-数据结构路径(例:'data.content')]
* @param {Any} defaultValue [可选-如果取不到值则默认返回该值]
*/
confirm(obj, path, defaultValue = null) {
if (!obj || typeof(obj) != 'object' || !path || typeof(path) != 'string') return
const reducer = (accumulator, currentValue) =>
(accumulator && accumulator[currentValue]) ?
accumulator[currentValue] :
defaultValue
path = path.split('.')
return path.reduce(reducer, obj)
},
/**
* ----- 柯里化版本 (为了不再重复输入obj这个参数) -----
* 在深层数据结构中取值(为了替代类似 res && res.data && res.data.content这种写法)
* @param {Object} obj [必填-需要取值的目标对象(例:res)]
*/
confirm_currying(obj) {
if (!obj || typeof(obj) != 'object') return
return (path, defaultValue = null) => {
if (!path || typeof(path) != 'string') return
const reducer = (accumulator, currentValue) =>
(accumulator && accumulator[currentValue]) ?
accumulator[currentValue] :
defaultValue
path = path.split('.')
return path.reduce(reducer, obj)
}
},
/**
* 判断一维数组中是否存在某个值
* @param {String} value 需要校验的字符串
* @param {Array} validList 被查找的一维数组
* @return {Boolean} 是否存在的结果
*/
oneOf(value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) {
return true
}
}
return false
},
/**
* 转换为金钱格式(千分位且保留两位小数)
* @param {Number | String} num [需转换的数字或字符串]
*/
toMoney(num) {
if (!num) {
return 0.00
}
num = this.toFloat(num).toFixed(2)
const arr = num.toString().split('.')
let int = (arr[0] || 0).toString(),
result = ''
while (int.length > 3) {
result = ',' + int.slice(-3) + result
int = int.slice(0, int.length - 3)
}
if (int) {
result = int + result
}
return `${result}.${arr[1]}`
},
/**
* 手机号码校验
* @param {String} num [需校验的手机号码]
*/
checkPhone(num) {
if (!num) return false
const filter = /^1[3-9][0-9]{9}$/
return filter.test(num)
},
/**
* 固定电话号码校验
* @param {String} num [需校验的固话]
*/
checkTel(num) {
if (!num) return false
const filter = /^(?:0[1-9][0-9]{1,2}-)?[2-8][0-9]{6,7}$/
return filter.test(num)
},
/**
* 身份证号码校验
* @param {String} num [需校验的身份证号码]
*/
checkID(num) {
if (!num) return false
const filter = /(^\d{15}$)|(^\d{17}([0-9]|X)$)/
return filter.test(num)
},
/**
* 数字校验(整数或者小数)
* @param {String} num [需校验的数字]
*/
checkNumber(num) {
if (!num && num != 0) return false
const filter = /^[0-9]+\.{0,1}[0-9]{0,2}$/
return filter.test(num)
},
/**
* 邮编校验(整数或者小数)
* @param {String} num [需校验的数字]
*/
checkZipCode(num) {
if (!num && num != 0) return false
const filter = /^[0-9]{6}$/
return filter.test(num)
},
/**
* 文本校验(只能为中文、英文、数字组合,不允许其他特殊符号)
* @param {String} txt [需校验的文本]
*/
checkContent(txt) {
const filter = /^[\u4E00-\u9FA5A-Za-z0-9]+$/
return filter.test(txt)
},
/**
* 密码校验(6位以上数字字母的组合)
* @param {String} txt [需校验的文本]
*/
checkPassword(num) {
if (!num && num != 0) return false
const filter = /^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,}$/
return filter.test(num)
},
/**
* 格式化日期
* @param {Date} value 日期
*/
formatDate(value) {
if(value){
return moment(value).format('YYYY-MM-DD')
} else {
return null
}
},
/**
* 判断是否是ie并返回版本号
*/
IEVersion() {
const userAgent = navigator.userAgent //取得浏览器的userAgent字符串
const isIE = userAgent.indexOf('compatible') > -1 && userAgent.indexOf('MSIE') > -1 //判断是否IE<11浏览器
const isEdge = userAgent.indexOf('Edge') > -1 && !isIE //判断是否IE的Edge浏览器
const isIE11 = userAgent.indexOf('Trident') > -1 && userAgent.indexOf('rv:11.0') > -1
if(isIE) {
const reIE = new RegExp('MSIE (\\d+\\.\\d+);')
reIE.test(userAgent)
const fIEVersion = parseFloat(RegExp['$1'])
if(fIEVersion == 7) {
return 7
} else if(fIEVersion == 8) {
return 8
} else if(fIEVersion == 9) {
return 9
} else if(fIEVersion == 10) {
return 10
} else {
return 6//IE版本<=7
}
} else if(isEdge) {
return 'edge'//edge
} else if(isIE11) {
return 11 //IE11
}else{
return -1//不是ie浏览器
}
}
}
/* 用于判断electron状态下使用localstorage替换js-cookie */
import jscookie from 'js-cookie'
import localcookie from './local-cookie'
const isElectronApp = window.navigator.userAgent.indexOf('Electron') !== -1
export default isElectronApp ? localcookie : jscookie
export default {
get(key) {
if (!key) {
return null
}
return localStorage.getItem(key)
},
set(key, val) {
if (!key) {
return null
}
localStorage.setItem(key, val)
},
remove(key) {
if (!key) {
return null
}
localStorage.removeItem(key)
},
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment