1
0
mirror of https://github.com/jumpserver/lina.git synced 2025-05-12 10:08:52 +00:00

perf: 优化 card table

This commit is contained in:
ibuler 2025-03-06 16:21:40 +08:00
parent ae7e4b6ddf
commit a6210c9add
13 changed files with 427 additions and 324 deletions
public
src
components
Form/AutoDataForm
Table
AutoDataTable
CardTable
styles
utils
views/assets/Cloud/Account

View File

@ -1,6 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<head>
<meta charset="utf-8">
<meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible">
<meta content="0" http-equiv="Expires">
@ -20,52 +20,71 @@
display: flex;
justify-content: center;
align-items: center;
background-color: white;
background-color: rgba(255, 255, 255, 0.98);
z-index: 9999;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(0, 0, 0, 0.1);
width: 40px;
height: 40px;
border: 3px solid transparent;
border-top-color: var(--color-primary);
border-radius: 50%;
border-top-color: #3498db;
animation: spin 1s infinite linear;
animation: spin 1s linear infinite;
}
.spinner::after {
content: '';
position: absolute;
top: -3px;
left: -3px;
width: 40px;
height: 40px;
border: 3px solid transparent;
border-top-color: rgba(64, 158, 255, 0.2);
border-radius: 50%;
animation: spin 2s linear infinite;
}
@keyframes spin {
to {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<script>
window.onload = function () {
if (location.pathname === '/') {
location.pathname = '/ui/'
}
const pathname = window.location.pathname
if (pathname.startsWith('/core')) {
return
}
if (pathname.indexOf('/ui') === -1) {
window.location.href = window.location.origin + '/ui/#' + pathname
}
if (pathname.startsWith('/ui/#/chat')) {
window.location.href = window.location.origin + pathname
}
}
</script>
<div id="app">
</div>
<div id="loading">
<div class="spinner"></div>
</div>
<!-- built files will be auto injected -->
</body>
</head>
<body>
<noscript>
<strong>
We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled.
Please enable it to continue.
</strong>
</noscript>
<script>
window.onload = function () {
if (location.pathname === '/') {
location.pathname = '/ui/'
}
const pathname = window.location.pathname
if (pathname.startsWith('/core')) {
return
}
if (pathname.indexOf('/ui') === -1) {
window.location.href = window.location.origin + '/ui/#' + pathname
}
if (pathname.startsWith('/ui/#/chat')) {
window.location.href = window.location.origin + pathname
}
}
</script>
<div id="app">
</div>
<div id="loading">
<div class="spinner"></div>
</div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,5 +1,5 @@
<template>
<div v-loading="loading">
<div>
<DataForm
v-if="!loading"
ref="dataForm"

View File

@ -1,5 +1,5 @@
<template>
<div v-loading="loading">
<div>
<DataTable
v-if="!loading"
ref="dataTable"

View File

@ -0,0 +1,150 @@
<template>
<div>
<span v-if="d.edition === 'enterprise'" class="enterprise">
{{ $t('Enterprise') }}
</span>
<el-row class="panel">
<el-col v-if="d.icon" :span="8" class="image">
<img
v-if="d.icon.startsWith('/') || d.icon.startsWith('data:')"
:alt="d.display_name"
:src="d.icon"
>
<Icon v-else :icon="d.icon" />
</el-col>
<el-col :span="16" class="text-zone">
<div class="one-line">
<b>{{ d.display_name }}</b>
<el-tag v-if="d.version" size="mini" style="margin-left: 5px">
{{ d.version }}
</el-tag>
</div>
<div :title="d.comment " class="comment">
{{ d.comment }}
</div>
<div class="tag-zone">
<el-tag v-for="tag of d.tags" :key="tag" size="mini">
{{ capitalize(tag) }}
</el-tag>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import Icon from '@/components/Widgets/Icon/index.vue'
export default {
name: 'Panel',
components: { Icon },
props: {
d: {
type: Object,
required: true
}
},
data() {
return {}
},
methods: {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
}
</script>
<style lang="scss" scoped>
.panel {
display: flex;
flex-wrap: nowrap;
margin-top: 0;
height: 100%;
.image {
display: flex;
justify-content: center;
align-items: center;
img {
width: 60px;
height: 60px;
object-fit: contain;
}
svg {
width: 40px;
height: 40px;
}
}
.text-zone {
display: flex;
flex-direction: column;
align-items: flex-start;
.one-line {
display: flex;
flex-wrap: wrap;
align-items: center;
padding-top: 10px;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
b {
padding-right: 5px;
}
span {
margin-left: 0 !important;
}
}
.comment {
display: -webkit-box;
height: 120px;
font-size: 12px;
padding: 10px 0;
cursor: pointer;
overflow: hidden;
-webkit-line-clamp: 4;
-webkit-box-flex: 1;
-webkit-box-orient: vertical;
text-align: left;
}
.tag-zone {
display: flex;
height: 30%;
align-items: center;
cursor: pointer;
}
}
}
.enterprise {
position: absolute;
right: -1px;
top: -1px;
background-color: var(--color-primary);
color: #fff;
padding: 3px 8px 4px 9px;
font-size: 13px;
border-radius: 3px 3px 3px 8px;
}
.tag-zone {
margin-top: 10px;
.el-tag {
margin-right: 3px;
}
}
.text-zone {
text-align: left;
height: 100%;
}
</style>

View File

@ -6,8 +6,8 @@
:table-url="tableUrl"
v-bind="headerActions"
/>
<el-row :gutter="10" class="the-row">
<IBox v-if="totalData.length === 0">
<el-row v-loading="loading" class="the-row">
<IBox v-if="totalData.length === 0" class="empty-box">
<el-empty :description="$t('NoData')" :image-size="200" class="no-data" style="padding: 20px" />
</IBox>
<div class="card-container">
@ -22,35 +22,7 @@
>
<keep-alive>
<slot :index="index" :item="d">
<span v-if="d.edition === 'enterprise'" class="enterprise">
{{ $t('Enterprise') }}
</span>
<el-row>
<el-col v-if="d.icon" :span="8" class="image">
<img
v-if="d.icon.startsWith('/') || d.icon.startsWith('data:')"
:alt="d.display_name"
:src="d.icon"
>
<Icon v-else :icon="d.icon" />
</el-col>
<el-col :span="16" class="text-zone">
<div class="one-line">
<b>{{ d.display_name }}</b>
<el-tag v-if="d.version" size="mini" style="margin-left: 5px">
{{ d.version }}
</el-tag>
</div>
<div :title="d.comment " class="comment">
{{ d.comment }}
</div>
<div class="tag-zone">
<el-tag v-for="tag of d.tags" :key="tag" size="mini">
{{ capitalize(tag) }}
</el-tag>
</div>
</el-col>
</el-row>
<Panel :d="d" />
</slot>
</keep-alive>
</el-card>
@ -68,11 +40,11 @@
</template>
<script>
import TableAction from '@/components/Table/ListTable/TableAction'
import { Pagination } from '@/components'
import Icon from '@/components/Widgets/Icon/index.vue'
import { mapGetters } from 'vuex'
import { Pagination } from '@/components'
import TableAction from '@/components/Table/ListTable/TableAction'
import IBox from '@/components/Common/IBox/index.vue'
import Panel from './Panel'
const defaultFirstPage = 1
@ -80,9 +52,9 @@ export default {
name: 'CardTable',
components: {
IBox,
Panel,
TableAction,
Pagination,
Icon
Pagination
},
props: {
// table
@ -120,6 +92,7 @@ export default {
paginationSize: 6,
paginationLayout: 'total, sizes, prev, pager, next',
paginationSizes: [6, 18, 27],
loading: true,
axiosConfig: {
raw: 1,
params: {
@ -135,16 +108,17 @@ export default {
return this.tableConfig.url || ''
}
},
mounted() {
this.getList()
async mounted() {
try {
await this.getList()
} finally {
this.loading = false
}
},
methods: {
isDisabled(item) {
return item.edition?.value === 'enterprise' && !this.hasValidLicense
},
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
},
getIcon(status) {
let iconClass = 'fa-check-circle'
if (status === false) {
@ -155,7 +129,7 @@ export default {
getPageQuery(currentPage, pageSize) {
return this.$refs.pagination.getPageQuery(currentPage, pageSize)
},
getList() {
async getList() {
if (this.tableConfig.totalData) {
this.totalData = this.tableConfig.totalData
this.total = this.totalData.length
@ -169,16 +143,10 @@ export default {
const queryString = Object.keys(query).map(key => key + '=' + query[key]).join('&')
const url = `${this.tableUrl}?${queryString}`
this.$axios
.get(url, this.axiosConfig)
.then(({ data: resp }) => {
this.total = resp?.count || 0
this.totalData = resp?.results || []
})
.catch(err => {
this.$log.error('Error occur: ', err)
this.total = 0
})
const resp = await this.$axios.get(url, this.axiosConfig)
const data = resp.data
this.total = data?.count || 0
this.totalData = data?.results || []
},
reloadTable() {
this.getList()
@ -240,6 +208,19 @@ export default {
</script>
<style lang="scss" scoped>
.the-row .empty-box {
display: block;
::v-deep {
.el-empty {
margin: 0 auto;
.el-empty__image {
}
}
}
}
.the-row {
margin-top: 15px;
max-width: 1600px;
@ -258,74 +239,6 @@ export default {
::v-deep .el-card__body {
height: 100%;
.el-row {
display: flex;
flex-wrap: nowrap;
margin-top: 0;
height: 100%;
.image {
display: flex;
justify-content: center;
align-items: center;
img {
width: 60px;
height: 60px;
object-fit: contain;
}
svg {
width: 40px;
height: 40px;
}
}
.text-zone {
display: flex;
flex-direction: column;
align-items: flex-start;
.one-line {
display: flex;
flex-wrap: wrap;
align-items: center;
padding-top: 10px;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
b {
padding-right: 5px;
}
span {
margin-left: 0 !important;
}
}
.comment {
display: -webkit-box;
height: 120px;
font-size: 12px;
padding: 10px 0;
cursor: pointer;
overflow: hidden;
-webkit-line-clamp: 4;
-webkit-box-flex: 1;
-webkit-box-orient: vertical;
text-align: left;
}
.tag-zone {
display: flex;
height: 30%;
align-items: center;
cursor: pointer;
}
}
}
}
&.is-disabled {
@ -353,30 +266,6 @@ export default {
}
}
.enterprise {
position: absolute;
right: -1px;
top: -1px;
background-color: var(--color-primary);
color: #fff;
padding: 3px 8px 4px 9px;
font-size: 13px;
border-radius: 3px 3px 3px 8px;
}
.tag-zone {
margin-top: 10px;
.el-tag {
margin-right: 3px;
}
}
.text-zone {
text-align: left;
height: 100%;
}
.pagination {
padding-top: 10px;
border-top: 1px solid #e7eaec;

View File

@ -696,5 +696,5 @@ li.rmenu i.fa {
}
div.el-loading-parent--relative {
min-height: 200px;
min-height: 150px;
}

View File

@ -143,6 +143,9 @@ function customizer(objValue, srcValue) {
export function newURL(url) {
let obj
if (!url) {
return ''
}
if (url.indexOf('//') > -1) {
obj = new URL(url)
} else {

View File

@ -1,23 +1,8 @@
<template>
<div>
<SmallCard ref="table" class="account-table" v-bind="$data" />
<SmallCard ref="table" class="account-table" v-bind="table" />
<CreateDialog v-if="visible" :visible.sync="visible" v-bind="providerConfig" />
<Drawer
v-if="updateVisible"
:destroy-on-close="true"
:show-buttons="false"
:title="$tc('CloudAccountUpdate')"
:visible.sync="updateVisible"
v-on="$listeners"
>
<AuthPanel
:object="object"
:provider="object.provider.value"
:visible.sync="updateVisible"
origin="update"
@submitSuccess="onSubmitSuccess"
/>
</Drawer>
<UpdateDialog v-if="updateVisible" :object="object" :update-visible="updateVisible" />
<Dialog
:close-on-click-modal="false"
:close-on-press-escape="false"
@ -62,94 +47,128 @@ import {
zstack
} from '../const'
import CreateDialog from './components/CreateDialog.vue'
import UpdateDialog from './components/UpdateDialog.vue'
import SmallCard from '@/components/Table/CardTable/DataCardTable/index.vue'
import { ACCOUNT_PROVIDER_ATTRS_MAP } from '@/views/assets/Cloud/const'
import Dialog from '@/components/Dialog/index.vue'
import Drawer from '@/components/Drawer/index.vue'
import AssetPanel from './components/AssetPanel.vue'
import AuthPanel from './components/AuthPanel.vue'
import { toSafeLocalDateStr } from '@/utils/time'
export default {
name: 'CloudAccountList',
components: {
Drawer,
AuthPanel,
AssetPanel,
Dialog,
SmallCard,
CreateDialog
CreateDialog,
UpdateDialog
},
data() {
const vm = this
return {
object: null,
tableConfig: {
url: '/api/v1/xpack/cloud/accounts/',
permissions: {
app: 'xpack',
resource: 'account'
}
},
headerActions: {
hasImport: false,
hasExport: false,
hasColumnSetting: false,
hasMoreActions: false,
searchConfig: {
getUrlQuery: false
table: {
tableConfig: {
url: '/api/v1/xpack/cloud/accounts/',
permissions: {
app: 'xpack',
resource: 'account'
}
},
moreCreates: {
loading: false,
callback: (option) => {
vm.$router.push({ name: 'AccountCreate', query: { provider: option.name }})
subComponentProps: {
handleUpdate: (obj) => {
this.object = obj
this.updateVisible = true
},
dropdown: [
{
name: 'publicCloud',
title: this.$t('PublicCloud'),
icon: 'public-cloud',
callback: () => {
const providers = [
aliyun, qcloud, qcloud_lighthouse, huaweicloud,
baiducloud, jdcloud, kingsoftcloud, aws_china,
aws_international, azure, azure_international,
gcp, ucloud, volcengine
]
this.providerConfig.providers = providers.map(
(item) => ACCOUNT_PROVIDER_ATTRS_MAP[item]
)
this.visible = true
getImage: (obj) => {
return ACCOUNT_PROVIDER_ATTRS_MAP[obj.provider.value].image
},
getInfos: (obj) => {
return [
{
title: this.$tc('TotalSyncRegion'),
content: obj?.task.regions.length
},
{
title: this.$tc('TotalSyncAsset'),
content: obj?.task.instance_count
},
{
title: this.$tc('DateLastSync'),
content: toSafeLocalDateStr(obj?.task.date_last_sync)
}
},
]
},
actions: [
{
name: 'privateCloud',
icon: 'private-cloud',
title: this.$t('PrivateCloud'),
callback: () => {
const providers = [
vmware, qingcloud_private, huaweicloud_private, state_private,
openstack, zstack, nutanix, fc, scp, apsara_stack
]
this.providerConfig.providers = providers.map(
(item) => ACCOUNT_PROVIDER_ATTRS_MAP[item]
)
this.visible = true
}
},
{
name: 'LAN',
title: this.$t('LAN'),
icon: 'computer',
callback: () => {
const providers = [lan]
this.providerConfig.providers = providers.map(
(item) => ACCOUNT_PROVIDER_ATTRS_MAP[item]
)
this.visible = true
}
id: 'online-sync',
name: this.$tc('SyncOnline'),
icon: 'el-icon-thumb',
callback: this.handleOnlineExecute,
disabled: !this.$hasPerm('xpack.change_syncinstancetask')
}
]
},
headerActions: {
hasImport: false,
hasExport: false,
hasColumnSetting: false,
hasMoreActions: false,
searchConfig: {
getUrlQuery: false
},
moreCreates: {
loading: false,
callback: (option) => {
vm.$router.push({ name: 'AccountCreate', query: { provider: option.name }})
},
dropdown: [
{
name: 'publicCloud',
title: this.$t('PublicCloud'),
icon: 'public-cloud',
callback: () => {
const providers = [
aliyun, qcloud, qcloud_lighthouse, huaweicloud,
baiducloud, jdcloud, kingsoftcloud, aws_china,
aws_international, azure, azure_international,
gcp, ucloud, volcengine
]
this.providerConfig.providers = providers.map(
(item) => ACCOUNT_PROVIDER_ATTRS_MAP[item]
)
this.visible = true
}
},
{
name: 'privateCloud',
icon: 'private-cloud',
title: this.$t('PrivateCloud'),
callback: () => {
const providers = [
vmware, qingcloud_private, huaweicloud_private, state_private,
openstack, zstack, nutanix, fc, scp, apsara_stack
]
this.providerConfig.providers = providers.map(
(item) => ACCOUNT_PROVIDER_ATTRS_MAP[item]
)
this.visible = true
}
},
{
name: 'LAN',
title: this.$t('LAN'),
icon: 'computer',
callback: () => {
const providers = [lan]
this.providerConfig.providers = providers.map(
(item) => ACCOUNT_PROVIDER_ATTRS_MAP[item]
)
this.visible = true
}
}
]
}
}
},
providerConfig: {
@ -157,41 +176,7 @@ export default {
},
visible: false,
updateVisible: false,
onlineSyncVisible: false,
subComponentProps: {
handleUpdate: (obj) => {
this.object = obj
this.updateVisible = true
},
getImage: (obj) => {
return ACCOUNT_PROVIDER_ATTRS_MAP[obj.provider.value].image
},
getInfos: (obj) => {
return [
{
title: this.$tc('TotalSyncRegion'),
content: obj?.task.regions.length
},
{
title: this.$tc('TotalSyncAsset'),
content: obj?.task.instance_count
},
{
title: this.$tc('DateLastSync'),
content: toSafeLocalDateStr(obj?.task.date_last_sync)
}
]
},
actions: [
{
id: 'online-sync',
name: this.$tc('SyncOnline'),
icon: 'el-icon-thumb',
callback: this.handleOnlineExecute,
disabled: !this.$hasPerm('xpack.change_syncinstancetask')
}
]
}
onlineSyncVisible: false
}
},
watch: {

View File

@ -1,5 +1,5 @@
<template>
<div>
<div class="asset-panel">
<el-alert
:center="false"
:closable="true"
@ -9,8 +9,8 @@
</el-alert>
<ImportTable
ref="importTable"
v-bind="settings"
origin="cloudSync"
v-bind="settings"
@cancel="closeDialog"
@finish="showResult"
/>
@ -162,3 +162,9 @@ export default {
}
}
</script>
<style lang="scss" scoped>
.asset-panel {
display: block;
}
</style>

View File

@ -22,15 +22,17 @@
</el-steps>
</el-col>
<el-col :span="18">
<component
:is="activeMenu"
:active.sync="active"
:object.sync="account"
:provider="iSelected"
:providers="providers"
:selected.sync="iSelected"
:visible.sync="iVisible"
/>
<div class="right-content">
<component
:is="activeMenu"
:active.sync="active"
:object.sync="account"
:provider="iSelected"
:providers="providers"
:selected.sync="iSelected"
:visible.sync="iVisible"
/>
</div>
</el-col>
</el-row>
</Drawer>
@ -43,10 +45,12 @@ import AuthPanel from '@/views/assets/Cloud/Account/components/AuthPanel'
import AssetPanel from '@/views/assets/Cloud/Account/components/AssetPanel'
import ResultPanel from '@/views/assets/Cloud/Account/components/ResultPanel'
import { ACCOUNT_PROVIDER_ATTRS_MAP } from '@/views/assets/Cloud/const'
import IBox from '@/components/Common/IBox/index.vue'
export default {
name: 'CreateDialog',
components: {
IBox,
Drawer,
AuthPanel,
AssetPanel,
@ -118,6 +122,11 @@ export default {
}
}
.right-content {
background-color: #fff;
padding: 20px 10px;
}
::v-deep .el-step {
.el-step__head {
&.is-process {

View File

@ -83,7 +83,7 @@ export default {
}
.cloud-select-wrap {
height: 300px;
background: #f3f3f3;
.el-row {
flex-direction: column;

View File

@ -1,23 +1,18 @@
<template>
<div>
<div class="result-panel">
<el-table
:data="tableData"
:show-header="false"
size="medium"
>
<el-table-column
prop="key"
width="120"
/>
<el-table-column
prop="value"
>
<el-table-column prop="key" width="120" />
<el-table-column prop="value">
<template v-slot="scope">
<el-link :underline="false" type="primary"> {{ scope.row.value }}</el-link>
<el-link :underline="false"> {{ scope.row.value }}</el-link>
</template>
</el-table-column>
</el-table>
<div style="float: right; margin: 10px">
<div style="margin-top: 20px">
<el-button size="small" type="primary" @click="handleClick">{{ $t('CloudAccountDetail') }}</el-button>
<el-button size="small" @click="handleClose">{{ $t('Close') }}</el-button>
</div>
@ -25,7 +20,6 @@
</template>
<script>
export default {
name: 'ResultPanel',
components: {},
@ -67,6 +61,10 @@ export default {
</script>
<style lang='scss' scoped>
.result-panel {
padding: 10px;
}
::v-deep .el-alert__content {
width: 100%;
}

View File

@ -0,0 +1,44 @@
<template>
<Drawer
v-if="updateVisible"
:destroy-on-close="true"
:show-buttons="false"
:title="$tc('CloudAccountUpdate')"
:visible.sync="updateVisible"
v-on="$listeners"
>
<div style="background: white">
<AuthPanel
:object="object"
:provider="object.provider.value"
:visible.sync="updateVisible"
origin="update"
@submitSuccess="onSubmitSuccess"
/>
</div>
</Drawer>
</template>
<script>
import AuthPanel from '@/views/assets/Cloud/Account/components/AuthPanel.vue'
import Drawer from '@/components/Drawer/index.vue'
export default {
name: 'UpdateDialog',
components: { Drawer, AuthPanel },
props: {
object: {
type: Object,
required: true
},
updateVisible: {
type: Boolean,
default: false
}
}
}
</script>
<style lang="scss" scoped>
</style>