Merge branch 'dev' of github.com:jetlinks/jetlinks-ui-vue into dev

This commit is contained in:
easy 2023-01-11 18:12:21 +08:00
commit fed499e18c
9 changed files with 447 additions and 31 deletions

5
components.d.ts vendored
View File

@ -11,6 +11,7 @@ declare module '@vue/runtime-core' {
AButton: typeof import('ant-design-vue/es')['Button']
ACard: typeof import('ant-design-vue/es')['Card']
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
ACheckboxGroup: typeof import('ant-design-vue/es')['CheckboxGroup']
ACol: typeof import('ant-design-vue/es')['Col']
ADatePicker: typeof import('ant-design-vue/es')['DatePicker']
ADivider: typeof import('ant-design-vue/es')['Divider']
@ -23,15 +24,19 @@ declare module '@vue/runtime-core' {
AModal: typeof import('ant-design-vue/es')['Modal']
APagination: typeof import('ant-design-vue/es')['Pagination']
APopconfirm: typeof import('ant-design-vue/es')['Popconfirm']
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASpace: typeof import('ant-design-vue/es')['Space']
ASpin: typeof import('ant-design-vue/es')['Spin']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
ATree: typeof import('ant-design-vue/es')['Tree']
ATreeSelect: typeof import('ant-design-vue/es')['TreeSelect']
AUpload: typeof import('ant-design-vue/es')['Upload']
BadgeStatus: typeof import('./src/components/BadgeStatus/index.vue')['default']
CardBox: typeof import('./src/components/CardBox/index.vue')['default']

View File

@ -1,4 +1,6 @@
import { get } from '@/utils/request'
import { get, post } from '@/utils/request'
// 三方应用账户信息
export const applicationInfo = (code: string) => get(`/application/sso/bind-code/${code}`)
export const applicationInfo = (code: string): any => get(`/application/sso/bind-code/${code}`)
// 立即绑定
export const bindAccount = (code: string): any => post(`/application/sso/me/bind/${code}`)

View File

@ -1,11 +1,252 @@
<template>
<div class=''>
<div class='JForm-content'>
<a-form
ref='form'
v-bind='props'
:model='formData.data'
layout='vertical'
>
<a-row :type='rowType'>
<a-col v-for='item in formOptions.data' :key='item.key' :span='item.span'>
<a-form-item
:name='item.name'
:required='item.required'
:rules='item.rules'
:noStyle='item.noStyle'
>
<template #label>
<span>{{ item.title }}</span>
<a-tooltip :title='item.tooltip'>
<QuestionCircleOutlined v-if='!!item.tooltip' style='margin-left: 4px; color: rgba(0,0,0,.45) ' />
</a-tooltip>
</template>
<a-input
v-if='item.component === componentType.input'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
/>
<a-select
v-else-if='item.component === componentType.select'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
:options='item.options'
/>
<a-inputnumber
v-else-if='item.component === componentType.inputNumber'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
/>
<a-input-password
v-else-if='item.component === componentType.password'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
/>
<a-switch
v-else-if='item.component === componentType.switch'
v-bind='item.componentProps'
v-model:checked='formData.data[item.name]'
/>
<a-radio-group
v-else-if='item.component === componentType.radio'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
/>
<a-checkbox-group
v-else-if='item.component === componentType.checkbox'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
:options='item.options'
/>
<a-time-picker
v-else-if='item.component === componentType.time'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
/>
<a-date-picker
v-else-if='item.component === componentType.date'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
/>
<a-tree-select
v-else-if='item.component === componentType.tree'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
:tree-data='item.options'
/>
<a-upload
v-else-if='item.component === componentType.upload'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
/>
<component
v-else
:is='item.component'
v-bind='item.componentProps'
v-model:value='formData.data[item.name]'
/>
</a-form-item>
</a-col>
</a-row>
</a-form>
</div>
</template>
<script setup type='ts' name='FormBuilder'>
const data = reactive({})
<script setup lang='ts' name='FormBuilder'>
import type { Options, OptionsItem, OptionsComponent } from './index.modules'
import { PropType } from 'vue'
import { get, isArray, isString, pick, set } from 'lodash-es'
import { formProps } from 'ant-design-vue/es/form'
import componentType from './util'
import {
QuestionCircleOutlined
} from '@ant-design/icons-vue';
const form = ref()
const props = defineProps({
...formProps,
options: {
type: Object as PropType<Options>,
default: () => []
},
initValue: {
type: Object,
default: () => ({})
},
column: {
type: Number,
default: 1
}
})
//
const formData = reactive({
data: {}
})
const formOptions = reactive<{ data: OptionsComponent[]}>({
data: []
}) // Item
const rowType = ref<string | undefined>(undefined)
const calculateItemSpan = (span?: number | string) => {
const itemSpan = 24 / props.column
if (!span) return itemSpan
if (isString(span) && span.includes('px')) {
rowType.value = 'flex'
} else {
return span
}
}
/**
* 根据传入的表单options生成表单
* @param data {Options}
* @param parentKey
*/
const handleFormData = (data: Options, parentKey: Array<string> = []): any => {
const cacheModel: any = {}
Object.keys(data).forEach(async (key) => {
const optionItem = data[key]
const _key = [...parentKey, key]
if ('type' in optionItem && optionItem.type === 'Object') {
const dataModel = handleFormData(optionItem.properties, _key)
cacheModel[key] = dataModel
} else if (!('visible' in optionItem) || ('visible' in optionItem && optionItem.visible !== true)){
//
const keyValue = get(formData.data, _key)
let _options: any[] = []
if (keyValue) { // formModel
cacheModel[key] = keyValue
} else {
cacheModel[key] = (optionItem as OptionsItem).default
}
// options
if ('options' in optionItem) {
_options = optionItem.options!
}
// onSearch
if ('onSearch' in optionItem) {
const data = await optionItem.onSearch!()
if (data) {
_options = data
}
}
const optionsItemProps = pick(optionItem, ['componentProps', 'title', 'component', 'rules', 'required', 'hidden', 'tooltip', 'noStyle'])
//
formOptions.data.push({
...optionsItemProps,
name: _key,
options: _options,
key: isArray(_key) ? _key.toString() : _key,
span: calculateItemSpan((optionItem as OptionsItem).span)
})
}
})
return cacheModel
}
/**
* 重置表单
*/
const resetModel = () => {
form.value.resetFields()
}
/**
* 验证并提交表单
*/
const formValidate = () => {
return new Promise((res, rej) => {
form.value.validate().then(() => {
res(formData.data)
}).catch((err: any) => {
rej(err)
})
})
}
/**
* 改变单个值
*/
const setItemValue = (key: string | (string | number)[], value: any) => {
set(formData.data, key, value)
}
/**
* 修改整个表单值
* @param data
*/
const setData = (data: any) => {
formData.data = data
}
if (props.initValue) {
formData.data = props.initValue
}
formData.data = handleFormData(props.options)
watch(props.options, (newValue: any) => {
formOptions.data = []
formData.data = handleFormData(newValue)
})
watch(props.initValue, (newValue: any) => {
formData.data = newValue
})
defineExpose({
resetModel,
formValidate,
setItemValue,
setData
})
</script>
<style scoped>

40
src/components/Form/index.modules.d.ts vendored Normal file
View File

@ -0,0 +1,40 @@
import type { FormProps } from 'ant-design-vue/es/form'
export interface OptionsComponent {
/** FormItem title **/
title?: string
/** 组件名称 **/
component?: string
/** 组件Props **/
componentProps?: any
/** 组件Options **/
options?: any[]
name?: any
[name: string]: any
}
export interface OptionsItem extends OptionsComponent{
/** 内置查询会覆盖options **/
onSearch?: () => Promise<any>
default?: any
/** 隐藏Item值不会进入到FormModel中 **/
visible?: boolean
/** 表单隐藏域 **/
hidden?: boolean,
span?: number | string
rules?: FormProps.rules
required?: boolean
tooltip?: string
noStyle?: boolean
}
interface ObjectTypes {
type: 'Object'
properties: {
[name: string]: OptionsItem
}
}
export interface Options extends FormProps {
[name: string]: ObjectTypes | OptionsItem
}

View File

@ -1,3 +1,4 @@
import FormBuilder from './FormBuilder.vue'
export { default as componentType } from './util'
export default FormBuilder

View File

@ -0,0 +1,15 @@
const optionComponentType = {
input: 'input',
inputNumber: 'inputNumber',
password: 'password',
switch: 'switch',
radio: 'radio',
checkbox: 'checkbox',
time: 'time',
date: 'date',
treeSelect: 'treeSelect',
upload: 'upload',
tree: 'tree',
select: 'select'
}
export default optionComponentType

View File

@ -0,0 +1,4 @@
.ant-form-item-required:before {
position: absolute;
right: -12px;
}

View File

@ -4,7 +4,7 @@
<div class="content">
<div class="title">第三方账户绑定</div>
<!-- 已登录-绑定三方账号 -->
<template v-if="false">
<template v-if="!token">
<div class="info">
<a-card style="width: 280px">
<template #title>
@ -28,14 +28,21 @@
</div>
</template>
<div class="info-body">
<img :src="getImage('/bind/wechat-webapp.png')" />
<img
:src="
accountInfo?.avatar ||
getImage('/bind/wechat-webapp.png')
"
/>
<p>用户名-</p>
<p>名称微信昵称</p>
<p>名称{{ accountInfo?.name || '-' }}</p>
</div>
</a-card>
</div>
<div class="btn">
<a-button type="primary">立即绑定</a-button>
<a-button type="primary" @click="handleBind"
>立即绑定</a-button
>
</div>
</template>
<!-- 未登录-绑定三方账号 -->
@ -74,23 +81,25 @@
</a-form-item>
<a-form-item
label="验证码"
v-bind="validateInfos.captcha"
v-bind="validateInfos.verifyCode"
>
<a-input
v-model:value="formData.captcha"
v-model:value="formData.verifyCode"
placeholder="请输入验证码"
>
<template #addonAfter>
<span style="cursor: pointer">
图形验证码
</span>
<img
:src="captcha.base64"
@click="getCode"
style="cursor: pointer"
/>
</template>
</a-input>
</a-form-item>
<a-form-item>
<a-button
type="primary"
@click="handleSubmit"
@click="handleLoginBind"
style="width: 100%"
>
登录并绑定账户
@ -105,32 +114,58 @@
</template>
<script setup lang="ts">
import { getImage } from '@/utils/comm';
import { getImage, LocalStore } from '@/utils/comm';
import { TOKEN_KEY } from '@/utils/variable';
import { Form } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { applicationInfo } from '@/api/bind';
import { applicationInfo, bindAccount } from '@/api/bind';
import { code, authLogin } from '@/api/login';
const useForm = Form.useForm;
interface formData {
username: string;
password: string;
captcha: string;
verifyCode: string;
}
const token = computed(() => LocalStore.get(TOKEN_KEY));
//
const getUrlCode = () => {
const url = new URLSearchParams(window.location.href);
return url.get('code') as string;
};
//
const accountInfo = ref({
avatar: '',
name: '',
});
const getAppInfo = async () => {
const code: string = '73ab60c88979a1475963a5dde31e374b';
const code = getUrlCode();
const res = await applicationInfo(code);
console.log('getAppInfo: ', res);
accountInfo.value = res?.result?.result;
};
getAppInfo();
//
/**
* 立即绑定
*/
const handleBind = async () => {
const code = getUrlCode();
const res = await bindAccount(code);
console.log('bindAccount: ', res);
message.success('绑定成功');
goRedirect();
setTimeout(() => window.close(), 1000);
};
// -
const formData = ref<formData>({
username: '',
password: '',
captcha: '',
verifyCode: '',
});
const formRules = ref({
username: [
@ -145,7 +180,7 @@ const formRules = ref({
message: '请输入密码',
},
],
captcha: [
verifyCode: [
{
required: true,
message: '请输入验证码',
@ -158,17 +193,40 @@ const { resetFields, validate, validateInfos } = useForm(
formRules.value,
);
/**
* 获取图形验证码
*/
const captcha = ref({
base64: '',
key: '',
});
const getCode = async () => {
const res: any = await code();
captcha.value = res.result;
};
getCode();
/**
* 登录并绑定账户
*/
const handleSubmit = () => {
const handleLoginBind = () => {
validate()
.then(() => {
console.log('toRaw:', toRaw(formData.value));
console.log('formData.value:', formData.value);
.then(async () => {
const code = getUrlCode();
const params = {
...formData.value,
verifyKey: captcha.value.key,
bindCode: code,
expires: 3600000,
};
const res = await authLogin(params);
console.log('res: ', res);
message.success('登录成功');
goRedirect();
setTimeout(() => window.close(), 1000);
})
.catch((err) => {
console.log('error', err);
getCode();
});
};

View File

@ -1,9 +1,59 @@
<template>
<Form />
<Form
ref='form'
:options='options'
:initValue='initValue'
/>
<a-button @click='submit'>提交</a-button>
<a-button @click='reset'>重置</a-button>
<a-button @click='setValue'>修改name</a-button>
</template>
<script setup name='FormDemo'>
const data = reactive({})
import { componentType } from 'components/Form'
const form = ref()
const initValue = reactive({})
const submit = () => {
form.value.formValidate().then(res => {
console.log(res)
})
}
const reset = () => {
}
const setValue =() => {
initValue.name = '111111'
}
const options = reactive({
name: {
component: componentType.input,
componentProps: {
style: {
width: '200px'
}
},
title: '测试',
required: true
},
sex: {
component: componentType.select,
title: '性别',
options: [
{ label: '111', value: 1 },
{ label: '222', value: 2 },
],
required: true,
rules: [
{ required: true, message: '请选择性别'}
],
tooltip: '性别',
default: 1
}
})
</script>
<style scoped>