iot-ui-vue/src/views/system/Apply/Save/components/EditForm.vue

2023 lines
81 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="edit-form-container">
<j-form
ref="formRef"
:model="form.data"
layout="vertical"
class="form"
@validate="getErrorNum"
>
<j-form-item
label="名称"
name="name"
:rules="[
{
required: true,
message: '请输入名称',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
v-model:value="form.data.name"
placeholder="请输入名称"
/>
</j-form-item>
<j-form-item
label="应用"
name="provider"
:rules="[
{
required: true,
message: '请选择应用',
},
]"
>
<j-radio-group
v-model:value="form.data.provider"
class="radio-container"
:disabled="!!routeQuery.id"
>
<j-radio-button value="internal-standalone">
<div>
<j-image
:preview="false"
:src="getImage('/apply/provider1.png')"
width="64px"
height="64px"
/>
<p>内部独立应用</p>
</div>
</j-radio-button>
<j-radio-button value="internal-integrated">
<div>
<j-image
:preview="false"
:src="getImage('/apply/provider2.png')"
/>
<p>内部集成应用</p>
</div>
</j-radio-button>
<j-radio-button value="wechat-webapp">
<div>
<j-image
:preview="false"
:src="getImage('/apply/provider4.png')"
/>
<p>微信网站应用</p>
</div>
</j-radio-button>
<j-radio-button value="dingtalk-ent-app">
<div>
<j-image
:preview="false"
:src="getImage('/apply/provider3.png')"
/>
<p>钉钉企业内部应用</p>
</div>
</j-radio-button>
<j-radio-button value="third-party">
<div>
<j-image
:preview="false"
:src="getImage('/apply/provider5.png')"
/>
<p>第三方应用</p>
</div>
</j-radio-button>
</j-radio-group>
</j-form-item>
<j-form-item
label="接入方式"
name="integrationModes"
:rules="[
{
required: true,
message: '请选择接入方式',
},
]"
>
<j-checkbox-group
v-model:value="form.data.integrationModes"
:options="joinOptions"
/>
</j-form-item>
<j-collapse v-model:activeKey="form.integrationModesISO">
<!-- 页面集成 -->
<j-collapse-panel
key="page"
v-if="form.data.integrationModes.includes('page')"
>
<template #header>
<span>
页面集成
<span
v-show="form.errorNumInfo.page.size"
class="error-info"
>
{{ form.errorNumInfo.page.size }}
</span>
</span>
</template>
<j-form-item
:name="['page', 'baseUrl']"
class="resetLabel"
:rules="[
{
required: true,
message: '请输入接入地址',
},
]"
>
<template #label>
<FormLabel
text="接入地址"
required
tooltip="填写访问其它平台的地址"
/>
</template>
<j-input
v-model:value="form.data.page.baseUrl"
placeholder="请输入接入地址"
/>
</j-form-item>
<j-form-item
label="路由方式"
:name="['page', 'routeType']"
:rules="[
{
required: true,
message: '请选择路由方式',
},
]"
>
<j-select v-model:value="form.data.page.routeType">
<j-select-option value="hash">hash</j-select-option>
<j-select-option value="history"
>history</j-select-option
>
</j-select>
</j-form-item>
<j-form-item v-if="form.data.provider === 'third-party'">
<template #label>
<FormLabel
text="参数"
tooltip="自定义参数,格式${name}"
/>
</template>
<RequestTable
v-model:value="form.data.page.parameters"
value-type="select"
:value-options="[
{ label: '用户ID', value: '用户ID' },
{ label: '用户名', value: '用户名' },
{ label: 'token', value: 'token' },
]"
/>
</j-form-item>
</j-collapse-panel>
<!-- API客户端 -->
<j-collapse-panel
key="apiClient"
v-if="form.data.integrationModes.includes('apiClient')"
>
<template #header>
<span>
API客户端
<span
v-show="form.errorNumInfo.apiClient.size"
class="error-info"
>
{{ form.errorNumInfo.apiClient.size }}
</span>
</span>
</template>
<j-form-item
class="resetLabel"
:name="['apiClient', 'baseUrl']"
:rules="[
{
required: true,
message: '请输入接口地址',
},
]"
>
<template #label>
<FormLabel
text="接口地址"
required
tooltip="访问Api服务的地址"
/>
</template>
<j-input
v-model:value="form.data.apiClient.baseUrl"
placeholder="请输入接口地址"
/>
</j-form-item>
<div v-if="form.data.provider === 'internal-standalone'">
<j-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'authorizationUrl',
]"
:rules="[
{
required: true,
message: '请输入授权地址',
},
]"
>
<template #label>
<FormLabel
text="授权地址"
required
tooltip="认证授权地址"
/>
</template>
<j-input
v-model:value="
form.data.apiClient.authConfig.oauth2
.authorizationUrl
"
placeholder="请输入授权地址"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'tokenUrl',
]"
:rules="[
{
required: true,
message: '请输入token地址',
},
]"
>
<template #label>
<FormLabel
text="token地址"
required
tooltip="设置token令牌的地址"
/>
</template>
<j-input
v-model:value="
form.data.apiClient.authConfig.oauth2
.tokenUrl
"
placeholder="请输入token地址"
/>
</j-form-item>
<j-form-item>
<template #label>
<FormLabel
text="回调地址"
tooltip="授权完成后跳转到具体页面的回调地址"
/>
</template>
<j-input
v-model:value="
form.data.apiClient.authConfig.oauth2
.redirectUri
"
placeholder="请输入回调地址"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'clientId',
]"
:rules="[
{
required: true,
message: '请输入appId',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="appId"
required
tooltip="第三方应用唯一标识"
/>
</template>
<j-input
v-model:value="
form.data.apiClient.authConfig.oauth2
.clientId
"
placeholder="请输入appId"
:disabled="!!form.data.id"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'clientSecret',
]"
:rules="[
{
required: true,
message: '请输入appKey',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="appKey"
required
tooltip="第三方应用唯一标识的密钥"
/>
</template>
<j-input
v-model:value="
form.data.apiClient.authConfig.oauth2
.clientSecret
"
placeholder="请输入appKey"
/>
</j-form-item>
</div>
<div v-else-if="form.data.provider === 'third-party'">
<j-form-item
label="认证方式"
:name="['apiClient', 'authConfig', 'type']"
:rules="[{ required: true }]"
>
<j-select
v-model:value="
form.data.apiClient.authConfig.type
"
>
<j-select-option value="oauth2">
OAuth2
</j-select-option>
<j-select-option value="basic">
基本认证
</j-select-option>
<j-select-option value="bearer">
bearer认证
</j-select-option>
</j-select>
</j-form-item>
<div
v-if="
form.data.apiClient.authConfig.type === 'oauth2'
"
>
<j-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'authorizationUrl',
]"
:rules="[
{
required: true,
message: '请输入授权地址',
},
]"
>
<template #label>
<FormLabel
text="授权地址"
required
tooltip="认证授权地址"
/>
</template>
<j-input
v-model:value="
form.data.apiClient.authConfig.oauth2
.authorizationUrl
"
placeholder="请输入授权地址"
/>
</j-form-item>
<j-form-item
label="请求方式"
:name="[
'apiClient',
'authConfig',
'oauth2',
'tokenRequestType',
]"
:rules="[
{
required: true,
message: '请选择请求方式',
},
]"
>
<j-select
v-model:value="
form.data.apiClient.authConfig.oauth2
.tokenRequestType
"
placeholder="请选择请求方式"
>
<j-select-option value="POST_BODY">
请求体
</j-select-option>
<j-select-option value="POST_URI">
请求头
</j-select-option>
</j-select>
</j-form-item>
<j-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'clientId',
]"
:rules="[
{
required: true,
message: '请输入client_id',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="client_id"
required
tooltip="应用唯一标识"
/>
</template>
<j-input
v-model:value="
form.data.apiClient.authConfig.oauth2
.clientId
"
placeholder="请输入client_id"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="[
'apiClient',
'authConfig',
'oauth2',
'clientSecret',
]"
:rules="[
{
required: true,
message: '请输入client_secret',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="client_secret"
required
tooltip="应用唯一标识的秘钥"
/>
</template>
<j-input
v-model:value="
form.data.apiClient.authConfig.oauth2
.clientSecret
"
placeholder="请输入client_secret"
/>
</j-form-item>
</div>
<div
v-else-if="
form.data.apiClient.authConfig.type === 'basic'
"
>
<j-form-item
label="用户名"
:name="[
'apiClient',
'authConfig',
'basic',
'username',
]"
:rules="[
{
required: true,
message: '请输入用户名',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
v-model:value="
form.data.apiClient.authConfig.basic
.username
"
placeholder="请输入用户名"
/>
</j-form-item>
<j-form-item
label="密码"
:name="[
'apiClient',
'authConfig',
'basic',
'password',
]"
:rules="[
{
required: true,
message: '请输入密码',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
v-model:value="
form.data.apiClient.authConfig.basic
.password
"
placeholder="请输入密码"
/>
</j-form-item>
</div>
<j-form-item
v-else-if="
form.data.apiClient.authConfig.type === 'bearer'
"
label="token"
:name="[
'apiClient',
'authConfig',
'bearer',
'token',
]"
:rules="[
{
required: true,
message: '请输入token',
},
]"
>
<j-input
v-model:value="
form.data.apiClient.authConfig.bearer.token
"
placeholder="请输入token"
/>
</j-form-item>
</div>
<div v-if="form.data.provider !== 'internal-integrated'">
<j-form-item
:name="['apiClient', 'headers']"
:rules="[
{
required: !headerValid,
message: '请输入请求头',
},
{
validator: headerValidator,
},
]"
>
<template #label>
<FormLabel
text="请求头"
tooltip="根据不同应用的调用规范,自定义请求头内容"
/>
</template>
<RequestTable
v-model:value="form.data.apiClient.headers"
v-model:valid="headerValid"
/>
</j-form-item>
<j-form-item
label="参数"
:name="['apiClient', 'parameters']"
:rules="[
{
required: !paramsValid,
message: '请输入参数',
},
{
validator: paramsValidator,
},
]"
>
<RequestTable
v-model:value="form.data.apiClient.parameters"
v-model:valid="paramsValid"
/>
</j-form-item>
</div>
</j-collapse-panel>
<!-- API服务 -->
<j-collapse-panel
key="apiServer"
v-if="form.data.integrationModes.includes('apiServer')"
>
<template #header>
<span>
API服务
<span
v-show="form.errorNumInfo.apiServer.size"
class="error-info"
>
{{ form.errorNumInfo.apiServer.size }}
</span>
</span>
</template>
<j-form-item
class="resetLabel"
v-if="!form.data.integrationModes.includes('apiClient')"
:name="['apiServer', 'appId']"
:rules="[{ required: true }]"
>
<template #label>
<FormLabel
text="appId"
required
tooltip="第三方应用唯一标识"
/>
</template>
<j-input
v-model:value="form.data.apiServer.appId"
disabled
placeholder="请输入appId"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="['apiServer', 'secureKey']"
:rules="[
{
required: true,
message: '请输入secureKey',
},
]"
>
<template #label>
<FormLabel
text="secureKey"
required
tooltip="第三方应用唯一标识匹配的秘钥"
/>
</template>
<j-input
v-model:value="form.data.apiServer.secureKey"
placeholder="请输入secureKey"
/>
</j-form-item>
<j-form-item
class="resetLabel"
v-show="form.data.provider === 'internal-standalone'"
>
<template #label>
<FormLabel
text="回调地址"
tooltip="授权完成后跳转到具体页面的回调地址"
/>
</template>
<j-input
v-model:value="form.data.apiServer.redirectUri"
placeholder="请输入redirectUri"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="['apiServer', 'roleIdList']"
:rules="[
{
required: true,
message: '请选择角色',
},
]"
>
<template #label>
<FormLabel
text="角色"
required
tooltip="为应用用户分配角色,根据绑定的角色,进行系统菜单赋权"
/>
</template>
<j-select
v-model:value="form.data.apiServer.roleIdList"
:options="form.roleIdList"
mode="multiple"
placeholder="请选择角色"
></j-select>
<PermissionButton
:hasPermission="`${rolePermission}:update`"
type="link"
@click="
clickAddItem(
form.data.apiServer.roleIdList,
'Role',
)
"
class="add-item"
>
<AIcon type="PlusOutlined" />
</PermissionButton>
</j-form-item>
<j-form-item>
<template #label>
<FormLabel
text="组织"
tooltip="为应用用户分配所属组织,根据绑定的组织,进行数据隔离"
/>
</template>
<j-tree-select
v-model:value="form.data.apiServer.orgIdList"
show-search
style="width: 100%"
:dropdown-style="{
maxHeight: '400px',
overflow: 'auto',
}"
:fieldNames="{
label: 'name',
value: 'id',
}"
multiple
:tree-data="form.orgIdList"
placeholder="请选择组织"
:filterTreeNode="
(v: string, node: any) => filterSelectNode(v, node, 'name')
"
>
<template #title="{ name }">
{{ name }}
</template>
</j-tree-select>
<PermissionButton
:hasPermission="`${deptPermission}:update`"
type="link"
@click="
clickAddItem(
form.data.apiServer.orgIdList,
'Department',
)
"
class="add-item"
>
<AIcon type="PlusOutlined" />
</PermissionButton>
</j-form-item>
<div v-if="form.data.provider === 'third-party'">
<j-form-item>
<template #label>
<FormLabel
text="redirectUrl"
tooltip="授权后自动跳转的页面地址"
/>
</template>
<j-input
v-model:value="form.data.apiServer.redirectUri"
placeholder="请输入redirectUrl"
/>
</j-form-item>
<j-form-item
label="IP白名单"
:name="['apiServer', 'ipWhiteList']"
:rules="[
{
validator: validateIP,
},
]"
>
<j-textarea
v-model:value="form.data.apiServer.ipWhiteList"
placeholder="请输入IP白名单多个地址回车分隔不填默认均可访问"
:rows="3"
style="width: 100%"
/>
</j-form-item>
</div>
</j-collapse-panel>
<!-- 单点登录 -->
<j-collapse-panel
key="ssoClient"
v-if="form.data.integrationModes.includes('ssoClient')"
>
<template #header>
<span>
单点登录
<span
v-show="form.errorNumInfo.ssoClient.size"
class="error-info"
:style="
form.errorNumInfo.ssoClient.size > 9
? { padding: '0 8px' }
: {}
"
>
{{ form.errorNumInfo.ssoClient.size }}
</span>
</span>
</template>
<!-- 第三方应用 -->
<div v-if="form.data.provider === 'third-party'">
<j-form-item
label="认证方式"
:name="['sso', 'configuration', 'oauth2', 'type']"
:rules="[
{
required: true,
message: '请选择认证方式',
},
]"
>
<j-select
v-model:value="
form.data.sso.configuration.oauth2.type
"
placeholder="请选择认证方式"
:options="[
{ label: 'oauth2', value: 'oauth2' },
]"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="['sso', 'configuration', 'oauth2', 'scope']"
:rules="[
{
required: true,
message: '请输入scope',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="scope"
required
tooltip="限制用户访问应用程序的权限"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.oauth2.scope
"
placeholder="请输入scope"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="[
'sso',
'configuration',
'oauth2',
'clientId',
]"
:rules="[
{
required: true,
message: '请输入client_id',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="client_id"
required
tooltip="应用唯一标识"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.oauth2.clientId
"
placeholder="请输入client_id"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="[
'sso',
'configuration',
'oauth2',
'clientSecret',
]"
:rules="[
{
required: true,
message: '请输入client_secret',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="client_secret"
required
tooltip="应用唯一标识的秘钥"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.oauth2
.clientSecret
"
placeholder="请输入client_secret"
/>
</j-form-item>
</div>
<j-form-item
v-if="
(form.data.provider === 'internal-standalone' ||
form.data.provider === 'third-party') &&
!form.data.integrationModes.includes('apiClient')
"
class="resetLabel"
:name="[
'sso',
'configuration',
'oauth2',
'authorizationUrl',
]"
:rules="[
{
required: true,
message: '请输入授权地址',
},
]"
>
<template #label>
<FormLabel
text="授权地址"
required
tooltip="认证授权地址"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.oauth2
.authorizationUrl
"
placeholder="请输入授权地址"
/>
</j-form-item>
<!-- 第三方应用 -->
<div v-if="form.data.provider === 'third-party'">
<j-form-item
class="resetLabel"
:name="[
'sso',
'configuration',
'oauth2',
'tokenUrl',
]"
:rules="[
{
required: true,
message: '请输入token地址',
},
]"
>
<template #label>
<FormLabel
text="token地址"
required
tooltip="设置token令牌的地址"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.oauth2.tokenUrl
"
placeholder="请输入token地址"
/>
</j-form-item>
<j-form-item label="logo">
<j-upload
v-model:file-list="form.fileList"
accept=".jpg,.png"
:maxCount="1"
list-type="picture-card"
:show-upload-list="false"
:headers="{
[TOKEN_KEY]: LocalStore.get(TOKEN_KEY),
}"
:action="`${BASE_API_PATH}/file/static`"
:beforeUpload="beforeLogoUpload"
@change="changeBackUpload"
>
<img
v-if="
form.data.sso.configuration.oauth2
.logoUrl
"
:src="
form.data.sso.configuration.oauth2
.logoUrl
"
alt="avatar"
style="width: 150px"
/>
<div v-else style="width: 150px">
<AIcon
:type="
form.uploadLoading
? 'LoadingOutlined'
: 'PlusOutlined'
"
/>
<div class="ant-upload-text">
点击上传图片
</div>
</div>
</j-upload>
</j-form-item>
<j-form-item
label="用户信息地址"
:name="[
'sso',
'configuration',
'oauth2',
'userInfoUrl',
]"
:rules="[
{
required: true,
message: '请输入用户信息地址',
},
]"
>
<j-input
v-model:value="
form.data.sso.configuration.oauth2
.userInfoUrl
"
placeholder="请输入用户信息地址"
/>
</j-form-item>
<j-form-item
class="resetLabel"
:name="[
'sso',
'configuration',
'oauth2',
'userProperty',
'userId',
]"
:rules="[
{
required: true,
message: '请输入用户ID',
},
]"
>
<template #label>
<FormLabel
text="用户ID"
required
tooltip="通过jsonpath表达式从授权结果中获取第三方平台用户的唯一标识"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.oauth2
.userProperty.userId
"
placeholder="输入从用户信息接口返回数据中的用户ID字段。示例:result.id"
/>
</j-form-item>
<j-form-item
label="用户名"
:name="[
'sso',
'configuration',
'oauth2',
'userProperty',
'username',
]"
:rules="[
{
required: true,
message: '请输入用户名',
},
]"
>
<j-input
v-model:value="
form.data.sso.configuration.oauth2
.userProperty.username
"
placeholder="输入从用户信息接口返回数据中的用户名字段。示例:result.name"
/>
</j-form-item>
<j-form-item label="头像">
<j-input
v-model:value="
form.data.sso.configuration.oauth2
.userProperty.avatar
"
placeholder="输入从用户信息接口返回数据中的用户头像字段。示例:result.avatar"
/>
</j-form-item>
</div>
<!-- 非第三方应用 -->
<div
v-else-if="
!form.data.integrationModes.includes('apiClient')
"
>
<j-form-item
v-if="form.data.provider === 'internal-standalone'"
>
<template #label>
<FormLabel
text="回调地址"
tooltip="授权完成后跳转到具体页面的回调地址"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.oauth2
.redirectUri
"
placeholder="请输入回调地址"
/>
</j-form-item>
<!-- 非钉钉 -->
<j-form-item
v-if="form.data.provider !== 'dingtalk-ent-app'"
class="resetLabel"
:name="['sso', 'configuration', 'appId']"
:rules="[
{
required: true,
message: '请输入appId',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="appId"
required
tooltip="第三方应用唯一标识"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.appId
"
placeholder="请输入appId"
/>
</j-form-item>
<!-- 非微信 -->
<j-form-item
v-if="form.data.provider !== 'wechat-webapp'"
class="resetLabel"
:name="['sso', 'configuration', 'appKey']"
:rules="[
{
required: true,
message: '请输入appKey',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="appKey"
required
tooltip="第三方应用唯一标识的密钥"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.appKey
"
placeholder="请输入appKey"
/>
</j-form-item>
<!-- 钉钉 + 微信 -->
<j-form-item
v-if="
form.data.provider === 'wechat-webapp' ||
form.data.provider === 'dingtalk-ent-app'
"
class="resetLabel"
:name="['sso', 'configuration', 'appSecret']"
:rules="[
{
required: true,
message: '请输入appSecret',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<template #label>
<FormLabel
text="appSecret"
required
tooltip="应用的唯一标识的秘钥"
/>
</template>
<j-input
v-model:value="
form.data.sso.configuration.appSecret
"
placeholder="请输入appSecret"
/>
</j-form-item>
</div>
<j-form-item class="resetLabel">
<template #label>
<FormLabel
text="自动创建用户"
required
tooltip="开启后,第三方用户第一次授权登录系统时,无需进入授权绑定页面。系统默认创建一个新用户与之绑定。"
/>
</template>
<j-switch
v-model:checked="form.data.sso.autoCreateUser"
/>
</j-form-item>
<div v-if="form.data.sso.autoCreateUser">
<j-form-item
label="用户名前缀"
:name="['sso', 'usernamePrefix']"
:rules="[
{
required: true,
message: '请输入用户名前缀',
},
]"
>
<j-input
v-model:value="form.data.sso.usernamePrefix"
placeholder="请输入用户名前缀"
/>
</j-form-item>
<j-form-item
label="默认密码"
:name="['sso', 'defaultPasswd']"
:rules="[
{
required: true,
message: '请输入默认密码',
},
{
max: 64,
message: '最多可输入64个字符',
},
]"
>
<j-input
v-model:value="form.data.sso.defaultPasswd"
placeholder="请输入默认密码"
/>
</j-form-item>
<j-form-item label="角色">
<j-select
v-model:value="form.data.sso.roleIdList"
mode="multiple"
:options="form.roleIdList"
placeholder="请选择角色"
></j-select>
<PermissionButton
:hasPermission="`${rolePermission}:update`"
type="link"
@click="
clickAddItem(
form.data.sso.roleIdList,
'Role',
)
"
class="add-item"
>
<AIcon type="PlusOutlined" />
</PermissionButton>
</j-form-item>
<j-form-item label="组织">
<j-tree-select
v-model:value="form.data.sso.orgIdList"
show-search
style="width: 100%"
:dropdown-style="{
maxHeight: '400px',
overflow: 'auto',
}"
:fieldNames="{
label: 'name',
value: 'id',
}"
multiple
:tree-data="form.orgIdList"
placeholder="请选择组织"
>
<template #title="{ name }">
{{ name }}
</template>
</j-tree-select>
<PermissionButton
:hasPermission="`${deptPermission}:update`"
type="link"
@click="
clickAddItem(
form.data.sso.orgIdList,
'Role',
)
"
class="add-item"
>
<AIcon type="PlusOutlined" />
</PermissionButton>
</j-form-item>
</div>
</j-collapse-panel>
</j-collapse>
<j-form-item label="说明" name="description">
<j-textarea
v-model:value="form.data.description"
placeholder="请输入说明"
showCount
:maxlength="200"
:rows="3"
style="width: 100%"
/>
</j-form-item>
</j-form>
<j-button
v-if="routeQuery.view !== 'true'"
@click="clickSave"
type="primary"
>
保存
</j-button>
<div class="dialog">
<MenuDialog
v-if="dialog.visible"
v-model:visible="dialog.visible"
:id="dialog.selectId"
:provider="dialog.selectProvider"
:mode="routeQuery.id ? 'edit' : 'add'"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { BASE_API_PATH, TOKEN_KEY } from '@/utils/variable';
import { LocalStore, filterSelectNode } from '@/utils/comm';
import { testIP } from '@/utils/validate';
import {
getDepartmentList_api,
addApp_api,
updateApp_api,
getAppInfo_api,
} from '@/api/system/apply';
import FormLabel from './FormLabel.vue';
import RequestTable from './RequestTable.vue';
import MenuDialog from '../../componenets/MenuDialog.vue';
import { getImage } from '@/utils/comm';
import type { formType, dictType, optionsType } from '../typing';
import { getRoleList_api } from '@/api/system/user';
import {
FormInstance,
message,
UploadChangeParam,
UploadFile,
} from 'ant-design-vue';
import { randomString } from '@/utils/utils';
import { cloneDeep, difference } from 'lodash';
import { useMenuStore } from '@/store/menu';
import { Rule } from 'ant-design-vue/lib/form';
const emit = defineEmits(['changeApplyType']);
const routeQuery = useRoute().query;
const menuStory = useMenuStore();
const deptPermission = 'system/Department';
const rolePermission = 'system/Role';
// 初始化表单
const initForm: formType = {
name: '',
provider: 'internal-standalone',
integrationModes: [],
description: '',
page: {
// 页面集成
baseUrl: '',
routeType: 'hash',
parameters: [],
},
apiClient: {
// API客户端
baseUrl: '',
headers: [], // 请求头
parameters: [], // 请求参数
authConfig: {
// API客户端
type: 'oauth2', // 类型, 可选值none, bearer, oauth2, basic, other
bearer: { token: '' }, // 授权信息
basic: { username: '', password: '' }, // 基本信息
// token: '',
oauth2: {
// OAuth2信息
authorizationUrl: '', // 授权地址
tokenUrl: '', // token地址
redirectUri: '', // 重定向地址
clientId: '', // 客户端ID
clientSecret: '', // 客户端密钥
grantType: '', // 类型
accessTokenProperty: '', // token属性名
tokenRequestType: undefined, // token请求方式, 可选值POST_URIPOST_BODY
},
},
},
apiServer: {
// API服务
appId: randomString(16),
secureKey: randomString(), // 密钥
redirectUri: '', // 重定向URL
roleIdList: [], // 角色列表
orgIdList: [], // 部门列表
ipWhiteList: '', // IP白名单
enableOAuth2: false, // 是否启用OAuth2
},
sso: {
// 统一单点登陆集成
configuration: {
// 配置
oauth2: {
// Oauth2单点登录配置
authorizationUrl: '', // 授权地址
redirectUri: '', // 重定向地址
clientId: '', // 客户端ID
clientSecret: '', // 客户端密钥
userInfoUrl: '', // 用户信息接口
scope: '', // scope
userProperty: {
// 用户属性字段信息
userId: '', // 用户ID
username: '', // 用户名
name: '', // 名称
avatar: '', // 头像
email: '', // 邮箱
telephone: '', // 电话
description: '', // 说明
},
grantType: '', // 类型
tokenUrl: '', // token地址
accessTokenProperty: '', // token属性名
tokenRequestType: '', // token请求方式
},
appId: '', // 微信单点登录配置
appKey: '', // 钉钉单点登录配置
appSecret: '', // 钉钉、微信单点登录配置
},
autoCreateUser: false, // 是否自动创建平台用户
usernamePrefix: '', // 用户ID前缀
roleIdList: [], // 自动创建平台用户时角色列表
orgIdList: [], // 自动创建平台用户时部门列表
defaultPasswd: '', // 默认密码
},
};
const formRef = ref<FormInstance>();
const form = reactive({
data: { ...initForm },
integrationModesISO: [] as string[], // 接入方式镜像 折叠面板使用
roleIdList: [] as optionsType, // 角色列表
orgIdList: [] as dictType, // 组织列表
errorNumInfo: {
page: new Set(),
apiClient: new Set(),
apiServer: new Set(),
ssoClient: new Set(),
},
fileList: [] as any[],
uploadLoading: false,
});
// 请求头和参数必填验证
const headerValid = ref(true);
const paramsValid = ref(true);
const headerValidator = () => {
return new Promise((resolve, reject) => {
headerValid.value ? resolve('') : reject('请输入完整的请求头');
});
};
const paramsValidator = () => {
return new Promise((resolve, reject) => {
paramsValid.value ? resolve('') : reject('请输入完整的请求参数');
});
};
// 接入方式的选项
const joinOptions = computed(() => {
if (form.data.provider === 'internal-standalone')
return [
{
label: '页面集成',
value: 'page',
},
{
label: 'API客户端',
value: 'apiClient',
},
{
label: 'API服务',
value: 'apiServer',
},
{
label: '单点登录',
value: 'ssoClient',
},
];
else if (form.data.provider === 'internal-integrated')
return [
{
label: '页面集成',
value: 'page',
},
{
label: 'API客户端',
value: 'apiClient',
},
];
else if (form.data.provider === 'wechat-webapp')
return [
{
label: '单点登录',
value: 'ssoClient',
},
];
else if (form.data.provider === 'dingtalk-ent-app')
return [
{
label: '单点登录',
value: 'ssoClient',
},
];
else if (form.data.provider === 'third-party')
return [
{
label: '页面集成',
value: 'page',
},
{
label: 'API客户端',
value: 'apiClient',
},
{
label: 'API服务',
value: 'apiServer',
},
{
label: '单点登录',
value: 'ssoClient',
},
];
});
const dialog = reactive({
visible: false,
selectId: '',
selectProvider: '' as any,
});
init();
function init() {
getRoleIdList();
getOrgIdList();
if (routeQuery.id) getInfo(routeQuery.id as string);
watch(
() => form.data.provider,
(n) => {
if (!form.data.id) {
// 新增时, 切换应用类型, 清空公用字段的值
form.data.page.baseUrl = '';
form.data.apiClient.baseUrl = '';
form.data.page.parameters = [];
form.data.apiClient.parameters = [];
form.data.apiClient.authConfig.oauth2.authorizationUrl = '';
form.data.sso.configuration.oauth2.authorizationUrl = '';
form.data.apiClient.authConfig.oauth2.clientId = '';
form.data.sso.configuration.oauth2.clientId = '';
form.data.apiClient.authConfig.oauth2.clientSecret = '';
form.data.sso.configuration.oauth2.clientSecret = '';
form.data.apiClient.headers = [];
form.data.apiServer.roleIdList = [];
form.data.apiServer.orgIdList = [];
form.data.description = '';
form.data.apiServer.redirectUri = '';
form.data.sso.configuration.appSecret = '';
// formRef.value?.resetFields();
}
emit('changeApplyType', n);
if (routeQuery.id) return;
if (n === 'wechat-webapp' || n === 'dingtalk-ent-app') {
form.data.integrationModes = ['ssoClient'];
form.integrationModesISO = ['ssoClient'];
} else form.data.integrationModes = [];
},
{ immediate: true },
);
watch(
() => form.data.integrationModes,
(n, o) => {
o.forEach((key) => {
if (!n.includes(key)) form.errorNumInfo[key].clear();
});
form.integrationModesISO = [...n];
},
);
}
function getInfo(id: string) {
getAppInfo_api(id).then((resp: any) => {
// 后端返回的headers和parameters中, key转为label
if (resp.result.apiClient) {
resp.result.apiClient.headers = resp.result.apiClient.headers?.map(
(m: any) => ({
...m,
label: m.key,
}),
);
resp.result.apiClient.parameters =
resp.result.apiClient.parameters?.map((m: any) => ({
...m,
label: m.key,
}));
}
if (resp.result.page) {
resp.result.page.parameters = resp.result.page.parameters?.map(
(m: any) => ({
...m,
label: m.key,
}),
);
}
form.data = {
...initForm, // 查询详情, 赋值初始字段. 解决编辑改变接入方式报错的问题: bug#10892
...resp.result,
integrationModes: resp.result.integrationModes?.map(
(item: any) => item.value,
),
} as formType;
form.data.apiServer && (form.data.apiServer.appId = id);
});
}
// 获取角色列表
function getRoleIdList() {
getRoleList_api().then((resp) => {
if (resp.status === 200) {
const result = resp.result as dictType;
form.roleIdList = result?.map((item) => ({
label: item.name,
value: item.id,
}));
}
});
}
// 获取组织列表
function getOrgIdList() {
getDepartmentList_api({ paging: false }).then((resp) => {
if (resp.status === 200) {
form.orgIdList = resp.result as dictType;
}
});
}
// 添加角色/组织
function clickAddItem(data: string[], target: string) {
const tab: any = window.open(`${origin}/#/system/${target}?save=true`);
tab.onTabSaveSuccess = (value: string) => {
data.push(value);
if (target === 'Role') getRoleIdList();
else getOrgIdList();
};
}
// 保存
function clickSave() {
formRef.value?.validate().then(() => {
const params = cloneDeep(form.data);
// 删除多余的参数
const list = ['page', 'apiClient', 'apiServer', 'ssoClient'];
difference(list, params.integrationModes).forEach((item) => {
if (item === 'ssoClient') {
// @ts-ignore
delete params['sso'];
}
delete params[item];
});
clearNullProp(params);
if (
params.provider === 'internal-standalone' &&
params.integrationModes.includes('page')
) {
// @ts-ignore
delete params.page.parameters;
}
if (
params.provider === 'internal-standalone' &&
params.integrationModes.includes('ssoClient') &&
params.integrationModes.length === 1
)
return message.warning('配置单点登录需同时配置api配置');
//独立应用-api客户端 id?clientId:appId
if (params.provider === 'internal-standalone') {
if (params.integrationModes.includes('apiClient')) {
params.id = params.apiClient.authConfig.oauth2.clientId;
}
if (
params.integrationModes.includes('apiServer') &&
!params.integrationModes.includes('apiClient')
) {
params.id = params.apiServer.appId;
}
}
// headers和params参数label需改为key传给后端
if (params.integrationModes.includes('apiClient')) {
params.apiClient.headers = params.apiClient.headers?.map(
(m: any) => ({
...m,
key: m.label,
}),
);
params.apiClient.parameters = params.apiClient.parameters?.map(
(m: any) => ({
...m,
key: m.label,
}),
);
}
if (params.integrationModes.includes('page')) {
params.page.parameters = params.page.parameters?.map((m: any) => ({
...m,
key: m.label,
}));
}
const request = routeQuery.id
? updateApp_api(routeQuery.id as string, params)
: addApp_api(params);
request.then((resp: any) => {
if (resp.status === 200) {
const isPage = params.integrationModes.includes('page');
if (isPage) {
// form.data = params;
dialog.selectId = routeQuery.id || resp.result.id;
dialog.selectProvider = form.data.provider;
dialog.visible = true;
} else {
message.success('保存成功');
menuStory.jumpPage('system/Apply');
}
}
});
});
}
function getErrorNum(
name: string | number | string[] | number[],
status: boolean,
) {
if (typeof name !== 'object') return;
const props = ['page', 'apiClient', 'apiServer', 'ssoClient'];
const prop = name[0] === 'sso' ? 'ssoClient' : name[0];
if (props.includes(prop + '')) {
const key = name.slice(1).join();
const set = form.errorNumInfo[prop] as Set<string>;
// 如果此项校验成功且在失败列表中,则从此列表中移除,反之则加上
if (status) {
if (set.has(key)) set.delete(key);
} else if (!set.has(key)) set.add(key);
}
}
const imageTypes = ref(['image/jpg', 'image/png', 'image/jpeg']);
const beforeLogoUpload = (file: any) => {
const isType: any = imageTypes.value.includes(file.type);
if (!isType) {
message.error(`请上传.jpg.png格式的图片`);
return false;
}
const isSize = file.size / 1024 / 1024 < 4;
if (!isSize) {
message.error(`图片大小必须小于${4}M`);
}
return isType && isSize;
};
function changeBackUpload(info: UploadChangeParam<UploadFile<any>>) {
if (info.file.status === 'uploading') {
form.uploadLoading = true;
} else if (info.file.status === 'done') {
info.file.url = info.file.response?.result;
form.uploadLoading = false;
form.data.sso.configuration.oauth2.logoUrl = info.file.response?.result;
} else if (info.file.status === 'error') {
form.uploadLoading = false;
message.error('logo上传失败请稍后再试');
}
}
function clearNullProp(obj: object) {
if (typeof obj !== 'object') return;
for (const prop in obj) {
if (Object.prototype.hasOwnProperty.call(obj, prop)) {
const val = obj[prop];
if (val === '') delete obj[prop];
else if (typeof val === 'object') clearNullProp(obj[prop]);
}
}
}
/**
* 验证IP合法性
* @param _rule
* @param value
*/
const validateIP = (_rule: Rule, value: string) => {
const ipList = value?.split(/[\n,]/g).filter((i: string) => i && i.trim());
const errorIPList = ipList.filter(
(f: string) => !testIP(f.replace(/\s*/g, '')),
);
return new Promise((resolve, reject) => {
!errorIPList.length
? resolve('')
: reject(`[${errorIPList}]不是正确的IP地址`);
});
};
</script>
<style lang="less" scoped>
.edit-form-container {
.form {
.ant-form-item {
&.resetLabel {
:deep(.ant-form-item-required) {
&::before {
display: none;
}
}
}
:deep(.ant-form-item-control) {
.ant-form-item-control-input-content {
display: flex;
.ant-upload-select-picture-card {
width: auto;
height: auto;
max-width: 150px;
max-height: 150px;
> .ant-upload {
height: 150px;
}
}
}
}
}
.radio-container {
.ant-radio-button-wrapper {
height: 120px;
width: 120px;
padding: 0 15px;
box-sizing: content-box;
margin-right: 20px;
color: #000;
&.ant-radio-button-wrapper-disabled {
opacity: 0.5;
}
&.ant-radio-button-wrapper-checked {
background-color: #fff;
border: 1px solid #1d39c4;
opacity: 1;
}
> :last-child {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
> div {
width: 100%;
text-align: center;
}
:deep(.ant-image) {
width: 64px;
height: 64px;
}
p {
margin: 0;
}
}
}
}
:deep(.ant-collapse-header) {
> span {
position: relative;
.error-info {
position: absolute;
text-align: center;
line-height: 14px;
min-width: 14px;
min-height: 14px;
right: -15px;
top: -5px;
font-size: 8px;
background-color: #ff4d4f;
color: #fff;
border-radius: 7px;
}
}
}
}
}
</style>