Merge pull request #4600 from jumpserver/dev

v4.6.0
This commit is contained in:
Bryan 2025-01-15 14:39:21 +08:00 committed by GitHub
commit e58ec6057c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 121 additions and 42 deletions

View File

@ -57,7 +57,7 @@
@click="handleClick(action)"
>
<el-tooltip :content="action.tip" :disabled="!action.tip" :open-delay="500" placement="top">
<span>
<span :title="action.tip">
<span v-if="action.icon && !action.icon.startsWith('el-')" style="vertical-align: initial">
<i v-if="action.icon.startsWith('fa')" :class="'fa ' + action.icon" />
<svg-icon v-else :icon-class="action.icon" />
@ -249,10 +249,11 @@ $color-drop-menu-border: #e4e7ed;
align-items: flex-end;
.el-button {
display: flex;
align-items: center;
padding: 2px 6px;
padding: 2px 5px;
color: $btn-text-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
* {
vertical-align: baseline !important;

View File

@ -223,6 +223,10 @@ export default {
line-height: 30px;
color: var(--color-text-primary);
span {
display: unset;
}
i {
color: var(--color-icon-primary);
}

View File

@ -34,14 +34,12 @@ class StrategyNormal extends StrategyAbstract {
onSelectionChange(val) {
this.elDataTable.selected = val
}
/**
* toggleRowSelection和clearSelection的表现与el-table一致
*/
toggleRowSelection(...args) {
return this.elTable.toggleRowSelection(...args)
}
clearSelection() {
return this.elTable.clearSelection()
}
@ -65,24 +63,54 @@ class StrategyPersistSelection extends StrategyAbstract {
*/
onSelect(selection, row) {
const isChosen = selection.indexOf(row) > -1
this.toggleRowSelection(row, isChosen)
}
/**
* 用户切换当前页的多选
*/
onSelectAll(selection, selectable = () => true) {
// 获取当前所有已选择的项
const selectedRows = this.elDataTable.data.filter(r => selection.includes(r))
const { id, selected, data } = this.elDataTable
const selectableRows = data.filter(selectable)
// const isSelected = !!selection.length
// 判断是否已全选
const isSelected = this.elDataTable.data.every(r => selectable(r) && selectedRows.includes(r))
// 创建已选择项的 id 集合,用于快速查找
const selectedIds = new Set(selected.map(r => r[id]))
const currentPageIds = new Set(selectableRows.map(row => row[id]))
this.elDataTable.data.forEach(r => {
if (selectable(r)) {
this.toggleRowSelection(r, isSelected)
// 前页面的选中状态
const currentPageSelectedCount = selectableRows.filter(row =>
selectedIds.has(row[id])
).length
// 判断是全选还是取消全选
const shouldSelectAll = currentPageSelectedCount < selectableRows.length
this.elTable.clearSelection()
if (shouldSelectAll) {
selectableRows.forEach(row => {
if (!selectedIds.has(row[id])) selected.push(row)
this.elTable.toggleRowSelection(row, true)
// ! 这里需要触发事件,否则在 el-table 中无法触发 selection-change 事件
this.elDataTable.$emit('toggle-row-selection', true, row)
})
} else {
const newSelected = []
selected.forEach(row => {
if (!currentPageIds.has(row[id])) {
newSelected.push(row)
} else {
this.elDataTable.$emit('toggle-row-selection', false, row)
}
})
this.elDataTable.selected = newSelected
}
this.elDataTable.$emit('selection-change', this.elDataTable.selected)
}
/**
* toggleRowSelection和clearSelection管理elDataTable的selected数组
@ -105,29 +133,26 @@ class StrategyPersistSelection extends StrategyAbstract {
this.elDataTable.$emit('toggle-row-selection', isSelected, row)
this.updateElTableSelection()
}
clearSelection() {
this.elDataTable.selected = []
this.updateElTableSelection()
}
/**
* 将selected状态同步到el-table中
*/
updateElTableSelection() {
const { data, id, selected } = this.elDataTable
const selectedIds = new Set(selected.map(r => r[id]))
// 历史勾选的行已经不在当前页了所以要将当前页的行数据和selected合并
const mergeData = _.uniqWith([...data, ...selected], _.isEqual)
this.elTable.clearSelection()
mergeData.forEach(r => {
const isSelected = !!selected.find(r2 => r[id] === r2[id])
data.forEach(row => {
const shouldBeSelected = selectedIds.has(row[id])
if (!this.elTable) return
if (!this.elTable) {
return
if (shouldBeSelected) {
this.elTable.toggleRowSelection(row, true)
}
this.elTable.toggleRowSelection(r, isSelected)
})
}
}

View File

@ -155,7 +155,13 @@ export default {
})
},
iExportOptions() {
return assignIfNot(this.exportOptions, { url: this.tableUrl })
/**
* 原本是使用 assignIfNot 此函数内部使用 partialRight, 该函数
* 只在目标对象的属性未定义时才从源对象复制属性如果目标对象已经有值则保留原值
* 那如果首次点击的树节点那么此时 url 就会被确定后续点击的树节点那么 url 就不会
* 改变了
*/
return Object.assign({}, this.exportOptions, { url: this.tableUrl })
}
},
methods: {

View File

@ -17,7 +17,7 @@
size="small"
type="info"
@click="handleTagClick(v,k)"
@close="handleTagClose(k)"
@close.stop="handleTagClose(k)"
>
<strong v-if="v.label">{{ v.label + ':' }}</strong>
<span v-if="v.valueLabel">{{ v.valueLabel }}</span>
@ -128,7 +128,7 @@ export default {
deep: true
},
filterTags: {
handler() {
handler(newValue) {
this.$emit('tag-search', this.filterMaps)
},
deep: true
@ -137,6 +137,15 @@ export default {
if (newValue === '' && oldValue !== '') {
this.emptyCount = 1
}
},
'$route'(to, from) {
if (from.query !== to.query) {
this.filterTags = {}
if (to.query && Object.keys(to.query).length) {
const routeFilter = this.checkInTableColumns(this.options)
this.filterTagSearch(routeFilter)
}
}
}
},
mounted() {

View File

@ -79,6 +79,9 @@ export default {
case 'account_template':
url = '/api/v1/accounts/account-templates/'
break
case 'label':
url = '/api/v1/labels/labels/'
break
case 'name_strategy':
options = this.nameOptions
break

View File

@ -6,6 +6,7 @@ export const resourceTypeOptions = [
{ label: i18n.t('Node'), value: 'node' },
{ label: i18n.t('Zone'), value: 'domain' },
{ label: i18n.t('AccountTemplate'), value: 'account_template' },
{ label: i18n.t('Tags'), value: 'label' },
{ label: i18n.t('Strategy'), value: 'name_strategy' }
]

View File

@ -117,7 +117,7 @@ export const ACCOUNT_PROVIDER_ATTRS_MAP = {
[vmware]: {
name: vmware,
title: 'VMware',
attrs: ['host', 'port', 'username', 'password'],
attrs: ['host', 'port', 'username', 'password', 'auto_sync_node'],
image: require('@/assets/img/cloud/vmware.svg')
},
[nutanix]: {

View File

@ -10,17 +10,24 @@
style="width: 100%"
>
<el-table-column :label="$tc('Ranking')" width="80px">
<template v-slot="scope">
<span>{{ scope.$index + 1 }}</span>
<template v-slot="scope" #header>
<el-tooltip :content="$t('Ranking')" placement="top" :open-delay="500">
<span style="cursor: pointer;">{{ $t('Ranking') }}</span>
</el-tooltip>
</template>
</el-table-column>
<el-table-column
v-for="i in config.columns"
:key="i.prop"
:label="i.label"
:prop="i.prop"
:width="i.width"
/>
>
<template #header>
<el-tooltip :content="i.label" placement="top" :open-delay="500">
<span style="cursor: pointer;">{{ i.label }}</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
</div>
</template>

View File

@ -30,17 +30,35 @@ export default {
let percentage = activeDecimal.dividedBy(totalDecimal).times(100)
percentage = isNaN(percentage) ? 0 : percentage
percentage = percentage.toFixed(2)
const formatTitle = (text) => {
if (!text) return ''
const maxLength = 23
const lines = []
for (let i = 0; i < text.length; i += maxLength) {
lines.push(text.slice(i, i + maxLength))
}
return lines.join('\n')
}
return {
title: [
{
text: this.config.chartTitle,
text: formatTitle(this.config.chartTitle),
textStyle: {
color: '#646A73',
fontSize: 12
fontSize: 12,
lineHeight: 16,
rich: {
width: 100,
overflow: 'break'
}
},
textAlign: 'center',
left: '48%',
top: '32%'
top: '32%',
width: 100,
overflow: 'break'
},
{
left: '48%',

View File

@ -19,6 +19,7 @@
type="info"
/>
<QuickActions
v-if="biometricFeaturesActions.some(action => action.has)"
:title="$tc('BiometricFeatures')"
type="warning"
:actions="biometricFeaturesActions"
@ -85,13 +86,17 @@ export default {
biometricFeaturesActions: [
{
title: this.$t('FacialFeatures'),
has: this.$store.getters.publicSettings.FACE_RECOGNITION_ENABLED &&
this.$store.getters.publicSettings.XPACK_LICENSE_EDITION_ULTIMATE,
attrs: {
type: 'primary',
label: this.$store.state.users.profile.is_face_code_set ? this.$t('Unbind') : this.$t('Bind')
},
callbacks: {
click: () => {
const next_url = this.$store.state.users.profile.is_face_code_set ? '/core/auth/profile/face/disable/' : '/core/auth/profile/face/enable/'
const next_url = this.$store.state.users.profile.is_face_code_set
? '/core/auth/profile/face/disable/'
: '/core/auth/profile/face/enable/'
window.open(next_url, '_blank')
}
}