refactor: ui

master
xiaojunnuo 2021-01-28 01:00:06 +08:00
parent fd130f86fd
commit 86b1e9959b
37 changed files with 17415 additions and 7 deletions

11
.gitignore vendored
View File

@ -5,8 +5,11 @@ out
gen gen
node_modules/ node_modules/
/test/*.private.* /test/*.private.*
/other
/other/node-acme-client/.idea/
/*.log /*.log
/other/certd-run
/other/node-acme-client /other/*/.idea
/other/*/node_modules
/packages/*/node_modules

1
other/certd-server Submodule

@ -0,0 +1 @@
Subproject commit 08b48c522b5a35f6c58130e7a43f525be0ac94df

View File

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

View File

@ -0,0 +1,5 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -0,0 +1,28 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/vue3-essential',
'@vue/standard'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/unit/**/*.spec.{j,t}s?(x)'
],
env: {
mocha: true
}
}
]
}

23
other/certd-ui/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

29
other/certd-ui/README.md Normal file
View File

@ -0,0 +1,29 @@
# certd-ui
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Run your unit tests
```
npm run test:unit
```
### Lints and fixes files
```
npm run lint
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

View File

@ -0,0 +1,6 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: ['@babel/plugin-proposal-optional-chaining']
}

15403
other/certd-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,52 @@
{
"name": "certd-ui",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit",
"lint": "vue-cli-service lint"
},
"dependencies": {
"@babel/plugin-proposal-optional-chaining": "^7.12.7",
"@certd/plugins": "^0.1.11",
"@certd/providers": "^0.1.11",
"ant-design-vue": "^2.0.0-rc.8",
"core-js": "^3.6.5",
"lodash-es": "^4.17.20",
"vue": "^3.0.0",
"vue-i18n": "^9.0.0-rc.2",
"vue-router": "^4.0.0-0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-plugin-eslint": "~4.5.0",
"@vue/cli-plugin-router": "~4.5.0",
"@vue/cli-plugin-unit-mocha": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/test-utils": "^2.0.0-0",
"babel-eslint": "^10.1.0",
"chai": "^4.1.2",
"eslint": "^6.7.2",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^7.0.0-0",
"less": "^3.0.4",
"less-loader": "^5.0.0",
"lint-staged": "^9.5.0"
},
"gitHooks": {
"pre-commit": "lint-staged"
},
"lint-staged": {
"*.{js,jsx,vue}": [
"vue-cli-service lint",
"git add"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -0,0 +1,41 @@
<template>
<a-config-provider :locale="locale">
<a-layout class="page-layout">
<a-layout-header>Cert-D</a-layout-header>
<a-layout style="flex:1">
<router-view/>
</a-layout>
<a-layout-footer>
by greper
</a-layout-footer>
</a-layout>
</a-config-provider>
</template>
<script>
import zhCN from 'ant-design-vue/es/locale/zh_CN'
import { useI18n } from 'vue-i18n'
export default {
data () {
return {
locale: zhCN
}
},
setup () {
const { t } = useI18n() // call `useI18n`, and spread `t` from `useI18n` returning
return { t } // return render context that included `t`
}
}
</script>
<style lang="less">
.page-layout{
height: 100%;
overflow-x: hidden;
.ant-layout-header{
color:#fff;
}
}
</style>

View File

@ -0,0 +1,9 @@
import { request } from './service'
export default {
list () {
return request({
url: '/providers/list'
})
}
}

View File

@ -0,0 +1,10 @@
import { assign, map } from 'lodash'
import { service, request } from './service'
const files = require.context('./modules', false, /\.js$/)
const generators = files.keys().map(key => files(key).default)
export default assign({}, ...map(generators, generator => generator({
service,
request
})))

View File

@ -0,0 +1,95 @@
import axios from 'axios'
import { get } from 'lodash-es'
import { errorLog, errorCreate } from './tools'
/**
* @description 创建请求实例
*/
function createService () {
// 创建一个 axios 实例
const service = axios.create()
// 请求拦截
service.interceptors.request.use(
config => config,
error => {
// 发送失败
console.log(error)
return Promise.reject(error)
}
)
// 响应拦截
service.interceptors.response.use(
response => {
// dataAxios 是 axios 返回数据中的 data
const dataAxios = response.data
// 这个状态码是和后端约定的
const { code } = dataAxios
// 根据 code 进行判断
if (code === undefined) {
// 如果没有 code 代表这不是项目后端开发的接口 比如可能是 D2Admin 请求最新版本
if (response.config.unpack) {
return dataAxios
}
return dataAxios.data
} else {
// 有 code 代表这是一个后端接口 可以进行进一步的判断
switch (code) {
case 0:
// [ 示例 ] code === 0 代表没有错误
// TODO 可能结果还需要code和msg进行后续处理所以返回全部结果
return dataAxios.data
case 'xxx':
// [ 示例 ] 其它和后台约定的 code
errorCreate(`[ code: xxx ] ${dataAxios.msg}: ${response.config.url}`)
break
default:
// 不是正确的 code
errorCreate(`${dataAxios.msg}: ${response.config.url}`)
break
}
}
},
error => {
const status = get(error, 'response.status')
switch (status) {
case 400: error.message = '请求错误'; break
case 401: error.message = '未授权,请登录'; break
case 403: error.message = '拒绝访问'; break
case 404: error.message = `请求地址出错: ${error.response.config.url}`; break
case 408: error.message = '请求超时'; break
case 500: error.message = '服务器内部错误'; break
case 501: error.message = '服务未实现'; break
case 502: error.message = '网关错误'; break
case 503: error.message = '服务不可用'; break
case 504: error.message = '网关超时'; break
case 505: error.message = 'HTTP版本不受支持'; break
default: break
}
errorLog(error)
return Promise.reject(error)
}
)
return service
}
/**
* @description 创建请求方法
* @param {Object} service axios 实例
*/
function createRequestFunction (service) {
return function (config) {
const configDefault = {
headers: {
'Content-Type': get(config, 'headers.Content-Type', 'application/json')
},
timeout: 5000,
baseURL: process.env.VUE_APP_API,
data: {}
}
return service(Object.assign(configDefault, config))
}
}
// 用于真实网络请求的实例和请求方法
export const service = createService()
export const request = createRequestFunction(service)

View File

@ -0,0 +1,73 @@
import { notification } from 'ant-design-vue'
/**
* @description 安全地解析 json 字符串
* @param {String} jsonString 需要解析的 json 字符串
* @param {String} defaultValue 默认值
*/
export function parse (jsonString = '{}', defaultValue = {}) {
let result = defaultValue
try {
result = JSON.parse(jsonString)
} catch (error) {
console.log(error)
}
return result
}
/**
* @description 接口请求返回
* @param {Any} data 返回值
* @param {String} msg 状态信息
* @param {Number} code 状态码
*/
export function response (data = {}, msg = '', code = 0) {
return [
200,
{ code, msg, data }
]
}
/**
* @description 接口请求返回 正确返回
* @param {Any} data 返回值
* @param {String} msg 状态信息
*/
export function responseSuccess (data = {}, msg = '成功') {
return response(data, msg)
}
/**
* @description 接口请求返回 错误返回
* @param {Any} data 返回值
* @param {String} msg 状态信息
* @param {Number} code 状态码
*/
export function responseError (data = {}, msg = '请求失败', code = 500) {
return response(data, msg, code)
}
/**
* @description 记录和显示错误
* @param {Error} error 错误对象
*/
export function errorLog (error) {
// 打印到控制台
console.log(error)
// 显示提示
notification({
message: error.message,
type: 'error',
duration: 5 * 1000
})
}
/**
* @description 创建一个错误
* @param {String} msg 错误信息
*/
export function errorCreate (msg) {
const error = new Error(msg)
errorLog(error)
throw error
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@ -0,0 +1,60 @@
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<p>
For a guide and recipes on how to configure / customize this project,<br>
check out the
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
</p>
<h3>Installed CLI Plugins</h3>
<ul>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-unit-mocha" target="_blank" rel="noopener">unit-mocha</a></li>
</ul>
<h3>Essential Links</h3>
<ul>
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
</ul>
<h3>Ecosystem</h3>
<ul>
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
</ul>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="less">
h3 {
margin: 40px 0 0;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="d-container">
<div class="box">
<div class="inner">
<div class="header">
<slot name="header"></slot>
</div>
<div class="body">
<slot></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'd-container'
}
</script>
<style lang="less">
.d-container{
height: 100%;
width: 100%;
position: relative;
.box {
height: 100%;
position: absolute;
width: 100%;
top: 0;
left: 0;
.inner{
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
.header{
flex-shrink: 0;
}
.body{
overflow-y: auto;
flex:1
}
.footer{
flex-shrink: 0;
}
}
}
}
</style>

View File

@ -0,0 +1,11 @@
import { createI18n } from 'vue-i18n'
import zh from '@/locales/zh.json'
import en from '@/locales/en.json'
export const i18n = createI18n({
// something vue-i18n options here ...
locale: 'zh', // set current locale
messages: {
en,
zh
}
})

View File

@ -0,0 +1,26 @@
import _ from 'lodash'
import {
PlusCircleOutlined,
PlusOutlined,
CheckOutlined, EditOutlined,
ArrowRightOutlined,
NodeIndexOutlined,
ThunderboltOutlined,
DeleteOutlined
} from '@ant-design/icons-vue'
const icons = {
PlusCircleOutlined,
PlusOutlined,
CheckOutlined,
EditOutlined,
ArrowRightOutlined,
NodeIndexOutlined,
ThunderboltOutlined,
DeleteOutlined
}
export default function (app) {
_.forEach(icons, item => {
app.component(item.name, item)
})
}

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,9 @@
{
"hello": "你好",
"domain": "域名",
"next": "下一步",
"submit": "提交",
"reset": "重置",
"please.input.domain": "请输入域名",
"email": "邮箱"
}

View File

@ -0,0 +1,16 @@
import { createApp } from 'vue'
import router from './router'
import App from './App.vue'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/antd.css'
import '@/style/common.less'
import { i18n } from '@/i18n'
import icons from './icons'
import DContainer from '@/components/d-container'
const app = createApp(App)
app.config.productionTip = false
app.use(i18n)
app.use(Antd)
icons(app)
app.component('d-container', DContainer)
app.use(router).mount('#app')

View File

@ -0,0 +1,24 @@
import { createRouter, createWebHashHistory } from 'vue-router'
import Home from '../views/Home.vue'
import Detail from '../views/detail/index.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/detail',
name: 'detail',
component: Detail
// component: () => import(/* webpackChunkName: "about" */ '../views/About.vue'
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
})
export default router

View File

@ -0,0 +1,78 @@
div#app {
height: 100%
}
h1, h2, h3, h4, h5, h6 {
margin-bottom: 0;
}
.flex-center {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.flex-row {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.ml-10{
margin-left:10px;
}
.mt-10{
margin-top:10px;
}
.mr-10{
margin-right:10px;
}
.mb-10{
margin-bottom:10px;
}
.ant-layout {
height: 100%
}
.ant-drawer-body{
padding:0px;
}
.ant-drawer-content {
.ant-drawer-wrapper-body{
display: flex;
flex-direction: column;
}
.ant-drawer-body {
position: relative;
flex:1;
}
}
.d-dialog{
.ant-modal-body{
max-height: 60vh;
overflow-y: auto;
}
@media(min-height:600px) and (max-height:700px){
.ant-modal-body {
max-height: 50vh
}
}
@media (max-height:600px) {
.ant-modal-body {
max-height: 40vh
}
}
}

View File

@ -0,0 +1,103 @@
<template>
<div class="page-index flex-center">
<H2 class="title">CERT-D</H2>
<div class="page-body">
<a-tabs @change="callback">
<a-tab-pane key="1" tab="创建新证书">
<div class="create-from-domains">
<div class="input-row flex-row">
<a-select
size="large"
mode="tags"
:placeholder="$t('please.input.domain')"
v-model:value="formData.cert.domains"
:open="false"
></a-select>
<div class="row-append">
<a-button size="large" type="primary" @click="createFromDomain"></a-button>
</div>
</div>
<div class="helper">
支持泛域名例如*.test.yourdomain.com<br/>
支持多个域名打包到一张证书输入一个域名后回车再输下一个
</div>
</div>
</a-tab-pane>
<a-tab-pane key="2" tab="从配置导入" force-render>
<a-textarea class="textarea" type="textarea" :auto-size="autoSize" allow-clear></a-textarea>
<a-button class="mt-10" type="primary" >导入</a-button>
</a-tab-pane>
</a-tabs>
</div>
</div>
</template>
<script>
// eslint-disable-next-line no-unused-vars
import { reactive, toRaw } from 'vue'
import { useRouter } from 'vue-router'
export default {
setup () {
const formData = reactive({
cert: {
domains: ['*.docmirror.cn'],
email: 'xiaojunnuo@qq.com',
dnsProvider: 'aliyun'
}
})
const router = useRouter()
const createFromDomain = () => {
router.push({ name: 'detail', params: { options: JSON.stringify(formData) } })
}
const autoSize = reactive({ minRows: 8, maxRows: 10 })
return {
createFromDomain,
formData,
autoSize
}
}
}
</script>
<style lang="less">
.page-index{
background-color: #fff;
height: 100%;
&.flex-center{
justify-content: flex-start;
}
.title{
margin:50px;
}
.page-body{
min-width: 700px;width: 60%
}
.create-from-domains{
width:100%;
.input-row{
width:100%;
.ant-select{
flex:1;
}
.row-append{
padding-left:10px
}
}
}
.helper{
margin-top:5px;
}
.ant-tabs-bar {
margin: 0 0 16px;
border-bottom: 1px solid #f0f0f0;
outline: none;
}
}
</style>

View File

@ -0,0 +1,214 @@
<template>
<a-drawer
title="证书申请配置"
placement="right"
:closable="true"
width="500px"
v-model:visible="visible"
:after-visible-change="afterVisibleChange"
>
<d-container>
<a-form class="domain-form" :scrollToFirstError="true" :label-col="labelCol" :wrapper-col="wrapperCol">
<h3>域名信息</h3>
<a-form-item :label="$t('domain')" v-bind="validateInfos.domains">
<a-select
mode="tags"
:placeholder="$t('please.input.domain')"
v-model:value="formData.domains"
:open="false"
></a-select>
<div class="helper">例如*.yourdomain.com</div>
</a-form-item>
<a-form-item :label="$t('email')" v-bind="validateInfos.email">
<a-input v-model:value="formData.email"/>
</a-form-item>
<a-form-item label="dns验证" v-bind="validateInfos.dnsProvider">
<provider-selector v-model:value="formData.dnsProvider"
:providers="accessProviders"
@update:providers="accessProvidersUpdate"
></provider-selector>
</a-form-item>
<h3>CSR <span>必须全英文</span></h3>
<a-form-item label="国家" v-bind="validateInfos['csr.country']">
<a-input v-model:value="formData.csr.country"/>
</a-form-item>
<a-form-item label="省份" v-bind="validateInfos['csr.state']">
<a-input v-model:value="formData.csr.state"/>
</a-form-item>
<a-form-item label="市区" v-bind="validateInfos['csr.locality']">
<a-input v-model:value="formData.csr.locality"/>
</a-form-item>
<a-form-item label="组织" v-bind="validateInfos['csr.organization']">
<a-input v-model:value="formData.csr.organization"/>
</a-form-item>
<a-form-item label="部门" v-bind="validateInfos['csr.organizationUnit']">
<a-input v-model:value="formData.csr.organizationUnit"/>
</a-form-item>
<a-form-item label="联系人邮箱">
<a-input v-model:value="formData.csr.emailAddress"/>
</a-form-item>
</a-form>
<template #footer>
<a-form-item :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" @click="onSubmit">
确定
</a-button>
</a-form-item>
</template>
</d-container>
</a-drawer>
</template>
<script>
import { reactive, toRaw, ref, watch } from 'vue'
import { useForm } from '@ant-design-vue/use'
import _ from 'lodash-es'
import ProviderSelector from '@/views/detail/components/provider-selector'
function useDrawer () {
const visible = ref(false)
const afterVisibleChange = (val) => {
console.log('visible', val)
}
const open = () => {
visible.value = true
}
const close = () => {
visible.value = false
}
return {
afterVisibleChange,
open,
close,
visible
}
}
export default {
name: 'cert-form',
components: { ProviderSelector },
emits: ['update:accessProviders', 'update:cert'],
//
props: {
cert: {
type: Object
},
accessProviders: {
type: Object
}
},
setup (props, context) {
const drawer = useDrawer()
const certFormData = {
domains: [],
email: undefined,
dnsProvider: '',
csr: {
country: '',
state: 'GuangDong',
locality: 'ShengZhen',
organization: 'CertD Org.',
organizationUnit: 'IT Department',
emailAddress: undefined
}
}
const formData = reactive(certFormData)
watch(props.cert, () => {
console.log('cert props')
_.merge(formData, props.cert)
}, { immediate: true })
const rules = reactive({
domains: [{
type: 'array',
required: true,
message: '请输入域名'
}],
email: [{
type: 'email',
required: true,
message: '请输入正确的邮箱'
}],
dnsProvider: [{
required: true,
message: '请选择dns授权提供者'
}],
'csr.country': [{ required: true, message: '请输入国家代码' }],
'csr.state': [{ required: true, message: '请输入省份' }],
'csr.locality': [{ required: true, message: '请输入市区' }],
'csr.organization': [{ required: false, message: '请输入组织名称' }],
'csr.organizationUnit': [{ required: false, message: '请输入部门名称' }],
'csr.emailAddress': [{ required: false, message: '请输入邮箱' }]
})
// eslint-disable-next-line no-unused-vars
const { resetFields, validate, validateInfos } = useForm(formData, rules)
const onSubmit = async e => {
e.preventDefault()
try {
const res = await validate()
console.log('validation', res, toRaw(formData))
context.emit('update:cert', formData)
console.log('1111')
drawer.close()
} catch (err) {
console.error('表单校验错误', err)
}
}
const reset = () => {
resetFields()
}
const providerManagerRef = ref(null)
const providerManagerOpen = () => {
console.log('providerManagerRef', providerManagerRef)
if (providerManagerRef.value) {
providerManagerRef.value.open()
}
}
const accessProvidersUpdate = (val) => {
console.log('accessUpdate', val)
context.emit('update:accessProviders', val)
}
return {
labelCol: { span: 4 },
wrapperCol: { span: 18 },
formData,
onSubmit,
reset,
validateInfos,
providerManagerRef,
providerManagerOpen,
accessProvidersUpdate,
...drawer
}
}
}
</script>
<style lang="less">
.ant-form.domain-form {
height: 100%;
overflow-y: auto;
padding: 10px 24px;
h3 {
span {
font-weight: 200;
margin-left: 5px;
font-size: 12px;
color: #888;
}
}
}
</style>

View File

@ -0,0 +1,268 @@
<template>
<a-drawer
title="授权管理"
placement="right"
:closable="true"
width="500px"
v-model:visible="visible"
:after-visible-change="onAfterVisibleChange"
>
<div class="d-container provider-manager">
<a-button @click="add">
添加授权
</a-button>
<a-list
class="list"
item-layout="horizontal"
:data-source="providerList"
>
<template #renderItem="{ item ,index }">
<a-list-item>
<template #actions>
<a-button type="primary" @click="openEdit(item,index)"><template #icon><EditOutlined /></template></a-button>
<a-button type="danger" @click="remove(item,index)"><template #icon ><DeleteOutlined /></template></a-button>
</template>
<a-radio :checked="item.key===selectedKey" @update:checked="selectedKey = item.key">{{item.key}} {{ item.name }}</a-radio>
</a-list-item>
</template>
</a-list>
<div>
<a-button @click="onProviderSelectSubmit"></a-button>
</div>
</div>
</a-drawer>
<a-modal v-model:visible="editVisible" dialogClass="d-dialog" width="700px" title="编辑授权" @ok="onSubmit">
<a-form ref="formRef" class="domain-form" :model="formData" labelWidth="150px" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-form-item label="类型" :rules="rules.type">
<a-radio-group :disabled="editIndex!=null" v-model:value="formData.type" @change="onTypeChanged" >
<a-radio-button v-for="(option) of providerDefineList" :key="option.name" :value="option.name">
{{option.label}}
</a-radio-button>
</a-radio-group>
</a-form-item>
<template v-if="formData.type && currentProvider">
<a-form-item label="key" name="key" :rules="rules.key">
<a-input :disabled="editIndex!=null" v-model:value="formData.key"/>
<div class="helper">不重复的key</div>
</a-form-item>
<a-form-item label="名称" name="name" :rules="rules.name">
<a-input v-model:value="formData.name"/>
</a-form-item>
<a-form-item v-for="(item,key) in currentProvider.input"
:key="key"
:label="item.label || key"
:name="key"
:rules="[{ required: true, message: '必填项' }]">
<a-input v-model:value="formData[key]" v-bind="item.attrs" ></a-input>
<div class="helper">{{item.desc}}</div>
</a-form-item>
</template>
</a-form>
</a-modal>
</template>
<script>
import { ref, reactive, nextTick, watch } from 'vue'
// eslint-disable-next-line no-unused-vars
import { useForm } from '@ant-design-vue/use'
import _ from 'lodash-es'
import providerApi from '@/api/api.providers'
function useEdit (props, context, providerList, onSave) {
const formData = reactive({
key: '',
name: '',
type: ''
})
const rules = reactive({
type: [{
required: true,
message: '请选择类型'
}],
key: [{
required: true,
message: '请输入key'
}, {
validator (rule, value) {
const providers = providerList.value
if (!providers || providers.length === 0) {
return Promise.resolve()
}
if (editIndex.value != null) {
return Promise.resolve()
}
const filter = providers.filter(item => item.key === value)
console.log('validate', filter)
if (filter.length === 0) {
return Promise.resolve()
} else {
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject('key不能重复')
}
},
message: 'key不能与其他授权配置重复'
}],
name: [{
required: true,
message: '请输入名称'
}]
})
const formRef = ref()
// eslint-disable-next-line no-unused-vars
// const { resetFields, validate, validateInfos } = useForm(formData, rules)
const onSubmit = async e => {
e.preventDefault()
await formRef.value.validate()
const newProvider = _.cloneDeep(formData)
onSave(newProvider, editIndex.value)
closeEdit()
}
const editVisible = ref(false)
const editIndex = ref(null)
const openEdit = (item, index) => {
if (item) {
editIndex.value = index
_.forEach(formData, (value, key) => {
formData[key] = null
})
_.merge(formData, item)
changeType(item.type)
} else {
editIndex.value = null
}
editVisible.value = true
}
const add = () => {
openEdit()
}
const closeEdit = () => {
editVisible.value = false
}
const providerDefineList = ref([])
const onCreated = async () => {
providerDefineList.value = await providerApi.list()
}
onCreated()
const currentProvider = ref(null)
const onTypeChanged = (e) => {
const value = e.target.value
changeType(value)
// input form rules
}
const changeType = (type) => {
if (providerDefineList.value == null) {
return
}
for (const item of providerDefineList.value) {
if (item.name === type) {
currentProvider.value = item
break
}
}
if (editIndex.value == null) {
formData.key = currentProvider.value.name
formData.name = currentProvider.value.label || currentProvider.value.name
}
}
return {
labelCol: { span: 6 },
wrapperCol: { span: 16 },
formData,
onSubmit,
rules,
editVisible,
formRef,
currentProvider,
providerDefineList,
editIndex,
openEdit,
onTypeChanged,
add
}
}
export default {
name: 'provider-manager',
props: {
value: {},
providers: {}
},
emits: ['update:value', 'update:providers'],
setup (props, context) {
const visible = ref(false)
const close = () => {
visible.value = false
}
const onAfterVisibleChange = () => {
}
const providerList = ref([])
const selectedKey = ref(null)
watch(() => props.providers, () => {
providerList.value = _.cloneDeep(props.providers || [])
}, { immediate: true })
watch(() => props.value, () => {
selectedKey.value = props.value
}, { immediate: true })
const onEditSave = (newProvier, editIndex) => {
if (editIndex == null) {
providerList.value.push(newProvier)
} else {
_.merge(providerList.value[editIndex], newProvier)
}
}
const editModule = useEdit(props, context, providerList, onEditSave)
const open = () => {
visible.value = true
if (providerList.value.length === 0) {
nextTick(() => {
editModule.add()
})
}
}
const remove = (item, index) => {
providerList.value.splice(index, 1)
}
const onProviderSelectSubmit = () => {
context.emit('update:providers', providerList.value)
context.emit('update:value', selectedKey.value)
close()
}
return {
providerList,
visible,
open,
close,
onAfterVisibleChange,
remove,
selectedKey,
onProviderSelectSubmit,
...editModule
}
}
}
</script>
<style lang="less">
.provider-manager{
padding:10px;
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<div class="provider-selector">
<a-select
:value="value"
@update:value="valueUpdate"
>
<a-select-option v-for="item of providers" :key="item.key" :value="item.key">
{{ item.name }}
</a-select-option>
</a-select>
<a-button class="suffix" @click="providerManagerOpen">
管理授权
</a-button>
</div>
<provider-manager ref="providerManagerRef"
:providers="providers"
:value="value"
@update:value="valueUpdate"
@update:providers="providersUpdate"
></provider-manager>
</template>
<script>
import { ref } from 'vue'
import ProviderManager from './provider-manager'
export default {
name: 'provider-selector',
components: { ProviderManager },
emits: ['update:providers', 'update:value'],
//
props: {
value: {
type: String
},
providers: {
type: Object
}
},
setup (props, context) {
const providerManagerRef = ref(null)
const providerManagerOpen = () => {
console.log('providerManagerRef', providerManagerRef)
if (providerManagerRef.value) {
providerManagerRef.value.open()
}
}
const providersUpdate = (val) => {
console.log('accessUpdate', val)
context.emit('update:providers', val)
}
const valueUpdate = (val) => {
context.emit('update:value', val)
}
return {
providersUpdate,
valueUpdate,
providerManagerOpen,
providerManagerRef
}
}
}
</script>
<style lang="less">
.provider-selector{
display: flex;
flex-direction: row;
.ant-select{
flex:1;
}
.suffix{
flex-shrink: 0;
margin-left:5px;
}
}
</style>

View File

@ -0,0 +1,243 @@
<template>
<a-drawer
title="编辑任务"
placement="right"
:closable="true"
width="600px"
v-model:visible="taskDrawerVisible"
:after-visible-change="taskDrawerOnAfterVisibleChange"
>
<template v-if="currentTask">
<d-container v-if="currentTask._isAdd" class="task-edit-form">
<a-row :gutter="10">
<a-col v-for="(item,index) of taskPluginDefineList" :key="index" class="task-plugin" :span="12">
<a-card hoverable :class="{'current':item.name === currentTask.type}"
@click="taskTypeSelected(item)" @dblclick="taskTypeSelected(item);taskTypeSave()">
<a-card-meta>
<template #title>
<a-avatar :src="item.icon||'/images/plugin.png'"/>
<span class="title">{{ item.label }}</span>
</template>
<template #description>
<span :title="item.desc">{{ item.desc }}</span>
</template>
</a-card-meta>
</a-card>
</a-col>
</a-row>
<a-button type="primary" @click="taskTypeSave">
确定
</a-button>
</d-container>
<d-container v-else class="d-container" >
<a-form class="task-form" :label-col="labelCol" :wrapper-col="wrapperCol">
<a-form-item label="任务名称">
<a-input
placeholder="请输入任务名称"
v-model:value="currentTask.taskName"
></a-input>
</a-form-item>
<a-form-item v-for="(value,key) in currentPlugin.input" :key="key" :label="value.label">
<a-input v-model:value="currentTask[key]"></a-input>
</a-form-item>
</a-form>
<template #footer>
<a-form-item :wrapper-col="{ span: 14, offset: 4 }">
<a-button type="primary" @click="taskSave">
确定
</a-button>
</a-form-item>
</template>
</d-container>
</template>
</a-drawer>
<provider-manager ref="providerManager"></provider-manager>
</template>
<script>
import { message } from 'ant-design-vue'
import ProviderManager from '@/views/detail/components/provider-manager'
import pluginsApi from '@/api/api.plugins'
import { ref } from 'vue'
// eslint-disable-next-line no-unused-vars
import _ from 'lodash-es'
/**
* task drawer
* @returns
*/
function useTaskForm (context) {
const taskPluginDefineList = ref([])
const onCreated = async () => {
const plugins = await pluginsApi.list()
taskPluginDefineList.value = plugins
}
onCreated()
const currentTask = ref()
const currentTaskIndex = ref()
const currentDeploy = ref()
const currentPlugin = ref(null)
const taskFormRef = ref(null)
const taskDrawerVisible = ref(false)
const taskAdd = (deploy) => {
const task = { taskName: '新任务', type: undefined, _isAdd: true }
currentDeploy.value = deploy
currentDeploy.value.tasks.push(task)
currentTask.value = deploy.tasks[deploy.tasks.length - 1]
taskDrawerShow()
}
const taskTypeSelected = (item) => {
currentTask.value.type = item.name
currentTask.value.taskName = item.label
}
const taskTypeSave = () => {
currentTask.value._isAdd = false
if (currentTask.value.type == null) {
message.warn('请先选择类型')
return
}
changeCurrentPlugin(currentTask.value)
}
const taskDrawerShow = () => {
taskDrawerVisible.value = true
}
const taskDrawerClose = () => {
taskDrawerVisible.value = false
}
const taskDrawerOnAfterVisibleChange = (val) => {
console.log('taskDrawerOnAfterVisibleChange', val)
}
const taskEdit = (deploy, task, index) => {
if (task) {
currentTask.value = task
currentTaskIndex.value = index
}
currentDeploy.value = deploy
changeCurrentPlugin(currentTask.value)
taskDrawerShow()
}
const changeCurrentPlugin = (task) => {
const taskType = task.type
const currentPlugins = taskPluginDefineList.value.filter(p => {
return p.name === taskType
})
if (currentPlugins.length <= 0) {
task.type = undefined
task._isAdd = true
throw new Error('未知插件:' + taskType)
}
currentPlugin.value = currentPlugins[0]
}
const taskSave = () => {
console.log('currentTask', currentTask)
// context.emit('update', currentTask.value)
taskDrawerClose()
}
return {
taskTypeSelected,
taskTypeSave,
taskPluginDefineList,
taskFormRef,
taskAdd,
taskEdit,
taskDrawerShow,
taskDrawerVisible,
taskDrawerOnAfterVisibleChange,
currentTask,
currentTaskIndex,
currentPlugin,
taskSave
}
}
function useProviderManager () {
const providerManager = ref(null)
const providerManagerOpen = () => {
providerManager.value.open()
}
return { providerManager, providerManagerOpen }
}
export default {
name: 'task-form',
components: { ProviderManager },
emits: ['update'],
props: {
options: {}
},
setup (props, context) {
return {
...useTaskForm(context),
...useProviderManager(),
labelCol: { span: 6 },
wrapperCol: { span: 16 }
}
}
}
</script>
<style lang="less">
.task-edit-form{
.body{
padding:10px;
.ant-card {
margin-bottom: 10px;
&.current {
border-color: #00B7FF;
}
.ant-card-meta-title {
display: flex;
flex-direction: row;
justify-content: flex-start;
}
.ant-avatar {
width: 24px;
height: 24px;
flex-shrink: 0;
}
.title {
margin-left: 5px;
white-space: nowrap;
flex: 1;
display: block;
overflow: hidden;
text-overflow: ellipsis;
}
}
.ant-card-body {
padding: 14px;
height: 100px;
overflow-y: hidden;
.ant-card-meta-description {
font-size: 10px;
line-height: 20px;
height: 40px;
}
}
}
}
</style>

View File

@ -0,0 +1,406 @@
<template>
<div class="page-detail">
<div class="flow">
<div class="flow-group flow-cert">
<h3 class="group-head">
证书申请
</h3>
<a-divider></a-divider>
<div class="cert-display">
<a-button class="cert-edit-btn" type="link" @click="certFormOpen">
编辑
</a-button>
<div class="label-list">
<div class="title">证书</div>
<div class="label-item">
<label>域名:</label>
<div class="value">
<a-tag type="primary" v-for="item of options.cert.domains " :key="item">
{{ item }}
</a-tag>
</div>
</div>
<div class="label-item">
<label>邮箱:</label>
<div>
{{ options.cert.email }}
</div>
</div>
<div class="label-item">
<label>CA:</label>
<div>
{{ options.cert.ca }}
</div>
</div>
<div class="title">CSR
<span>必须全英文</span></div>
<div class="label-item">
<label>country:</label>
<div>
{{ options.cert.csr.country }}
</div>
</div>
<div class="label-item">
<label>state:</label>
<div>
{{ options.cert.csr.state }}
</div>
</div>
<div class="label-item">
<label>locality:</label>
<div>
{{ options.cert.csr.locality }}
</div>
</div>
<div class="label-item">
<label>org:</label>
<div>
{{ options.cert.csr.organization }}
</div>
</div>
<div class="label-item">
<label>orgUnit:</label>
<div>
{{ options.cert.csr.organizationUnit }}
</div>
</div>
</div>
</div>
</div>
<div class="flow-group flow-deploy">
<h3 class="group-head">
部署流程
<PlusCircleOutlined title="添加部署流程" class="add-icon" @click="deployAdd"/>
</h3>
<a-divider></a-divider>
<div class="deploy-list">
<a-card class="deploy-item" v-for="(deploy,index) of options.deploy" :key="index">
<template #title>
<div class="deploy-name">
<template v-if="deploy._isEdit">
<a-input v-model:value="deploy.deployName"
:validateStatus="deploy.deployName?'':'error'"
placeholder="请输入流程名称"
@keyup.enter="deployCloseEditMode(deploy)"
>
<template #suffix>
<CheckOutlined @click="deployCloseEditMode(deploy)" style="color: rgba(0,0,0,.45)"/>
</template>
</a-input>
</template>
<template v-else>
<span @click="deployNameEdit"> <NodeIndexOutlined/> {{ deploy.deployName }}</span>
<EditOutlined class="ml-10 edit-icon" @click="deployOpenEditMode(deploy)"/>
</template>
</div>
</template>
<div class="task-list">
<div class="task-item-wrapper" v-for="(task,iindex) of deploy.tasks" :key="iindex">
<a-button class="task-item" shape="round" @click="taskEdit(deploy,task,index)">
<ThunderboltOutlined/>
{{ task.taskName }}
</a-button>
<ArrowRightOutlined class="task-next-icon"/>
</div>
<div class="task-item-wrapper">
<a-button type="primary" class="task-item" shape="round" @click="taskAdd(deploy)">
<PlusOutlined/>
添加新任务
</a-button>
</div>
</div>
</a-card>
</div>
</div>
</div>
<cert-form ref="certFormRef" v-model:cert="options.cert" v-model:access-providers="options.accessProviders"></cert-form>
<task-form ref="taskFormRef" ></task-form>
</div>
</template>
<script>
import { message } from 'ant-design-vue'
// eslint-disable-next-line no-unused-vars
import { reactive, ref, toRef } from 'vue'
// eslint-disable-next-line no-unused-vars
import { useRoute } from 'vue-router'
import CertForm from '@/views/detail/components/cert-form'
import TaskForm from './components/task-form'
import _ from 'lodash-es'
function useDeploy (options) {
const deployAdd = () => {
options.deploy.push({
deployName: `D${options.deploy.length + 1}-新部署流程`,
_isEdit: false,
tasks: []
})
}
const deployCloseEditMode = (deploy) => {
if (!deploy.deployName) {
message.error('请输入流程名称')
return
}
deploy._isEdit = false
console.log('options', options)
}
const deployOpenEditMode = (deploy) => {
deploy._isEdit = true
}
return {
deployAdd, deployCloseEditMode, deployOpenEditMode
}
}
export default {
components: { CertForm, TaskForm },
setup () {
const route = useRoute()
console.log('route', route)
const optionParams = route.params.options ? JSON.parse(route.params.options) : {}
const optionsDefault = {
cert: {
csr: {
country: 'CN',
state: 'GuangDong',
locality: 'ShengZhen',
organization: 'CertD Org.',
organizationUnit: 'IT Department'
}
},
accessProviders: [{ key: 'aliyun', type: 'aliyun', name: 'aliyun' }],
deploy: []
}
_.merge(optionsDefault, optionParams)
const options = reactive(optionsDefault)
const certFormChanged = (value) => {
console.log('certFormChanged', value)
options.cert = value
}
const certFormRef = ref(null)
const certFormOpen = () => {
certFormRef.value.open()
}
const taskFormRef = ref(null)
const taskAdd = (deploy) => {
taskFormRef.value.taskAdd(deploy)
}
const taskEdit = (deploy, task, index) => {
taskFormRef.value.taskEdit(deploy, task, index)
}
return {
options,
certFormChanged,
certFormRef,
certFormOpen,
...useDeploy(options),
taskFormRef,
taskAdd,
taskEdit
}
}
}
</script>
<style lang="less">
.page-detail {
height: 100%;
overflow-y: auto;
position: relative;
width: 100%;
background-color: #fff;
.label-list {
.title{
font-weight: 500;
font-size: 16px;
color:#555;
span{
font-weight: 200;
margin-left:5px;
font-size: 12px;
color:#888;
}
}
.label-item {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: baseline;
padding: 8px 0px;
label {
width: 70px;
flex-shrink: 0;
color: rgba(0, 0, 0, 0.85);
font-weight: normal;
font-size: 14px;
line-height: 1.5715;
text-align: end;
padding-right: 10px;
}
.value {
flex: 1;
}
}
}
.flow {
height: 100%;
h3 {
text-align: center;
}
display: flex;
flex-direction: row;
.flow-group {
min-width: 300px;
height: 100%;
border-right: 1px #eee solid;
padding: 20px;
.group-head {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.add-icon {
margin-left: 20px;
font-size: 24px;
color: #737070;
}
}
}
.flow-cert {
max-width: 400px;
.cert-display {
position: relative;
.cert-edit-btn {
position: absolute;
right: 0px;
top: 0px;
}
}
}
.add-icon {
font-size: 26px;
}
.flow-deploy {
flex-shrink: 0;
flex: 1;
display: flex;
flex-direction: column;
.deploy-list {
flex: 1;
overflow-y: auto;
.deploy-item {
margin-bottom: 10px;
}
}
// min-width:70%;
.deploy-name {
max-width: 300px;
.edit-icon {
color: #737070;
}
}
}
}
}
.ant-form.task-form {
padding: 10px 24px;
}
.task-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-items: center;
> * {
margin-bottom: 10px;
}
.task-item-wrapper {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
}
.task-item {
//border: 1px solid #eee;
//padding: 10px 20px;
//border-radius: 20px;
}
.task-add-icon {
font-size: 24px;
margin-right: 10px;
}
.task-next-icon {
margin-left: 10px;
margin-right: 10px;
}
}
.task-type-selector {
}
.task-form {
.task-plugin-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
> * {
margin-right: 8px;
}
.task-plugin {
margin-bottom: 10px;
}
}
}
</style>

View File

@ -0,0 +1,13 @@
import { expect } from 'chai'
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
props: { msg }
})
expect(wrapper.text()).to.include(msg)
})
})

View File

@ -12,15 +12,19 @@ export class AliyunDnsProvider extends AbstractDnsProvider {
type: String, type: String,
desc: 'accessKeyId', desc: 'accessKeyId',
attrs: { attrs: {
placeholder: 'accessKeyId' placeholder: 'accessKeyId',
} rules: [{ required: true, message: '必填项' }]
},
required: true
}, },
accessKeySecret: { accessKeySecret: {
type: String, type: String,
desc: 'accessKeySecret', desc: 'accessKeySecret',
attrs: { attrs: {
placeholder: 'accessKeySecret' placeholder: 'accessKeySecret',
rules: [{ required: true, message: '必填项' }]
} }
} }
}, },
output: { output: {