Compare commits

...

64 Commits

Author SHA1 Message Date
zhaojisen
b6b1492bc1 fix some slot errors 2026-01-14 11:15:11 +08:00
zhaojisen
c585f7789e fix: Fix the issue of router-view being wrapped by keep-alive 2026-01-08 18:00:24 +08:00
zhaojisen
bfde64311c fix: Replacement lifecycle hook 2026-01-08 17:56:23 +08:00
zhaojisen
f4286978ae fix: Replace all mini with small 2026-01-08 17:44:37 +08:00
ibuler
64a9a582df perf: change table btn 2025-12-25 14:48:06 +08:00
ibuler
1318e64c15 perf: filter input 2025-12-25 11:37:00 +08:00
ibuler
e06c3393d3 perf: remove unuseable template 2025-12-25 10:00:55 +08:00
ibuler
c6ea8488ec perf: update scss-loader 2025-12-24 14:08:40 +08:00
ibuler
261e84f7e7 perf: change .sync to model 2025-12-24 11:54:55 +08:00
ibuler
ec7d4fa65e perf: update visible 2025-12-23 17:43:34 +08:00
ibuler
d6bd8afb20 perf: change theme 2025-12-23 16:33:34 +08:00
ibuler
ba2bd0eb38 perf: change btn to small 2025-12-23 15:30:51 +08:00
ibuler
14e40d9ea9 perf: fix some lint err 2025-12-22 18:15:58 +08:00
ibuler
c965a21937 perf: update v-deep and swich route err 2025-12-22 16:27:23 +08:00
ibuler
1daf140b86 perf: update some this 2025-12-19 18:14:43 +08:00
ibuler
6ad64bc990 perf: table formatter 2025-12-18 18:04:27 +08:00
ibuler
32add61fa7 perf: drawer 2025-12-18 15:10:03 +08:00
ibuler
1850c68e6b perf: update theme css 2025-12-18 10:47:44 +08:00
ibuler
5a0dacb630 update vue3 2025-12-18 10:35:21 +08:00
ibuler
eb92fbb744 for vue3 2025-12-17 19:31:14 +08:00
feng
841ba521f6 perf: assistant content 2025-12-12 10:17:18 +08:00
feng
d2feaa021e perf: Chat input tool switch css 2025-12-11 15:24:13 +08:00
feng
c231f66e4a perf: remove typeOptions 2025-12-10 18:36:42 +08:00
feng
7fe6ab5f1b perf: mcp tools 2025-12-10 18:17:38 +08:00
feng
c037b52bb6 perf: tools 2025-12-10 17:11:07 +08:00
feng
37a45cbc6e perf: thinking 2025-12-10 17:11:07 +08:00
github-actions[bot]
3d90d56373 perf: Update Dockerfile with new base image tag 2025-12-10 17:11:07 +08:00
feng
587f49286f perf: To kael 2025-12-10 17:11:07 +08:00
feng
5e1b32aaf9 perf: Open web ui settigns 2025-12-10 17:11:07 +08:00
feng
e1bc8245c9 fix: Ticket comment not display 2025-12-10 17:11:07 +08:00
w940853815
6b6de2b2c5 perf: Update asset detection logic in AccountRiskList for improved menu display 2025-12-10 17:11:07 +08:00
feng
7913979b4b perf: Account bulk error prompt 2025-12-10 17:11:07 +08:00
w940853815
16d8c7c9ac perf: Enhance error message handling for nested plain objects in getErrorResponseMsg 2025-12-10 17:11:07 +08:00
zhaojisen
768210ef86 Fixed: Fix the issue where the global organization text is not displayed completely in the organization dropdown. 2025-12-10 17:11:07 +08:00
w940853815
ce9d598683 fix: Ensure progress display is shown correctly during job execution 2025-12-10 17:11:07 +08:00
zhaojisen
2d75514aa1 Fixed: Fix the issue where creating an SSH public key for a third-party user redirects to the login password tab. 2025-12-10 17:11:07 +08:00
w940853815
669831c533 fix: Click Review to enter comments, and the page will be dimmed 2025-12-10 17:11:07 +08:00
w940853815
eb3f3ba441 perf: Add support for error message extraction from plain objects in getErrorResponseMsg 2025-12-10 17:11:07 +08:00
feng
46cf67e10f fix: Account push show params 2025-12-10 17:11:07 +08:00
w940853815
4e0b20b8e8 perf: Add condition to check node type in AccountRiskList for asset detection 2025-12-10 17:11:07 +08:00
w940853815
a6faad4b93 perf: Refactor error handling and improve parameter assignment in job forms 2025-12-10 17:11:07 +08:00
w940853815
cb51ccae61 perf: Improve error message formatting for array of strings 2025-12-10 17:11:07 +08:00
w940853815
5a9d94aad0 perf: Enhance AccountFormatter with options to exclude accounts 2025-12-10 17:11:07 +08:00
w940853815
69bdc7c0a2 perf: Job execution detail content add text-overflow class 2025-12-10 17:11:07 +08:00
w940853815
2441c08da5 perf: Support batch import of leak passwords 2025-12-10 17:11:07 +08:00
ibuler
f9c244f006 perf: small size some png 2025-12-10 17:11:07 +08:00
fit2bot
9c5ff2b3a8 perf: upgrade vue version (#5260)
* perf: upgrade vue version

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: zhaojisen <1301338853@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-12-10 17:11:07 +08:00
w940853815
91bfb4d723 perf: Add confirmation dialog before syncing platform protocols 2025-12-10 17:11:07 +08:00
feng
8d69418613 perf: Bulk account support node 2025-10-31 17:19:11 +08:00
w940853815
2d798053b3 perf: Translate select assets 2025-10-31 10:35:56 +08:00
ibuler
e48385c70e perf: revert keyword to search 2025-10-30 16:27:40 +08:00
fit2bot
4ed529bbfb perf: remove sub mod (#5254)
* perf: remove sub mod

* perf: Update Dockerfile with new base image tag

---------

Co-authored-by: ibuler <ibuler@qq.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-10-30 15:23:58 +08:00
fit2bot
ca5350cc96 perf: update lodash version 2025-10-30 14:09:50 +08:00
w940853815
9046245cdb perf: Add header to file uploader with clear selection 2025-10-28 18:50:07 +08:00
fit2bot
991a512e85 perf: Optimize form uniqueness validation (#5250)
* perf: Optimize form uniqueness validation

* perf: Add some comments

* perf: check logical

---------

Co-authored-by: zhaojisen <1301338853@qq.com>
2025-10-28 16:41:49 +08:00
dependabot[bot]
6c008e3879 chore(deps): bump dompurify from 3.1.6 to 3.2.4
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.1.6 to 3.2.4.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.1.6...3.2.4)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.2.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 15:25:17 +08:00
zhaojisen
d16fbc3b13 Fixed: Fix ESLint errors 2025-10-27 14:38:12 +08:00
fit2bot
a0354e30c7 perf: org select style 2025-10-27 12:04:31 +08:00
mikebofs
0661bb0ea0 perf: all assets can add to zone 2025-10-24 10:39:09 +08:00
ibuler
e72fe04525 perf: add stat load helptip 2025-10-24 10:38:30 +08:00
feng
59cec3d6a9 perf: getDrawerTitle 2025-10-24 10:36:58 +08:00
w940853815
829f4ceaa4 fix: Replace alert with console.log for error handling 2025-10-22 18:22:51 +08:00
w940853815
b615e35e49 fix: Update help message rendering to use v-html for proper display 2025-10-22 14:59:55 +08:00
w940853815
cda282ac6b fix: Details page does not display labels. 2025-10-20 10:35:02 +08:00
573 changed files with 13805 additions and 14671 deletions

View File

@@ -1,15 +1,21 @@
module.exports = {
root: true,
// Explicitly use vue-eslint-parser for .vue SFCs
parser: 'vue-eslint-parser',
parserOptions: {
parser: 'babel-eslint',
sourceType: 'module'
parser: '@babel/eslint-parser',
sourceType: 'module',
ecmaVersion: 2021,
ecmaFeatures: { jsx: true },
// 允许使用项目的 babel 配置babel.config.js避免 JSX 等语法解析错误
requireConfigFile: true
},
env: {
browser: true,
node: true,
es6: true
},
extends: ['plugin:vue/recommended', 'eslint:recommended'],
extends: ['plugin:vue/vue3-recommended', 'eslint:recommended'],
globals: {
window: true,
_: true
@@ -18,20 +24,28 @@ module.exports = {
// add your custom rules here
// it is base on https://github.com/vuejs/eslint-config-vue
rules: {
'vue/max-attributes-per-line': [
2,
{
singleline: 10,
multiline: {
max: 1,
allowFirstLine: false
}
}
],
'vue/max-attributes-per-line': ['warn', { singleline: 10, multiline: 1 }],
'vue/singleline-html-element-content-newline': 'off',
'vue/multiline-html-element-content-newline': 'off',
'vue/name-property-casing': ['error', 'PascalCase'],
// Vue 3: use the new rule name instead of the removed `vue/name-property-casing`
'vue/component-definition-name-casing': ['error', 'PascalCase'],
'vue/no-v-html': 'off',
// Vue 3 新增的一些默认规则在老项目里成本很高,这里关掉以减少迁移成本
'vue/multi-word-component-names': 'off',
'vue/no-reserved-component-names': 'off',
'vue/no-deprecated-v-bind-sync': 'off',
'vue/no-deprecated-dollar-listeners-api': 'off',
'vue/no-deprecated-slot-attribute': 'off',
'vue/no-deprecated-v-on-native-modifier': 'off',
'vue/no-deprecated-destroyed-lifecycle': 'error',
'vue/no-deprecated-slot-scope-attribute': 'off',
'vue/no-deprecated-props-default-this': 'off',
'vue/no-deprecated-dollar-scopedslots-api': 'off',
'vue/no-mutating-props': 'off',
'vue/no-deprecated-filter': 'off',
'vue/no-computed-properties-in-data': 'off',
// other rules used in inline disable comments
'vue/no-deprecated-router-link-tag-prop': 'off',
'accessor-pairs': 2,
'arrow-spacing': [
2,
@@ -294,7 +308,8 @@ module.exports = {
],
skipIfMatch: [
'http://[^s]*',
'^[-\\w]+/[-\\w\\.]+$' // For import paths
'^[-\\w]+/[-\\w\\.]+$',
String.raw`^\/api\/[a-z0-9\/._-]+$`
],
minLength: 3
}

View File

@@ -7,5 +7,6 @@ module.exports = {
useTabs: false,
bracketSpacing: true,
arrowParens: 'avoid',
endOfLine: 'auto'
endOfLine: 'auto',
spaceBeforeFunctionParen: false
}

3
.yarnrc Normal file
View File

@@ -0,0 +1,3 @@
cache-folder ".yarn-cache"
global-folder ".yarn-global"
prefix ".yarn-global"

View File

@@ -1,4 +1,4 @@
FROM jumpserver/lina-base:20250910_084112 AS stage-build
FROM jumpserver/lina-base:20251204_081759 AS stage-build
ARG VERSION
ENV VERSION=$VERSION

View File

@@ -1,8 +1,14 @@
module.exports = {
presets: [
'@vue/app'
],
presets: ['@vue/cli-plugin-babel/preset'],
plugins: [
'@babel/plugin-proposal-optional-chaining',
[
'@vue/babel-plugin-jsx',
{
// Keep Vue 2-style v-model and onXXX handlers compatible where used
transformOn: true,
mergeProps: true
}
]
]
}

View File

@@ -1,7 +1,8 @@
module.exports = {
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
testEnvironment: 'jsdom',
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\.vue$': '@vue/vue3-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
@@ -9,7 +10,6 @@ module.exports = {
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],

View File

@@ -5,7 +5,7 @@
"author": "JumpServer Team <support@lxware.hk>",
"license": "GPL-3.0-or-later",
"scripts": {
"dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"dev": "NODE_OPTIONS='--openssl-legacy-provider' vue-cli-service serve",
"serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve",
"build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build",
"build:prod": "vue-cli-service build",
@@ -21,133 +21,127 @@
"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 zh_Hant",
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja zh_Hant"
"apply-i18n": "python ./src/i18n/langs/i18n-util.py apply en ja zh_Hant",
"prepare": "husky install || true"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
"@fontsource/open-sans": "^5.0.24",
"@fontsource/open-sans": "^5.2.7",
"@kangc/v-md-editor": "^1.7.12",
"@traptitech/markdown-it-katex": "^3.6.0",
"@ztree/ztree_v3": "3.5.44",
"axios": "0.28.0",
"axios-retry": "^3.1.9",
"@vue/compat": "^3.4.27",
"@ztree/ztree_v3": "3.5.48",
"axios": "^1.7.9",
"axios-retry": "^4.5.0",
"babel-loader": "^10.0.0",
"cache-loader": "^4.1.0",
"babel-runtime": "^6.26.0",
"caniuse-lite": "^1.0.30001642",
"cron-parser": "^4.0.0",
"crypto-js": "^4.1.1",
"core-js": "^3.47.0",
"cron-parser": "^4.9.0",
"crypto-js": "^4.2.0",
"css-color-function": "^1.3.3",
"decimal.js": "^10.4.3",
"deepmerge": "^4.2.2",
"dompurify": "^3.1.6",
"echarts": "4.7.0",
"element-ui": "^2.15.14",
"elementui-lts": "^2.16.0",
"decimal.js": "^10.6.0",
"deepmerge": "^4.3.1",
"dompurify": "^3.3.1",
"echarts": "^5.5.0",
"element-plus": "^2.8.4",
"eslint-plugin-html": "^6.0.0",
"highlight.js": "^11.9.0",
"highlight.js": "^11.11.1",
"install": "^0.13.0",
"jquery": "^3.6.1",
"js-cookie": "2.2.0",
"jsencrypt": "^3.2.1",
"jquery": "^3.7.1",
"js-cookie": "^3.0.5",
"jsencrypt": "^3.5.4",
"less": "^3.10.3",
"less-loader": "^5.0.0",
"lodash": "^4.17.21",
"lodash.clonedeep": "^4.5.0",
"lodash.frompairs": "^4.0.1",
"lodash.get": "^4.4.2",
"lodash.has": "^4.5.2",
"lodash.includes": "^4.3.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isplainobject": "^4.0.6",
"lodash.set": "^4.3.2",
"lodash.topairs": "^4.3.0",
"lodash.values": "^4.3.0",
"markdown-it": "^13.0.2",
"markdown-it-link-attributes": "^4.0.1",
"moment": "^2.29.4",
"mitt": "^3.0.1",
"moment": "^2.30.1",
"moment-parseformat": "^4.0.0",
"normalize.css": "7.0.0",
"npm": "^7.8.0",
"nprogress": "0.2.0",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"path-to-regexp": "3.3.0",
"socket.io-client": "^4.8.2",
"sortablejs": "^1.15.6",
"v-sanitize": "^0.0.13",
"vue": "2.6.10",
"uuid": "^10.0.0",
"v-sanitize": "^0.0.14",
"vue": "^3.4.27",
"vue-codemirror": "4.0.6",
"vue-cookie": "^1.1.4",
"vue-echarts": "^5.0.0-beta.0",
"vue-i18n": "^8.15.5",
"vue-cookie-next": "^1.0.0",
"vue-echarts": "^8.0.1",
"vue-i18n": "^9.9.0",
"vue-json-editor": "^1.4.3",
"vue-markdown": "^2.2.4",
"vue-password-strength-meter": "^1.7.2",
"vue-router": "3.0.6",
"vue-select": "^3.9.5",
"vue-router": "^4.3.0",
"vue-select": "^3.20.4",
"vuejs-logger": "^1.5.4",
"vuex": "3.1.0",
"watermark-js-plus": "^1.5.8",
"xss": "^1.0.14",
"vuex": "^4.1.0",
"watermark-js-plus": "^1.6.3",
"xss": "^1.0.15",
"xterm": "^4.5.0",
"xterm-addon-fit": "^0.3.0",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/core": "7.18.6",
"@babel/register": "7.0.0",
"@vue/cli-plugin-babel": "3.6.0",
"@vue/cli-plugin-eslint": "^3.9.1",
"@vue/cli-plugin-unit-jest": "3.6.3",
"@vue/cli-service": "3.6.0",
"@vue/test-utils": "1.0.0-beta.29",
"autoprefixer": "^9.5.1",
"babel-core": "6.26.3",
"babel-eslint": "10.0.1",
"babel-jest": "23.6.0",
"chalk": "2.4.2",
"compression-webpack-plugin": "^6.1.1",
"connect": "3.6.6",
"deasync": "^0.1.29",
"eslint": "^5.15.3",
"eslint-config-prettier": "^6.15.0",
"eslint-plugin-prettier": "^3.4.1",
"@babel/core": "^7.28.5",
"@babel/eslint-parser": "^7.24.7",
"@babel/register": "^7.28.3",
"@vue/babel-plugin-jsx": "^1.2.2",
"@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-plugin-unit-jest": "^5.0.8",
"@vue/cli-service": "^5.0.8",
"@vue/compiler-sfc": "^3.4.27",
"@vue/test-utils": "^2.4.2",
"@vue/vue3-jest": "^29.2.4",
"autoprefixer": "^10.4.20",
"babel-jest": "^30.2.0",
"balanced-match": "^3.0.1",
"chalk": "^5.6.2",
"color-name": "^2.1.0",
"compression-webpack-plugin": "^11.1.0",
"connect": "^3.7.0",
"deasync": "^0.1.31",
"entities": "^7.0.0",
"eslint": "^8.57.1",
"eslint-config-prettier": "^9.1.2",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-spellcheck": "^0.0.20",
"eslint-plugin-vue": "5.2.2",
"eslint-plugin-vue": "^9.33.0",
"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",
"lint-staged": "^10.1.2",
"mockjs": "1.0.1-beta3",
"prettier": "^3.6.2",
"pretty-bytes": "^5.6.0",
"runjs": "^4.3.2",
"sass": "~1.32.6",
"sass-loader": "^7.1.0",
"script-ext-html-webpack-plugin": "2.1.3",
"github-markdown-css": "^5.8.1",
"html-webpack-plugin": "^5.5.3",
"husky": "^9.1.7",
"less-loader": "^11.1.4",
"lint-staged": "^16.2.7",
"mockjs": "^1.1.0",
"prettier": "^3.7.4",
"pretty-bytes": "^7.1.0",
"runjs": "^4.4.1",
"sass": "^1.83.0",
"sass-loader": "^16.0.6",
"script-loader": "0.7.2",
"serve-static": "^1.16.0",
"strip-ansi": "^7.1.0",
"svg-sprite-loader": "4.1.3",
"svgo": "1.2.2",
"vue-i18n-extract": "^1.1.1",
"vue-template-compiler": "2.6.10",
"webpack": "^4.28.4"
"semver": "^7.7.3",
"serve-static": "^1.16.3",
"strip-ansi": "^7.1.2",
"svg-sprite-loader": "^6.0.11",
"svgo": "^4.0.0",
"vue-eslint-parser": "^10.2.0",
"vue-i18n-extract": "^2.0.7",
"vue-loader": "^17.0.0",
"webpack": "^5.104.1"
},
"engines": {
"node": ">=8.9",
"node": ">=16",
"npm": ">= 3.0.0"
},
"browserslist": [
"> 1%",
"last 4 versions",
"ie 11"
"not ie <= 11"
],
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"src/**/*.{js,vue}": [
"eslint --fix"

View File

@@ -9,7 +9,6 @@
<meta content="no-cache" http-equiv="Cache">
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<title><%= webpackConfig.name %></title>
<link href="<%= BASE_URL %>theme/element-ui.css" rel="stylesheet">
<style>
#loading {
position: fixed;

View File

@@ -91,7 +91,7 @@
}
td .el-button.el-button--mini {
td .el-button.el-button--small {
padding: 1px 6px;
line-height: 1.5;

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,7 @@
<template>
<div id="app">
<router-view v-if="isRouterAlive" />
<!-- 使用 key 而不是 v-if 来强制重新渲染 router-view避免根节点被完全卸载导致的 DOM insertBefore 错误 -->
<router-view :key="isRouterAlive" />
</div>
</template>
@@ -43,6 +44,30 @@ export default {
}
}
},
// Vue 3 错误捕获钩子 - 捕获子组件错误,防止整个应用崩溃
errorCaptured(err, instance, info) {
// 在开发环境下打印详细错误信息
if (process.env.NODE_ENV === 'development') {
console.error('Error Captured in App:', err)
console.error('Component instance:', instance)
console.error('Error info:', info)
} else {
console.error('Component Error:', err?.message || err)
}
// 尝试显示友好的错误提示
try {
if (this.$message && typeof this.$message.error === 'function') {
this.$message.error(err?.message || '组件加载出错,请刷新页面重试')
}
} catch (e) {
// 如果 message 服务不可用,忽略
}
// 返回 false 可以阻止错误继续向上传播
// 但这里返回 true让全局错误处理器也能处理
return true
},
methods: {
getWaterMarkFields() {
const user = this.currentUser
@@ -70,7 +95,9 @@ export default {
const safeFields = { ...fields, ...allVariables }
// 安全解析模板
return new Function(...Object.keys(safeFields), `return \`${template}\`;`)(...Object.values(safeFields))
return new Function(...Object.keys(safeFields), `return \`${template}\`;`)(
...Object.values(safeFields)
)
},
createWatermark() {

View File

@@ -18,14 +18,14 @@ export function toggleLockSession(data) {
export function getAllCommandStorage() {
return request({
url: `/api/v1/terminal/command-storages/`,
url: '/api/v1/terminal/command-storages/',
method: 'get'
})
}
export function getAllReplayStorage() {
return request({
url: `/api/v1/terminal/replay-storages/`,
url: '/api/v1/terminal/replay-storages/',
method: 'get'
})
}
@@ -54,13 +54,13 @@ function SetToDefaultStorage(url) {
export function SetToDefaultCommandStorage(id) {
return SetToDefaultStorage(
`/api/v1/terminal/command-storages/${id}/`,
`/api/v1/terminal/command-storages/${id}/`
)
}
export function SetToDefaultReplayStorage(id) {
return SetToDefaultStorage(
`/api/v1/terminal/replay-storages/${id}/`,
`/api/v1/terminal/replay-storages/${id}/`
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -15,10 +15,25 @@ export const accountFieldsMeta = (vm) => {
}
return {
nodes: {
component: Select2,
label: vm.$t('Node'),
el: {
value: [],
ajax: {
url: '/api/v1/assets/nodes/',
transformOption: (item) => {
return { label: item.full_value, value: item.id }
}
}
},
hidden: () => {
return !vm.addTemplate
}
},
assets: {
component: AssetSelect,
label: vm.$t('Asset'),
rules: [Required],
el: {
multiple: false
},
@@ -33,7 +48,7 @@ export const accountFieldsMeta = (vm) => {
get disabled() {
return vm.isDisabled
},
multiple: false,
multiple: vm.addTemplate,
ajax: {
url: '/api/v1/accounts/account-templates/',
transformOption: (item) => {

View File

@@ -2,9 +2,9 @@
<AutoDataForm
v-if="!loading"
ref="AutoDataForm"
v-bind="$data"
:class="addTemplate? '': 'account-add'"
:submit-btn-text="submitBtnText"
v-bind="$data"
@submit="confirm"
/>
</template>
@@ -63,7 +63,7 @@ export default {
encryptedFields: ['secret'],
fields: [
[this.$t('Basic'), ['name', 'username', 'privileged', 'su_from', 'su_from_username', 'template']],
[this.$t('Asset'), ['assets']],
[this.$t('Asset'), ['nodes', 'assets']],
[this.$t('Secret'), [
'secret_type', 'password', 'ssh_key', 'token',
'access_key', 'passphrase', 'api_key',
@@ -172,7 +172,7 @@ export default {
<style lang='scss' scoped>
.account-add {
::v-deep .el-form-item {
:deep(.el-form-item) {
//margin-bottom: 5px;
.help-block {
@@ -180,7 +180,7 @@ export default {
}
}
::v-deep .form-group-header {
:deep(.form-group-header) {
.hr-line-dashed {
//margin: 5px 0;
}

View File

@@ -1,7 +1,7 @@
<template v-if="iVisible">
<template>
<Drawer
v-model:visible="iVisible"
:title="title"
:visible="iVisible"
class="drawer"
@close-drawer="handleCloseDrawer"
>
@@ -26,6 +26,7 @@ import Drawer from '@/components/Drawer/index.vue'
import AccountCreateUpdateForm from '@/components/Apps/AccountCreateUpdateForm/index.vue'
import IBox from '@/components/Common/IBox/index.vue'
import Page from '@/layout/components/Page/index.vue'
import vModelMixin from '@/utils/vue/vModelMixin'
export default {
name: 'CreateAccountDialog',
@@ -35,6 +36,7 @@ export default {
Page,
AccountCreateUpdateForm
},
mixins: [vModelMixin('visible')],
props: {
visible: {
type: Boolean,
@@ -54,11 +56,12 @@ export default {
},
title: {
type: String,
default: function() {
return this.$t('AddAccount')
default() {
return 'AddAccount'
}
}
},
emits: ['update:visible', 'add', 'bulk-create-done'],
data() {
return {
loading: false,
@@ -68,14 +71,6 @@ export default {
computed: {
protocols() {
return this.asset ? this.asset.protocol : []
},
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
@@ -93,23 +88,33 @@ export default {
iVisible = true
data = formValue
url = `/api/v1/accounts/accounts/bulk/`
if (data.assets.length === 0) {
this.$message.error(this.$tc('PleaseSelectAsset'))
if (
(!data.assets || data.assets.length === 0) &&
(!data.nodes || data.nodes.length === 0)
) {
this.$message.error(this.$tc('PleaseSelectAssetOrNode'))
return
}
}
this.$axios.post(url, data, {
disableFlashErrorMsg: iVisible
}).then((data) => {
this.handleResult(data, null)
this.iVisible = iVisible
if (!iVisible) {
this.$emit('add', true)
}
}).catch(error => {
this.iVisible = true
this.handleResult(null, error)
})
this.$axios
.post(url, data, {
disableFlashErrorMsg: iVisible
})
.then(data => {
this.handleResult(data, null)
this.$emit('update:visible', iVisible)
if (!iVisible) {
this.$emit('add', true)
}
})
.catch(error => {
if (error?.response?.data?.code === 'no_valid_assets') {
this.$message.error(error?.response?.data?.detail)
return
}
this.$emit('update:visible', true)
this.handleResult(null, error)
})
},
editAccount(form) {
const data = { ...form }
@@ -123,11 +128,14 @@ export default {
this.handleAccountOperation(this.account.id, 'move-to-assets', data)
break
default:
this.$axios.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data).then(() => {
this.iVisible = false
this.$emit('add', true)
this.$message.success(this.$tc('UpdateSuccessMsg'))
}).catch(error => this.setFieldError(error))
this.$axios
.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data)
.then(() => {
this.$emit('update:visible', false)
this.$emit('add', true)
this.$message.success(this.$tc('UpdateSuccessMsg'))
})
.catch(error => this.setFieldError(error))
}
},
handleResult(resp, error) {
@@ -164,7 +172,7 @@ export default {
let current = key
let errorTips = data[current]
if (errorTips instanceof Array) {
errorTips = _.filter(errorTips, (item) => Object.keys(item).length > 0)
errorTips = _.filter(errorTips, item => Object.keys(item).length > 0)
for (const i of errorTips) {
if (i instanceof Object) {
err += i?.port?.join(',')
@@ -183,15 +191,18 @@ export default {
}
},
handleCloseDrawer() {
this.iVisible = false
this.$emit('update:visible', false)
// Reflect.deleteProperty(this.$route.query, 'flag')
},
handleAccountOperation(id, path, data) {
this.$axios.post(`/api/v1/accounts/accounts/${id}/${path}/`, data).then((res) => {
this.iVisible = false
this.$emit('add', true)
this.handleResult(res, null)
}).catch(error => this.handleResult(null, error))
this.$axios
.post(`/api/v1/accounts/accounts/${id}/${path}/`, data)
.then(res => {
this.$emit('update:visible', false)
this.$emit('add', true)
this.handleResult(res, null)
})
.catch(error => this.handleResult(null, error))
}
}
}
@@ -199,8 +210,7 @@ export default {
<style lang="scss" scoped>
.drawer {
::v-deep .el-drawer__body {
:deep(.el-drawer__body) {
.el-form {
margin-right: 30px;
}

View File

@@ -9,49 +9,54 @@
/>
<ViewSecret
v-if="showViewSecretDialog"
v-model:visible="showViewSecretDialog"
:account="account"
:url="secretUrl"
:visible.sync="showViewSecretDialog"
/>
<UpdateSecretInfo
v-if="showUpdateSecretDialog"
v-model:visible="showUpdateSecretDialog"
:account="account"
:visible.sync="showUpdateSecretDialog"
@updateAuthDone="onUpdateAuthDone"
@update-auth-done="onUpdateAuthDone"
/>
<AccountCreateUpdate
v-if="showAddDialog"
v-model:visible="showAddDialog"
:account="account"
:add-template="addTemplate"
:asset="iAsset"
:title="accountCreateUpdateTitle"
:visible.sync="showAddDialog"
@add="addAccountSuccess"
@bulk-create-done="showBulkCreateResult($event)"
/>
<ResultDialog
v-if="showResultDialog"
v-model:visible="showResultDialog"
:result="createAccountResults"
:visible.sync="showResultDialog"
@close-all="closeAll"
/>
<AccountBulkUpdateDialog
v-if="updateSelectedDialogSetting.visible"
:visible.sync="updateSelectedDialogSetting.visible"
v-bind="updateSelectedDialogSetting"
v-model:visible="updateSelectedDialogSetting.visible"
@update="handleAccountBulkUpdate"
/>
<PasswordHistoryDialog
v-if="showPasswordHistoryDialog"
v-model:visible="showPasswordHistoryDialog"
:account="currentAccountColumn"
:visible.sync="showPasswordHistoryDialog"
/>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import { accountOtherActions, accountQuickFilters, connectivityMeta, isDirectoryServiceAccount } from './const'
import {
accountOtherActions,
accountQuickFilters,
connectivityMeta,
isDirectoryServiceAccount
} from './const'
import { openTaskPage } from '@/utils/jms/index'
import {
AccountConnectFormatter,
@@ -85,9 +90,7 @@ export default {
},
exportUrl: {
type: String,
default() {
return this.url.replace('/accounts/accounts/', '/accounts/account-secrets/')
}
default: ''
},
hasLeftActions: {
type: Boolean,
@@ -123,14 +126,11 @@ export default {
},
columnsMeta: {
type: Object,
default: () => {
}
default: () => {}
},
columnsDefault: {
type: Array,
default: () => ([
'name', 'username', 'secret', 'asset', 'platform', 'connect'
])
default: () => ['name', 'username', 'secret', 'asset', 'platform', 'connect']
},
headerExtraActions: {
type: Array,
@@ -161,6 +161,8 @@ export default {
showResultDialog: false,
showAddDialog: false,
showAddTemplateDialog: false,
iExportUrl:
this.exportUrl || this.url.replace('/accounts/accounts/', '/accounts/account-secrets/'),
detailDrawer: () => import('@/views/accounts/Account/AccountDetail/index.vue'),
createAccountResults: [],
iAsset: this.asset,
@@ -224,7 +226,7 @@ export default {
},
ds: {
width: '100px',
formatter: (row) => {
formatter: row => {
if (row.ds && row.ds['domain_name']) {
return row.ds['domain_name']
} else {
@@ -292,7 +294,8 @@ export default {
hasUpdate: false, // can set function(row, value)
hasDelete: true, // can set function(row, value)
hasClone: false,
canDelete: ({ row }) => vm.$hasPerm('accounts.delete_account') && !isDirectoryServiceAccount(row, this),
canDelete: ({ row }) =>
vm.$hasPerm('accounts.delete_account') && !isDirectoryServiceAccount(row, this),
moreActionsTitle: this.$t('More'),
extraActions: accountOtherActions(this)
}
@@ -360,21 +363,27 @@ export default {
type: 'primary',
icon: 'verify',
can: ({ selectedRows }) => {
return selectedRows.length > 0 &&
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(selectedRows[0].asset.type.value) === -1 &&
!this.$store.getters.currentOrgIsRoot && vm.$hasPerm('accounts.verify_account')
return (
selectedRows.length > 0 &&
['clickhouse', 'redis', 'website', 'chatgpt'].indexOf(
selectedRows[0].asset.type.value
) === -1 &&
!this.$store.getters.currentOrgIsRoot &&
vm.$hasPerm('accounts.verify_account')
)
},
callback: function({ selectedRows }) {
const ids = selectedRows.map(v => {
return v.id
})
this.$axios.post(
'/api/v1/accounts/accounts/tasks/',
{ action: 'verify', accounts: ids }).then(res => {
openTaskPage(res['task'])
}).catch(err => {
this.$message.error(this.$tc('BulkVerifyErrorMsg' + ' ' + err))
})
this.$axios
.post('/api/v1/accounts/accounts/tasks/', { action: 'verify', accounts: ids })
.then(res => {
openTaskPage(res['task'])
})
.catch(err => {
this.$message.error(this.$tc('BulkVerifyErrorMsg' + ' ' + err))
})
}.bind(this)
},
{
@@ -389,13 +398,14 @@ export default {
const ids = selectedRows.map(v => {
return v.id
})
this.$axios.patch(
'/api/v1/accounts/accounts/clear-secret/',
{ account_ids: ids }).then(() => {
this.$message.success(this.$tc('ClearSuccessMsg'))
}).catch(err => {
this.$message.error(this.$tc('ClearErrorMsg' + ' ' + err))
})
this.$axios
.patch('/api/v1/accounts/accounts/clear-secret/', { account_ids: ids })
.then(() => {
this.$message.success(this.$tc('ClearSuccessMsg'))
})
.catch(err => {
this.$message.error(this.$tc('ClearErrorMsg' + ' ' + err))
})
}.bind(this)
},
{
@@ -403,10 +413,12 @@ export default {
title: this.$t('UpdateSelected'),
icon: 'batch-update',
can: ({ selectedRows }) => {
return selectedRows.length > 0 &&
return (
selectedRows.length > 0 &&
!this.$store.getters.currentOrgIsRoot &&
vm.$hasPerm('accounts.change_account') &&
selectedRows.every(i => i.secret_type.value === selectedRows[0].secret_type.value)
)
},
callback: ({ selectedRows }) => {
vm.updateSelectedDialogSetting.selectedRows = selectedRows
@@ -442,7 +454,11 @@ export default {
watch: {
url(iNew) {
this.$set(this.tableConfig, 'url', iNew)
this.$set(this.headerActions.exportOptions, 'url', iNew.replace(/(.*)accounts/, '$1account-secrets'))
this.$set(
this.headerActions.exportOptions,
'url',
iNew.replace(/(.*)accounts/, '$1account-secrets')
)
}
},
mounted() {
@@ -478,7 +494,9 @@ export default {
this.$refs.ListTable.reloadTable()
},
async getAssetDetail() {
const { query: { asset } } = this.$route
const {
query: { asset }
} = this.$route
if (asset) {
this.iAsset = await this.$axios.get(`/api/v1/assets/assets/${asset}/`)
}

View File

@@ -1,8 +1,8 @@
<template>
<Dialog
v-bind="$attrs"
:show-cancel="false"
:title="title"
v-bind="$attrs"
@confirm="closeDialog"
v-on="$listeners"
>
@@ -13,7 +13,7 @@
</Dialog>
</template>
<script>
<script lang="jsx">
import Dialog from '@/components/Dialog/index.vue'
import DataTable from '@/components/Table/DataTable/index.vue'
@@ -49,6 +49,10 @@ export default {
prop: 'asset',
label: this.$t('Asset')
},
{
prop: 'account',
label: this.$t('Account')
},
{
prop: 'state',
label: this.$t('Status'),
@@ -110,10 +114,7 @@ export default {
color: var(--color-success);
}
.color-default {
}
::v-deep .el-data-table .el-table .el-table__row > td > div > span {
:deep(.el-data-table .el-table .el-table__row > td > div > span) {
white-space: inherit;
}

View File

@@ -1,5 +1,9 @@
<template>
<GenericListTableDialog :visible.sync="iVisible" v-bind="config" />
<GenericListTableDialog
v-bind="config"
:visible="visible"
@update:visible="$emit('update:visible', $event)"
/>
</template>
<script>
@@ -20,6 +24,7 @@ export default {
default: false
}
},
emits: ['update:visible'],
data() {
return {
config: {
@@ -59,20 +64,8 @@ export default {
}
}
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
}
}
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -1,8 +1,8 @@
<template>
<Dialog
v-model:visible="show"
:destroy-on-close="true"
:show-cancel="false"
:visible.sync="show"
:width="'50'"
v-bind="$attrs"
@confirm="accountConfirmHandle"
@@ -65,7 +65,7 @@ export default {
</script>
<style lang="scss" scoped>
.item-textarea ::v-deep .el-textarea__inner {
.item-textarea :deep(.el-textarea__inner) {
height: 110px;
}
@@ -78,12 +78,12 @@ export default {
border-bottom: none;
}
::v-deep .el-form-item__label {
:deep(.el-form-item__label) {
padding-right: 20px;
line-height: 30px;
}
::v-deep .el-form-item__content {
:deep(.el-form-item__content) {
line-height: 30px;
pre {

View File

@@ -1,11 +1,10 @@
<template>
<Dialog
v-model:visible="iVisible"
:destroy-on-close="true"
:show-buttons="false"
:title="$tc('UpdateAssetUserToken')"
:visible.sync="iVisible"
width="800px"
v-on="$listeners"
>
<AutoDataForm
:fields="fields"
@@ -25,9 +24,11 @@ import Dialog from '@/components/Dialog/index.vue'
import { accountFieldsMeta } from '@/components/Apps/AccountCreateUpdateForm/const'
import { encryptPassword } from '@/utils/secure'
import AutoDataForm from '@/components/Form/AutoDataForm/index.vue'
import vModelMixin from '@/utils/vue/vModelMixin'
export default {
name: 'UpdateSecretInfo',
mixins: [vModelMixin('visible')],
components: {
AutoDataForm,
Dialog
@@ -42,12 +43,19 @@ export default {
default: false
}
},
emits: ['update:visible', 'update-auth-done'],
data() {
const accountMeta = accountFieldsMeta(this)
return {
fields: [
'name', 'secret_type', 'password', 'ssh_key', 'token',
'access_key', 'passphrase', 'api_key'
'name',
'secret_type',
'password',
'ssh_key',
'token',
'access_key',
'passphrase',
'api_key'
],
fieldsMeta: {
...accountMeta,
@@ -64,34 +72,25 @@ export default {
}
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
methods: {
handleConfirm(form) {
const secretType = this.account.secret_type.value
const data = {
secret: encryptPassword(form[secretType])
}
this.$axios.patch(
`/api/v1/accounts/accounts/${this.account.id}/`,
data,
{ disableFlashErrorMsg: true }
).then(res => {
this.$message.success(this.$tc('UpdateSuccessMsg'))
this.iVisible = false
}).catch(err => {
const errMsg = Object.values(err.response.data).join(', ')
this.$message.error(this.$tc('UpdateErrorMsg') + ' ' + errMsg)
this.iVisible = false
})
this.$axios
.patch(`/api/v1/accounts/accounts/${this.account.id}/`, data, {
disableFlashErrorMsg: true
})
.then(res => {
this.$message.success(this.$tc('UpdateSuccessMsg'))
this.$emit('update:visible', false)
})
.catch(err => {
const errMsg = Object.values(err.response.data).join(', ')
this.$message.error(this.$tc('UpdateErrorMsg') + ' ' + errMsg)
this.$emit('update:visible', false)
})
},
handleCancel() {
this.$emit('update:visible', false)

View File

@@ -1,12 +1,12 @@
<template>
<div>
<Dialog
v-bind="$attrs"
v-model:visible="showSecret"
:destroy-on-close="true"
:show-cancel="false"
:title="title"
:visible.sync="showSecret"
:title="iTitle"
:width="'50'"
v-bind="$attrs"
@confirm="accountConfirmHandle"
v-on="$listeners"
>
@@ -20,10 +20,12 @@
<el-form-item :label="secretTypeLabel">
<SecretViewerFormatter
:cell-value="secretInfo.secret"
:col="{ formatterArgs: {
name: account['name'],
secretType: secretType || ''
}}"
:col="{
formatterArgs: {
name: account['name'],
secretType: secretType || ''
}
}"
@input="onShowKeyCopyFormatterChange"
/>
</el-form-item>
@@ -36,12 +38,12 @@
<el-form-item :label="$tc('DateUpdated')">
<span>{{ account['date_updated'] | date }}</span>
</el-form-item>
<el-form-item v-if="showPasswordRecord" v-perms="'accounts.view_accountsecret'" :label="$tc('PasswordRecord')">
<el-link
:underline="false"
type="success"
@click="showHistoryDialog"
>
<el-form-item
v-if="showPasswordRecord"
v-perms="'accounts.view_accountsecret'"
:label="$tc('PasswordRecord')"
>
<el-link :underline="false" type="success" @click="showHistoryDialog">
<span style="padding-right: 30px">
{{ versions }}
</span>
@@ -51,13 +53,13 @@
</Dialog>
<PasswordHistoryDialog
v-if="showPasswordHistoryDialog"
v-model:visible="showPasswordHistoryDialog"
:account="account"
:visible.sync="showPasswordHistoryDialog"
/>
</div>
</template>
<script>
<script lang="jsx">
import Dialog from '@/components/Dialog/index.vue'
import PasswordHistoryDialog from './PasswordHistoryDialog.vue'
import { SecretViewerFormatter } from '@/components/Table/TableFormatters'
@@ -89,15 +91,14 @@ export default {
},
title: {
type: String,
default: function() {
return this.$tc('Detail')
}
default: ''
},
showPasswordRecord: {
type: Boolean,
default: true
}
},
emits: ['update:visible'],
data() {
return {
modifiedSecret: '',
@@ -107,6 +108,7 @@ export default {
mfaDialogVisible: true,
sshKeyFingerprint: '-',
historyCount: 0,
iTitle: this.title || this.$tc('Detail'),
showPasswordHistoryDialog: false
}
},
@@ -140,13 +142,14 @@ export default {
name: this.secretInfo.name,
secret: encryptPassword(this.modifiedSecret)
}
const url = this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
const url =
this.type === 'account' ? `/api/v1/accounts/accounts` : `/api/v1/accounts/account-templates`
this.$axios.patch(`${url}/${this.account.id}/`, params).then(() => {
this.$message.success(this.$tc('UpdateSuccessMsg'))
})
},
showSecretDialog() {
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then((res) => {
return this.$axios.get(this.url, { disableFlashErrorMsg: true }).then(res => {
this.secretInfo = res
this.sshKeyFingerprint = res?.spec_info?.ssh_key_fingerprint || '-'
this.showSecret = true
@@ -167,54 +170,54 @@ export default {
</script>
<style lang="scss" scoped>
.item-textarea ::v-deep .el-textarea__inner {
height: 110px;
.item-textarea :deep(.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 {
border-bottom: 1px solid #EBEEF5;
padding: 5px 0;
margin-bottom: 0;
&:last-child {
border-bottom: none;
}
::v-deep .el-form-item__label {
display: flex;
align-items: center;
justify-content: flex-start;
padding-right: 20px;
line-height: 30px;
word-break: keep-all;
overflow-wrap: break-word;
white-space: normal;
}
::v-deep .el-form-item__content {
line-height: 30px;
pre {
margin: 0;
}
}
:deep(.el-form-item__label) {
display: flex;
align-items: center;
justify-content: flex-start;
padding-right: 20px;
line-height: 30px;
word-break: keep-all;
overflow-wrap: break-word;
white-space: normal;
}
ul {
margin: 0;
}
:deep(.el-form-item__content) {
line-height: 30px;
li {
display: block;
font-size: 13px;
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.title {
color: #303133;
font-weight: 500;
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

@@ -161,11 +161,11 @@ export default {
</script>
<style lang="scss" scoped>
.page ::v-deep .page-heading {
.page :deep(.page-heading) {
display: none;
}
.el-dialog__wrapper ::v-deep .el-dialog__body {
.el-dialog__wrapper :deep(.el-dialog__body) {
padding: 0 0 0 3px;
.tree-table {
@@ -191,11 +191,11 @@ export default {
}
}
.page ::v-deep .treebox .ztree {
.page :deep(.treebox .ztree) {
}
.asset-select-dialog ::v-deep .el-icon-circle-check {
.asset-select-dialog :deep(.el-icon-circle-check) {
display: none;
}
</style>

View File

@@ -11,12 +11,12 @@
<AssetSelectDialog
v-if="dialogVisible"
ref="dialog"
v-model:visible="dialogVisible"
:base-node-url="baseNodeUrl"
:base-url="baseUrl"
:tree-setting="treeSetting"
:tree-url-query="treeUrlQuery"
:value="value"
:visible.sync="dialogVisible"
v-bind="$attrs"
@cancel="handleCancel"
@confirm="handleConfirm"
@@ -135,11 +135,11 @@ export default {
width: 100%;
}
.page ::v-deep .page-heading {
.page :deep(.page-heading) {
display: none;
}
.el-dialog__wrapper ::v-deep .el-dialog__body {
.el-dialog__wrapper :deep(.el-dialog__body) {
padding: 0 0 0 3px;
.tree-table {
@@ -168,7 +168,7 @@ export default {
}
}
.page ::v-deep .treebox {
.page :deep(.treebox) {
height: inherit !important;
}
</style>

View File

@@ -1,7 +1,7 @@
<template>
<TreeTable
ref="TreeList"
:active-menu.sync="treeTableConfig.activeMenu"
v-model:active-menu="treeTableConfig.activeMenu"
:component="treeComponent"
:table-config="tableConfig"
:tree-tab-config="treeTableConfig"

View File

@@ -1,12 +1,7 @@
<template>
<div>
<div>
<el-button
:disabled="isDisabled"
size="mini"
type="primary"
@click="onOpenDialog"
>
<el-button :disabled="isDisabled" size="small" type="primary" @click="onOpenDialog">
{{ $tc('Setting') }}
</el-button>
</div>
@@ -16,7 +11,7 @@
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="visible"
:visible="visible"
v-bind="$attrs"
width="60%"
v-on="$listeners"
@@ -50,7 +45,7 @@ export default {
title: {
type: String,
default: function() {
return this.$t('PushParams')
return 'PushParams'
}
},
assets: {
@@ -127,21 +122,21 @@ export default {
this.remoteMeta = data.actions[this.config.method.toUpperCase()] || {}
},
async getFilterPlatforms() {
return await this.$axios.post(
'/api/v1/assets/platforms/filter-nodes-assets/',
{
'node_ids': this.nodes,
'asset_ids': this.assets,
'platform_ids': this.platforms.map(i => i.id || i.pk || i)
}
)
return await this.$axios.post('/api/v1/assets/platforms/filter-nodes-assets/', {
node_ids: this.nodes,
asset_ids: this.assets,
platform_ids: this.platforms.map(i => i.id || i.pk || i)
})
},
async handleFieldChange() {
const platforms = await this.getFilterPlatforms()
let pushAccountMethods = platforms.map(i => i.automation[this.method])
pushAccountMethods = _.uniq(pushAccountMethods)
// 检测是否有可设置的推送方式
const hasCanSettingPushMethods = _.intersection(pushAccountMethods, Object.keys(this.remoteMeta))
const hasCanSettingPushMethods = _.intersection(
pushAccountMethods,
Object.keys(this.remoteMeta)
)
this.setFormConfig(hasCanSettingPushMethods)
this.isDisabled = hasCanSettingPushMethods.length <= 0
},
@@ -152,14 +147,14 @@ export default {
this.config.fields = []
// Todo: 未来改成后端处理,生成 serializer, 这里就不用判断类型了
const typeMapper = {
'string': 'input',
'boolean': 'switch'
string: 'input',
boolean: 'switch'
}
for (const method of methods) {
const filterField = this.remoteMeta[method] || {}
// 修改资产、节点时不点击设置按钮也需要获取form表单值暴露出去
if (this.form.hasOwnProperty(method)) {
if (Object.prototype.hasOwnProperty.call(this.form, method)) {
newForm[method] = this.form[method]
}
fields.push([filterField.label, [method]])
@@ -199,5 +194,4 @@ export default {
}
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -2,7 +2,7 @@
<div>
<div>
<el-button
size="mini"
size="small"
type="primary"
@click="onOpenDialog"
>
@@ -15,9 +15,10 @@
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="visible"
:visible="visible"
v-bind="$attrs"
width="40%"
@update:visible="$emit('update:visible', $event)"
v-on="$listeners"
>
<BlockedIPList />
@@ -43,7 +44,7 @@ export default {
title: {
type: String,
default: function() {
return this.$t('BlockedIPS')
return 'BlockedIPS'
}
},
url: {

View File

@@ -1,10 +1,10 @@
<template>
<div>
<Dialog
v-model:visible="showSecret"
:destroy-on-close="true"
:show-cancel="false"
:title="title"
:visible.sync="showSecret"
:title="iTitle"
:width="'50'"
v-bind="$attrs"
@confirm="accountConfirmHandle"
@@ -53,16 +53,15 @@ export default {
},
title: {
type: String,
default: function() {
return this.$tc('ViewSecret')
}
default: ''
}
},
data() {
return {
secretInfo: {},
showSecret: false,
mfaDialogVisible: true
mfaDialogVisible: true,
iTitle: this.title || this.$tc('ViewSecret')
}
},
computed: {},
@@ -88,7 +87,7 @@ export default {
</script>
<style lang="scss" scoped>
.item-textarea ::v-deep .el-textarea__inner {
.item-textarea :deep(.el-textarea__inner) {
height: 110px;
}
@@ -101,12 +100,12 @@ export default {
border-bottom: none;
}
::v-deep .el-form-item__label {
:deep(.el-form-item__label) {
padding-right: 20px;
line-height: 30px;
}
::v-deep .el-form-item__content {
:deep(.el-form-item__content) {
line-height: 30px;
pre {

View File

@@ -1,12 +1,60 @@
<template>
<div class="container">
<div v-if="hasPrompt" class="chat-action">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled"
v-bind="select"
@change="onSelectChange"
/>
<div class="chat-action">
<div class="model-select">
<Select2
v-model="select.value"
:disabled="isLoading || isSelectDisabled || loading || !options.length"
v-bind="select"
@change="onSelectChange"
/>
</div>
<el-dropdown
:hide-on-click="false"
trigger="click"
>
<span class="el-dropdown-link">
<i class="fa fa-plug" />
</span>
<el-dropdown-menu slot="dropdown">
<div class="menu-section">
<div v-if="toolsLoading">
<i class="el-icon-loading" /> {{ $t('Loading') }}
</div>
<div v-else class="menu-body">
<div>
<div
v-for="item in toolOptions"
:key="item.value"
>
<div style="padding: 0 10px">
<i class="fa fa-wrench item-icon" />
<span class="item-label">{{ item.label }}</span>
&nbsp;&nbsp;&nbsp;
<el-switch
:value="selectedToolsSet.has(item.value)"
@change="() => toggleTool(item.value)"
/>
</div>
</div>
<div
v-for="item in toolServerOptions"
:key="item.value"
>
<div>
<i class="fa fa-server item-icon" />
<span class="item-label">{{ item.label }}</span>
<el-switch
:value="selectedToolServersSet.has(item.value)"
@change="() => toggleToolServer(item.value)"
/>
</div>
</div>
</div>
</div>
</div>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="chat-input">
<el-input
@@ -37,9 +85,37 @@ export default {
type: Boolean,
default: false
},
hasPrompt: {
modelOptions: {
type: Array,
default: () => []
},
selectedModel: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: true
default: false
},
toolOptions: {
type: Array,
default: () => []
},
toolServerOptions: {
type: Array,
default: () => []
},
selectedTools: {
type: Array,
default: () => []
},
selectedToolServers: {
type: Array,
default: () => []
},
toolsLoading: {
type: Boolean,
default: false
}
},
data() {
@@ -47,15 +123,10 @@ export default {
isIM: false,
inputValue: '',
select: {
url: '/api/v1/settings/chatai-prompts/',
value: '',
multiple: false,
placeholder: this.$t('Role'),
ajax: {
transformOption: (item) => {
return { label: item.name, value: item.content }
}
}
placeholder: this.$t('Model'),
options: []
}
}
},
@@ -64,7 +135,32 @@ export default {
isLoading: state => state.chat.loading
}),
isSelectDisabled() {
return !!this.select.value
return false
},
options() {
return (this.modelOptions || []).map(item => {
return { label: item.name || item.id, value: item.id }
})
},
selectedToolsSet() {
return new Set(this.selectedTools || [])
},
selectedToolServersSet() {
return new Set(this.selectedToolServers || [])
}
},
watch: {
modelOptions: {
immediate: true,
handler(val) {
this.select.options = (val || []).map(item => ({ label: item.name || item.id, value: item.id }))
}
},
selectedModel: {
immediate: true,
handler(val) {
this.select.value = val || ''
}
}
},
methods: {
@@ -84,7 +180,25 @@ export default {
this.inputValue = ''
},
onSelectChange(value) {
this.$emit('select-prompt', value)
this.$emit('select-model', value)
},
toggleTool(id) {
const set = new Set(this.selectedTools || [])
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
this.$emit('select-tools', Array.from(set))
},
toggleToolServer(id) {
const set = new Set(this.selectedToolServers || [])
if (set.has(id)) {
set.delete(id)
} else {
set.add(id)
}
this.$emit('select-tool-servers', Array.from(set))
}
}
}
@@ -99,9 +213,18 @@ export default {
.chat-action {
width: 100%;
margin: 6px 0;
display: flex;
align-items: center;
gap: 8px;
&::v-deep .el-select {
width: 50%;
.model-select {
flex: 0 0 48%;
max-width: 240px;
min-width: 160px;
}
&:deep(.el-select) {
width: 100%;
.el-input__inner {
height: 28px;
@@ -129,7 +252,7 @@ export default {
flex-direction: column;
border-radius: 12px;
&::v-deep .el-textarea {
&:deep(.el-textarea) {
height: 100%;
.el-textarea__inner {

View File

@@ -3,9 +3,13 @@
<div class="chart-item-container">
<div class="avatar">
<el-avatar
:src="isUserRole ? userUrl : chatUrl"
v-if="isUserRole"
:src="userUrl"
class="header-avatar"
/>
<el-avatar v-else class="header-avatar" :style="{ backgroundColor: 'transparent' }">
<ModelIcon :name="modelIconName" class-name="model-icon" />
</el-avatar>
</div>
<div class="content">
<div class="operational">
@@ -76,6 +80,7 @@
<script>
import MessageText from './MessageText.vue'
import ModelIcon from '../../models/ModelIcon.vue'
import { mapGetters, mapState } from 'vuex'
import { copy } from '@/utils/common/index'
import { useChat } from '../../useChat.js'
@@ -85,7 +90,8 @@ const { setLoading, removeLoadingMessageInChat } = useChat()
export default {
components: {
MessageText
MessageText,
ModelIcon
},
props: {
item: {
@@ -93,6 +99,10 @@ export default {
default: () => {
}
},
selectedModel: {
type: String,
default: ''
},
isTerminal: {
type: Boolean,
default: false
@@ -129,10 +139,8 @@ export default {
? this.$i18n.t('ServerBusyRetry')
: ''
},
chatUrl() {
return this.publicSettings.CHAT_AI_TYPE === 'gpt'
? require('@/assets/img/chat.png')
: require('@/assets/img/deepSeek.png')
modelIconName() {
return (this.item?.message?.model || this.selectedModel || this.publicSettings.CHAT_AI_TYPE || '').toString()
}
},
methods: {
@@ -171,11 +179,18 @@ export default {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: transparent;
&::v-deep img {
&:deep(img) {
background-color: #fff;
}
}
.model-icon {
width: 100%;
height: 100%;
display: block;
}
}
.content {
@@ -234,7 +249,7 @@ export default {
margin: unset;
padding-left: 0.5rem;
::v-deep p {
:deep(p) {
color: #8b8b8b;
}
}

View File

@@ -40,7 +40,7 @@ export default {
text() {
const value = this.message?.content || ''
if (value && this.markdown) {
return this.markdown?.render(value)
return this.renderContentWithDetails(value)
}
return this.$xss.process(value)
}
@@ -51,7 +51,7 @@ export default {
updated() {
this.addEvents()
},
destroyed() {
unmounted() {
this.removeEvents()
},
methods: {
@@ -120,6 +120,77 @@ export default {
btn.addEventListener('click', callback)
})
},
renderContentWithDetails(value) {
// Kael responses may wrap reasoning/thinking in <details type="reasoning">; render them with a custom block.
const detailRegex = /<details[^>]*>[\s\S]*?<\/details>/gi
let result = ''
let lastIndex = 0
let match
let hasDetails = false
while ((match = detailRegex.exec(value))) {
hasDetails = true
const preceding = value.slice(lastIndex, match.index)
if (preceding.trim()) {
result += this.markdown.render(preceding)
}
result += this.renderDetailBlock(match[0])
lastIndex = match.index + match[0].length
}
if (!hasDetails) {
return this.markdown.render(value)
}
const remaining = value.slice(lastIndex)
if (remaining.trim()) {
result += this.markdown.render(remaining)
}
return result
},
renderDetailBlock(detailStr) {
const attributes = this.extractAttributes(detailStr)
const inner = detailStr.replace(/^<details[^>]*>/i, '').replace(/<\/details>$/i, '')
const summaryMatch = inner.match(/<summary>([\s\S]*?)<\/summary>/i)
const summary = summaryMatch ? this.decodeHtml(summaryMatch[1]) : ''
const body = summaryMatch ? inner.replace(summaryMatch[0], '') : inner
const bodyHtml = body.trim() ? this.markdown.render(this.decodeHtml(body.trim())) : ''
const baseClass = 'kael-detail'
if (attributes.type === 'reasoning') {
const statusClass = attributes.done === 'true' ? 'is-done' : 'is-pending'
const title = summary || this.$t('DeeplyThoughtAbout')
return `<div class="${baseClass} ${baseClass}--reasoning ${statusClass}">
<div class="${baseClass}__header">
<span class="${baseClass}__status-dot"></span>
<span class="${baseClass}__title">${title}</span>
</div>
${bodyHtml ? `<div class="${baseClass}__body">${bodyHtml}</div>` : ''}
</div>`
}
return `<div class="${baseClass}">
${summary ? `<div class="${baseClass}__header">${summary}</div>` : ''}
${bodyHtml ? `<div class="${baseClass}__body">${bodyHtml}</div>` : ''}
</div>`
},
extractAttributes(detailStr) {
const attrs = {}
const attrMatch = detailStr.match(/^<details([^>]*)>/i)
const attrStr = (attrMatch && attrMatch[1]) || ''
attrStr.replace(/(\w+)="(.*?)"/g, (all, key, val) => {
attrs[key] = val
return all
})
return attrs
},
decodeHtml(str) {
if (!str) return ''
const textArea = document.createElement('textarea')
textArea.innerHTML = str
return textArea.value
},
removeBtnClickEvent(selector) {
const buttons = this.$refs.textRef.querySelectorAll(selector)
buttons.forEach((btn) => {
@@ -142,13 +213,13 @@ export default {
font-size: 13px;
max-width: 300px;;
&::v-deep p {
&:deep(p) {
margin-bottom: 0 !important;
}
background: inherit;
&::v-deep pre {
&:deep(pre) {
padding: 0 0 6px 0;
.hljs.code-block-body {
@@ -156,7 +227,7 @@ export default {
}
}
&::v-deep .code-block-wrapper {
&:deep(.code-block-wrapper) {
background: #1F2329;
padding: 0;
margin: 5px 0;
@@ -210,7 +281,7 @@ export default {
}
}
::v-deep .link-style {
:deep(.link-style) {
color: #487bf4;
&:hover {
@@ -258,4 +329,64 @@ export default {
.loading-box span:nth-child(3) {
animation-delay: 0.49s;
}
.kael-detail {
margin: 8px 0;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid #e5e5e5;
background: #f7f8fa;
&__header {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #6b7280;
}
&__title {
font-weight: 600;
}
&__status-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: #10b981;
}
&__body {
margin-top: 6px;
padding-left: 8px;
border-left: 2px solid #e5e5e5;
}
&--reasoning.is-pending {
border-color: #f59e0b40;
background: #fff8e6;
.kael-detail__status-dot {
background: #f59e0b;
animation: kael-pulse 1.2s ease-in-out infinite;
}
}
&--reasoning.is-done {
border-color: #dbeafe;
background: #f4f6ff;
}
}
@keyframes kael-pulse {
0% {
opacity: 0.45;
}
50% {
opacity: 1;
}
100% {
opacity: 0.45;
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -92,7 +92,7 @@ export default {
transform: rotate(-90deg)
}
::v-deep .el-tabs {
:deep(.el-tabs) {
.el-tabs__item {
padding: 0 10px;
font-size: 14px;

View File

@@ -29,7 +29,7 @@
</div>
<div class="sidebar">
<Sidebar
:active.sync="active"
v-model:active="active"
:expanded="expanded"
v-bind="$attrs"
@close="onClose"
@@ -46,9 +46,9 @@
import Sidebar from './components/Sidebar/index.vue'
import Chat from './components/ChitChat/index.vue'
import { getInputFocus } from './useChat.js'
import { ws } from '@/utils/request'
import DrawerPanel from '@/components/Apps/DrawerPanel/index.vue'
import { ObjectLocalStorage } from '@/utils/common'
import i18n from '@/i18n/i18n'
import { mapGetters } from 'vuex'
const aiPannelLocalStorage = new ObjectLocalStorage('ai_panel_settings')
@@ -62,7 +62,7 @@ export default {
title: {
type: String,
default: function() {
return this.$t('ChatAI')
return i18n.global.t('ChatAI')
}
},
defaultShowPanel: {
@@ -82,7 +82,8 @@ export default {
height: '400px',
expanded: false,
clientOffset: {},
currentTerminalContent: {}
currentTerminalContent: {},
initialized: false
}
},
computed: {
@@ -123,11 +124,18 @@ export default {
document.body.appendChild(script)
}
},
initAssistant() {
if (this.initialized) return
this.initialized = true
this.$nextTick(() => {
this.$refs.component?.init()
})
},
handlePostMessage() {
window.addEventListener('message', (event) => {
if (event.data === 'show-chat-panel') {
this.$refs.drawer.show = true
this.initWebSocket()
this.initAssistant()
return
}
const msg = event.data
@@ -152,11 +160,6 @@ export default {
}
this.$refs.drawer.handleHeaderMoveUp(event)
},
initWebSocket() {
if (!ws) {
this.$refs.component?.init()
}
},
onClose() {
this.$refs.drawer.show = false
},
@@ -170,7 +173,6 @@ export default {
},
save_pannel_settings() {
aiPannelLocalStorage.set('expanded', this.expanded)
console.log('AI panel settings saved:', this.expanded)
},
updateExpandedState(expanded) {
this.expanded = expanded
@@ -184,8 +186,8 @@ export default {
})
},
onToggle(status) {
this.initWebSocket()
if (status) {
this.initAssistant()
getInputFocus()
}
}

View File

@@ -0,0 +1,33 @@
<template>
<svg
:stroke-width="strokeWidth"
width="20"
height="20"
viewBox="0 0 20 20"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
:class="className"
>
<path
fill="#6b7280"
style="fill: #6b7280 !important"
d="M11.2475 18.25C10.6975 18.25 10.175 18.1455 9.67999 17.9365C9.18499 17.7275 8.74499 17.436 8.35999 17.062C7.94199 17.205 7.50749 17.2765 7.05649 17.2765C6.31949 17.2765 5.63749 17.095 5.01049 16.732C4.38349 16.369 3.87749 15.874 3.49249 15.247C3.11849 14.62 2.93149 13.9215 2.93149 13.1515C2.93149 12.8325 2.97549 12.486 3.06349 12.112C2.62349 11.705 2.28249 11.2375 2.04049 10.7095C1.79849 10.1705 1.67749 9.6095 1.67749 9.0265C1.67749 8.4325 1.80399 7.8605 2.05699 7.3105C2.30999 6.7605 2.66199 6.2875 3.11299 5.8915C3.57499 5.4845 4.10849 5.204 4.71349 5.05C4.83449 4.423 5.08749 3.862 5.47249 3.367C5.86849 2.861 6.35249 2.465 6.92449 2.179C7.49649 1.893 8.10699 1.75 8.75599 1.75C9.30599 1.75 9.82849 1.8545 10.3235 2.0635C10.8185 2.2725 11.2585 2.564 11.6435 2.938C12.0615 2.795 12.496 2.7235 12.947 2.7235C13.684 2.7235 14.366 2.905 14.993 3.268C15.62 3.631 16.1205 4.126 16.4945 4.753C16.8795 5.38 17.072 6.0785 17.072 6.8485C17.072 7.1675 17.028 7.514 16.94 7.888C17.38 8.295 17.721 8.768 17.963 9.307C18.205 9.835 18.326 10.3905 18.326 10.9735C18.326 11.5675 18.1995 12.1395 17.9465 12.6895C17.6935 13.2395 17.336 13.718 16.874 14.125C16.423 14.521 15.895 14.796 15.29 14.95C15.169 15.577 14.9105 16.138 14.5145 16.633C14.1295 17.139 13.651 17.535 13.079 17.821C12.507 18.107 11.8965 18.25 11.2475 18.25ZM7.17199 16.1875C7.72199 16.1875 8.20049 16.072 8.60749 15.841L11.7095 14.059C11.8195 13.982 11.8745 13.8775 11.8745 13.7455V12.3265L7.88149 14.62C7.63949 14.763 7.39749 14.763 7.15549 14.62L4.03699 12.8215C4.03699 12.8545 4.03149 12.893 4.02049 12.937C4.02049 12.981 4.02049 13.047 4.02049 13.135C4.02049 13.696 4.15249 14.213 4.41649 14.686C4.69149 15.148 5.07099 15.511 5.55499 15.775C6.03899 16.05 6.57799 16.1875 7.17199 16.1875ZM7.33699 13.498C7.40299 13.531 7.46349 13.5475 7.51849 13.5475C7.57349 13.5475 7.62849 13.531 7.68349 13.498L8.92099 12.7885L4.94449 10.4785C4.70249 10.3355 4.58149 10.121 4.58149 9.835V6.2545C4.03149 6.4965 3.59149 6.8705 3.26149 7.3765C2.93149 7.8715 2.76649 8.4215 2.76649 9.0265C2.76649 9.5655 2.90399 10.0825 3.17899 10.5775C3.45399 11.0725 3.81149 11.4465 4.25149 11.6995L7.33699 13.498ZM11.2475 17.161C11.8305 17.161 12.3585 17.029 12.8315 16.765C13.3045 16.501 13.6785 16.138 13.9535 15.676C14.2285 15.214 14.366 14.697 14.366 14.125V10.561C14.366 10.429 14.311 10.33 14.201 10.264L12.947 9.538V14.1415C12.947 14.4275 12.826 14.642 12.584 14.785L9.46549 16.5835C10.0045 16.9685 10.5985 17.161 11.2475 17.161ZM11.8745 11.122V8.878L10.01 7.822L8.12899 8.878V11.122L10.01 12.178L11.8745 11.122ZM7.05649 5.8585C7.05649 5.5725 7.17749 5.358 7.41949 5.215L10.538 3.4165C9.99899 3.0315 9.40499 2.839 8.75599 2.839C8.17299 2.839 7.64499 2.971 7.17199 3.235C6.69899 3.499 6.32499 3.862 6.04999 4.324C5.78599 4.786 5.65399 5.303 5.65399 5.875V9.4225C5.65399 9.5545 5.70899 9.659 5.81899 9.736L7.05649 10.462V5.8585ZM15.4385 13.7455C15.9885 13.5035 16.423 13.1295 16.742 12.6235C17.072 12.1175 17.237 11.5675 17.237 10.9735C17.237 10.4345 17.0995 9.9175 16.8245 9.4225C16.5495 8.9275 16.192 8.5535 15.752 8.3005L12.6665 6.5185C12.6005 6.4745 12.54 6.458 12.485 6.469C12.43 6.469 12.375 6.4855 12.32 6.5185L11.0825 7.2115L15.0755 9.538C15.1965 9.604 15.2845 9.692 15.3395 9.802C15.4055 9.901 15.4385 10.022 15.4385 10.165V13.7455ZM12.122 5.3635C12.364 5.2095 12.606 5.2095 12.848 5.3635L15.983 7.195C15.983 7.118 15.983 7.019 15.983 6.898C15.983 6.37 15.851 5.8695 15.587 5.3965C15.334 4.9125 14.9655 4.5275 14.4815 4.2415C14.0085 3.9555 13.4585 3.8125 12.8315 3.8125C12.2815 3.8125 11.803 3.928 11.396 4.159L8.29399 5.941C8.18399 6.018 8.12899 6.1225 8.12899 6.2545V7.6735L12.122 5.3635Z"
/>
</svg>
</template>
<script>
export default {
name: 'ChatGPTIcon',
props: {
className: {
type: String,
default: 'size-8'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,45 @@
<template>
<svg
:stroke-width="strokeWidth"
viewBox="0 0 24 16"
overflow="visible"
width="20"
height="20"
:class="className"
>
<g style="transform: translateX(13px) rotateZ(0deg); transform-origin: 4.775px 7.73501px;">
<path
shape-rendering="geometricPrecision"
fill-opacity="1"
fill="#8c653f"
style="fill: #8c653f !important"
d=" M0,0 C0,0 6.1677093505859375,15.470022201538086 6.1677093505859375,15.470022201538086 C6.1677093505859375,15.470022201538086 9.550004005432129,15.470022201538086 9.550004005432129,15.470022201538086 C9.550004005432129,15.470022201538086 3.382294178009033,0 3.382294178009033,0 C3.382294178009033,0 0,0 0,0 C0,0 0,0 0,0z"
/>
</g>
<g opacity="1" style="transform: none; transform-origin: 7.935px 7.73501px;">
<path
shape-rendering="geometricPrecision"
fill-opacity="1"
fill="#8c653f"
style="fill: #8c653f !important"
d=" M5.824605464935303,9.348296165466309 C5.824605464935303,9.348296165466309 7.93500280380249,3.911694288253784 7.93500280380249,3.911694288253784 C7.93500280380249,3.911694288253784 10.045400619506836,9.348296165466309 10.045400619506836,9.348296165466309 C10.045400619506836,9.348296165466309 5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309 C5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309 5.824605464935303,9.348296165466309z M6.166755199432373,0 C6.166755199432373,0 0,15.470022201538086 0,15.470022201538086 C0,15.470022201538086 3.4480772018432617,15.470022201538086 3.4480772018432617,15.470022201538086 C3.4480772018432617,15.470022201538086 4.709278583526611,12.22130012512207 4.709278583526611,12.22130012512207 C4.709278583526611,12.22130012512207 11.16093635559082,12.22130012512207 11.16093635559082,12.22130012512207 C11.16093635559082,12.22130012512207 12.421928405761719,15.470022201538086 12.421928405761719,15.470022201538086 C12.421928405761719,15.470022201538086 15.87000560760498,15.470022201538086 15.87000560760498,15.470022201538086 C15.87000560760498,15.470022201538086 9.703250885009766,0 9.703250885009766,0 C9.703250885009766,0 6.166755199432373,0 6.166755199432373,0 C6.166755199432373,0 6.166755199432373,0 6.166755199432373,0z"
/>
</g>
</svg>
</template>
<script>
export default {
name: 'ClaudeIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<svg
id="图层_1"
xmlns="http://www.w3.org/2000/svg"
data-name="图层 1"
viewBox="0 0 71.69 52.76"
:class="className"
:stroke-width="strokeWidth"
>
<path
id="path"
fill="#4d6bfe"
style="fill: #4d6bfe !important"
d="M523.77,276.34c-.76-.38-1.08.33-1.53.69a4,4,0,0,0-.41.41,5.07,5.07,0,0,1-4.1,1.87,8,8,0,0,0-6.46,2.53,5.82,5.82,0,0,0-3.72-4.62,6.39,6.39,0,0,1-2.85-1.94,7.76,7.76,0,0,1-.92-2.31c-.16-.48-.32-1-.87-1.05s-.83.41-1.07.82a11,11,0,0,0-1.26,5.5,11.9,11.9,0,0,0,5.49,10.14.75.75,0,0,1,.39,1c-.25.84-.54,1.65-.79,2.49-.17.53-.41.65-1,.42a16.63,16.63,0,0,1-5.18-3.52c-2.56-2.48-4.88-5.21-7.76-7.35-.68-.5-1.36-1-2.06-1.41-2.94-2.86.39-5.2,1.16-5.48s.28-1.29-2.33-1.28-5,.88-8,2a8.23,8.23,0,0,1-1.39.41,28.67,28.67,0,0,0-8.61-.3,18.57,18.57,0,0,0-13.44,7.83c-4,5.47-4.91,11.67-3.76,18.15a27.68,27.68,0,0,0,10,16.88,26.8,26.8,0,0,0,19.23,6.39c4.43-.25,9.36-.84,14.92-5.55a13.84,13.84,0,0,0,5.32,1.18,17.24,17.24,0,0,0,5.09-.38c2.2-.46,2.05-2.5,1.25-2.87-6.43-3-5-1.78-6.3-2.77,3.27-3.87,8.2-7.89,10.13-20.92a12.44,12.44,0,0,0,0-2.52c0-.51.1-.71.68-.76a12.55,12.55,0,0,0,4.62-1.42c4.17-2.28,5.85-6,6.25-10.51A1.57,1.57,0,0,0,523.77,276.34Zm-36.34,40.37c-6.24-4.9-9.27-6.52-10.52-6.45s-1,1.41-.7,2.28a8.49,8.49,0,0,0,1.11,2.21,1.14,1.14,0,0,1-.34,1.8c-2,1.24-5.5-.42-5.66-.5a26.08,26.08,0,0,1-9.87-9.88,30.15,30.15,0,0,1-3.87-13.39c-.06-1.15.28-1.56,1.42-1.77a14.31,14.31,0,0,1,4.57-.11,28.56,28.56,0,0,1,16.33,8.29,54.06,54.06,0,0,1,6.58,8.63,41.46,41.46,0,0,0,7.41,8.71,24.36,24.36,0,0,0,2.66,2C494.16,318.82,490.16,318.87,487.43,316.71Zm3-19.23a.92.92,0,0,1,.92-.92.83.83,0,0,1,.32.06.8.8,0,0,1,.34.22.9.9,0,0,1,.25.64.92.92,0,0,1-1.83,0Zm9.29,4.76a5.27,5.27,0,0,1-1.77.48,3.75,3.75,0,0,1-2.38-.76,3.57,3.57,0,0,1-1.65-2.26,5.16,5.16,0,0,1,0-1.76,2,2,0,0,0-.71-2.17,3.1,3.1,0,0,0-2.06-.59,1.63,1.63,0,0,1-.76-.24.75.75,0,0,1-.34-1.07,3.47,3.47,0,0,1,.57-.62,3.9,3.9,0,0,1,3.43,0,10,10,0,0,1,3,2.34,18.62,18.62,0,0,1,2,2.73,10.9,10.9,0,0,1,1.33,2.53C500.65,301.47,500.4,302,499.71,302.24Z"
transform="translate(-452.83 -271.91)"
/>
</svg>
</template>
<script>
export default {
name: 'DeepSeekIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
width="800px"
height="800px"
viewBox="0 0 512 512"
:class="className"
:stroke-width="strokeWidth"
>
<path
fill="#4285f4"
style="fill: #4285f4 !important"
d="M473.16,221.48l-2.26-9.59H262.46v88.22H387c-12.93,61.4-72.93,93.72-121.94,93.72-35.66,0-73.25-15-98.13-39.11a140.08,140.08,0,0,1-41.8-98.88c0-37.16,16.7-74.33,41-98.78s61-38.13,97.49-38.13c41.79,0,71.74,22.19,82.94,32.31l62.69-62.36C390.86,72.72,340.34,32,261.6,32h0c-60.75,0-119,23.27-161.58,65.71C58,139.5,36.25,199.93,36.25,256S56.83,369.48,97.55,411.6C141.06,456.52,202.68,480,266.13,480c57.73,0,112.45-22.62,151.45-63.66,38.34-40.4,58.17-96.3,58.17-154.9C475.75,236.77,473.27,222.12,473.16,221.48Z"
/>
</svg>
</template>
<script>
export default {
name: 'GeminiIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,33 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
aria-hidden="true"
focusable="false"
fill="currentColor"
:class="className"
:stroke-width="strokeWidth"
>
<path
fill="#000000"
style="fill: #000000 !important"
d="m3.005 8.858 8.783 12.544h3.904L6.908 8.858zM6.905 15.825 3 21.402h3.907l1.951-2.788zM16.585 2l-6.75 9.64 1.953 2.79L20.492 2zM17.292 7.965v13.437h3.2V3.395z"
/>
</svg>
</template>
<script>
export default {
name: 'GrokIcon',
props: {
className: {
type: String,
default: 'size-4'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
}
}
</script>

View File

@@ -0,0 +1,50 @@
<template>
<component
:is="resolvedIcon"
v-if="resolvedIcon"
:class-name="className"
:stroke-width="strokeWidth"
/>
</template>
<script>
import ChatGPTIcon from './ChatGPT.vue'
import DeepSeekIcon from './DeepSeek.vue'
import GrokIcon from './Grok.vue'
import ClaudeIcon from './Claude.vue'
import GeminiIcon from './Gemini.vue'
export default {
name: 'ModelIcon',
props: {
name: {
type: String,
default: ''
},
className: {
type: String,
default: 'size-5'
},
strokeWidth: {
type: [String, Number],
default: '1.5'
}
},
computed: {
normalizedName() {
return (this.name || '').toLowerCase()
},
resolvedIcon() {
const name = this.normalizedName
if (!name) return null
if (name.includes('gpt')) return ChatGPTIcon
if (name.includes('deep-seek')) return DeepSeekIcon
if (name.includes('deepseek')) return DeepSeekIcon
if (name.includes('grok')) return GrokIcon
if (name.includes('claude')) return ClaudeIcon
if (name.includes('gemini')) return GeminiIcon
return null
}
}
}
</script>

View File

@@ -74,7 +74,7 @@ export default {
this.init()
this.insertToBody()
},
beforeDestroy() {
beforeUnmount() {
const element = this.$refs.drawer
element.remove()
// window.removeEventListener('click', this.closeSidebar)

View File

@@ -1,31 +1,32 @@
<template>
<Dialog
v-if="iVisible"
v-if="visible"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="$tc('TestGatewayTestConnection')"
:visible.sync="iVisible"
:visible="visible"
top="35vh"
width="40%"
@update:visible="$emit('update:visible', $event)"
>
<el-row :gutter="20">
<el-col :md="4" :sm="24">
<div style="line-height: 34px">{{ $t('SSHPort') }}</div>
</el-col>
<el-col :md="14" :sm="24">
<el-input v-model="port" />
<el-input v-model="iPort" />
<span class="help-tips help-block">{{ $t('TestGatewayHelpMessage') }}</span>
</el-col>
<el-col :md="4" :sm="24">
<el-button
:loading="loading"
size="mini"
style="line-height:20px "
size="small"
style="line-height: 20px"
type="primary"
@click="dialogConfirm"
>
{{ this.$t('Confirm') }}
{{ $t('Confirm') }}
</el-button>
</el-col>
</el-row>
@@ -59,16 +60,17 @@ export default {
default: ''
}
},
emits: ['update:visible', 'update:port'],
data() {
return {}
},
computed: {
iVisible: {
iPort: {
get() {
return this.visible
return this.port
},
set(val) {
this.$emit('update:visible', val)
this.$emit('update:port', val)
}
}
},
@@ -77,14 +79,13 @@ export default {
if (isNaN(this.port)) {
return this.$message.error(this.$tc('TestPortErrorMsg'))
}
this.$axios.post(
`/api/v1/assets/gateways/${this.cell}/test-connective/`,
{ port: this.port }
)
.then((res) => {
this.$axios
.post(`/api/v1/assets/gateways/${this.cell}/test-connective/`, { port: this.port })
.then(res => {
openTaskPage(res['task'])
}).finally(() => {
this.iVisible = false
})
.finally(() => {
this.$emit('update:visible', false)
})
}
}

View File

@@ -44,9 +44,7 @@ export default {
},
getShowUrl: {
type: Function,
default({ row, col }) {
return this.tableUrl.replace('/assets/', `/assets/${row.id}/accounts/`)
}
default: ({ row, col }) => this.tableUrl.replace('/assets/', `/assets/${row.id}/accounts/`)
},
name: {
type: Object,

View File

@@ -37,8 +37,12 @@ export default {
},
headerActions: {
hasExport: false,
hasImport: false,
hasExport: true,
hasImport: true,
importOptions: {
encryptFields: [''], // 这里不加密 password''只是为了保证数组有值
canImportUpdate: false
},
hasCreate: true,
hasSearch: true,
hasRefresh: true,

View File

@@ -1,11 +1,7 @@
<template>
<div>
<div>
<el-button
size="mini"
type="primary"
@click="onOpenDialog"
>
<el-button size="small" type="primary" @click="onOpenDialog">
{{ $tc('View') }}
</el-button>
</div>
@@ -14,9 +10,10 @@
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="visible"
:visible="visible"
v-bind="$attrs"
width="40%"
@update:visible="$emit('update:visible', $event)"
v-on="$listeners"
>
<LeakPasswordList />
@@ -42,7 +39,7 @@ export default {
title: {
type: String,
default: function() {
return this.$t('LeakPasswordList')
return 'LeakPasswordList'
}
},
url: {
@@ -72,5 +69,4 @@ export default {
}
</script>
<style scoped>
</style>
<style scoped></style>

View File

@@ -27,7 +27,7 @@
</div>
</template>
<script>
<script lang="jsx">
import IBox from '@/components/Common/IBox/index.vue'
import DiffDetail from '@/components/Dialog/DiffDetail.vue'
import { openTaskPage } from '@/utils/jms/index'

View File

@@ -5,7 +5,7 @@
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="visible"
:visible="visible"
class="dialog-content"
v-bind="$attrs"
width="600px"
@@ -13,30 +13,25 @@
v-on="$listeners"
>
<div v-if="confirmTypeRequired === 'relogin'">
<el-row :gutter="24" style="margin: 0 auto;">
<el-row :gutter="24" style="margin: 0 auto">
<el-col :md="24" :sm="24">
<el-alert
:title="$tc('ReLoginTitle')"
center
style="margin-bottom: 20px;"
type="error"
/>
<el-alert :title="$tc('ReLoginTitle')" center style="margin-bottom: 20px" type="error" />
</el-col>
</el-row>
<el-row :gutter="24" style="margin: 0 auto;">
<el-row :gutter="24" style="margin: 0 auto">
<el-col :md="24" :sm="24">
<el-button class="confirm-btn" size="mini" type="primary" @click="logout">
{{ this.$t('ReLogin') }}
<el-button class="confirm-btn" size="small" type="primary" @click="logout">
{{ $t('ReLogin') }}
</el-button>
</el-col>
</el-row>
</div>
<div v-else>
<el-row :gutter="24" style="margin: 0 auto;">
<el-row :gutter="24" style="margin: 0 auto">
<el-col :md="24" :sm="24" :span="24" class="add">
<el-select
v-model="subTypeSelected"
style="width: 100%; margin-bottom: 20px;"
style="width: 100%; margin-bottom: 20px"
@change="handleSubTypeChange"
>
<el-option
@@ -49,19 +44,22 @@
</el-select>
</el-col>
</el-row>
<el-row v-if="!noCodeMFA.includes(subTypeSelected)" :gutter="24" style="margin: 0 auto;">
<el-col :md="24" :sm="24" style="display: flex; align-items: center; ">
<el-row v-if="!noCodeMFA.includes(subTypeSelected)" :gutter="24" style="margin: 0 auto">
<el-col :md="24" :sm="24" style="display: flex; align-items: center">
<el-input
v-model="secretValue"
:placeholder="inputPlaceholder"
:show-password="showPassword"
@keyup.enter.native="handleConfirm"
/>
<span v-if="subTypeSelected === 'sms' || subTypeSelected === 'email'" style="margin: -1px 0 0 20px;">
<span
v-if="subTypeSelected === 'sms' || subTypeSelected === 'email'"
style="margin: -1px 0 0 20px"
>
<el-button
:disabled="smsBtnDisabled"
size="mini"
style="line-height: 14px; float: right;"
size="small"
style="line-height: 14px; float: right"
type="primary"
@click="sendCode"
>
@@ -72,50 +70,46 @@
</el-row>
<el-row>
<el-col>
<iframe v-if="passkeyVisible" :src="passkeyUrl" style="display: none" />
<iframe
v-if="passkeyVisible"
:src="passkeyUrl"
style="display: none"
/>
<iframe
v-if="isFaceCaptureVisible && subTypeSelected ==='face' && faceCaptureUrl"
v-if="isFaceCaptureVisible && subTypeSelected === 'face' && faceCaptureUrl"
:src="faceCaptureUrl"
allow="camera"
sandbox="allow-scripts allow-same-origin"
style="width: 100%; height: 600px;border: none;"
style="width: 100%; height: 600px; border: none"
/>
</el-col>
</el-row>
<el-row :gutter="24" style="margin: 20px auto 10px;">
<el-row :gutter="24" style="margin: 20px auto 10px">
<el-col :md="24" :sm="24">
<el-button
v-if="!noCodeMFA.includes(subTypeSelected)"
class="confirm-btn"
size="mini"
size="small"
type="primary"
@click="handleConfirm"
>
{{ this.$t('Confirm') }}
{{ $t('Confirm') }}
</el-button>
<el-button
v-if="subTypeSelected === 'face'"
v-show="!isFaceCaptureVisible"
class="confirm-btn"
size="mini"
size="small"
type="primary"
@click="handleFaceCapture"
>
{{ this.$tc('VerifyFace') }}
{{ $tc('VerifyFace') }}
</el-button>
<el-button
v-if="subTypeSelected === 'passkey'"
v-loading="passkeyVisible"
class="confirm-btn"
size="mini"
size="small"
type="primary"
@click="handlePasskeyVerify"
>
{{ this.$tc('Next') }}
{{ $tc('Next') }}
</el-button>
</el-col>
</el-row>
@@ -173,7 +167,7 @@ export default {
mounted() {
this.$eventBus.$on('showConfirmDialog', this.performConfirm)
},
beforeDestroy() {
beforeUnmount() {
this.$eventBus.$off('showConfirmDialog', this.performConfirm)
},
methods: {
@@ -195,55 +189,65 @@ export default {
this.$log.debug('perform confirm action')
const confirmType = response.data?.code
const confirmUrl = '/api/v1/authentication/confirm/'
this.$axios.get(confirmUrl, { params: { confirm_type: confirmType } }).then((data) => {
this.confirmTypeRequired = data.confirm_type
this.$axios
.get(confirmUrl, { params: { confirm_type: confirmType } })
.then(data => {
this.confirmTypeRequired = data.confirm_type
if (this.confirmTypeRequired === 'relogin') {
this.$axios.post(confirmUrl, { 'confirm_type': 'relogin', 'secret_key': 'x' }).then(() => {
this.callback()
this.visible = false
}).catch(() => {
this.title = this.$t('NeedReLogin')
this.visible = true
})
return
}
this.subTypeChoices = data.content
const defaultSubType = this.subTypeChoices.filter(item => !item.disabled)[0]
this.subTypeSelected = defaultSubType.name
this.inputPlaceholder = defaultSubType.placeholder
this.visible = true
}).catch((err) => {
const data = err.response?.data
const msg = data?.error || data?.detail || data?.msg || this.$t('GetConfirmTypeFailed')
this.$message.error(msg)
this.cancel(err)
}).finally(() => {
this.processing = false
})
if (this.confirmTypeRequired === 'relogin') {
this.$axios
.post(confirmUrl, { confirm_type: 'relogin', secret_key: 'x' })
.then(() => {
this.callback()
this.visible = false
})
.catch(() => {
this.title = this.$t('NeedReLogin')
this.visible = true
})
return
}
this.subTypeChoices = data.content
const defaultSubType = this.subTypeChoices.filter(item => !item.disabled)[0]
this.subTypeSelected = defaultSubType.name
this.inputPlaceholder = defaultSubType.placeholder
this.visible = true
})
.catch(err => {
const data = err.response?.data
const msg = data?.error || data?.detail || data?.msg || this.$t('GetConfirmTypeFailed')
this.$message.error(msg)
this.cancel(err)
})
.finally(() => {
this.processing = false
})
}, 500),
logout() {
window.location.href = `${process.env.VUE_APP_LOGOUT_PATH}?next=${this.$route.fullPath}`
},
sendCode() {
this.$axios.post(`/api/v1/authentication/mfa/select/`, { type: this.subTypeSelected }).then(res => {
this.$message.success(this.$tc('VerificationCodeSent'))
let time = 60
this.smsBtnDisabled = true
this.$axios
.post(`/api/v1/authentication/mfa/select/`, { type: this.subTypeSelected })
.then(res => {
this.$message.success(this.$tc('VerificationCodeSent'))
let time = 60
this.smsBtnDisabled = true
const interval = setInterval(() => {
time -= 1
this.smsBtnText = `${this.$t('Pending')}: ${time}`
const interval = setInterval(() => {
time -= 1
this.smsBtnText = `${this.$t('Pending')}: ${time}`
if (time <= 0) {
clearInterval(interval)
this.smsBtnText = this.$t('SendVerificationCode')
this.smsBtnDisabled = false
}
}, 1000)
}).catch(() => {
this.$message.error(this.$tc('FailedToSendVerificationCode'))
})
if (time <= 0) {
clearInterval(interval)
this.smsBtnText = this.$t('SendVerificationCode')
this.smsBtnDisabled = false
}
}, 1000)
})
.catch(() => {
this.$message.error(this.$tc('FailedToSendVerificationCode'))
})
},
handlePasskeyVerify() {
this.passkeyVisible = true
@@ -267,23 +271,26 @@ export default {
},
startFaceCapture() {
const url = '/api/v1/authentication/face/context/'
this.$axios.post(url).then(data => {
const token = data['token']
this.faceCaptureUrl = '/facelive/capture?token=' + token
this.isFaceCaptureVisible = true
this.$axios
.post(url)
.then(data => {
const token = data['token']
this.faceCaptureUrl = '/facelive/capture?token=' + token
this.isFaceCaptureVisible = true
const timer = setInterval(() => {
this.$axios.get(url + `?token=${token}`).then(data => {
if (data['is_finished']) {
clearInterval(timer)
this.isFaceCaptureVisible = false
this.handleConfirm()
}
})
}, 1000)
}).catch(() => {
this.$message.error(this.$tc('FailedToStartFaceCapture'))
})
const timer = setInterval(() => {
this.$axios.get(url + `?token=${token}`).then(data => {
if (data['is_finished']) {
clearInterval(timer)
this.isFaceCaptureVisible = false
this.handleConfirm()
}
})
}, 1000)
})
.catch(() => {
this.$message.error(this.$tc('FailedToStartFaceCapture'))
})
},
handleFaceCapture() {
this.startFaceCapture()
@@ -306,27 +313,33 @@ export default {
const data = {
confirm_type: this.confirmTypeRequired,
mfa_type: this.confirmTypeRequired === 'mfa' ? this.subTypeSelected : '',
secret_key: this.confirmTypeRequired === 'password' ? encryptPassword(this.secretValue) : this.secretValue
secret_key:
this.confirmTypeRequired === 'password'
? encryptPassword(this.secretValue)
: this.secretValue
}
this.$axios.post(`/api/v1/authentication/confirm/`, data).then(() => {
this.onSuccess()
}).catch((err) => {
this.$message.error(err.message || this.$tc('ConfirmFailed'))
this.faceCaptureUrl = null
this.isFaceCaptureVisible = false
})
this.$axios
.post(`/api/v1/authentication/confirm/`, data)
.then(() => {
this.onSuccess()
})
.catch(err => {
this.$message.error(err.message || this.$tc('ConfirmFailed'))
this.faceCaptureUrl = null
this.isFaceCaptureVisible = false
})
}
}
}
</script>
<style lang="scss" scoped>
.dialog-content ::v-deep .el-dialog__footer {
.dialog-content :deep(.el-dialog__footer) {
padding: 0;
}
.dialog-content ::v-deep .el-dialog {
.dialog-content :deep(.el-dialog) {
padding: 8px;
.el-dialog__body {

View File

@@ -115,7 +115,7 @@ export default {
<style lang='scss' scoped>
.variable-add {
::v-deep .el-form-item {
:deep(.el-form-item) {
margin-bottom: 5px;
.help-block {
@@ -123,7 +123,7 @@ export default {
}
}
::v-deep .form-group-header {
:deep(.form-group-header) {
.hr-line-dashed {
margin: 5px 0;
}

View File

@@ -53,7 +53,7 @@ export default {
<style lang='scss' scoped>
.variable-set {
::v-deep .el-form-item {
:deep(.el-form-item) {
margin-bottom: 5px;
.help-block {
@@ -61,7 +61,7 @@ export default {
}
}
::v-deep .form-group-header {
:deep(.form-group-header) {
.hr-line-dashed {
margin: 5px 0;
}

View File

@@ -3,10 +3,11 @@
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="iVisible"
:visible="visible"
class="help-dialog"
top="1vh"
width="50%"
@update:visible="$emit('update:visible', $event)"
>
<p>{{ variablesHelpText }}</p>
<table border="1" class="help-table">
@@ -19,8 +20,12 @@
<td :title="$tc('ClickCopy')" class="item-td text-link" @click="onCopy(item.name)">
<label class="item-label">{{ item.name }}</label>
</td>
<td><span>{{ item.label }}</span></td>
<td><span>{{ item.default }}</span></td>
<td>
<span>{{ item.label }}</span>
</td>
<td>
<span>{{ item.default }}</span>
</td>
</tr>
</table>
</Dialog>
@@ -46,25 +51,16 @@ export default {
variablesHelpText: {
type: String,
default() {
return this.$t('WatermarkVariableHelpText')
return 'WatermarkVariableHelpText'
}
}
},
emits: ['update:visible'],
data() {
return {
title: this.$t('BuiltinVariable')
}
},
computed: {
iVisible: {
set(val) {
this.$emit('update:visible', val)
},
get() {
return this.visible
}
}
},
methods: {
onCopy(key) {
copy(key)
@@ -73,26 +69,28 @@ export default {
}
</script>
<style>
.help-dialog.dialog .el-dialog__footer {
<style lang="scss" scoped>
:deep(.help-dialog.dialog .el-dialog__footer) {
border-top: none;
padding: 8px;
}
</style>
<style lang="scss" scoped>
.help-table {
width: 100%;
border-collapse: collapse;
border: 1px solid #dee2e6;
}
&::v-deep th, td {
:deep(.help-table) {
th,
td {
height: 40px;
padding: 0 8px;
text-align: left;
}
&::v-deep .item-td, .item-label {
.item-td,
.item-label {
cursor: pointer;
color: var(--color-primary);
}

View File

@@ -1,4 +1,4 @@
<script type="text/jsx">
<script lang="jsx">
import { toSafeLocalDateStr } from '@/utils/common/time'
export default {

View File

@@ -10,7 +10,7 @@
</div>
</template>
<script>
<script lang="jsx">
import DetailCard from './index.vue'
import { copy } from '@/utils/common/index'
import { toSafeLocalDateStr } from '@/utils/common/time'
@@ -143,7 +143,7 @@ export default {
for (const [index, item] of value.entries()) {
if (tp === 'object') {
const firstValue = value[0]
if (firstValue.hasOwnProperty('name')) {
if (Object.prototype.hasOwnProperty.call(firstValue, 'name')) {
value.forEach(item => {
const fieldName = `${name}.${item.name}`
if (excludes.includes(fieldName)) {

View File

@@ -5,7 +5,7 @@
<div v-if="item.has !== false" :key="item.key" :class="item.class " :label="item.key" class="el-form-item">
<span slot="label" class="el-form-item__label"> {{ formateLabel(item.key) }}</span>
<span class="item-value el-form-item__content">
<template
<component
:is="item.component"
v-if="item.component"
v-bind="{...item}"
@@ -29,7 +29,7 @@ export default {
title: {
type: String,
default() {
return this.$t('BasicInfo')
return 'BasicInfo'
}
},
fa: {
@@ -52,7 +52,7 @@ export default {
data() {
return {
iItems: this.items.filter(item => {
return !item.hasOwnProperty('has') || item.has === true
return !Object.prototype.hasOwnProperty.call(item, 'has') || item.has === true
})
}
},
@@ -76,7 +76,7 @@ export default {
font-size: 13px;
line-height: 2;
::v-deep .el-form-item {
:deep(.el-form-item) {
border-bottom: 1px dashed #F4F4F4;
padding: 1px 0;
margin-bottom: 0;
@@ -93,11 +93,11 @@ export default {
&.array-item {
border-bottom: none;
::v-deep .el-form-item__content {
:deep(.el-form-item__content) {
border-bottom: 1px dashed #EBEEF5
}
::v-deep .el-form-item__label:last-child {
:deep(.el-form-item__label:last-child) {
border: 1px dashed #EBEEF5;
}
}
@@ -127,13 +127,13 @@ export default {
padding: 5px 0;
}
::v-deep .el-tag--mini {
:deep(.el-tag--small) {
margin-right: 3px;
}
}
.item-value {
::v-deep span {
:deep(span) {
//display: -webkit-box;
overflow: hidden;
text-overflow: ellipsis;

View File

@@ -1,34 +1,52 @@
<template>
<IBox :title="title" :type="type" class="the-box" v-bind="$attrs">
<table class="CardTable" style="width: 100%;table-layout:fixed;">
<table class="CardTable" style="width: 100%; table-layout: fixed">
<tr>
<td colspan="2">
<Select2 ref="select2" v-model="select2.value" :disabled="iDisabled" show-select-all v-bind="select2" />
<Select2
ref="select2"
v-model="select2.value"
:disabled="iDisabled"
show-select-all
v-bind="select2"
/>
</td>
</tr>
<slot />
<tr>
<td colspan="2">
<el-button :disabled="iDisabled" :loading="submitLoading" :type="type" size="small" @click="addObjects">
<el-button
:disabled="iDisabled"
:loading="submitLoading"
:type="type"
size="small"
@click="addObjects"
>
{{ $t('Add') }}
</el-button>
</td>
</tr>
<template v-if="showHasObjects">
<tr v-for="obj of iHasObjects" :key="obj.value" class="item">
<td style="width: 100%;overflow: hidden;text-overflow: ellipsis;white-space: nowrap;">
<td style="width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap">
<el-tooltip
:content="obj.label.toString()"
:open-delay="500"
effect="dark"
placement="left"
style="margin: 4px;"
style="margin: 4px"
>
<b>{{ obj.label }}</b>
</el-tooltip>
</td>
<td>
<el-button :disabled="iDisabled" size="mini" style="float: right" type="danger" @click="removeObject(obj)">
<el-button
:disabled="iDisabled"
size="small"
style="float: right"
type="danger"
@click="removeObject(obj)"
>
<i class="fa fa-minus" />
</el-button>
</td>
@@ -36,7 +54,13 @@
</template>
<tr v-if="params.hasMore && showHasMore" class="item">
<td colspan="2">
<el-button :disabled="iDisabled" :type="type" size="small" style="width: 100%" @click="loadMore">
<el-button
:disabled="iDisabled"
:type="type"
size="small"
style="width: 100%"
@click="loadMore"
>
<i class="fa fa-arrow-down" />
{{ $t('More') }}
</el-button>
@@ -107,8 +131,7 @@ export default {
},
performDelete: {
type: Function,
default: (obj, that) => {
}
default: (obj, that) => {}
},
allowCreate: {
type: Boolean,
@@ -116,14 +139,13 @@ export default {
},
select2Config: {
type: Object,
default: () => {
}
default: () => {}
},
onDeleteSuccess: {
type: Function,
default(obj, that) {
// 从 hasObjects 中移除这个object
const theRemoveIndex = that.iHasObjects.findIndex((v) => v.value === obj.value)
const theRemoveIndex = that.iHasObjects.findIndex(v => v.value === obj.value)
that.iHasObjects.splice(theRemoveIndex, 1)
// 从 disabled values 中移除这个 value
@@ -154,8 +176,7 @@ export default {
},
performAdd: {
type: Function,
default: (objects, that) => {
}
default: (objects, that) => {}
},
showAddAll: {
type: Boolean,
@@ -166,11 +187,14 @@ export default {
default(objects, that) {
that.$log.debug('Select value', that.select2.value)
const oldValues = that.iHasObjects.map(item => item.value)
that.iHasObjects = [...that.iHasObjects, ...objects.filter(item => !oldValues.includes(item.value))]
that.iHasObjects = [
...that.iHasObjects,
...objects.filter(item => !oldValues.includes(item.value))
]
that.$refs.select2.clearSelected()
that.$message.success(that.$t('AddSuccessMsg'))
this.$refs.select2.refresh()
this.$emit('addSuccess')
that.$refs.select2.refresh()
that.$emit('addSuccess')
}
},
getHasObjects: {
@@ -231,7 +255,7 @@ export default {
this.$log.debug('hasObjects change, add ', addValues, 'remove ', removeValues)
let disabledValues = this.select2.disabledValues
if (removeValues.length > 0) {
disabledValues = disabledValues.filter((v) => {
disabledValues = disabledValues.filter(v => {
return removeValues.indexOf(v) === -1
})
}
@@ -269,7 +293,7 @@ export default {
const params = this.safeMakeParams(this.params)
let data = await this.$axios.get(this.iAjax.url, {
params: params,
validateStatus: (status) => {
validateStatus: status => {
if (status === 403) {
return 200
}
@@ -278,8 +302,8 @@ export default {
})
data = this.iAjax.processResults.bind(this)(data)
if (data.results) {
data.results.forEach((v) => {
if (!this.iHasObjects.find((item) => item.value === v.value)) {
data.results.forEach(v => {
if (!this.iHasObjects.find(item => item.value === v.value)) {
this.iHasObjects.push(v)
}
})
@@ -295,7 +319,7 @@ export default {
this.select2.disabledValues = this.hasObjectsId
if (this.getHasObjects) {
this.getHasObjects(this.hasObjectsId).then((data) => {
this.getHasObjects(this.hasObjectsId).then(data => {
this.iHasObjects = data
})
} else {
@@ -305,22 +329,25 @@ export default {
}
},
removeObject(obj) {
this.performDelete(obj, this).then(() => {
this.onDeleteSuccess(obj, this)
}).catch(error => {
this.onDeleteFail(error, this)
})
this.performDelete(obj, this)
.then(() => {
this.onDeleteSuccess(obj, this)
})
.catch(error => {
this.onDeleteFail(error, this)
})
},
addObjects() {
const objects = this.$refs.select2.$refs.select.selected.map(item => ({ label: item.label, value: item.value }))
const objects = this.$refs.select2.$refs.select.selected.map(item => ({
label: item.label,
value: item.value
}))
if (objects.length === 0) {
return
}
this.performAdd(objects, this).then(
() => {
this.onAddSuccess(objects, this)
}
)
this.performAdd(objects, this).then(() => {
this.onAddSuccess(objects, this)
})
},
async selectAll() {
this.selectAllDisabled = true
@@ -333,8 +360,9 @@ export default {
}
</script>
<style lang='scss' scoped>
b, strong {
<style lang="scss" scoped>
b,
strong {
font-weight: 700;
font-size: 13px;
}
@@ -346,14 +374,14 @@ tr td {
}
tr.item td {
border-top: 1px dashed #EBEEF5;
border-top: 1px dashed #ebeef5;
}
.box-margin {
margin-bottom: 20px;
}
.the-box ::v-deep .el-card__body {
.the-box :deep(.el-card__body) {
padding: 20px;
}
</style>

View File

@@ -1,10 +1,9 @@
<template>
<div :class="grouped ? 'el-button-group' : 'el-button-ungroup'" class="layout">
<template v-for="action in iActions">
<template v-for="action in iActions" :key="action.name">
<el-dropdown
v-if="action.dropdown"
v-show="action.dropdown.length > 0"
:key="action.name"
:class="[action.name, { grouped: action.grouped }]"
:size="action.size"
:split-button="!!action.split"
@@ -25,50 +24,44 @@
class="more-action"
v-bind="{ ...cleanButtonAction(action), icon: '' }"
>
<span class="pre-icon">
<Icon v-if="action.icon" :icon="action.icon" />
</span>
<Icon v-if="action.icon" :icon="action.icon" class="pre-icon" />
<span v-if="action.title">
{{ action.title }}<i class="el-icon-arrow-down el-icon--right" />
{{ action.title }}<el-icon class="el-icon--right"><arrow-down /></el-icon>
</span>
</el-button>
<el-dropdown-menu slot="dropdown" style="overflow: auto; max-height: 60vh">
<template v-for="option in action.dropdown">
<div
v-if="option.group"
:key="'group:' + option.name"
class="dropdown-menu-title"
style="width: 130px"
>
{{ option.group }}
</div>
<el-tooltip
:key="option.name"
:content="option.tip"
:disabled="!option.tip"
:open-delay="500"
placement="top"
>
<el-dropdown-item
:key="option.name"
:command="[option, action]"
:title="option.tip"
class="dropdown-item"
v-bind="{ ...option, icon: '' }"
<template #dropdown>
<el-dropdown-menu style="overflow: auto; max-height: 60vh">
<template v-for="option in action.dropdown" :key="option.name">
<div v-if="option.group" class="dropdown-menu-title" style="width: 130px">
{{ option.group }}
</div>
<el-tooltip
:content="option.tip"
:disabled="!option.tip"
:open-delay="500"
placement="top"
>
<span v-if="actionsHasIcon(action.dropdown)" class="pre-icon">
<Icon v-if="option.icon" :icon="option.icon" />
</span>
{{ option.title }}
</el-dropdown-item>
</el-tooltip>
</template>
</el-dropdown-menu>
<el-dropdown-item
:key="option.name"
:command="[option, action]"
:title="option.tip"
class="dropdown-item"
v-bind="{ ...option, icon: '' }"
>
<span v-if="actionsHasIcon(action.dropdown)" class="pre-icon">
<Icon v-if="option.icon" :icon="option.icon" />
</span>
{{ option.title }}
</el-dropdown-item>
</el-tooltip>
</template>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-else
:key="action.name"
:class="[action.name, { grouped: action.grouped }]"
:size="size"
class="action-item"
@@ -76,12 +69,12 @@
@click="handleClick(action)"
>
<el-tooltip :content="action.tip" :disabled="!action.tip" placement="top">
<span>
<span v-if="action.icon" style="vertical-align: initial">
<Icon :icon="action.icon" />
<div>
<Icon v-if="action.icon" :icon="action.icon" class="pre-icon" />
<span>
{{ action.title }}
</span>
{{ action.title }}
</span>
</div>
</el-tooltip>
</el-button>
</template>
@@ -236,9 +229,28 @@ $color-drop-menu-border: #e4e7ed;
// 通用
.layout {
// 确保所有按钮都使用 flex 布局,内容垂直居中
:deep(.el-button) {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 1;
// 确保按钮内部内容垂直居中
> span {
display: inline-flex;
align-items: center;
line-height: 1;
}
}
.action-item {
margin-left: 5px;
.pre-icon + span {
margin-left: 3px;
}
&.grouped {
margin-left: 0;
}
@@ -247,10 +259,6 @@ $color-drop-menu-border: #e4e7ed;
margin-left: 0;
}
}
.el-button.el-button--default {
color: var(--color-text-primary) !important;
}
}
// 主要是左侧 LeftSide
@@ -258,14 +266,22 @@ $color-drop-menu-border: #e4e7ed;
.action-item.el-dropdown {
font-size: 11px;
// 确保下拉按钮也垂直居中
:deep(.el-button) {
display: inline-flex;
align-items: center;
justify-content: center;
height: 30px;
}
.more-action.el-button--default {
::v-deep .el-icon-arrow-down.el-icon--right {
:deep(.el-icon-arrow-down.el-icon--right) {
color: var(--color-icon-primary) !important;
}
}
.el-button--primary {
::v-deep .el-icon-arrow-down.el-icon--right {
:deep(.el-icon-arrow-down.el-icon--right) {
color: #ffffff !important;
}
@@ -275,121 +291,4 @@ $color-drop-menu-border: #e4e7ed;
}
}
}
// 主要是 Table 中的操作列
.layout.table-actions {
display: flex;
justify-content: center;
align-items: flex-end;
.el-button {
padding: 2px 5px;
line-height: 1.3;
font-size: 13px;
&:not(.is-plain) {
color: $btn-text-color;
}
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
* {
vertical-align: baseline !important;
}
}
::v-deep .action-item.el-dropdown .el-button {
display: block;
color: var(--color-primary);
background-color: $color-btn-background;
border-color: $color-btn-focus-background;
&:focus {
color: $btn-text-color;
background-color: $color-btn-focus-background !important;
}
&:hover {
color: $btn-text-color;
background-color: $color-btn-focus-background;
}
}
}
// 下拉 options
.el-dropdown-menu {
::v-deep .more-batch-processing {
&:hover {
background-color: transparent !important;
}
&.el-dropdown-menu__item--divided {
margin-top: 0;
border-top: none;
color: var(--color-text-primary);
cursor: auto;
font-size: 12px;
line-height: 30px;
border-bottom: 1px solid $color-divided;
&:before {
height: 0;
}
}
}
.dropdown-item {
color: var(--color-text-primary);
line-height: 34px;
.pre-icon {
width: 17px;
display: inline-block;
}
::v-deep i.fa {
font-size: 13px;
height: 13px;
width: 13px;
margin-right: 0;
}
::v-deep .svg-icon {
font-size: 13px;
height: 13px;
width: 13px;
}
}
.el-dropdown-menu__item {
padding: 0 20px;
&.is-disabled {
color: var(--color-disabled);
cursor: not-allowed;
pointer-events: auto;
}
&:not(.is-disabled):hover {
background-color: var(--color-disabled-background);
}
}
.dropdown-menu-title {
text-align: left;
font-size: 12px;
color: $color-drop-menu-title;
line-height: 30px;
padding-left: 10px;
padding-top: 10px;
border-top: solid 1px $color-drop-menu-border;
&:first-child {
padding-top: 0;
border-top: none;
}
}
}
</style>

View File

@@ -0,0 +1,138 @@
<template>
<div v-if="hasError" class="error-boundary">
<div class="error-boundary-content">
<h2 class="error-title">{{ $t('ComponentError') || '组件加载出错' }}</h2>
<p class="error-message">{{ errorMessage }}</p>
<div class="error-actions">
<el-button type="primary" @click="handleRetry">{{ $t('Retry') || '重试' }}</el-button>
<el-button @click="handleGoHome">{{ $t('GoHomePage') || '返回首页' }}</el-button>
</div>
<details v-if="showDetails" class="error-details">
<summary>错误详情 (开发环境)</summary>
<pre>{{ errorDetails }}</pre>
</details>
</div>
</div>
<slot v-else />
</template>
<script>
export default {
name: 'ErrorBoundary',
data() {
return {
hasError: false,
error: null,
errorInfo: null
}
},
computed: {
errorMessage() {
if (this.error) {
return this.error.message || String(this.error)
}
return '未知错误'
},
errorDetails() {
if (!this.error) return ''
return JSON.stringify({
message: this.error.message,
stack: this.error.stack,
info: this.errorInfo
}, null, 2)
},
showDetails() {
return process.env.NODE_ENV === 'development' && this.error
}
},
errorCaptured(err, instance, info) {
// 捕获子组件错误
this.hasError = true
this.error = err
this.errorInfo = info
// 在开发环境下打印错误
if (process.env.NODE_ENV === 'development') {
console.error('ErrorBoundary caught error:', err)
console.error('Component instance:', instance)
console.error('Error info:', info)
}
// 返回 false 阻止错误继续向上传播
return false
},
methods: {
handleRetry() {
// 重置错误状态,重新渲染子组件
this.hasError = false
this.error = null
this.errorInfo = null
this.$forceUpdate()
},
handleGoHome() {
this.$router.push('/')
}
}
}
</script>
<style lang="scss" scoped>
.error-boundary {
display: flex;
justify-content: center;
align-items: center;
min-height: 400px;
padding: 20px;
}
.error-boundary-content {
text-align: center;
max-width: 600px;
}
.error-title {
font-size: 24px;
color: #f56c6c;
margin-bottom: 16px;
}
.error-message {
font-size: 16px;
color: #606266;
margin-bottom: 24px;
word-break: break-word;
}
.error-actions {
display: flex;
justify-content: center;
gap: 12px;
margin-bottom: 24px;
}
.error-details {
margin-top: 24px;
text-align: left;
summary {
cursor: pointer;
color: #909399;
margin-bottom: 8px;
&:hover {
color: #606266;
}
}
pre {
background: #f5f7fa;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
font-size: 12px;
color: #606266;
max-height: 300px;
overflow-y: auto;
}
}
</style>

View File

@@ -1,9 +1,10 @@
<template>
<el-card :class="'ibox ' + type" :shadow="shadow" v-bind="$attrs">
<template #header>
<template v-if="title" #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" />
<h5>{{ title }}</h5>
</div>
</slot>
</template>
@@ -40,53 +41,53 @@ export default {
}
</script>
<style lang='scss' scoped>
.ibox {
/*height: 100%;*/
clear: both;
padding: 0;
}
<style lang="scss" scoped>
.ibox {
/*height: 100%;*/
clear: both;
padding: 0;
}
.ibox ::v-deep .el-card__header {
border-color: #e7eaec;
border-image: none;
margin-bottom: 0;
padding: 10px 15px;
min-height: 30px;
line-height: 1.32;
font-weight: normal;
}
.ibox :deep(.el-card__header) {
border-color: #e7eaec;
border-image: none;
margin-bottom: 0;
padding: 10px 15px;
min-height: 30px;
line-height: 1.32;
font-weight: normal;
}
.ibox-title h5 {
display: inline-block;
font-size: 13px;
margin: 0;
padding: 0;
text-overflow: ellipsis;
font-weight: 500;
}
.ibox-title h5 {
display: inline-block;
font-size: 13px;
margin: 0;
padding: 0;
text-overflow: ellipsis;
font-weight: 500;
}
.ibox-tools a {
cursor: pointer;
margin-left: 5px;
color: #c4c4c4;
}
.ibox-tools a {
cursor: pointer;
margin-left: 5px;
color: #c4c4c4;
}
.ibox-tools {
display: block;
float: none;
margin-top: 0;
position: relative;
padding: 0;
text-align: right;
}
.ibox-tools {
display: block;
float: none;
margin-top: 0;
position: relative;
padding: 0;
text-align: right;
}
.fa {
font-size: 14px;
}
.fa {
font-size: 14px;
}
.ibox ::v-deep .el-card__body {
//padding: 30px 30px 20px 30px; // 这个设置会影响详情中的 quick update 和 relations
color: var(--color-icon-primary);
}
.ibox :deep(.el-card__body) {
//padding: 30px 30px 20px 30px; // 这个设置会影响详情中的 quick update 和 relations
color: var(--color-icon-primary);
}
</style>

View File

@@ -27,7 +27,7 @@ export default {
title: {
type: String,
default() {
return this.$t('QuickUpdate')
return 'QuickUpdate'
}
},
actions: {
@@ -39,11 +39,11 @@ export default {
</script>
<style scoped>
.quick-actions ::v-deep table {
.quick-actions :deep(table) {
width: 100%;
}
.quick-actions ::v-deep tr > td {
.quick-actions :deep(tr > td) {
line-height: 1.43;
padding: 8px 0;
vertical-align: top;
@@ -51,11 +51,11 @@ export default {
width: 50%;
}
.quick-actions ::v-deep tr > td > span:last-child {
.quick-actions :deep(tr > td > span:last-child) {
float: right;
}
.quick-actions ::v-deep button {
.quick-actions :deep(button) {
padding: 4px 5px;
font-size: 13px;
min-width: 65px;

View File

@@ -111,7 +111,7 @@ export default {
.ring {
padding: 26px 0 10px;
& ::v-deep .echarts {
& :deep(.echarts) {
width: 100% !important;
height: 278px !important;
}

View File

@@ -44,7 +44,7 @@ export default {
this._chartId = `chart_${Date.now()}_${Math.random().toString(36).slice(2)}`
window._echarts.total.add(this._chartId)
},
beforeDestroy() {
beforeUnmount() {
if (window._echarts) {
window._echarts.total.delete(this._chartId)
window._echarts.finished.delete(this._chartId)

View File

@@ -246,7 +246,7 @@ export default {
this._mql._handler = handler
}
},
beforeDestroy() {
beforeUnmount() {
window.removeEventListener('beforeprint', this._before)
window.removeEventListener('afterprint', this._after)
if (this._mql) {

View File

@@ -107,16 +107,16 @@ export default {
}
}
::v-deep .el-table td, .el-table th {
:deep(.el-table td, .el-table th) {
padding: 5px 0;
}
::v-deep .el-table th, .el-table tr {
:deep(.el-table th, .el-table tr) {
background-color: #F5F6F7 !important;
}
::v-deep .el-table .cell {
:deep(.el-table .cell) {
white-space: nowrap;
}
</style>

View File

@@ -1,11 +1,6 @@
<template>
<span>
<el-radio-group
v-model="select"
class="switch"
size="mini"
@change="onChange"
>
<el-radio-group v-model="select" class="switch" size="small" @change="onChange">
<el-radio-button v-for="i in iOptions" :key="i.value" :label="i.value">
{{ i.label }}
</el-radio-button>
@@ -83,7 +78,7 @@ $origin-color: #ffffff;
.switch {
font-weight: 400;
::v-deep .el-radio-button {
:deep(.el-radio-button) {
&.is-active {
.el-radio-button__inner {
border-color: var(--color-primary);
@@ -93,7 +88,7 @@ $origin-color: #ffffff;
}
}
::v-deep .el-radio-button {
:deep(.el-radio-button) {
.el-radio-button__inner {
color: var(--color-text-primary);
background: $origin-color;

View File

@@ -1,15 +1,15 @@
<template>
<Dialog
v-if="detailVisible"
v-model:visible="detailVisible"
:modal="false"
:show-cancel="false"
:show-confirm="false"
:title="title"
:visible.sync="detailVisible"
>
<div>
<div v-if="isEmpty()" style="text-align: center">
{{ this.$tc('NoContent') }}
{{ $tc('NoContent') }}
</div>
<div v-else>
<el-table
@@ -99,7 +99,7 @@ export default {
width: 100%;
max-height: 80vh;
& ::v-deep td {
& :deep(td) {
padding: 5px 0 !important;
}
}

View File

@@ -1,12 +1,13 @@
<template>
<Dialog
:show-cancel="false"
:visible="iVisible"
:visible="visible"
class="processing-dialog"
height="300"
:title="$tc('Processing')"
width="300"
@confirm="iVisible=false"
@update:visible="$emit('update:visible', $event)"
@confirm="$emit('update:visible', false)"
>
<div id="load">
<div class="spinner" />
@@ -26,25 +27,16 @@ export default {
default: false
}
},
emits: ['update:visible'],
data() {
return {}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
}
}
</script>
<style lang="scss" scoped>
.processing-dialog {
::v-deep .el-dialog__body {
:deep(.el-dialog__body) {
overflow: hidden;
}
}

View File

@@ -1,10 +1,11 @@
<template>
<Dialog
:visible="iVisible"
:visible="visible"
height="300"
title="Processing"
width="300"
class="processing-dialog"
@update:visible="$emit('update:visible', $event)"
>
<div id="load">
<div class="spinner" />
@@ -24,36 +25,27 @@ export default {
default: false
}
},
emits: ['update:visible'],
data() {
return {}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
}
}
</script>
<style lang="scss" scoped>
.processing-dialog {
::v-deep .el-dialog__body {
:deep(.el-dialog__body) {
overflow: hidden;
}
}
.spinner {
width: 100px;
height: 100px;
border: 5px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--color-primary);
animation: spin 1s infinite linear;
width: 100px;
height: 100px;
border: 5px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: var(--color-primary);
animation: spin 1s infinite linear;
}
#load {
@@ -62,8 +54,8 @@ export default {
}
@keyframes spin {
to {
transform: rotate(360deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -1,26 +1,27 @@
<template>
<div>
<Dialog
v-if="iVisible"
v-if="visible"
v-model:visible="iVisible"
:destroy-on-close="true"
:show-cancel="false"
:show-confirm="false"
:title="$tc('Report')"
:visible.sync="iVisible"
top="35vh"
width="80%"
@close="loading=true"
@close="loading = true"
>
<span v-if="loading" v-loading="loading" class="loading" />
<iframe title="dialog" :src="url" style="border: none;" @load="onIframeLoad" />
<iframe title="dialog" :src="url" style="border: none" @load="onIframeLoad" />
</Dialog>
</div>
</template>
<script>
import Dialog from '@/components/Dialog/index.vue'
import vModelMixin from '@/utils/vue/vModelMixin'
export default {
mixins: [vModelMixin('visible')],
name: 'ReportDialog',
components: {
Dialog
@@ -35,25 +36,14 @@ export default {
default: ''
}
},
emits: ['update:visible'],
data() {
return {
loading: true
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
}
},
mounted() {
},
beforeMount() {
},
mounted() {},
beforeMount() {},
methods: {
onIframeLoad() {
this.loading = false

View File

@@ -1,8 +1,8 @@
<template>
<Dialog
v-model:visible="iVisible"
:show-cancel="false"
:title="title"
:visible.sync="visible"
:close-on-click-modal="false"
width="700px"
@close="onClose"
@@ -30,12 +30,14 @@
import i18n from '@/i18n/i18n'
import { copy } from '@/utils/common/index'
import Dialog from '@/components/Dialog/index'
import vModelMixin from '@/utils/vue/vModelMixin'
export default {
name: 'Secret',
components: {
Dialog
},
mixins: [vModelMixin('visible')],
props: {
title: {
type: String,
@@ -65,11 +67,10 @@ export default {
}
}
}
</script>
<style lang='scss' scoped>
.secret {
<style lang="scss" scoped>
.secret {
color: #2b2f3a;
margin-top: 20px;
}

View File

@@ -35,7 +35,7 @@
<script>
export default {
name: 'Dialog',
name: 'DialogComponent',
props: {
title: {
type: String,
@@ -56,7 +56,7 @@ export default {
confirmTitle: {
type: String,
default() {
return this.$t('Confirm')
return 'Confirm'
}
},
showCancel: {
@@ -66,7 +66,7 @@ export default {
cancelTitle: {
type: String,
default() {
return this.$t('Cancel')
return 'Cancel'
}
},
showButtons: {
@@ -106,11 +106,11 @@ export default {
</script>
<style lang="scss" scoped>
.dialog.shadow ::v-deep .el-dialog {
.dialog.shadow :deep(.el-dialog) {
box-shadow: 1px 2px 12px 0 rgba(0, 0, 0, 0.6);
}
.dialog ::v-deep .el-dialog {
.dialog :deep(.el-dialog) {
border-radius: 0.3em;
max-width: min(100vw, 1500px);
@@ -128,14 +128,14 @@ export default {
display: none;
}
&__header {
&.dialog__header {
box-sizing: border-box;
padding: 15px 22px;
border-bottom: 1px solid #dee2e6;
font-weight: 400;
}
&__body {
&.dialog__body {
padding: 20px 30px;
font-size: 13px;
@@ -144,7 +144,7 @@ export default {
}
}
&__footer {
&.dialog__footer {
border-top: 1px solid #dee2e6;
padding: 16px 25px;
justify-content: flex-end;
@@ -152,12 +152,12 @@ export default {
}
@media (max-width: 900px) {
.dialog ::v-deep .el-dialog {
.dialog :deep(.el-dialog) {
max-width: calc(100% - 30px);
}
}
.dialog-footer ::v-deep button.el-button {
.dialog-footer :deep(button.el-button) {
font-size: 13px;
padding: 8px 12px;
}

View File

@@ -1,17 +1,18 @@
<template>
<!-- DEBUG: Drawer visible={{ visible }}, component={{ component ? 'EXISTS' : 'EMPTY' }}, title={{ title }} -->
<el-drawer
ref="drawer"
v-el-drawer-drag-width
:model-value="visible"
:append-to-body="true"
:before-close="handleClose"
:class="['drawer', { 'drawer__no-footer': !hasFooter }]"
:modal="modal"
:size="size"
:title="title"
:visible.sync="iVisible"
custom-class="drawer"
destroy-on-close
direction="rtl"
@update:model-value="handleUpdateModelValue"
v-on="$listeners"
>
<div class="drawer__content">
@@ -31,6 +32,7 @@
<script>
import { getDrawerWidth } from '@/utils/common/index'
import { useDrawerDrag } from '@/utils/vue/useDrawerDrag'
export default {
props: {
@@ -72,22 +74,64 @@ export default {
data() {
return {
loading: false,
formLabelWidth: '80px'
formLabelWidth: '80px',
drawerDrag: null
}
},
computed: {
iVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
watch: {
visible(val) {
console.debug('>>> Drawer visible watch:', val, {
component: this.component ? 'EXISTS' : 'EMPTY',
title: this.title
})
if (val) {
// 抽屉打开时,初始化拖拽功能
this.$nextTick(() => {
if (!this.drawerDrag) {
this.drawerDrag = useDrawerDrag({
storageKey: 'drawerWidth'
})
}
this.drawerDrag.start()
})
} else {
// 抽屉关闭时,清理拖拽功能
if (this.drawerDrag) {
this.drawerDrag.cleanup()
}
}
}
},
mounted() {
console.debug('>>> Drawer mounted:', {
visible: this.visible,
component: this.component ? 'EXISTS' : 'EMPTY',
title: this.title
})
if (this.visible) {
this.$nextTick(() => {
if (!this.drawerDrag) {
this.drawerDrag = useDrawerDrag({
storageKey: 'drawerWidth'
})
}
this.drawerDrag.start()
})
}
},
beforeUnmount() {
if (this.drawerDrag) {
this.drawerDrag.cleanup()
}
},
methods: {
handleUpdateModelValue(val) {
console.debug('>>> Drawer handleUpdateModelValue:', val, {
component: this.component ? 'EXISTS' : 'EMPTY',
title: this.title
})
this.$emit('update:visible', val)
},
handleClose(done) {
this.$emit('close-drawer')
done()
@@ -96,29 +140,25 @@ export default {
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.drawer__no-footer {
::v-deep {
.drawer {
:deep(.drawer){
.page {
height: calc(100vh - 55px);
}
}
}
}
@media (max-width: 992px) {
.drawer ::v-deep {
.el-form-item {
.drawer :deep(.el-form-item){
display: flex;
flex-direction: column;
gap: 0.3rem;
}
}
}
.drawer {
::v-deep {
:deep(.el-form-item){
min-width: 565px;
.el-card__body {
@@ -242,7 +282,7 @@ export default {
}
.el-drawer__header {
border-bottom: 1px solid #EBEEF5;
border-bottom: 1px solid #ebeef5;
margin-bottom: 0;
padding: 15px 20px;
font-size: 16px;
@@ -282,7 +322,8 @@ export default {
}
}
.drawer__content, .tab-page-content {
.drawer__content,
.tab-page-content {
height: 100%;
background: #f3f3f3;
}

View File

@@ -29,6 +29,7 @@
import DataForm from '../DataForm/index.vue'
import FormGroupHeader from '@/components/Form/FormGroupHeader/index.vue'
import { FormFieldGenerator } from '@/components/Form/AutoDataForm/utils'
import { UniqueCheck } from '@/components/Form/DataForm/rules'
export default {
name: 'AutoDataForm',
@@ -114,6 +115,47 @@ export default {
this.totalFields = generator.generateFields(this.fields, this.fieldsMeta, this.remoteMeta)
this.groups = generator.groups
this.$log.debug('Total fields: ', this.totalFields)
this.applyUniqueRules()
},
applyUniqueRules() {
const fields = this.totalFields || []
const currentIdGetter = () => {
return this.$route?.params?.id || this.form?.id || this.iForm?.id
}
// 移除 url 后拼接的参数
const defaultListUrl = (() => {
try {
const u = new URL(this.url, location.origin)
u.pathname = u.pathname.replace(/\/(\d+|[0-9a-fA-F-]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\/?$/, '/')
return u.origin ? u.origin + u.pathname : u.pathname
} catch (e) {
return (this.url || '').replace(/\/(\d+|[0-9a-fA-F-]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12})\/?($|\?)/, '/$2')
}
})()
fields.forEach(field => {
const conf = field?.uniqueCheck
if (!conf) return
const confObj = (typeof conf === 'object') ? conf : {}
const param = confObj.param || field.prop || field.id
const url = confObj.url || defaultListUrl
const label = confObj.label || field.label || param
const entityName = confObj.entityName || ''
if (!Array.isArray(field.rules)) field.rules = []
field.rules.push(UniqueCheck({
url,
param,
label,
entityName,
getIgnoreId: currentIdGetter,
fieldName: field.prop || field.id
}))
})
},
_cleanFormValue(form, remoteMeta) {
if (!form) {
@@ -181,8 +223,21 @@ export default {
const mapped = {}
Object.entries(errors || {}).forEach(([k, v]) => {
let msg = v
if (Array.isArray(v)) msg = v.join('; ')
else if (typeof v === 'object' && v !== null) msg = JSON.stringify(v)
console.log(k, v)
// v是数组并且数组都是字符串则拼接为字符串
if (Array.isArray(v) && v.every(item => typeof item === 'string')) msg = v.join('; ')
// 处理 [{"port":["请确保该值小于或者等于 65535。"]},{},{}] 这种情况
else if (Array.isArray(v) && v.every(item => _.isPlainObject(item))) {
const subMsg = []
v.forEach((subItem) => {
Object.values(subItem).forEach((subMsgArr) => {
if (Array.isArray(subMsgArr)) {
subMsg.push(...subMsgArr)
}
})
})
msg = subMsg.join(' ')
} else if (typeof v === 'object' && v !== null) msg = JSON.stringify(v)
mapped[k] = String(msg || '')
})
this.serverErrors = mapped

View File

@@ -1,4 +1,4 @@
import Vue from 'vue'
// Vue dependency removed; use console for debug logs
import ObjectSelect2 from '@/components/Form/FormFields/NestedObjectSelect2.vue'
import NestedField from '@/components/Form/AutoDataForm/components/NestedField.vue'
import rules from '@/components/Form/DataForm/rules'
@@ -7,6 +7,7 @@ import JsonEditor from '@/components/Form/FormFields/JsonEditor.vue'
import { assignIfNot, toSentenceCase } from '@/utils/common/index'
import TagInput from '@/components/Form/FormFields/TagInput.vue'
import i18n from '@/i18n/i18n'
import _ from 'lodash'
export class FormFieldGenerator {
constructor() {
@@ -119,7 +120,7 @@ export class FormFieldGenerator {
const f = this.generateField(name, nestedFieldsMeta, nestedFieldsRemoteMeta)
fields.push(f)
}
Vue.$log.debug('NestFields: ', fields)
console.debug('NestFields: ', fields)
return fields
}
@@ -242,7 +243,7 @@ export class FormFieldGenerator {
field = this.setPlaceholder(field, remoteFieldMeta)
field = this.afterGenerateField(field)
_.set(field, 'attrs.error', '')
Vue.$log.debug('Generate field: ', name, field)
console.debug('Generate field: ', name, field)
return field
}
@@ -255,7 +256,7 @@ export class FormFieldGenerator {
return field
}
if (field.type === 'select' || [ObjectSelect2].indexOf(field.component) > -1) {
field.el.placeholder = i18n.t('PleaseSelect') + label.toLowerCase()
field.el.placeholder = i18n.global.t('PleaseSelect') + label.toLowerCase()
} else if (field.type === 'input') {
field.el.placeholder = field.label
}

View File

@@ -1,10 +1,9 @@
<template>
<div>
<template v-for="(item, index) in data.items">
<template v-for="(item, index) in data.items" :key="item.id || index">
<slot :name="`id:${item.id}`" />
<slot :name="`$id:${item.id}`" />
<render-form-item
:key="index"
:prop="`${data.id}.${item.id}`"
:data="item"
:value="value"
@@ -12,7 +11,7 @@
:disabled="disabled"
:readonly="readonly"
:options="options[item.id]"
@updateValue="updateValue"
@update-value="updateValue"
/>
</template>
</div>

View File

@@ -20,7 +20,8 @@
placement="right"
popper-class="help-tips"
>
<div slot="content" v-sanitize="data.helpTip" class="help-tip-content" /> <!-- Noncompliant -->
<div slot="content" v-sanitize="data.helpTip" class="help-tip-content" />
<!-- Noncompliant -->
<i class="fa fa-question-circle-o help-tip-icon" />
</el-tooltip>
</span>
@@ -28,18 +29,12 @@
<template v-if="readonly && hasReadonlyContent">
<div
v-if="data.type === 'input'"
:style="
componentProps.type === 'textarea'
? {padding: '10px 0', lineHeight: 1.5}
: ''
"
:style="componentProps.type === 'textarea' ? { padding: '10px 0', lineHeight: 1.5 } : ''"
>
{{ itemValue }}
</div>
<div v-else-if="data.type === 'select'">
<template>
{{ multipleValue }}
</template>
{{ multipleValue }}
</div>
</template>
<custom-component
@@ -50,15 +45,10 @@
v-bind="componentProps"
v-on="listeners"
>
<template v-for="opt in options">
<el-option
v-if="data.type === 'select'"
:key="opt.label"
v-bind="opt"
/>
<template v-for="opt in options" :key="opt.value">
<el-option v-if="data.type === 'select'" v-bind="opt" />
<el-checkbox-button
v-else-if="data.type === 'checkbox-group' && data.style === 'button'"
:key="opt.value"
:label="'value' in opt ? opt.value : opt.label"
v-bind="opt"
>
@@ -67,7 +57,6 @@
<el-checkbox
v-else-if="data.type === 'checkbox-group' && data.style !== 'button'"
:key="opt.value"
:label="'value' in opt ? opt.value : opt.label"
v-bind="opt"
>
@@ -81,7 +70,6 @@
<!-- FYI: radio value 属性可以在没有 radio-group 时用来关联到同一个 v-model -->
<el-radio
v-else-if="data.type === 'radio-group'"
:key="opt.label"
:label="'value' in opt ? opt.value : opt.label"
v-bind="opt"
>
@@ -112,10 +100,10 @@
<script>
import getEnableWhenStatus from '../util/enable-when'
import { noop } from '../util/utils'
import _get from 'lodash.get'
import _includes from 'lodash.includes'
import _topairs from 'lodash.topairs'
import _frompairs from 'lodash.frompairs'
import _get from 'lodash/get'
import _includes from 'lodash/includes'
import _topairs from 'lodash/toPairs'
import _frompairs from 'lodash/fromPairs'
function validator(data) {
if (!data) {
@@ -175,7 +163,8 @@ export default {
data() {
return {
propsInner: {},
isBlurTrigger: this.data.rules &&
isBlurTrigger:
this.data.rules &&
this.data.rules.some(rule => {
return rule.required && rule.trigger === 'blur'
})
@@ -214,10 +203,7 @@ export default {
} = this
return {
..._frompairs(
_topairs(on).map(([eName, handler]) => [
eName,
(...args) => handler(args, updateForm)
]),
_topairs(on).map(([eName, handler]) => [eName, (...args) => handler(args, updateForm)])
),
// 手动更新表单数据
input: (value, ...rest) => {
@@ -242,9 +228,7 @@ export default {
multipleValue: ({ data, itemValue, options = [] }) => {
const multipleSelectValue =
_get(data, 'el.multiple') && Array.isArray(itemValue)
? itemValue
: [itemValue]
_get(data, 'el.multiple') && Array.isArray(itemValue) ? itemValue : [itemValue]
return multipleSelectValue
.map(val => (options.find(op => op.value === val) || {}).label)
.join()
@@ -264,8 +248,7 @@ export default {
if (v.url === oldV.url || v.request === oldV.request) return
}
const isOptionsCase =
['select', 'checkbox-group', 'radio-group'].indexOf(this.data.type) >
-1
['select', 'checkbox-group', 'radio-group'].indexOf(this.data.type) > -1
const {
url,
request = () => this.$axios.get(url).then(resp => resp.data),
@@ -333,7 +316,7 @@ export default {
}
}
</script>
<style lang='scss' scoped>
<style lang="scss" scoped>
.help-tips {
opacity: 0.8;
line-height: 2;
@@ -341,8 +324,8 @@ export default {
}
.help-block {
::v-deep .el-alert__icon {
font-size: 16px
:deep(.el-alert__icon) {
font-size: 16px;
}
&.checkbox {

View File

@@ -1,10 +1,15 @@
<template>
<el-form ref="elForm" :model="value" class="el-form-renderer" v-bind="$attrs" @submit.native.prevent>
<template v-for="item in innerContent">
<el-form
ref="elForm"
:model="value"
class="el-form-renderer"
v-bind="$attrs"
@submit.native.prevent
>
<template v-for="item in innerContent" :key="item.id">
<slot v-if="!isHidden(item)" :name="`id:${item.id}`" />
<component
:is="item.type === GROUP ? 'render-form-group' : 'render-form-item'"
:key="item.id"
:data="item"
:server-errors="serverErrors"
:disabled="disabled || item.disabled"
@@ -12,7 +17,7 @@
:options="options[item.id]"
:readonly="readonly || item.readonly"
:value="value"
@updateValue="updateValue"
@update-value="updateValue"
/>
<slot v-if="!isHidden(item)" :name="`$id:${item.id}`" />
</template>
@@ -20,13 +25,19 @@
</el-form>
</template>
<script>
import _set from 'lodash.set'
import _isequal from 'lodash.isequal'
import _clonedeep from 'lodash.clonedeep'
import _set from 'lodash/set'
import _isequal from 'lodash/isEqual'
import _clonedeep from 'lodash/cloneDeep'
import RenderFormGroup from './components/render-form-group.vue'
import RenderFormItem from './components/render-form-item.vue'
import transformContent from './util/transform-content'
import { collect, correctValue, mergeValue, transformInputValue, transformOutputValue } from './util/utils'
import {
collect,
correctValue,
mergeValue,
transformInputValue,
transformOutputValue
} from './util/utils'
const GROUP = 'group'
@@ -120,7 +131,9 @@ export default {
this.initValue = _clonedeep(this.value)
this.$nextTick(() => {
// proxy
Object.keys(this.$refs.elForm.$options.methods).forEach(item => {
const methods = this.$refs.elForm.$options.methods || {}
Object.keys(methods).forEach(item => {
if (item in this) return
this[item] = this.$refs.elForm[item]
})
@@ -213,6 +226,11 @@ export default {
return item.hidden(this.value)
}
return false
},
clearValidate() {
if (this.$refs.elForm) {
this.$refs.elForm.clearValidate()
}
}
}
}

View File

@@ -1,5 +1,5 @@
import _get from 'lodash.get'
import _has from 'lodash.has'
import _get from 'lodash/get'
import _has from 'lodash/has'
/**
* 处理 enableWhen
@@ -20,7 +20,5 @@ export default function getEnableWhenStatus(enableWhen, value) {
})
}
return Array.isArray(enableWhen)
? enableWhen.some(handleCondition)
: handleCondition(enableWhen)
return Array.isArray(enableWhen) ? enableWhen.some(handleCondition) : handleCondition(enableWhen)
}

View File

@@ -1,5 +1,5 @@
/* eslint-disable no-sequences */
import _kebabcase from 'lodash.kebabcase'
import _ from 'lodash'
/**
* content 的每一项会浅拷贝一层
* 只可以在 item 层新增修改属性,如 item.a = b
@@ -13,7 +13,7 @@ export default function transformContent(content) {
removeDollarInKey(item)
extractRulesFromComponent(item)
// 有些旧写法是 checkboxGroup & radioGroup
item.type = _kebabcase(item.type)
item.type = _.kebabCase(item.type)
}
return item
@@ -34,8 +34,5 @@ export function extractRulesFromComponent(item) {
if (!component || typeof component === 'string') return
const { rules = [] } = component
item.rules = [
...(item.rules || []),
...(typeof rules === 'function' ? rules(item) : rules)
]
item.rules = [...(item.rules || []), ...(typeof rules === 'function' ? rules(item) : rules)]
}

View File

@@ -1,5 +1,5 @@
import _frompairs from 'lodash.frompairs'
import _isplainobject from 'lodash.isplainobject'
import _frompairs from 'lodash/fromPairs'
import _isplainobject from 'lodash/isPlainObject'
export function noop() {}
@@ -12,11 +12,9 @@ export function collect(content, key) {
value: item.type === 'group' ? collect(item.items, key) : item[key]
}))
.filter(
({ type, value }) =>
value !== undefined ||
(type === 'group' && Object.keys(value).length),
({ type, value }) => value !== undefined || (type === 'group' && Object.keys(value).length)
)
.map(({ id, value }) => [id, value]),
.map(({ id, value }) => [id, value])
)
}

View File

@@ -12,19 +12,10 @@
:style="{ '--label-width': labelWidth }"
v-bind="$attrs"
:server-errors="serverErrors"
v-on="$listeners"
>
<!-- slot 透传 -->
<slot
v-for="item in fields"
:slot="`id:${item.id}`"
:name="`id:${item.id}`"
/>
<slot
v-for="item in fields"
:slot="`$id:${item.id}`"
:name="`$id:${item.id}`"
/>
<slot v-for="item in fields" :slot="`id:${item.id}`" :name="`id:${item.id}`" />
<slot v-for="item in fields" :slot="`$id:${item.id}`" :name="`$id:${item.id}`" />
<div v-if="hasButtons" class="form-buttons">
<el-button
@@ -43,15 +34,11 @@
size="small"
@click="submitForm('form', true)"
>
{{ $t("SaveAndAddAnother") }}
{{ $t('SaveAndAddAnother') }}
</el-button>
<el-button
v-if="defaultButton && hasReset"
size="small"
@click="resetForm('form')"
>
{{ $t("Reset") }}
<el-button v-if="defaultButton && hasReset" size="small" @click="resetForm('form')">
{{ $t('Reset') }}
</el-button>
<el-button
@@ -75,15 +62,23 @@ import ElFormRender from './components/el-form-renderer'
import { randomString } from '@/utils/common/index'
const scrollToError = (
el,
formInstance,
scrollOption = {
behavior: 'smooth',
block: 'center'
}
) => {
setTimeout(() => {
const isError = el.getElementsByClassName('is-error')
isError[0].scrollIntoView(scrollOption)
// formInstance 是 ElFormRender 组件实例,需要访问内部的 el-form 元素
const elForm = formInstance.$refs?.elForm
if (!elForm || !elForm.$el) {
return
}
const formEl = elForm.$el
const errorElements = formEl.getElementsByClassName('is-error')
if (errorElements && errorElements.length > 0) {
errorElements[0].scrollIntoView(scrollOption)
}
}, 0)
}
@@ -91,6 +86,7 @@ export default {
components: {
ElFormRender
},
inheritAttrs: true,
props: {
defaultButton: {
type: Boolean,
@@ -110,7 +106,7 @@ export default {
},
submitBtnSize: {
type: String,
default: 'small'
default: 'default'
},
submitBtnText: {
type: String,
@@ -149,9 +145,10 @@ export default {
},
labelWidth: {
type: String,
default: '25%'
default: '18.2%'
}
},
emits: ['submit', 'invalid'],
data() {
return {
basicForm: this.form,
@@ -203,17 +200,16 @@ export default {
* @param {string} formName - 表单的引用名称
* @param {boolean} [addContinue] - 是否继续添加
*/
submitForm(formName, addContinue) {
async submitForm(formName, addContinue) {
const form = this.$refs[formName]
form.validate(valid => {
if (valid) {
this.$emit('submit', form.getFormValue(), form, addContinue)
} else {
this.$emit('invalid', valid)
scrollToError(form.$el)
return false
}
})
try {
await form.validate()
this.$emit('submit', form.getFormValue(), form, addContinue)
} catch (error) {
this.$emit('invalid', false)
scrollToError(form)
return false
}
},
// 重置表单
resetForm() {
@@ -222,7 +218,7 @@ export default {
handleClick(button) {
const callback =
button.callback ||
function(values, form) {
function (values, form) {
// debug('Click ', button.title, ': ', values)
}
const form = this.$refs['form']
@@ -244,24 +240,28 @@ export default {
margin-right: 80px;
margin-bottom: 20px;
::v-deep .el-input-group__prepend {
.el-form {
margin-right: 0;
}
:deep(.el-input-group__prepend) {
border-radius: 0;
}
::v-deep .form-group-header {
:deep(.form-group-header) {
margin-left: 50px;
color: var(--color-text-primary);
}
&.label-top {
::v-deep .el-form-item {
:deep(.el-form-item) {
.el-form-item__content {
width: 100%;
}
}
}
::v-deep .el-form-item {
:deep(.el-form-item) {
margin-bottom: 10px;
.el-form-item__label {
@@ -360,17 +360,17 @@ export default {
}
}
::v-deep .form-buttons {
:deep(.form-buttons) {
margin-top: 30px;
margin-left: var(--label-width);
}
}
.mobile.el-form ::v-deep .el-form-item__content {
.mobile.el-form :deep(.el-form-item__content) {
width: 100%;
}
.el-form.mobile ::v-deep .form-group-header {
.el-form.mobile :deep(.form-group-header) {
margin-left: 0;
}
</style>

View File

@@ -1,4 +1,5 @@
import i18n from '@/i18n/i18n'
import request from '@/utils/request'
export const Required = {
required: true, message: i18n.t('FieldRequiredError'), trigger: 'blur'
@@ -118,3 +119,69 @@ export default {
matchAlphanumericUnderscore,
MatchExcludeParenthesis
}
/**
* @description 表单唯一性校验
*
* @param {Object} options
* @param {string} 列表查询地址
* @param {string} 查询参数名
* @param {string} 字段中文名
* @param {string} 字段名
* @param {function(): (string|number)} 返回更新场景下的当前对象 id
*/
export function UniqueCheck(options = {}) {
const { url, param, label, fieldName, getIgnoreId } = options
function existsInResponse(res) {
if (Array.isArray(res)) return res.length > 0
if (res && typeof res === 'object') {
if (typeof res.count === 'number') return res.count > 0
if (Array.isArray(res.results)) return res.results.length > 0
}
return !!res
}
function extractIds(res) {
if (Array.isArray(res)) return res.map(i => i?.id).filter(Boolean)
if (res && Array.isArray(res.results)) return res.results.map(i => i?.id).filter(Boolean)
return []
}
return {
async validator(rule, value, callback) {
try {
let v = value
if (typeof v === 'string') v = v.trim()
if (v === '' || v === undefined || v === null) return callback()
if (!url || !param) return callback()
const res = await request.get(url, { params: { [param]: v } })
let duplicated = existsInResponse(res)
if (duplicated && typeof getIgnoreId === 'function') {
const curId = getIgnoreId()
if (curId) {
const ids = extractIds(res)
// 查询结果只包含自身,因此不被视为重复
if (ids.length >= 1 && ids.every(id => id === curId)) {
duplicated = false
}
}
}
if (duplicated) {
const _label = label || fieldName || ''
const msg = `${_label}${i18n.t('Existing')}`
callback(new Error(msg))
} else {
callback()
}
} catch (e) {
callback()
}
},
trigger: ['blur']
}
}

View File

@@ -49,19 +49,18 @@ export default {
<style lang="scss" scoped>
::v-deep .el-select {
:deep(.el-select) {
width: 100%;
}
::v-deep .el-form-item__content {
:deep(.el-form-item__content) {
width: 100% !important;
}
::v-deep .form-buttons {
:deep(.form-buttons) {
margin: 0 !important;
}
.attr-input {
}
</style>

View File

@@ -13,7 +13,7 @@
/>
</template>
<script>
<script lang="jsx">
export default {
props: {
value: {
@@ -111,7 +111,7 @@ export default {
<style lang="scss" scoped>
.el-tree-custom ::v-deep {
.el-tree-custom :deep(.el-tree){
.help-tips {
margin-left: 10px;
font-size: 12px;

View File

@@ -17,13 +17,13 @@ export default {
trueText: {
type: String,
default: function() {
return this.$t('Yes')
return 'Yes'
}
},
falseText: {
type: String,
default: function() {
return this.$t('No')
return 'No'
}
},
trueIcon: {

View File

@@ -8,14 +8,13 @@
:label="item.name"
:prop="item.name"
>
<template v-if="item.type === 'button' && !item.isVisible">
<el-tooltip :disabled="!item.tip" :content="item.tip">
<el-button
:type="item.el && item.el.type"
class="start-stop-btn"
:disabled="item.disabled"
size="mini"
size="small"
@click="item.callback()"
>
<i :class="item.icon" />
@@ -32,7 +31,7 @@
:fetch-suggestions="item.el.query"
:placeholder="item.placeholder"
class="inline-input"
size="mini"
size="small"
clearable
@change="handleInputChange(item)"
@select="handleInputChange(item)"
@@ -47,7 +46,7 @@
:class="!isFold ? 'special-style' : ''"
:placeholder="item.placeholder"
class="inline-input"
size="mini"
size="small"
@change="item.callback(formModel[item.name])"
/>
</el-tooltip>
@@ -66,10 +65,10 @@
:placeholder="item.name"
class="autoWidth-select"
default-first-option
size="mini"
size="small"
@change="item.callback(item.value)"
>
<template slot="prefix">{{ item.label + ':' + item.value }}</template>
<template #prefix>{{ item.label + ':' + item.value }}</template>
<el-option
v-for="(option, id) in item.options"
:key="id"
@@ -86,12 +85,14 @@
<el-dropdown
class="select-dropdown"
trigger="click"
@command="(command) => {
item.value = command
item.callback(command)
}"
@command="
command => {
item.value = command
item.callback(command)
}
"
>
<el-button size="mini" type="primary">
<el-button size="small" type="primary">
<div class="text-content">
<span class="content">
{{ getLabel(item.value, item.options) }}
@@ -99,15 +100,17 @@
</span>
</div>
</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>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="(option, i) in item.options"
:key="i"
:command="option.value"
>
{{ option.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</template>
@@ -124,7 +127,7 @@
</template>
</el-form-item>
<div
v-if="toolbar.hasOwnProperty('fold')"
v-if="Object.prototype.hasOwnProperty.call(toolbar, 'fold')"
:class="!isFold ? 'sepcial-icon' : ''"
class="fold"
>
@@ -143,12 +146,12 @@
<el-button
v-if="item.type === 'button'"
:disabled="item.disabled"
size="mini"
size="small"
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;" />
<svg-icon v-else :icon-class="item.icon" style="font-size: 14px" />
</el-button>
</el-tooltip>
</div>
@@ -216,7 +219,7 @@ export default {
const actions = Object.values(actionsObj)
actions.forEach(action => {
if (!this.formModel.hasOwnProperty(action.name)) {
if (!Object.prototype.hasOwnProperty.call(this.formModel, action.name)) {
this.$set(this.formModel, action.name, action.value || '')
}
})
@@ -234,7 +237,9 @@ export default {
Object.values(actionsObj).forEach(action => {
if (action.name === this.$t('RunAs') && action.type === 'input') {
rules[action.name] = [{ required: true, message: this.$t('RequiredRunas'), trigger: 'blur' }]
rules[action.name] = [
{ required: true, message: this.$t('RequiredRunas'), trigger: 'blur' }
]
}
})
@@ -281,8 +286,8 @@ export default {
</script>
<style lang="scss" scoped>
$header-bg-color: #F5F6F7;
$input-border-color: #C0C4CC;
$header-bg-color: #f5f6f7;
$input-border-color: #c0c4cc;
.code-editor {
display: flex;
@@ -307,7 +312,7 @@ $input-border-color: #C0C4CC;
margin-right: 5px;
// input 框与 label 相关样式
::v-deep .el-form-item__label {
:deep(.el-form-item__label) {
display: flex;
justify-items: flex-start;
align-items: center;
@@ -318,20 +323,20 @@ $input-border-color: #C0C4CC;
font-size: 11px;
}
::v-deep .el-form-item__content .inline-input .el-input__inner {
:deep(.el-form-item__content .inline-input .el-input__inner) {
//width: 130px;
min-width: 130px;
}
// 执行、暂停按钮
::v-deep .el-form-item__content .start-stop-btn {
:deep(.el-form-item__content .start-stop-btn) {
display: flex;
align-items: center;
height: 28px;
margin-bottom: 1.5px;
}
::v-deep .el-form-item__content .select-dropdown .el-button {
:deep(.el-form-item__content) .select-dropdown .el-button {
width: 125px;
background-color: $header-bg-color;
border-color: $input-border-color;
@@ -349,7 +354,6 @@ $input-border-color: #C0C4CC;
display: flex;
justify-content: space-between;
}
;
}
}
@@ -404,18 +408,16 @@ $input-border-color: #C0C4CC;
}
}
::v-deep .CodeMirror pre.CodeMirror-line,
::v-deep .CodeMirror-linenumber.CodeMirror-gutter-elt {
:deep(.CodeMirror) pre.CodeMirror-line,
:deep(.CodeMirror-linenumber.CodeMirror-gutter-elt) {
line-height: 18px !important;
}
.runas-input {
height: 28px;
::v-deep {
.el-select {
width: 100px;
}
:deep(.el-select) {
width: 100px;
}
}
@@ -423,7 +425,7 @@ $input-border-color: #C0C4CC;
min-width: 100px;
}
.autoWidth-select ::v-deep .el-input__prefix {
.autoWidth-select :deep(.el-input__prefix) {
position: relative;
left: 0;
box-sizing: border-box;
@@ -432,7 +434,7 @@ $input-border-color: #C0C4CC;
visibility: hidden;
}
.autoWidth-select ::v-deep input {
.autoWidth-select :deep(input) {
position: absolute;
padding-left: 0px;
border: none;
@@ -443,7 +445,7 @@ $input-border-color: #C0C4CC;
line-height: 27px;
}
::v-deep .el-select {
:deep(.el-select) {
top: -1px;
.el-input .el-select__caret {
@@ -464,7 +466,7 @@ $input-border-color: #C0C4CC;
line-height: 28px;
padding-left: 15px;
font-size: 0;
border: 1px solid #DCDFE6;
border: 1px solid #dcdfe6;
border-radius: 4px;
background-color: #e6e6e6;
}

View File

@@ -50,7 +50,7 @@
<div class="popup-main">
<div class="popup-result">
<p class="title">{{ this.$t('TimeExpression') }}</p>
<p class="title">{{ $t('TimeExpression') }}</p>
<table>
<thead>
<th v-for="item of tabTitles" :key="item" width="40">{{ item }}</th>
@@ -62,40 +62,40 @@
max="5"
min="0"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
size="mini"
size="small"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.hour"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
size="mini"
size="small"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.day"
onkeyup="value=value.replace(/[^\0-9\\-\*\,]/g,'')"
size="mini"
size="small"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.month"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
size="mini"
size="small"
/>
</td>
<td>
<el-input
v-model.trim="contabValueObj.week"
onkeyup="value=value.replace(/[^\0-9\-\*\,]/g,'')"
size="mini"
size="small"
/>
</td>
</tbody>
</table>
<CrontabResult :ex="contabValueString" @crontabDiffChange="crontabDiffChangeHandle" />
<CrontabResult :ex="contabValueString" @crontab-diff-change="crontabDiffChangeHandle" />
</div>
<div class="pop_btn">
@@ -103,14 +103,14 @@
size="small"
@click="clearCron"
>
{{ this.$t('Reset') }}
{{ $t('Reset') }}
</el-button>
<el-button
size="small"
type="primary"
@click="submitFill"
>
{{ this.$t('Confirm') }}
{{ $t('Confirm') }}
</el-button>
</div>
</div>
@@ -457,9 +457,9 @@ export default {
overflow-y: auto;
}
::v-deep {
.el-form-item--mini.el-form-item,
.el-form-item--small.el-form-item {
:deep(.el-form-item){
&.el-form-item--small,
&.el-form-item--small {
margin-bottom: 5px;
}
@@ -477,7 +477,7 @@ export default {
border: 1px solid #dcdfe6;
}
::v-deep .el-tabs__header {
:deep(.el-tabs__header) {
background-color: white;
margin-top: -10px;
padding: 0 30px;
@@ -499,11 +499,11 @@ export default {
}
.tab-page {
::v-deep .page-heading {
:deep(.page-heading) {
border-bottom: none;
}
::v-deep .page-content {
:deep(.page-content) {
overflow-y: hidden;
padding: 0;
}
@@ -520,7 +520,7 @@ export default {
}
}
::v-deep .el-tabs__nav-wrap {
:deep(.el-tabs__nav-wrap) {
position: static;
}
</style>

View File

@@ -3,28 +3,28 @@
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
{{ this.$t('Day') }}{{ this.$t('WildcardsAllowed') }}[, - * /]
{{ $t('Day') }}{{ $t('WildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('From') }}
<el-input-number v-model="cycle01" :max="31" :min="0" size="mini" /> -
<el-input-number v-model="cycle02" :max="31" :min="0" size="mini" /> {{ this.$t('Day') }}
{{ $t('From') }}
<el-input-number v-model="cycle01" :max="31" :min="0" size="small" /> -
<el-input-number v-model="cycle02" :max="31" :min="0" size="small" /> {{ $t('Day') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('Every') }}
<el-input-number v-model="average02" :max="31" :min="1" size="mini" /> {{ this.$t('Day') }} {{ this.$t('ExecuteOnce') }}
{{ $t('Every') }}
<el-input-number v-model="average02" :max="31" :min="1" size="small" /> {{ $t('Day') }} {{ $t('ExecuteOnce') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="7">
{{ this.$t('Appoint') }}
{{ $t('Appoint') }}
<el-select
v-model="checkboxList"
:placeholder="$tc('ManyChoose')"

View File

@@ -3,31 +3,31 @@
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
{{ this.$t('Hour') }}{{ this.$t('WildcardsAllowed') }}[, - * /]
{{ $t('Hour') }}{{ $t('WildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('From') }}
<el-input-number v-model="cycle01" :max="23" :min="0" size="mini" />
{{ $t('From') }}
<el-input-number v-model="cycle01" :max="23" :min="0" size="small" />
-
<el-input-number v-model="cycle02" :max="23" :min="0" size="mini" />
{{ this.$t('Hour') }}
<el-input-number v-model="cycle02" :max="23" :min="0" size="small" />
{{ $t('Hour') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('Every') }}
<el-input-number v-model="average02" :max="23" :min="1" size="mini" />
{{ this.$t('Hour') }} {{ this.$t('ExecuteOnce') }}
{{ $t('Every') }}
<el-input-number v-model="average02" :max="23" :min="1" size="small" />
{{ $t('Hour') }} {{ $t('ExecuteOnce') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('Appoint') }}
{{ $t('Appoint') }}
<el-select
v-model="checkboxList"
:placeholder="$tc('ManyChoose')"

View File

@@ -2,21 +2,21 @@
<template>
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1" size="mini">
{{ this.$t('Min') }}{{ this.$t('WildcardsAllowed') }}[, - * /]
<el-radio v-model="radioValue" :label="1" size="small">
{{ $t('Min') }}{{ $t('WildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('From') }}
<el-input-number v-model="average02" :max="59" :min="1" size="mini" />
{{ this.$t('Min') }} {{ this.$t('ExecuteOnce') }}
{{ $t('From') }}
<el-input-number v-model="average02" :max="59" :min="1" size="small" />
{{ $t('Min') }} {{ $t('ExecuteOnce') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('Appoint') }}
{{ $t('Appoint') }}
<el-select
v-model="checkboxList"
:placeholder="$tc('ManyChoose')"

View File

@@ -3,28 +3,28 @@
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
{{ this.$t('Month') }}{{ this.$t('WildcardsAllowed') }}[, - * /]
{{ $t('Month') }}{{ $t('WildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="2">
{{ this.$t('From') }}
<el-input-number v-model="cycle01" :max="12" :min="1" size="mini" /> -
<el-input-number v-model="cycle02" :max="12" :min="1" size="mini" /> {{ this.$t('Month') }}
{{ $t('From') }}
<el-input-number v-model="cycle01" :max="12" :min="1" size="small" /> -
<el-input-number v-model="cycle02" :max="12" :min="1" size="small" /> {{ $t('Month') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('Every') }}
<el-input-number v-model="average02" :max="12" :min="1" size="mini" /> {{ this.$t('Month') }} {{ this.$t('ExecuteOnce') }}
{{ $t('Every') }}
<el-input-number v-model="average02" :max="12" :min="1" size="small" /> {{ $t('Month') }} {{ $t('ExecuteOnce') }}
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="4">
{{ this.$t('Appoint') }}
{{ $t('Appoint') }}
<el-select
v-model="checkboxList"
:placeholder="$tc('ManyChoose')"

View File

@@ -1,12 +1,12 @@
/* eslint-disable */
<template>
<div class="popup-result-time">
<p class="title">{{ this.$t('RunningTimes') }}</p>
<p class="title">{{ $t('RunningTimes') }}</p>
<ul class="popup-result-scroll">
<template v-if="isShow">
<li v-for="item in resultList" :key="item">{{ item }}</li>
</template>
<li v-else>{{ this.$t('CalculationResults') }}</li>
<li v-else>{{ $t('CalculationResults') }}</li>
</ul>
</div>
</template>

View File

@@ -3,21 +3,21 @@
<el-form size="small">
<el-form-item>
<el-radio v-model="radioValue" :label="1">
{{ this.$t('Week') }}{{ this.$t('WildcardsAllowed') }}[, - * /]
{{ $t('Week') }}{{ $t('WildcardsAllowed') }}[, - * /]
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="3">
{{ this.$t('CycleFromWeek') }}
<el-input-number v-model="cycle01" :max="7" :min="1" size="mini" /> -
<el-input-number v-model="cycle02" :max="7" :min="1" size="mini" />
{{ $t('CycleFromWeek') }}
<el-input-number v-model="cycle01" :max="7" :min="1" size="small" /> -
<el-input-number v-model="cycle02" :max="7" :min="1" size="small" />
</el-radio>
</el-form-item>
<el-form-item>
<el-radio v-model="radioValue" :label="6">
{{ this.$t('Appoint') }}
{{ $t('Appoint') }}
<el-select
v-model="checkboxList"
:placeholder="$tc('ManyChoose')"

View File

@@ -4,9 +4,9 @@
<el-input v-model="input" clearable @clear="onClear" @focus="showDialog" />
</div>
<Dialog
v-model:visible="showCron"
:show-buttons="false"
:title="$tc('NewCron')"
:visible.sync="showCron"
append-to-body
top="8vh"
width="650px"

View File

@@ -102,7 +102,7 @@ export default {
<style lang='scss' scoped>
html:lang(pt-br) {
.datepicker ::v-deep .el-range-separator {
.datepicker :deep(.el-range-separator) {
padding: 0 10px;
}
}
@@ -117,17 +117,17 @@ html:lang(pt-br) {
border-radius: 2px;
height: 28px;
::v-deep .el-range-separator,
::v-deep .el-input__icon {
:deep(.el-range-separator),
:deep(.el-input__icon) {
line-height: 26px;
color: var(--color-icon-primary) !important;
}
::v-deep .el-range-input {
:deep(.el-range-input) {
color: var(--color-text-primary) !important;
}
::v-deep .el-range-input {
:deep(.el-range-input) {
width: 49%;
}
}

View File

@@ -1,23 +1,28 @@
<template>
<div>
<div v-for="(command, index) in iValue" :key="index" :prop="'iValue.' + index + '.value'" class="command-item">
<el-input v-model="iValue[index]" size="mini">
<template slot="prepend"> {{ inputTitle + ' ' + (index + 1) }}</template>
<div
v-for="(command, index) in iValue"
:key="index"
:prop="'iValue.' + index + '.value'"
class="command-item"
>
<el-input v-model="iValue[index]" size="small">
<template #prepend> {{ inputTitle + ' ' + (index + 1) }}</template>
</el-input>
<div class="input-button">
<el-button
:disabled="deleteDisabled()"
icon="el-icon-minus"
size="mini"
style="flex-shrink: 0;"
size="small"
style="flex-shrink: 0"
type="danger"
@click="handleDelete(command)"
/>
<el-button
v-if="index === iValue.length - 1"
icon="el-icon-plus"
size="mini"
style="flex-shrink: 0;"
size="small"
style="flex-shrink: 0"
type="primary"
@click="handleAdd()"
/>
@@ -27,7 +32,6 @@
</template>
<script>
export default {
props: {
value: {
@@ -82,9 +86,9 @@ export default {
.input-button {
margin-top: 2px;
display: flex;
margin-left: 20px
margin-left: 20px;
}
.input-button ::v-deep .el-button.el-button--mini {
.input-button :deep(.el-button.el-button--small) {
height: 25px;
padding: 5px;
}

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