Compare commits

..

8 Commits

Author SHA1 Message Date
fit2bot
ce57c6d0a9 feat: Update v2.28.8 2023-03-02 16:24:00 +08:00
Bai
171ceeef3a fix: 修改翻译 2023-03-02 12:25:56 +08:00
吴小白
754e861294 Merge pull request #2370 from jumpserver/pr@v2.28@fix_ticket_create_hasclose
fix: 修复创建工单后没有关闭操作问题;修复工单详情控制台有报错提示问题
2023-01-10 11:48:28 +08:00
“huailei000”
e7b37d43f2 fix: 修复创建工单后没有关闭操作问题;修复工单详情控制台有报错提示问题 2023-01-10 11:22:16 +08:00
jiangweidong
fd749ef211 perf: OpenID支持PKCE方式对接 2022-12-13 16:13:50 +08:00
jiangweidong
5667c39bcb fix: 解决命令过滤获取不到一些应用的系统用户问题 2022-11-24 09:37:57 +08:00
jiangweidong
982ce90a9a fix: 去掉clickhouse改密页面入口 2022-11-22 18:39:40 +08:00
吴小白
86ce758adb feat: 更新 action node 版本 2022-11-17 21:56:54 +08:00
674 changed files with 19001 additions and 27316 deletions

View File

@@ -4,17 +4,11 @@ root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{js,jsx,ts,tsx,vue}]
indent_size = 2
[*.py]
indent_size = 4
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

View File

@@ -1,32 +0,0 @@
name: "Run Build Test"
on:
push:
branches:
- pr@*
- repr@*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: docker/setup-qemu-action@v2
- uses: docker/setup-buildx-action@v2
- uses: docker/build-push-action@v3
with:
context: .
push: false
tags: jumpserver/lina:test
file: Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
- uses: LouisBrunner/checks-action@v1.5.0
if: always()
with:
token: ${{ secrets.GITHUB_TOKEN }}
name: Check Build
conclusion: ${{ job.status }}

View File

@@ -39,7 +39,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- name: Build it and upload
uses: jumpserver/action-build-upload-assets@node10
uses: jumpserver/action-build-upload-assets@node14.16
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:

1
.gitignore vendored
View File

@@ -16,4 +16,3 @@ tests/**/coverage/
*.njsproj
*.sln
.env.development
.python-version

View File

@@ -1,22 +1,24 @@
FROM node:14.16 as stage-build
ARG TARGETARCH
ARG NPM_REGISTRY="https://registry.npmmirror.com"
ENV NPM_REGISTY=$NPM_REGISTRY
WORKDIR /data
RUN set -ex \
&& npm config set registry ${NPM_REGISTRY} \
&& yarn config set registry ${NPM_REGISTRY}
&& yarn config set registry ${NPM_REGISTRY} \
&& yarn config set cache-folder /root/.cache/yarn/lina
ADD package.json yarn.lock /data
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked,id=lina \
RUN --mount=type=cache,target=/root/.cache/yarn \
yarn install
ARG VERSION
ENV VERSION=$VERSION
ADD . /data
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn,sharing=locked,id=lina \
sed -i "s@version-dev@${VERSION}@g" src/layout/components/NavHeader/About.vue \
RUN --mount=type=cache,target=/root/.cache/yarn \
sed -i "s@Version <strong>.*</strong>@Version <strong>${VERSION}</strong>@g" src/layout/components/Footer/index.vue \
&& yarn build
FROM nginx:alpine

2
GITSHA
View File

@@ -1 +1 @@
cdf46e6369d783ba5e42a2e61e88f43c5312ec80
171ceeef3af2be631e4a9a865e944ecfbeaed022

View File

@@ -12,16 +12,13 @@
"build:stage": "vue-cli-service build --mode staging",
"preview": "node build/index.js --preview",
"lint": "eslint --ext .js,.vue src",
"fix": "eslint --ext .js,.vue --fix src",
"test:unit": "jest --clearCache && vue-cli-service test:unit",
"test:ci": "npm run lint && npm run test:unit",
"svgo": "svgo -f src/icons/svg --config=src/icas/svgo.yml",
"vue-i18n-extract": "vue-i18n-extract",
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'",
"vue-i18n-report-json": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -o /tmp/abc.json",
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a",
"diff-i18n": "python ./src/i18n/langs/i18n-util.py diff en ja",
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja"
"vue-i18n-report-add-miss": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json' -a"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
@@ -61,19 +58,17 @@
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"vue": "2.6.10",
"vue-codemirror": "4.0.6",
"vue-codemirror-lite": "^1.0.4",
"vue-cookie": "^1.1.4",
"vue-echarts": "^5.0.0-beta.0",
"vue-i18n": "^8.15.5",
"vue-json-editor": "^1.4.3",
"vue-markdown": "^2.2.4",
"vue-moment": "^4.1.0",
"vue-password-strength-meter": "^1.7.2",
"vue-router": "3.0.6",
"vue-select": "^3.9.5",
"vuejs-logger": "^1.5.4",
"vuex": "3.1.0",
"xss": "^1.0.14",
"xterm": "^4.5.0",
"xterm-addon-fit": "^0.3.0",
"zxcvbn": "^4.4.2"
@@ -97,7 +92,6 @@
"eslint": "^5.15.3",
"eslint-plugin-vue": "5.2.2",
"eslint-plugin-vue-i18n": "^0.3.0",
"github-markdown-css": "^5.1.0",
"html-webpack-plugin": "3.2.0",
"husky": "^4.2.3",
"less-loader": "^5.0.0",

View File

@@ -17,15 +17,9 @@
</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
const baseUrl = "/ui/"
if (location.pathname === '/' && baseUrl !== '/') {
location.pathname = baseUrl
}
}
</script>

View File

