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

View File

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

View File

@ -34,14 +34,12 @@ class StrategyNormal extends StrategyAbstract {
onSelectionChange(val) { onSelectionChange(val) {
this.elDataTable.selected = val this.elDataTable.selected = val
} }
/** /**
* toggleRowSelection和clearSelection的表现与el-table一致 * toggleRowSelection和clearSelection的表现与el-table一致
*/ */
toggleRowSelection(...args) { toggleRowSelection(...args) {
return this.elTable.toggleRowSelection(...args) return this.elTable.toggleRowSelection(...args)
} }
clearSelection() { clearSelection() {
return this.elTable.clearSelection() return this.elTable.clearSelection()
} }
@ -52,12 +50,12 @@ class StrategyNormal extends StrategyAbstract {
*/ */
class StrategyPersistSelection extends StrategyAbstract { class StrategyPersistSelection extends StrategyAbstract {
/** /**
* el-table selection-change 事件不适用于开启跨页保存的情况 * el-table的selection-change事件不适用于开启跨页保存的情况
* 比如当开启 persistSelection时发生以下两个场景 * 比如当开启persistSelection时发生以下两个场景
* 1. 用户点击翻页 * 1. 用户点击翻页
* 2. 用户点击行首的切换全选项按钮清空当前页多选项数据 * 2. 用户点击行首的切换全选项按钮清空当前页多选项数据
* 其中场景 1 应该保持 selected 不变而场景 2 只应该从 selected 移除当前页所有行保留其他页面的多选状态 * 其中场景1应该保持selected不变而场景2只应该从selected移除当前页所有行保留其他页面的多选状态
* el-table selection-change 事件在两个场景中无差别发生所以这里不处理这个事件 * el-table的selection-change事件在两个场景中无差别发生所以这里不处理这个事件
*/ */
/** /**
@ -65,24 +63,54 @@ class StrategyPersistSelection extends StrategyAbstract {
*/ */
onSelect(selection, row) { onSelect(selection, row) {
const isChosen = selection.indexOf(row) > -1 const isChosen = selection.indexOf(row) > -1
this.toggleRowSelection(row, isChosen) this.toggleRowSelection(row, isChosen)
} }
/** /**
* 用户切换当前页的多选 * 用户切换当前页的多选
*/ */
onSelectAll(selection, selectable = () => true) { onSelectAll(selection, selectable = () => true) {
// 获取当前所有已选择的项 const { id, selected, data } = this.elDataTable
const selectedRows = this.elDataTable.data.filter(r => selection.includes(r)) const selectableRows = data.filter(selectable)
// const isSelected = !!selection.length
// 判断是否已全选 // 创建已选择项的 id 集合,用于快速查找
const isSelected = this.elDataTable.data.every(r => selectable(r) && selectedRows.includes(r)) 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)) { const currentPageSelectedCount = selectableRows.filter(row =>
this.toggleRowSelection(r, isSelected) 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数组 * toggleRowSelection和clearSelection管理elDataTable的selected数组
@ -105,29 +133,26 @@ class StrategyPersistSelection extends StrategyAbstract {
this.elDataTable.$emit('toggle-row-selection', isSelected, row) this.elDataTable.$emit('toggle-row-selection', isSelected, row)
this.updateElTableSelection() this.updateElTableSelection()
} }
clearSelection() { clearSelection() {
this.elDataTable.selected = [] this.elDataTable.selected = []
this.updateElTableSelection() this.updateElTableSelection()
} }
/** /**
* 将selected状态同步到el-table中 * 将selected状态同步到el-table中
*/ */
updateElTableSelection() { updateElTableSelection() {
const { data, id, selected } = this.elDataTable const { data, id, selected } = this.elDataTable
const selectedIds = new Set(selected.map(r => r[id]))
// 历史勾选的行已经不在当前页了所以要将当前页的行数据和selected合并 this.elTable.clearSelection()
const mergeData = _.uniqWith([...data, ...selected], _.isEqual)
mergeData.forEach(r => { data.forEach(row => {
const isSelected = !!selected.find(r2 => r[id] === r2[id]) const shouldBeSelected = selectedIds.has(row[id])
if (!this.elTable) return
if (!this.elTable) { if (shouldBeSelected) {
return this.elTable.toggleRowSelection(row, true)
} }
this.elTable.toggleRowSelection(r, isSelected)
}) })
} }
} }

View File

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

View File

@ -17,7 +17,7 @@
size="small" size="small"
type="info" type="info"
@click="handleTagClick(v,k)" @click="handleTagClick(v,k)"
@close="handleTagClose(k)" @close.stop="handleTagClose(k)"
> >
<strong v-if="v.label">{{ v.label + ':' }}</strong> <strong v-if="v.label">{{ v.label + ':' }}</strong>
<span v-if="v.valueLabel">{{ v.valueLabel }}</span> <span v-if="v.valueLabel">{{ v.valueLabel }}</span>
@ -128,7 +128,7 @@ export default {
deep: true deep: true
}, },
filterTags: { filterTags: {
handler() { handler(newValue) {
this.$emit('tag-search', this.filterMaps) this.$emit('tag-search', this.filterMaps)
}, },
deep: true deep: true
@ -137,6 +137,15 @@ export default {
if (newValue === '' && oldValue !== '') { if (newValue === '' && oldValue !== '') {
this.emptyCount = 1 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() { mounted() {

View File

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

View File

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

View File

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

View File

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

View File

@ -30,17 +30,35 @@ export default {
let percentage = activeDecimal.dividedBy(totalDecimal).times(100) let percentage = activeDecimal.dividedBy(totalDecimal).times(100)
percentage = isNaN(percentage) ? 0 : percentage percentage = isNaN(percentage) ? 0 : percentage
percentage = percentage.toFixed(2) 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 { return {
title: [ title: [
{ {
text: this.config.chartTitle, text: formatTitle(this.config.chartTitle),
textStyle: { textStyle: {
color: '#646A73', color: '#646A73',
fontSize: 12 fontSize: 12,
lineHeight: 16,
rich: {
width: 100,
overflow: 'break'
}
}, },
textAlign: 'center', textAlign: 'center',
left: '48%', left: '48%',
top: '32%' top: '32%',
width: 100,
overflow: 'break'
}, },
{ {
left: '48%', left: '48%',

View File

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