Compare commits

..

9 Commits

Author SHA1 Message Date
wang-liang0615
f194d26450 env 2024-11-07 11:52:19 +08:00
wang-liang0615
093098bbec feat:add employee work_region 2024-11-07 11:46:29 +08:00
Zhuohao Li
28dca7f086 fix permission bug (#632)
不同的appid下可能有相同的resource type name.
2024-10-27 14:04:34 +08:00
pycook
4a3c21eec4 feat(api): add builtin attributes (#631) 2024-10-22 18:21:07 +08:00
Leo Song
5d28c28023 Merge pull request #630 from veops/dev_ui_241022
fix(ui): update userPanel style
2024-10-22 14:12:14 +08:00
songlh
ba6edb3abe fix(ui): update userPanel style 2024-10-22 14:11:36 +08:00
Leo Song
2f1d57cee1 Merge pull request #629 from veops/dev_ui_241022
feat(ui): add userPanel component
2024-10-22 14:01:02 +08:00
songlh
4111c634d9 feat(ui): add userPanel component 2024-10-22 13:59:38 +08:00
pycook
f1fababa3d feat(api): save relation search option 2024-10-18 11:03:31 +08:00
24 changed files with 1338 additions and 660 deletions

View File

@@ -1,14 +1,13 @@
# -*- coding:utf-8 -*-
import click
import copy
import datetime
import json
import requests
import time
import uuid
import click
import requests
from flask import current_app
from flask.cli import with_appcontext
from flask_login import login_user
@@ -37,11 +36,14 @@ from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import App
from api.models.acl import ResourceType
from api.models.cmdb import Attribute
from api.models.cmdb import AttributeHistory
from api.models.cmdb import CI
from api.models.cmdb import CIRelation
from api.models.cmdb import CIType
from api.models.cmdb import CITypeTrigger
from api.models.cmdb import OperationRecord
from api.models.cmdb import PreferenceRelationView
from api.tasks.cmdb import batch_ci_cache
@click.command()
@@ -557,5 +559,20 @@ def cmdb_patch(version):
existed.update(option=option, commit=False)
db.session.commit()
if version >= "2.4.14": # update ci columns: updated_at and updated_by
ci_ids = []
for i in CI.get_by(only_query=True).filter(CI.updated_at.is_(None)):
hist = AttributeHistory.get_by(ci_id=i.id, only_query=True).order_by(AttributeHistory.id.desc()).first()
if hist is not None:
record = OperationRecord.get_by_id(hist.record_id)
if record is not None:
u = UserCache.get(record.uid)
i.update(updated_at=record.created_at, updated_by=u and u.nickname, flush=True)
ci_ids.append(i.id)
db.session.commit()
batch_ci_cache.apply_async(args=(ci_ids,))
except Exception as e:
print("cmdb patch failed: {}".format(e))

View File

@@ -45,6 +45,7 @@ from api.lib.notify import notify_send
from api.lib.perm.acl.acl import ACLManager
from api.lib.perm.acl.acl import is_app_admin
from api.lib.perm.acl.acl import validate_permission
from api.lib.perm.acl.cache import UserCache
from api.lib.secrets.inner import InnerCrypt
from api.lib.secrets.vault import VaultClient
from api.lib.utils import handle_arg_list
@@ -206,6 +207,8 @@ class CIManager(object):
res['_type'] = ci_type.id
res['ci_type_alias'] = ci_type.alias
res['_id'] = ci_id
res['_updated_at'] = str(ci.updated_at)
res['_updated_by'] = ci.updated_by
return res
@@ -581,6 +584,9 @@ class CIManager(object):
else:
ci_relation_add(ref_ci_dict, ci.id)
u = UserCache.get(current_user.uid)
ci.update(updated_at=now, updated_by=u and u.nickname)
@staticmethod
def update_unique_value(ci_id, unique_name, unique_value):
ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format("id={}".format(ci_id)))

View File

@@ -1,6 +1,8 @@
# -*- coding:utf-8 -*-
from flask_babel import lazy_gettext as _l
from api.lib.utils import BaseEnum
@@ -110,17 +112,23 @@ class ExecuteStatusEnum(BaseEnum):
FAILED = '1'
RUNNING = '2'
class RelationSourceEnum(BaseEnum):
ATTRIBUTE_VALUES = "0"
AUTO_DISCOVERY = "1"
BUILTIN_ATTRIBUTES = {
"_updated_at": _l("Update Time"),
"_updated_by": _l("Updated By"),
}
CMDB_QUEUE = "one_cmdb_async"
REDIS_PREFIX_CI = "ONE_CMDB"
REDIS_PREFIX_CI_RELATION = "CMDB_CI_RELATION"
REDIS_PREFIX_CI_RELATION2 = "CMDB_CI_RELATION2"
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id'}
BUILTIN_KEYWORDS = {'id', '_id', 'ci_id', 'type', '_type', 'ci_type', 'ticket_id', *BUILTIN_ATTRIBUTES.keys()}
L_TYPE = None
L_CI = None

View File

@@ -1,6 +1,5 @@
# -*- coding:utf-8 -*-
from collections import defaultdict
import copy
@@ -17,6 +16,7 @@ from api.lib.cmdb.cache import CITypeAttributesCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.cache import CMDBCounterCache
from api.lib.cmdb.ci_type import CITypeAttributeManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import ConstraintEnum
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
@@ -25,7 +25,6 @@ from api.lib.cmdb.perms import CIFilterPermsCRUD
from api.lib.cmdb.resp_format import ErrFormat
from api.lib.exception import AbortException
from api.lib.perm.acl.acl import ACLManager
from api.models.cmdb import CITypeAttribute
from api.models.cmdb import CITypeGroup
from api.models.cmdb import CITypeGroupItem
from api.models.cmdb import CITypeRelation
@@ -137,17 +136,24 @@ class PreferenceManager(object):
_type = CITypeCache.get(type_id)
type_id = _type and _type.id
attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
PreferenceShowAttributes.uid == current_user.uid).filter(
PreferenceShowAttributes.type_id == type_id).filter(
PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
CITypeAttribute.attr_id).all()
# attrs = db.session.query(PreferenceShowAttributes, CITypeAttribute.order).join(
# CITypeAttribute, CITypeAttribute.attr_id == PreferenceShowAttributes.attr_id).filter(
# PreferenceShowAttributes.uid == current_user.uid).filter(
# PreferenceShowAttributes.type_id == type_id).filter(
# PreferenceShowAttributes.deleted.is_(False)).filter(CITypeAttribute.deleted.is_(False)).group_by(
# CITypeAttribute.attr_id).all()
attrs = PreferenceShowAttributes.get_by(uid=current_user.uid, type_id=type_id, to_dict=False)
result = []
for i in sorted(attrs, key=lambda x: x.PreferenceShowAttributes.order):
item = i.PreferenceShowAttributes.attr.to_dict()
item.update(dict(is_fixed=i.PreferenceShowAttributes.is_fixed))
for i in sorted(attrs, key=lambda x: x.order):
if i.attr_id:
item = i.attr.to_dict()
elif i.builtin_attr:
item = dict(name=i.builtin_attr, alias=BUILTIN_ATTRIBUTES[i.builtin_attr])
else:
item = dict(name="", alias="")
item.update(dict(is_fixed=i.is_fixed))
result.append(item)
is_subscribed = True
@@ -156,10 +162,14 @@ class PreferenceManager(object):
choice_web_hook_parse=False,
choice_other_parse=False)
result = [i for i in result if i['default_show']]
for i in BUILTIN_ATTRIBUTES:
result.append(dict(name=i, alias=BUILTIN_ATTRIBUTES[i]))
is_subscribed = False
for i in result:
if i["is_choice"]:
if i.get("is_choice"):
i.update(dict(choice_value=AttributeManager.get_choice_values(
i["id"], i["value_type"], i.get("choice_web_hook"), i.get("choice_other"))))
@@ -173,24 +183,34 @@ class PreferenceManager(object):
_attr, is_fixed = x
else:
_attr, is_fixed = x, False
attr = AttributeCache.get(_attr) or abort(404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
if _attr in BUILTIN_ATTRIBUTES:
attr = None
builtin_attr = _attr
else:
attr = AttributeCache.get(_attr) or abort(
404, ErrFormat.attribute_not_found.format("id={}".format(_attr)))
builtin_attr = None
existed = PreferenceShowAttributes.get_by(type_id=type_id,
uid=current_user.uid,
attr_id=attr.id,
attr_id=attr and attr.id,
builtin_attr=builtin_attr,
first=True,
to_dict=False)
if existed is None:
PreferenceShowAttributes.create(type_id=type_id,
uid=current_user.uid,
attr_id=attr.id,
attr_id=attr and attr.id,
builtin_attr=builtin_attr,
order=order,
is_fixed=is_fixed)
else:
existed.update(order=order, is_fixed=is_fixed)
attr_dict = {int(i[0]) if isinstance(i, list) else int(i): j for i, j in attr_order}
attr_dict = {(int(i[0]) if i[0].isdigit() else i[0]) if isinstance(i, list) else
(int(i) if i.isdigit() else i): j for i, j in attr_order}
for i in existed_all:
if i.attr_id not in attr_dict:
if (i.attr_id and i.attr_id not in attr_dict) or (i.builtin_attr and i.builtin_attr not in attr_dict):
i.soft_delete()
if not existed_all and attr_order:
@@ -384,7 +404,7 @@ class PreferenceManager(object):
def add_search_option(**kwargs):
kwargs['uid'] = current_user.uid
if kwargs['name'] in ('__recent__', '__favor__'):
if kwargs['name'] in ('__recent__', '__favor__', '__relation_favor__'):
if kwargs['name'] == '__recent__':
for i in PreferenceSearchOption.get_by(
only_query=True, name=kwargs['name'], uid=current_user.uid).order_by(

View File

@@ -15,6 +15,7 @@ from api.extensions import db
from api.lib.cmdb.cache import AttributeCache
from api.lib.cmdb.cache import CITypeCache
from api.lib.cmdb.ci import CIManager
from api.lib.cmdb.const import BUILTIN_ATTRIBUTES
from api.lib.cmdb.const import PermEnum
from api.lib.cmdb.const import ResourceTypeEnum
from api.lib.cmdb.const import RetKey
@@ -304,14 +305,21 @@ class Search(object):
(self.page - 1) * self.count, sort_type, self.count))
def __sort_by_field(self, field, sort_type, query_sql):
attr = AttributeCache.get(field)
attr_id = attr.id
if field not in BUILTIN_ATTRIBUTES:
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT {0}.ci_id, {1}.value
FROM ({2}) AS {0} INNER JOIN {1} ON {1}.ci_id = {0}.ci_id
WHERE {1}.attr_id = {3}""".format("ALIAS", table_name, query_sql, attr_id)
new_table = _v_query_sql
attr = AttributeCache.get(field)
attr_id = attr.id
table_name = TableMap(attr=attr).table_name
_v_query_sql = """SELECT ALIAS.ci_id, {0}.value
FROM ({1}) AS ALIAS INNER JOIN {0} ON {0}.ci_id = ALIAS.ci_id
WHERE {0}.attr_id = {2}""".format(table_name, query_sql, attr_id)
new_table = _v_query_sql
else:
_v_query_sql = """SELECT c_cis.id AS ci_id, c_cis.{0} AS value
FROM c_cis INNER JOIN ({1}) AS ALIAS ON ALIAS.ci_id = c_cis.id""".format(
field[1:], query_sql)
new_table = _v_query_sql
if self.only_type_query or not self.type_id_list or self.multi_type_has_ci_filter:
return ("SELECT SQL_CALC_FOUND_ROWS DISTINCT C.ci_id FROM ({0}) AS C ORDER BY C.value {2} "

View File

@@ -71,7 +71,7 @@ class PermissionCRUD(object):
@classmethod
def get_all2(cls, resource_name, resource_type_name, app_id):
rt = ResourceType.get_by(name=resource_type_name, first=True, to_dict=False)
rt = ResourceType.get_by(name=resource_type_name, app_id=app_id, first=True, to_dict=False)
rt or abort(404, ErrFormat.resource_type_not_found.format(resource_type_name))
r = Resource.get_by(name=resource_name, resource_type_id=rt.id, app_id=app_id, first=True, to_dict=False)

View File

@@ -253,6 +253,7 @@ class CI(Model):
status = db.Column(db.Enum(*CIStatusEnum.all(), name="status"))
heartbeat = db.Column(db.DateTime, default=lambda: datetime.datetime.now())
is_auto_discovery = db.Column('a', db.Boolean, default=False)
updated_by = db.Column(db.String(64))
ci_type = db.relationship("CIType", backref="c_cis.type_id")
@@ -534,6 +535,7 @@ class CustomDashboard(Model):
type_id = db.Column(db.Integer, db.ForeignKey('c_ci_types.id'))
attr_id = db.Column(db.Integer, db.ForeignKey('c_attributes.id'))
builtin_attr = db.Column(db.String(256), nullable=True)
level = db.Column(db.Integer)
options = db.Column(db.JSON)

View File

@@ -11,38 +11,11 @@
<span class="common-settings-btn-text">{{ $t('settings') }}</span>
</span>
<a-popover
overlayClassName="lang-popover-wrap"
placement="bottomRight"
:getPopupContainer="(trigger) => trigger.parentNode"
>
<span class="locale">{{ languageList.find((lang) => lang.key === locale).title }}</span>
<div class="lang-menu" slot="content">
<a
v-for="(lang) in languageList"
:key="lang.key"
:class="['lang-menu-item', lang.key === locale ? 'lang-menu-item_active' : '']"
@click="changeLang(lang.key)"
>
{{ lang.title }}
</a>
</div>
</a-popover>
<a-popover
:overlayStyle="{ width: '130px' }"
placement="bottomRight"
overlayClassName="custom-user"
>
<template slot="content">
<router-link :to="{ name: 'setting_person' }" :style="{ color: '#000000a6' }">
<div class="custom-user-item">
<a-icon type="user" :style="{ marginRight: '10px' }" />
<span>{{ $t('topMenu.personalCenter') }}</span>
</div>
</router-link>
<div @click="handleLogout" class="custom-user-item">
<a-icon type="logout" :style="{ marginRight: '10px' }" />
<span>{{ $t('topMenu.logout') }}</span>
</div>
<UserPanel />
</template>
<span class="action ant-dropdown-link user-dropdown-menu user-info-wrap">
<a-avatar
@@ -63,11 +36,13 @@
import { mapState, mapActions, mapGetters, mapMutations } from 'vuex'
import DocumentLink from './DocumentLink.vue'
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import UserPanel from './userPanel.vue'
export default {
name: 'UserMenu',
components: {
DocumentLink,
UserPanel
},
data() {
return {

View File

@@ -0,0 +1,487 @@
<template>
<div class="user-panel">
<a-avatar
class="user-panel-avatar"
size="small"
icon="user"
:src="avatarSrc"
/>
<div class="user-panel-nickname">
{{ userInfo.nickname }}
</div>
<div class="user-panel-info">
<ops-icon
type="veops-company"
class="user-panel-info-icon"
/>
<div class="user-panel-info-text">
{{ companyName }}
</div>
</div>
<div class="user-panel-info">
<ops-icon
type="veops-emails"
class="user-panel-info-icon"
/>
<div class="user-panel-info-text">
{{ email }}
</div>
</div>
<div class="user-panel-btn">
<div
v-for="(item) in userBtnGroup"
:key="item.type"
class="user-panel-btn-item"
@click="clickBtnGroup(item.type)"
>
<ops-icon
:type="item.icon"
class="user-panel-btn-icon"
/>
<span class="user-panel-btn-title">
{{ $t(item.title) }}
</span>
</div>
</div>
<div class="user-panel-row">
<div class="user-panel-row-label">
{{ $t('userPanel.switchLanguage') }}
</div>
<div class="user-panel-lang">
<div
v-for="(lang, index) in languageList"
:key="index"
:class="['user-panel-lang-item', lang.key === locale ? 'user-panel-lang-item_active' : '']"
@click="changeLang(lang.key)"
>
{{ lang.title }}
</div>
</div>
</div>
<div class="user-panel-row">
<div class="user-panel-row-label">
{{ $t('userPanel.bindAccount') }}
</div>
<div class="user-panel-bind">
<a-tooltip
v-for="(item) in bindList"
:key="item.type"
:title="$t(item.title)"
>
<ops-icon
class="user-panel-bind-item"
:type="userInfo.notice_info && userInfo.notice_info[item.type] ? item.existedIcon : item.icon"
@click="handleBindInfo(item.type)"
/>
</a-tooltip>
</div>
</div>
<div class="user-panel-account">
<div
v-for="(item, index) in accountActions"
:key="index"
class="user-panel-account-item"
@click="handleLogout"
>
<ops-icon class="user-panel-account-icon" :type="item.icon" />
<span class="user-panel-account-title">
{{ $t(item.title) }}
</span>
</div>
</div>
</div>
</template>
<script>
import { mapActions, mapState, mapMutations } from 'vuex'
import { setDocumentTitle, domTitle } from '@/utils/domUtil'
import {
bindPlatformByUid,
unbindPlatformByUid,
} from '@/api/employee'
import { getCompanyInfo } from '@/api/company'
export default {
name: 'UserPanel',
data() {
return {
userBtnGroup: [
{
icon: 'veops-personal',
title: 'userPanel.myProfile',
type: 'myProfile'
},
{
icon: 'a-veops-account1',
title: 'userPanel.accountPassword',
type: 'accountPassword'
}
],
languageList: [
{
title: '简中',
key: 'zh'
},
{
title: 'EN',
key: 'en'
},
],
bindList: [
{
type: 'wechatApp',
icon: 'qiyeweixin',
existedIcon: 'wechatApp',
title: 'wechat'
},
{
type: 'feishuApp',
icon: 'ops-setting-notice-feishu-selected',
existedIcon: 'feishuApp',
title: 'feishu'
},
{
type: 'dingdingApp',
icon: 'ops-setting-notice-dingding-selected',
existedIcon: 'dingdingApp',
title: 'dingding'
},
],
accountActions: [
{
icon: 'veops-switch',
title: 'userPanel.switchAccount'
},
{
icon: 'veops-sign_out',
title: 'userPanel.logout'
},
],
hoverBindAccountList: []
}
},
computed: {
...mapState({
email: (state) => state.user.email,
locale: (state) => state.locale,
userInfo: (state) => state.user,
companyName: (state) => state.company.name
}),
avatarSrc() {
const avatar = this.userInfo.avatar
if (!avatar) {
return null
}
return avatar.startsWith('https') ? avatar : `/api/common-setting/v1/file/${avatar}`
}
},
mounted() {
if (this.companyName === undefined) {
this.getCompanyInfo()
}
},
methods: {
...mapActions(['Logout', 'GetInfo']),
...mapMutations(['SET_LOCALE', 'SET_COMPANY_NAME']),
async getCompanyInfo() {
const res = await getCompanyInfo()
const name = res?.info?.name || ''
this.SET_COMPANY_NAME(name)
},
changeLang(lang) {
this.SET_LOCALE(lang)
this.$i18n.locale = lang
this.$nextTick(() => {
setDocumentTitle(`${this.$t(this.$route.meta.title)} - ${domTitle}`)
})
},
handleBindInfo(platform) {
const isBind = this?.userInfo?.notice_info?.[platform]
const uid = this?.userInfo?.uid
if (isBind) {
this.$confirm({
title: this.$t('warning'),
content: this.$t('cs.person.confirmUnbind'),
onOk: () => {
unbindPlatformByUid(platform, uid)
.then(() => {
this.$message.success(this.$t('cs.person.unbindSuccess'))
})
.finally(() => {
this.GetInfo()
})
},
})
} else {
bindPlatformByUid(platform, uid)
.then(() => {
this.$message.success(this.$t('cs.person.bindSuccess'))
})
.finally(() => {
this.GetInfo()
})
}
},
handleLogout() {
this.$confirm({
title: this.$t('tip'),
content: this.$t('topMenu.confirmLogout'),
onOk: () => {
this.Logout()
},
onCancel() {},
})
},
clickBtnGroup(type) {
switch (type) {
case 'myProfile':
if (this.$route.name === 'setting_person') {
this.$bus.$emit('changeSettingPersonCurrent', '1')
} else {
this.$router.push({
name: 'setting_person',
query: {
current: '1'
}
})
}
break
case 'accountPassword':
if (this.$route.name === 'setting_person') {
this.$bus.$emit('changeSettingPersonCurrent', '2')
} else {
this.$router.push({
name: 'setting_person',
query: {
current: '2'
}
})
}
break
default:
break
}
},
handleBindAccountMouse(type, isHover) {
const index = this.hoverBindAccountList.findIndex((item) => item === type)
if (isHover && index === -1) {
this.hoverBindAccountList.push(type)
} else if (!isHover && index !== -1) {
this.hoverBindAccountList.splice(index, 1)
}
}
}
}
</script>
<style lang="less" scoped>
.user-panel {
display: flex;
flex-direction: column;
align-items: center;
width: 350px;
padding: 0 20px;
&-avatar {
width: 62px;
height: 62px;
border-radius: 62px;
margin-top: 13px;
display: flex;
align-items: center;
justify-content: center;
color: #000000;
background-color: #FFFFFF;
font-size: 48px !important;
}
&-nickname {
color: #1D2129;
font-size: 15px;
font-weight: 700;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
margin-top: 8px;
}
&-info {
display: flex;
align-items: center;
column-gap: 6px;
margin-top: 6px;
max-width: 100%;
&-icon {
flex-shrink: 0;
font-size: 12px;
}
&-text {
font-size: 12px;
font-weight: 400;
color: #4E5969;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
text-wrap: nowrap;
}
}
&-btn {
width: 100%;
height: 72px;
display: flex;
align-items: center;
margin-top: 11px;
&-icon {
font-size: 22px;
color: #CACDD9;
}
&-title {
font-size: 14px;
font-weight: 400;
color: #1D2129;
margin-top: 8px;
}
&-item {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #F7F8FA;
cursor: pointer;
&:hover {
background-color: #EBEFF8;
.user-panel-btn-icon {
color: #2F54EB;
}
.user-panel-btn-title {
color: #2F54EB;
}
}
}
}
&-row {
width: 100%;
margin-top: 22px;
display: flex;
align-items: center;
justify-content: space-between;
&-label {
font-size: 14px;
font-weight: 400;
color: #4E5969;
}
}
&-lang {
display: flex;
align-items: center;
height: 28px;
width: 108px;
border-radius: 28px;
overflow: hidden;
&-item {
flex: 1;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background-color: #F7F8FA;
cursor: pointer;
&:first-child {
border-right: solid 1px #E4E7ED;
}
&_active {
background-color: #EBEFF8;
color: #2F54EB;
}
&:hover {
color: #2F54EB;
}
}
}
&-bind {
display: flex;
align-items: center;
column-gap: 22px;
&-item {
cursor: pointer;
font-size: 16px;
}
}
&-account {
margin-top: 22px;
padding-top: 13px;
padding-bottom: 20px;
border-top: solid 1px #F0F1F5;
display: flex;
align-items: center;
justify-content: space-evenly;
width: 100%;
&-icon {
font-size: 14px;
color: #CACDD9;
}
&-title {
font-size: 14px;
color: #86909C;
margin-left: 5px;
}
&-item {
display: flex;
align-items: center;
justify-content: center;
flex: 1;
cursor: pointer;
&:hover {
.user-panel-account-icon {
color: #2F54EB;
}
.user-panel-account-title {
color: #2F54EB;
}
}
}
}
}
</style>

View File

@@ -168,6 +168,15 @@ export default {
monetaryAmount: 'monetary amount',
custom: 'custom',
},
userPanel: {
myProfile: 'My Profile',
accountPassword: 'Password',
notice: 'Notice',
switchLanguage: 'Switch Language',
bindAccount: 'Bind Account',
switchAccount: 'Switch Account',
logout: 'Logout'
},
cmdb: cmdb_en,
cs: cs_en,
acl: acl_en,

View File

@@ -168,6 +168,15 @@ export default {
monetaryAmount: '货币金额',
custom: '自定义',
},
userPanel: {
myProfile: '个人中心',
accountPassword: '账号密码',
notice: '通知中心',
switchLanguage: '切换语言',
bindAccount: '绑定账号',
switchAccount: '切换账号',
logout: '退出账号'
},
cmdb: cmdb_zh,
cs: cs_zh,
acl: acl_zh,

View File

@@ -0,0 +1,11 @@
const company = {
state: {
name: undefined
},
mutations: {
SET_COMPANY_NAME: (state, name) => {
state.name = name
},
},
}
export default company

View File

@@ -46,7 +46,8 @@ const user = {
sex: '',
position_name: '',
direct_supervisor_id: null,
auth_enable: {}
auth_enable: {},
notice_info: {}
},
mutations: {
@@ -54,7 +55,7 @@ const user = {
state.token = token
},
SET_USER_INFO: (state, { name, welcome, avatar, roles, info, uid, rid, username, mobile, department_id, employee_id, email, nickname, sex, position_name, direct_supervisor_id, annual_leave }) => {
SET_USER_INFO: (state, { name, welcome, avatar, roles, info, uid, rid, username, mobile, department_id, employee_id, email, nickname, sex, position_name, direct_supervisor_id, annual_leave, notice_info }) => {
state.name = name
state.welcome = welcome
state.avatar = avatar
@@ -73,6 +74,7 @@ const user = {
state.position_name = position_name
state.direct_supervisor_id = direct_supervisor_id
state.annual_leave = annual_leave
state.notice_info = notice_info
},
LOAD_ALL_USERS: (state, users) => {
@@ -160,7 +162,8 @@ const user = {
sex: res.sex,
position_name: res.position_name,
direct_supervisor_id: res.direct_supervisor_id,
annual_leave: res.annual_leave
annual_leave: res.annual_leave,
notice_info: res.notice_info
})
})
}).catch(error => {

View File

@@ -7,6 +7,7 @@ import user from './global/user'
import routes from './global/routes'
import notice from './global/notice'
import getters from './global/getters'
import company from './global/company'
import appConfig from '@/config/app'
Vue.use(Vuex)
@@ -16,7 +17,8 @@ const store = new Vuex.Store({
app,
user,
routes,
notice
notice,
company
},
state: {
windowWidth: 800,

View File

@@ -860,20 +860,30 @@ body {
.vue-treeselect__control {
border-radius: 2px !important;
height: 32px;
border-color: #e4e7ed;
.vue-treeselect__value-container{
height: 30px;
}
.vue-treeselect__input-container{
display: flex;
align-items: center;
height: 30px;
}
}
.vue-treeselect__placeholder,
.vue-treeselect__single-value {
line-height: 32px !important;
line-height: 28px !important;
}
.vue-treeselect__input {
height: 32px !important;
line-height: 32px !important;
height: 28px !important;
line-height: 28px !important;
}
}
// vue-treeselect 多选样式
.ops-setting-treeselect.vue-treeselect--multi {
.vue-treeselect__control {
border-radius: 2px !important;
border-color: #e4e7ed;
}
}

View File

@@ -47,6 +47,7 @@
</template>
<script>
import { mapMutations } from 'vuex'
import { getCompanyInfo, postCompanyInfo, putCompanyInfo } from '@/api/company'
import SpanTitle from '../components/spanTitle.vue'
import { mixinPermissions } from '@/utils/mixin'
@@ -82,6 +83,7 @@ export default {
} else {
this.infoData = res.info
this.getId = res.id
this.SET_COMPANY_NAME(res?.info?.name || '')
}
},
computed: {
@@ -122,6 +124,7 @@ export default {
},
},
methods: {
...mapMutations(['SET_COMPANY_NAME']),
async onSubmit() {
this.$refs.infoData.validate(async (valid) => {
if (valid) {
@@ -130,6 +133,7 @@ export default {
} else {
await putCompanyInfo(this.getId, this.infoData)
}
this.SET_COMPANY_NAME(this.infoData.name || '')
this.$message.success(this.$t('saveSuccess'))
} else {
this.$message.warning(this.$t('cs.companyInfo.checkInputCorrect'))

View File

@@ -1,393 +0,0 @@
<template>
<a-modal
:visible="visible"
:title="$t('cs.companyStructure.batchImport')"
dialogClass="ops-modal setting-structure-upload"
:width="800"
@cancel="close"
>
<div class="setting-structure-upload-steps">
<div
:class="{ 'setting-structure-upload-step': true, selected: index + 1 <= currentStep }"
v-for="(step, index) in stepList"
:key="step.value"
>
<div :class="{ 'setting-structure-upload-step-icon': true }">
<ops-icon :type="step.icon" />
</div>
<span>{{ step.label }}</span>
</div>
</div>
<template v-if="currentStep === 1">
<a-upload :multiple="false" :customRequest="customRequest" accept=".xlsx" :showUploadList="false">
<a-button :style="{ marginBottom: '20px' }" type="primary"> <a-icon type="upload" />{{ $t('cs.companyStructure.selectFile') }}</a-button>
</a-upload>
<p><a @click="download">{{ $t('cs.companyStructure.clickDownloadImportTemplate') }}</a></p>
</template>
<div
:style="{
height: '60px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
whiteSpace: 'pre-wrap',
}"
v-if="currentStep === 3"
>
{{ $t('cs.companyStructure.importSuccess', { allCount: allCount })
}}<span :style="{ color: '#2362FB' }"> {{ allCount - errorCount }} </span>{{ $t('cs.companyStructure.count') }},
{{ $t('cs.companyStructure.importFailed') }}<span :style="{ color: '#D81E06' }"> {{ errorCount }} </span
>{{ $t('cs.companyStructure.count') }}
</div>
<vxe-table
v-if="currentStep === 2 || has_error"
ref="employeeTable"
stripe
:data="importData"
show-overflow
show-header-overflow
highlight-hover-row
size="small"
class="ops-stripe-table"
:max-height="400"
:column-config="{ resizable: true }"
>
<vxe-column field="email" :title="$t('cs.companyStructure.email')" min-width="120" fixed="left"></vxe-column>
<vxe-column field="username" :title="$t('cs.companyStructure.username')" min-width="80" ></vxe-column>
<vxe-column field="nickname" :title="$t('cs.companyStructure.nickname')" min-width="80"></vxe-column>
<vxe-column field="password" :title="$t('cs.companyStructure.password')" min-width="80"></vxe-column>
<vxe-column field="sex" :title="$t('cs.companyStructure.sex')" min-width="60"></vxe-column>
<vxe-column field="mobile" :title="$t('cs.companyStructure.mobile')" min-width="80"></vxe-column>
<vxe-column field="position_name" :title="$t('cs.companyStructure.positionName')" min-width="80"></vxe-column>
<vxe-column field="department_name" :title="$t('cs.companyStructure.departmentName')" min-width="80"></vxe-column>
<vxe-column v-if="has_error" field="err" :title="$t('cs.companyStructure.importFailedReason')" min-width="120" fixed="right">
<template #default="{ row }">
<span :style="{ color: '#D81E06' }">{{ row.err }}</span>
</template>
</vxe-column>
</vxe-table>
<a-space slot="footer">
<a-button size="small" type="primary" ghost @click="close">{{ $t('cancel') }}</a-button>
<a-button v-if="currentStep !== 1" size="small" type="primary" ghost @click="goPre">{{ $t('cs.companyStructure.prevStep') }}</a-button>
<a-button v-if="currentStep !== 3" size="small" type="primary" @click="goNext">{{ $t('cs.companyStructure.nextStep') }}</a-button>
<a-button v-else size="small" type="primary" @click="close">{{ $t('cs.companyStructure.done') }}</a-button>
</a-space>
</a-modal>
</template>
<script>
import { downloadExcel, excel2Array } from '@/utils/download'
import { importEmployee } from '@/api/employee'
export default {
name: 'BatchUpload',
data() {
const stepList = [
{
value: 1,
label: this.$t('cs.companyStructure.uploadFile'),
icon: 'icon-shidi-tianjia',
},
{
value: 2,
label: this.$t('cs.companyStructure.confirmData'),
icon: 'icon-shidi-yunshangchuan',
},
{
value: 3,
label: this.$t('cs.companyStructure.uploadDone'),
icon: 'icon-shidi-queren',
},
]
const common_importParamsList = [
'email',
'username',
'nickname',
'password',
'sex',
'mobile',
'position_name',
'department_name',
'entry_date',
'is_internship',
'leave_date',
'id_card',
'nation',
'id_place',
'party',
'household_registration_type',
'hometown',
'marry',
'max_degree',
'emergency_person',
'emergency_phone',
'bank_card_number',
'bank_card_name',
'opening_bank',
'account_opening_location',
'school',
'major',
'education',
'graduation_year',
]
return {
stepList,
common_importParamsList,
visible: false,
currentStep: 1,
importData: [],
has_error: false,
allCount: 0,
errorCount: 0,
}
},
methods: {
open() {
this.importData = []
this.has_error = false
this.errorCount = 0
this.visible = true
},
close() {
this.currentStep = 1
this.visible = false
},
async goNext() {
if (this.currentStep === 2) {
// 此处调用后端接口
this.allCount = this.importData.length
const importData = this.importData.map((item) => {
const { _X_ROW_KEY, ...rest } = item
const keyArr = Object.keys(rest)
keyArr.forEach((key) => {
if (rest[key]) {
rest[key] = rest[key] + ''
}
})
rest.educational_experience = [
{
school: rest.school,
major: rest.major,
education: rest.education,
graduation_year: rest.graduation_year,
},
]
delete rest.school
delete rest.major
delete rest.education
delete rest.graduation_year
return rest
})
const res = await importEmployee({ employee_list: importData })
if (res.length) {
const errData = res.filter((item) => {
return item.err.length
})
console.log('err', errData)
this.has_error = true
this.errorCount = errData.length
this.currentStep += 1
this.importData = errData
this.$message.error(this.$t('cs.companyStructure.dataErr'))
} else {
this.currentStep += 1
this.$message.success(this.$t('cs.companyStructure.opSuccess'))
}
this.$emit('refresh')
}
},
goPre() {
this.has_error = false
this.errorCount = 0
this.currentStep -= 1
},
download() {
const data = [
[
{
v: '1、表头标“*”的红色字体为必填项\n2、邮箱、用户名不允许重复\n3、登录密码密码由6-20位字母、数字组成\n4、部门上下级部门间用"/"隔开,且从最上级部门开始,例如“深圳分公司/IT部/IT二部”。如出现相同的部门则默认导入组织架构中顺序靠前的部门',
t: 's',
s: {
alignment: {
wrapText: true,
vertical: 'center',
},
},
},
],
[
{
v: '*邮箱',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*用户名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*姓名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*密码',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '性别',
t: 's',
},
{
v: '手机号',
t: 's',
},
{
v: '岗位',
t: 's',
},
{
v: '部门',
t: 's',
},
],
]
data[1] = data[1].filter((item) => item['v'] !== '目前所属主体')
data[1] = data[1].filter((item) => item['v'] !== '初始入职日期')
downloadExcel(data, this.$t('cs.companyStructure.downloadTemplateName'))
},
customRequest(data) {
this.fileList = [data.file]
excel2Array(data.file).then((res) => {
res = res.filter((item) => item.length)
this.importData = res.slice(2).map((item) => {
const obj = {}
// 格式化日期字段
item[8] = this.formatDate(item[8]) // 目前主体入职日期
item[10] = this.formatDate(item[10]) // 离职日期
item[28] = this.formatDate(item[28]) // 毕业年份
item.forEach((ele, index) => {
obj[this.common_importParamsList[index]] = ele
})
return obj
})
this.currentStep = 2
})
},
formatDate(numb) {
if (numb) {
const time = new Date((numb - 1) * 24 * 3600000 + 1)
time.setYear(time.getFullYear() - 70)
time.setMonth(time.getMonth())
time.setHours(time.getHours() - 8)
time.setMinutes(time.getMinutes())
time.setMilliseconds(time.getMilliseconds())
// return time.valueOf()
// 日期格式
const format = 'Y-m-d'
const year = time.getFullYear()
// 由于 getMonth 返回值会比正常月份小 1
let month = time.getMonth() + 1
let day = time.getDate()
month = month > 9 ? month : `0${month}`
day = day > 9 ? day : `0${day}`
const hash = {
Y: year,
m: month,
d: day,
}
return format.replace(/\w/g, (o) => {
return hash[o]
})
} else {
return null
}
},
},
}
</script>
<style lang="less">
.setting-structure-upload {
.ant-modal-body {
padding: 24px 48px;
}
.setting-structure-upload-steps {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 20px;
.setting-structure-upload-step {
display: inline-block;
text-align: center;
position: relative;
.setting-structure-upload-step-icon {
width: 86px;
height: 86px;
display: flex;
align-items: center;
justify-content: center;
background-image: url('../../../assets/icon-bg.png');
margin-bottom: 20px;
> i {
font-size: 40px;
color: #fff;
}
}
> span {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.5);
}
}
.setting-structure-upload-step:not(:first-child)::before {
content: '';
height: 2px;
width: 223px;
position: absolute;
background-color: #e7ecf3;
left: -223px;
top: 43px;
z-index: 0;
}
.selected.setting-structure-upload-step {
&:not(:first-child)::before {
background-color: #7eb0ff;
}
}
.selected {
.setting-structure-upload-step-icon {
background-image: url('../../../assets/icon-bg-selected.png');
}
> span {
color: rgba(0, 0, 0, 0.8);
}
}
}
}
</style>

View File

@@ -10,10 +10,22 @@
:body-style="{ height: `${windowHeight - 320}px`, overflow: 'hidden', overflowY: 'scroll' }"
>
<a-form-model ref="employeeFormData" :model="employeeFormData" :rules="rules" :colon="false">
<a-form-model-item ref="email" :label="$t('cs.companyStructure.email')" prop="email" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='email')!==-1">
<a-input v-model="employeeFormData.email" :placeholder="$t('cs.companyStructure.emailPlaceholder')"/>
<a-form-model-item
ref="email"
:label="$t('cs.companyStructure.email')"
prop="email"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'email') !== -1"
>
<a-input v-model="employeeFormData.email" :placeholder="$t('cs.companyStructure.emailPlaceholder')" />
</a-form-model-item>
<a-form-model-item ref="username" :label="$t('cs.companyStructure.username')" prop="username" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='username')!==-1">
<a-form-model-item
ref="username"
:label="$t('cs.companyStructure.username')"
prop="username"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'username') !== -1"
>
<a-input v-model="employeeFormData.username" :placeholder="$t('cs.companyStructure.usernamePlaceholder')" />
</a-form-model-item>
<a-form-model-item
@@ -23,31 +35,82 @@
prop="password"
:style="formModalItemStyle"
>
<a-input-password v-model="employeeFormData.password" :placeholder="$t('cs.companyStructure.passwordPlaceholder')" />
<a-input-password
v-model="employeeFormData.password"
:placeholder="$t('cs.companyStructure.passwordPlaceholder')"
/>
</a-form-model-item>
<a-form-model-item ref="nickname" :label="$t('cs.companyStructure.nickname')" prop="nickname" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='nickname')!==-1">
<a-form-model-item
ref="nickname"
:label="$t('cs.companyStructure.nickname')"
prop="nickname"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'nickname') !== -1"
>
<a-input v-model="employeeFormData.nickname" :placeholder="$t('cs.companyStructure.nicknamePlaceholder')" />
</a-form-model-item>
<a-form-model-item :label="$t('cs.companyStructure.sex')" prop="sex" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='sex')!==-1">
<a-form-model-item
:label="$t('cs.companyStructure.sex')"
prop="sex"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'sex') !== -1"
>
<a-select v-model="employeeFormData.sex" :placeholder="$t('cs.companyStructure.sexPlaceholder')">
<a-select-option value=""> {{ $t('cs.companyStructure.male') }} </a-select-option>
<a-select-option value=""> {{ $t('cs.companyStructure.female') }} </a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item ref="mobile" :label="$t('cs.companyStructure.mobile')" prop="mobile" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='mobile')!==-1">
<a-form-model-item
ref="mobile"
:label="$t('cs.companyStructure.mobile')"
prop="mobile"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'mobile') !== -1"
>
<a-input v-model="employeeFormData.mobile" :placeholder="$t('cs.companyStructure.mobilePlaceholder')" />
</a-form-model-item>
<div :style="{ width: '361px', display: 'inline-block', margin: '0 7px' }" v-if="attributes.findIndex(v=>v=='department_id')!==-1">
<div :style="{ height: '41px', lineHeight: '40px' }">{{ $t('cs.companyStructure.departmentName') }}</div>
<DepartmentTreeSelect v-model="employeeFormData.department_id" />
</div>
<a-form-model-item ref="position_name" :label="$t('cs.companyStructure.positionName')" prop="position_name" :style="formModalItemStyle" v-if="attributes.findIndex(v=>v=='position_name')!==-1">
<a-input v-model="employeeFormData.position_name" :placeholder="$t('cs.companyStructure.positionNamePlaceholder')" />
<a-form-model-item
ref="department_id"
:label="$t('cs.companyStructure.departmentName')"
prop="department_id"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'department_id') !== -1"
>
<DepartmentTreeSelect style="margin-top: 4px" v-model="employeeFormData.department_id" />
</a-form-model-item>
<a-form-model-item
ref="position_name"
:label="$t('cs.companyStructure.positionName')"
prop="position_name"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'position_name') !== -1"
>
<a-input
v-model="employeeFormData.position_name"
:placeholder="$t('cs.companyStructure.positionNamePlaceholder')"
/>
</a-form-model-item>
<a-form-model-item
ref="direct_supervisor_id"
:label="$t('cs.companyStructure.selectDirectSupervisor')"
prop="direct_supervisor_id"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'direct_supervisor_id') !== -1"
>
<EmployeeTreeSelect style="margin-top: 4px" v-model="employeeFormData.direct_supervisor_id" />
</a-form-model-item>
<a-form-model-item
ref="work_region"
:label="$t('cs.companyStructure.work_region')"
prop="work_region"
:style="formModalItemStyle"
v-if="attributes.findIndex((v) => v == 'work_region') !== -1"
>
<a-select v-model="employeeFormData.work_region" :placeholder="$t('cs.companyStructure.workRegionPlaceholder')">
<a-select-option value="china_mainland"> {{ $t('cs.companyStructure.china_mainland') }} </a-select-option>
<a-select-option value="china_hk"> {{ $t('cs.companyStructure.china_hk') }} </a-select-option>
</a-select>
</a-form-model-item>
<div :style="{ width: '361px', display: 'inline-block', margin: '0 7px' }" v-if="attributes.findIndex(v=>v=='direct_supervisor_id')!==-1">
<div :style="{ height: '41px', lineHeight: '40px' }">{{ $t('cs.companyStructure.selectDirectSupervisor') }}</div>
<EmployeeTreeSelect v-model="employeeFormData.direct_supervisor_id" />
</div>
</a-form-model>
<template slot="footer">
<a-button key="back" @click="close"> {{ $t('cancel') }} </a-button>
@@ -75,7 +138,7 @@ export default {
educational_experience: [],
children_information: [],
file_is_show: true,
attributes: []
attributes: [],
}
},
created() {
@@ -85,7 +148,7 @@ export default {
},
inject: ['provide_allTreeDepartment', 'provide_allFlatEmployees'],
computed: {
...mapState({
...mapState({
windowHeight: (state) => state.windowHeight,
}),
departemntTreeSelectOption() {
@@ -103,7 +166,12 @@ export default {
rules() {
return {
email: [
{ required: true, whitespace: true, message: this.$t('cs.companyStructure.emailPlaceholder'), trigger: 'blur' },
{
required: true,
whitespace: true,
message: this.$t('cs.companyStructure.emailPlaceholder'),
trigger: 'blur',
},
{
pattern: /^[a-zA-Z0-9_.-]+@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.[a-zA-Z0-9]{2,6}$/,
message: this.$t('cs.companyStructure.emailFormatErr'),
@@ -112,12 +180,29 @@ export default {
{ max: 50, message: this.$t('cs.person.inputStrCountLimit', { limit: 50 }) },
],
username: [
{ required: true, whitespace: true, message: this.$t('cs.companyStructure.usernamePlaceholder'), trigger: 'blur' },
{
required: true,
whitespace: true,
message: this.$t('cs.companyStructure.usernamePlaceholder'),
trigger: 'blur',
},
{ max: 20, message: this.$t('cs.person.inputStrCountLimit', { limit: 20 }) },
],
password: [{ required: true, whitespace: true, message: this.$t('cs.companyStructure.passwordPlaceholder'), trigger: 'blur' }],
password: [
{
required: true,
whitespace: true,
message: this.$t('cs.companyStructure.passwordPlaceholder'),
trigger: 'blur',
},
],
nickname: [
{ required: true, whitespace: true, message: this.$t('cs.companyStructure.nicknamePlaceholder'), trigger: 'blur' },
{
required: true,
whitespace: true,
message: this.$t('cs.companyStructure.nicknamePlaceholder'),
trigger: 'blur',
},
{ max: 20, message: this.$t('cs.person.inputStrCountLimit', { limit: 20 }) },
],
mobile: [
@@ -128,7 +213,7 @@ export default {
},
],
}
}
},
},
beforeDestroy() {
Bus.$off('getAttributes')
@@ -136,7 +221,8 @@ export default {
methods: {
async open(getData, type) {
// 提交时去掉school, major, education, graduation_year, name, gender, birthday, parental_leave_left
const { school, major, education, graduation_year, name, gender, birthday, parental_leave_left, ...newGetData } = getData
const { school, major, education, graduation_year, name, gender, birthday, parental_leave_left, ...newGetData } =
getData
const _getData = _.cloneDeep(newGetData)
const { direct_supervisor_id } = newGetData
if (direct_supervisor_id) {
@@ -149,46 +235,54 @@ export default {
// if (type !== 'add' && this.employeeFormData.educational_experience.length !== 0) {
// this.educational_experience = this.employeeFormData.educational_experience
// }
this.children_information = this.formatChildrenInformationList() || [{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0
}]
this.educational_experience = this.formatEducationalExperienceList() || [{
id: uuidv4(),
school: '',
major: '',
education: undefined,
graduation_year: null
}]
this.children_information = this.formatChildrenInformationList() || [
{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0,
},
]
this.educational_experience = this.formatEducationalExperienceList() || [
{
id: uuidv4(),
school: '',
major: '',
education: undefined,
graduation_year: null,
},
]
this.type = type
this.visible = true
},
close() {
this.$refs.employeeFormData.resetFields()
this.educational_experience = [{
school: '',
major: '',
education: undefined,
graduation_year: null
}]
this.children_information = [{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0
}]
this.educational_experience = [
{
school: '',
major: '',
education: undefined,
graduation_year: null,
},
]
this.children_information = [
{
id: uuidv4(),
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0,
},
]
this.visible = false
},
formatChildrenInformationList() {
let arr = []
arr = this.employeeFormData.children_information ? this.employeeFormData.children_information : undefined
if (arr && arr.length) {
arr.forEach(item => {
arr.forEach((item) => {
item.id = uuidv4()
})
return arr
@@ -199,7 +293,7 @@ export default {
let arr = []
arr = this.employeeFormData.educational_experience ? this.employeeFormData.educational_experience : undefined
if (arr && arr.length) {
arr.forEach(item => {
arr.forEach((item) => {
item.id = uuidv4()
})
return arr
@@ -212,12 +306,12 @@ export default {
school: '',
major: '',
education: undefined,
graduation_year: null
graduation_year: null,
}
this.educational_experience.push(newEducational_experience)
},
removeEducation(removeId) {
const _idx = this.educational_experience.findIndex(item => item.id === removeId)
const _idx = this.educational_experience.findIndex((item) => item.id === removeId)
if (_idx !== -1) {
this.educational_experience.splice(_idx, 1)
}
@@ -228,12 +322,12 @@ export default {
name: '',
gender: undefined,
birthday: null,
parental_leave_left: 0
parental_leave_left: 0,
}
this.children_information.push(newChildrenInfo)
},
removeChildren(removeId) {
const _idx = this.children_information.findIndex(item => item.id === removeId)
const _idx = this.children_information.findIndex((item) => item.id === removeId)
if (_idx !== -1) {
this.children_information.splice(_idx, 1)
}
@@ -254,10 +348,10 @@ export default {
// }
if (date !== null) {
if (param === 'graduation_year') {
const _idx = this.educational_experience.findIndex(item => item.id === id)
const _idx = this.educational_experience.findIndex((item) => item.id === id)
this.educational_experience[_idx].graduation_year = moment(date).format('YYYY-MM')
} else if (param === 'birthday') {
const _idx = this.children_information.findIndex(item => item.id === id)
const _idx = this.children_information.findIndex((item) => item.id === id)
this.children_information[_idx].birthday = moment(date).format('YYYY-MM-DD')
} else {
this.employeeFormData[param] = moment(date).format('YYYY-MM-DD')
@@ -303,7 +397,7 @@ export default {
</script>
<style lang="less" scoped>
.el-date-picker {
width: 100%;
height: 36px;
width: 100%;
height: 36px;
}
</style>

View File

@@ -16,17 +16,17 @@
:key="group.id"
>
<div
class="ops-setting-structure-sidebar-group-header"
:class="{ 'group-selected': groupIndex === activeGroupIndex }"
:class="{
'ops-setting-structure-sidebar-group-header': true,
'group-selected': groupIndex === activeGroupIndex,
}"
>
<div class="ops-setting-structure-sidebar-group-header-avatar">
<a-icon :type="group.icon"/>
<a-icon :type="group.icon" />
</div>
<span
class="ops-setting-structure-sidebar-group-header-title"
@click="
clickSelectGroup(groupIndex)
"
@click="clickSelectGroup(groupIndex)"
:id="[group.id === 0 ? 'employee' : 'department']"
>
{{ group.title }}
@@ -100,7 +100,7 @@
/>
</div>
<!-- 筛选框 -->
<div class="Screening-box" v-if="activeGroupIndex === 1" style="background-color: rgb(240, 245, 255) ;">
<div class="Screening-box" v-if="activeGroupIndex === 1" style="background-color: rgb(240, 245, 255)">
<a-popover
@visibleChange="visibleChange"
trigger="click"
@@ -122,16 +122,17 @@
</div>
</template>
<span :style="{ whiteSpace: 'nowrap' }">
<a-icon class="screening-box-scene-icon" type="filter"/>
<a-icon class="screening-box-scene-icon" type="filter" />
{{ getCurrentSceneLabel() }}
<a-icon class="screening-box-scene-icon" :type="displayTimeIcon"/>
<a-icon class="screening-box-scene-icon" :type="displayTimeIcon" />
</span>
</a-popover>
</div>
<SearchForm
ref="search"
:canSearchPreferenceAttrList="canSearchPreferenceAttrList"
@refresh="handleSearch"/>
@refresh="handleSearch"
/>
</div>
<div>
<a-space v-if="isEditable">
@@ -168,23 +169,24 @@
<div>
<div :style="{ marginTop: '8px' }" class="ops-list-batch-action" v-show="!!selectedRowKeys.length">
<span @click="downloadEmployeeAll">{{ $t('cs.companyStructure.downloadAll') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="exportSelectEvent">{{ $t('cs.companyStructure.downloadSelected') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('department_id')">{{ $t('cs.companyStructure.editDepartment') }}</span>
<a-divider type="vertical"/>
<span
@click="openBatchModal('direct_supervisor_id')">{{ $t('cs.companyStructure.editDirectSupervisor') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('direct_supervisor_id')">{{
$t('cs.companyStructure.editDirectSupervisor')
}}</span>
<a-divider type="vertical" />
<span @click="openBatchModal('position_name')">{{ $t('cs.companyStructure.editPosition') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('password')">{{ $t('cs.companyStructure.resetPassword') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('block', null, 1)">{{ $t('cs.companyStructure.block') }}</span>
<a-divider type="vertical"/>
<a-divider type="vertical" />
<span @click="openBatchModal('block', null, 0)">{{ $t('cs.companyStructure.recover') }}</span>
<a-divider type="vertical"/>
<span>{{ $t('selectRows', {rows: selectedRowKeys.length}) }}</span>
<a-divider type="vertical" />
<span>{{ $t('selectRows', { rows: selectedRowKeys.length }) }}</span>
</div>
</div>
<!-- <div>
@@ -195,11 +197,11 @@
</div>
</div>
<!-- 批量操作对话框 -->
<BatchModal ref="BatchModal" @refresh="updateAll"/>
<BatchModal ref="BatchModal" @refresh="updateAll" />
<!-- 部门表单对话框 -->
<DepartmentModal ref="DepartmentModal" @refresh="clickSelectGroup(1)"/>
<DepartmentModal ref="DepartmentModal" @refresh="clickSelectGroup(1)" />
<!-- 员工表单对话框 -->
<EmployeeModal ref="EmployeeModal" @refresh="updateAll"/>
<EmployeeModal ref="EmployeeModal" @refresh="updateAll" />
<!-- 表格展示 -->
<EmployeeTable
@@ -211,7 +213,7 @@
@onSelectChange="onSelectChange"
@openEmployeeModal="openEmployeeModal"
@openBatchModal="openBatchModal"
@tranferAttributes="getAttributes"
@transferAttributes="getAttributes"
:isEditable="isEditable"
:loading="loading"
>
@@ -225,7 +227,9 @@
:page-size-options="pageSizeOptions"
:current="tablePage.currentPage"
:total="tablePage.totalResult"
:show-total="(total, range) => $t('pagination.total', { range0: range[0], range1: range[1], total:total })"
:show-total="
(total, range) => $t('pagination.total', { range0: range[0], range1: range[1], total: total })
"
:page-size="tablePage.pageSize"
:default-current="1"
@change="pageOrSizeChange"
@@ -252,6 +256,9 @@
clickSelectGroup(1)
}
"
@downloadTemplate="downloadTemplate"
@customRequest="customRequest"
@import="importEmployee"
/>
</div>
</template>
@@ -262,15 +269,22 @@ import SplitPane from '@/components/SplitPane'
import CollapseTransition from '@/components/CollapseTransition'
import Bus from './eventBus/bus'
import CategroyTree from './CategoryTree'
import BatchUpload from './BatchUpload'
import BatchUpload from '../components/BatchUpload.vue'
import BatchModal from './BatchModal.vue'
import EmployeeModal from './EmployeeModal.vue'
import DepartmentModal from './DepartmentModal.vue'
import EmployeeTable from '../components/employeeTable.vue'
import { getDepartmentList, deleteDepartmentById, getAllDepartmentList, getAllDepAndEmployee } from '@/api/company'
import { getEmployeeList, getEmployeeCount, downloadAllEmployee, getEmployeeListByFilter } from '@/api/employee'
import {
getEmployeeList,
getEmployeeCount,
downloadAllEmployee,
getEmployeeListByFilter,
importEmployee,
} from '@/api/employee'
import { mixinPermissions } from '@/utils/mixin'
import SearchForm from '../components/SearchForm.vue'
import { downloadExcel, excel2Array } from '@/utils/download'
export default {
name: 'CompanyStructure',
@@ -284,10 +298,22 @@ export default {
EmployeeModal,
DepartmentModal,
EmployeeTable,
SearchForm
SearchForm,
},
data() {
const common_importParamsList = [
'email',
'username',
'nickname',
'password',
'sex',
'mobile',
'position_name',
'department_name',
'work_region',
]
return {
common_importParamsList,
isActive: '',
visible: true,
localStorageKey: 'itsm-company-strcutre',
@@ -322,7 +348,7 @@ export default {
attributes: [],
pageSizeOptions: ['50', '100', '200', '9999'],
expression: [],
loading: false
loading: false,
}
},
// created() {
@@ -363,16 +389,26 @@ export default {
value: 'sex',
is_choice: true,
choice_value: [
{ label: this.$t('cs.companyStructure.male'), value: '' },
{ label: this.$t('cs.companyStructure.female'), value: '' }]
{ label: this.$t('cs.companyStructure.male'), value: '' },
{ label: this.$t('cs.companyStructure.female'), value: '' },
],
},
{ label: this.$t('cs.companyStructure.mobile'), value: 'mobile' },
{ label: this.$t('cs.companyStructure.departmentName'), value: 'department_name' },
{ label: this.$t('cs.companyStructure.positionName'), value: 'position_name' },
{ label: this.$t('cs.companyStructure.supervisor'), value: 'direct_supervisor_id' },
{
label: this.$t('cs.companyStructure.work_region'),
value: 'work_region',
is_choice: true,
choice_value: [
{ label: this.$t('cs.companyStructure.china_mainland'), value: 'china_mainland' },
{ label: this.$t('cs.companyStructure.china_hk'), value: 'china_hk' },
],
},
]
},
sceneList () {
sceneList() {
return [
{
label: this.$t('all'),
@@ -388,7 +424,7 @@ export default {
},
]
},
groupData () {
groupData() {
return [
{
id: 0,
@@ -396,7 +432,7 @@ export default {
icon: 'user',
},
]
}
},
},
provide() {
return {
@@ -426,11 +462,8 @@ export default {
} else {
this.currentScene = 0
}
// console.log(this.currentScene)
// this.init()
this.clickSelectGroup(0).then(val => {
this.clickSelectItem(0)
})
this.updateCount()
this.clickSelectItem(0)
Bus.$on('updataAllIncludeEmployees', () => {
this.getAllFlatEmployees()
this.getAllDepAndEmployee()
@@ -492,7 +525,7 @@ export default {
setSearchPreferenceAttrList() {
this.canSearchPreferenceAttrList.forEach((item) => {
if (!this.attributes.includes(item.value)) {
this.canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter(v => v.value !== item.value)
this.canSearchPreferenceAttrList = this.canSearchPreferenceAttrList.filter((v) => v.value !== item.value)
}
})
},
@@ -504,9 +537,6 @@ export default {
this.$refs.ScreeningBoxScenePopover.$refs.tooltip.onVisibleChange(false)
}
document.getElementById('department').click()
// this.currentPage = 1
// this.updateTableData(1)
// this.departmentList = this.reqDepartmentList(-1)
},
clickHandler(event) {
this.isActive = event.target.innerText
@@ -544,37 +574,6 @@ export default {
this.activeEmployeeCount = res1.employee_count
this.deactiveEmployeeCount = res2.employee_count
},
async updateTableData(currentPage = 1, pageSize = this.tablePage.pageSize) {
this.selectedRowKeys = []
let reqEmployeeData = null
if (this.activeGroupIndex === 0) {
reqEmployeeData = await getEmployeeList({
...this.tableFilterData,
block_status: this.block_status,
page: currentPage,
page_size: pageSize,
search: this.filterName,
order: this.tableSortData || 'direct_supervisor_id',
})
} else if (this.activeGroupIndex === 1) {
reqEmployeeData = await getEmployeeList({
...this.tableFilterData,
block_status: this.currentScene,
department_id: this.selectDepartment.id,
page: currentPage,
page_size: pageSize,
search: this.filterName,
order: this.tableSortData || 'direct_supervisor_id',
})
}
this.tableData = this.FilterTableData(reqEmployeeData)
this.tablePage = {
...this.tablePage,
currentPage: reqEmployeeData.page,
pageSize: reqEmployeeData.page_size,
totalResult: reqEmployeeData.total,
}
},
async updateTableDataByFilter(currentPage = 1, pageSize = this.tablePage.pageSize) {
this.loading = true
this.selectedRowKeys = []
@@ -623,7 +622,8 @@ export default {
let max_index = 0
educational_experience.forEach((item, index) => {
if (index < educational_experience.length - 1) {
max_index = item.graduation_year > educational_experience[index + 1].graduation_year ? index : index + 1
max_index =
item.graduation_year > educational_experience[index + 1].graduation_year ? index : index + 1
}
})
tableData[index].school = educational_experience[max_index].school
@@ -708,7 +708,7 @@ export default {
} else {
block_status = this.currentScene
}
downloadAllEmployee({ block_status: block_status }).then(res => {
downloadAllEmployee({ block_status: block_status }).then((res) => {
const content = res
const blob = new Blob([content], { type: 'application/vnd.ms-excel' })
const url = window.URL.createObjectURL(blob)
@@ -787,7 +787,8 @@ export default {
},
// 请求部门数据
async reqDepartmentList(departmentId) {
const res = (await getDepartmentList({ department_parent_id: departmentId, block: this.currentScene })).departments
const res = (await getDepartmentList({ department_parent_id: departmentId, block: this.currentScene }))
.departments
return this.transformDepartmentData(res)
},
openDepartmentModal(type) {
@@ -828,10 +829,10 @@ export default {
},
sortChangeEvent({ sortList }) {
this.tableSortData = sortList
.map((item) => {
return `${item.order === 'asc' ? '' : '-'}${item.property}`
})
.join(',')
.map((item) => {
return `${item.order === 'asc' ? '' : '-'}${item.property}`
})
.join(',')
this.updateTableDataByFilter()
},
filterChangeEvent({ column, property, values, datas, filterList, $event }) {
@@ -852,6 +853,137 @@ export default {
exportSelectEvent() {
Bus.$emit('reqExportSelectEvent')
},
downloadTemplate() {
const data = [
[
{
v: '1、表头标“*”的红色字体为必填项\n2、邮箱、用户名不允许重复\n3、登录密码密码由6-20位字母、数字组成\n4、部门上下级部门间用"/"隔开,且从最上级部门开始,例如“深圳分公司/IT部/IT二部”。如出现相同的部门则默认导入组织架构中顺序靠前的部门',
t: 's',
s: {
alignment: {
wrapText: true,
vertical: 'center',
},
},
},
],
[
{
v: '*邮箱',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*用户名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*姓名',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '*密码',
t: 's',
s: {
font: {
color: {
rgb: 'FF0000',
},
},
},
},
{
v: '性别',
t: 's',
},
{
v: '手机号',
t: 's',
},
{
v: '部门',
t: 's',
},
{
v: '岗位',
t: 's',
},
{
v: '工作地区',
t: 's',
},
],
]
downloadExcel(data, this.$t('cs.companyStructure.downloadTemplateName'))
},
customRequest({ data }, callback) {
excel2Array(data.file).then((res) => {
res = res.filter((item) => item.length)
callback(
res.slice(2).map((item) => {
const obj = {}
item.forEach((ele, index) => {
obj[this.common_importParamsList[index]] = ele
})
return obj
})
)
})
},
async importEmployee({ importData }, callback) {
this.allCount = importData.length
const _importData = importData.map((item) => {
const { _X_ROW_KEY, ...rest } = item
const keyArr = Object.keys(rest)
keyArr.forEach((key) => {
if (rest[key]) {
rest[key] = rest[key] + ''
}
})
rest.educational_experience = [
{
school: rest.school,
major: rest.major,
education: rest.education,
graduation_year: rest.graduation_year,
},
]
delete rest.school
delete rest.major
delete rest.education
delete rest.graduation_year
const regionMap = {
中国大陆: 'china_mainland',
中国香港: 'china_hk',
'Chinese Mainland': 'china_mainland',
'HK China': 'china_hk',
}
rest.work_region = regionMap[rest.work_region] ?? res.work_region
return rest
})
const res = await importEmployee({ employee_list: _importData })
callback(res)
},
},
}
</script>
@@ -1051,7 +1183,6 @@ export default {
color: @primary-color;
font-size: 12px;
}
}
}
}
@@ -1068,7 +1199,6 @@ export default {
}
}
}
}
.ops-setting-structure-main-pagination {
width: 100%;

View File

@@ -0,0 +1,243 @@
<template>
<a-modal
:visible="visible"
:title="$t('cs.companyStructure.batchImport')"
dialogClass="ops-modal setting-structure-upload"
:width="800"
@cancel="close"
>
<div class="setting-structure-upload-steps">
<div
:class="{ 'setting-structure-upload-step': true, selected: index + 1 <= currentStep }"
v-for="(step, index) in stepList"
:key="step.value"
>
<div :class="{ 'setting-structure-upload-step-icon': true }">
<ops-icon :type="step.icon" />
</div>
<span>{{ step.label }}</span>
</div>
</div>
<template v-if="currentStep === 1">
<a-upload :multiple="false" :customRequest="customRequest" accept=".xlsx" :showUploadList="false">
<a-button :style="{ marginBottom: '20px' }" type="primary">
<a-icon type="upload" />{{ $t('cs.companyStructure.selectFile') }}</a-button
>
</a-upload>
<p>
<a @click="download">
<slot name="downloadTemplateText">{{ $t('cs.companyStructure.clickDownloadImportTemplate') }}</slot>
</a>
</p>
</template>
<div
:style="{
height: '60px',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
whiteSpace: 'pre-wrap',
}"
v-if="currentStep === 3"
>
{{ $t('cs.companyStructure.importSuccess', { allCount: allCount })
}}<span :style="{ color: '#2362FB' }"> {{ allCount - errorCount }} </span>{{ $t('cs.companyStructure.count') }},
{{ $t('cs.companyStructure.importFailed') }}<span :style="{ color: '#D81E06' }"> {{ errorCount }} </span
>{{ $t('cs.companyStructure.count') }}
</div>
<slot>
<vxe-table
v-if="currentStep === 2 || has_error"
ref="employeeTable"
stripe
:data="importData"
show-overflow
show-header-overflow
highlight-hover-row
size="small"
class="ops-stripe-table"
:max-height="400"
:column-config="{ resizable: true }"
>
<vxe-column field="email" :title="$t('cs.companyStructure.email')" min-width="120" fixed="left"></vxe-column>
<vxe-column field="username" :title="$t('cs.companyStructure.username')" min-width="80"></vxe-column>
<vxe-column field="nickname" :title="$t('cs.companyStructure.nickname')" min-width="80"></vxe-column>
<vxe-column field="password" :title="$t('cs.companyStructure.password')" min-width="80"></vxe-column>
<vxe-column field="sex" :title="$t('cs.companyStructure.sex')" min-width="60"></vxe-column>
<vxe-column field="mobile" :title="$t('cs.companyStructure.mobile')" min-width="80"></vxe-column>
<vxe-column
field="department_name"
:title="$t('cs.companyStructure.departmentName')"
min-width="80"
></vxe-column>
<vxe-column field="position_name" :title="$t('cs.companyStructure.positionName')" min-width="80"></vxe-column>
<vxe-column field="work_region" :title="$t('cs.companyStructure.work_region')" min-width="80"></vxe-column>
<vxe-column
v-if="has_error"
field="err"
:title="$t('cs.companyStructure.importFailedReason')"
min-width="120"
fixed="right"
>
<template #default="{ row }">
<span :style="{ color: '#D81E06' }">{{ row.err }}</span>
</template>
</vxe-column>
</vxe-table>
</slot>
<a-space slot="footer">
<a-button size="small" type="primary" ghost @click="close">{{ $t('cancel') }}</a-button>
<a-button v-if="currentStep !== 1" size="small" type="primary" ghost @click="goPre">{{
$t('cs.companyStructure.prevStep')
}}</a-button>
<a-button v-if="currentStep !== 3" size="small" type="primary" @click="goNext">{{
$t('cs.companyStructure.nextStep')
}}</a-button>
<a-button v-else size="small" type="primary" @click="close">{{ $t('cs.companyStructure.done') }}</a-button>
</a-space>
</a-modal>
</template>
<script>
export default {
name: 'BatchUpload',
data() {
const stepList = [
{
value: 1,
label: this.$t('cs.companyStructure.uploadFile'),
icon: 'icon-shidi-tianjia',
},
{
value: 2,
label: this.$t('cs.companyStructure.confirmData'),
icon: 'icon-shidi-yunshangchuan',
},
{
value: 3,
label: this.$t('cs.companyStructure.uploadDone'),
icon: 'icon-shidi-queren',
},
]
return {
stepList,
visible: false,
currentStep: 1,
importData: [],
has_error: false,
allCount: 0,
errorCount: 0,
}
},
methods: {
open() {
this.importData = []
this.has_error = false
this.errorCount = 0
this.visible = true
},
close() {
this.currentStep = 1
this.visible = false
},
async goNext() {
if (this.currentStep === 2) {
this.allCount = this.importData.length
this.$emit('import', { importData: this.importData }, (res) => {
if (res.length) {
const errData = res.filter((item) => {
return item.err.length
})
console.log('err', errData)
this.has_error = true
this.errorCount = errData.length
this.currentStep += 1
this.importData = errData
this.$message.error(this.$t('cs.companyStructure.dataErr'))
} else {
this.currentStep += 1
this.$message.success(this.$t('cs.companyStructure.opSuccess'))
}
this.$emit('refresh')
})
}
},
goPre() {
this.has_error = false
this.errorCount = 0
this.currentStep -= 1
},
download() {
this.$emit('downloadTemplate')
},
customRequest(data) {
this.fileList = [data.file]
this.$emit('customRequest', { data }, (importData) => {
this.importData = importData
this.currentStep = 2
})
},
},
}
</script>
<style lang="less">
.setting-structure-upload {
.ant-modal-body {
padding: 24px 48px;
}
.setting-structure-upload-steps {
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 20px;
.setting-structure-upload-step {
display: inline-block;
text-align: center;
position: relative;
.setting-structure-upload-step-icon {
width: 86px;
height: 86px;
display: flex;
align-items: center;
justify-content: center;
background-image: url('../../../assets/icon-bg.png');
margin-bottom: 20px;
> i {
font-size: 40px;
color: #fff;
}
}
> span {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.5);
}
}
.setting-structure-upload-step:not(:first-child)::before {
content: '';
height: 2px;
width: 223px;
position: absolute;
background-color: #e7ecf3;
left: -223px;
top: 43px;
z-index: 0;
}
.selected.setting-structure-upload-step {
&:not(:first-child)::before {
background-color: #7eb0ff;
}
}
.selected {
.setting-structure-upload-step-icon {
background-image: url('../../../assets/icon-bg-selected.png');
}
> span {
color: rgba(0, 0, 0, 0.8);
}
}
}
}
</style>

View File

@@ -92,7 +92,7 @@
<span>{{ $t('cs.companyStructure.sex') }}</span>
</span>
</template>
<template #default="{row}">
<template #default="{ row }">
<span v-if="row.sex === ''">{{ $t('cs.companyStructure.male') }}</span>
<span v-if="row.sex === ''">{{ $t('cs.companyStructure.female') }}</span>
</template>
@@ -175,6 +175,26 @@
}}</span>
</template>
</vxe-column>
<vxe-column
field="work_region"
v-if="
checkedCols.findIndex((v) => v == 'work_region') !== -1 &&
attributes.findIndex((v) => v == 'work_region') !== -1
"
:title="$t('cs.companyStructure.work_region')"
min-width="120px"
key="work_region"
>
<template #header>
<span class="vxe-handle">
<OpsMoveIcon class="move-icon" />
<span>{{ $t('cs.companyStructure.work_region') }}</span>
</span>
</template>
<template #default="{ row }">
{{ $t(`cs.companyStructure.${row.work_region}`) }}
</template>
</vxe-column>
<vxe-column
field="control"
width="100px"
@@ -197,7 +217,7 @@
>
<template slot="content">
<div :style="{ maxHeight: `${windowHeight - 320}px`, overflowY: 'auto', width: '160px' }">
<a-checkbox-group v-model="unsbmitCheckedCols" :options="options" style="display: grid;">
<a-checkbox-group v-model="unsbmitCheckedCols" :options="options" style="display: grid">
</a-checkbox-group>
</div>
<div
@@ -209,22 +229,24 @@
justifyContent: 'flex-end',
}"
>
<a-button :style="{ marginRight: '10px' }" size="small" @click="handleCancel">{{ $t('cancel') }}</a-button>
<a-button :style="{ marginRight: '10px' }" size="small" @click="handleCancel">{{
$t('cancel')
}}</a-button>
<a-button size="small" @click="handleSubmit" type="primary">{{ $t('confirm') }}</a-button>
</div>
</template>
<a-icon type="control" style="cursor: pointer;" />
<a-icon type="control" style="cursor: pointer" />
</a-popover>
</template>
</template>
<template #default="{ row }">
<a-space v-if="tableType === 'structure'">
<a><a-icon type="edit" @click="openEmployeeModal(row, 'edit')"/></a>
<a><a-icon type="edit" @click="openEmployeeModal(row, 'edit')" /></a>
<a-tooltip>
<template slot="title">
{{ $t('cs.companyStructure.resetPassword') }}
</template>
<a><a-icon type="reload" @click="openBatchModal('password', row)"/></a>
<a><a-icon type="reload" @click="openBatchModal('password', row)" /></a>
</a-tooltip>
<a-tooltip v-if="!row.block">
<template slot="title">
@@ -305,6 +327,7 @@ export default {
{ label: this.$t('cs.companyStructure.departmentName'), value: 'department_name' },
{ label: this.$t('cs.companyStructure.positionName'), value: 'position_name' },
{ label: this.$t('cs.companyStructure.supervisor'), value: 'direct_supervisor_id' },
{ label: this.$t('cs.companyStructure.work_region'), value: 'work_region' },
]
const checkedCols = JSON.parse(localStorage.getItem('setting-table-CheckedCols')) || [
'nickname',
@@ -315,6 +338,7 @@ export default {
'department_name',
'position_name',
'direct_supervisor_id',
'work_region',
]
return {
filterRoleList: [],
@@ -442,7 +466,7 @@ export default {
}
}
Bus.$emit('getAttributes', this.attributes)
this.$emit('tranferAttributes', this.attributes)
this.$emit('transferAttributes', this.attributes)
},
getIsInterInship(is_internship) {
return this.internMap.filter((item) => item.id === is_internship)[0]['label']
@@ -541,7 +565,7 @@ export default {
useStyle: true, // 是否导出样式
isFooter: false, // 是否导出表尾比如合计
// 过滤那个字段导出
columnFilterMethod: function(column, $columnIndex) {
columnFilterMethod: function (column, $columnIndex) {
return !(column.$columnIndex === 0)
// 0是复选框 不导出
},

View File

@@ -19,7 +19,7 @@ const cs_en = {
app: 'APP Authority',
basic: 'Basic Settings',
theme: 'Theme Settings',
security: 'Security Settings'
security: 'Security Settings',
},
companyInfo: {
spanCompany: 'Description',
@@ -201,7 +201,11 @@ const cs_en = {
createEmployee: 'Create Employee',
editEmployee: 'Edit Employee',
role: 'Role',
selectDisplayColumn: 'Please select columns to display'
selectDisplayColumn: 'Please select columns to display',
work_region: 'Work Region',
workRegionPlaceholder: 'Please select work region',
china_mainland: 'Chinese Mainland',
china_hk: 'HK China',
},
auth: {
basic: 'Basic',
@@ -220,7 +224,7 @@ const cs_en = {
user: 'User',
username: 'Username',
userPlaceholder: 'Please enter username',
userHelp: 'User DN: cn={},ou=users,dc=xxx,dc=com {} will be replaced by username'
userHelp: 'User DN: cn={},ou=users,dc=xxx,dc=com {} will be replaced by username',
},
cas: {
server: 'Server Address',
@@ -237,10 +241,11 @@ const cs_en = {
validateRoutePlaceholder: 'Please enter validate route',
afterLoginRoute: 'Redirect Route',
afterLoginRoutePlaceholder: 'Please enter redirect route',
userMap: 'User Attribute Mapping'
userMap: 'User Attribute Mapping',
},
autoRedirectLogin: 'Auto Redirect to Third-party Login Page',
autoRedirectLoginHelp: 'If disabled, a confirmation will be displayed to redirect to third-party login page. Click the Cancel button will go to the built-in login page',
autoRedirectLoginHelp:
'If disabled, a confirmation will be displayed to redirect to third-party login page. Click the Cancel button will go to the built-in login page',
usernameOrEmail: 'Username/Email',
usernameOrEmailPlaceholder: 'Please enter username/email',
password: 'Password',
@@ -257,7 +262,7 @@ const cs_en = {
userInfo: 'User Info',
scopes: 'Scopes',
scopesPlaceholder: 'Please enter scopes',
}
},
},
duty: {
basicSetting: 'Basic Settings',
@@ -274,13 +279,13 @@ const cs_en = {
mainDutyPeople: 'Main Duty Person',
deputyDutyPeople: 'Deputy Duty Person',
dutyRule: 'Duty Rule',
'一': 'Mon',
'二': 'Tue',
'三': 'Wed',
'四': 'Thu',
'五': 'Fri',
'六': 'Sat',
'日': 'Sun',
: 'Mon',
: 'Tue',
: 'Wed',
: 'Thu',
: 'Fri',
: 'Sat',
: 'Sun',
searchPlaceholder: 'Please search',
dutyTable: 'Duty Schedule',
dutyMember: 'Duty Member',
@@ -304,7 +309,7 @@ const cs_en = {
offDutyReceiverPlaceholder: 'Please select off-duty receiver',
titleLimit: 'Please enter title (20 characters)',
remarkLimit: 'Remark 150 characters max',
frequencyLimit: 'Please enter duty frequency (positive integer)'
frequencyLimit: 'Please enter duty frequency (positive integer)',
},
group: {
groupName: 'User Group',
@@ -329,7 +334,7 @@ const cs_en = {
moreThan: 'More Than',
lessThan: 'Less Than',
operatorInPlaceholder: 'Separate by ;',
selectEmployee: 'Select Employee'
selectEmployee: 'Select Employee',
},
notice: {
corpid: 'Corp ID',
@@ -368,7 +373,8 @@ const cs_en = {
disableCreationOfRequestsViaEmail: 'Disable Creation of Requests Via Email',
specifyAllowedEmails: 'Specify Allowed Emails/Domains, Separate Multiple Values By Comma',
specifyAllowedEmailsExample: 'E.g. user@domain.com,*@domain.com',
specifyAllowedEmailsLimit: 'Limit cannot apply to requests already in sessions, it will aggregate to its parent ticket',
specifyAllowedEmailsLimit:
'Limit cannot apply to requests already in sessions, it will aggregate to its parent ticket',
messageConfig: 'Message Settings',
moveWrongMessagesToFolder: 'Move Messages to Wrong Folder',
knowMore: 'Learn More',
@@ -438,7 +444,7 @@ const cs_en = {
myDepartmentAndSubordinateDepartments: 'My Department And Subordinate Departments',
test: 'Test',
selectApp: 'Select App',
}
},
}
export default cs_en

View File

@@ -19,7 +19,7 @@ const cs_zh = {
app: '应用权限',
basic: '基础设置',
theme: '主题配置',
security: '安全配置'
security: '安全配置',
},
companyInfo: {
spanCompany: '公司描述',
@@ -201,7 +201,11 @@ const cs_zh = {
createEmployee: '新建员工',
editEmployee: '编辑员工',
role: '角色',
selectDisplayColumn: '请选择需要展示的列'
selectDisplayColumn: '请选择需要展示的列',
work_region: '工作地区',
workRegionPlaceholder: '请选择工作地区',
china_mainland: '中国大陆',
china_hk: '中国香港',
},
auth: {
basic: '基本',
@@ -220,7 +224,7 @@ const cs_zh = {
user: '用户',
username: '用户名称',
userPlaceholder: '请输入用户名称',
userHelp: '用户dn: cn={},ou=users,dc=xxx,dc=com {}会替换成用户名'
userHelp: '用户dn: cn={},ou=users,dc=xxx,dc=com {}会替换成用户名',
},
cas: {
server: '服务端地址',
@@ -237,7 +241,7 @@ const cs_zh = {
validateRoutePlaceholder: '请输入验证路由',
afterLoginRoute: '重定向路由',
afterLoginRoutePlaceholder: '请输入重定向路由',
userMap: '用户属性映射'
userMap: '用户属性映射',
},
autoRedirectLogin: '自动跳转到第三方登录页',
autoRedirectLoginHelp: '如果关闭,则会弹出跳转到第三方登录页的确认,点取消按钮会进入系统内置的登录页',
@@ -257,7 +261,7 @@ const cs_zh = {
userInfo: '用户信息',
scopes: '授权范围',
scopesPlaceholder: '请输入授权范围',
}
},
},
duty: {
basicSetting: '基础设置',
@@ -274,13 +278,13 @@ const cs_zh = {
mainDutyPeople: '主值班人',
deputyDutyPeople: '副值班人',
dutyRule: '排班规则',
'一': '一',
'二': '二',
'三': '三',
'四': '四',
'五': '五',
'六': '六',
'日': '日',
: '一',
: '二',
: '三',
: '四',
: '五',
: '六',
: '日',
searchPlaceholder: '请查找',
dutyTable: '值班表',
dutyMember: '值班人员',
@@ -304,7 +308,7 @@ const cs_zh = {
offDutyReceiverPlaceholder: '请选择非值班时间接收人',
titleLimit: '请输入标题20个字符',
remarkLimit: '备注150个字符以内',
frequencyLimit: '请输入值班频次(正整数)'
frequencyLimit: '请输入值班频次(正整数)',
},
group: {
groupName: '用户分组',
@@ -329,7 +333,7 @@ const cs_zh = {
moreThan: '大于',
lessThan: '小于',
operatorInPlaceholder: '以 ; 分隔',
selectEmployee: '选择员工'
selectEmployee: '选择员工',
},
notice: {
corpid: '企业ID',
@@ -438,6 +442,6 @@ const cs_zh = {
myDepartmentAndSubordinateDepartments: '本部门及下属部门',
test: '测试',
selectApp: '选择应用',
}
},
}
export default cs_zh

View File

@@ -2,27 +2,13 @@
<div class="setting-person">
<div class="setting-person-left">
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '1'
})
}
"
@click="clickSideItem('1')"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '1' }"
>
<ops-icon type="icon-shidi-yonghu" />{{ $t('cs.person.spanTitle') }}
</div>
<div
@click="
() => {
$refs.personForm.clearValidate()
$nextTick(() => {
current = '2'
})
}
"
@click="clickSideItem('2')"
:class="{ 'setting-person-left-item': true, 'setting-person-left-item-selected': current === '2' }"
>
<a-icon type="unlock" theme="filled" />{{ $t('cs.person.accountAndPassword') }}
@@ -240,7 +226,14 @@ export default {
}
},
},
beforeDestroy() {
this.$bus.$off('changeSettingPersonCurrent', this.clickSideItemv)
},
mounted() {
this.$bus.$on('changeSettingPersonCurrent', this.clickSideItem)
if (this.$route?.query?.current) {
this.current = this.$route.query.current
}
this.getAllFlatEmployees()
this.getAllFlatDepartment()
this.getEmployeeByUid()
@@ -249,6 +242,12 @@ export default {
...mapActions(['GetInfo']),
getDepartmentName,
getDirectorName,
clickSideItem(type) {
this.$refs.personForm.clearValidate()
this.$nextTick(() => {
this.current = type
})
},
getEmployeeByUid() {
getEmployeeByUid(this.uid).then((res) => {
this.form = { ...res }