@@ -194,16 +194,17 @@ td .el-button.el-button--mini {
line-height: 34px;
}
.option-group .el-select-dropdown__item.hover, .option-group .el-select-dropdown__item.selected {
.el-select-dropdown.is-multiple .el-select-dropdown__item.selected {
color: #606266;
background-color: #ddd;
font-weight: 400;
}
.el-select-dropdown__item.hover {
background-color: primary;
color: white;
}
.option-group:has(.hover) .el-select-dropdown__item.selected {
background-color: light-2;
}
.el-select-dropdown__item.is-disabled:hover{
color:#c0c4cc;
}
@@ -214,7 +215,7 @@ td .el-button.el-button--mini {
.el-select-dropdown.is-multiple .el-select-dropdown__item.selected.hover {
color: white;
background-color: light-4;
background-color: primary;
}
.el-tag.el-tag--info {
@@ -330,12 +331,13 @@ td .el-button.el-button--mini {
}
.el-table .cell,
.el-table td:first-child .cell,
.el-table th:first-child .cell {
.el-table--border td:first-child .cell,
.el-table--border th:first-child .cell {
padding-left: 10px;
padding-right: 14px;
}
.el-tag--default.el-tag--dark {
background-color: #d1dade;
color: #5e5e5e;
@@ -451,48 +453,11 @@ td .el-button.el-button--mini {
.el-select-dropdown__item.selected {
font-weight: 400;
color: white;
background-color: primary;
color: #606266;
background-color: #ddd;
}
.el-input-group__prepend div.el-select .el-input__inner,
.el-input-group__prepend div.el-select .el-input__inner:hover {
color: #303133;
}
.el-input-group__append, .el-input-group__prepend {
color: primary
}
.el-input.is-disabled .el-input__inner {
color: $--color-text-primary;
cursor: not-allowed;
}
.el-step__description.is-finish {
color: #676a6c;
}
.el-alert {
border: solid 1px #e7eaec;
}
.el-alert.el-alert--success.is-light {
border-color: var(--color-success-light);
}
.el-alert.el-alert--primary.is-light {
border-color: var(--color-primary-light);
}
.el-alert.el-alert--info.is-light {
border-color: var(--color-info-light);
}
.el-alert.el-alert--warning.is-light {
border-color: var(--color-warning-light);
}
.el-alert.el-alert--error.is-light {
border-color: var(--color-danger-light);
}

File diff suppressed because one or more lines are too long

View File

@@ -40,10 +40,3 @@ export function getCommandFilterList(data) {
})
}
export function getCategoryTypes() {
return request({
url: '/api/v1/assets/categories/',
method: 'get'
})
}

View File

@@ -1,13 +1,6 @@
import request from '@/utils/request'
export function createSourceIdCache(ids) {
ids = ids.map(item => {
if (typeof item === 'object' && item.id) {
return item.id
} else {
return item
}
})
return request({
url: '/api/v1/common/resources/cache/',
method: 'post',

View File

@@ -1,5 +1,12 @@
import request from '@/utils/request'
export function getTaskDetail(id) {
return request({
url: `/api/v1/ops/tasks/${id}/`,
method: 'get'
})
}
export function getAdhocDetail(id) {
return request({
url: `/api/v1/ops/adhoc/${id}/`,
@@ -13,34 +20,3 @@ export function getHistoryExecutionDetail(id) {
method: 'get'
})
}
export function getTaskDetail(id) {
return request({
url: `/api/v1/ops/job-execution/task-detail/${id}/`,
method: 'get'
})
}
export function getJob(id) {
return request({
url: `/api/v1/ops/jobs/${id}/`,
method: 'get'
})
}
export function uploadPlaybook(form) {
return request({
url: '/api/v1/ops/playbooks/',
method: 'post',
headers: { 'Content-Type': 'multipart/form-data' },
data: form
})
}
export function renameFile(playbookId, node) {
return request({
url: `/api/v1/ops/playbook/${playbookId}/file/`,
method: 'patch',
data: node
})
}

View File

@@ -0,0 +1,43 @@
import request from '@/utils/request'
export function getAssetPermissionDetail(id) {
return request({
url: `/api/v1/perms/asset-permissions/${id}/`,
method: 'get'
})
}
export function getRemoteAppPermissionDetail(id) {
return request({
url: `/api/v1/perms/remote-app-permissions/${id}/`,
method: 'get'
})
}
export function getDatabaseAppPermissionDetail(id) {
return request({
url: `/api/v1/perms/database-app-permissions/${id}/`,
method: 'get'
})
}
export function getUserAssetGrantedSystemUsers(userId, assetId) {
return request({
url: `/api/v1/perms/users/${userId}/assets/${assetId}/system-users/?cache_policy=1`,
method: 'get'
})
}
export function getMyAssetGrantedSystemUsers(userId, assetId) {
return request({
url: `/api/v1/perms/users/assets/${assetId}/system-users/?cache_policy=1`,
method: 'get'
})
}
export function getUserGroupAssetGrantedSystemUsers(gId, assetId) {
return request({
url: `/api/v1/perms/user-groups/${gId}/assets/${assetId}/system-users/?cache_policy=1`,
method: 'get'
})
}

BIN
src/assets/img/admin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,257 +0,0 @@
<template>
<AutoDataForm
v-if="!loading"
ref="AutoDataForm"
v-bind="$data"
@submit="confirm"
/>
</template>
<script>
import AutoDataForm from '@/components/AutoDataForm'
import { UpdateToken } from '@/components/FormFields'
import Select2 from '@/components/FormFields/Select2'
import AssetSelect from '@/components/AssetSelect'
import { encryptPassword } from '@/utils/crypto'
import { RequiredChange, Required } from '@/components/DataForm/rules'
export default {
name: 'AccountCreateForm',
components: {
AutoDataForm
},
props: {
asset: {
type: Object,
default: null
},
platform: {
type: Object,
default: null
},
account: {
type: Object,
default: null
},
// 默认组件密码加密
encryptPassword: {
type: Boolean,
default: true
}
},
data() {
return {
loading: true,
usernameChanged: false,
defaultPrivilegedAccounts: ['root', 'administrator'],
iPlatform: {
automation: {},
protocols: [
{
name: 'ssh',
secret_types: ['password', 'ssh_key', 'token', 'api_key']
}
]
},
url: '/api/v1/accounts/accounts/',
form: this.account || {},
encryptedFields: ['secret'],
fields: [
[this.$t('assets.Asset'), ['assets']],
[this.$t('common.Basic'), ['name', 'username', ...this.controlShowField()]],
[this.$t('assets.Secret'), [
'secret_type', 'secret', 'ssh_key', 'token',
'api_key', 'passphrase'
]],
[this.$t('common.Other'), ['push_now', 'is_active', 'comment']]
],
fieldsMeta: {
assets: {
rules: [Required],
component: AssetSelect,
label: this.$t('assets.Asset'),
el: {
multiple: false
},
hidden: () => {
return this.platform || this.asset
}
},
name: {
rules: [RequiredChange],
on: {
input: ([value], updateForm) => {
if (!this.usernameChanged) {
if (!this.account?.name) {
updateForm({ username: value })
}
const maybePrivileged = this.defaultPrivilegedAccounts.includes(value)
if (maybePrivileged) {
updateForm({ privileged: true })
}
}
}
}
},
username: {
el: {
disabled: !!this.account?.name
},
on: {
input: ([value], updateForm) => {
this.usernameChanged = true
},
change: ([value], updateForm) => {
const maybePrivileged = this.defaultPrivilegedAccounts.includes(value)
if (maybePrivileged) {
updateForm({ privileged: true })
}
}
}
},
su_from: {
component: Select2,
hidden: (formValue) => {
return !this.asset?.id
},
el: {
multiple: false,
clearable: true,
ajax: {
url: `/api/v1/accounts/accounts/su-from-accounts/?account=${this.account?.id || ''}&asset=${this.asset?.id || ''}`,
transformOption: (item) => {
return { label: `${item.name}(${item.username})`, value: item.id }
}
}
}
},
secret: {
label: this.$t('assets.Password'),
component: UpdateToken,
hidden: (formValue) => formValue.secret_type !== 'password'
},
ssh_key: {
label: this.$t('assets.PrivateKey'),
el: {
type: 'textarea',
rows: 4
},
hidden: (formValue) => formValue.secret_type !== 'ssh_key'
},
passphrase: {
label: this.$t('assets.Passphrase'),
component: UpdateToken,
hidden: (formValue) => formValue.secret_type !== 'ssh_key'
},
token: {
label: this.$t('assets.Token'),
el: {
type: 'textarea',
rows: 4
},
hidden: (formValue) => formValue.secret_type !== 'token'
},
api_key: {
id: 'api_key',
label: this.$t('assets.AccessKey'),
el: {
type: 'textarea',
rows: 4
},
hidden: (formValue) => formValue.secret_type !== 'api_key'
},
secret_type: {
type: 'radio-group',
options: []
},
push_now: {
hidden: () => {
const automation = this.iPlatform.automation || {}
return !automation.push_account_enabled || !automation.ansible_enabled || !this.$hasPerm('accounts.push_account')
}
}
},
hasSaveContinue: false
}
},
async mounted() {
try {
await this.getPlatform()
this.setSecretTypeOptions()
} finally {
this.loading = false
}
},
methods: {
async getPlatform() {
if (this.platform) {
this.iPlatform = this.platform
}
if (!this.asset || !this.asset.platform) {
return
}
const platformId = this.asset.platform.id
this.iPlatform = await this.$axios.get(`/api/v1/assets/platforms/${platformId}/`)
},
setSecretTypeOptions() {
const choices = [
{
label: this.$t('assets.Password'),
value: 'password'
},
{
label: this.$t('assets.SSHKey'),
value: 'ssh_key'
},
{
label: this.$t('assets.Token'),
value: 'token'
},
{
label: this.$t('assets.AccessKey'),
value: 'api_key'
}
]
const secretTypes = []
this.iPlatform.protocols?.forEach(p => {
secretTypes.push(...p['secret_types'])
})
if (!this.form.secret_type) {
this.form.secret_type = secretTypes[0]
}
this.fieldsMeta.secret_type.options = choices.filter(item => {
return secretTypes.indexOf(item.value) > -1
})
},
controlShowField() {
const privileged = ['privileged']
let suFrom = ['su_from']
const filterSuFrom = ['database', 'device', 'cloud', 'web']
const asset = this?.asset || {}
if (filterSuFrom.includes(asset?.category?.value)) {
suFrom = []
}
return [...privileged, ...suFrom]
},
confirm(form) {
const secretType = form.secret_type || ''
if (secretType !== 'password') {
form.secret = form[secretType]
delete form[secretType]
}
form.secret = this.encryptPassword ? encryptPassword(form.secret) : form.secret
if (!form.secret) {
delete form['secret']
}
if (this.account?.name) {
this.$emit('edit', form)
} else {
this.$emit('add', form)
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,156 @@
<template>
<div>
<GenericListPage :table-config="tableConfig" :header-actions="headerActions" :help-message="title" />
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
</div>
</template>
<script>
import { ActionsFormatter, DetailFormatter, DisplayFormatter } from '@/components/TableFormatters'
import ShowSecretInfo from '../AccountListTable/ShowSecretInfo'
import { GenericListPage } from '@/layout/components'
export default {
name: 'AccountHistoryTable',
components: {
GenericListPage,
ShowSecretInfo
},
props: {
url: {
type: String,
required: true
},
exportUrl: {
type: String,
default() {
return this.url.replace('/assets/accounts-history/', '/assets/account-history-secrets/')
}
},
hasLeftActions: {
type: Boolean,
default: false
},
otherActions: {
type: Array,
default: null
},
hasClone: {
type: Boolean,
default: false
}
},
data() {
const vm = this
return {
showViewSecretDialog: false,
account: {},
tableConfig: {
url: this.url,
permissions: {
app: 'assets',
resource: 'authbook'
},
columns: [
'hostname', 'ip', 'username', 'version',
'systemuser', 'date_created', 'date_updated', 'actions'
],
columnsShow: {
min: ['username', 'actions'],
default: ['hostname', 'ip', 'username', 'version', 'actions']
},
columnsMeta: {
hostname: {
prop: 'hostname',
label: this.$t('assets.Hostname'),
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
can: this.$hasPerm('assets.view_asset'),
getRoute({ row }) {
return {
name: 'AssetDetail',
params: { id: row.asset }
}
}
}
},
ip: {
width: '120px'
},
username: {
showOverflowTooltip: true
},
systemuser: {
formatter: DisplayFormatter
},
version: {
width: '70px'
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
can: this.$hasPerm('assets.view_assethistoryaccountsecret'),
type: 'primary',
callback: ({ row }) => {
vm.account = row
vm.showViewSecretDialog = true
}
}
]
}
}
}
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: false,
hasCreate: false,
hasImport: false,
hasExport: this.$hasPerm('assets.view_assethistoryaccountsecret'),
exportOptions: {
url: this.exportUrl,
mfaVerifyRequired: true
},
searchConfig: {
exclude: ['systemuser', 'asset']
},
hasSearch: true
}
}
},
computed: {
title() {
return this.$t('accounts.AccountHistableHelpMessage')
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
this.$set(this.headerActions.exportOptions, 'url', iNew.replace('/accounts-history/', '/account-history-secrets/'))
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
methods: {
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -1,142 +0,0 @@
<template>
<Dialog
:title="title"
:visible.sync="iVisible"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:close-on-click-modal="false"
v-bind="$attrs"
width="70%"
v-on="$listeners"
>
<AccountCreateUpdateForm
v-if="!loading"
ref="form"
:account="account"
:asset="asset"
@add="addAccount"
@edit="editAccount"
/>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
import AccountCreateUpdateForm from '@/components/AccountCreateUpdateForm'
export default {
name: 'CreateAccountDialog',
components: {
Dialog,
AccountCreateUpdateForm
},
props: {
visible: {
type: Boolean,
default: false
},
asset: {
type: Object,
default: null
},
account: {
type: Object,
default: () => ({})
},
title: {
type: String,
default: function() {
return this.$t('assets.AddAccount')
}
}
},
data() {
return {
loading: false,
platform: {}
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
},
protocols() {
return this.asset ? this.asset.protocol : []
}
},
methods: {
addAccount(form) {
const formValue = Object.assign({}, form)
let assets = []
if (this.asset) {
assets = [this.asset.id]
} else {
assets = formValue.assets
}
delete formValue.assets
if (assets.length === 0) {
this.$message.error(this.$tc('assets.PleaseSelectAsset'))
return
}
const data = []
for (const asset of assets) {
data.push({
...formValue,
asset
})
}
this.$axios.post(`/api/v1/accounts/accounts/`, data).then(() => {
this.iVisible = false
this.$emit('add', true)
this.$message.success(this.$tc('common.createSuccessMsg'))
}).catch(error => this.setFieldError(error))
},
editAccount(form) {
const data = { ...form }
this.$axios.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data).then(() => {
this.iVisible = false
this.$emit('add', true)
this.$message.success(this.$tc('common.updateSuccessMsg'))
}).catch(error => this.setFieldError(error))
},
setFieldError(error) {
const response = error.response
const data = response.data
const refsAutoDataForm = this.$refs.form.$refs.AutoDataForm
if (response.status === 400) {
for (const key of Object.keys(data)) {
let err = ''
let current = key
let errorTips = data[current]
if (errorTips instanceof Array) {
errorTips = _.filter(errorTips, (item) => Object.keys(item).length > 0)
for (const i of errorTips) {
if (i instanceof Object) {
err += i?.port?.join(',')
} else {
err += errorTips
}
}
} else {
err = errorTips
}
if (current === 'secret') {
current = refsAutoDataForm.form.secret_type?.value || key
}
refsAutoDataForm.setFieldError(current, err)
}
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,340 +0,0 @@
<template>
<div>
<ListTable ref="ListTable" :header-actions="headerActions" :table-config="tableConfig" />
<ViewSecret
v-if="showViewSecretDialog"
:account="account"
:url="secretUrl"
:visible.sync="showViewSecretDialog"
/>
<UpdateSecretInfo
v-if="showUpdateSecretDialog"
:account="account"
:visible.sync="showUpdateSecretDialog"
@updateAuthDone="onUpdateAuthDone"
/>
<AccountCreateUpdate
v-if="showAddDialog"
:account="account"
:asset="iAsset"
:title="accountCreateUpdateTitle"
:visible.sync="showAddDialog"
@add="addAccountSuccess"
/>
</div>
</template>
<script>
import ListTable from '@/components/ListTable/index'
import { ActionsFormatter } from '@/components/TableFormatters'
import ViewSecret from './ViewSecret'
import UpdateSecretInfo from './UpdateSecretInfo'
import AccountCreateUpdate from './AccountCreateUpdate'
import { connectivityMeta } from './const'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'AccountListTable',
components: {
ListTable,
UpdateSecretInfo,
ViewSecret,
AccountCreateUpdate
},
props: {
url: {
type: String,
required: true
},
exportUrl: {
type: String,
default() {
return this.url.replace('/accounts/accounts/', '/accounts/account-secrets/')
}
},
hasLeftActions: {
type: Boolean,
default: false
},
otherActions: {
type: Array,
default: null
},
hasClone: {
type: Boolean,
default: false
},
asset: {
type: Object,
default: null
},
columns: {
type: Array,
default: () => []
},
hasExport: {
type: Boolean,
default: true
},
hasImport: {
type: Boolean,
default: true
},
columnsMeta: {
type: Object,
default: () => {
}
},
headerExtraActions: {
type: Array,
default: () => []
}
},
data() {
const vm = this
return {
showViewSecretDialog: false,
showUpdateSecretDialog: false,
showAddDialog: false,
accountCreateUpdateTitle: this.$t('assets.AddAccount'),
iAsset: this.asset,
account: {},
secretUrl: '',
tableConfig: {
url: this.url,
permissions: {
app: 'assets',
resource: 'account'
},
columnsExclude: ['spec_info'],
columns: [
'name', 'username', 'asset', 'privileged',
'secret_type', 'source', 'actions'
],
columnsMeta: {
name: {
formatter: function(row) {
const to = {
name: 'AssetAccountDetail',
params: { id: row.id }
}
if (vm.$hasPerm('accounts.view_account')) {
return <router-link to={to}>{row.name}</router-link>
} else {
return <span>{row.name}</span>
}
}
},
asset: {
label: this.$t('assets.Asset'),
formatter: function(row) {
const to = {
name: 'AssetDetail',
params: { id: row.asset.id }
}
if (vm.$hasPerm('assets.view_asset')) {
return <router-link to={to}>{row.asset.name}</router-link>
} else {
return <span>{row.asset.name}</span>
}
}
},
secret_type: {
width: '100px',
formatter: function(row) {
return row.secret_type.label
}
},
source: {
formatter: function(row) {
return row.source.label
}
},
has_secret: {
width: '100px',
formatterArgs: {
showFalse: false
}
},
privileged: {
label: this.$t('assets.Privileged'),
width: '120px',
formatterArgs: {
showText: false,
showFalse: false
}
},
connectivity: connectivityMeta,
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
can: this.$hasPerm('accounts.view_accountsecret'),
type: 'primary',
callback: ({ row }) => {
// debugger
vm.secretUrl = `/api/v1/accounts/account-secrets/${row.id}/`
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
},
{
name: 'Delete',
title: this.$t('common.Delete'),
can: this.$hasPerm('accounts.delete_account'),
type: 'primary',
callback: ({ row }) => {
this.$axios.delete(`/api/v1/accounts/accounts/${row.id}/`).then(() => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$refs.ListTable.reloadTable()
})
}
},
{
name: 'Test',
title: this.$t('common.Test'),
can: ({ row }) =>
!this.$store.getters.currentOrgIsRoot &&
this.$hasPerm('accounts.change_account') &&
row.asset['auto_info'].ansible_enabled &&
row.asset['auto_info'].ping_enabled,
callback: ({ row }) => {
this.$axios.post(
`/api/v1/accounts/accounts/tasks/`,
{ action: 'verify', accounts: [row.id] }
).then(res => {
openTaskPage(res['task'])
})
}
},
{
name: 'Update',
title: this.$t('common.Update'),
can: this.$hasPerm('accounts.change_account') && !this.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
const data = {
...this.asset,
...row.asset
}
vm.account = row
vm.iAsset = data
vm.showAddDialog = false
vm.accountCreateUpdateTitle = this.$t('assets.UpdateAccount')
setTimeout(() => {
vm.showAddDialog = true
})
}
}
]
}
},
...this.columnsMeta
}
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: true,
hasCreate: false,
hasImport: this.hasImport,
hasExport: this.hasExport && this.$hasPerm('accounts.view_accountsecret'),
handleImportClick: ({ selectedRows }) => {
this.$eventBus.$emit('showImportDialog', {
selectedRows,
url: '/api/v1/accounts/accounts/',
name: this?.name
})
},
exportOptions: {
url: this.exportUrl,
mfaVerifyRequired: true
},
importOptions: {
canImportCreate: this.$hasPerm('accounts.add_account'),
canImportUpdate: this.$hasPerm('accounts.change_account')
},
extraActions: [
{
name: 'add',
title: this.$t('common.Add'),
type: 'primary',
can: () => {
return vm.$hasPerm('accounts.add_account') && !this.$store.getters.currentOrgIsRoot
},
callback: async() => {
await this.getAssetDetail()
setTimeout(() => {
vm.iAsset = this.asset
vm.account = {}
vm.accountCreateUpdateTitle = this.$t('assets.AddAccount')
vm.showAddDialog = true
})
}
},
...this.headerExtraActions
// {
// name: 'autocreate',
// title: this.$t('accounts.AutoCreate'),
// type: 'default'
// }
],
canBulkDelete: vm.$hasPerm('accounts.delete_account'),
searchConfig: {
getUrlQuery: false,
exclude: ['asset']
},
hasSearch: true
}
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
this.$set(this.headerActions.exportOptions, 'url', iNew.replace(/(.*)accounts/, '$1account-secrets'))
}
},
mounted() {
if (this.columns.length > 0) {
this.tableConfig.columns = this.columns
}
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
methods: {
onUpdateAuthDone(account) {
Object.assign(this.account, account)
},
addAccountSuccess() {
this.$refs.ListTable.reloadTable()
},
async getAssetDetail() {
const { query: { asset }} = this.$route
if (asset) {
this.iAsset = await this.$axios.get(`/api/v1/assets/assets/${asset}/`)
}
},
refresh() {
this.$refs.ListTable.reloadTable()
}
}
}
</script>
<style lang='scss' scoped>
.cell a {
color: var(--color-info);
}
</style>

View File

@@ -1,77 +0,0 @@
<template>
<GenericListTableDialog :visible.sync="iVisible" v-bind="config" />
</template>
<script>
import { GenericListTableDialog } from '@/layout/components'
import { ShowKeyCopyFormatter } from '@/components/TableFormatters'
export default {
components: {
GenericListTableDialog
},
props: {
account: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
config: {
title: this.$t('accounts.HistoryPassword'),
visible: false,
width: '60%',
tableConfig: {
url: `/api/v1/accounts/account-secrets/${this.account.id}/histories/`,
columns: ['secret', 'secret_type', 'history_date'],
columnsMeta: {
secret: {
label: this.$t('assets.Password'),
formatter: ShowKeyCopyFormatter,
formatterArgs: {
hasDownload: false,
name: this.account.name
}
},
history_date: {
label: this.$t('accounts.HistoryDate')
},
secret_type: {
width: '200px'
},
version: {
width: '100px'
},
actions: {
has: false
}
}
},
headerActions: {
hasLeftActions: false,
hasSearch: false
}
}
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,83 @@
<template>
<div>
<UserConfirmDialog
:url="url"
@UserConfirmDone="getAuthInfo"
@UserConfirmCancel="exit"
/>
<Dialog
:title="dialogTitle"
:show-confirm="false"
:show-cancel="false"
:destroy-on-close="true"
:width="'50'"
:visible.sync="showAuthInfo"
v-bind="$attrs"
v-on="$listeners"
>
<div>
<el-form label-position="right" label-width="80px" :model="authInfo">
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="account.hostname" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="authInfo.password" type="password" show-password />
</el-form-item>
<el-form-item :label="this.$t('users.SSHKey')">
<el-input v-model="authInfo['private_key']" class="item-textarea" type="textarea" show-password />
</el-form-item>
</el-form>
</div>
</Dialog>
</div>
</template>
<script>
import Dialog from '@/components/Dialog'
import UserConfirmDialog from '@/components/UserConfirmDialog'
export default {
name: 'ShowSecretInfo',
components: {
Dialog,
UserConfirmDialog
},
props: {
account: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
dialogTitle: this.$t('common.ViewSecret'),
authInfo: {},
showAuthInfo: false,
url: `/api/v1/assets/account-secrets/${this.account.id}/`
}
},
methods: {
getAuthInfo() {
this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(resp => {
this.authInfo = resp
this.showAuthInfo = true
})
},
exit() {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped>
.item-textarea >>> .el-textarea__inner {
height: 110px;
}
</style>

View File

@@ -1,27 +1,27 @@
<template>
<Dialog
:destroy-on-close="true"
:title="$tc('assets.UpdateAssetUserToken')"
:visible.sync="visible"
width="50"
@cancel="handleCancel()"
:title="this.$t('assets.UpdateAssetUserToken')"
:visible.sync="visible"
:destroy-on-close="true"
@confirm="handleConfirm()"
@cancel="handleCancel()"
v-on="$listeners"
>
<el-form label-position="right" label-width="90px">
<el-form-item :label="$tc('assets.Name')">
<el-input v-model="account['asset_name']" readonly />
<el-form-item :label="this.$t('assets.Hostname')">
<el-input v-model="account.hostname" readonly />
</el-form-item>
<el-form-item :label="$tc('assets.Username')">
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="$tc('assets.Password')">
<el-form-item :label="this.$t('assets.Password')">
<UpdateToken v-model="authInfo.password" />
</el-form-item>
<el-form-item :label="$tc('assets.SSHSecretKey')">
<el-form-item :label="this.$t('assets.SSHSecretKey')">
<UploadKey @input="getFile" />
</el-form-item>
<el-form-item :label="$tc('assets.Passphrase')">
<el-form-item :label="this.$t('assets.Passphrase')">
<UpdateToken v-model="authInfo.passphrase" />
</el-form-item>
</el-form>
@@ -32,7 +32,6 @@
import Dialog from '@/components/Dialog'
import { UpdateToken, UploadKey } from '@/components/FormFields'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'UpdateSecretInfo',
components: {
@@ -52,7 +51,7 @@ export default {
},
data() {
return {
secretInfo: {
authInfo: {
password: '',
private_key: '',
passphrase: ''
@@ -62,15 +61,15 @@ export default {
methods: {
handleConfirm() {
const data = {}
if (this.secretInfo.password !== '') {
data.password = encryptPassword(this.secretInfo.password)
if (this.authInfo.password !== '') {
data.password = encryptPassword(this.authInfo.password)
}
if (this.secretInfo.private_key !== '') {
data.private_key = encryptPassword(this.secretInfo.private_key)
if (this.secretInfo.passphrase) data.passphrase = this.secretInfo.passphrase
if (this.authInfo.private_key !== '') {
data.private_key = encryptPassword(this.authInfo.private_key)
if (this.authInfo.passphrase) data.passphrase = this.authInfo.passphrase
}
this.$axios.patch(
`/api/v1/accounts/accounts/${this.account.id}/`,
`/api/v1/assets/accounts/${this.account.id}/`,
data,
{ disableFlashErrorMsg: true }
).then(res => {
@@ -88,7 +87,7 @@ export default {
this.$emit('update:visible', false)
},
getFile(file) {
this.secretInfo.private_key = file
this.authInfo.private_key = file
}
}
}

View File

@@ -1,190 +0,0 @@
<template>
<div>
<UserConfirmDialog
:url="url"
@UserConfirmCancel="exit"
@UserConfirmDone="getAuthInfo"
/>
<Dialog
:destroy-on-close="true"
:show-cancel="false"
:title="title"
:visible.sync="showSecret"
:width="'50'"
v-bind="$attrs"
@confirm="showSecret = false"
v-on="$listeners"
>
<el-form :model="secretInfo" class="password-form" label-position="right" label-width="100px">
<el-form-item :label="$tc('assets.Name')">
<span>{{ account['name'] }}</span>
</el-form-item>
<el-form-item :label="$tc('assets.Username')">
<span>{{ account['username'] }}</span>
</el-form-item>
<el-form-item :label="secretTypeLabel">
<ShowKeyCopyFormatter
:cell-value="secretInfo.secret"
:col="{ formatterArgs: {
name: account['name'],
}}"
/>
</el-form-item>
<el-form-item v-if="secretType === 'ssh_key'" :label="$tc('assets.sshKeyFingerprint')">
<span>{{ sshKeyFingerprint }}</span>
</el-form-item>
<el-form-item :label="$tc('common.DateCreated')">
<span>{{ account['date_created'] | date }}</span>
</el-form-item>
<el-form-item :label="$tc('common.DateUpdated')">
<span>{{ account['date_updated'] | date }}</span>
</el-form-item>
<el-form-item v-if="showPasswordRecord" v-perms="'accounts.view_accountsecret'" :label="$tc('accounts.PasswordRecord')">
<el-link
:underline="false"
type="success"
@click="showHistoryDialog"
>
<span style="padding-right: 30px">
{{ versions }}
</span>
</el-link>
</el-form-item>
</el-form>
</Dialog>
<PasswordHistoryDialog
v-if="showPasswordHistoryDialog"
:account="account"
:visible.sync="showPasswordHistoryDialog"
/>
</div>
</template>
<script>
import Dialog from '@/components/Dialog'
import PasswordHistoryDialog from './PasswordHistoryDialog'
import UserConfirmDialog from '@/components/UserConfirmDialog'
import { ShowKeyCopyFormatter } from '@/components/TableFormatters'
export default {
name: 'ShowSecretInfo',
components: {
Dialog,
PasswordHistoryDialog,
UserConfirmDialog,
ShowKeyCopyFormatter
},
props: {
account: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
},
url: {
type: String,
default: ''
},
title: {
type: String,
default: function() {
return this.$tc('assets.AccountDetail')
}
},
showPasswordRecord: {
type: Boolean,
default: true
}
},
data() {
return {
secretInfo: {},
versions: '-',
showSecret: false,
sshKeyFingerprint: '',
historyCount: 0,
showPasswordHistoryDialog: false
}
},
computed: {
secretTypeLabel() {
return this.account['secret_type'].label || 'Password'
},
secretType() {
return this.account['secret_type'].value
}
},
mounted() {
if (this.showPasswordRecord) {
const url = `/api/v1/accounts/account-secrets/${this.account.id}/histories/?limit=1`
this.$axios.get(url, { disableFlashErrorMsg: true }).then(resp => {
this.versions = resp.count
})
}
},
methods: {
getAuthInfo() {
this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(resp => {
this.secretInfo = resp
this.sshKeyFingerprint = resp?.spec_info
this.showSecret = true
})
},
exit() {
this.$emit('update:visible', false)
},
showHistoryDialog() {
this.showPasswordHistoryDialog = true
}
}
}
</script>
<style lang="scss" scoped>
.item-textarea >>> .el-textarea__inner {
height: 110px;
}
.el-form-item {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
&:last-child {
border-bottom: none;
}
>>> .el-form-item__label {
padding-right: 20px;
line-height: 30px;
}
>>> .el-form-item__content {
line-height: 30px;
pre {
margin: 0;
}
}
}
ul {
margin: 0;
}
li {
display: block;
font-size: 13px;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.title {
color: #303133;
font-weight: 500;
}
}
</style>

View File

@@ -1,26 +1,36 @@
import { toSafeLocalDateStr } from '@/utils/common'
import ChoicesFormatter from '@/components/TableFormatters/ChoicesFormatter'
import i18n from '@/i18n/i18n'
import { ChoicesFormatter } from '@/components/TableFormatters'
export const connectivityMeta = {
label: i18n.t('assets.Connectivity'),
label: i18n.t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
faChoices: {
'-': '',
ok: 'fa-check-circle',
err: 'fa-times-circle'
iconChoices: {
ok: 'fa-check',
failed: 'fa-times',
unknown: 'fa-circle-o'
},
classChoices: {
ok: 'text-primary',
err: 'text-danger'
failed: 'text-danger',
unknown: 'text-warning'
},
getText({ cellValue }) {
if (cellValue?.value === '-') {
return '-'
} else {
return cellValue?.label
hasTips: true,
getTips: ({ row, cellValue }) => {
const mapper = {
'ok': i18n.t('assets.Reachable'),
'failed': i18n.t('assets.Unreachable'),
'unknown': i18n.t('assets.Unknown')
}
let tips = mapper[cellValue]
if (row['date_verified']) {
const datetime = toSafeLocalDateStr(row['date_verified'])
tips += '<br> ' + datetime
}
return tips
}
},
width: '100px'
width: '90px',
align: 'center'
}

View File

@@ -0,0 +1,213 @@
<template>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
<UpdateSecretInfo v-if="showUpdateSecretDialog" :visible.sync="showUpdateSecretDialog" :account="account" @updateAuthDone="onUpdateAuthDone" />
</div>
</template>
<script>
import ListTable from '@/components/ListTable/index'
import { ActionsFormatter, DetailFormatter, DisplayFormatter } from '@/components/TableFormatters'
import ShowSecretInfo from './ShowSecretInfo'
import UpdateSecretInfo from './UpdateSecretInfo'
import { connectivityMeta } from './const'
import { openTaskPage } from '@/utils/jms'
// import i18n from '@/i18n/i18n'
export default {
name: 'AccountListTable',
components: {
ListTable,
UpdateSecretInfo,
ShowSecretInfo
},
props: {
url: {
type: String,
required: true
},
exportUrl: {
type: String,
default() {
return this.url.replace('/assets/accounts/', '/assets/account-secrets/')
}
},
hasLeftActions: {
type: Boolean,
default: false
},
otherActions: {
type: Array,
default: null
},
hasClone: {
type: Boolean,
default: false
}
},
data() {
const vm = this
return {
showViewSecretDialog: false,
showUpdateSecretDialog: false,
account: {},
tableConfig: {
url: this.url,
permissions: {
app: 'assets',
resource: 'authbook'
},
columns: [
'hostname', 'ip', 'username', 'version', 'connectivity',
'systemuser', 'date_created', 'date_updated', 'actions'
],
columnsShow: {
min: ['username', 'actions'],
default: ['hostname', 'ip', 'username', 'version', 'actions']
},
columnsMeta: {
hostname: {
prop: 'hostname',
label: this.$t('assets.Hostname'),
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
can: this.$hasPerm('assets.view_asset'),
getRoute({ row }) {
return {
name: 'AssetDetail',
params: { id: row.asset }
}
}
}
},
ip: {
width: '120px'
},
username: {
showOverflowTooltip: true
},
systemuser: {
formatter: DisplayFormatter
},
version: {
width: '70px'
},
connectivity: connectivityMeta,
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
can: this.$hasPerm('assets.view_assetaccountsecret'),
type: 'primary',
callback: ({ row }) => {
vm.account = row
vm.showViewSecretDialog = false
setTimeout(() => {
vm.showViewSecretDialog = true
})
}
},
{
name: 'Delete',
title: this.$t('common.Delete'),
can: this.$hasPerm('assets.delete_authbook'),
type: 'primary',
callback: ({ row }) => {
this.$axios.delete(`/api/v1/assets/accounts/${row.id}/`).then(() => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$refs.ListTable.reloadTable()
})
}
},
{
name: 'Test',
title: this.$t('common.Test'),
can: this.$hasPerm('assets.test_authbook'),
callback: ({ row }) => {
this.$axios.post(
`/api/v1/assets/accounts/${row.id}/verify/`,
{ action: 'test' }
).then(res => {
openTaskPage(res['task'])
})
}
},
{
name: 'Update',
title: this.$t('common.Update'),
can: this.$hasPerm('assets.change_assetaccountsecret') && !this.$store.getters.currentOrgIsRoot,
callback: ({ row }) => {
vm.account = row
vm.showUpdateSecretDialog = false
setTimeout(() => {
vm.showUpdateSecretDialog = true
})
}
}
// {
// name: 'History',
// title: i18n.t('common.History'),
// can: this.$hasPerm('assets.view_assethistoryaccount') && !this.$store.getters.currentOrgIsRoot,
// callback: ({ row }) => {
// this.$router.push({
// name: 'AssetAccountHistoryList',
// query: { id: row.id }
// })
// }
// }
]
}
}
}
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: true,
hasCreate: false,
hasImport: false,
hasExport: this.$hasPerm('assets.view_assetaccountsecret'),
exportOptions: {
url: this.exportUrl,
mfaVerifyRequired: true
},
searchConfig: {
exclude: ['systemuser', 'asset']
},
hasSearch: true
}
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
this.$set(this.headerActions.exportOptions, 'url', iNew.replace('/accounts/', '/account-secrets/'))
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
methods: {
onUpdateAuthDone(account) {
Object.assign(this.account, account)
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -18,16 +18,16 @@ export default {
type: Array,
default: () => []
},
moreActionBtn: {
type: Object,
default: () => ({})
},
moreActionsTitle: {
type: String,
default() {
return this.$t('common.MoreActions')
}
},
moreActionsType: {
type: String,
default: 'default'
},
moreActionsPlacement: {
type: String,
default: 'bottom'
@@ -43,21 +43,14 @@ export default {
return actions
},
iMoreAction() {
const defaultBtn = {
return {
name: 'moreActions',
title: this.$t('common.MoreActions'),
type: 'primary',
plain: true
}
const btn = {
...defaultBtn,
...this.moreActionBtn,
title: this.iMoreActionsTitle,
dropdown: this.moreActions || []
}
if (this.moreActionsTitle) {
btn.title = this.moreActionsTitle
}
return btn
},
iMoreActionsTitle() {
return this.moreActionsTitle || this.$t('common.MoreActions')
}
}
}

View File

@@ -0,0 +1,78 @@
<template>
<div>
<UserConfirmDialog
:url="url"
@UserConfirmDone="getAuthInfo"
@UserConfirmCancel="exit"
/>
<Dialog
:title="dialogTitle"
:show-confirm="false"
:show-cancel="false"
:destroy-on-close="true"
:width="'50'"
:visible.sync="showAuthInfo"
v-bind="$attrs"
v-on="$listeners"
>
<div>
<el-form label-position="right" label-width="80px" :model="authInfo">
<el-form-item :label="this.$t('applications.appName')">
<el-input v-model="account['app_display']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Username')">
<el-input v-model="account['username']" readonly />
</el-form-item>
<el-form-item :label="this.$t('assets.Password')">
<el-input v-model="authInfo.password" type="password" show-password />
</el-form-item>
</el-form>
</div>
</Dialog>
</div>
</template>
<script>
import Dialog from '@/components/Dialog'
import UserConfirmDialog from '@/components/UserConfirmDialog'
export default {
name: 'ShowSecretInfo',
components: {
Dialog,
UserConfirmDialog
},
props: {
account: {
type: Object,
default: () => ({})
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
dialogTitle: this.$t('common.ViewSecret'),
authInfo: {},
showAuthInfo: false,
url: `/api/v1/applications/account-secrets/${this.account.id}/`
}
},
methods: {
getAuthInfo() {
this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(resp => {
this.authInfo = resp
this.showAuthInfo = true
})
},
exit() {
this.$emit('update:visible', false)
}
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,31 @@
import { ChoicesFormatter } from '@/components/TableFormatters'
import { toSafeLocalDateStr } from '@/utils/common'
import i18n from '@/i18n/i18n'
export const connectivityMeta = {
label: i18n.t('assets.Reachable'),
formatter: ChoicesFormatter,
formatterArgs: {
iconChoices: {
ok: 'fa-check text-primary',
failed: 'fa-times text-danger',
unknown: 'fa-circle text-warning'
},
hasTips: true,
getTips: ({ row, cellValue }) => {
const mapper = {
'ok': i18n.t('assets.Reachable'),
'failed': i18n.t('assets.Unreachable'),
'unknown': i18n.t('assets.Unknown')
}
let tips = mapper[cellValue]
if (row['date_verified']) {
const datetime = toSafeLocalDateStr(row['date_verified'])
tips += '<br> ' + datetime
}
return tips
}
},
width: '90px',
align: 'center'
}

View File

@@ -0,0 +1,173 @@
<template>
<div>
<ListTable ref="ListTable" :table-config="tableConfig" :header-actions="headerActions" />
<ShowSecretInfo v-if="showViewSecretDialog" :visible.sync="showViewSecretDialog" :account="account" />
</div>
</template>
<script>
import ListTable from '@/components/ListTable/index'
import { ActionsFormatter, DetailFormatter } from '@/components/TableFormatters'
import ShowSecretInfo from './ShowSecretInfo'
export default {
name: 'Detail',
components: {
ListTable,
ShowSecretInfo
},
props: {
url: {
type: String,
required: true
},
exportUrl: {
type: String,
default() {
return this.url.replace('/applications/accounts/', '/applications/account-secrets/')
}
},
hasLeftActions: {
type: Boolean,
default: false
},
otherActions: {
type: Array,
default: null
},
hasClone: {
type: Boolean,
default: false
},
systemUserDisabled: {
type: Boolean,
default: true
}
},
data() {
return {
showViewSecretDialog: false,
showUpdateSecretDialog: false,
account: {},
tableConfig: {
url: this.url,
columns: [
'app_display', 'username', 'category_display',
'type_display', 'systemuser', 'actions'
],
columnsMeta: {
app_display: {
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
getRoute({ row }) {
switch (row['category']) {
case 'remote_app':
return {
name: 'RemoteAppDetail',
params: { id: row.app }
}
case 'db':
return {
name: 'DatabaseAppDetail',
params: { id: row.app }
}
default:
return {
name: 'KubernetesAppDetail',
params: { id: row.app }
}
}
}
}
},
username: {
showOverflowTooltip: true
},
systemuser: {
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
can: this.systemUserDisabled && this.$hasPerm('assets.view_systemuser'),
getTitle({ row }) {
return row.systemuser_display
},
getRoute({ row }) {
return {
name: 'SystemUserDetail',
params: { id: row.systemuser }
}
}
}
},
actions: {
formatter: ActionsFormatter,
formatterArgs: {
hasUpdate: false, // can set function(row, value)
hasDelete: false, // can set function(row, value)
hasClone: this.hasClone,
moreActionsTitle: this.$t('common.More'),
extraActions: [
{
name: 'View',
title: this.$t('common.View'),
type: 'primary',
can: this.$hasPerm('applications.view_applicationaccountsecret'),
callback: function({ row }) {
this.account = row
this.showViewSecretDialog = true
}.bind(this)
},
{
name: 'Update',
title: this.$t('common.Update'),
can: !this.$store.getters.currentOrgIsRoot,
callback: function({ row }) {
this.$message.success(this.$tc('applications.updateAccountMsg'))
}.bind(this)
}
]
}
}
}
},
headerActions: {
hasLeftActions: this.hasLeftActions,
hasMoreActions: false,
hasImport: false,
hasExport: this.$hasPerm('applications.view_applicationaccountsecret'),
exportOptions: {
url: this.exportUrl,
mfaVerifyRequired: true
},
searchConfig: {
exclude: ['systemuser', 'app']
},
hasSearch: true
}
}
},
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
}
},
mounted() {
if (this.otherActions) {
const actionColumn = this.tableConfig.columns[this.tableConfig.columns.length - 1]
for (const item of this.otherActions) {
actionColumn.formatterArgs.extraActions.push(item)
}
}
},
methods: {
onUpdateAuthDone(account) {
Object.assign(this.account, account)
}
}
}
</script>
<style lang='less' scoped>
</style>

View File

@@ -1,199 +0,0 @@
<template>
<Dialog
:title="$tc('assets.Assets')"
custom-class="asset-select-dialog"
top="1vh"
v-bind="$attrs"
width="80vw"
@cancel="handleCancel"
@close="handleClose"
@confirm="handleConfirm"
v-on="$listeners"
>
<AssetTreeTable
ref="ListPage"
:header-actions="headerActions"
:node-url="baseNodeUrl"
:table-config="tableConfig"
:tree-url="`${baseNodeUrl}children/tree/`"
:url="baseUrl"
class="tree-table"
v-bind="$attrs"
/>
</Dialog>
</template>
<script>
import AssetTreeTable from '@/components/AssetTreeTable'
import Dialog from '@/components/Dialog'
export default {
componentName: 'AssetSelectDialog',
components: { AssetTreeTable, Dialog },
props: {
baseUrl: {
type: String,
default: '/api/v1/assets/assets/'
},
baseNodeUrl: {
type: String,
default: '/api/v1/assets/nodes/'
},
value: {
type: Array,
default: () => []
},
canSelect: {
type: Function,
default(row, index) {
return true
}
},
disabled: {
type: [Boolean, Function],
default: false
}
},
data() {
const vm = this
return {
dialogVisible: false,
rowSelected: _.cloneDeep(this.value) || [],
rowsAdd: [],
tableConfig: {
url: this.baseUrl,
hasTree: true,
canSelect: this.canSelect,
columns: [
{
prop: 'name',
label: this.$t('assets.Name'),
sortable: true
},
{
prop: 'address',
label: this.$t('assets.ipDomain'),
sortable: 'custom'
},
{
prop: 'platform',
label: this.$t('assets.Platform'),
sortable: true,
formatter: function(row) {
return row.platform.name
}
},
{
prop: 'protocols',
formatter: function(row) {
const data = row.protocols.map(p => {
return <el-tag size='mini'>{p.name}/{p.port} </el-tag>
})
return <span> {data} </span>
},
label: this.$t('assets.Protocols')
},
{
prop: 'actions',
has: false
}
],
listeners: {
'toggle-row-selection': (isSelected, row) => {
if (isSelected) {
vm.addRowToSelect(row)
} else {
vm.removeRowFromSelect(row)
}
}
},
theRowDefaultIsSelected: (row) => {
return this.value.indexOf(row.id) > -1
}
},
headerActions: {
hasLeftActions: false,
hasRightActions: false,
searchConfig: {
getUrlQuery: false
}
}
}
},
methods: {
handleClose() {
this.$eventBus.$emit('treeComponentKey')
},
handleConfirm() {
this.$emit('confirm', this.rowSelected, this.rowsAdd)
if (this.rowSelected.length > 0) {
this.handleClose()
}
},
handleCancel() {
this.$emit('cancel')
this.handleClose()
},
addRowToSelect(row) {
const selectValueIndex = this.rowSelected.indexOf(row.id)
if (selectValueIndex === -1) {
this.rowSelected.push(row.id)
this.rowsAdd.push(row)
}
},
removeRowFromSelect(row) {
const selectValueIndex = this.rowSelected.indexOf(row.id)
if (selectValueIndex > -1) {
this.rowSelected.splice(selectValueIndex, 1)
}
}
}
}
</script>
<style lang="scss" scoped>
.page ::v-deep .page-heading {
display: none;
}
.el-dialog__wrapper ::v-deep .el-dialog__body {
padding: 0 0 0 3px;
.tree-table {
.search {
.el-input__inner {
background-color: #f3f3f3;
}
.el-cascader {
background-color: #f3f3f3;
}
}
.left {
padding: 5px;
}
.right {
height: calc(100vh - 200px);
overflow: auto;
}
.mini {
padding-top: 8px;
}
.transition-box {
padding: 5px;
}
}
}
.page ::v-deep .treebox .ztree {
}
.asset-select-dialog ::v-deep .el-icon-circle-check {
display: none;
}
</style>

View File

@@ -1,96 +1,151 @@
<template>
<div class="asset-select-formatter">
<div class="asset-select-dialog">
<Select2
ref="select2"
v-model="select2Config.value"
v-bind="select2Config"
@input="onInputChange"
v-on="$listeners"
@focus.stop="handleFocus"
/>
<AssetSelectDialog
v-if="dialogVisible"
ref="dialog"
:base-node-url="baseNodeUrl"
:base-url="baseUrl"
:tree-url-query="treeUrlQuery"
:value="value"
:visible.sync="dialogVisible"
v-bind="$attrs"
@cancel="handleCancel"
@confirm="handleConfirm"
v-on="$listeners"
/>
<Dialog
v-if="dialogVisible"
:title="this.$t('assets.Assets')"
:visible.sync="dialogVisible"
custom-class="asset-select-dialog"
width="80vw"
top="1vh"
@confirm="handleConfirm"
@cancel="handleCancel"
>
<TreeTable
ref="ListPage"
:tree-setting="treeSetting"
:table-config="tableConfig"
:header-actions="headerActions"
/>
</Dialog>
</div>
</template>
<script>
import TreeTable from '@/components/TreeTable'
import { DetailFormatter } from '@/components/TableFormatters'
import Select2 from '@/components/FormFields/Select2'
import AssetSelectDialog from './dialog.vue'
import { b } from 'css-color-function/lib/adjusters'
import Dialog from '@/components/Dialog'
export default {
componentName: 'AssetSelect',
components: { AssetSelectDialog, Select2 },
components: { TreeTable, Select2, Dialog },
props: {
baseUrl: {
type: String,
default: '/api/v1/assets/assets/'
},
baseNodeUrl: {
type: String,
default: '/api/v1/assets/nodes/'
},
treeUrlQuery: {
type: Object,
default: () => {}
},
value: {
type: Array,
default: () => []
},
canSelect: {
type: Function,
default(row, index) {
return true
}
},
disabled: {
type: [Boolean, Function],
default: false
}
},
data() {
const iValue = []
for (const item of this.value) {
if (typeof item === 'object') {
iValue.push(item.id)
} else {
iValue.push(item)
const select2Config = {
value: this.value,
multiple: true,
clearable: true,
ajax: {
url: '/api/v1/assets/assets/?fields_size=mini',
transformOption: (item) => {
return { label: item.hostname + '(' + item.ip + ')', value: item.id }
}
}
}
const vm = this
return {
dialogVisible: false,
initialValue: _.cloneDeep(iValue),
select2Config: {
value: iValue,
multiple: true,
clearable: true,
ajax: {
url: this.baseUrl,
transformOption: (item) => {
return { label: item.name + '(' + item.address + ')', value: item.id }
initialValue: _.cloneDeep(this.value),
rowSelected: [],
initSelection: null,
treeSetting: {
showMenu: false,
showRefresh: true,
showAssets: false,
showSearch: true,
customTreeHeader: true,
url: '/api/v1/assets/assets/?fields_size=mini',
nodeUrl: '/api/v1/assets/nodes/',
// ?assets=0不显示资产. =1显示资产
treeUrl: '/api/v1/assets/nodes/children/tree/?assets=0'
},
select2Config: select2Config,
dialogSelect2Config: select2Config,
tableConfig: {
url: '/api/v1/assets/assets/?fields_size=mini',
hasTree: true,
canSelect: this.canSelect,
columns: [
{
prop: 'hostname',
label: this.$t('assets.Hostname'),
sortable: true,
showOverflowTooltip: true,
formatter: DetailFormatter,
formatterArgs: {
route: 'AssetDetail'
}
},
{
prop: 'ip',
label: this.$t('assets.ipDomain'),
sortable: 'custom'
},
{
prop: 'platform',
label: this.$t('assets.Platform'),
sortable: true
},
{
prop: 'protocols',
formatter: function(row) {
return <span> {row.protocols.toString()} </span>
},
label: this.$t('assets.Protocols')
}
],
listeners: {
'toggle-row-selection': (isSelected, row) => {
if (isSelected) {
vm.addRowToSelect(row)
} else {
vm.removeRowFromSelect(row)
}
}
},
theRowDefaultIsSelected: (row) => {
return this.value.indexOf(row.id) > -1
}
},
headerActions: {
hasLeftActions: false,
hasRightActions: false
}
}
},
methods: {
b,
handleFocus() {
this.$refs.select2.selectRef.blur()
this.dialogVisible = true
},
handleConfirm(valueSelected, rowsAdd) {
if (valueSelected === undefined) {
return
}
this.$refs.select2.iValue = valueSelected
this.addRowsToSelect(rowsAdd)
this.onInputChange(valueSelected)
handleConfirm() {
this.dialogVisible = false
},
handleCancel() {
this.$refs.select2.iValue = this.initialValue
this.dialogVisible = false
},
onInputChange(val) {
@@ -101,64 +156,53 @@ export default {
// 如果select2的options中没有那么可能无法显示正常的值
if (selectOptionsHas === undefined) {
const option = {
label: `${row.name}(${row.address})`,
label: `${row.hostname}(${row.ip})`,
value: row.id
}
options.push(option)
}
},
addRowsToSelect(rows) {
addRowToSelect(row) {
const outSelectOptions = this.$refs.select2.options
for (const row of rows) {
this.addToSelect(outSelectOptions, row)
this.addToSelect(outSelectOptions, row)
const selectValue = this.$refs.select2.iValue
const selectValueIndex = selectValue.indexOf(row.id)
if (selectValueIndex === -1) {
selectValue.push(row.id)
}
this.onInputChange(selectValue)
},
removeRowFromSelect(row) {
const selectValue = this.$refs.select2.iValue
const selectValueIndex = selectValue.indexOf(row.id)
if (selectValueIndex > -1) {
selectValue.splice(selectValueIndex, 1)
}
}
}
}
</script>
<style lang="scss" scoped>
.el-select {
width: 100%;
}
.page ::v-deep .page-heading {
display: none;
}
.el-dialog__wrapper ::v-deep .el-dialog__body {
padding: 0 0 0 3px;
.tree-table {
.search {
.el-input__inner {
background-color: #f3f3f3;
}
.el-cascader {
background-color: #f3f3f3;
}
}
.left {
padding: 5px;
.ztree {
height: calc(100vh - 250px) !important;
}
}
.mini {
padding-top: 8px;
}
.transition-box {
padding: 5px;
}
<style lang='scss' scoped>
.el-select{
width: 100%;
}
.page ::v-deep .page-heading{
display: none;
}
.el-dialog__wrapper ::v-deep .el-dialog__body{
padding: 5px 10px;
}
.page ::v-deep .treebox {
height: inherit !important;
}
.asset-select-dialog >>> .transition-box:first-child {
background-color: #f3f3f3 ;
}
.el-dialog__wrapper ::v-deep .el-dialog__body .wrapper-content {
padding: 10px;
}
}
.page ::v-deep .treebox {
height: inherit !important;
}
</style>

View File

@@ -1,175 +0,0 @@
<template>
<TreeTable
ref="TreeList"
component="TabTree"
:table-config="tableConfig"
:active-menu.sync="treeTableConfig.activeMenu"
:tree-tab-config="treeTableConfig"
v-bind="$attrs"
v-on="$listeners"
>
<template #table>
<slot name="table" />
</template>
<div slot="rMenu" slot-scope="{data}">
<slot name="rMenu" :data="data" />
</div>
</TreeTable>
</template>
<script>
import TreeTable from '../TreeTable'
import { setRouterQuery, setUrlParam } from '@/utils/common'
import $ from '@/utils/jquery-vendor'
export default {
components: {
TreeTable
},
props: {
url: {
type: String,
default: '/api/v1/assets/assets/'
},
nodeUrl: {
type: String,
default: '/api/v1/assets/nodes/'
},
treeUrl: {
type: String,
default: '/api/v1/assets/nodes/children/tree/'
},
treeUrlQuery: {
type: Object,
default: () => ({})
},
treeSetting: {
type: Object,
default: () => ({})
},
tableConfig: {
type: Object,
default: () => ({})
},
showAssets: {
type: Boolean,
default: false
}
},
data() {
const showAssets = this.treeSetting?.showAssets || this.showAssets
const treeUrlQuery = this.setTreeUrlQuery()
const assetTreeUrl = `${this.treeUrl}?assets=${showAssets ? '1' : '0'}&${treeUrlQuery}`
return {
treeTabConfig: {
activeMenu: 'CustomTree',
submenu: [
{
title: this.$t('assets.AssetTree'),
name: 'CustomTree',
treeSetting: {
showAssets,
showMenu: false,
showRefresh: true,
showCreate: true,
showUpdate: true,
showDelete: true,
hasRightMenu: true,
showSearch: true,
url: this.url,
nodeUrl: this.nodeUrl,
treeUrl: assetTreeUrl,
callback: {
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode)
},
...this.treeSetting
}
},
{
title: this.$t('assets.BuiltinTree'),
name: 'BuiltinTree',
treeSetting: {
showRefresh: true,
showAssets: false,
showSearch: false,
customTreeHeaderName: this.$t('assets.BuiltinTree'),
url: '/api/v1/assets/nodes/category/tree/',
nodeUrl: this.treeSetting?.nodeUrl || this.nodeUrl,
treeUrl: `/api/v1/assets/nodes/category/tree/?assets=${showAssets ? '1' : '0'}&count_resource=${this.treeSetting.countResource || 'asset'}`,
callback: {
onSelected: (event, treeNode) => this.getAssetsUrl(treeNode)
}
}
}
]
}
}
},
computed: {
treeTableConfig() {
if (this.treeSetting.notShowBuiltinTree) {
// eslint-disable-next-line vue/no-side-effects-in-computed-properties
this.treeTabConfig.submenu.splice(1, 1)
}
return this.treeTabConfig
}
},
mounted() {
this.decorateRMenu()
const treeSetting = this.treeTabConfig.submenu[0].treeSetting
treeSetting.hasRightMenu = !this.currentOrgIsRoot
treeSetting.showCreate = this.$hasPerm('assets.add_node')
treeSetting.showUpdate = this.$hasPerm('assets.change_node')
treeSetting.showDelete = this.$hasPerm('assets.delete_node')
},
methods: {
setTreeUrlQuery() {
let str = ''
for (const key in this.treeUrlQuery) {
str += `${key}=${this.treeUrlQuery[key]}&`
}
str = str.substr(0, str.length - 1)
return str
},
decorateRMenu() {
const show_current_asset = this.$cookie.get('show_current_asset') || '0'
if (show_current_asset === '1') {
$('#m_show_asset_all_children_node').css('color', '#606266')
$('#m_show_asset_only_current_node').css('color', 'green')
} else {
$('#m_show_asset_all_children_node').css('color', 'green')
$('#m_show_asset_only_current_node').css('color', '#606266')
}
},
getAssetsUrl(treeNode) {
let url = this.treeSetting?.url || this.url
if (treeNode.meta.type === 'node') {
const nodeId = treeNode.meta.data.id
url = setUrlParam(url, 'node_id', nodeId)
url = setUrlParam(url, 'asset_id', '')
} else if (treeNode.meta.type === 'asset') {
const assetId = treeNode.meta.data?.id || treeNode.id
url = setUrlParam(url, 'node_id', '')
url = setUrlParam(url, 'asset_id', assetId)
} else if (treeNode.meta.type === 'category') {
url = setUrlParam(url, 'category', treeNode.meta.category)
} else if (treeNode.meta.type === 'type') {
url = setUrlParam(url, 'category', treeNode.meta.category)
url = setUrlParam(url, 'type', treeNode.meta._type)
} else if (treeNode.meta.type === 'platform') {
url = setUrlParam(url, 'platform', treeNode.id)
}
const query = this.setTreeUrlQuery()
url = query ? `${url}&${query}` : url
this.$set(this.tableConfig, 'url', url)
setRouterQuery(this, url)
}
}
}
</script>
<style lang='scss' scoped>
</style>

View File

@@ -1,6 +1,5 @@
<template>
<DataForm
:disabled="disabled"
:fields="iFields"
:form="value"
style="margin-left: -26%;margin-right: -6%"
@@ -29,10 +28,6 @@ export default {
errors: {
type: [Object, String],
default: ''
},
disabled: {
type: Boolean,
default: false
}
},
data() {

View File

@@ -1,24 +1,13 @@
<template>
<DataForm
v-if="!loading"
ref="dataForm"
:fields="totalFields"
:form="iForm"
v-bind="$attrs"
v-on="$listeners"
>
<span
<DataForm ref="dataForm" v-loading="loading" :fields="totalFields" :form="iForm" v-bind="$attrs" v-on="$listeners">
<FormGroupHeader
v-for="(group, i) in groups"
:key="'group-'+group.name"
:slot="'id:'+group.name"
>
<FormGroupHeader
v-if="!groupHidden(group, i)"
:group="group"
:index="i"
:line="i !== 0 && !groupHidden(groups[i - 1], i - 1)"
/>
</span>
:key="'group-'+group.name"
:group="group"
:index="i"
:line="i !== 0"
/>
</DataForm>
</template>
@@ -26,7 +15,6 @@
import DataForm from '../DataForm'
import FormGroupHeader from '@/components/FormGroupHeader'
import { FormFieldGenerator } from '@/components/AutoDataForm/utils'
export default {
name: 'AutoDataForm',
components: {
@@ -63,52 +51,32 @@ export default {
totalFields: [],
loading: true,
groups: [],
iForm: this.form,
errors: {}
}
},
computed: {
iForm() {
const iForm = {}
Object.entries(this.form).forEach(([key, value]) => {
// 初始值是 choice 对象
if (value && typeof value === 'object' && value.label && value.value !== undefined) {
iForm[key] = value.value
} else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' &&
value[0].label && value[0].value !== undefined) {
iForm[key] = value.map(item => item.value)
} else {
iForm[key] = value
}
})
return iForm
}
},
mounted() {
this.optionUrlMetaAndGenerateColumns()
},
methods: {
async optionUrlMetaAndGenerateColumns() {
let data = { actions: {}}
if (this.url) {
data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
}
this.remoteMeta = data.actions[this.method.toUpperCase()] || {}
this.$emit('afterRemoteMeta', this.remoteMeta)
this.generateColumns()
this.$emit('afterGenerateColumns', this.totalFields)
this.cleanFormValue()
this.loading = false
optionUrlMetaAndGenerateColumns() {
this.$store.dispatch('common/getUrlMeta', { url: this.url }).then(data => {
this.remoteMeta = data.actions[this.method.toUpperCase()] || {}
this.generateColumns()
this.cleanFormValue()
}).catch(err => {
this.$log.error(err)
}).finally(() => {
this.loading = false
})
},
generateColumns() {
const generator = new FormFieldGenerator(this.$emit)
const generator = new FormFieldGenerator()
this.totalFields = generator.generateFields(this.fields, this.fieldsMeta, this.remoteMeta)
this.groups = generator.groups
this.$log.debug('Total fields: ', this.totalFields)
},
_cleanFormValue(form, remoteMeta) {
if (!form) {
form = {}
}
for (const [k, v] of Object.entries(remoteMeta)) {
let valueSet = form[k]
if (v.type === 'nested object' && v.children) {
@@ -146,18 +114,6 @@ export default {
} else {
field.attrs.error = error
}
},
groupHidden(group, i) {
for (const field of group.fields) {
let hidden = field.hidden
if (typeof hidden === 'function') {
hidden = hidden(this.iForm)
}
if (!hidden) {
return false
}
}
return true
}
}
}

View File

@@ -1,36 +1,26 @@
import Vue from 'vue'
import Select2 from '@/components/FormFields/Select2'
import ObjectSelect2 from '@/components/FormFields/NestedObjectSelect2'
import NestedField from '@/components/AutoDataForm/components/NestedField'
import Switcher from '@/components/FormFields/Switcher'
import Swicher from '@/components/FormFields/Swicher'
import rules from '@/components/DataForm/rules'
import BasicTree from '@/components/FormFields/BasicTree'
import JsonEditor from '@/components/FormFields/JsonEditor'
import { assignIfNot } from '@/utils/common'
export class FormFieldGenerator {
constructor(emit) {
this.$emite = emit
constructor() {
this.groups = []
}
generateFieldByType(type, field, fieldMeta, fieldRemoteMeta) {
switch (type) {
case 'labeled_choice':
case 'choice':
// Value 处理事在 AutoDataForm 中处理的
if (!fieldRemoteMeta['read_only']) {
field.options = fieldRemoteMeta.choices
}
type = 'radio-group'
if (!fieldRemoteMeta.read_only) {
field.options = fieldRemoteMeta.choices.map(v => {
return { label: v.display_name, value: v.value }
})
}
break
case 'multiple choice':
field.options = fieldRemoteMeta.choices
type = 'checkbox-group'
break
case 'tree':
field.el.tree = fieldRemoteMeta.tree
field.component = BasicTree
field.el.choices = fieldRemoteMeta['choices']
break
case 'datetime':
type = 'date-picker'
@@ -38,19 +28,12 @@ export class FormFieldGenerator {
type: 'datetime'
}
break
case 'json':
type = 'json-editor'
field.component = JsonEditor
break
case 'field':
type = ''
field.component = Select2
if (fieldRemoteMeta.required) {
field.el.clearable = false
}
if (fieldRemoteMeta.child && fieldRemoteMeta.child.type === 'nested object') {
field.component = ObjectSelect2
}
break
case 'string':
type = 'input'
@@ -64,38 +47,35 @@ export class FormFieldGenerator {
break
case 'boolean':
type = ''
field.component = Switcher
break
case 'object_related_field':
field.component = ObjectSelect2
break
case 'm2m_related_field':
field.component = ObjectSelect2
field.component = Swicher
break
case 'nested object':
type = 'nestedField'
field.component = NestedField
field.label = ''
field.labelWidth = 0
field.el = { ...field.el, ...fieldMeta }
field.el.fields = this.generateNestFields(field, fieldMeta, fieldRemoteMeta)
field.el.errors = {}
Vue.$log.debug('All fields in generate: ', field.el.allFields)
break
default:
type = 'input'
break
}
// 上面重写了 type
if (type === 'radio-group') {
if (field.options.length > 4) {
type = 'select'
field.el.filterable = true
if (!fieldRemoteMeta.read_only) {
const options = fieldRemoteMeta.choices.map(v => {
return { label: v.display_name, value: v.value }
})
if (options.length > 4) {
type = 'select'
field.el.filterable = true
}
}
}
field.type = type
return field
}
generateNestFields(field, fieldMeta, fieldRemoteMeta) {
const fields = []
const nestedFields = fieldMeta.fields || []
@@ -108,7 +88,6 @@ export class FormFieldGenerator {
Vue.$log.debug('NestFields: ', fields)
return fields
}
generateFieldByName(name, field) {
switch (name) {
case 'email':
@@ -123,7 +102,6 @@ export class FormFieldGenerator {
}
return field
}
generateFieldByOther(field, fieldMeta, fieldRemoteMeta) {
const filedRules = field.rules || []
if (fieldRemoteMeta.required) {
@@ -133,20 +111,15 @@ export class FormFieldGenerator {
filedRules.push(rules.RequiredChange)
}
}
// 一些 field 有 choices 但不是 choiceField
if (fieldRemoteMeta.choices && field.type.indexOf('choice') === -1) {
field.el.choices = fieldRemoteMeta.choices
}
field.rules = filedRules
return field
}
generateField(name, fieldsMeta, remoteFieldsMeta) {
let field = { id: name, prop: name, el: {}, attrs: {}, rules: [] }
const remoteFieldMeta = remoteFieldsMeta[name] || {}
const fieldMeta = fieldsMeta[name] || {}
field.label = remoteFieldMeta.label
field.helpText = remoteFieldMeta['help_text']
field.helpText = remoteFieldMeta.help_text
field = this.generateFieldByType(remoteFieldMeta.type, field, fieldMeta, remoteFieldMeta)
field = this.generateFieldByName(name, field)
field = this.generateFieldByOther(field, fieldMeta, remoteFieldMeta)
@@ -159,20 +132,16 @@ export class FormFieldGenerator {
// Vue.$log.debug('Generate field: ', name, field)
return field
}
generateFieldGroup(field, fieldsMeta, remoteFieldsMeta) {
const [groupTitle, fields] = field
const _fields = this.generateFields(fields, fieldsMeta, remoteFieldsMeta)
const group = {
this.groups.push({
id: groupTitle,
title: groupTitle,
fields: _fields,
name: _fields[0].id
}
this.groups.push(group)
return _fields
name: fields[0],
fields: fields
})
return this.generateFields(fields, fieldsMeta, remoteFieldsMeta)
}
generateFields(_fields, fieldsMeta, remoteFieldsMeta) {
let fields = []
for (let field of _fields) {

View File

@@ -33,8 +33,7 @@ export default {
},
computed: {
iOption() {
const options = this.options.concat(this.internalOptions)
return _.uniqWith(options, _.isEqual)
return this.options.concat(this.internalOptions)
}
},
watch: {
@@ -68,16 +67,16 @@ export default {
type: field.type,
value: name
}
if (['choice', 'labeled_choice'].indexOf(field.type) > -1 && field.choices) {
if (field.type === 'choice' && field.choices) {
option.children = field.choices.map(item => {
if (typeof (item.value) === 'boolean') {
if (item.value) {
return { label: item.label, value: 'True' }
return { label: item.display_name, value: 'True' }
} else {
return { label: item.label, value: 'False' }
return { label: item.display_name, value: 'False' }
}
}
return { label: item.label, value: item.value }
return { label: item.display_name, value: item.value }
})
}
if (field.type === 'boolean') {

View File

@@ -1,13 +1,12 @@
<template>
<Dialog
v-if="showColumnSettingPopover"
:cancel-title="$tc('common.RestoreDefault')"
:destroy-on-close="true"
:title="$tc('common.CustomCol')"
:title="$t('common.CustomCol')"
:visible.sync="showColumnSettingPopover"
:destroy-on-close="true"
:show-cancel="false"
width="35%"
top="10%"
width="50%"
@cancel="restoreDefault()"
@confirm="handleColumnConfirm()"
>
<el-alert type="success">
@@ -24,20 +23,24 @@
style="margin-top:5px;"
>
<el-checkbox
:disabled="item.prop==='actions' || minColumns.indexOf(item.prop)!==-1"
:label="item.prop"
:disabled="
item.prop==='id' ||
item.prop==='actions' ||
minColumns.indexOf(item.prop)!==-1
"
>
{{ item.label }}
</el-checkbox>
</el-col>
</el-row>
</el-checkbox-group>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog/index'
export default {
name: 'ColumnSettingPopover',
components: {
@@ -79,10 +82,6 @@ export default {
handleColumnConfirm() {
this.showColumnSettingPopover = false
this.$emit('columnsUpdate', { columns: this.iCurrentColumns, url: this.url })
},
restoreDefault() {
this.showColumnSettingPopover = false
this.$emit('columnsUpdate', { columns: null, url: this.url })
}
}
}

View File

@@ -11,8 +11,8 @@
/>
<ColumnSettingPopover
:current-columns="popoverColumns.currentCols"
:min-columns="popoverColumns.minCols"
:total-columns-list="popoverColumns.totalColumnsList"
:min-columns="popoverColumns.minCols"
:url="config.url"
@columnsUpdate="handlePopoverColumnsChange"
/>
@@ -22,13 +22,15 @@
<script type="text/jsx">
import DataTable from '../DataTable'
import {
ActionsFormatter, ArrayFormatter, ChoicesFormatter, DateFormatter, DetailFormatter, DisplayFormatter,
ObjectRelatedFormatter
DateFormatter,
DetailFormatter,
DisplayFormatter,
ActionsFormatter,
ChoicesFormatter
} from '@/components/TableFormatters'
import i18n from '@/i18n/i18n'
import { newURL, replaceAllUUID } from '@/utils/common'
import ColumnSettingPopover from './components/ColumnSettingPopover'
import { newURL } from '@/utils/common'
export default {
name: 'AutoDataTable',
components: {
@@ -61,12 +63,13 @@ export default {
}
}
},
computed: {},
computed: {
},
watch: {
config: {
handler: function(iNew, iOld) {
handler(iNew) {
this.optionUrlMetaAndGenCols()
this.$log.debug('AutoDataTable Config change found: ')
this.$log.debug('AutoDataTable Config change found')
},
deep: true
}
@@ -76,12 +79,8 @@ export default {
},
methods: {
async optionUrlMetaAndGenCols() {
if (this.config.url === '') {
return
}
const url = (this.config.url.indexOf('?') === -1)
? `${this.config.url}?draw=1&display=1`
: `${this.config.url}&draw=1&display=1`
if (this.config.url === '') { return }
const url = (this.config.url.indexOf('?') === -1) ? `${this.config.url}?draw=1&display=1` : `${this.config.url}&draw=1&display=1`
this.$store.dispatch('common/getUrlMeta', { url: url }).then(data => {
const method = this.method.toUpperCase()
this.meta = data.actions && data.actions[method] ? data.actions[method] : {}
@@ -118,22 +117,7 @@ export default {
case 'is_valid':
col.label = i18n.t('common.Validity')
col.formatter = ChoicesFormatter
col.formatterArgs = {
textChoices: {
true: i18n.t('common.Yes'),
false: i18n.t('common.No')
}
}
col.width = '80px'
break
case 'is_active':
col.formatter = ChoicesFormatter
col.formatterArgs = {
textChoices: {
true: i18n.t('common.Active'),
false: i18n.t('common.Inactive')
}
}
col.align = 'center'
col.width = '80px'
break
case 'datetime':
@@ -145,41 +129,21 @@ export default {
}
return col
},
generateColumnByType(type, col, meta) {
generateColumnByType(type, col) {
switch (type) {
case 'choice':
col.sortable = 'custom'
col.formatter = DisplayFormatter
break
case 'labeled_choice':
col.sortable = 'custom'
col.formatter = ChoicesFormatter
break
case 'boolean':
col.formatter = ChoicesFormatter
col.align = 'center'
col.width = '80px'
break
case 'datetime':
col.formatter = DateFormatter
col.width = '160px'
break
case 'object_related_field':
col.formatter = ObjectRelatedFormatter
break
case 'm2m_related_field':
col.formatter = ObjectRelatedFormatter
break
case 'list':
col.formatter = ArrayFormatter
break
case 'json':
case 'field':
if (meta.child && meta.child.type === 'nested object') {
col.formatter = ObjectRelatedFormatter
}
break
}
// this.$log.debug('Field: ', type, col.prop, col)
return col
},
addHelpTipsIfNeed(col) {
@@ -191,9 +155,9 @@ export default {
return (
<span>{column.label}
<el-tooltip placement='bottom' effect='light' popperClass='help-tips'>
<div slot='content' domPropsInnerHTML={helpTips}/>
<div slot='content' domPropsInnerHTML={helpTips} />
<el-button style='padding: 0'>
<i class='fa fa-info-circle'/>
<i class='fa fa-info-circle' />
</el-button>
</el-tooltip>
</span>
@@ -219,12 +183,12 @@ export default {
col.filters = column.choices.map(item => {
if (typeof (item.value) === 'boolean') {
if (item.value) {
return { text: item['label'], value: 'True' }
return { text: item['display_name'], value: 'True' }
} else {
return { text: item['label'], value: 'False' }
return { text: item['display_name'], value: 'False' }
}
}
return { text: item['label'], value: item.value }
return { text: item['display_name'], value: item.value }
})
col.sortable = false
col['column-key'] = col.prop
@@ -232,65 +196,22 @@ export default {
}
return col
},
addOrderingIfNeed(col) {
if (col.prop) {
const column = this.meta[col.prop] || {}
if (column.order) {
col.sortable = 'custom'
col['column-key'] = col.prop
}
}
return col
},
setDefaultFormatterIfNeed(col) {
if (!col.formatter) {
col.formatter = (row, column, cellValue) => {
let value = cellValue
let padding = '0'
const excludes = [undefined, null, '']
if (excludes.indexOf(value) !== -1) {
padding = '6px'
value = '-'
}
return <span style={{ marginLeft: padding }}>{value}</span>
}
}
return col
},
generateColumn(name) {
const colMeta = this.meta[name] || {}
const customMeta = this.config.columnsMeta ? this.config.columnsMeta[name] : {}
let col = { prop: name, label: colMeta.label, showOverflowTooltip: true }
let col = { prop: name, label: colMeta.label }
col = this.generateColumnByName(name, col)
col = this.generateColumnByType(colMeta.type, col, colMeta)
col = this.setDefaultFormatterIfNeed(col)
col = this.generateColumnByType(colMeta.type, col)
col = Object.assign(col, customMeta)
col = this.addHelpTipsIfNeed(col)
col = this.addFilterIfNeed(col)
col = this.addOrderingIfNeed(col)
return col
},
generateTotalColumns() {
const config = _.cloneDeep(this.config)
let columns = []
const allColumnNames = Object.entries(this.meta)
.filter(([name, meta]) => !meta['write_only'])
.map(([name, meta]) => name)
.concat(config.columnsExtra || [])
let configColumns = config.columns || allColumnNames
const columnsExclude = config.columnsExclude || []
configColumns = configColumns.filter(item => !columnsExclude.includes(item))
// 解决后端 API 返回字段中包含 actions 的问题;
const hasColumnActions = configColumns.findIndex(item => item?.prop === 'actions') !== -1
if (!hasColumnActions) {
configColumns = [...configColumns.filter(i => i !== 'actions'), 'actions']
}
for (let col of configColumns) {
for (let col of config.columns) {
if (typeof col === 'object') {
columns.push(col)
} else if (typeof col === 'string') {
@@ -298,11 +219,7 @@ export default {
columns.push(col)
}
}
columns = columns.filter(item => {
if (item?.showFullContent) {
item.className = 'show-full-content'
}
let has = item.has
if (has === undefined) {
has = true
@@ -335,8 +252,7 @@ export default {
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
let tableName = this.config.name || this.$route.name + '_' + newURL(this.iConfig.url).pathname
tableName = replaceAllUUID(tableName)
const tableName = this.config.name || this.$route.name + '_' + newURL(this.iConfig.url).pathname
const configShowColumnsNames = _.get(_tableConfig[tableName], 'showColumns', null)
let showColumnsNames = configShowColumnsNames || defaultColumnsNames
if (showColumnsNames.length === 0) {
@@ -377,17 +293,11 @@ export default {
},
handlePopoverColumnsChange({ columns, url }) {
this.$log.debug('Columns change: ', columns)
if (columns === null) {
columns = this.cleanedColumnsShow.default
}
this.popoverColumns.currentCols = columns
const _tableConfig = localStorage.getItem('tableConfig')
? JSON.parse(localStorage.getItem('tableConfig'))
: {}
let tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
// 替换url中的uuid避免同一个类型接口生成多个keylocalStorage中的数据无法共用
tableName = replaceAllUUID(tableName)
const tableName = this.config.name || this.$route.name + '_' + newURL(url).pathname
_tableConfig[tableName] = {
'showColumns': columns
}

View File

@@ -39,6 +39,8 @@ export default {
showDelete: true,
showUpdate: true,
showSearch: false,
// 自定义header
customTreeHeader: false,
customTreeHeaderName: this.$t('assets.AssetTree'),
async: {
enable: true,
@@ -126,9 +128,9 @@ export default {
query['asset'] = ''
url = `${this.setting.url}${combinator}node_id=${objectId}&show_current_asset=${show_current_asset}`
} else if (treeNode.meta.type === 'asset') {
query['asset'] = treeNode.meta.data?.id || treeNode.id
query['asset'] = treeNode.meta.data.id
query['node'] = ''
url = `${this.setting.url}${combinator}asset_id=${query.asset}&show_current_asset=${show_current_asset}`
url = `${this.setting.url}${combinator}asset_id=${objectId}&show_current_asset=${show_current_asset}`
}
this.$router.push({ query })
this.$emit('urlChange', url)
@@ -142,16 +144,15 @@ export default {
this.$axios.delete(
`${this.treeSetting.nodeUrl}${currentNode.meta.data.id}/`
).then(() => {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$message.success(this.$t('common.deleteSuccessMsg'))
this.zTree.removeNode(currentNode)
this.refreshTree()
}).catch(() => {
// this.$message.error(this.$tc('common.deleteErrorMsg') + ' ' + error)
// this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
},
onRename: function(event, treeId, treeNode, isCancel) {
const currentNodeId = this.currentNodeId || treeNode.meta.data?.id || ''
const url = `${this.treeSetting.nodeUrl}${currentNodeId}/`
const url = `${this.treeSetting.nodeUrl}${this.currentNodeId}/`
if (isCancel) {
return
}
@@ -166,10 +167,8 @@ export default {
treeNode.name = treeNode.name + ' (' + assetsAmount + ')'
treeNode.meta.data = res
this.zTree.updateNode(treeNode)
this.$message.success(this.$tc('common.updateSuccessMsg'))
}).finally(() => {
this.refreshTree()
})
this.$message.success(this.$t('common.updateSuccessMsg'))
}).finally(() => { this.refreshTree() })
},
onBodyMouseDown: function(event) {
const rMenuID = this.$refs.dataztree.$refs.ztree.iRMenuID
@@ -235,9 +234,9 @@ export default {
nodes: treeNodesIds
}
).then((res) => {
this.$message.success(this.$tc('common.updateSuccessMsg'))
this.$message.success(this.$t('common.updateSuccessMsg'))
}).catch(error => {
this.$message.error(this.$tc('common.updateErrorMsg' + ' ' + error))
this.$message.error(this.$t('common.updateErrorMsg' + ' ' + error))
}).finally()
},
createTreeNode: function() {
@@ -265,9 +264,9 @@ export default {
const node = this.zTree.getNodeByParam('id', newNode.id, parentNode)
this.currentNodeId = node.meta.data.id || newNode.id
this.zTree.editName(node)
this.$message.success(this.$tc('common.createSuccessMsg'))
this.$message.success(this.$t('common.createSuccessMsg'))
}).catch(error => {
this.$message.error(this.$tc('common.createErrorMsg') + ' ' + error)
this.$message.error(this.$t('common.createErrorMsg') + ' ' + error)
})
},
refresh: function() {
@@ -305,13 +304,11 @@ export default {
text-decoration: none;
background-color: #f5f5f5;
}
.rmenu:hover {
.rmenu:hover{
background-color: #f5f7fa;
}
.data-z-tree >>> .fa {
width: 10px;
margin-right: 3px;
}
</style>

View File

@@ -2,7 +2,7 @@
<template>
<div>
<el-tabs type="border-card">
<el-tab-pane v-if="shouldHide('min')" :label="$tc('common.CronTab.min')">
<el-tab-pane v-if="shouldHide('min')" :label="this.$t('common.CronTab.min')">
<CrontabMin
ref="cronmin"
:check="checkNumber"
@@ -11,7 +11,7 @@
/>
</el-tab-pane>
<el-tab-pane v-if="shouldHide('hour')" :label="$tc('common.CronTab.hour')">
<el-tab-pane v-if="shouldHide('hour')" :label="this.$t('common.CronTab.hour')">
<CrontabHour
ref="cronhour"
:check="checkNumber"
@@ -20,7 +20,7 @@
/>
</el-tab-pane>
<el-tab-pane v-if="shouldHide('day')" :label="$tc('common.CronTab.day')">
<el-tab-pane v-if="shouldHide('day')" :label="this.$t('common.CronTab.day')">
<CrontabDay
ref="cronday"
:check="checkNumber"
@@ -29,7 +29,7 @@
/>
</el-tab-pane>
<el-tab-pane v-if="shouldHide('month')" :label="$tc('common.CronTab.month')">
<el-tab-pane v-if="shouldHide('month')" :label="this.$t('common.CronTab.month')">
<CrontabMonth
ref="cronmonth"
:check="checkNumber"
@@ -38,7 +38,7 @@
/>
</el-tab-pane>
<el-tab-pane v-if="shouldHide('week')" :label="$tc('common.CronTab.week')">
<el-tab-pane v-if="shouldHide('week')" :label="this.$t('common.CronTab.week')">
<CrontabWeek
ref="cronweek"
:check="checkNumber"
@@ -390,7 +390,6 @@ export default {
text-align: center;
margin-top: 20px;
}
.popup-main {
position: relative;
margin: 10px auto 0;
@@ -399,14 +398,12 @@ export default {
font-size: 12px;
overflow: hidden;
}
.popup-title {
overflow: hidden;
line-height: 34px;
padding-top: 6px;
background: #f2f2f2;
}
.popup-result {
position: relative;
box-sizing: border-box;
@@ -416,7 +413,6 @@ export default {
border: 1px solid #dcdfe6;
box-shadow: 0 2px 4px 0 rgb(0 0 0 / 12%), 0 0 6px 0 rgb(0 0 0 / 4%);
}
.popup-result .title {
position: absolute;
top: -17px;
@@ -428,13 +424,11 @@ export default {
line-height: 30px;
background: #fff;
}
.popup-result table {
text-align: center;
width: 100%;
margin: 0 auto;
}
.popup-result table span {
display: block;
width: 100%;
@@ -445,14 +439,12 @@ export default {
overflow: hidden;
border: 1px solid #e8e8e8;
}
.popup-result-scroll {
font-size: 12px;
line-height: 24px;
height: 10em;
overflow-y: auto;
}
.el-form-item--mini.el-form-item,
.el-form-item--small.el-form-item {
margin-bottom: 10px;

View File

@@ -25,13 +25,7 @@
<el-form-item>
<el-radio v-model="radioValue" :label="7">
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
multiple
style="width:100%"
>
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
<el-option v-for="item in 31" :key="item" :value="item">{{ item }}</el-option>
</el-select>
</el-radio>
@@ -51,8 +45,7 @@ export default {
},
check: {
type: Function,
default: () => {
}
default: () => {}
}
},
data() {
@@ -191,6 +184,6 @@ export default {
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
margin-bottom: 10px;
}
</style>

View File

@@ -25,13 +25,7 @@
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
multiple
style="width:100%"
>
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
<el-option v-for="item in 24" :key="item" :value="item-1">{{ item-1 }}</el-option>
</el-select>
</el-radio>
@@ -51,8 +45,7 @@ export default {
},
check: {
type: Function,
default: () => {
}
default: () => {}
}
},
data() {
@@ -160,6 +153,6 @@ export default {
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px
}
margin-bottom: 10px
}
</style>

View File

@@ -25,14 +25,7 @@
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
multiple
style="width:100%"
size="small"
>
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%" size="small">
<el-option v-for="item in 60" :key="item" :value="item-1">{{ item-1 }}</el-option>
</el-select>
</el-radio>
@@ -53,8 +46,7 @@ export default {
},
check: {
type: Function,
default: () => {
}
default: () => {}
}
},
data() {
@@ -159,6 +151,6 @@ export default {
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
margin-bottom: 10px;
}
</style>

View File

@@ -25,13 +25,7 @@
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
multiple
style="width:100%"
>
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
<el-option v-for="item in 12" :key="item" :value="item">{{ item }}</el-option>
</el-select>
</el-radio>
@@ -51,8 +45,7 @@ export default {
},
check: {
type: Function,
default: () => {
}
default: () => {}
}
},
data() {
@@ -165,6 +158,6 @@ export default {
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
margin-bottom: 10px;
}
</style>

View File

@@ -13,7 +13,7 @@
<script>
import parser from 'cron-parser'
import { toSafeLocalDateStr } from '@/utils/common'
import moment from 'moment'
export default {
name: 'CrontabResult',
props: {
@@ -46,10 +46,10 @@ export default {
const rule = 0 + ' ' + this.$options.propsData.ex
try {
this.resultList = []
const interval = parser.parseExpression(rule)
var interval = parser.parseExpression(rule)
for (let index = 0; index < 5; index++) {
const cur = interval.next().toString()
this.resultList.push(toSafeLocalDateStr(cur))
this.resultList.push(moment(cur).format('YYYY-MM-DD HH:mm:ss'))
}
} catch (error) {
this.isShow = false

View File

@@ -18,13 +18,7 @@
<el-form-item>
<el-radio v-model="radioValue" :label="6">
{{ this.$t('common.CronTab.appoint') }}
<el-select
v-model="checkboxList"
clearable
:placeholder="$tc('common.CronTab.manyChoose')"
multiple
style="width:100%"
>
<el-select v-model="checkboxList" clearable :placeholder="this.$t('common.CronTab.manyChoose')" multiple style="width:100%">
<el-option v-for="(item,index) of weekList" :key="index" :value="index+1">{{ item }}</el-option>
</el-select>
</el-radio>
@@ -45,8 +39,7 @@ export default {
},
check: {
type: Function,
default: () => {
}
default: () => {}
}
},
data() {
@@ -180,6 +173,6 @@ export default {
<style scoped>
.el-form-item--small.el-form-item {
margin-bottom: 10px;
}
margin-bottom: 10px;
}
</style>

View File

@@ -3,7 +3,7 @@
<div class="box">
<el-input v-model="input" clearable @focus="showDialog" @clear="onClear" />
</div>
<el-dialog :title="$tc('common.CronTab.newCron')" :visible.sync="showCron" top="8vh" width="580px" append-to-body>
<el-dialog :title="this.$t('common.CronTab.newCron')" :visible.sync="showCron" top="8vh" width="580px" append-to-body>
<Crontab
:expression="expression"
@hide="showCron = false"
@@ -15,7 +15,6 @@
<script>
import Crontab from './Crontab.vue'
export default {
components: { Crontab },
props: {
@@ -52,5 +51,5 @@ export default {
<style scoped>
.el-dialog__body {
padding: 12px 16px;
}
}
</style>

View File

@@ -10,7 +10,7 @@
placement="bottom-start"
@command="handleDropdownCallback"
>
<el-button class="more-action" :size="size" v-bind="cleanButtonAction(action)">
<el-button :size="size" v-bind="cleanButtonAction(action)">
{{ action.title }}<i class="el-icon-arrow-down el-icon--right" />
</el-button>
<el-dropdown-menu slot="dropdown" style="overflow: auto;max-height: 60vh">
@@ -28,7 +28,6 @@
:command="[option, action]"
v-bind="option"
>
<i v-if="option.fa" :class="'fa ' + option.fa" />
{{ option.title }}
</el-dropdown-item>
</template>
@@ -45,11 +44,7 @@
>
<el-tooltip :disabled="!action.tip" :content="action.tip" placement="top">
<span>
<span v-if="action.fa" style="vertical-align: initial;">
<i v-if="action.fa.startsWith('fa-')" :class="'fa ' + action.fa" />
<svg-icon v-else :icon-class="action.fa" style="font-size: 14px;" />
</span>
{{ action.title }}
<i v-if="action.fa" :class="'fa ' + action.fa" />{{ action.title }}
</span>
</el-tooltip>
</el-button>

View File

@@ -43,25 +43,16 @@
<template v-for="opt in options">
<el-option
v-if="data.type === 'select'"
:key="opt.label"
:key="opt.value"
v-bind="opt"
/>
<el-checkbox-button
v-else-if="data.type === 'checkbox-group' && data.style === 'button'"
:key="opt.value"
v-bind="opt"
:label="'value' in opt ? opt.value : opt.label"
>
{{ opt.label }}
</el-checkbox-button>
<!-- TODO: 支持 el-checkbox-button 变体 -->
<el-checkbox
v-else-if="data.type === 'checkbox-group' && data.style !== 'button'"
:key="opt.value"
v-else-if="data.type === 'checkbox-group'"
:key="opt.label"
v-bind="opt"
:label="'value' in opt ? opt.value : opt.label"
>
{{ opt.label }}
{{ opt.value }}
</el-checkbox>
<!-- WARNING: radio label 属性来表示 value 的含义 -->
<!-- FYI: radio value 属性可以在没有 radio-group 时用来关联到同一个 v-model -->
@@ -238,9 +229,7 @@ export default {
.then(resp => {
if (isOptionsCase) {
let formRenderer = this.$parent
while (formRenderer.$options._componentTag !== 'el-form-renderer') {
formRenderer = formRenderer.$parent
}
while (formRenderer.$options._componentTag !== 'el-form-renderer') { formRenderer = formRenderer.$parent }
formRenderer.setOptions(this.prop, resp)
} else {
this.propsInner = { [prop]: resp }

View File

@@ -14,36 +14,10 @@
<slot v-for="item in fields" :slot="`$id:${item.id}`" :name="`$id:${item.id}`" />
<el-form-item v-if="hasButtons" class="form-buttons">
<el-button
v-for="button in moreButtons"
:key="button.title"
:loading="button.loading"
size="small"
v-bind="button"
@click="handleClick(button)"
>
{{ button.title }}
</el-button>
<el-button v-if="defaultButton && hasReset" size="small" @click="resetForm('form')">
{{ $t('common.Reset') }}
</el-button>
<el-button
v-if="defaultButton && hasSaveContinue"
size="small"
@click="submitForm('form', true)"
>
{{ $t('common.SaveAndAddAnother') }}
</el-button>
<el-button
v-if="defaultButton"
:disabled="!canSubmit"
:loading="isSubmitting"
size="small"
type="primary"
@click="submitForm('form')"
>
{{ $t('common.Submit') }}
</el-button>
<el-button v-for="button in moreButtons" :key="button.title" size="small" v-bind="button" :loading="button.loading" @click="handleClick(button)">{{ button.title }}</el-button>
<el-button v-if="defaultButton && hasReset" size="small" @click="resetForm('form')">{{ $t('common.Reset') }}</el-button>
<el-button v-if="defaultButton && hasSaveContinue" size="small" @click="submitForm('form', true)">{{ $t('common.SaveAndAddAnother') }}</el-button>
<el-button v-if="defaultButton" size="small" :loading="isSubmitting" type="primary" @click="submitForm('form')">{{ $t('common.Submit') }}</el-button>
</el-form-item>
</ElFormRender>
</template>
@@ -51,7 +25,6 @@
<script>
import ElFormRender from './components/el-form-renderer'
import { scrollToError } from '@/utils'
export default {
components: {
ElFormRender
@@ -69,10 +42,6 @@ export default {
type: Boolean,
default: true
},
canSubmit: {
type: Boolean,
default: true
},
hasSaveContinue: {
type: Boolean,
default: true
@@ -156,7 +125,7 @@ export default {
}
.el-form ::v-deep .el-form-item__error {
position: inherit;
position: inherit;
}
.el-form ::v-deep .form-group-header {
@@ -175,12 +144,10 @@ export default {
font-size: 12px;
line-height: 18px;
}
.el-form ::v-deep .help-block a {
color: var(--color-primary);
}
.form-buttons {
margin-top: 20px;
padding-top: 10px;
}
</style>

View File

@@ -14,22 +14,9 @@ export const EmailCheck = {
trigger: ['blur', 'change']
}
export const IpCheck = {
required: true,
validator: (rule, value, callback) => {
value = value?.trim()
if (/^[\w://.?-]+$/.test(value)) {
callback()
} else {
callback(new Error(i18n.t('common.FormatError')))
}
},
trigger: ['blur', 'change']
}
export const specialEmojiCheck = {
validator: (rule, value, callback) => {
value = value?.trim()
value = value.trim()
if (/[\uD83C[\uDF00-\uDFFF]|\uD83D[\uDC00-\uDE4F]/.test(value)) {
callback(new Error(i18n.t('common.NotSpecialEmoji')))
} else {
@@ -39,26 +26,11 @@ export const specialEmojiCheck = {
trigger: ['blur', 'change']
}
// 只能输入字母、数字、下划线
export const matchAlphanumericUnderscore = {
validator: (rule, value, callback) => {
value = value?.trim()
if (!/^[a-zA-Z0-9_]+$/.test(value)) {
callback(new Error(i18n.t('common.notAlphanumericUnderscore')))
} else {
callback()
}
},
trigger: ['blur', 'change']
}
export default {
IpCheck,
Required,
RequiredChange,
EmailCheck,
specialEmojiCheck,
matchAlphanumericUnderscore
specialEmojiCheck
}
export const JsonRequired = {
@@ -66,7 +38,7 @@ export const JsonRequired = {
trigger: 'change',
validator: (rule, value, callback) => {
try {
typeof value === 'string' ? JSON.parse(value) : value
JSON.parse(value)
callback()
} catch (e) {
callback(new Error(i18n.t('common.InvalidJson')))
@@ -79,8 +51,8 @@ export const JsonRequiredUserNameMapped = {
trigger: 'change',
validator: (rule, value, callback) => {
try {
const v = typeof value === 'string' ? JSON.parse(value) : value
const hasUserName = _.map(v, (value) => value)
JSON.parse(value)
const hasUserName = _.map(JSON.parse(value), (value) => value)
if (!hasUserName.includes('username')) {
callback(new Error(i18n.t('common.requiredHasUserNameMapped')))
}

View File

@@ -8,12 +8,12 @@
<el-table
ref="table"
v-loading="loading"
v-bind="tableAttrs"
:data="data"
:row-class-name="rowClassName"
v-bind="tableAttrs"
@select="selectStrategy.onSelect"
v-on="$listeners"
@selection-change="selectStrategy.onSelectionChange"
@select="selectStrategy.onSelect"
@select-all="selectStrategy.onSelectAll($event, canSelect)"
@sort-change="onSortChange"
>
@@ -91,28 +91,28 @@
<!--非树-->
<template v-else>
<el-data-table-column v-if="hasSelection" :align="selectionAlign" :selectable="canSelect" type="selection" />
<el-data-table-column v-if="hasSelection" type="selection" :align="selectionAlign" :selectable="canSelect" />
<el-data-table-column
v-for="col in columns"
:key="col.prop"
:filter-method="typeof col.filterMethod === 'function' ? col.filterMethod : null"
:filter-multiple="false"
:filters="col.filters || null"
:formatter="typeof col.formatter === 'function' ? col.formatter : null"
:filters="col.filters || null"
:filter-multiple="false"
:filter-method="typeof col.filterMethod === 'function' ? col.filterMethod : null"
v-bind="{align: columnsAlign, ...col}"
>
<template v-if="col.formatter && typeof col.formatter !== 'function'" v-slot:default="{row, column, index}">
<div
:is="col.formatter"
:key="row.id"
:cell-value="row[col.prop]"
:col="col"
:table-data="data"
:row="row"
:column="column"
:index="index"
:reload="getList"
:row="row"
:table-data="data"
:url="url"
:reload="getList"
:col="col"
:cell-value="row[col.prop]"
/>
</template>
</el-data-table-column>
@@ -122,12 +122,13 @@
<el-pagination
v-if="hasPagination"
:background="paginationBackground"
:current-page="page"
:layout="paginationLayout"
:page-size="size"
:page-sizes="paginationSizes"
:page-size="size"
:total="total"
:background="paginationBackground"
style="text-align: right; padding: 10px 0;"
:layout="paginationLayout"
v-bind="extraPaginationAttrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
@@ -135,18 +136,18 @@
<the-dialog
ref="dialog"
:button-size="buttonSize"
:dialog-attrs="dialogAttrs"
:new-title="dialogNewTitle"
:edit-title="dialogEditTitle"
:view-title="dialogViewTitle"
:form="form"
:form-attrs="formAttrs"
:new-title="dialogNewTitle"
:view-title="dialogViewTitle"
:dialog-attrs="dialogAttrs"
:button-size="buttonSize"
@confirm="onConfirm"
>
<template v-slot="scope">
<!-- @slot 表单作用域插槽当编辑查看时传入row新增时row=null -->
<slot :row="scope.row" name="form" />
<slot name="form" :row="scope.row" />
</template>
</the-dialog>
</template>
@@ -166,7 +167,6 @@ import getLocatedSlotKeys from './utils/extract-keys'
import transformSearchImmediatelyItem from './utils/search-immediately-item'
import isFalsey from './utils/is-falsey'
import merge from 'deepmerge'
const defaultFirstPage = 1
const noPaginationDataPath = 'payload'
@@ -268,8 +268,7 @@ export default {
*/
beforeSearch: {
type: Function,
default() {
}
default() {}
},
/**
* 单选, 适用场景: 不可以批量删除
@@ -361,36 +360,28 @@ export default {
*/
newText: {
type: String,
default: function() {
return this.$t('ops.Add')
}
default: '新增'
},
/**
* 修改按钮文案
*/
editText: {
type: String,
default: function() {
return this.$t('ops.Modify')
}
default: '修改'
},
/**
* 查看按钮文案
*/
viewText: {
type: String,
default: function() {
return this.$t('ops.View')
}
default: '查看'
},
/**
* 删除按钮文案
*/
deleteText: {
type: String,
default: function() {
return this.$t('ops.Delete')
}
default: '删除'
},
/**
* 删除提示语。接受要删除的数据(单个对象或数组);返回字符串
@@ -400,7 +391,7 @@ export default {
deleteMessage: {
type: Function,
default() {
return this.$t('ops.Confirm') + this.deleteText + '?'
return `确认${this.deleteText}吗?`
}
},
/**
@@ -459,7 +450,7 @@ export default {
onSuccess: {
type: Function,
default() {
return this.$message.success(this.$t('ops.SuccessfulOperation'))
return this.$message.success('操作成功')
}
},
/**
@@ -716,8 +707,7 @@ export default {
},
extraPaginationAttrs: {
type: Object,
default: () => {
}
default: () => {}
},
hasSelection: {
type: Boolean,
@@ -1047,7 +1037,6 @@ export default {
} else {
this.innerQuery = merge(this.innerQuery, attrs)
}
this.selected.splice(0, this.selected.length)
return this.getList()
},
searchDate(attrs) {
@@ -1167,7 +1156,7 @@ export default {
* @param {object|object[]} - 要删除的数据对象或数组
*/
onDefaultDelete(data) {
this.$confirm(this.deleteMessage(data), this.$t('common.Info'), {
this.$confirm(this.deleteMessage(data), '提示', {
type: 'warning',
confirmButtonClass: 'el-button--danger',
beforeClose: async(action, instance, done) => {
@@ -1210,9 +1199,7 @@ export default {
remain === 0 &&
this.page === lastPage &&
this.page > defaultFirstPage
) {
this.page--
}
) { this.page-- }
},
// 树形table相关

View File

@@ -1,22 +1,16 @@
.el-data-table ::v-deep .el-table td {
padding: 4px 0;
}
.el-data-table ::v-deep .el-table th {
padding: 4px 0;
}
.el-data-table ::v-deep .el-form-item {
margin-bottom: 10px !important;
margin-top: 10px;
}
.el-data-table ::v-deep .el-pagination {
text-align: right;
padding: 15px 25px 20px 15px;
.el-pagination__total {
float: left;
.el-data-table ::v-deep .el-pagination{
text-align: center !important;
}
.el-data-table ::v-deep .el-table td{
padding: 4px 0;
}
.el-data-table ::v-deep .el-table th{
padding: 4px 0;
}
.el-data-table ::v-deep .el-form-item{
margin-bottom:10px !important ;
margin-top:10px;
}
.el-data-table ::v-deep .el-pagination{
padding:15px 0 !important ;
}
}

View File

@@ -1,7 +1,7 @@
<template>
<ElDatableTable
ref="table"
class="el-data-table"
class="el-table"
v-bind="tableConfig"
@update="onUpdate"
v-on="iListeners"
@@ -21,8 +21,7 @@ export default {
props: {
config: {
type: Object,
default: () => {
}
default: () => {}
}
},
data() {
@@ -48,13 +47,9 @@ export default {
buttonSize: 'mini',
tableAttrs: {
stripe: false, // 斑马纹表格
border: false, // 表格边框
border: true, // 表格边框
fit: true, // 宽度自适应,
tooltipEffect: 'dark',
rowClassName: ({ row }) => {
const selected = this.dataTable.selected.find(item => item.id === row.id)
return selected ? 'selected-row' : ''
}
tooltipEffect: 'dark'
},
extraButtons: userTableActions.extraButtons,
onEdit: (row) => {
@@ -70,7 +65,6 @@ export default {
},
pageCount: 5,
paginationLayout: 'total, sizes, prev, pager, next',
paginationSize: JSON.parse(localStorage.getItem('paginationSize')) || 15,
paginationSizes: [15, 30, 50, 100],
paginationBackground: true,
transformQuery: query => {
@@ -92,36 +86,40 @@ export default {
}
return query
},
theRowDefaultIsSelected: (row) => {
return false
}
theRowDefaultIsSelected: (row) => { return false }
}
}
},
computed: {
iListeners() {
const defaultListeners = {}
return Object.assign(defaultListeners, this.$listeners, this.tableConfig?.listeners)
},
dataTable() {
return this.$refs.table
},
tableConfig() {
const tableDefaultConfig = this.defaultConfig
tableDefaultConfig.paginationSize = _.get(this.globalTableConfig, 'paginationSize', 15)
let tableAttrs = tableDefaultConfig.tableAttrs
if (this.config.tableAttrs) {
tableAttrs = Object.assign(tableAttrs, this.config.tableAttrs)
}
const config = Object.assign(tableDefaultConfig, this.config)
config.tableAttrs = tableAttrs
this.$log.debug('elTableConfig', config)
return config
},
iListeners() {
return Object.assign({}, this.$listeners, this.tableConfig.listeners)
},
dataTable() {
return this.$refs.table
},
...mapGetters({
'globalTableConfig': 'tableConfig'
})
},
watch: {},
watch: {
config: {
handler() {
// this.getList()
},
deep: true
}
},
methods: {
getList() {
this.$refs.table.clearSelection()
@@ -155,7 +153,6 @@ export default {
}
},
handleSizeChange(val) {
localStorage.setItem('paginationSize', val)
this.$store.commit('table/SET_TABLE_CONFIG',
{
key: 'paginationSize',
@@ -167,55 +164,37 @@ export default {
}
</script>
<style lang="scss" scoped>
.el-data-table > > > .el-table {
.table {
margin-top: 15px;
}
<style lang="less" scoped>
.el-table__row {
&.selected-row {
background-color: #f5f7fa;
}
& > td {
line-height: 1.5;
padding: 6px 0;
font-size: 13px;
* {
vertical-align: middle;
}
.el-checkbox {
vertical-align: super;
}
& > div > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}
.el-table__header > thead > tr > th {
padding: 6px 0;
background-color: #ffffff;
font-size: 13px;
line-height: 1.5;
.cell {
white-space: nowrap !important;
overflow: hidden;
text-overflow: ellipsis;
}
}
.el-table ::v-deep .el-table__row > td {
line-height: 1.5;
padding: 8px 0;
}
.el-data-table >>> .el-table .el-table__header > thead > tr .is-sortable {
padding: 5px 0;
.cell {
padding-top: 3px!important;
}
.el-table ::v-deep .el-table__row > td> div > span {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.el-table ::v-deep .el-table__header > thead > tr >th {
padding: 8px 0;
background-color: #F5F5F6;
font-size: 13px;
line-height: 1.5;
}
.table{
margin-top: 15px;
}
//分页
.el-pagination ::v-deep .el-pagination__total{
float: left;
}
.el-pagination ::v-deep .el-pagination__sizes{
float: left;
}
//修改颜色
// .el-button--text{
// color: #409EFF;
// }
</style>

View File

@@ -1,31 +1,47 @@
<template>
<div>
<div class="treebox">
<div>
<el-input
v-if="treeSetting.showSearch && showTreeSearch"
v-model="treeSearchValue"
:placeholder="$tc('common.Search')"
class="fixed-tree-search"
prefix-icon="fa fa-search"
size="mini"
@input="treeSearchHandle"
>
<span slot="suffix">
<!-- <i class="fa fa-search" style="font-size: 14px; color: #676A6C;" /> -->
<svg-icon
:icon-class="'close'"
class="icon"
style="font-size: 14px;"
@click="onClose"
<div
v-if="treeSetting.customTreeHeader"
class="tree-header treebox"
>
<div class="content">
<span class="title">
{{ treeSetting.customTreeHeaderName }}
</span>
<span class="tree-banner-icon-zone">
<a id="searchIcon" class="tree-search special">
<i
class="fa fa-search tree-banner-icon"
@click.stop="treeSearch"
/>
</span>
</el-input>
<input
id="searchInput"
v-model="treeSearchValue"
type="text"
autocomplete="off"
class="tree-input"
>
</a>
<i
class="fa fa-refresh tree-banner-icon"
style="margin-right: 2px;"
@click.stop="refresh"
/>
</span>
</div>
<ul v-show="loading" class="ztree">
{{ this.$t('common.tree.Loading') }}...
</ul>
<ul v-show="!loading" :id="iZTreeID" :key="iZTreeID" class="ztree" />
<ul v-show="!loading" :id="iZTreeID" class="ztree" />
<div v-if="treeSetting.treeUrl===''" class="tree-empty">
{{ this.$t('common.tree.Empty') }}
</div>
</div>
<div v-else class="treebox">
<ul v-show="loading" class="ztree">
{{ this.$t('common.tree.Loading') }}...
</ul>
<ul v-show="!loading" :id="iZTreeID" class="ztree" />
<div v-if="treeSetting.treeUrl===''" class="tree-empty">
{{ this.$t('common.tree.Empty') }}
<a id="tree-refresh"><i class="fa fa-refresh" /></a>
@@ -46,17 +62,16 @@ import $ from '@/utils/jquery-vendor.js'
import '@ztree/ztree_v3/js/jquery.ztree.all.min.js'
import '@ztree/ztree_v3/js/jquery.ztree.exhide.min.js'
import '@/styles/ztree.css'
import '@/styles/ztree_icon.css'
import axiosRetry from 'axios-retry'
const defaultObject = {
type: Object,
default: () => {
}
default: () => {}
}
export default {
name: 'ZTree',
components: {},
components: {
},
props: {
setting: defaultObject
},
@@ -68,7 +83,6 @@ export default {
rMenu: '',
init: false,
loading: false,
showTreeSearch: JSON.parse(localStorage.getItem('showTreeSearch')) || false,
treeSearchValue: ''
}
},
@@ -79,99 +93,88 @@ export default {
},
mounted() {
window.refresh = this.refresh
window.onSearch = this.onSearch
window.treeSearch = this.treeSearch
this.initTree()
},
beforeDestroy() {
$.fn.zTree.destroy(this.iZTreeID)
},
methods: {
async initTree(refresh = false) {
initTree: function() {
const vm = this
let treeUrl
this.loading = true
if (refresh && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 &&
this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1
) {
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1)
? `${this.treeSetting.treeUrl}?rebuild_tree=1`
: `${this.treeSetting.treeUrl}&rebuild_tree=1`
if (this.init) {
this.loading = true
}
if (this.init && this.treeSetting.treeUrl.indexOf('/perms/') !== -1 && this.treeSetting.treeUrl.indexOf('rebuild_tree') === -1) {
treeUrl = (this.treeSetting.treeUrl.indexOf('?') === -1) ? `${this.treeSetting.treeUrl}?rebuild_tree=1` : `${this.treeSetting.treeUrl}&rebuild_tree=1`
} else {
treeUrl = this.treeSetting.treeUrl
}
if (refresh) {
$.fn.zTree.destroy(this.iZTreeID)
}
let res = await this.$axios.get(treeUrl, {
this.$axios.get(treeUrl, {
'axios-retry': {
retries: 20,
retryCondition: e => {
return axiosRetry.isNetworkOrIdempotentRequestError(e) || e.response.status === 409
},
shouldResetTimeout: true,
retryDelay: () => {
return 5000
}
retryDelay: () => { return 5000 }
}
}).then(res => {
if (!res) res = []
if (res.length === 0) {
res.push({
name: this.$t('common.tree.Empty')
})
}
this.treeSetting.treeUrl = treeUrl
if (this.init) {
vm.zTree.destroy()
}
})
vm.loading = false
if (!res) res = []
if (res?.length === 0) {
res?.push({
name: this.$t('common.tree.Empty')
})
}
this.treeSetting.treeUrl = treeUrl
vm.zTree = $.fn.zTree.init($(`#${this.iZTreeID}`), this.treeSetting, res)
const rootNode = this.zTree.getNodes()[0]
this.rootNodeAddDom(rootNode)
// 手动上报事件, Tree加载完成
this.$emit('TreeInitFinish', this.zTree)
if (this.treeSetting.showMenu) {
this.rMenu = $(`#${this.iRMenuID}`)
}
if (this.treeSetting?.otherMenu) {
$('.menu-actions').append(this.otherMenu)
}
this.zTree = $.fn.zTree.init($(`#${this.iZTreeID}`), this.treeSetting, res)
if (!this.treeSetting.customTreeHeader) {
this.rootNodeAddDom(this.zTree)
}
// 手动上报事件, Tree加载完成
this.$emit('TreeInitFinish', this.zTree)
if (this.treeSetting.showMenu) {
this.rMenu = $(`#${this.iRMenuID}`)
}
if (this.treeSetting.otherMenu) {
$('.menu-actions').append(this.otherMenu)
}
}).finally(_ => {
vm.loading = false
vm.init = true
})
},
onSearch() {
this.showTreeSearch = !this.showTreeSearch
localStorage.setItem('showTreeSearch', JSON.stringify(this.showTreeSearch))
},
onClose() {
this.refresh()
this.onSearch()
},
rootNodeAddDom(rootNode) {
rootNodeAddDom(ztree) {
const { showSearch, showRefresh } = this.treeSetting
const searchIcon = `
<a class="tree-action-btn" id='search-btn' onclick="onSearch()">
<i class='fa fa-search tree-banner-icon'></i>
</a>`
const refreshIcon = `
<a id='tree-refresh' class="tree-action-btn" onclick='refresh()'>
<i class='fa fa-refresh'></i>
</a>`
const searchIcon = `<a class="tree-search" id="searchIcon">
<i class='fa fa-search tree-banner-icon' onclick="treeSearch()" /></i>
<input type="text" autocomplete="off" id="searchInput" class="tree-input" />
</a>`
const refreshIcon = "<a id='tree-refresh' onclick='refresh()'><i class='fa fa-refresh'></i></a>"
const treeActions = `${showSearch ? searchIcon : ''}${showRefresh ? refreshIcon : ''}`
const icons = `
<span style="float: right; margin-right: 10px">
${treeActions}
</span>`
const icons = `<span class="">${treeActions}</span>`
const rootNode = ztree.getNodes()[0]
if (rootNode) {
const $rootNodeRef = $('#' + rootNode.tId + '_a')
$rootNodeRef.after(icons)
}
},
async refresh() {
refresh() {
this.treeSearchValue = ''
if (this.treeSetting?.callback?.refresh) {
await this.treeSetting.callback.refresh()
const result = this.treeSetting?.callback?.refresh()
if (result && result.then) {
result.finally(() => {
this.initTree()
})
} else {
this.initTree()
}
this.zTree.destroy()
setTimeout(() => this.initTree(true), 200)
},
treeSearch() {
const searchIcon = document.getElementById(`searchIcon`)
@@ -187,15 +190,16 @@ export default {
searchIcon.classList.toggle('active')
}
}
searchInput.oninput = e => this.treeSearchHandle((e.target.value || ''))
searchInput.oninput = _.debounce((e) => {
e.stopPropagation()
const value = e.target.value || ''
if (this.treeSetting.async.enable) {
this.filterAssetsServer(value)
} else {
this.filterTree(value)
}
}, 600)
},
treeSearchHandle: _.debounce(function(value) {
if (this.treeSetting.async.enable) {
this.filterAssetsServer(value)
} else {
this.filterTree(value)
}
}, 600),
getCheckedNodes: function() {
return this.zTree.getCheckedNodes(true)
},
@@ -319,10 +323,8 @@ export default {
const newNode = { id: 'search', name: name, isParent: true, open: true, zAsync: true }
searchNode = this.zTree.addNodes(null, newNode)[0]
searchNode.zAsync = true
this.rootNodeAddDom(searchNode)
const nodesGroupByOrg = this.groupBy(nodes, (node) => {
return node.meta?.data?.org_name
return node.meta.data.org_name
})
for (const item of nodesGroupByOrg) {
@@ -330,6 +332,7 @@ export default {
}
searchNode.open = true
})
return
}
}
@@ -350,18 +353,15 @@ export default {
list-style: none;
background-clip: padding-box;
}
.dataTables_wrapper .dataTables_processing {
opacity: .9;
border: none;
}
div.rMenu li {
div.rMenu li{
margin: 6px 0;
cursor: pointer;
list-style: none outside none;
}
.dropdown-menu {
border: medium none;
min-width: 160px;
@@ -379,15 +379,12 @@ export default {
top: 100%;
z-index: 1000;
}
.ztree ::v-deep .fa {
font: normal normal normal 14px/1 FontAwesome !important;
}
.dropdown a:hover {
background-color: #f1f1f1
}
.dropdown-menu > li > a {
border-radius: 3px;
color: inherit;
@@ -400,51 +397,27 @@ export default {
clear: both;
white-space: nowrap;
}
.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus {
color: #262626;
text-decoration: none;
background-color: #f5f5f5;
}
.treebox {
background-color: transparent;
>>> .ztree {
overflow: auto;
background-color: transparent;
max-height: calc(100vh - 220px);
min-height: 500px;
li {
background-color: transparent !important;
.button {
background-color: rgba(0, 0, 0, 0);
}
ul {
background-color: transparent !important;
}
}
}
height: 80vh;
overflow: auto;
}
::v-deep #tree-refresh {
margin-left: 3px;
}
::v-deep .tree-banner-icon-zone {
position: absolute;
right: 7px;
height: 30px;
overflow: hidden;
.fa {
color: #838385 !important;;
color: #838385!important;;
&:hover {
color: #606266 !important;;
color: #606266!important;;
}
}
}
@@ -459,11 +432,9 @@ export default {
vertical-align: sub;
transition: .25s;
overflow: hidden;
.fa {
width: 13px !important;
width: 13px!important;
}
.fa-search {
padding-top: 1px;
}
@@ -471,20 +442,23 @@ export default {
::v-deep .tree-search .tree-banner-icon {
position: absolute;
top: 4px;
top: 1px;
left: 6px;
width: 6px;
height: 6px;
border-radius: 12px;
padding: 10px 6px;
overflow: hidden;
background-color: transparent !important;
background-color: transparent!important;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}
::v-deep .tree-search.active {
::v-deep .tree-search.active {
width: 160px;
background-color: #ffffff !important;
background-color: #ffffff!important;
}
::v-deep .tree-search.active:hover {
@@ -496,7 +470,7 @@ export default {
left: 20px;
width: 133px;
height: 100%;
background-color: #ffffff !important;
background-color: #ffffff!important;
color: #606266;
display: flex;
justify-content: center;
@@ -504,14 +478,11 @@ export default {
border: none;
outline: none;
}
.tree-header {
position: relative;
.title {
font-weight: 500;
}
.content {
height: 30px;
line-height: 30px;
@@ -522,67 +493,19 @@ export default {
overflow: hidden;
cursor: pointer;
background-color: #D7D8DC;
.rotate {
transition: all .1 .8s;
transition: all .1.8s;
transform: rotate(-90deg);
}
.fa-caret-down {
font-size: 16px;
}
.special {
top: 1px !important;
top: 1px!important;
}
}
}
.tree-empty {
margin-left: 4px;
}
.fixed-tree-search {
margin-bottom: 10px;
& > > > .el-input__inner {
border-radius: 4px;
background: #fafafa;
padding-right: 32px;
}
& > > > .el-input__suffix {
padding-right: 8px;
}
& > > > .el-input__prefix {
padding-left: 6px;
}
& > > > .el-input__suffix-inner {
line-height: 30px;
}
}
.icon-refresh {
border-radius: 4px;
padding: 0 1px;
z-index: 1;
&:hover {
cursor: pointer;
color: #606266;
border-color: #d2d2d2;
background-color: #e6e6e6;
}
}
.icon {
cursor: pointer;
}
.tree-action-btn {
padding: 0 2px;
color: red;
}
</style>

View File

@@ -1,6 +1,4 @@
<script type="text/jsx">
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'ItemValue',
props: {
@@ -17,60 +15,27 @@ export default {
default: null
}
},
computed: {
displayValue() {
if ([null, undefined, ''].includes(this.value)) {
return '-'
}
if (typeof this.value === 'boolean') {
return this.toChoicesDisplay(this.value)
} else if (typeof this.value === 'object') {
return this.value
} else if (this.value instanceof Array) {
return this.value.map(item => {
if (typeof item === 'object') {
return item.label || item.title
} else {
return item
}
}).join(', ')
} else if (this.isDatetime(this.value)) {
return toSafeLocalDateStr(this.value)
} else {
return this.value
}
}
},
methods: {
toChoicesDisplay(value) {
if (!value) {
return this.$t('common.No')
}
return this.$t('common.Yes')
},
isDatetime(value) {
if (typeof value !== 'string') {
return false
}
if (value.split(' ').length !== 3) {
return false
}
if (value.split(' ')[1].split(':').length !== 3) {
return false
}
if (isNaN(value) && !isNaN(Date.parse(value))) {
return true
}
}
},
render(h) {
if (typeof this.formatter === 'function') {
return this.formatter(this.item, this.value)
}
if (typeof this.value === 'boolean') {
return (
<span class='item-value'>{this.toChoicesDisplay(this.value)}</span>
)
}
if (this.value instanceof Array) {
const newArr = this.value || []
return (
<span>
<span class='item-value'>
{
newArr.map((item, index) => <div key={index}>{item.key}{item.value} </div>)
}
@@ -78,14 +43,15 @@ export default {
)
}
return (
<span>{this.displayValue}</span>
<span class='item-value'>{this.value}</span>
)
}
}
</script>
<style scoped>
a {
color: var(--color-success);
.item-value {
word-break: break-word;
}
</style>

View File

@@ -1,139 +0,0 @@
<template>
<DetailCard v-if="!loading" :items="items" v-bind="$attrs" />
</template>
<script>
import DetailCard from './index'
import { toSafeLocalDateStr } from '@/utils/common'
export default {
name: 'AutoDetailCard',
components: { DetailCard },
props: {
object: {
type: Object,
default: () => ({})
},
url: {
type: String,
required: true
},
fields: {
type: Array,
default: null
},
excludes: {
type: Array,
default: null
},
showUndefine: {
type: Boolean,
default: true
},
formatters: {
type: Object,
default: () => ({})
}
},
data() {
return {
items: [],
loading: true
}
},
async mounted() {
await this.optionAndGenFields()
this.loading = false
},
methods: {
async optionAndGenFields() {
const data = await this.$store.dispatch('common/getUrlMeta', { url: this.url })
const remoteMeta = data.actions['GET'] || {}
let fields = this.fields
fields = fields || Object.keys(remoteMeta)
const defaultExcludes = ['org_id']
const excludes = (this.excludes || []).concat(defaultExcludes)
fields = fields.filter(item => !excludes.includes(item))
for (const name of fields) {
if (typeof name === 'object') {
this.items.push(name)
continue
}
const fieldMeta = remoteMeta[name]
if (!fieldMeta) {
continue
}
if (fieldMeta['write_only']) {
continue
}
let value = this.object[name]
const label = fieldMeta.label
if (Array.isArray(value)) {
if (typeof value[0] === 'object') {
value.forEach(item => {
const fieldName = `${name}.${item.name}`
if (excludes.includes(fieldName)) {
return
}
this.items.push({
key: item.label,
value: item.value
})
})
} else if (typeof value[0] === 'string') {
value.forEach((item, index) => {
let data = {}
if (index === 0) {
data = {
key: label,
value: value[index]
}
} else {
data = {
value: value[index]
}
}
this.items.push(data)
})
}
continue
}
if (value === null || value === '') {
value = '-'
} else if (fieldMeta.type === 'datetime') {
value = toSafeLocalDateStr(value)
} else if (fieldMeta.type === 'labeled_choice') {
value = value?.['label']
} else if (fieldMeta.type === 'related_field' || fieldMeta.type === 'nested object') {
value = value['name']
} else if (fieldMeta.type === 'm2m_related_field') {
value = value.map(item => item['name']).join(', ')
} else if (fieldMeta.type === 'boolean') {
value = value ? this.$t('common.Yes') : this.$t('common.No')
}
if (value === undefined) {
if (this.showUndefine) {
value = '-'
} else {
continue
}
}
const item = {
key: label,
value: value,
formatter: this.formatters[name]
}
this.items.push(item)
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,11 +1,22 @@
<template>
<IBox :title="title" :fa="fa">
<el-form class="content" label-position="left" label-width="25%">
<el-form-item v-for="item in items" :key="item.key" :label="item.key">
<ItemValue class="item-value" :value="item.value" v-bind="item" />
</el-form-item>
</el-form>
<slot />
<IBox :title="title" fa="fa-info-circle">
<div class="content">
<el-row v-if="this.$route.params.id" :gutter="10" class="item">
<el-col :span="6"><div :style="{ 'text-align': align }" class="item-label"><label>ID: </label></div></el-col>
<el-col :span="18"><div class="item-text">{{ this.$route.params.id }}</div></el-col>
</el-row>
<el-row v-for="item in items" :key="'card-' + item.key" :gutter="10" class="item">
<el-col :span="6">
<div :style="{ 'text-align': align }" class="item-label"><label>{{ item.key }}: </label></div>
</el-col>
<el-col :span="18">
<div class="item-text">
<ItemValue :value="item.value" v-bind="item" />
</div>
</el-col>
</el-row>
<slot />
</div>
</IBox>
</template>
@@ -23,10 +34,6 @@ export default {
return this.$t('common.BasicInfo')
}
},
fa: {
type: String,
default: 'fa-info-circle'
},
items: {
type: Array,
default: () => []
@@ -39,42 +46,7 @@ export default {
}
</script>
<style lang="scss" scoped>
.el-card__body {
padding: 20px 40px;
}
.el-form-item {
border-bottom: 1px dashed #EBEEF5;
padding: 1px 0;
margin-bottom: 0;
&:last-child {
border-bottom: none;
}
&:hover {
}
>>> .el-form-item__label {
padding-right: 8%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
>>> .el-form-item__content {
font-size: 13px;
}
>>> .el-tag--mini {
margin-right: 3px;
}
}
.item-value span {
word-break: break-word;
}
<style scoped>
.content {
font-size: 13px;
line-height: 2.5;

View File

@@ -1,97 +0,0 @@
<template>
<Dialog
v-if="detailVisible"
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="detailVisible"
>
<div>
<div v-if="isEmpty()" style="text-align: center">
{{ this.$tc('common.NoContent') }}
</div>
<div v-else>
<el-table
:data="diff"
height="500"
style="width: 100%"
>
<el-table-column
:label="$tc('audits.ChangeField')"
:prop="fieldName"
show-overflow-tooltip
width="100"
/>
<el-table-column
:label="$tc('audits.BeforeChange')"
:prop="leftKeyName"
show-overflow-tooltip
/>
<el-table-column
:label="$tc('audits.AfterChange')"
:prop="rightKeyName"
show-overflow-tooltip
/>
</el-table>
</div>
</div>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog/index'
export default {
name: 'DiffDetail',
components: {
Dialog
},
props: {
title: {
type: String,
default: () => ''
},
fieldName: {
type: String,
default: () => 'field'
},
leftKeyName: {
type: String,
default: () => 'before'
},
rightKeyName: {
type: String,
default: () => 'after'
}
},
data() {
return {
diff: '',
detailVisible: false
}
},
methods: {
isEmpty() {
const content = this.diff
return !content || JSON.stringify(content) === '{}'
},
show(data) {
this.diff = data
this.detailVisible = true
}
}
}
</script>
<style scoped>
.el-tag {
width: 100%;
white-space: normal;
height: auto;
}
.el-table::before {
background-color: inherit;
}
</style>

View File

@@ -4,18 +4,16 @@
:top="top"
:width="iWidth"
class="dialog"
:append-to-body="true"
:modal-append-to-body="true"
v-bind="$attrs"
:append-to-body="false"
:modal-append-to-body="false"
v-on="$listeners"
>
<slot />
<div slot="footer" class="dialog-footer">
<slot name="footer">
<el-button v-if="showCancel" @click="onCancel">{{ cancelTitle }}</el-button>
<el-button v-if="showConfirm" type="primary" :loading="loadingStatus" @click="onConfirm">
{{ confirmTitle }}
</el-button>
<el-button v-if="showCancel" size="small" @click="onCancel">{{ cancelTitle }}</el-button>
<el-button v-if="showConfirm" type="primary" size="small" :loading="loadingStatus" @click="onConfirm">{{ confirmTitle }}</el-button>
</slot>
</div>
</el-dialog>
@@ -60,14 +58,11 @@ export default {
default() {
return this.$t('common.Confirm')
}
},
maxWidth: {
type: String,
default: '1200px'
}
},
data() {
return {}
return {
}
},
computed: {
iWidth() {
@@ -86,30 +81,12 @@ export default {
</script>
<style lang="scss" scoped>
.dialog >>> .el-dialog {
border-radius: 0.3em;
max-width: 1500px;
&__header {
box-sizing: border-box;
padding: 15px 22px;
border-bottom: 1px solid #dee2e6;
font-weight: 400;
}
&__body {
padding: 20px 30px;
}
&__footer {
border-top: 1px solid #dee2e6;
padding: 16px;
justify-content: flex-end;
}
.dialog >>> .el-dialog__header {
/*padding-top: 10px;*/
}
.dialog-footer >>> button.el-button {
font-size: 13px;
padding: 10px 20px;
.dialog-footer {
padding-right: 20px;
}
</style>

View File

@@ -1,77 +0,0 @@
<template>
<el-tree
:data="iTree"
show-checkbox
node-key="value"
:default-expand-all="true"
:default-expanded-keys="iValue"
:default-checked-keys="iValue"
:props="defaultProps"
@check="handleCheckChange"
/>
</template>
<script>
export default {
props: {
value: {
type: Array,
default: () => []
},
tree: {
type: Array,
default: () => []
},
readonly: {
type: Boolean,
default: false
}
},
data() {
return {
defaultProps: {
children: 'children',
label: 'label'
}
}
},
computed: {
iValue() {
return this.value.map(item => {
if (item.value) {
return item.value
}
return item
})
},
iTree() {
if (!this.readonly) {
return this.tree
} else {
return this.setTreeReadonly(this.tree)
}
}
},
methods: {
handleCheckChange(node, { checkedNodes }) {
const checkedKeys = checkedNodes
.filter(item => !item.children)
.map(node => node.value)
this.$emit('input', checkedKeys)
},
setTreeReadonly(tree) {
return tree.map(item => {
item.disabled = true
if (item.children) {
item.children = this.setTreeReadonly(item.children)
}
return item
})
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,282 +0,0 @@
<template>
<div class="code-editor" style="font-size: 12px">
<div class="toolbar">
<div
v-for="(item,index) in toolbar.left"
:key="index"
style="display: inline-block; margin: 0 2px"
>
<el-tooltip :content="item.tip" :disabled="!item.tip" placement="top">
<el-button
v-if="item.type ==='button'"
:disabled="item.disabled"
:type="item.el&&item.el.type"
size="mini"
@click="item.callback()"
>
<i :class="item.icon" style="margin-right: 4px;" />{{ item.name }}
</el-button>
<el-autocomplete
v-if="item.type === 'input' && item.el.autoComplete"
v-model="item.value"
:fetch-suggestions="item.el.query"
class="inline-input"
size="mini"
@select="item.callback(item.value)"
@change="item.callback(item.value)"
/>
<div v-if="item.type==='select' && item.el && item.el.create" class="select-content">
<span class="filter-label">
{{ item.name }}:
</span>
<el-select
v-if="item.type==='select' && item.el && item.el.create"
:key="index"
v-model="item.value"
:allow-create="item.el.create || false"
:filterable="item.el.create || false"
:multiple="item.el.multiple"
:placeholder="item.name"
class="autoWidth-select"
default-first-option
size="mini"
@change="item.callback(item.value)"
>
<template slot="prefix">
{{ item.label + ':' + item.value }}
</template>
<el-option
v-for="(option,id) in item.options"
:key="id"
:label="option.label"
:title="option.value"
:value="option.value"
/>
</el-select>
</div>
<el-dropdown
v-if="item.type==='select' && (!item.el || !item.el.create) "
trigger="click"
@command="(command) => {
item.value= command
item.callback(command)
}"
>
<el-button size="mini" type="default">
<b>{{ item.name }}:</b> {{ getLabel(item.value, item.options) }} <i
class="el-icon-arrow-down el-icon--right"
/>
</el-button>
<el-dropdown-menu v-slot="dropdown">
<el-dropdown-item v-for="(option,i) in item.options" :key="i" :command="option.value">
{{ option.label }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-switch
v-if="item.type === 'switch'"
v-model="item.value"
:active-text="item.name"
:disabled="item.disabled"
@change="item.callback( item.value)"
/>
</el-tooltip>
</div>
<div class="right-side" style="float: right">
<div
v-for="(item,index) in toolbar.right"
:key="index"
style="display: inline-block"
>
<el-tooltip :content="item.tip">
<el-button
v-if="item.type ==='button'"
:disabled="item.disabled"
size="mini"
style="background-color: transparent"
type="default"
@click="item.callback()"
>
<i v-if="item.icon.startsWith('fa')" :class="'fa ' + item.icon" />
<svg-icon v-else :icon-class="item.icon" style="font-size: 14px;" />
</el-button>
</el-tooltip>
</div>
</div>
</div>
<codemirror ref="myCm" v-model="iValue" :options="iOptions" class="editor" />
</div>
</template>
<script>
import { codemirror } from 'vue-codemirror'
import 'codemirror/mode/shell/shell'
import 'codemirror/mode/powershell/powershell'
import 'codemirror/mode/python/python'
import 'codemirror/mode/yaml/yaml'
import 'codemirror/mode/ruby/ruby' // theme css
import 'codemirror/theme/base16-light.css'
import 'codemirror/theme/idea.css'
import 'codemirror/theme/mbo.css'
import 'codemirror/theme/duotone-light.css'
import 'codemirror/lib/codemirror.css'
export default {
components: {
codemirror
},
props: {
toolbar: {
type: [Array, Object],
default: () => []
},
value: {
type: [String, Object],
default: () => ''
},
options: {
type: Object,
default: () => {
return {}
}
}
},
data() {
return {}
},
computed: {
iValue: {
get() {
return this.value
},
set(val) {
this.$emit('update:value', val)
this.$emit('change', val)
}
},
iOptions() {
const defaultOptions = {
tabSize: 4,
mode: 'shell',
lineNumbers: true,
theme: 'idea',
placeholder: 'Code goes here...',
autofocus: true
}
return Object.assign(defaultOptions, this.options)
}
},
methods: {
getLabel(value, items) {
for (const item of items) {
if (item.value === value) {
return item.label
}
}
}
}
}
</script>
<style lang="scss" scoped>
.editor {
border: solid 1px #f3f3f3;
}
.toolbar {
height: 100%;
width: 100%;
line-height: 29px;
vertical-align: bottom;
display: inline-block;
padding: 3px 3px 3px 0;
margin-bottom: 5px;
}
> > > .CodeMirror pre.CodeMirror-line,
> > > .CodeMirror-linenumber.CodeMirror-gutter-elt {
line-height: 18px !important;
}
.runas-input {
height: 28px;
> > > {
.el-select {
width: 100px;
}
}
}
.right-side {
.el-button {
border: none;
padding: 2px;
font-size: 14px;
width: 26px;
height: 26px;
color: #888;
background-color: transparent;
margin-left: 2px;
}
}
.autoWidth-select {
min-width: 100px;
}
.autoWidth-select > > > .el-input__prefix {
position: relative;
left: 0px;
box-sizing: border-box;
height: 28px;
line-height: 28px;
visibility: hidden;
}
.autoWidth-select > > > input {
position: absolute;
padding-left: 0px;
border: none;
color: #606266;
background-color: #e6e6e6;
font-size: 12px;
font-weight: 470;
line-height: 27px;
}
> > > .el-select {
top: -1px;
.el-input .el-select__caret {
color: #7a7c7f;
}
}
> > > .el-button.el-button--default {
background-color: #e6e6e6;
}
.filter-label {
font-size: 12px;
font-weight: 700;
}
.select-content {
display: inline-block;
position: relative;
top: 1px;
height: 28px;
line-height: 28px;
padding-left: 15px;
font-size: 0;
border: 1px solid #DCDFE6;
border-radius: 4px;
background-color: #e6e6e6;
}
</style>

View File

@@ -2,9 +2,9 @@
<el-date-picker
v-model="value"
type="datetimerange"
:range-separator="$tc('common.To')"
:start-placeholder="$tc('common.DateStart')"
:end-placeholder="$tc('common.DateEnd')"
:range-separator="this.$t('common.To')"
:start-placeholder="this.$t('common.DateStart')"
:end-placeholder="this.$t('common.DateEnd')"
size="small"
:clearable="false"
class="datepicker"
@@ -92,26 +92,22 @@ export default {
</script>
<style lang='scss' scoped>
.datepicker {
.datepicker{
width: 233px;
& >>> .el-range__icon {
&>>> .el-range__icon {
margin-top: 2px;
margin-right: 3px;
}
& >>> .el-range-input {
&>>> .el-range-input {
width: 49%;
}
}
.el-input__inner {
.el-input__inner{
border: 1px solid #dcdee2;
border-radius: 3px;
height: 32x;
}
.el-date-editor ::v-deep .el-range-separator {
.el-date-editor ::v-deep .el-range-separator{
line-height: 28px;
}
</style>

View File

@@ -2,8 +2,8 @@
<div class="json-editor">
<JsonEditor
v-model="resultInfo"
:mode="'code'"
:show-btns="false"
:mode="'code'"
@json-change="onJsonChange"
@json-save="onJsonSave"
@has-error="onError"
@@ -18,8 +18,8 @@ export default {
components: { JsonEditor },
props: {
value: {
type: [String, Object, Array],
default: () => ({})
type: String,
default: () => ''
}
},
data() {
@@ -29,7 +29,7 @@ export default {
}
},
created() {
this.resultInfo = typeof this.value === 'string' ? JSON.parse(this.value) : this.value
this.resultInfo = JSON.parse(this.value)
},
methods: {
// 数据改变
@@ -38,14 +38,14 @@ export default {
},
// 保存
onJsonSave(value) {
this.resultInfo = typeof value === 'string' ? JSON.parse(value) : value
this.resultInfo = value
this.hasJsonFlag = true
setTimeout(() => {
this.$emit('change', this.resultInfo)
this.$emit('change', JSON.stringify(this.resultInfo))
}, 500)
},
onError: _.debounce(function(value) {
this.$message.error(this.$tc('common.FormatError'))
this.$message.error(this.$t('common.FormatError'))
}, 1100)
}
}
@@ -53,25 +53,20 @@ export default {
<style lang="scss" scoped>
@import "~@/styles/variables.scss";
.json-editor {
& > > > .jsoneditor {
&>>> .jsoneditor {
border: 1px solid #e5e6e7;
}
& > > > .jsoneditor-compact {
&>>> .jsoneditor-compact {
display: none;
}
& > > > .jsoneditor-modes {
&>>> .jsoneditor-modes {
display: none;
}
& > > > .jsoneditor-poweredBy {
&>>> .jsoneditor-poweredBy {
display: none;
}
& > > > .jsoneditor-menu {
&>>> .jsoneditor-menu {
background: var(--color-primary);
border-bottom: 1px solid var(--color-primary);
}

View File

@@ -1,105 +0,0 @@
<template>
<Select2
v-model="iValue"
:multiple="multiple"
v-bind="attrsWithoutValue"
@change="onChange"
@change-options="onChangeOptions"
/>
</template>
<script>
import Select2 from './Select2'
export default {
name: 'NestedObjectSelect2',
components: {
Select2
},
props: {
value: {
type: [Array, String, Number, Boolean, Object],
default: () => ([])
},
multiple: {
type: Boolean,
default: true
},
// 自定义label字段的name
customLabelKeyName: {
type: String,
default: 'name'
}
},
data() {
return {}
},
computed: {
attrsWithoutValue() {
const attrs = Object.assign({}, this.$attrs)
delete attrs.value
return attrs
},
iValue: {
set(val) {
const value = this.valuesToObjects(val)
this.$log.debug('set iValue', value)
this.$emit('input', value)
},
get() {
const value = this.objectsToValues(this.value)
return value
}
}
},
methods: {
onChange(val) {
val = this.valuesToObjects(val)
this.$log.debug('onChange .... ', val)
this.$emit('change', val)
},
onChangeOptions(val) {
val = this.valuesToObjects(val)
this.$log.debug('onChangeOptions', val)
this.$emit('changeOptions', val)
},
valuesToObjects(values) {
let value = values
if (!this.multiple && !Array.isArray(value)) {
value = [value]
}
value = value.map(v => {
// uuid v4
const uuid = /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i
return typeof v === 'object' ? v
: this.$attrs?.allowCreate && !uuid.test(v) ? { [this.customLabelKeyName]: v } : { pk: v }
})
if (!this.multiple) {
value = value[0]
}
return value
},
objectsToValues(objects) {
let val = objects
if (!this.multiple) {
val = [val]
}
val = val.map((v) => {
if (v && typeof v === 'object') {
return v.pk || v.id || (this.$attrs?.allowCreate ? (v?.[this.customLabelKeyName] + ':' + v?.value) : '')
} else {
return v
}
})
if (!this.multiple) {
val = val[0]
}
return val
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,181 +0,0 @@
<template>
<Dialog
:close-on-click-modal="false"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="$tc('assets.PlatformProtocolConfig') + '' + item.name"
class="setting-dialog"
v-bind="$attrs"
width="70%"
v-on="$listeners"
>
<el-alert v-if="disabled" type="success">
{{ $t('assets.InheritPlatformConfig') }}
<el-link :href="platformDetail" class="link-more" target="_blank">
{{ $t('common.View') }}
</el-link>
<i class="fa fa-external-link" />
</el-alert>
<AutoDataForm
:disabled="disabled"
:form="form"
class="data-form"
v-bind="config"
@submit="onSubmit"
/>
</Dialog>
</template>
<script>
import { AutoDataForm, Dialog } from '@/components'
export default {
name: 'ProtocolSetting',
components: {
Dialog,
AutoDataForm
},
props: {
item: {
type: Object,
default: () => ({})
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
bases: ['required', 'default'],
defaultSetting: {
sftp_enabled: true,
sftp_home: '/tmp',
username_selector: '#username',
password_selector: '#password',
submit_selector: '.btn-submit'
},
loading: true,
form: {},
platformDetail: '#/console/assets/platforms/' + this.$route.query.platform,
config: {
hasSaveContinue: false,
hasButtons: !this.disabled,
url: '',
fields: [
[this.$t('common.Basic'), [
{
id: 'required',
label: this.$t('assets.Required'),
type: 'switch',
helpText: this.$t('assets.RequiredProtocols')
},
{
id: 'default',
label: this.$t('assets.Default'),
type: 'switch',
helpText: this.$t('assets.DefaultProtocol')
}
]],
[this.$t('assets.LoginConfig'), [
{
id: 'console',
label: 'Console',
type: 'switch',
hidden: () => this.item.name !== 'rdp'
},
{
id: 'security',
label: 'Security',
hidden: () => this.item.name !== 'rdp',
type: 'radio-group',
options: [
{ label: 'Any', value: 'any' },
{ label: 'RDP', value: 'rdp' },
{ label: 'NLA', value: 'nla' },
{ label: 'TLS', value: 'tls' }
]
},
{
id: 'sftp_enabled',
label: this.$t('common.Enable') + ' SFTP',
type: 'switch',
hidden: () => this.item.name !== 'ssh'
},
{
id: 'sftp_home',
label: 'SFTP home',
type: 'input',
helpText: this.$t('assets.SFTPHelpMessage'),
hidden: (form) => this.item.name !== 'ssh' || !form['sftp_enabled']
},
{
id: 'username_selector',
label: this.$t('assets.UserNameSelector'),
type: 'input',
hidden: (form) => this.item.name !== 'http'
},
{
id: 'password_selector',
label: this.$t('assets.PasswordSelector'),
type: 'input',
hidden: (form) => this.item.name !== 'http'
},
{
id: 'submit_selector',
label: this.$t('assets.SubmitSelector'),
type: 'input',
hidden: (form) => this.item.name !== 'http'
},
{
id: 'auth_username',
label: this.$t('assets.AuthUsername'),
type: 'switch',
hidden: (form) => this.item.name !== 'redis'
}
]]
]
}
}
},
created() {
const itemSetting = this.item.setting || this.defaultSetting
for (const i of this.bases) {
if (this.item.hasOwnProperty(i)) {
itemSetting[i] = this.item[i]
}
}
this.form = itemSetting
},
methods: {
onSubmit(form) {
for (const i of this.bases) {
if (form.hasOwnProperty(i)) {
this.item[i] = form[i]
}
}
this.item.setting = form
this.$emit('update:visible', false)
}
}
}
</script>
<style lang="scss" scoped>
.data-form > > > .el-form-item.form-buttons {
padding-top: 10px;
margin-bottom: 0;
}
.setting-dialog > > > .el-dialog__body {
padding-top: 10px;
}
.link-more {
font-size: 10px;
margin-left: 10px;
border-bottom: solid 1px;
color: inherit;
}
</style>

View File

@@ -1,233 +0,0 @@
<template>
<div :class="showSetting ? 'show-setting' : 'hide-setting'">
<div v-for="(item, index) in items" :key="item.name" class="protocol-item">
<el-input
v-model="item.port"
:class="readonly ? '' : 'input-with-select'"
:placeholder="portPlaceholder"
:readonly="readonly"
v-bind="$attrs"
>
<el-select
slot="prepend"
v-model="item.name"
:disabled="cannotDelete(item)"
class="prepend"
@change="handleProtocolChange($event, item)"
>
<el-option v-for="p of remainProtocols" :key="p.name" :label="p.name" :value="p.name" />
</el-select>
<el-button
v-if="showSetting(item)"
slot="append"
icon="el-icon-setting"
@click="onSettingClick(item)"
/>
</el-input>
<div v-if="!readonly" class="input-button" style="display: flex; margin-left: 20px">
<el-button
:disabled="cannotDelete(item)"
icon="el-icon-minus"
size="mini"
style="flex-shrink: 0;"
type="danger"
@click="handleDelete(index)"
/>
<el-button
v-if="index === items.length - 1"
:disabled="remainProtocols.length === 0 || !item.port"
icon="el-icon-plus"
size="mini"
style="flex-shrink: 0;"
type="primary"
@click="handleAdd(index)"
/>
</div>
</div>
<ProtocolSettingDialog
v-if="showDialog"
:disabled="settingReadonly || readonly"
:item="settingItem"
:visible.sync="showDialog"
/>
</div>
</template>
<script>
import ProtocolSettingDialog from './ProtocolSettingDialog'
export default {
components: {
ProtocolSettingDialog
},
props: {
value: {
type: [String, Array],
default: () => []
},
title: {
type: String,
default: ''
},
choices: {
type: Array,
default: () => ([])
},
readonly: {
type: Boolean,
default: false
},
settingReadonly: {
type: Boolean,
default: false
},
showSetting: {
type: Function,
default: (item) => true
}
},
data() {
return {
name: '',
items: [],
settingItem: {},
showDialog: false
}
},
computed: {
selectedProtocolNames() {
return this.items.map(item => item.name)
},
remainProtocols() {
return this.choices.filter(proto => {
return this.selectedProtocolNames.indexOf(proto.name) === -1
})
},
portPlaceholder() {
if (this.settingReadonly) {
return this.$t('applications.port')
} else {
return this.$t('assets.DefaultPort')
}
},
iChoices() {
return this.choices.map(item => {
delete item?.id
return item
})
}
},
watch: {
iChoices: {
handler(value) {
this.setDefaultItems(value)
}
},
items: {
handler(value) {
this.$emit('input', value)
},
immediate: true,
deep: true
}
},
mounted() {
this.setDefaultItems(this.iChoices)
this.$log.debug('Choices: ', this.choices)
this.$log.debug('Value: ', this.value)
},
methods: {
handleDelete(index) {
this.items = this.items.filter((value, i) => {
return i !== index
})
},
cannotDelete(item) {
const full = this.iChoices.find(choice => {
return choice.name === item.name
})
return full?.primary || full?.required
},
handleAdd(index) {
this.items.push({ ...this.remainProtocols[0] })
},
handleProtocolChange(evt, item) {
const selected = this.choices.find(item => item.name === evt)
item.name = selected.name
item.port = selected.port
},
setDefaultItems(choices) {
if (this.value instanceof Array && this.value.length > 0) {
const protocols = []
this.value.forEach(item => {
// 有默认值的情况下设置为只读或者有id、有setting是平台
if (!this.settingReadonly || (item?.id && item?.setting)) {
protocols.push(item)
} else {
// 获取资产协议配置
const assetDefaultItems = this.getAssetDefaultItems(item, choices)
protocols.push(...assetDefaultItems)
}
})
this.items = protocols
} else {
const defaults = choices.filter(item => (item.required || item.primary || item.default))
this.items = defaults
}
},
getAssetDefaultItems(item, choices) {
const protocols = []
const protocol = choices.find(i => i.name === item.name) || {}
protocols.push({ ...protocol, ...item })
return protocols
},
onSettingClick(item) {
this.settingItem = item
this.showDialog = true
}
}
}
</script>
<style lang="less" scoped>
.el-select .el-input {
width: 130px;
}
.el-select {
max-width: 120px;
}
.input-with-select {
flex-shrink: 1;
width: calc(100% - 80px) !important;
}
.input-with-select .el-input-group__prepend {
background-color: #fff;
}
.el-select ::v-deep .el-input__inner {
width: 110px;
}
.protocol-item {
display: flex;
margin: 5px 0;
}
.input-button {
margin-top: 4px;
}
.input-button ::v-deep .el-button.el-button--mini {
height: 25px;
padding: 5px;
}
.el-input-group__append .el-button {
font-size: 14px;
color: #1a1a1a;
padding: 9px 20px;
}
</style>

View File

@@ -4,26 +4,26 @@
v-model="iValue"
v-loading="!initialized"
v-loadmore="loadMore"
:clearable="clearable"
:disabled="selectDisabled"
:multiple="multiple"
:options="iOptions"
:remote="remote"
:remote-method="filterOptions"
class="select2"
:multiple="multiple"
:clearable="clearable"
filterable
popper-append-to-body
class="select2"
:disabled="selectDisabled"
v-bind="$attrs"
@change="onChange"
v-on="$listeners"
@visible-change="onVisibleChange"
v-on="$listeners"
>
<el-option
v-for="item in iOptions"
:key="item.value"
:disabled="checkDisabled(item)"
:label="item.label"
:value="item.value"
:disabled="checkDisabled(item)"
/>
</el-select>
</template>
@@ -78,7 +78,7 @@ export default {
},
// 初始化值,也就是选中的值
value: {
type: [Array, String, Number, Boolean, Object],
type: [Array, String, Number, Boolean],
default() {
return this.multiple ? [] : ''
}
@@ -137,13 +137,7 @@ export default {
if (noValue && !this.initialized) {
return
}
if (val && val.constructor === Object && val.value) {
this.$emit('input', val.value)
} else if (val && val.constructor === Object && val.id) {
this.$emit('input', val.id)
} else {
this.$emit('input', val)
}
this.$emit('input', val)
},
get() {
return this.value
@@ -167,10 +161,6 @@ export default {
return { label: item.name, value: item.id }
}
const transformOption = this.ajax.transformOption || defaultTransformOption
const defaultFilterOption = (item) => {
return item
}
const filterOption = this.ajax.filterOption || defaultFilterOption
const defaultProcessResults = (data) => {
let results = []
let more = false
@@ -184,7 +174,7 @@ export default {
total = data.count
}
results = results.map(transformOption)
results = results.filter(filterOption)
results = results.filter(Boolean)
return { results: results, pagination: more, total: total }
}
const defaultAjax = {
@@ -199,21 +189,24 @@ export default {
}
},
watch: {
// url(newValue, oldValue) {
// this.$log.debug('Select url changed: ', oldValue, ' => ', newValue)
// this.iAjax.url = newValue
// this.refresh()
// },
iAjax(newValue, oldValue) {
this.$log.debug('Select url changed: ', oldValue, ' => ', newValue)
this.refresh()
},
value: {
handler(newValue, oldValue) {
},
deep: true
value(iNew) {
this.iValue = iNew
}
},
async mounted() {
// this.$log.debug('Select2 url is: ', this.iAjax.url)
if (!this.initialized) {
await this.initialSelect()
setTimeout(() => {
this.$log.debug('Value is : ', this.value)
this.iValue = this.value
this.initialized = true
})
@@ -326,11 +319,13 @@ export default {
addOption(option) {
this.iOptions.push(option)
},
getOptionsByValues(values) {
return this.iOptions.filter((v) => {
return values.indexOf(v.value) !== -1
})
},
getSelectedOptions() {
let values = this.iValue
if (!Array.isArray(values)) {
values = [values]
}
const values = this.iValue
return this.iOptions.filter((v) => {
return values.indexOf(v.value) !== -1
})
@@ -343,9 +338,9 @@ export default {
},
onChange(values) {
const options = this.getSelectedOptions()
this.$log.debug('Current select options: ', options, 'Val: ', this.value)
this.$log.debug('Current select options: ', options)
this.$emit('changeOptions', options)
// this.$emit('change', options) // 事件重复
this.$emit('change', options)
},
onVisibleChange(visible) {
if (!visible && this.params.search) {
@@ -359,12 +354,11 @@ export default {
</script>
<style scoped>
.select2 {
width: 100%;
}
.select2 >>> .el-tag.el-tag--info {
height: auto;
white-space: normal;
}
.select2 {
width: 100%;
}
.select2 >>> .el-tag.el-tag--info {
height: auto;
white-space: normal;
}
</style>

View File

@@ -10,7 +10,7 @@
<script>
export default {
name: 'Switcher', // Switch js
name: 'Switcher',
props: {
type: {
type: String,

View File

@@ -1,122 +0,0 @@
<template>
<div class="filter-field">
<el-tag
v-for="(v, k) in filterTags"
:key="k"
:disable-transitions="true"
:type="tagType"
closable
size="small"
@click="handleTagClick(v,k)"
@close="handleTagClose(v)"
>
{{ v }}
</el-tag>
<component
:is="component"
ref="SearchInput"
v-model.trim="filterValue"
:fetch-suggestions="autocomplete"
:placeholder="this.$t('common.EnterToContinue')"
class="search-input"
@blur="focus = false"
@change="handleConfirm"
@focus="focus = true"
@select="handleSelect"
@keyup.enter.native="handleConfirm"
/>
</div>
</template>
<script>
import i18n from '@/i18n/i18n'
export default {
props: {
value: {
type: Array,
default: () => []
},
tagType: {
type: String,
default: 'info'
},
placeholder: {
type: String,
default: () => i18n.t('perms.Input')
},
autocomplete: {
type: Function,
default: null
}
},
data() {
return {
filterTags: this.value,
focus: false,
filterValue: '',
component: this.autocomplete ? 'el-autocomplete' : 'el-input'
}
},
methods: {
handleTagClose(tag) {
this.filterTags.splice(this.filterTags.indexOf(tag), 1)
this.$emit('change', this.filterTags)
},
handleSelect(item) {
this.filterValue = item.value
this.handleConfirm()
},
handleConfirm() {
if (this.filterValue === '') return
if (!this.filterTags.includes(this.filterValue)) {
this.filterTags.push(this.filterValue)
this.filterValue = ''
this.$emit('change', this.filterTags)
}
},
handleTagClick(v, k) {
if (this.filterValue.length !== 0) {
this.handleConfirm()
}
this.$delete(this.filterTags, k)
this.filterValue = v
this.$refs.SearchInput.focus()
}
}
}
</script>
<style lang="scss" scoped>
.el-tag + .el-tag {
margin-left: 4px;
}
.filter-field {
display: flex;
align-items: center;
padding-left: 2px;
border: 1px solid #dcdee2;
border-radius: 1px;
background-color: #fff;
&:hover {
border-color: #C0C4CC;
}
}
.search-input > > > .el-input__inner {
max-width: 100%;
border: none;
padding-left: 5px;
}
.el-input > > > .el-input__inner {
border: none !important;
font-size: 13px;
}
.filter-field > > > .el-input__inner {
//height: 32px;
}
</style>

View File

@@ -1,93 +0,0 @@
<template>
<div>
<el-button
v-show="!isShow"
type="text"
class="button-text"
:disabled="disabled"
@click="isShow=true"
>
{{ iLabel }}
<svg-icon class-name="icon" icon-class="switch" />
</el-button>
<Select2
v-show="isShow"
ref="select2"
v-model="iValue"
:disabled="disabled"
v-bind="$attrs"
@change="onSelectChange"
v-on="$listeners"
/>
</div>
</template>
<script>
import Select2 from './Select2'
import { hasUUID } from '@/utils/common'
export default {
components: {
Select2
},
props: {
value: {
type: String,
default: () => ''
},
label: {
type: String,
default: () => ''
},
showSelect: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
isShow: this.showSelect,
iLabel: this.label
}
},
computed: {
iValue: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
created() {
const { path } = this.$route
if (hasUUID(path) && this.value) {
this.isShow = false
}
},
methods: {
onSelectChange(val) {
const options = this.$refs.select2.options.filter(item => item.value === val)
const label = options.length > 0 ? options[0].label : ''
this.isShow = false
this.iLabel = val ? label : '-'
}
}
}
</script>
<style scoped>
.button-text {
color: #676a6c;
padding-left: 0!important;
padding-right: 0!important;
}
.icon {
color: #676a6c!important;
}
</style>

View File

@@ -32,10 +32,6 @@ export default {
return this.$t('common.Update')
}
},
showInput: {
type: Boolean,
default: true
},
placeholder: {
type: String,
default: () => ''
@@ -43,13 +39,13 @@ export default {
},
data() {
return {
isShow: this.showInput,
isShow: false,
curValue: this.value
}
},
created() {
if (this.$route.path.indexOf('/update') !== -1) {
this.isShow = false
if (this.$route.path.indexOf('/create') !== -1) {
this.isShow = true
}
},
methods: {

View File

@@ -1,48 +1,42 @@
import Link from './Link'
import Text from './Text'
import Select2 from './Select2'
import TagInput from './TagInput'
import Switcher from './Switcher'
import UploadKey from './UploadKey'
import JsonEditor from './JsonEditor'
import UploadField from './UploadField'
import UpdateToken from './UpdateToken'
import UserPassword from './UserPassword'
import PasswordInput from './PasswordInput'
import WeekCronSelect from './WeekCronSelect'
import NestedObjectSelect2 from './NestedObjectSelect2'
import DatetimeRangePicker from './DatetimeRangePicker'
import Link from './Link'
import PasswordInput from './PasswordInput'
import Select2 from './Select2'
import Swicher from './Swicher'
import UploadField from './UploadField'
import UploadKey from './UploadKey'
import UserPassword from './UserPassword'
import WeekCronSelect from './WeekCronSelect'
import UpdateToken from './UpdateToken'
import JsonEditor from './JsonEditor'
import Text from './Text'
export default {
Text,
DatetimeRangePicker,
Link,
Switcher,
PasswordInput,
Select2,
TagInput,
Swicher,
UploadKey,
JsonEditor,
UpdateToken,
UploadField,
UserPassword,
PasswordInput,
WeekCronSelect,
NestedObjectSelect2,
DatetimeRangePicker
UpdateToken,
JsonEditor,
Text
}
export {
Text,
DatetimeRangePicker,
Link,
Switcher,
PasswordInput,
Select2,
TagInput,
Swicher,
UploadKey,
JsonEditor,
UpdateToken,
UploadField,
UserPassword,
PasswordInput,
WeekCronSelect,
NestedObjectSelect2,
DatetimeRangePicker
UpdateToken,
JsonEditor,
Text
}

View File

@@ -1,7 +1,7 @@
<template>
<div class="form-group-header">
<div v-if="line" class="hr-line-dashed" />
<h3>{{ group['title'] }} </h3>
<h3>{{ group.title }}</h3>
</div>
</template>

View File

@@ -1,92 +0,0 @@
<template>
<Dialog
v-if="iVisible"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="$tc('assets.TestGatewayTestConnection')"
:visible.sync="iVisible"
top="35vh"
width="40%"
>
<el-row :gutter="20">
<el-col :md="4" :sm="24">
<div style="line-height: 34px">{{ $t('assets.SSHPort') }}</div>
</el-col>
<el-col :md="14" :sm="24">
<el-input v-model="port" />
<span class="help-tips help-block">{{ $t('assets.TestGatewayHelpMessage') }}</span>
</el-col>
<el-col :md="4" :sm="24">
<el-button
:loading="loading"
size="mini"
style="line-height:20px "
type="primary"
@click="dialogConfirm"
>
{{ this.$t('common.Confirm') }}
</el-button>
</el-col>
</el-row>
</Dialog>
</template>
<script>
import Dialog from '@/components/Dialog'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'GatewayDialog',
components: {
Dialog
},
props: {
visible: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
},
port: {
type: Number,
default: 0
},
cell: {
type: String,
default: ''
}
},
data() {
return {}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
dialogConfirm() {
if (isNaN(this.port)) {
return this.$message.error(this.$tc('common.TestPortErrorMsg'))
}
this.$axios.post(
`/api/v1/assets/gateways/${this.cell}/test-connective/`,
{ port: this.port }
)
.then((res) => {
openTaskPage(res['task'])
}).finally(() => {
this.iVisible = false
})
}
}
}
</script>

View File

@@ -1,10 +1,10 @@
<template>
<TreeTable :header-actions="headerActions" :table-config="tableConfig" :tree-setting="treeSetting" />
<TreeTable :table-config="tableConfig" :header-actions="headerActions" :tree-setting="treeSetting" />
</template>
<script type="text/jsx">
import { DetailFormatter, SystemUserFormatter } from '@/components/TableFormatters'
import TreeTable from '../TreeTable'
import { DetailFormatter } from '@/components/TableFormatters'
export default {
name: 'GrantedAssets',
@@ -35,7 +35,7 @@ export default {
getShowUrl: {
type: Function,
default({ row, col }) {
return this.tableUrl.replace('/assets/', `/assets/${row.id}/accounts/`)
return this.tableUrl.replace('/assets/', `/assets/${row.id}/system-users/?cache_policy=1`)
}
}
},
@@ -57,21 +57,34 @@ export default {
tableConfig: {
url: this.tableUrl,
hasTree: true,
columnsExclude: ['spec_info'],
columnShow: {
min: ['name', 'address', 'accounts']
},
columnsMeta: {
name: {
columns: [
{
prop: 'hostname',
label: this.$t('assets.Hostname'),
formatter: DetailFormatter,
sortable: true,
formatterArgs: {
route: 'AssetDetail'
}
},
showOverflowTooltip: true
},
actions: {
has: false
{
prop: 'ip',
label: this.$t('assets.IP'),
width: '140px',
sortable: 'custom'
},
{
prop: 'systemUsers',
label: this.$t('assets.SystemUsers'),
align: 'center',
formatter: SystemUserFormatter,
formatterArgs: {
getUrl: this.getShowUrl.bind(this)
},
showOverflowTooltip: true
}
}
]
},
headerActions: {
hasLeftActions: false,

View File

@@ -1,6 +1,6 @@
<template>
<div style="padding: 0 20px;" @click="toggleClick">
<svg-icon icon-class="direction-left" class="hamburger" :class="{'is-active':isActive}" />
<div style="padding: 0 15px;" @click="toggleClick">
<svg-icon icon-class="hamburger" class="hamburger" :class="{'is-active':isActive}" />
</div>
</template>
@@ -26,10 +26,10 @@ export default {
.hamburger {
display: inline-block;
vertical-align: middle;
font-size: 16px;
font-size: 20px;
color: $menuText;
}
.hamburger.is-active {
transform: rotate(-180deg);
transform: rotate(180deg);
}
</style>

View File

@@ -3,7 +3,7 @@
<template #header>
<slot name="header">
<div v-if="title" slot="header" class="clearfix ibox-title">
<i v-if="fa" :class="'fa ' + fa" /> <h5>{{ title }}</h5>
<i v-if="fa" :class="'fa ' + fa" /> {{ title }}
</div>
</slot>
</template>
@@ -41,6 +41,7 @@ export default {
/*height: 100%;*/
clear: both;
padding: 0;
background-color: #ffffff;
}
.ibox >>> .el-card__header {
@@ -55,11 +56,11 @@ export default {
.ibox-title h5 {
display: inline-block;
font-size: 13px;
font-size: 14px;
margin: 0;
padding: 0;
text-overflow: ellipsis;
font-weight: 500;
float: left;
}
.ibox-tools a {
@@ -82,7 +83,8 @@ export default {
}
.ibox >>> .el-card__body {
padding: 15px 30px 20px 30px;
background-color: #ffffff;
padding: 15px 20px 20px 20px;
color: inherit;
}
</style>

View File

@@ -1,42 +1,28 @@
<template>
<div>
<div v-if="mfaDialogShow">
<UserConfirmDialog
:url="url"
@UserConfirmDone="showExportDialog"
@UserConfirmCancel="handleExportCancel"
@AuthMFAError="handleAuthMFAError"
/>
</div>
<UserConfirmDialog
v-if="mfaDialogShow"
:url="url"
@UserConfirmDone="showExportDialog"
@UserConfirmCancel="handleExportCancel"
/>
<Dialog
v-if="exportDialogShow"
:title="$tc('common.Export')"
:title="$t('common.Export')"
:visible.sync="exportDialogShow"
:destroy-on-close="true"
@confirm="handleExportConfirm()"
@cancel="handleExportCancel()"
>
<el-form label-position="left" style="padding-left: 20px">
<el-form-item :label="$tc('common.fileType' )" :label-width="'100px'">
<el-form label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
<el-radio-group v-model="exportTypeOption">
<el-radio
v-for="option of exportTypeOptions"
:key="option.value"
style="padding: 10px 20px;"
:label="option.value"
:disabled="!option.can"
>{{ option.label }}</el-radio>
<el-radio v-for="option of exportTypeOptions" :key="option.value" style="padding: 10px 20px;" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item class="export-form" :label="$tc('common.imExport.ExportRange')" :label-width="'100px'">
<el-form-item class="export-form" :label="this.$t('common.imExport.ExportRange')" :label-width="'100px'">
<el-radio-group v-model="exportOption">
<el-radio
v-for="option of exportOptions"
:key="option.value"
class="export-item"
:label="option.value"
:disabled="!option.can"
>{{ option.label }}</el-radio>
<el-radio v-for="option of exportOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
@@ -67,8 +53,7 @@ export default {
},
beforeExport: {
type: Function,
default: () => {
}
default: () => {}
},
mfaVerifyRequired: {
type: Boolean,
@@ -197,8 +182,8 @@ export default {
}
query['format'] = exportTypeOption
const queryStr =
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
(url.indexOf('?') > -1 ? '&' : '?') +
queryUtil.stringify(query, '=', '&')
return this.downloadCsv(url + queryStr)
},
async handleExport() {
@@ -220,9 +205,6 @@ export default {
vm.exportDialogShow = false
vm.mfaDialogShow = false
}, 100)
},
handleAuthMFAError() {
this.mfaDialogShow = false
}
}
}

View File

@@ -5,14 +5,14 @@
:destroy-on-close="true"
:close-on-click-modal="false"
:loading-status="loadStatus"
width="60%"
width="80%"
class="importDialog"
:show-cancel="false"
:show-confirm="false"
@close="handleImportCancel"
>
<el-form v-if="!showTable" label-position="left" style="padding-left: 20px">
<el-form-item :label="$tc('common.Import' )" :label-width="'100px'">
<el-form v-if="!showTable" label-position="left" style="padding-left: 50px">
<el-form-item :label="$t('common.Import' )" :label-width="'100px'">
<el-radio v-if="canImportCreate" v-model="importOption" class="export-item" label="create">
{{ this.$t('common.Create') }}
</el-radio>
@@ -22,12 +22,12 @@
<div style="line-height: 1.5">
<span class="el-upload__tip">
{{ downloadTemplateTitle }}
<el-link type="success" @click="downloadTemplateFile('csv')"> CSV </el-link>
<el-link type="success" @click="downloadTemplateFile('xlsx')"> XLSX </el-link>
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('csv')"> CSV </el-link>
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('xlsx')"> XLSX </el-link>
</span>
</div>
</el-form-item>
<el-form-item :label="$tc('common.Upload' )" :label-width="'100px'" class="file-uploader">
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'" class="file-uploader">
<el-upload
ref="upload"
drag
@@ -40,13 +40,9 @@
accept=".csv,.xlsx"
>
<i class="el-icon-upload" />
<div class="el-upload__text">
{{ $t('common.imExport.dragUploadFileInfo') }}
</div>
<div class="el-upload__text">{{ $t('common.imExport.dragUploadFileInfo') }}</div>
<div slot="tip" class="el-upload__tip">
<span :class="{'hasError': hasFileFormatOrSizeError }">
{{ $t('common.imExport.uploadCsvLth10MHelpText') }}
</span>
<span :class="{'hasError': hasFileFormatOrSizeError }">{{ $t('common.imExport.uploadCsvLth10MHelpText') }}</span>
<div v-if="renderError" class="hasError">{{ renderError }}</div>
</div>
</el-upload>
@@ -91,7 +87,7 @@ export default {
default: false
},
canImportUpdate: {
type: [Boolean, Function],
type: Boolean,
default: false
}
},
@@ -172,10 +168,7 @@ export default {
this.$axios.post(
renderToJsonUrl,
file.raw,
{
headers: { 'Content-Type': isCsv ? 'text/csv' : 'text/xlsx' },
disableFlashErrorMsg: true
}
{ headers: { 'Content-Type': isCsv ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
).then(data => {
this.jsonData = data
this.showTable = true
@@ -212,8 +205,9 @@ export default {
}
return this.url.indexOf('?') === -1 ? `${this.url}?${query}` : `${this.url}&${query}`
},
catchError(err) {
console.log(err)
// eslint-disable-next-line handle-callback-err
catchError(error) {
// debug(error)
},
onSuccess(msg) {
this.errorMsg = ''
@@ -226,7 +220,7 @@ export default {
window.URL.revokeObjectURL(url)
},
async handleImportConfirm() {
await this.$refs['importTable'].performUpload()
this.$refs['importTable'].performUpload()
},
handleImportCancel() {
this.showImportDialog = false
@@ -249,6 +243,10 @@ export default {
overflow: auto
}
.importDialog >>> .el-form-item.file-uploader {
padding-right: 150px;
}
.file-uploader >>> .el-upload {
width: 100%;
//padding-right: 150px;
@@ -289,9 +287,5 @@ export default {
.el-upload__tip {
line-height: 1.5;
padding-top: 0;
.el-link {
margin-left: 10px;
}
}
</style>

View File

@@ -35,8 +35,6 @@
import DataTable from '@/components/DataTable'
import { sleep, getUpdateObjURL } from '@/utils/common'
import { EditableInputFormatter, StatusFormatter } from '@/components/TableFormatters'
import { encryptPassword } from '@/utils/crypto'
export default {
name: 'ImportTable',
components: {
@@ -178,7 +176,7 @@ export default {
align: 'center',
formatter: StatusFormatter,
formatterArgs: {
faChoices: {
iconChoices: {
ok: 'fa-check text-primary',
error: 'fa-times text-danger',
pending: 'fa-clock-o'
@@ -227,6 +225,7 @@ export default {
prop: item[1],
label: item[0],
minWidth: colMaxWidth + 'px',
showOverflowTooltip: true,
formatter: EditableInputFormatter,
formatterArgs: {
onEnter: ({ row, col, oldValue, newValue }) => {
@@ -244,12 +243,6 @@ export default {
const totalData = []
tableData.forEach(item => {
this.$set(item, '@status', 'pending')
const encryptFields = ['password', 'secret', 'private_key']
for (const field of encryptFields) {
if (item[field]) {
item[field] = encryptPassword(item[field])
}
}
totalData.push(item)
})
return totalData
@@ -356,7 +349,7 @@ export default {
this.importTaskStatus = 'done'
}
if (this.failedCount > 0) {
this.$message.error(this.$tc('common.imExport.hasImportErrorItemMsg') + '')
this.$message.error(this.$t('common.imExport.hasImportErrorItemMsg') + '')
}
},
async performUpdateObject(item) {
@@ -432,7 +425,6 @@ export default {
min-height: 20px;
height: 100%;
overflow: auto;
max-height: 160px;
}
</style>

View File

@@ -2,8 +2,8 @@
<DataActions
v-if="hasLeftActions"
:actions="iActions"
class="header-action"
v-bind="$attrs"
class="header-action"
/>
</template>
@@ -26,14 +26,10 @@ export default {
canCreate: defaultTrue,
createRoute: {
type: [String, Object, Function],
default() {
default: function() {
return this.$route.name?.replace('List', 'Create')
}
},
onCreate: {
type: Function,
default: null
},
createInNewPage: {
type: Boolean,
default: false
@@ -42,11 +38,6 @@ export default {
canBulkDelete: defaultTrue,
hasBulkUpdate: defaultFalse,
canBulkUpdate: defaultTrue,
handleBulkUpdate: {
type: Function,
default: () => {
}
},
hasMoreActions: defaultTrue,
tableUrl: {
type: String,
@@ -54,8 +45,7 @@ export default {
},
reloadTable: {
type: Function,
default: () => {
}
default: () => {}
},
performBulkDelete: {
type: Function,
@@ -87,14 +77,39 @@ export default {
}
},
data() {
const defaultActions = [
{
name: 'actionCreate',
title: this.createTitle,
type: 'primary',
has: this.hasCreate && !this.moreCreates,
can: this.canCreate,
callback: this.handleCreate
}
]
if (this.moreCreates) {
const defaultMoreCreate = {
name: 'actionMoreCreate',
title: this.createTitle,
type: 'primary',
has: true,
can: this.canCreate,
dropdown: [],
callback: this.handleCreate
}
const createCreateAction = Object.assign(defaultMoreCreate, this.moreCreates)
defaultActions.push(createCreateAction)
}
const vm = this
return {
defaultActions: defaultActions,
defaultMoreActions: [
{
title: this.$t('common.deleteSelected'),
name: 'actionDeleteSelected',
has: this.hasBulkDelete,
can({ selectedRows }) {
// vm.$log.debug('Delete select rows length: ', selectedRows.length)
return selectedRows.length > 0 && vm.canBulkDelete
},
callback: this.defaultBulkDeleteCallback
@@ -103,12 +118,8 @@ export default {
title: this.$t('common.updateSelected'),
name: 'actionUpdateSelected',
has: this.hasBulkUpdate,
can: function({ selectedRows }) {
let canBulkUpdate = vm.canBulkUpdate
if (typeof canBulkUpdate === 'function') {
canBulkUpdate = canBulkUpdate({ selectedRows })
}
return selectedRows.length > 0 && canBulkUpdate
can: ({ selectedRows }) => {
return selectedRows.length > 0 && vm.canBulkUpdate
},
callback: this.handleBulkUpdate
}
@@ -116,32 +127,6 @@ export default {
}
},
computed: {
defaultActions() {
const defaultActions = [
{
name: 'actionCreate',
title: this.createTitle,
type: 'primary',
has: this.hasCreate && !this.moreCreates,
can: this.canCreate,
callback: this.onCreate || this.handleCreate
}
]
if (this.moreCreates) {
const defaultMoreCreate = {
name: 'actionMoreCreate',
title: this.createTitle,
type: 'primary',
has: true,
can: this.canCreate,
dropdown: [],
callback: this.onCreate || this.handleCreate
}
const createCreateAction = Object.assign(defaultMoreCreate, this.moreCreates)
defaultActions.push(createCreateAction)
}
return defaultActions
},
iActions() {
return [...this.actions, this.moreAction]
},
@@ -192,7 +177,7 @@ export default {
},
defaultBulkDeleteCallback({ selectedRows, reloadTable }) {
const msg = this.$t('common.deleteWarningMsg') + ' ' + selectedRows.length + ' ' + this.$t('common.rows') + ' ?'
const title = this.$tc('common.Info')
const title = this.$t('common.Info')
const performDelete = this.performBulkDelete || this.defaultPerformBulkDelete
this.$alert(msg, title, {
type: 'warning',
@@ -205,9 +190,9 @@ export default {
await performDelete(selectedRows)
done()
reloadTable()
this.$message.success(this.$tc('common.bulkDeleteSuccessMsg'))
this.$message.success(this.$t('common.bulkDeleteSuccessMsg'))
} catch (error) {
this.$message.error(this.$tc('common.bulkDeleteErrorMsg') + error)
this.$message.error(this.$t('common.bulkDeleteErrorMsg') + error)
} finally {
instance.confirmButtonLoading = false
}
@@ -223,6 +208,8 @@ export default {
const data = await createSourceIdCache(ids)
const url = (this.tableUrl.indexOf('?') === -1) ? `${this.tableUrl}?spm=` + data.spm : `${this.tableUrl}&spm=` + data.spm
return this.$axios.delete(url)
},
handleBulkUpdate({ selectedRows }) {
}
}
}

View File

@@ -82,17 +82,17 @@ export default {
default: false
},
canBulkUpdate: {
type: [Boolean, Function],
type: Boolean,
default: false
}
},
data() {
return {
defaultRightSideActions: [
{ name: 'actionColumnSetting', fa: 'system-setting', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleTableSettingClick.bind(this) },
{ name: 'actionImport', fa: 'upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImportClick.bind(this) },
{ name: 'actionExport', fa: 'download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExportClick.bind(this) },
{ name: 'actionRefresh', fa: 'refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefreshClick.bind(this) }
{ name: 'actionColumnSetting', fa: 'fa-cog', tip: this.$t('common.CustomCol'), has: this.hasColumnSetting, callback: this.handleTableSettingClick.bind(this) },
{ name: 'actionImport', fa: 'fa-upload', tip: this.$t('common.Import'), has: this.hasImport, callback: this.handleImportClick.bind(this) },
{ name: 'actionExport', fa: 'fa-download', tip: this.$t('common.Export'), has: this.hasExport, callback: this.handleExportClick.bind(this) },
{ name: 'actionRefresh', fa: 'fa-refresh', tip: this.$t('common.Refresh'), has: this.hasRefresh, callback: this.handleRefreshClick.bind(this) }
],
dialogExportVisible: false
}

View File

@@ -1,56 +1,31 @@
<template>
<div :class="device" class="table-header clearfix">
<div class="table-header clearfix" :class="device">
<slot name="header">
<LeftSide
v-if="hasLeftActions"
:selected-rows="selectedRows"
:table-url="tableUrl"
class="left-side"
v-bind="$attrs"
v-on="$listeners"
/>
<RightSide
v-if="hasRightActions"
:selected-rows="selectedRows"
:table-url="tableUrl"
class="right-side"
v-bind="$attrs"
v-on="$listeners"
/>
<div :class="searchClass" class="search">
<AutoDataSearch
v-if="hasSearch"
class="right-side-item action-search"
v-bind="iSearchTableConfig"
@tagSearch="handleTagSearch"
/>
<DatetimeRangePicker
v-if="hasDatePicker"
class="datepicker"
v-bind="datePicker"
@dateChange="handleDateChange"
/>
<LeftSide v-if="hasLeftActions" class="left-side" :selected-rows="selectedRows" :table-url="tableUrl" v-bind="$attrs" v-on="$listeners" />
<RightSide v-if="hasRightActions" class="right-side" :selected-rows="selectedRows" :table-url="tableUrl" v-bind="$attrs" v-on="$listeners" />
<div class="search" :class="searchClass">
<AutoDataSearch v-if="hasSearch" class="right-side-item action-search" v-bind="iSearchTableConfig" @tagSearch="handleTagSearch" />
<DatetimeRangePicker v-if="hasDatePicker" v-bind="datePicker" class="datepicker" @dateChange="handleDateChange" />
</div>
</slot>
</div>
</template>
<script>
import LeftSide from './LeftSide'
import RightSide from './RightSide'
import AutoDataSearch from '@/components/AutoDataSearch'
import LeftSide from './LeftSide'
import DatetimeRangePicker from '@/components/FormFields/DatetimeRangePicker'
import { getDaysAgo, getDaysFuture } from '@/utils/common'
import RightSide from './RightSide'
const defaultTrue = { type: Boolean, default: true }
const defaultFalse = { type: Boolean, default: false }
export default {
name: 'TableAction',
components: {
LeftSide,
RightSide,
AutoDataSearch,
DatetimeRangePicker
LeftSide,
DatetimeRangePicker,
RightSide
},
props: {
hasLeftActions: defaultTrue,
@@ -59,10 +34,7 @@ export default {
hasDatePicker: defaultFalse,
datePicker: {
type: Object,
default: () => ({
dateStart: getDaysAgo(7).toISOString(),
dateEnd: getDaysFuture(1).toISOString()
})
default: () => ({})
},
searchConfig: {
type: Object,
@@ -74,13 +46,11 @@ export default {
},
datePick: {
type: Function,
default: (val) => {
}
default: (val) => {}
},
searchTable: {
type: Function,
default: (val) => {
}
default: (val) => {}
},
selectedRows: {
type: Array,
@@ -165,66 +135,53 @@ export default {
display: flex;
padding-left: 10px;
align-items: center;
justify-content: center;
justify-content:center;
}
.table-action-right-side {
display: flex;
justify-content: center;
justify-content:center;
}
.export-item {
display: block;
padding: 5px 20px;
}
.datepicker {
.datepicker{
margin-left: 10px;
}
.table-header {
line-height: 32px;
}
.left-side {
float: left;
display: block;
}
.right-side {
float: right;
}
.search {
display: flex;
flex-direction: row;
}
.mobile .search {
display: inherit;
}
.mobile .search .datepicker {
margin-left: 0;
}
.search.left {
float: left;
padding: 0 !important;
}
.search.right {
float: right;
}
.mobile .search.right {
float: none;
}
.mobile .search.right .action-search {
width: 100%;
}
.mobile .right-side {
padding-top: 5px;
}

View File

@@ -1,35 +1,33 @@
<template>
<div>
<TableAction
v-if="hasActions"
:date-pick="handleDateChange"
:reload-table="reloadTable"
:search-table="search"
:selected-rows="selectedRows"
:table-url="tableUrl"
:search-table="search"
:date-pick="handleDateChange"
:selected-rows="selectedRows"
:reload-table="reloadTable"
v-bind="iHeaderActions"
/>
<IBox class="table-content">
<AutoDataTable
ref="dataTable"
:config="iTableConfig"
:filter-table="filter"
v-on="$listeners"
:config="iTableConfig"
@selection-change="handleSelectionChange"
v-on="$listeners"
/>
</IBox>
</div>
</template>
<script>
import { getResourceFromApiUrl } from '@/utils/jms'
import deepmerge from 'deepmerge'
import { mapGetters } from 'vuex'
import AutoDataTable from '../AutoDataTable'
import IBox from '../IBox'
import TableAction from './TableAction'
import Emitter from '@/mixins/emitter'
import AutoDataTable from '../AutoDataTable'
import { getDayEnd, getDaysAgo } from '@/utils/common'
import { getResourceFromApiUrl } from '@/utils/jms'
import deepmerge from 'deepmerge'
import { mapGetters } from 'vuex'
export default {
name: 'ListTable',
@@ -52,21 +50,10 @@ export default {
}
},
data() {
let extraQuery = {}
if (this.headerActions.hasDatePicker) {
extraQuery = {
date_from: getDaysAgo(7).toISOString(),
date_to: getDayEnd().toISOString()
}
this.headerActions.datePicker = Object.assign({
dateStart: extraQuery.date_from,
dateEnd: extraQuery.date_to
}, this.headerActions.datePicker)
}
return {
selectedRows: [],
init: false,
extraQuery: extraQuery
extraQuery: {}
}
},
computed: {
@@ -92,9 +79,6 @@ export default {
}
return Object.assign(defaults, this.headerActions)
},
hasActions() {
return this.iHeaderActions.has === undefined ? true : this.iHeaderActions.has
},
iTableConfig() {
const config = deepmerge(this.tableConfig, {
extraQuery: this.extraQuery
@@ -123,7 +107,7 @@ export default {
return config
},
tableUrl() {
return this.tableConfig.url || ''
return this.tableConfig.url
},
permissions() {
// 获取 permissions获取不到通过 url 解析
@@ -210,32 +194,25 @@ export default {
.table-content {
margin-top: 10px;
& > > > .el-card__body {
& >>> .el-card__body {
padding: 0;
}
& > > > .el-table__header thead > tr > th {
& >>> .el-table__header thead > tr > th {
background-color: white;
}
& > > > .el-table__row .cell {
&>>> .el-table__row .cell {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
& > > > .el-table__expanded-cell pre {
&>>> .el-table__expanded-cell pre {
max-height: 500px;
overflow-y: scroll;
}
& > > > .el-button-ungroup .el-dropdown > .more-action {
height: 24.6px;
}
}
//修改颜色
.el-button--text {
color: #409EFF;
}
// .el-button--text{
// color: #409EFF;
// }
</style>

View File

@@ -1,37 +0,0 @@
<template>
<div class="markdown-body">
<VueMarkdown :source="value" />
</div>
</template>
<script>
import VueMarkdown from 'vue-markdown'
import 'github-markdown-css/github-markdown-light.css'
export default {
components: {
VueMarkdown
},
props: {
value: {
type: String,
default: ''
}
},
data() {
return {}
}
}
</script>
<style lang='scss' scoped>
.markdown-body * {
padding: 10px;
background-color: #f3f3f3;
color: #1a1a1a;
font-size: 13px;
//& >>> .table * {
// background-color: #f3f3f3;
//}
}
</style>

View File

@@ -1,115 +0,0 @@
<template>
<div class="el-page">
<el-pagination
v-if="hasPagination"
:current-page="page"
:page-sizes="paginationSizes"
:page-size="size"
:total="total"
:background="paginationBackground"
:layout="paginationLayout"
v-bind="extraPaginationAttrs"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</template>
<script>
const defaultFirstPage = 1
export default {
name: 'Pagination',
components: {},
props: {
hasPagination: {
type: Boolean,
default: true
},
firstPage: {
type: Number,
default: defaultFirstPage
},
pageSizeKey: {
type: String,
default: 'limit'
},
pageKey: {
type: String,
default: 'offset'
},
page: {
type: Number,
default: 1
},
noPaginationSize: {
type: Number,
default: -1
},
paginationSize: {
type: Number,
default: 10
},
total: {
type: Number,
default: 0
},
paginationSizes: {
type: Array,
default: () => [10, 20, 30, 40, 50, 100]
},
paginationBackground: {
type: Boolean,
default: true
},
paginationLayout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
},
extraPaginationAttrs: {
type: Object,
default: () => {}
},
transformQuery: {
type: Function,
default: null
}
},
data() {
return {
size: this.paginationSize || this.paginationSizes[0]
}
},
methods: {
handleSizeChange(val) {
this.$emit('sizeChange', val)
},
handleCurrentChange(val) {
this.$emit('currentSizeChange', val)
},
getPageQuery(currentPage, pageSize) {
// 构造query对象
let query = {}
query[this.pageSizeKey] = this.hasPagination
? pageSize
: this.noPaginationSize
const offset = (currentPage - 1) * pageSize
query[this.pageKey] = offset
if (this.transformQuery) {
query = this.transformQuery(query)
}
return query
}
}
}
</script>
<style scoped>
>>> .el-pagination {
text-align: right;
}
>>> .el-pagination__total {
float: left;
}
</style>

View File

@@ -3,44 +3,23 @@
<td>{{ action.title }}:</td>
<td>
<span>
<component
:is="iType"
v-model="action.attrs.model"
v-bind="action.attrs"
v-on="callbacks"
>
{{ label }}
</component>
<component :is="iType" v-model="action.attrs.model" v-bind="action.attrs" v-on="callbacks">{{ label }}</component>
</span>
</td>
</tr>
</template>
<script>
import Switcher from '../FormFields/Switcher'
import Select2 from '../FormFields/Select2'
import UpdateSelect from '../FormFields/UpdateSelect'
class Action {
constructor() {
this.title = ''
this.type = ''
this.attrs = {}
this.callbacks = ''
}
}
import Switcher from '../FormFields/Swicher'
export default {
name: 'ActionItem',
components: {
Switcher,
Select2,
UpdateSelect
Switcher
},
props: {
action: {
type: [Action, Object],
default: () => ({ title: '' })
type: Object,
default: () => ({})
}
},
data() {
@@ -51,14 +30,8 @@ export default {
computed: {
iType() {
switch (this.action.type) {
case 'switch':
return 'Switcher'
case 'switcher':
return 'Switcher'
case 'select2':
return 'Select2'
case 'updateSelect':
return 'UpdateSelect'
default:
return 'el-button'
}

View File

@@ -1,5 +1,5 @@
<template>
<IBox :type="type" :title="title" v-bind="$attrs">
<IBox :fa="icon" :type="type" :title="title" v-bind="$attrs">
<table style="width: 100%;table-layout:fixed;" class="CardTable">
<tr>
<td colspan="2">

View File

@@ -1,86 +0,0 @@
<template>
<div>
<el-row :gutter="20">
<el-col :md="12" :sm="24">
<IBox :title="title" class="block" v-bind="$attrs">
<el-timeline>
<el-timeline-item
v-for="(activity, index) in activities"
:key="index"
:size="activity.size"
:timestamp="activity.timestamp"
:type="activity.type"
placement="bottom"
>
{{ activity.content }}
<el-link v-if="activity.detail_url" type="primary" @click.native="onClick(activity.r_type, activity.detail_url)">
{{ $tc('common.Detail') }}
</el-link>
</el-timeline-item>
</el-timeline>
</IBox>
</el-col>
</el-row>
<DiffDetail ref="DetailDialog" :title="$tc('route.OperateLog')" />
</div>
</template>
<script>
import IBox from '@/components/IBox'
import DiffDetail from '@/components/Dialog/DiffDetail'
import { openTaskPage } from '@/utils/jms'
export default {
name: 'ResourceActivity',
components: {
IBox,
DiffDetail
},
props: {
object: {
type: Object,
default: () => ({})
}
},
data() {
return {
activityUrl: `/api/v1/audits/activities/?resource_id=${this.object.id}`,
title: `${this.$t('common.Activity')} - ${this.$t('common.Last30')}`,
activities: [
{
content: this.$t('common.Now'),
timestamp: this.$moment().format('YYYY-MM-DD HH:mm:ss'),
type: 'primary'
}
]
}
},
mounted() {
this.getActivities()
},
methods: {
getActivities() {
this.$axios.get(this.activityUrl).then(res => {
for (const i in res) {
this.activities.push(res[i])
}
})
},
onClick(type, taskUrl) {
if (type === 'O') {
this.$axios.get(taskUrl).then(
res => {
this.$refs.DetailDialog.show(res.diff)
}
)
} else {
openTaskPage('', 'celery', taskUrl)
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -59,7 +59,4 @@ export default {
mask-size: cover!important;
display: inline-block;
}
::v-deep path {
fill: inherit!important;
}
</style>

View File

@@ -1,198 +0,0 @@
<template>
<div class="tree-tab">
<el-tabs
v-if="tabIndices.length > 0"
v-model="iActiveMenu"
:class="{ 'only-submenu': tabIndices.length === 1}"
class="page-submenu"
stretch
@tab-click="handleTabClick"
>
<template v-for="item in tabIndices">
<el-tab-pane
:key="item.name"
:disabled="item.disabled"
:label-content="item.labelContent"
:name="item.name"
>
<span slot="label">
<i v-if="item.icon" :class="item.icon" class="fa " />
{{ item.title }}
<slot :tab="item.name" name="badge" />
</span>
</el-tab-pane>
</template>
</el-tabs>
<transition appear mode="out-in" name="fade-transform">
<slot>
<keep-alive v-if="flag">
<AutoDataZTree
:key="componentKey"
ref="AutoDataZTree"
:setting="activeTreeSetting"
@urlChange="handleUrlChange"
>
<div slot="rMenu" slot-scope="{data}">
<slot :data="data" name="rMenu" />
</div>
</AutoDataZTree>
</keep-alive>
</slot>
</transition>
</div>
</template>
<script>
import AutoDataZTree from '../AutoDataZTree'
import merge from 'webpack-merge'
const ACTIVE_TREE_TAB_KEY = 'activeTreeTab'
export default {
name: 'TabTree',
components: {
AutoDataZTree
},
props: {
submenu: {
type: Array,
default: () => []
},
activeMenu: {
type: String,
required: true
}
},
data() {
return {
flag: false,
componentKey: 1,
activeTreeSetting: {}
}
},
computed: {
iActiveMenu: {
get() {
return this.activeMenu
},
set(item) {
this.$emit('update:activeMenu', item)
this.changeTreeSetting(item)
}
},
tabIndices() {
const map = []
this.submenu.forEach((v) => {
const hidden = typeof v.hidden === 'function' ? v.hidden() : v.hidden
if (!hidden) {
map.push(v)
}
})
return map
}
},
watch: {
activeMenu(val) {
this.changeTreeSetting(val)
}
},
async mounted() {
this.iActiveMenu = await this.getPropActiveTab()
this.$eventBus.$on('treeComponentKey', () => {
this.componentKey += 1
})
},
methods: {
hideRMenu() {
this.$refs.AutoDataZTree?.hideRMenu()
},
getSelectedNodes: function() {
return this.$refs.AutoDataZTree.getSelectedNodes()
},
getNodes: function() {
return this.$refs.AutoDataZTree.getNodes()
},
selectNode: function(node) {
return this.$refs.AutoDataZTree.selectNode(node)
},
handleUrlChange(url) {
this.$emit('urlChange', url)
},
handleTabClick(tab) {
this.componentKey += 1
this.$emit('tab-click', tab)
this.$emit('update:activeMenu', tab.name)
this.$cookie.set(ACTIVE_TREE_TAB_KEY, tab.name, 1)
if (this.$router.currentRoute.query[ACTIVE_TREE_TAB_KEY]) {
this.$router.push({
query: merge(this.$route.query, { [ACTIVE_TREE_TAB_KEY]: '' })
})
}
},
changeTreeSetting(tabName) {
const vm = this
try {
this.flag = false
for (const tab of this.submenu) {
if (tab.name === tabName) {
vm.activeTreeSetting = tab.treeSetting
break
}
}
} finally {
this.flag = true
}
},
getPropActiveTab() {
let activeTab = ''
const preActiveTabs = [
this.$route.query[ACTIVE_TREE_TAB_KEY],
this.$cookie.get(ACTIVE_TREE_TAB_KEY),
this.activeMenu
]
for (const preTab of preActiveTabs) {
const currentTab = typeof preTab === 'object' ? preTab?.name : preTab
for (const tabName of this.tabIndices) {
const currentTabName = tabName?.name || ''
if (currentTab?.toLowerCase() === currentTabName?.toLowerCase()) {
return currentTabName
}
}
}
activeTab = this.tabIndices[0].name
return activeTab
}
}
}
</script>
<style lang="scss" scoped>
>>> .ztree,
>>> .ztree li,
>>> .ztree li ul,
.tree-tab {
}
>>> .ztree {
padding: 0;
}
.page-submenu >>> .el-tabs__nav-wrap {
position: static;
.el-tabs__item.is-active {
color: var(--menu-text-active);
}
}
.only-submenu {
&>>> .el-tabs__active-bar {
transform: none!important;
}
&>>> .el-tabs__item.is-active {
text-align: left;
padding: 0 20px;
}
}
</style>

View File

@@ -1,11 +1,5 @@
<template>
<ActionsGroup
v-loading="loadingStatus"
:size="'mini'"
:actions="actions"
:more-actions="moreActions"
:more-actions-title="moreActionsTitle"
/>
<ActionsGroup v-loading="loadingStatus" :size="'mini'" :actions="actions" :more-actions="moreActions" :more-actions-title="moreActionsTitle" />
</template>
<script>
@@ -27,8 +21,6 @@ const defaultUpdateCallback = function({ row, col }) {
if (typeof updateRoute === 'object') {
route = Object.assign(route, updateRoute)
} else if (typeof updateRoute === 'function') {
route = updateRoute({ row, col })
} else {
route.name = updateRoute
}
@@ -42,8 +34,6 @@ const defaultCloneCallback = function({ row, col }) {
if (typeof cloneRoute === 'object') {
route = Object.assign(route, cloneRoute)
} else if (typeof cloneRoute === 'function') {
route = cloneRoute({ row, col })
} else {
route.name = cloneRoute
}
@@ -70,7 +60,7 @@ const defaultDeleteCallback = function({ row, col, cellValue, reload }) {
await performDelete.bind(this)({ row: row, col: col })
done()
reload()
this.$message.success(this.$tc('common.deleteSuccessMsg'))
this.$message.success(this.$t('common.deleteSuccessMsg'))
} finally {
instance.confirmButtonLoading = false
}

View File

@@ -1,75 +0,0 @@
<template>
<DetailFormatter :row="row" :col="col">
<template>
<el-popover
placement="top-start"
:title="title"
width="400"
trigger="hover"
:disabled="!showItems"
>
<div class="detail-content">
<div v-for="item of items" :key="item" class="detail-item">
<span class="detail-item-name">{{ item }}</span>
</div>
</div>
<span slot="reference">{{ cellValue && cellValue.length }}</span>
</el-popover>
</template>
</DetailFormatter>
</template>
<script>
import DetailFormatter from './DetailFormatter'
import BaseFormatter from './base'
export default {
name: 'AmountFormatter',
components: {
DetailFormatter
},
extends: BaseFormatter,
data() {
return {
formatterArgsNew: {
showItems: true,
getItem(item) {
return item.name
}
}
}
},
computed: {
title() {
return this.formatterArgs.title || ''
},
items() {
const getItem = this.formatterArgs.getItem || function(item) { return item.name }
return this.cellValue?.map(item => getItem(item))
},
showItems() {
return this.formatterArgs.showItems !== false && this.cellValue?.length > 0
}
},
mounted() {
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.detail-content {
padding: 10px;
max-height: 60vh;
overflow-y: auto;
}
.detail-item {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
&:hover {
background-color: #F5F7FA;
}
}
</style>

View File

@@ -1,35 +1,12 @@
<template>
<span>{{ value }}</span>
<span>{{ cellValue.toString() }}</span>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'ArrayFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
delimiter: ', '
}
}
}
},
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
computed: {
value() {
if (!(this.cellValue instanceof Array)) {
return this.cellValue
}
return this.cellValue.join(this.formatterArgs.delimiter)
}
}
extends: BaseFormatter
}
</script>

View File

@@ -0,0 +1,46 @@
<template>
<i :class="'fa ' + iconClass" />
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'BooleanFormatter',
extends: BaseFormatter,
props: {
formatterArgsDefault: {
type: Object,
default() {
return {
iconChoices: {
true: 'fa-check text-primary',
false: 'fa-times text-danger'
},
showFalse: true,
typeChange(val) {
return !!val
}
}
}
}
},
data() {
return {
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
}
},
computed: {
iconClass() {
const key = this.formatterArgs.typeChange(this.cellValue)
if (!key && !this.formatterArgs.showFalse) {
return ''
}
return this.formatterArgs.iconChoices[key]
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,19 +1,15 @@
<template>
<span>
<el-tooltip v-if="shown" :disabled="!formatterArgs.hasTips" effect="dark" placement="bottom">
<div slot="content" v-html="tips" />
<span :class="classes">
<i v-if="formatterArgs.showIcon && icon" :class="'fa ' + icon" />
<span v-if="formatterArgs.showText">{{ text }}</span>
</span>
</el-tooltip>
<span v-else>-</span>
</span>
<el-tooltip v-if="shown" :disabled="!formatterArgs.hasTips" placement="bottom" effect="dark">
<div slot="content" v-html="tips" />
<span :class="classes">
<i v-if="formatterArgs.useIcon" :class="'fa ' + icon" />
<span v-if="formatterArgs.useText">{{ text }}</span>
</span>
</el-tooltip>
</template>
<script>
import BaseFormatter from './base'
export default {
name: 'ChoicesFormatter',
extends: BaseFormatter,
@@ -22,9 +18,9 @@ export default {
type: Object,
default() {
return {
faChoices: {
true: 'fa-check-circle',
false: 'fa-times-circle'
iconChoices: {
true: 'fa-check',
false: 'fa-times'
},
classChoices: {
true: 'text-primary',
@@ -35,19 +31,11 @@ export default {
false: this.$t('common.No')
},
getKey({ row, cellValue }) {
return (cellValue && typeof cellValue === 'object') ? cellValue.value : cellValue
},
getText({ row, cellValue }) {
const key = this.getKey({ row, cellValue })
return (cellValue && typeof cellValue === 'object') ? cellValue.label : this.textChoices[key] || cellValue
},
getIcon({ row, cellValue }) {
const key = this.getKey({ row, cellValue })
return this.faChoices[key]
return cellValue
},
hasTips: false,
showIcon: true,
showText: true,
useIcon: true,
useText: false,
showFalse: true,
getTips: ({ row, cellValue }) => {
return cellValue
@@ -68,18 +56,13 @@ export default {
)
},
icon() {
const icon = this.formatterArgs.getIcon(
{ row: this.row, cellValue: this.cellValue }
)
return icon
return this.formatterArgs.iconChoices[this.key]
},
classes() {
return this.formatterArgs.classChoices[this.key]
},
text() {
return this.formatterArgs.getText(
{ row: this.row, cellValue: this.cellValue }
)
return this.formatterArgs.textChoices[this.key]
},
tips() {
return this.formatterArgs.getTips({ cellValue: this.cellValue, row: this.row })
@@ -90,8 +73,7 @@ export default {
}
return true
}
},
methods: {}
}
}
</script>

View File

@@ -29,7 +29,7 @@ export default {
this.$message.success(this.$tc('common.deleteSuccessMsg'))
reload()
}).catch(error => {
this.$message.error(this.$tc('common.deleteErrorMsg') + ' ' + error)
this.$message.error(this.$t('common.deleteErrorMsg') + ' ' + error)
})
},
onDelete(col, row, cellValue, reload) {
@@ -43,8 +43,7 @@ export default {
if (this.col.objects === 'all') {
return false
}
const objectIds = this.col.objects.map(i => i.id)
return objectIds.indexOf(this.cellValue) === -1
return this.col.objects.indexOf(this.cellValue) === -1
}
}
}

Some files were not shown because too many files have changed in this diff Show More