0.6.0 更新项目、任务成员

This commit is contained in:
磷叶 2025-03-07 16:30:46 +08:00
parent e68b6444b1
commit d45d98661c
21 changed files with 1194 additions and 708 deletions

View File

@ -10,10 +10,11 @@ export function listTask(query) {
}
// 查询任务详细
export function getTask(id) {
export function getTask(id, receive) {
return request({
url: '/bst/task/' + id,
method: 'get'
method: 'get',
params: { receive }
})
}

View File

@ -1,26 +1,20 @@
<template>
<div class="avatar-list" v-if="list.length > 0">
<div class="avatar-list">
<el-tooltip
v-for="(item, index) in displayList"
:key="item[idProp]"
:content="item[showProp]"
placement="top"
>
<div class="avatar-item"
:style="{
marginLeft: index !== 0 ? `-${size * 0.3}px` : '0',
zIndex: list.length - index
}"
@click="handleClick(item)"
>
<avatar
:size="size"
:src="item[avatarProp]"
:name="item[showProp]"
:char-index="charIndex"
/>
<div class="avatar-item" :style="listItemStyle(index)" @click="handleClick(item)">
<avatar :size="size" :src="item[avatarProp]" :name="item[showProp]" :char-index="charIndex"/>
</div>
</el-tooltip>
<div v-if="enableEdit" class="avatar-item add-btn" :style="listItemStyle(displayList.length)" @click="handleEdit()">
<el-avatar :size="size">
<i class="el-icon-plus" style="color: #409EFF;" />
</el-avatar>
</div>
<!-- 总数显示 -->
<el-popover
@ -67,11 +61,11 @@ export default {
},
showProp: {
type: String,
default: 'nickName'
default: 'userName'
},
avatarProp: {
type: String,
default: 'avatar'
default: 'userAvatar'
},
idProp: {
type: String,
@ -93,6 +87,11 @@ export default {
bstType: {
type: String,
default: null
},
//
enableEdit: {
type: Boolean,
default: false
}
},
computed: {
@ -104,9 +103,21 @@ export default {
return this.list.slice(0, this.maxCount);
}
return this.list;
},
//
listItemStyle() {
return (index) => {
return {
marginLeft: index !== 0 ? `-${this.size * 0.3}px` : '0',
zIndex: this.list.length - index
}
}
}
},
methods: {
handleEdit() {
this.$emit('edit')
},
handleClick(item) {
if (this.bstType === 'user') {
this.$router.push({
@ -168,6 +179,15 @@ export default {
font-size: 16px;
}
}
.avatar-item.add-btn {
background-color: #fff;
border: 2px dotted #409EFF;
color: #409EFF;
&:hover {
transform: translateX(30%);
}
}
.total-text {
border: none;

View File

@ -5,7 +5,7 @@
<div>
<el-form :model="queryParams" ref="queryForm" size="small" v-show="showSearch" :inline="true" label-width="5em">
<el-form-item label="归属部门" prop="deptId">
<dept-tree-select v-model="queryParams.deptId" class="small-tree-select" @change="handleQuery"/>
<dept-select v-model="queryParams.deptId" check-strictly @change="handleQuery"/>
</el-form-item>
<el-form-item label="姓名" prop="nickName">
<el-input
@ -16,31 +16,6 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="登录账号" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入登录账号"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="用户状态"
clearable
style="width: 240px"
@change="handleQuery"
>
<el-option
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
@ -52,14 +27,16 @@
</el-row>
<el-table
size="small"
ref="multipleTable"
:data="tableData"
v-loading="loadTable"
@row-click="changeSelection"
@row-dblclick="select"
@select-all="selectionAll"
@select="changeSelection"
@select="handleSelect"
highlight-current-row
max-height="500px"
>
<el-table-column align="center" type="selection" v-if="multiple"/>
<el-table-column label="#" type="index" align="center"/>
@ -82,16 +59,7 @@
{{d.row.dept == null ? '' : d.row.dept.deptName}}
</template>
<template v-else-if="column.key === 'status'">
<dict-tag :value="d.row.status" :options="dict.type.sys_normal_disable"/>
</template>
<template v-else-if="column.key === 'employStatus'">
<dict-tag :value="d.row.employStatus" :options="dict.type.user_employ_status"/>
</template>
<template v-else-if="['birthday'].includes(column.key)">
{{calcBirthDay(d.row[column.key], new Date()) | dv}}
</template>
<template v-else-if="['employDate'].includes(column.key)">
{{calcFullYear(d.row[column.key], new Date()) | dv}}
<dict-tag :value="d.row.status" :options="dict.type.user_status"/>
</template>
<template v-else>
{{d.row[column.key] | dv}}
@ -109,11 +77,11 @@
@pagination="getList"
/>
</div>
</template>
</template>
<script>
import { deepClone } from '@/utils'
import DeptTreeSelect from '@/components/Business/Dept/DeptTreeSelect.vue'
import DeptSelect from '@/components/Business/Dept/DeptSelect.vue'
import BooleanTag from '@/components/BooleanTag/index.vue'
import { $showColumns } from '@/utils/mixins'
import { listUser } from '@/api/system/user'
@ -127,8 +95,8 @@ const defaultSort = {
export default {
name: "UserCheck",
mixins: [$showColumns],
dicts: ['sys_normal_disable', 'sys_user_sex', 'user_employ_status'],
components: {BooleanTag, DeptTreeSelect},
dicts: ['user_status'],
components: {BooleanTag, DeptSelect},
props: {
//
title: {
@ -183,11 +151,8 @@ export default {
{key: 'userId', visible: false, label: 'ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'nickName', visible: true, label: '姓名', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'deptId', visible: true, label: '部门', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'userName', visible: true, label: '登录账号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'status', visible: true, label: '账号状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'remark', visible: true, label: '备注', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'loginIp', visible: true, label: '最后登录IP', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'loginDate', visible: true, label: '最后登录时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'createTime', visible: true, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
],
}
@ -259,6 +224,9 @@ export default {
this.select(this.row);
}
},
handleSelect(selection, row) {
this.changeSelection(row);
},
//
changeSelection(row){
if(this.multiple){

View File

@ -2,7 +2,7 @@
<!--版本更新内容添加prop属性修复多选-->
<template>
<el-dialog :title="title" :visible="show" width="80%" top="2vh" @close="close" :append-to-body="true">
<el-dialog :title="title" :visible="show" width="900px" top="2vh" @close="close" :append-to-body="true">
<user-check
v-if="show"
ref="check"

View File

@ -0,0 +1,143 @@
<template>
<el-select
v-model="selectedValue"
placeholder="请选择"
clearable
filterable
:multiple="multiple"
:loading="loading"
@change="handleChange"
>
<div class="select-footer">
<div style="text-align: center; color: #8492a6; font-size: 13px; line-height: 28px; ">
{{ total }}条数据
</div>
<el-button v-if="multiple && !isEmpty(options)" style="margin-left: 10px;" size="mini" type="text" @click.stop="handleSelectAll">
{{ isAllSelected ? '取消全选' : '全选' }}
</el-button>
</div>
<el-option
v-for="item in options"
:key="item.userId"
:value="item.userId"
:label="item.nickName"
>
<avatar :size="24" :src="item.avatar" :name="item.nickName" :id="item.userId" />
<span class="user-name">{{ item.nickName }}</span>
</el-option>
<el-option v-if="isEmpty(value) && isEmpty(options)" style="display:none" disabled :value="null"></el-option>
</el-select>
</template>
<script>
import { listUser } from '@/api/system/user';
import Avatar from '@/components/Avatar';
import {isEmpty} from '@/utils'
export default {
name: 'UserRemoteSelect',
components: {
Avatar
},
props: {
// id
value: {
type: [String, Array],
default: null
},
//
query: {
type: Object,
default: () => ({})
},
//
multiple: {
type: Boolean,
default: false
},
},
data() {
return {
options: [],
total: 0,
loading: false,
queryParams: {
pageNum: 1,
pageSize: 100,
keyword: null
}
}
},
computed: {
selectedValue: {
get() {
return this.value
},
set(value) {
this.$emit('input', value)
}
},
//
isAllSelected() {
return this.multiple && this.options.length > 0 && Array.isArray(this.value) && this.value.length === this.options.length;
}
},
watch: {
query: {
handler(nv) {
this.queryParams = {
...this.queryParams,
...nv
}
this.getOptions();
},
immediate: true
}
},
created() {
this.getOptions();
},
methods: {
isEmpty,
//
handleSelectAll() {
if (this.isAllSelected) {
this.handleChange([]);
} else {
const allUserIds = this.options.map(item => item.userId);
this.handleChange(allUserIds);
}
},
handleChange(value) {
let list = this.options.filter(item => value.includes(item.userId));
this.$emit('change', list);
},
getOptions() {
this.loading = true;
this.queryParams = {
...this.queryParams,
...this.query
}
listUser(this.queryParams).then(res => {
this.options = res.rows;
this.total = res.total;
}).finally(() => {
this.loading = false;
});
}
}
}
</script>
<style lang="scss" scoped>
.select-footer {
padding: 5px 12px;
border-bottom: 1px solid #EBEEF5;
display: flex;
justify-content: flex-end;
align-items: center;
}
.user-name {
padding-left: 6px;
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<div>
<div class="collapse-title" @click="onChange" >
<div class="title-text">{{title}}</div>
<el-link type="primary" :underline="false">
<template v-if="show">
<i class="el-icon-arrow-up"/>
</template>
<template v-else>
<i class="el-icon-arrow-down"/>
</template>
</el-link>
</div>
<div class="collapse-content">
<el-collapse-transition>
<div v-show="show">
<slot></slot>
</div>
</el-collapse-transition>
</div>
</div>
</template>
<script>
export default {
name: "CollapsePanel",
props: {
title: {
type: String,
default: null
},
value: {
type: Boolean,
default: false
}
},
data() {
return {
show: this.value
}
},
watch: {
value(val) {
this.show = val;
}
},
methods: {
onChange() {
this.show = !this.show;
this.$emit('input', this.show);
}
}
}
</script>
<style scoped lang="scss">
.collapse-title {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
background-color: #f4faff;
padding: 8px 14px;
border-radius: 8px;
cursor: pointer;
transition: .25s;
&:hover {
background-color: #e5f2ff;
}
.title-text {
font-size: 14px;
color: #1890ff;
flex: 1;
}
}
</style>

View File

@ -271,14 +271,18 @@ export default {
//
handleMouseEnter() {
if (!this.isListening) {
this.$refs.pasteArea.focus();
if (this.$refs.pasteArea) {
this.$refs.pasteArea.focus();
}
this.isListening = true;
}
},
//
handleMouseLeave() {
if (this.isListening) {
this.$refs.pasteArea.blur();
if (this.$refs.pasteArea) {
this.$refs.pasteArea.blur();
}
this.isListening = false;
}
},

View File

@ -8,12 +8,12 @@ export const TaskType = {
// 任务状态
export const TaskStatus = {
WAIT_COMPLETED: "1", // 待完成(弃用)
WAIT_RECEIVE: "1", // 待接收
PROCESSING: "2", // 进行中
WAIT_CONFIRM: "3", // 待确认(用)
PASS: "4", // 通过
REJECT: "5", // 驳回(弃用)
CANCEL: "6", // 取消
WAIT_CONFIRM: "3", // 待确认(用)
PASS: "4", // 通过
REJECT: "5", // 驳回(停用)
CANCEL: "6", // 取消
// 获取可以提交的任务状态
canSubmit() {
@ -21,11 +21,23 @@ export const TaskStatus = {
},
// 获取可以取消的任务状态
canCancel() {
return [this.WAIT_COMPLETED, this.PROCESSING]
return [this.WAIT_RECEIVE, this.PROCESSING]
},
// 获取可以完成的任务状态
// 获取可以通过的任务状态
canPass() {
return [this.PROCESSING]
},
// 获取可以开始的任务状态
canStart() {
return [this.WAIT_RECEIVE, this.CANCEL]
},
// 获取未完成任务状态
unComplete() {
return [this.PROCESSING]
},
// 获取已完成任务状态
complete() {
return [this.PASS]
}
}
@ -142,3 +154,13 @@ export const Role = {
SYS_ADMIN: "sys_admin", // 系统管理员
ADMIN: "admin", // 管理员
}
// 项目成员角色
export const ProjectMemberRole = {
OWNER: "OWNER", // 项目负责人
FOLLOWER: "FOLLOWER", // 项目跟进人
UI: "UI", // UI设计
DEV: "DEV", // 开发
QA: "QA", // 测试
NORMAL: "NORMAL", // 普通成员
}

View File

@ -0,0 +1,104 @@
<template>
<el-dialog :visible.sync="dialogVisible" title="编辑成员" width="800px" @open="handleOpen">
<el-form :model="form" v-loading="loading" label-position="top" ref="form">
<project-member-edit-list :form="form" :rules="rules.memberList" />
</el-form>
<template #footer>
<el-button type="primary" @click="handleSubmit">保存</el-button>
<el-button @click="dialogVisible = false">取消</el-button>
</template>
</el-dialog>
</template>
<script>
import { getProject, updateProject } from '@/api/bst/project';
import ProjectMemberEditList from '@/views/bst/project/edit/components/ProjectMemberEditList.vue';
export default {
name: 'ProjectMemberEditDialog',
components: {
ProjectMemberEditList
},
props: {
visible: {
type: Boolean,
default: false,
},
// ID
id: {
type: String,
default: null
},
},
data() {
return {
loading: false,
form: {
memberList: []
},
rules: {
memberList: {
userId: [
{ required: true, message: "成员不能为空", trigger: "blur" }
],
role: [
{ required: true, message: "角色不能为空", trigger: "change" }
]
}
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
}
},
methods: {
handleOpen() {
if (this.id == null) {
this.$message.error('项目ID不能为空');
this.dialogVisible = false;
return;
}
this.getDetail();
},
getDetail() {
this.loading = true;
getProject(this.id).then(res => {
this.form = res.data;
}).finally(() => {
this.loading = false;
});
},
handleSubmit() {
this.$refs.form.validate().then((valid) => {
if (valid) {
this.loading = true;
updateProject({
id: this.form.id,
memberList: this.form.memberList
}).then(res => {
if (res.code === 200) {
this.$message.success('保存成功');
this.dialogVisible = false;
this.$emit('success');
}
}).finally(() => {
this.loading = false;
});
}
});
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,123 @@
<template>
<div>
<el-table
:data="form.memberList"
size="mini"
stripe
:header-cell-style="headerCellStyle"
class="mini-table table-form"
ref="table"
>
<el-table-column label="序号" type="index" align="center" width="60" />
<table-form-col label="成员" prop-prefix="memberList" prop="userId" required :rules="rules.userId" align="left" width="200">
<template slot-scope="d">
<avatar :src="d.row.userAvatar" :name="d.row.userName" :char-index="-1" :size="24" />
{{d.row.userName}}
</template>
</table-form-col>
<table-form-col label="角色" prop-prefix="memberList" prop="role" required :rules="rules.role">
<template slot-scope="d">
<el-radio-group v-model="d.row.role" placeholder="请选择角色">
<el-radio v-for="item in dict.type.project_member_role" :key="item.value" :label="item.value">
{{item.label}}
</el-radio>
</el-radio-group>
</template>
</table-form-col>
<el-table-column label="操作" align="center" width="120">
<template #header>
<el-button size="small" icon="el-icon-plus" type="text" @click="handleAdd" >新增成员</el-button>
</template>
<template slot-scope="d">
<el-button
type="text"
@click="handleDel(d.$index, d.row)"
icon="el-icon-delete"
size="small"
>删除</el-button>
</template>
</el-table-column>
</el-table>
<user-dialog
:show.sync="showUserDialog"
multiple
:query="userQuery"
@select="handleSelectUser"
/>
</div>
</template>
<script>
import TableFormCol from '@/components/TableFormCol/index.vue'
import UserDialog from '@/components/Business/User/UserDialog.vue'
import Avatar from '@/components/Avatar/index.vue'
import { ProjectMemberRole } from '@/utils/enums'
export default {
name: "ProjectMemberEditList",
dicts: ['project_member_role'],
components: { TableFormCol, UserDialog, Avatar},
props: {
form: {
type: Object,
default: () =>({
memberList: [], //
})
},
rules: {
type: Object,
default: () => ({})
}
},
data() {
return {
showUserDialog: false,
headerCellStyle: {
backgroundColor: "#f5f7fa",
},
}
},
computed: {
userQuery() {
return {
excludeUserIds: this.form.memberList.map(item => item.userId),
}
}
},
methods: {
//
handleAdd() {
this.showUserDialog = true;
},
handleSelectUser(users) {
users.forEach(user => {
this.form.memberList.push(this.getNewRow(user))
})
this.showUserDialog = false;
},
//
getNewRow(user) {
return {
id: null,
userId: user.userId,
role: ProjectMemberRole.NORMAL,
// vo
userName: user.nickName,
userAvatar: user.avatar,
}
},
//
handleDel(index, row) {
this.$confirm(`是否删除成员 【${row.userName}】 ?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.form.memberList.splice(index, 1)
})
}
}
}
</script>

View File

@ -5,7 +5,7 @@
<el-button type="primary" plain @click="submitForm" icon="el-icon-check" size="small" :loading="submitLoading"> </el-button>
</edit-header>
<div class="app-container">
<el-form ref="form" :model="form" :rules="rules" label-width="80px" v-loading="loading">
<el-form ref="form" :model="form" :rules="rules" size="small" label-width="80px" v-loading="loading">
<el-row>
<form-col :span="span" label="项目名称" prop="name">
<el-input v-model="form.name" placeholder="请输入项目名称" />
@ -31,15 +31,6 @@
placeholder="请选择到期时间">
</el-date-picker>
</form-col>
<form-col :span="span" label="负责人" prop="ownerId">
<user-select v-model="form.ownerId" />
</form-col>
<form-col :span="span" label="跟进人" prop="followId">
<user-select v-model="form.followId" />
</form-col>
<form-col :span="span * 2" label="项目成员" prop="memberIds">
<user-select v-model="form.memberIds" multiple />
</form-col>
<form-col :span="24" label="备注" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
</form-col>
@ -47,6 +38,12 @@
<image-upload v-model="form.attaches" :limit="999" :file-type="fileType" edit-name value-type="array-object"/>
</form-col>
</el-row>
<el-tabs>
<el-tab-pane label="项目成员">
<project-member-edit-list :form="form" :rules="rules.memberList" />
</el-tab-pane>
</el-tabs>
</el-form>
</div>
</div>
@ -60,9 +57,10 @@ import EditHeader from "@/components/EditHeader/index.vue";
import CustomerInput from "@/components/Business/Customer/CustomerInput.vue";
import UserSelect from "@/components/Business/User/UserSelect.vue";
import { FileType } from '@/utils/constants';
import ProjectMemberEditList from './components/ProjectMemberEditList.vue';
export default {
name: "ProjectEdit",
components: { FormCol, EditHeader, UserSelect, CustomerInput },
components: { FormCol, EditHeader, UserSelect, CustomerInput, ProjectMemberEditList },
dicts: ['project_status'],
data() {
return {
@ -70,12 +68,22 @@ export default {
submitLoading: false,
span: 6,
//
form: {},
form: {
memberList: [],
},
//
rules: {
name: [
{ required: true, message: "项目名称不能为空", trigger: "blur" }
],
memberList: {
userId: [
{ required: true, message: "成员不能为空", trigger: "blur" }
],
role: [
{ required: true, message: "角色不能为空", trigger: "blur" }
]
}
},
fileType: FileType.all(),
};
@ -121,7 +129,7 @@ export default {
remark: null,
attaches: [],
customerId: this.$route.query.customerId,
memberIds: [],
memberList: [],
// vo
customerName: this.$route.query.customerName,
};

View File

@ -25,11 +25,8 @@
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="负责人" prop="ownerId">
<user-select v-model="queryParams.ownerId" clearable @change="handleQuery"/>
</el-form-item>
<el-form-item label="跟进人" prop="followId">
<user-select v-model="queryParams.followId" clearable @change="handleQuery"/>
<el-form-item label="成员" prop="joinUserId">
<user-select v-model="queryParams.joinUserId" clearable @change="handleQuery"/>
</el-form-item>
<el-form-item label="开发超期" prop="devOverdue">
<el-radio-group v-model="queryParams.devOverdue" @change="handleQuery">
@ -136,6 +133,17 @@
<template v-else-if="column.key === 'time'">
{{getExpireTime(d.row).value | dv}}
</template>
<template v-else-if="column.key === 'memberList'">
<div style="margin: 0 auto; width: fit-content">
<avatar-list :list="d.row.memberList" :size="24" :char-index="-1" :max-count="4" unit="人"/>
</div>
</template>
<template v-else-if="column.key === 'ownerName' && d.row.memberList != null">
{{d.row.memberList.filter(item => item.role === ProjectMemberRole.OWNER).map(item => item.userName).join(",")}}
</template>
<template v-else-if="column.key === 'followName' && d.row.memberList != null">
{{d.row.memberList.filter(item => item.role === ProjectMemberRole.FOLLOWER).map(item => item.userName).join(",")}}
</template>
<template v-else>
{{d.row[column.key]}}
</template>
@ -246,7 +254,7 @@
<script>
import { listProject, delProject, completeProject, acceptProject } from "@/api/bst/project";
import { $showColumns } from '@/utils/mixins';
import { ProjectStatus } from '@/utils/enums';
import { ProjectStatus, ProjectMemberRole } from '@/utils/enums';
import FormCol from "@/components/FormCol/index.vue";
import ProjectStartDialog from '@/views/bst/project/components/ProjectStartDialog.vue';
import ProjectMaintenanceDialog from '@/views/bst/project/components/ProjectMaintenanceDialog.vue';
@ -256,6 +264,7 @@ import CustomerLink from '@/components/Business/Customer/CustomerLink.vue';
import BooleanTag from '@/components/BooleanTag/index.vue';
import UserSelect from '@/components/Business/User/UserSelect.vue';
import {ProjectUtils} from '@/views/bst/project/utils.js';
import AvatarList from '@/components/AvatarList/index.vue';
export default {
name: "Project",
@ -269,7 +278,8 @@ export default {
ProjectLink,
CustomerLink,
BooleanTag,
UserSelect
UserSelect,
AvatarList
},
props: {
query: {
@ -287,6 +297,7 @@ export default {
},
data() {
return {
ProjectMemberRole,
showTaskDialog: false, //
ProjectStatus,
showStartDialog: false, //
@ -297,14 +308,15 @@ export default {
columns: [
{key: 'id', visible: false, label: 'ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'no', visible: false, label: '项目编号', minWidth: null, sortable: true, overflow: true, align: 'center', width: null},
{key: 'name', visible: true, label: '项目名称', minWidth: "250", sortable: true, overflow: false, align: 'left', width: null},
{key: 'name', visible: true, label: '名称', minWidth: "250", sortable: true, overflow: false, align: 'left', width: null},
{key: 'memberList', visible: true, label: '成员', minWidth: null, sortable: false, overflow: false, align: 'center', width: null},
{key: 'status', visible: false, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'customerName', visible: true, label: '客户', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'expireTime', visible: false, label: '到期时间', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'expectedCompleteDate', visible: false, label: '预计开发', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'maintenanceEndDate', visible: false, label: '运维结束', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'ownerName', visible: true, label: '负责人', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'followName', visible: true, label: '跟进人', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'ownerName', visible: true, label: '负责人', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'followName', visible: true, label: '跟进人', minWidth: null, sortable: false, overflow: false, align: 'center', width: "100"},
{key: 'amount', visible: false, label: '项目金额', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'receivedAmount', visible: false, label: '已收金额', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'operationAmount', visible: false, label: '运维费用', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},

View File

@ -106,11 +106,7 @@
<el-col :span="6">
<!-- 成员 -->
<el-card class="box-card" header="项目成员">
<el-descriptions :column="2">
<el-descriptions-item label="负责人">{{ detail.ownerName | dv}}</el-descriptions-item>
<el-descriptions-item label="跟进人">{{ detail.followName | dv}}</el-descriptions-item>
</el-descriptions>
<avatar-list :list="detail.memberList" :size="32" :char-index="-1"/>
<avatar-list :list="detail.memberList" :size="32" :char-index="-1" enable-edit @edit="handleEditMember"/>
</el-card>
<!-- 附件信息 -->
<el-card class="box-card" header="附件">
@ -150,6 +146,13 @@
:id="detail.id"
@success="getDetail"
/>
<!-- 编辑成员弹窗 -->
<project-member-edit-dialog
:visible.sync="editMemberDialogVisible"
:id="detail.id"
@success="getDetail"
/>
</div>
</template>
@ -163,6 +166,8 @@ import ProjectMaintenanceDialog from '@/views/bst/project/components/ProjectMain
import AvatarList from '@/components/AvatarList/index.vue';
import CustomerLink from '@/components/Business/Customer/CustomerLink.vue';
import { checkPermi } from '@/utils/permission';
import ProjectMemberEditDialog from '@/views/bst/project/components/ProjectMemberEditDialog.vue';
export default {
name: "ProjectView",
components: {
@ -171,7 +176,8 @@ export default {
ProjectStartDialog,
ProjectMaintenanceDialog,
AvatarList,
CustomerLink
CustomerLink,
ProjectMemberEditDialog
},
dicts: ['project_status'],
data() {
@ -183,7 +189,9 @@ export default {
//
imageTypes: ['.png', '.jpg', '.jpeg', '.gif', '.bmp'],
//
ProjectStatus
ProjectStatus,
//
editMemberDialogVisible: false,
}
},
computed: {
@ -210,6 +218,9 @@ export default {
this.getDetail();
},
methods: {
handleEditMember() {
this.editMemberDialogVisible = true;
},
checkPermi,
//
handleAccept(row) {

View File

@ -13,24 +13,31 @@
<form-col :span="24" label="项目" prop="projectId">
<project-select v-model="form.projectId" :disabled="initData.projectId != null" @change="onChangeProject"/>
</form-col>
<form-col :span="24" label="附件" prop="picture">
<form-col :span="24" label="附件" prop="picture">
<image-upload v-model="form.picture" :file-type="FileType"/>
</form-col>
<form-col :span="24" label="类型" prop="type">
<form-col :span="span" label="类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio-button v-for="dict in dict.type.task_type" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</form-col>
<form-col :span="24" label="优先级" prop="level">
<form-col :span="span" label="优先级" prop="level">
<el-radio-group v-model="form.level">
<el-radio-button v-for="dict in dict.type.task_level" :key="dict.value" :label="dict.value">{{ dict.label }}</el-radio-button>
</el-radio-group>
</form-col>
<form-col :span="24" label="任务内容" prop="description">
<el-input v-model="form.description" placeholder="请输入任务内容" type="textarea" maxlength="1000" show-word-limit/>
<el-input v-model="form.description" placeholder="请输入任务内容" type="textarea" maxlength="1000" show-word-limit :autosize="{ minRows: 3, maxRows: 10 }"/>
</form-col>
<form-col :span="24" label="负责人" prop="ownerIds">
<user-select v-model="form.ownerIds" multiple :filter-method="filterOwnerMethod"/>
<form-col :span="24" label="负责人" prop="memberList">
<user-remote-select
:value="userRemoteValue"
multiple
:init-options="userRemoteInitOptions"
@change="onChangeMemberList"
:query="userQuery"
style="width: 100%;"
/>
</form-col>
<form-col :span="span" label="截止时间" prop="expireTime">
<el-date-picker clearable
@ -38,6 +45,7 @@
v-model="form.expireTime"
type="datetime"
value-format="yyyy-MM-dd HH:mm:ss"
default-time="23:59:59"
placeholder="请选择截止时间">
</el-date-picker>
</form-col>
@ -59,10 +67,10 @@ import UserSelect from '@/components/Business/User/UserSelect.vue';
import { FileType } from '@/utils/constants';
import { checkRole } from '@/utils/permission';
import { mapGetters } from 'vuex';
import UserRemoteSelect from '@/components/Business/User/UserRemoteSelect.vue';
export default {
name: "TaskEditDialog",
components: { FormCol, ProjectSelect, UserSelect },
components: { FormCol, ProjectSelect, UserSelect, UserRemoteSelect },
dicts: ['task_status', 'task_level', 'task_type'],
props: {
show: {
@ -84,11 +92,13 @@ export default {
loading: false,
submitLoading: false,
//
form: {},
form: {
memberList: []
},
//
rules: {
name: [
{ required: true, message: "任务名称不能为空", trigger: "blur" }
description: [
{ required: true, message: "任务内容不能为空", trigger: "blur" }
],
type: [
{ required: true, message: "类型不能为空", trigger: "change" }
@ -96,7 +106,7 @@ export default {
level: [
{ required: true, message: "优先级不能为空", trigger: "change" }
],
ownerIds: [
memberList: [
{ required: true, type: 'array', message: "负责人不能为空", trigger: "blur" }
],
},
@ -120,48 +130,39 @@ export default {
set(value) {
this.$emit('update:show', value);
}
},
//
userRemoteValue() {
return this.form.memberList.map(item => item.userId);
},
//
userRemoteInitOptions() {
return this.form.memberList.map(item => {
return {
userId: item.userId,
nickName: item.userName,
avatar: item.userAvatar,
}
});
},
userQuery() {
return {
projectId: this.form.projectId
}
}
},
methods: {
/**
* 过滤可选择的负责人列表
* @param {Array} data 用户列表数据
* @returns {Array} 过滤后的用户列表
*/
filterOwnerMethod(data) {
// ,
if (this.form.projectId) {
const { projectOwnerId, projectFollowId, projectMemberIds } = this.form;
//
if (!projectOwnerId || !projectFollowId || !projectMemberIds) {
return [];
onChangeMemberList(selection) {
this.form.memberList = selection.map(item => {
return {
userId: item.userId,
userName: item.nickName
}
// ID()
const projectUserIds = [
projectOwnerId,
projectFollowId,
...projectMemberIds
].filter(Boolean);
//
data = data.filter(item => projectUserIds.includes(item.userId));
}
//
if (!checkRole([Role.SYS_ADMIN, Role.ADMIN])) {
data = data.filter(item => item.userId === this.userId);
}
return data;
});
},
onChangeProject(project) {
this.form.projectOwnerId = project.ownerId;
this.form.projectFollowId = project.followId;
this.form.projectMemberIds = project.memberIds;
if (this.form.ownerIds.length > 0) {
this.form.ownerIds = [];
if (this.form.memberList.length > 0) {
this.form.memberList = [];
this.$message("由于更换了项目,负责人已清空");
}
},
@ -185,7 +186,7 @@ export default {
picture: null,
description: null,
expireTime: null,
ownerIds: [],
memberList: [],
...this.initData
};
this.resetForm("form");

View File

@ -1,48 +1,37 @@
<template>
<div class="task-process-panel">
<el-card class="box-card" header="负责人">
<div class="process-list">
<el-row :gutter="10">
<el-col :span="24" v-for="owner in ownerList" :key="owner.userId">
<div class="process-item">
<div class="user-info">
<avatar :size="28" :src="owner.avatar" :name="owner.nickName" :id="owner.userId" />
<span class="user-name">{{ owner.nickName }}</span>
<div class="status-tag">
<el-tag size="mini" :type="hasSubmitted(owner.userId) ? 'success' : 'info'">
{{ hasSubmitted(owner.userId) ? '已完成' : '未完成' }}
</el-tag>
</div>
</div>
<div class="process-info">
<div class="info-item">
<span class="label">提交数</span>
<span class="value">{{ getSubmitCount(owner.userId) }}</span>
</div>
<div class="info-item">
<span class="label">通过数</span>
<span class="value">{{ getPassCount(owner.userId) }}</span>
</div>
<div class="info-item">
<span class="label">通过时间</span>
<span class="value">{{ getFirstPassSubmitTime(owner.userId) }}</span>
</div>
</div>
<el-row :gutter="10">
<el-col :span="24" v-for="member in memberList" :key="member.id">
<div class="process-item">
<div class="user-info">
<avatar :size="24" :src="member.userAvatar" :name="member.userName" :id="member.userId" :char-index="-1" />
<span class="user-name">{{ member.userName }}</span>
<div class="status-tag">
<dict-tag :value="member.status" :options="dict.type.task_member_status" size="mini" />
</div>
</el-col>
</el-row>
</div>
</el-card>
</div>
<div class="process-info">
<div class="info-item">
<span class="label">接收时间</span>
<span class="value">{{member.receiveTime | dv}}</span>
</div>
<div class="info-item">
<span class="label">提交时间</span>
<span class="value">{{member.submitTime | dv}}</span>
</div>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import Avatar from '@/components/Avatar'
import { TaskSubmitStatus } from '@/utils/enums'
import { parseTime } from '@/utils'
export default {
name: 'TaskProcessPanel',
dicts: ['task_member_status'],
components: {
Avatar
},
@ -53,94 +42,60 @@ export default {
}
},
computed: {
ownerList() {
return this.detail.ownerList || []
memberList() {
return this.detail.memberList || []
},
submitList() {
return this.detail.submitList || []
}
},
methods: {
//
getSubmitCount(userId) {
return this.submitList.filter(item => item.userId === userId).length
},
//
getPassCount(userId) {
return this.submitList.filter(item => item.userId === userId && item.status === TaskSubmitStatus.PASS).length
},
//
getFirstPassSubmitTime(userId) {
const userPassSubmits = this.submitList.filter(item =>
item.userId === userId &&
item.status === TaskSubmitStatus.PASS
)
if (userPassSubmits.length === 0) return '暂未通过'
const firstPassSubmit = userPassSubmits.reduce((prev, curr) => {
return new Date(prev.createTime) < new Date(curr.createTime) ? prev : curr
})
return firstPassSubmit.createTime;
},
//
hasSubmitted(userId) {
return this.submitList.some(item =>
item.userId === userId &&
item.status === TaskSubmitStatus.PASS
)
}
}
}
</script>
<style lang="scss" scoped>
.task-process-panel {
.process-list {
.process-item {
padding: 8px 12px;
margin-bottom: 8px;
border: 1px solid #ebeef5;
border-radius: 4px;
transition: all 0.3s;
.process-item {
padding: 8px 12px;
margin-bottom: 4px;
border: 1px solid #ebeef5;
border-radius: 4px;
transition: all 0.3s;
&:hover {
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
&:hover {
box-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.1);
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 6px;
.user-name {
margin-left: 6px;
font-size: 13px;
font-weight: bold;
flex: 1;
}
.user-info {
display: flex;
align-items: center;
margin-bottom: 6px;
.user-name {
margin-left: 8px;
font-size: 13px;
font-weight: bold;
flex: 1;
}
.status-tag {
margin-left: auto;
}
.status-tag {
margin-left: auto;
}
}
.process-info {
display: flex;
flex-wrap: wrap;
gap: 8px;
.process-info {
display: flex;
flex-wrap: wrap;
gap: 8px;
.info-item {
font-size: 12px;
flex: 1;
min-width: 120px;
.info-item {
font-size: 12px;
flex: 1;
min-width: 120px;
.label {
color: #909399;
}
.label {
color: #909399;
}
.value {
color: #303133;
margin-left: 4px;
}
.value {
color: #303133;
margin-left: 4px;
}
}
}

View File

@ -61,7 +61,7 @@
<el-col :span="10">
<!-- 附件展示 -->
<el-card class="card-box" header="任务附件">
<el-card class="card-box">
<attach-list :file-list="detail.picture" :column="2" />
</el-card>
@ -160,7 +160,7 @@ export default {
/** 获取详细信息 */
getInfo(id) {
this.loading = true;
getTask(id).then(response => {
getTask(id, true).then(response => {
this.detail = response.data;
}).finally(() => {
this.loading = false;

View File

@ -14,15 +14,14 @@
/>
</el-select>
</el-form-item>
<el-form-item label="状态" prop="status" v-if="isShow('description')">
<el-radio-group v-model="queryParams.status" placeholder="请选择状态" clearable @change="handleQuery">
<el-radio-button :label="null">全部</el-radio-button>
<el-radio-button
<el-form-item label="状态" prop="statusList" v-if="isShow('description')">
<el-checkbox-group v-model="queryParams.statusList" placeholder="请选择状态" multiple clearable @change="handleQuery">
<el-checkbox-button
v-for="dict in dict.type.task_status"
:key="dict.value"
:label="dict.value"
>{{ dict.label }}</el-radio-button>
</el-radio-group>
>{{ dict.label }}</el-checkbox-button>
</el-checkbox-group>
</el-form-item>
<el-form-item label="优先级" prop="level" v-if="isShow('description')">
<el-select v-model="queryParams.level" placeholder="请选择优先级" clearable @change="handleQuery">
@ -139,9 +138,9 @@
截止:{{d.row.expireTime | dv}}<br/>
完成:{{d.row.passTime | dv}}<br/>
</template>
<template v-else-if="column.key === 'owner'">
<template v-else-if="column.key === 'memberList'">
<div style="margin: 0 auto; width: fit-content">
<avatar-list :list="d.row.ownerList" :max-count="3" unit="人" :char-index="-1" bst-type="user"/>
<avatar-list :list="d.row.memberList" :max-count="3" unit="人" :char-index="-1" bst-type="user"/>
</div>
</template>
<template v-else>
@ -251,10 +250,10 @@ export default {
{key: 'description', visible: true, label: '任务内容', minWidth: "350", sortable: false, overflow: false, align: 'left', width: null},
{key: 'picture', visible: true, label: '图片', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'projectName', visible: true, label: '项目', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'owner', visible: true, label: '负责人', minWidth: null, sortable: false, overflow: false, align: 'center', width: "150"},
{key: 'createName', visible: true, label: '创建人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'memberList', visible: true, label: '负责人', minWidth: null, sortable: false, overflow: false, align: 'center', width: "150"},
{key: 'receivedCount', visible: true, label: '接收', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"},
{key: 'submitCount', visible: true, label: '提交', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"},
{key: 'passCount', visible: true, label: '完成', minWidth: null, sortable: false, overflow: false, align: 'center', width: "50"},
{key: 'createName', visible: true, label: '创建人', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'expireTime', visible: true, label: '时间', minWidth: null, sortable: true, overflow: false, align: 'left', width: "190"},
],
//
@ -289,7 +288,7 @@ export default {
projectId: null,
name: null,
type: null,
status: TaskStatus.PROCESSING,
statusList: [TaskStatus.PROCESSING, TaskStatus.WAIT_RECEIVE],
level: null,
createId: null,
ownerId: null,

View File

@ -9,8 +9,8 @@ export const $task = {
return (row) => {
return checkPermi(['bst:task:submit'])
&& TaskStatus.canSubmit().includes(row.status)
&& row.ownerIds != null
&& row.ownerIds.includes(this.userId);
&& row.memberList != null
&& row.memberList.some(item => item.userId === this.userId);
}
},
// 是否可以取消
@ -45,7 +45,6 @@ export const $task = {
// 是否是创建者
isCreator() {
return (row) => {
console.log("isCreator", row, this.userId);
return row.createId === this.userId;
}
}

View File

@ -0,0 +1,246 @@
<template>
<el-dialog :title="title" @open="handleOpen" :visible.sync="dialogVisible" width="700px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" size="small" label-width="6em" v-loading="loading">
<el-row :gutter="12">
<form-col :span="24" label="归属部门" prop="deptId">
<dept-select v-model="form.deptId" check-strictly/>
</form-col>
<form-col :span="span" label="姓名" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入姓名" maxlength="30" />
</form-col>
<form-col :span="span" label="角色">
<el-select v-model="form.roleIds" size="small" multiple placeholder="请选择角色" style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
:disabled="item.status == 1"
></el-option>
</el-select>
</form-col>
<form-col :span="span" label="登录账号" prop="userName">
<el-input v-model="form.userName" placeholder="请输入登录账号" maxlength="30" />
</form-col>
<form-col :span="span" label="登录密码" prop="password" v-if="form.userId == null">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password/>
</form-col>
</el-row>
<el-row :gutter="12">
<form-col :span="span" label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</form-col>
<form-col :span="span" label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</form-col>
<form-col :span="span" label="用户性别">
<el-radio-group v-model="form.sex">
<el-radio
v-for="dict in dict.type.sys_user_sex"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</form-col>
<form-col :span="span" label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.user_status"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</form-col>
<form-col :span="span" label="入职日期" prop="employDate">
<el-date-picker style="width: 100%" clearable v-model="form.employDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期"/>
</form-col>
<form-col :span="span" label="离职日期" prop="resignDate">
<el-date-picker style="width: 100%" clearable v-model="form.resignDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期"/>
</form-col>
<form-col :span="span" label="身份证正面" prop="idCardFront" label-width="6em">
<image-upload v-model="form.idCardFront" :limit="1" :file-size="10" :is-show-tip="false"/>
</form-col>
<form-col :span="span" label="身份证反面" prop="idCardBack" label-width="6em">
<image-upload v-model="form.idCardBack" :limit="1" :file-size="10" :is-show-tip="false"/>
</form-col>
<form-col :span="span" label="简历" prop="resume">
<image-upload v-model="form.resume" :limit="1" :file-size="10" :is-show-tip="false"/>
</form-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
</template>
<script>
import { addUser, getUser, updateUser } from '@/api/system/user'
import FormCol from '@/components/FormCol/index.vue'
import { parseTime } from '@/utils/ruoyi'
import DeptSelect from '@/components/Business/Dept/DeptSelect.vue'
export default {
name: "UserFormDialog",
dicts: ['user_status', 'sys_user_sex'],
components: { FormCol, DeptSelect },
props: {
visible: {
type: Boolean,
default: false
},
// ID
userId: {
type: [Number, String],
default: null
}
},
data() {
return {
span: 12,
//
deptOptions: [],
//
postOptions: [],
//
roleOptions: [],
//
form: {},
//
initPassword: "",
//
rules: {
userNo: [
{ required: true, message: "工号不能为空", trigger: "blur" }
],
userName: [
{ required: true, message: "登录账号不能为空", trigger: "blur" },
{ min: 2, max: 20, message: '登录账号长度必须介于 2 和 20 之间', trigger: 'blur' }
],
phonenumber: [
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur"}
],
deptId: [
{ required: true, message: "归属部门不能为空", trigger: "blur" }
],
nickName: [
{ required: true, message: "姓名不能为空", trigger: "blur" }
],
password: [
{ required: true, message: "用户密码不能为空", trigger: "blur" },
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
],
email: [
{type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"]}
],
},
loading: false
};
},
computed: {
title() {
return this.form.userId ? "修改用户" : "添加用户";
},
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
}
},
created() {
this.getConfigKey("sys.user.initPassword").then(response => {
this.initPassword = response.msg;
});
},
methods: {
handleOpen() {
this.getUser();
},
/** 获取用户详情 */
getUser() {
this.loading = true;
getUser(this.userId).then(response => {
if (this.userId) {
this.form = response.data;
this.postOptions = response.posts;
this.roleOptions = response.roles;
this.$set(this.form, "postIds", response.postIds);
this.$set(this.form, "roleIds", response.roleIds);
this.form.password = "";
} else {
this.postOptions = response.posts;
this.roleOptions = response.roles;
this.reset();
this.form.password = this.initPassword;
}
}).finally(() => {
this.loading = false;
});
},
//
cancel() {
this.dialogVisible = false;
},
//
reset() {
this.form = {
userId: null,
deptId: null,
userName: null,
nickName: null,
password: null,
phonenumber: null,
email: null,
sex: '2',
status: "0",
remark: null,
birthday: null,
employDate: parseTime(new Date(),"{y}-{m}-{d}"),
resignDate: null,
postIds: [],
roleIds: [],
idCardFront: null,
idCardBack: null,
resume: null
};
this.resetForm("form");
},
/** 提交按钮 */
submitForm: function() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.userId != null) {
this.loading = true;
updateUser(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.dialogVisible = false;
this.$emit('success');
}).finally(() => {
this.loading = false;
});
} else {
this.loading = true;
addUser(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.dialogVisible = false;
this.$emit('success');
}).finally(() => {
this.loading = false;
});
}
}
});
},
//
setDeptOptions(options) {
this.deptOptions = options;
}
}
};
</script>

View File

@ -1,282 +1,195 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<!--部门数据-->
<el-col :span="4" :xs="24">
<div class="head-container">
<el-input
v-model="deptName"
placeholder="请输入部门名称"
clearable
size="small"
prefix-icon="el-icon-search"
style="margin-bottom: 20px"
/>
</div>
<div class="head-container">
<el-tree
:data="deptOptions"
:props="defaultProps"
:expand-on-click-node="false"
:filter-node-method="filterNode"
ref="tree"
node-key="id"
default-expand-all
highlight-current
@node-click="handleNodeClick"
/>
</div>
</el-col>
<!--用户数据-->
<el-col :span="20" :xs="24">
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="姓名" prop="nickName">
<el-input
v-model="queryParams.nickName"
placeholder="请输入姓名"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="登录账号" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入登录账号"
clearable
style="width: 240px"
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="用户状态"
clearable
style="width: 240px"
@change="handleQuery"
>
<el-option
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
style="width: 240px"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleQuery"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:user:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:user:remove']"
>删除</el-button>
</el-col>
<!-- <el-col :span="1.5">-->
<!-- <el-button-->
<!-- type="info"-->
<!-- plain-->
<!-- icon="el-icon-upload2"-->
<!-- size="mini"-->
<!-- @click="handleImport"-->
<!-- v-hasPermi="['system:user:import']"-->
<!-- >导入</el-button>-->
<!-- </el-col>-->
<!-- <el-col :span="1.5">-->
<!-- <el-button-->
<!-- type="warning"-->
<!-- plain-->
<!-- icon="el-icon-download"-->
<!-- size="mini"-->
<!-- @click="handleExport"-->
<!-- v-hasPermi="['system:user:export']"-->
<!-- >导出</el-button>-->
<!-- </el-col>-->
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<template v-for="column of showColumns">
<el-table-column
:key="column.key"
:label="column.label"
:prop="column.key"
:align="column.align"
:min-width="column.minWidth"
:sort-orders="orderSorts"
:sortable="column.sortable"
:show-overflow-tooltip="column.overflow"
:width="column.width"
>
<template slot-scope="d">
<template v-if="column.key === 'userId'">
{{d.row[column.key]}}
</template>
<template v-else-if="column.key === 'deptId'">
{{d.row.dept == null ? '' : d.row.dept.deptName}}
</template>
<template v-else-if="column.key === 'status'">
<dict-tag :value="d.row.status" :options="dict.type.sys_normal_disable"/>
</template>
<template v-else-if="column.key === 'employStatus'">
<dict-tag :value="d.row.employStatus" :options="dict.type.user_employ_status"/>
</template>
<template v-else-if="column.key === 'roles'">
<template v-for="(role, index) in d.row.roles">
<el-tag :key="index" v-if="!isEmpty(role.roleName)">{{role.roleName}}</el-tag>
</template>
</template>
<template v-else>
{{d.row[column.key] | dv}}
</template>
</template>
</el-table-column>
</template>
<el-table-column
label="操作"
align="center"
width="160"
class-name="small-padding fixed-width"
>
<template slot-scope="scope" v-if="scope.row.userId !== '1'">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
v-hasPermi="['system:user:query']"
>详情</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:user:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:user:remove']"
>删除</el-button>
<el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)" v-hasPermi="['system:user:resetPwd', 'system:user:edit']">
<el-button size="mini" type="text" icon="el-icon-d-arrow-right">更多</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="handleResetPwd" icon="el-icon-key"
v-hasPermi="['system:user:resetPwd']">重置密码</el-dropdown-item>
<el-dropdown-item command="handleAuthRole" icon="el-icon-circle-check"
v-hasPermi="['system:user:edit']">分配角色</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
<el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
<el-form-item label="姓名" prop="nickName">
<el-input
v-model="queryParams.nickName"
placeholder="请输入姓名"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="登录账号" prop="userName">
<el-input
v-model="queryParams.userName"
placeholder="请输入登录账号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="归属部门" prop="deptId">
<dept-select
v-model="queryParams.deptId"
placeholder="请选择归属部门"
clearable
@change="handleQuery"
check-strictly
/>
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber">
<el-input
v-model="queryParams.phonenumber"
placeholder="请输入手机号码"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="queryParams.email"
placeholder="请输入邮箱"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="用户状态"
clearable
@change="handleQuery"
>
<el-option
v-for="dict in dict.type.user_status"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="dateRange"
value-format="yyyy-MM-dd"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
@change="handleQuery"
></el-date-picker>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
<el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button
type="primary"
plain
icon="el-icon-plus"
size="mini"
@click="handleAdd"
v-hasPermi="['system:user:add']"
>新增</el-button>
</el-col>
<el-col :span="1.5">
<el-button
type="danger"
plain
icon="el-icon-delete"
size="mini"
:disabled="multiple"
@click="handleDelete"
v-hasPermi="['system:user:remove']"
>删除</el-button>
</el-col>
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList" :columns="columns"></right-toolbar>
</el-row>
<!-- 添加或修改用户配置对话框 -->
<el-dialog :title="title" :visible.sync="open" width="700px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-row>
<form-col :span="24" label="归属部门" prop="deptId">
<treeselect v-model="form.deptId" :options="deptOptions" :show-count="true" placeholder="请选择归属部门" />
</form-col>
<form-col :span="12" label="姓名" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入姓名" maxlength="30" />
</form-col>
<form-col :span="12" label="角色">
<el-select v-model="form.roleIds" multiple placeholder="请选择角色" style="width: 100%">
<el-option
v-for="item in roleOptions"
:key="item.roleId"
:label="item.roleName"
:value="item.roleId"
:disabled="item.status == 1"
></el-option>
</el-select>
</form-col>
<form-col :span="12" label="登录账号" prop="userName">
<el-input v-model="form.userName" placeholder="请输入登录账号" maxlength="30" />
</form-col>
<form-col :span="12" label="登录密码" prop="password" v-if="form.userId == null">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password/>
</form-col>
<form-col :span="12" label="账号状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
v-for="dict in dict.type.sys_normal_disable"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</form-col>
<form-col :span="12" label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</form-col>
<form-col :span="12" label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</form-col>
<form-col :span="12" label="入职日期" prop="birthday">
<el-date-picker style="width: 100%" clearable v-model="form.employDate" type="date" value-format="yyyy-MM-dd" placeholder="选择日期"/>
</form-col>
<form-col :span="12" label="用户性别">
<el-radio-group v-model="form.sex">
<el-radio
v-for="dict in dict.type.sys_user_sex"
:key="dict.value"
:label="dict.value"
>{{dict.label}}</el-radio>
</el-radio-group>
</form-col>
</el-row>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"> </el-button>
<el-button @click="cancel"> </el-button>
</div>
</el-dialog>
<el-table v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="50" align="center" />
<template v-for="column of showColumns">
<el-table-column
:key="column.key"
:label="column.label"
:prop="column.key"
:align="column.align"
:min-width="column.minWidth"
:sort-orders="orderSorts"
:sortable="column.sortable"
:show-overflow-tooltip="column.overflow"
:width="column.width"
>
<template slot-scope="d">
<template v-if="column.key === 'userId'">
{{d.row[column.key]}}
</template>
<template v-else-if="column.key === 'deptId'">
{{d.row.dept == null ? '' : d.row.dept.deptName}}
</template>
<template v-else-if="column.key === 'status'">
<dict-tag :value="d.row.status" :options="dict.type.user_status" size="small"/>
</template>
<template v-else-if="column.key === 'roles'">
<template v-for="(role, index) in d.row.roles">
<el-tag :key="index" v-if="!isEmpty(role.roleName)" size="small" style="margin-right: 4px;">{{role.roleName}}</el-tag>
</template>
</template>
<template v-else>
{{d.row[column.key]}}
</template>
</template>
</el-table-column>
</template>
<el-table-column
label="操作"
align="center"
width="160"
class-name="small-padding fixed-width"
>
<template slot-scope="scope" v-if="scope.row.userId !== '1'">
<el-button
size="mini"
type="text"
icon="el-icon-view"
@click="handleView(scope.row)"
v-hasPermi="['system:user:query']"
>详情</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-edit"
@click="handleUpdate(scope.row)"
v-hasPermi="['system:user:edit']"
>修改</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-delete"
@click="handleDelete(scope.row)"
v-hasPermi="['system:user:remove']"
>删除</el-button>
<el-dropdown size="mini" @command="(command) => handleCommand(command, scope.row)" v-hasPermi="['system:user:resetPwd', 'system:user:edit']">
<el-button size="mini" type="text" icon="el-icon-d-arrow-right">更多</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="handleResetPwd" icon="el-icon-key"
v-hasPermi="['system:user:resetPwd']">重置密码</el-dropdown-item>
<el-dropdown-item command="handleAuthRole" icon="el-icon-circle-check"
v-hasPermi="['system:user:edit']">分配角色</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<pagination
v-show="total>0"
:total="total"
:page.sync="queryParams.pageNum"
:limit.sync="queryParams.pageSize"
@pagination="getList"
/>
<!-- 用户表单对话框组件 -->
<user-form-dialog
:visible.sync="open"
:userId="userId"
@success="getList"
ref="userFormDialog"
/>
<!-- 用户导入对话框 -->
<import-dialog
@ -291,30 +204,26 @@
<script>
import {
addUser,
changeUserStatus,
delUser,
deptTreeSelect,
getUser,
listUser,
resetUserPwd,
updateUser
resetUserPwd
} from '@/api/system/user'
import { getToken } from '@/utils/auth'
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
import FormCol from '@/components/FormCol/index.vue'
import { $showColumns } from '@/utils/mixins'
import { calcBirthDay, calcFullYear } from '@/utils/date'
import { parseTime } from '@/utils/ruoyi'
import ImportDialog from '@/components/ImportDialog/index.vue'
import { isEmpty } from '@/utils/index'
import UserFormDialog from './components/UserFormDialog'
import DeptSelect from '@/components/Business/Dept/DeptSelect.vue'
export default {
name: "User",
mixins: [$showColumns],
dicts: ['sys_normal_disable', 'sys_user_sex', 'user_employ_status'],
components: {ImportDialog, FormCol, Treeselect },
dicts: ['user_status', 'sys_user_sex', 'user_employ_status'],
components: {UserFormDialog, ImportDialog, FormCol, DeptSelect },
data() {
return {
//
@ -333,24 +242,16 @@ export default {
total: 0,
//
userList: null,
//
title: "",
//
deptOptions: undefined,
//
open: false,
// ID
userId: null,
//
deptName: undefined,
//
initPassword: undefined,
//
dateRange: [],
//
postOptions: [],
//
roleOptions: [],
//
form: {},
//
deptOptions: undefined,
defaultProps: {
children: "children",
label: "label"
@ -377,10 +278,10 @@ export default {
orderByColumn: "createTime",
isAsc: "desc",
excludeUserId: 1,
userName: undefined,
phonenumber: undefined,
status: undefined,
deptId: undefined
userName: null,
phonenumber: null,
status: null,
deptId: null
},
//
columns: [
@ -389,38 +290,15 @@ export default {
{key: 'userName', visible: true, label: '登录账号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'deptName', visible: true, label: '归属部门', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'roles', visible: true, label: '角色', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'status', visible: true, label: '账号状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'loginIp', visible: true, label: '最后登录IP', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'loginDate', visible: true, label: '最后登录时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'createTime', visible: true, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
],
//
rules: {
userNo: [
{ required: true, message: "工号不能为空", trigger: "blur" }
],
userName: [
{ required: true, message: "登录账号不能为空", trigger: "blur" },
{ min: 2, max: 20, message: '登录账号长度必须介于 2 和 20 之间', trigger: 'blur' }
],
phonenumber: [
{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur"}
],
deptId: [
{ required: true, message: "归属部门不能为空", trigger: "blur" }
],
nickName: [
{ required: true, message: "姓名不能为空", trigger: "blur" }
],
password: [
{ required: true, message: "用户密码不能为空", trigger: "blur" },
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' },
{ pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }
],
email: [
{type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"]}
],
}
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'phonenumber', visible: true, label: '手机号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'email', visible: true, label: '邮箱', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'loginIp', visible: true, label: '登录IP', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'loginDate', visible: true, label: '登录时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'createTime', visible: true, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'employDate', visible: true, label: '入职日期', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'resignDate', visible: true, label: '离职日期', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
]
};
},
watch: {
@ -432,9 +310,6 @@ export default {
created() {
this.getList();
this.getDeptTree();
this.getConfigKey("sys.user.initPassword").then(response => {
this.initPassword = response.msg;
});
},
methods: {
handleView(row) {
@ -480,32 +355,6 @@ export default {
row.status = row.status === "0" ? "1" : "0";
});
},
//
cancel() {
this.open = false;
this.reset();
},
//
reset() {
this.form = {
userId: null,
deptId: undefined,
userName: undefined,
nickName: undefined,
password: undefined,
phonenumber: undefined,
email: undefined,
sex: '2',
status: "0",
employStatus: '1',
remark: undefined,
birthday: null,
employDate: parseTime(new Date(),"{y}-{m}-{d}"),
postIds: [],
roleIds: []
};
this.resetForm("form");
},
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
@ -540,29 +389,13 @@ export default {
},
/** 新增按钮操作 */
handleAdd() {
this.reset();
getUser().then(response => {
this.postOptions = response.posts;
this.roleOptions = response.roles;
this.open = true;
this.title = "添加用户";
this.form.password = this.initPassword;
});
this.userId = null;
this.open = true;
},
/** 修改按钮操作 */
handleUpdate(row) {
this.reset();
const userId = row.userId || this.ids;
getUser(userId).then(response => {
this.form = response.data;
this.postOptions = response.posts;
this.roleOptions = response.roles;
this.$set(this.form, "postIds", response.postIds);
this.$set(this.form, "roleIds", response.roleIds);
this.open = true;
this.title = "修改用户";
this.form.password = "";
});
this.userId = row.userId || this.ids;
this.open = true;
},
/** 重置密码按钮操作 */
handleResetPwd(row) {
@ -588,26 +421,6 @@ export default {
const userId = row.userId;
this.$router.push("/system/user-auth/role/" + userId);
},
/** 提交按钮 */
submitForm: function() {
this.$refs["form"].validate(valid => {
if (valid) {
if (this.form.userId != undefined) {
updateUser(this.form).then(response => {
this.$modal.msgSuccess("修改成功");
this.open = false;
this.getList();
});
} else {
addUser(this.form).then(response => {
this.$modal.msgSuccess("新增成功");
this.open = false;
this.getList();
});
}
}
});
},
/** 删除按钮操作 */
handleDelete(row) {
const userIds = row.userId || this.ids;
@ -618,37 +431,10 @@ export default {
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 导出按钮操作 */
handleExport() {
this.download('system/user/export', {
...this.queryParams
}, `user_${new Date().getTime()}.xlsx`)
},
/** 导入按钮操作 */
handleImport() {
this.upload.title = "用户导入";
this.upload.open = true;
},
/** 下载模板操作 */
importTemplate() {
this.download('system/user/importTemplate', {
}, `user_template_${new Date().getTime()}.xlsx`)
},
//
handleFileUploadProgress(event, file, fileList) {
this.upload.isUploading = true;
},
//
handleFileSuccess(response, file, fileList) {
this.upload.open = false;
this.upload.isUploading = false;
this.$refs.upload.clearFiles();
this.$alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + "</div>", "导入结果", { dangerouslyUseHTMLString: true });
this.getList();
},
//
submitFileForm() {
this.$refs.upload.submit();
}
}
};

View File

@ -12,12 +12,19 @@
<div class="name">{{ detail.nickName | dv}}</div>
</div>
<div>
<line-field label="手机号" :value="detail.phonenumber" />
<line-field label="所属部门" :value="detail.deptName" />
<line-field label="账号" :value="detail.userName" />
<line-field label="入职时间" :value="detail.employDate" />
<line-field label="最后登录IP" :value="detail.loginIp" />
<line-field label="最后登录时间" :value="detail.loginDate" />
<line-field label="所属部门" :value="detail.deptName" />
<line-field label="角色">
{{ detail.roles.map(role => role.roleName).join(',') }}
</line-field>
<line-field label="手机号" :value="detail.phonenumber" />
<line-field label="邮箱" :value="detail.email" />
<collapse-panel title="更多信息" :value="false">
<line-field label="入职时间" :value="detail.employDate" />
<line-field label="离职时间" :value="detail.resignDate" />
<line-field label="最后登录IP" :value="detail.loginIp" />
<line-field label="最后登录时间" :value="detail.loginDate" />
</collapse-panel>
</div>
</el-card>
</el-col>
@ -60,6 +67,7 @@ import Project from '@/views/bst/project/index.vue'
import Task from '@/views/bst/task/index.vue'
import { checkPermi } from '@/utils/permission'
import UserStatistics from '@/views/system/user/view/components/UserStatistics'
import CollapsePanel from '@/components/CollapsePanel'
export default {
name: 'UserView',
components: {
@ -68,7 +76,8 @@ export default {
Avatar,
Project,
Task,
UserStatistics
UserStatistics,
CollapsePanel
},
data() {
return {