Browse Source

Merge pull request #785 from colinin/form-design

Form design
pull/786/head
yx lin 3 years ago
committed by GitHub
parent
commit
0f2d9a4727
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      apps/vue/package.json
  2. 1
      apps/vue/src/api/messages/model/notificationsModel.ts
  3. 4
      apps/vue/src/components/FormDesign/index.ts
  4. 78
      apps/vue/src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue
  5. 135
      apps/vue/src/components/FormDesign/src/components/VFormCreate/index.vue
  6. 74
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue
  7. 234
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue
  8. 60
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue
  9. 139
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue
  10. 53
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue
  11. 72
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue
  12. 147
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue
  13. 110
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue
  14. 119
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue
  15. 60
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue
  16. 121
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue
  17. 75
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue
  18. 285
      apps/vue/src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue
  19. 1241
      apps/vue/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts
  20. 351
      apps/vue/src/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts
  21. 324
      apps/vue/src/components/FormDesign/src/components/VFormDesign/index.vue
  22. 101
      apps/vue/src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue
  23. 150
      apps/vue/src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue
  24. 91
      apps/vue/src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue
  25. 128
      apps/vue/src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue
  26. 226
      apps/vue/src/components/FormDesign/src/components/VFormDesign/styles/drag.less
  27. 15
      apps/vue/src/components/FormDesign/src/components/VFormDesign/styles/variable.less
  28. 0
      apps/vue/src/components/FormDesign/src/components/VFormDesign/styles/variable.wxss
  29. 204
      apps/vue/src/components/FormDesign/src/components/VFormItem/index.vue
  30. 64
      apps/vue/src/components/FormDesign/src/components/VFormItem/vFormItem.vue
  31. 98
      apps/vue/src/components/FormDesign/src/components/VFormPreview/index.vue
  32. 65
      apps/vue/src/components/FormDesign/src/components/VFormPreview/useForm.vue
  33. 74
      apps/vue/src/components/FormDesign/src/components/index.ts
  34. 493
      apps/vue/src/components/FormDesign/src/core/formItemConfig.ts
  35. 739
      apps/vue/src/components/FormDesign/src/core/iconConfig.ts
  36. 18
      apps/vue/src/components/FormDesign/src/hooks/useFormDesignState.ts
  37. 62
      apps/vue/src/components/FormDesign/src/hooks/useFormInstanceMethods.ts
  38. 195
      apps/vue/src/components/FormDesign/src/hooks/useVFormMethods.ts
  39. 10
      apps/vue/src/components/FormDesign/src/typings/base-type.ts
  40. 52
      apps/vue/src/components/FormDesign/src/typings/form-type.ts
  41. 349
      apps/vue/src/components/FormDesign/src/typings/v-form-component.ts
  42. 200
      apps/vue/src/components/FormDesign/src/utils/index.ts
  43. 18
      apps/vue/src/components/FormDesign/src/utils/message.ts
  44. 2
      apps/vue/src/components/Table/src/components/settings/ColumnSetting.vue
  45. 2
      apps/vue/src/enums/imEnum.ts
  46. 2
      apps/vue/src/layouts/default/header/components/notify/useMessages.ts
  47. 68
      apps/vue/src/layouts/default/header/components/notify/useNotifications.ts
  48. 4
      apps/vue/src/layouts/page/index.vue
  49. 3
      apps/vue/src/utils/helper/treeHelper.ts
  50. 2
      apps/vue/types/abp.d.ts
  51. 14
      aspnet-core/modules/common/LINGYUN.Abp.Notifications.Core/LINGYUN/Abp/Notifications/NotificationData.cs
  52. 6
      aspnet-core/modules/common/LINGYUN.Abp.Notifications.Core/LINGYUN/Abp/Notifications/NotificationType.cs
  53. 41
      aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/DefaultNotificationDataSerializer.cs
  54. 5
      aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/INotificationDataSerializer.cs
  55. 39
      aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/NotificationDataConverter.cs
  56. 23
      aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Core/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs
  57. 21
      aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs
  58. 10
      aspnet-core/services/LINGYUN.Abp.Applications/AbpApplicationsModule.cs
  59. 14
      aspnet-core/services/LINGYUN.Abp.Applications/App.xaml
  60. 11
      aspnet-core/services/LINGYUN.Abp.Applications/App.xaml.cs
  61. 14
      aspnet-core/services/LINGYUN.Abp.Applications/AppShell.xaml
  62. 9
      aspnet-core/services/LINGYUN.Abp.Applications/AppShell.xaml.cs
  63. 64
      aspnet-core/services/LINGYUN.Abp.Applications/LINGYUN.Abp.Applications.csproj
  64. 41
      aspnet-core/services/LINGYUN.Abp.Applications/MainPage.xaml
  65. 24
      aspnet-core/services/LINGYUN.Abp.Applications/MainPage.xaml.cs
  66. 44
      aspnet-core/services/LINGYUN.Abp.Applications/MauiProgram.cs
  67. 6
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Android/AndroidManifest.xml
  68. 9
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Android/MainActivity.cs
  69. 14
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Android/MainApplication.cs
  70. 6
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Android/Resources/values/colors.xml
  71. 8
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/MacCatalyst/AppDelegate.cs
  72. 30
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/MacCatalyst/Info.plist
  73. 14
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/MacCatalyst/Program.cs
  74. 16
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Tizen/Main.cs
  75. 15
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Tizen/tizen-manifest.xml
  76. 8
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Windows/App.xaml
  77. 23
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Windows/App.xaml.cs
  78. 46
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Windows/Package.appxmanifest
  79. 15
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Windows/app.manifest
  80. 8
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/iOS/AppDelegate.cs
  81. 32
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/iOS/Info.plist
  82. 14
      aspnet-core/services/LINGYUN.Abp.Applications/Platforms/iOS/Program.cs
  83. 8
      aspnet-core/services/LINGYUN.Abp.Applications/Properties/launchSettings.json
  84. 4
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/AppIcon/appicon.svg
  85. 8
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/AppIcon/appiconfg.svg
  86. BIN
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/Fonts/OpenSans-Regular.ttf
  87. BIN
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/Fonts/OpenSans-Semibold.ttf
  88. 93
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/Images/dotnet_bot.svg
  89. 15
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/Raw/AboutAssets.txt
  90. 8
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/Splash/splash.svg
  91. 44
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/Styles/Colors.xaml
  92. 405
      aspnet-core/services/LINGYUN.Abp.Applications/Resources/Styles/Styles.xaml
  93. 3
      aspnet-core/services/LINGYUN.Abp.Applications/appsettings.json
  94. 9
      aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/EventBus/Distributed/NotificationEventHandler.cs

3
apps/vue/package.json

@ -80,7 +80,8 @@
"vxe-table": "^4.3.9",
"vxe-table-plugin-export-xlsx": "^3.0.4",
"xe-utils": "^3.5.7",
"xlsx": "^0.18.5"
"xlsx": "^0.18.5",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@commitlint/cli": "^16.2.3",

1
apps/vue/src/api/messages/model/notificationsModel.ts

@ -7,6 +7,7 @@ export enum NotificationType {
Application = 0,
System = 10,
User = 20,
ServiceCallback = 30,
}
export enum NotificationSeverity {

4
apps/vue/src/components/FormDesign/index.ts

@ -0,0 +1,4 @@
import VFormCreate from './src/components/VFormCreate/index.vue';
import VFormDesign from './src/components/VFormDesign/index.vue';
export { VFormCreate, VFormDesign };

78
apps/vue/src/components/FormDesign/src/components/VFormCreate/components/FormRender.vue

@ -0,0 +1,78 @@
<template>
<template v-if="['Grid'].includes(schema.component)">
<Row class="grid-row">
<Col
class="grid-col"
v-for="(colItem, index) in schema.columns"
:key="index"
:span="colItem.span"
>
<FormRender
v-for="(item, k) in colItem.children"
:key="k"
:schema="item"
:formData="formData"
:formConfig="formConfig"
:setFormModel="setFormModel"
/>
</Col>
</Row>
</template>
<VFormItem
v-else
:formConfig="formConfig"
:schema="schema"
:formData="formData"
:setFormModel="setFormModel"
@change="$emit('change', { schema: schema, value: $event })"
@submit="$emit('submit', schema)"
@reset="$emit('reset')"
>
<template
v-if="schema.componentProps && schema.componentProps.slotName"
#[schema.componentProps!.slotName]
>
<slot :name="schema.componentProps!.slotName"></slot>
</template>
</VFormItem>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { IVFormComponent, IFormConfig } from '../../../typings/v-form-component';
import VFormItem from '../../VFormItem/index.vue';
import { Row, Col } from 'ant-design-vue';
export default defineComponent({
name: 'FormRender',
components: {
VFormItem,
Row,
Col,
},
props: {
formData: {
type: Object,
default: () => ({}),
},
schema: {
type: Object as PropType<IVFormComponent>,
default: () => ({}),
},
formConfig: {
type: Object as PropType<IFormConfig>,
default: () => [] as IFormConfig[],
},
setFormModel: {
type: Function as PropType<(key: string, value: any) => void>,
default: null,
},
},
emits: ['change', 'submit', 'reset'],
setup(_props) {},
});
</script>
<style>
.v-form-render-item {
overflow: hidden;
}
</style>

135
apps/vue/src/components/FormDesign/src/components/VFormCreate/index.vue

@ -0,0 +1,135 @@
<!--
* @Description: 表单渲染器根据json生成表单
-->
<template>
<div class="v-form-container">
<Form class="v-form-model" ref="eFormModel" :model="formModel" v-bind="formModelProps">
<Row>
<FormRender
v-for="(schema, index) of noHiddenList"
:key="index"
:schema="schema"
:formConfig="formConfig"
:formData="formModelNew"
@change="handleChange"
:setFormModel="setFormModel"
@submit="handleSubmit"
@reset="resetFields"
>
<template v-if="schema && schema.componentProps" #[`schema.componentProps!.slotName`]>
<slot
:name="schema.componentProps!.slotName"
v-bind="{ formModel: formModel, field: schema.field, schema }"
></slot>
</template>
</FormRender>
</Row>
</Form>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, provide, ref, unref } from 'vue';
import FormRender from './components/FormRender.vue';
import { IFormConfig, AForm } from '../../typings/v-form-component';
import { Form, Row, Col } from 'ant-design-vue';
import { useFormInstanceMethods } from '../../hooks/useFormInstanceMethods';
import { IProps, IVFormMethods, useVFormMethods } from '../../hooks/useVFormMethods';
import { useVModel } from '@vueuse/core';
import { omit } from 'lodash-es';
export default defineComponent({
name: 'VFormCreate',
components: {
FormRender,
Form,
Row,
},
props: {
fApi: {
type: Object,
},
formModel: {
type: Object,
default: () => ({}),
},
formConfig: {
type: Object as PropType<IFormConfig>,
required: true,
},
},
emits: ['submit', 'change', 'update:fApi', 'update:formModel'],
setup(props, context) {
const wrapperComp = props.formConfig.layout == 'vertical' ? Col : Row;
const { emit } = context;
const eFormModel = ref<AForm | null>(null);
const formModelNew = computed({
get: () => props.formModel,
set: (value) => emit('update:formModel', value),
});
const noHiddenList = computed(() => {
return (
props.formConfig.schemas &&
props.formConfig.schemas.filter((item) => item.hidden !== true)
);
});
const fApi = useVModel(props, 'fApi', emit);
const { submit, validate, clearValidate, resetFields, validateField } =
useFormInstanceMethods(props, formModelNew, context, eFormModel);
const { linkOn, ...methods } = useVFormMethods(
{ formConfig: props.formConfig, formData: props.formModel } as unknown as IProps,
context,
eFormModel,
{
submit,
validate,
validateField,
resetFields,
clearValidate,
},
);
fApi.value = methods;
const handleChange = (_event) => {
const { schema, value } = _event;
const { field } = unref(schema);
linkOn[field!]?.forEach((formItem) => {
formItem.update?.(value, formItem, fApi.value as IVFormMethods);
});
};
/**
* 获取表单属性
*/
const formModelProps = computed(
() => omit(props.formConfig, ['disabled', 'labelWidth', 'schemas']) as Recordable,
);
const handleSubmit = () => {
submit();
};
provide('formModel', formModelNew);
const setFormModel = (key, value) => {
formModelNew.value[key] = value;
};
provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel);
// inject
return {
eFormModel,
submit,
validate,
validateField,
resetFields,
clearValidate,
handleChange,
formModelProps,
handleSubmit,
setFormModel,
formModelNew,
wrapperComp,
noHiddenList,
};
},
});
</script>
<style lang="less" scoped>
.v-form-model {
overflow: hidden;
}
</style>

74
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/CodeModal.vue

@ -0,0 +1,74 @@
<!--
* @Description: 渲染代码
-->
<template>
<Modal
title="代码"
:footer="null"
:visible="visible"
@cancel="visible = false"
wrapClassName="v-code-modal"
style="top: 20px"
width="850px"
:destroyOnClose="true"
>
<PreviewCode :editorJson="editorVueJson" fileFormat="vue" />
</Modal>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from 'vue';
import { formatRules, removeAttrs } from '../../../utils';
import PreviewCode from './PreviewCode.vue';
import { IFormConfig } from '../../../typings/v-form-component';
import { Modal } from 'ant-design-vue';
const codeVueFront = `<template>
<div>
<v-form-create
:formConfig="formConfig"
:formData="formData"
v-model="fApi"
/>
<a-button @click="submit">提交</a-button>
</div>
</template>
<script>
export default {
name: 'Demo',
data () {
return {
fApi:{},
formData:{},
formConfig: `;
/* eslint-disable */
let codeVueLast = `
}
},
methods: {
async submit() {
const data = await this.fApi.submit()
console.log(data)
}
}
}
<\/script>`;
//
export default defineComponent({
name: 'CodeModal',
components: { PreviewCode, Modal },
setup() {
const state = reactive({
visible: false,
jsonData: {} as IFormConfig,
});
const showModal = (formConfig: IFormConfig) => {
formConfig.schemas && formatRules(formConfig.schemas);
state.visible = true;
state.jsonData = formConfig;
};
const editorVueJson = computed(() => {
return codeVueFront + JSON.stringify(removeAttrs(state.jsonData), null, '\t') + codeVueLast;
});
return { ...toRefs(state), editorVueJson, showModal };
},
});
</script>

234
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/ComponentProps.vue

@ -0,0 +1,234 @@
<!--
* @Description: 组件属性控件
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择组件" />
<Form label-align="left" layout="vertical">
<!-- 循环遍历渲染组件属性 -->
<div v-if="formConfig.currentItem && formConfig.currentItem.componentProps">
<FormItem v-for="item in inputOptions" :key="item.name" :label="item.label">
<!-- 处理数组属性placeholder -->
<div v-if="item.children">
<component
v-for="(child, index) of item.children"
:key="index"
v-bind="child.componentProps"
:is="child.component"
v-model:value="formConfig.currentItem.componentProps[item.name][index]"
/>
</div>
<!-- 如果不是数组则正常处理属性值 -->
<component
v-else
class="component-prop"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.componentProps[item.name]"
/>
</FormItem>
<FormItem label="控制属性">
<Col v-for="item in controlOptions" :key="item.name">
<Checkbox
v-if="showControlAttrs(item.includes)"
v-bind="item.componentProps"
v-model:checked="formConfig.currentItem.componentProps[item.name]"
>
{{ item.label }}
</Checkbox>
</Col>
</FormItem>
</div>
<FormItem label="关联字段">
<Select
mode="multiple"
v-model:value="formConfig.currentItem['link']"
:options="linkOptions"
/>
</FormItem>
<FormItem
label="选项"
v-if="
[
'Select',
'CheckboxGroup',
'RadioGroup',
'TreeSelect',
'Cascader',
'AutoComplete',
].includes(formConfig.currentItem.component)
"
>
<FormOptions />
</FormItem>
<FormItem label="表格" v-if="['Table'].includes(formConfig.currentItem.component)">
<FormOptions />
</FormItem>
<FormItem label="栅格" v-if="['Grid'].includes(formConfig.currentItem.component)">
<FormOptions />
</FormItem>
</Form>
</div>
</div>
</template>
<script lang="ts">
import {
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
InputNumber,
RadioGroup,
} from 'ant-design-vue';
import RadioButtonGroup from '/@/components/Form/src/components/RadioButtonGroup.vue';
import { Col, Row } from 'ant-design-vue';
import { computed, defineComponent, ref, watch } from 'vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import {
baseComponentControlAttrs,
baseComponentAttrs,
baseComponentCommonAttrs,
componentPropsFuncs,
} from '../../VFormDesign/config/componentPropsConfig';
import FormOptions from './FormOptions.vue';
import { formItemsForEach, remove } from '../../../utils';
import { IBaseFormAttrs } from '../config/formItemPropsConfig';
export default defineComponent({
name: 'ComponentProps',
components: {
FormOptions,
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
InputNumber,
RadioGroup,
RadioButtonGroup,
Col,
Row,
},
setup() {
// compuated
const allOptions = ref([] as Omit<IBaseFormAttrs, 'tag'>[]);
const showControlAttrs = (includes: string[] | undefined) => {
if (!includes) return true;
return includes.includes(formConfig.value.currentItem!.component);
};
const { formConfig } = useFormDesignState();
if (formConfig.value.currentItem) {
formConfig.value.currentItem.componentProps =
formConfig.value.currentItem.componentProps || {};
}
watch(
() => formConfig.value.currentItem?.field,
(_newValue, oldValue) => {
formConfig.value.schemas &&
formItemsForEach(formConfig.value.schemas, (item) => {
if (item.link) {
const index = item.link.findIndex((linkItem) => linkItem === oldValue);
index !== -1 && remove(item.link, index);
}
});
},
);
watch(
() => formConfig.value.currentItem && formConfig.value.currentItem.component,
() => {
allOptions.value = [];
baseComponentControlAttrs.forEach((item) => {
item.category = 'control';
if (!item.includes) {
// include
allOptions.value.push(item);
} else if (item.includes.includes(formConfig.value.currentItem!.component)) {
// include
allOptions.value.push(item);
}
});
baseComponentCommonAttrs.forEach((item) => {
item.category = 'input';
if (item.includes) {
if (item.includes.includes(formConfig.value.currentItem!.component)) {
allOptions.value.push(item);
}
} else if (item.exclude) {
if (!item.exclude.includes(formConfig.value.currentItem!.component)) {
allOptions.value.push(item);
}
} else {
allOptions.value.push(item);
}
});
baseComponentAttrs[formConfig.value.currentItem!.component] &&
baseComponentAttrs[formConfig.value.currentItem!.component].forEach(async (item) => {
if (item.component) {
if (['Switch', 'Checkbox', 'Radio'].includes(item.component)) {
item.category = 'control';
allOptions.value.push(item);
} else {
item.category = 'input';
allOptions.value.push(item);
}
}
});
},
{
immediate: true,
},
);
//
const controlOptions = computed(() => {
return allOptions.value.filter((item) => {
return item.category == 'control';
});
});
//
const inputOptions = computed(() => {
return allOptions.value.filter((item) => {
return item.category == 'input';
});
});
watch(
() => formConfig.value.currentItem!.componentProps,
() => {
const func = componentPropsFuncs[formConfig.value.currentItem!.component];
if (func) {
func(formConfig.value.currentItem!.componentProps, allOptions.value);
}
},
{
immediate: true,
deep: true,
},
);
const linkOptions = computed(() => {
return (
formConfig.value.schemas &&
formConfig.value.schemas
.filter((item) => item.key !== formConfig.value.currentItem!.key)
.map(({ label, field }) => ({ label: label + '/' + field, value: field }))
);
});
return {
formConfig,
showControlAttrs,
linkOptions,
controlOptions,
inputOptions,
};
},
});
</script>

60
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormItemColumnProps.vue

@ -0,0 +1,60 @@
<!--
* @Description: 表单项属性
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择控件" />
<Form v-else label-align="left" layout="vertical">
<div v-for="item of baseItemColumnProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
v-if="formConfig.currentItem.colProps"
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.colProps[item.name]"
/>
</FormItem>
</div>
</Form>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { baseItemColumnProps } from '../config/formItemPropsConfig';
import { Empty, Input, Form, FormItem, Switch, Checkbox, Select, Slider } from 'ant-design-vue';
import RuleProps from './RuleProps.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { isArray } from 'lodash-es';
export default defineComponent({
name: 'FormItemProps',
components: {
RuleProps,
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
Slider,
},
// props: {} as PropsOptions,
setup() {
const { formConfig } = useFormDesignState();
const showProps = (exclude: string[] | undefined) => {
if (!exclude) {
return true;
}
return isArray(exclude) ? !exclude.includes(formConfig.value.currentItem!.component) : true;
};
return {
baseItemColumnProps,
formConfig,
showProps,
};
},
});
</script>

139
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormItemProps.vue

@ -0,0 +1,139 @@
<!--
* @Description: 表单项属性控件属性面板
-->
<template>
<div class="properties-content">
<div class="properties-body" v-if="formConfig.currentItem?.itemProps">
<Empty class="hint-box" v-if="!formConfig.currentItem.key" description="未选择控件" />
<Form v-else label-align="left" layout="vertical">
<div v-for="item of baseFormItemProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem[item.name]"
/>
</FormItem>
</div>
<div v-for="item of advanceFormItemProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.itemProps[item.name]"
/>
</FormItem> </div
><div v-for="item of advanceFormItemColProps" :key="item.name">
<FormItem :label="item.label" v-if="showProps(item.exclude)">
<component
class="component-props"
v-bind="item.componentProps"
:is="item.component"
v-model:value="formConfig.currentItem.itemProps[item.name]['span']"
/>
</FormItem>
</div>
<FormItem label="控制属性" v-if="controlPropsList.length">
<Col v-for="item of controlPropsList" :key="item.name">
<Checkbox v-model:checked="formConfig.currentItem.itemProps[item.name]">
{{ item.label }}
</Checkbox>
</Col>
</FormItem>
<FormItem label="是否必选" v-if="!['Grid'].includes(formConfig.currentItem.component)">
<Switch v-model:checked="formConfig.currentItem.itemProps['required']" />
<Input
v-if="formConfig.currentItem.itemProps['required']"
v-model:value="formConfig.currentItem.itemProps['message']"
placeholder="请输入必选提示"
/>
</FormItem>
<FormItem
v-if="!['Grid'].includes(formConfig.currentItem.component)"
label="校验规则"
:class="{ 'form-rule-props': !!formConfig.currentItem.itemProps['rules'] }"
>
<RuleProps />
</FormItem>
</Form>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, watch } from 'vue';
import {
baseFormItemControlAttrs,
baseFormItemProps,
advanceFormItemProps,
advanceFormItemColProps,
} from '../../VFormDesign/config/formItemPropsConfig';
import {
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
Slider,
Col,
RadioGroup,
} from 'ant-design-vue';
import RuleProps from './RuleProps.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { isArray } from 'lodash-es';
export default defineComponent({
name: 'FormItemProps',
components: {
RuleProps,
Empty,
Input,
Form,
FormItem,
Switch,
Checkbox,
Select,
Slider,
Col,
RadioGroup,
},
// props: {} as PropsOptions,
setup() {
const { formConfig } = useFormDesignState();
watch(
() => formConfig.value,
() => {
if (formConfig.value.currentItem) {
formConfig.value.currentItem.itemProps = formConfig.value.currentItem.itemProps || {};
formConfig.value.currentItem.itemProps.labelCol =
formConfig.value.currentItem.itemProps.labelCol || {};
formConfig.value.currentItem.itemProps.wrapperCol =
formConfig.value.currentItem.itemProps.wrapperCol || {};
}
},
{ deep: true, immediate: true },
);
const showProps = (exclude: string[] | undefined) => {
if (!exclude) {
return true;
}
return isArray(exclude) ? !exclude.includes(formConfig.value.currentItem!.component) : true;
};
const controlPropsList = computed(() => {
return baseFormItemControlAttrs.filter((item) => {
return showProps(item.exclude);
});
});
return {
baseFormItemProps,
advanceFormItemProps,
advanceFormItemColProps,
formConfig,
controlPropsList,
showProps,
};
},
});
</script>

53
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormNode.vue

@ -0,0 +1,53 @@
<!--
* @Description: 拖拽节点控件
-->
<template>
<div
class="drag-move-box"
@click.stop="handleSelectItem"
:class="{ active: schema.key === formConfig.currentItem?.key }"
>
<div class="form-item-box">
<VFormItem :formConfig="formConfig" :schema="schema" />
</div>
<div class="show-key-box">
{{ schema.label + (schema.field ? '/' + schema.field : '') }}
</div>
<FormNodeOperate :schema="schema" :currentItem="formConfig.currentItem" />
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, PropType } from 'vue';
import { IVFormComponent } from '../../../typings/v-form-component';
import FormNodeOperate from './FormNodeOperate.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import VFormItem from '../../VFormItem/index.vue';
// import VFormItem from '../../VFormItem/vFormItem.vue';
export default defineComponent({
name: 'FormNode',
components: {
VFormItem,
FormNodeOperate,
},
props: {
schema: {
type: Object as PropType<IVFormComponent>,
required: true,
},
},
setup(props) {
const { formConfig, formDesignMethods } = useFormDesignState();
const state = reactive({});
// formDesignMethods
const handleSelectItem = () => {
// formDesignMethods
formDesignMethods.handleSetSelectItem(props.schema);
};
return {
...toRefs(state),
handleSelectItem,
formConfig,
};
},
});
</script>

72
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormNodeOperate.vue

@ -0,0 +1,72 @@
<!--
* @Description: 节点操作复制删除控件
-->
<template>
<div class="copy-delete-box">
<a class="copy" :class="activeClass" @click.stop="handleCopy">
<Icon icon="ant-design:copy-outlined" />
</a>
<a class="delete" :class="activeClass" @click.stop="handleDelete">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { IVFormComponent } from '../../../typings/v-form-component';
import { remove } from '../../../utils';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import Icon from '/@/components/Icon/index';
export default defineComponent({
name: 'FormNodeOperate',
components: {
Icon,
},
props: {
schema: {
type: Object,
default: () => ({}),
},
currentItem: {
type: Object,
default: () => ({}),
},
},
setup(props) {
const { formConfig, formDesignMethods } = useFormDesignState();
const activeClass = computed(() => {
return props.schema.key === props.currentItem.key ? 'active' : 'unactivated';
});
/**
* 删除当前项
*/
const handleDelete = () => {
const traverse = (schemas: IVFormComponent[]) => {
schemas.some((formItem, index) => {
const { component, key } = formItem;
//
['Grid', 'Tabs'].includes(component) &&
formItem.columns?.forEach((item) => traverse(item.children));
if (key === props.currentItem.key) {
let params: IVFormComponent =
schemas.length === 1
? { component: '' }
: schemas.length - 1 > index
? schemas[index + 1]
: schemas[index - 1];
formDesignMethods.handleSetSelectItem(params);
remove(schemas, index);
return true;
}
});
};
traverse(formConfig.value!.schemas);
};
const handleCopy = () => {
formDesignMethods.handleCopy();
};
return { activeClass, handleDelete, handleCopy };
},
});
</script>

147
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormOptions.vue

@ -0,0 +1,147 @@
<template>
<div>
<div v-if="['Grid'].includes(formConfig.currentItem!.component)">
<div v-for="(item, index) of formConfig.currentItem!['columns']" :key="index">
<div class="options-box">
<Input v-model:value="item.span" class="options-value" />
<a class="options-delete" @click="deleteGridOptions(index)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</div>
<a @click="addGridOptions">
<Icon icon="ant-design:file-add-outlined" />
添加栅格
</a>
</div>
<div v-else-if="['Table'].includes(formConfig.currentItem!.component)">
<div v-for="(item, index) of formConfig.currentItem!.componentProps!['columns']" :key="index">
<div class="options-box">
<Input v-model:value="item.title" />
<Input v-model:value="item.dataIndex" class="options-value" />
<a class="options-delete" @click="deleteTableColumns(index)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</div>
<a @click="addTableColumns">
<Icon icon="ant-design:file-add-outlined" />
添加列
</a>
</div>
<div v-else>
<div v-for="(item, index) of formConfig.currentItem!.componentProps![key]" :key="index">
<div class="options-box">
<Input v-model:value="item.label" />
<Input v-model:value="item.value" class="options-value" />
<a class="options-delete" @click="deleteOptions(index)">
<Icon icon="ant-design:delete-outlined" />
</a>
</div>
</div>
<a @click="addOptions">
<Icon icon="ant-design:file-add-outlined" />
添加选项
</a>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { remove } from '../../../utils';
import message from '../../../utils/message';
import { Input } from 'ant-design-vue';
import Icon from '/@/components/Icon/index';
export default defineComponent({
name: 'FormOptions',
components: { Input, Icon },
// props: {},
setup() {
const state = reactive({});
const { formConfig } = useFormDesignState();
const key = formConfig.value.currentItem?.component === 'TreeSelect' ? 'treeData' : 'options';
const addOptions = () => {
if (!formConfig.value.currentItem?.componentProps?.[key])
formConfig.value.currentItem!.componentProps![key] = [];
const len = formConfig.value.currentItem?.componentProps?.[key].length + 1;
formConfig.value.currentItem!.componentProps![key].push({
label: `选项${len}`,
value: '' + len,
});
};
const deleteOptions = (index: number) => {
remove(formConfig.value.currentItem?.componentProps?.[key], index);
};
const addGridOptions = () => {
formConfig.value.currentItem?.['columns']?.push({
span: 12,
children: [],
});
};
const deleteGridOptions = (index: number) => {
if (index === 0) return message.warning('请至少保留一个栅格');
remove(formConfig.value.currentItem!['columns']!, index);
};
const addTableColumns = () => {
const len = formConfig.value.currentItem?.componentProps?.['columns'].length + 1;
formConfig.value.currentItem!.componentProps!['columns']?.push({
align: 'left',
title: `${len}`,
dataIndex: `col${len}`
});
const dataSource = formConfig.value.currentItem!.componentProps!['dataSource'];
dataSource && Array.isArray(dataSource) && dataSource.forEach((data, di) => {
Reflect.defineProperty(data, `col${len}`, { value: `数据${len}-${di + 1}` });
});
}
const deleteTableColumns = (index: number) => {
if (index === 0) return message.warning('请至少保留一个列');
remove(formConfig.value.currentItem!.componentProps!['columns']!, index);
const dataSource = formConfig.value.currentItem!.componentProps!['dataSource'];
dataSource && Array.isArray(dataSource) && dataSource.forEach((data) => {
Reflect.deleteProperty(data, `col${index + 1}`);
});
}
return {
...toRefs(state),
formConfig,
addOptions,
deleteOptions,
key,
deleteGridOptions,
addGridOptions,
deleteTableColumns,
addTableColumns,
};
},
});
</script>
<style lang="less" scoped>
.options-box {
display: flex;
align-items: center;
margin-bottom: 5px;
.options-value {
margin: 0 8px;
}
.options-delete {
width: 30px;
height: 30px;
flex-shrink: 0;
line-height: 30px;
text-align: center;
border-radius: 50%;
background: #f5f5f5;
color: #666;
&:hover {
background: #ff4d4f;
}
}
}
</style>

110
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/FormProps.vue

@ -0,0 +1,110 @@
<!--
* @Description: 右侧属性面板控件 表单属性面板
-->
<template>
<div class="properties-content">
<Form class="properties-body" label-align="left" layout="vertical">
<!-- <e-upload v-model="fileList"></e-upload>-->
<FormItem label="表单布局">
<RadioGroup button-style="solid" v-model:value="formConfig.layout">
<RadioButton value="horizontal">水平</RadioButton>
<RadioButton value="vertical" :disabled="formConfig.labelLayout === 'Grid'">
垂直
</RadioButton>
<RadioButton value="inline" :disabled="formConfig.labelLayout === 'Grid'">
行内
</RadioButton>
</RadioGroup>
</FormItem>
<!-- <Row> -->
<FormItem label="标签布局">
<RadioGroup
buttonStyle="solid"
v-model:value="formConfig.labelLayout"
@change="lableLayoutChange"
>
<RadioButton value="flex">固定</RadioButton>
<RadioButton value="Grid" :disabled="formConfig.layout !== 'horizontal'">
栅格
</RadioButton>
</RadioGroup>
</FormItem>
<!-- </Row> -->
<FormItem label="标签宽度(px)" v-show="formConfig.labelLayout === 'flex'">
<InputNumber
:style="{ width: '100%' }"
v-model:value="formConfig.labelWidth"
:min="0"
:step="1"
/>
</FormItem>
<div v-if="formConfig.labelLayout === 'Grid'">
<FormItem label="labelCol">
<Slider v-model:value="formConfig.labelCol!.span" :max="24" />
</FormItem>
<FormItem label="wrapperCol">
<Slider v-model:value="formConfig.wrapperCol!.span" :max="24" />
</FormItem>
<FormItem label="标签对齐">
<RadioGroup button-style="solid" v-model:value="formConfig.labelAlign">
<RadioButton value="left">靠左</RadioButton>
<RadioButton value="right">靠右</RadioButton>
</RadioGroup>
</FormItem>
<FormItem label="控件大小">
<RadioGroup button-style="solid" v-model:value="formConfig.size">
<RadioButton value="default">默认</RadioButton>
<RadioButton value="small"></RadioButton>
<RadioButton value="large"></RadioButton>
</RadioGroup>
</FormItem>
</div>
<FormItem label="表单属性">
<Col
><Checkbox v-model:checked="formConfig.colon" v-if="formConfig.layout == 'horizontal'"
>label后面显示冒号</Checkbox
></Col
>
<Col><Checkbox v-model:checked="formConfig.disabled">禁用</Checkbox></Col>
<Col><Checkbox v-model:checked="formConfig.hideRequiredMark">隐藏必选标记</Checkbox></Col>
</FormItem>
</Form>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { InputNumber, Slider, Checkbox, Col, RadioChangeEvent } from 'ant-design-vue';
// import RadioButtonGroup from '/@/components/RadioButtonGroup.vue';
import { Form, FormItem, Radio } from 'ant-design-vue';
export default defineComponent({
name: 'FormProps',
components: {
InputNumber,
Slider,
Checkbox,
RadioGroup: Radio.Group,
RadioButton: Radio.Button,
Form,
FormItem,
Col,
},
setup() {
const { formConfig } = useFormDesignState();
formConfig.value = formConfig.value || {
labelCol: { span: 24 },
wrapperCol: { span: 24 },
};
const lableLayoutChange = (e: RadioChangeEvent) => {
if (e.target.value === 'Grid') {
formConfig.value.layout = 'horizontal';
}
};
return { formConfig, lableLayoutChange };
},
});
</script>

119
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/ImportJsonModal.vue

@ -0,0 +1,119 @@
<!--
* @Description: 导入JSON模板
-->
<template>
<Modal
title="JSON数据"
:visible="state.visible"
@ok="handleImportJson"
@cancel="handleCancel"
cancelText="关闭"
:destroyOnClose="true"
wrapClassName="v-code-modal"
style="top: 20px"
:width="850"
>
<p class="hint-box">导入格式如下:</p>
<div class="v-json-box">
<CodeEditor v-model:value="state.json" ref="myEditor" :mode="MODE.JSON" />
</div>
<template #footer>
<Button @click="handleCancel">取消</Button>
<Upload
class="upload-button"
:beforeUpload="beforeUpload"
:showUploadList="false"
accept="application/json"
>
<Button type="primary">导入json文件</Button>
</Upload>
<Button type="primary" @click="handleImportJson">确定</Button>
</template>
</Modal>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { Button } from 'ant-design-vue';
// import message from '../../../utils/message';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
// import { codemirror } from 'vue-codemirror-lite';
import { IFormConfig } from '../../../typings/v-form-component';
import { formItemsForEach, generateKey } from '../../../utils';
import { CodeEditor, MODE } from '/@/components/CodeEditor';
import { useMessage } from '/@/hooks/web/useMessage';
import { Upload, Modal } from 'ant-design-vue';
const { createMessage } = useMessage();
const state = reactive({
visible: false,
json: `{
"schemas": [
{
"component": "input",
"label": "输入框",
"field": "input_2",
"span": 24,
"props": {
"type": "text"
}
}
],
"layout": "horizontal",
"labelLayout": "flex",
"labelWidth": 100,
"labelCol": {},
"wrapperCol": {}
}`,
jsonData: {
schemas: {},
config: {},
},
handleSetSelectItem: null,
});
const { formDesignMethods } = useFormDesignState();
const handleCancel = () => {
state.visible = false;
};
const showModal = () => {
state.visible = true;
};
const handleImportJson = () => {
// JSON
try {
const editorJsonData = JSON.parse(state.json) as IFormConfig;
editorJsonData.schemas &&
formItemsForEach(editorJsonData.schemas, (formItem) => {
generateKey(formItem);
});
formDesignMethods.setFormConfig({
...editorJsonData,
activeKey: 1,
currentItem: { component: '' },
});
handleCancel();
createMessage.success('导入成功');
} catch {
createMessage.error('导入失败,数据格式不对');
}
};
const beforeUpload = (e: File) => {
// json
const reader = new FileReader();
reader.readAsText(e);
reader.onload = function () {
state.json = this.result as string;
handleImportJson();
};
return false;
};
defineExpose({
showModal,
});
</script>
<style lang="less" scoped>
.upload-button {
margin: 0 10px;
}
</style>

60
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/JsonModal.vue

@ -0,0 +1,60 @@
<!--
* @Description: 渲染JSON数据
-->
<template>
<Modal
title="JSON数据"
:footer="null"
:visible="visible"
@cancel="handleCancel"
:destroyOnClose="true"
wrapClassName="v-code-modal"
style="top: 20px"
width="850px"
>
<PreviewCode :editorJson="editorJson" />
</Modal>
</template>
<script lang="ts">
import { computed, defineComponent, reactive, toRefs } from 'vue';
import PreviewCode from './PreviewCode.vue';
import { IFormConfig } from '../../../typings/v-form-component';
import { formatRules, removeAttrs } from '../../../utils';
import { Modal } from 'ant-design-vue';
export default defineComponent({
name: 'JsonModal',
components: {
PreviewCode,
Modal,
},
emits: ['cancel'],
setup(_props, { emit }) {
const state = reactive<{
visible: boolean;
jsonData: IFormConfig;
}>({
visible: false, // json
jsonData: {} as IFormConfig, // json
});
/**
* 显示Json数据弹框
* @param jsonData
*/
const showModal = (jsonData: IFormConfig) => {
formatRules(jsonData.schemas);
state.jsonData = jsonData;
state.visible = true;
};
// json
const editorJson = computed(() => {
return JSON.stringify(removeAttrs(state.jsonData), null, '\t');
});
//
const handleCancel = () => {
state.visible = false;
emit('cancel');
};
return { ...toRefs(state), editorJson, handleCancel, showModal };
},
});
</script>

121
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/LayoutItem.vue

@ -0,0 +1,121 @@
<!--
* @Description: 表单项布局控件
* 千万不要在template下面的第一行加注释因为这里拖动的第一个元素
-->
<template>
<Col v-bind="colPropsComputed">
<template v-if="['Grid'].includes(schema.component)">
<div
class="grid-box"
:class="{ active: schema.key === currentItem.key }"
@click.stop="handleSetSelectItem(schema)"
>
<Row class="grid-row" v-bind="schema.componentProps">
<Col
class="grid-col"
v-for="(colItem, index) in schema.columns"
:key="index"
:span="colItem.span"
>
<draggable
class="list-main draggable-box"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
v-bind="{
group: 'form-draggable',
ghostClass: 'moving',
animation: 180,
handle: '.drag-move',
}"
item-key="key"
v-model="colItem.children"
@start="$emit('dragStart', $event, colItem.children)"
@add="$emit('handleColAdd', $event, colItem.children)"
>
<template #item="{ element }">
<LayoutItem
class="drag-move"
:schema="element"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</template>
</draggable>
</Col>
</Row>
<FormNodeOperate :schema="schema" :currentItem="currentItem" />
</div>
</template>
<FormNode
v-else
:key="schema.key"
:schema="schema"
:current-item="currentItem"
@handle-copy="$emit('handle-copy')"
@handle-delete="$emit('handle-delete')"
/>
</Col>
</template>
<script lang="ts">
import { computed, defineComponent, PropType, reactive, toRefs } from 'vue';
import draggable from 'vuedraggable';
import FormNode from './FormNode.vue';
import FormNodeOperate from './FormNodeOperate.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { IVFormComponent } from '../../../typings/v-form-component';
import { Row, Col } from 'ant-design-vue';
export default defineComponent({
name: 'LayoutItem',
components: {
FormNode,
FormNodeOperate,
draggable,
Row,
Col,
},
props: {
schema: {
type: Object as PropType<IVFormComponent>,
required: true,
},
currentItem: {
type: Object,
required: true,
},
},
emits: ['dragStart', 'handleColAdd', 'handle-copy', 'handle-delete'],
setup(props) {
const {
formDesignMethods: { handleSetSelectItem },
formConfig,
} = useFormDesignState();
const state = reactive({});
const colPropsComputed = computed(() => {
const { colProps = {} } = props.schema;
return colProps;
});
const list1 = computed(() => props.schema.columns);
// AColdiv
const layoutTag = computed(() => {
return formConfig.value.layout === 'horizontal' ? 'Col' : 'div';
});
return {
...toRefs(state),
colPropsComputed,
handleSetSelectItem,
layoutTag,
list1,
};
},
});
</script>
<style lang="less">
@import url(../styles/variable.less);
.layout-width {
width: 100%;
}
.hidden-item {
background-color: rgb(240, 191, 195);
}
</style>

75
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/PreviewCode.vue

@ -0,0 +1,75 @@
<template>
<div>
<div class="v-json-box">
<CodeEditor :value="editorJson" ref="myEditor" :mode="MODE.JSON" />
</div>
<div class="copy-btn-box">
<a-button
@click="handleCopyJson"
type="primary"
class="copy-btn"
data-clipboard-action="copy"
:data-clipboard-text="editorJson"
>
复制数据
</a-button>
<a-button @click="handleExportJson" type="primary">导出代码</a-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { unref } from 'vue';
import { CodeEditor, MODE } from '/@/components/CodeEditor';
import { useCopyToClipboard } from '/@/hooks/web/useCopyToClipboard';
import { useMessage } from '/@/hooks/web/useMessage';
const props = defineProps({
fileFormat: {
type: String,
default: 'json',
},
editorJson: {
type: String,
default: '',
},
});
const exportData = (data: string, fileName = `file.${props.fileFormat}`) => {
let content = 'data:text/csv;charset=utf-8,';
content += data;
const encodedUri = encodeURI(content);
const actions = document.createElement('a');
actions.setAttribute('href', encodedUri);
actions.setAttribute('download', fileName);
actions.click();
};
const handleExportJson = () => {
exportData(props.editorJson);
};
const { clipboardRef, copiedRef } = useCopyToClipboard();
const { createMessage } = useMessage();
const handleCopyJson = () => {
//
const value = props.editorJson;
if (!value) {
createMessage.warning('代码为空!');
return;
}
clipboardRef.value = value;
if (unref(copiedRef)) {
createMessage.warning('复制成功!');
}
}
</script>
<style lang="less" scoped>
// modal
.copy-btn-box {
padding-top: 8px;
text-align: center;
.copy-btn {
margin-right: 8px;
}
}
</style>

285
apps/vue/src/components/FormDesign/src/components/VFormDesign/components/RuleProps.vue

@ -0,0 +1,285 @@
<!--
* @Description: 正则校验选项组件
-->
<template>
<div class="rule-props-content">
<Form v-if="formConfig.currentItem && formConfig.currentItem['rules']">
<div
v-for="(item, index) of formConfig.currentItem['rules']"
:key="index"
class="rule-props-item"
>
<Icon
icon="ant-design:close-circle-filled"
class="rule-props-item-close"
@click="removeRule(index)"
/>
<FormItem label="正则" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
<AutoComplete
v-model:value="item.pattern"
placeholder="请输入正则表达式"
:dataSource="patternDataSource"
/>
</FormItem>
<FormItem label="文案" :labelCol="{ span: 6 }" :wrapperCol="{ span: 16 }">
<Input v-model:value="item.message" placeholder="请输入提示文案" />
</FormItem>
</div>
</Form>
<a @click="addRules">
<Icon icon="ant-design:file-add-outlined" />
添加正则
</a>
</div>
</template>
<script lang="ts">
import { ref, defineComponent } from 'vue';
import { remove } from '../../../utils';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { isArray } from 'lodash-es';
import { Form, FormItem, AutoComplete, Input } from 'ant-design-vue';
import Icon from '/@/components/Icon';
export default defineComponent({
name: 'RuleProps',
components: {
Form,
FormItem,
AutoComplete,
Input,
Icon,
},
setup() {
//
const { formConfig } = useFormDesignState();
// currentItem
/**
* 添加正则校验判断当前组件的rules是不是数组如果不是数组使用set方法重置成数组然后添加正则校验
*/
const addRules = () => {
if (!isArray(formConfig.value.currentItem!.rules))
formConfig.value.currentItem!['rules'] = [];
formConfig.value.currentItem!.rules?.push({ pattern: '', message: '' });
};
/**
* 删除正则校验当正则规则为0时删除rules属性
* @param index {number} 需要删除的规则下标
*/
const removeRule = (index: number) => {
remove(formConfig.value.currentItem!.rules as Array<any>, index);
if (formConfig.value.currentItem!.rules?.length === 0)
delete formConfig.value.currentItem!['rules'];
};
const patternDataSource = ref([
{
value: '/^(?:(?:\\+|00)86)?1[3-9]\\d{9}$/',
text: '手机号码',
},
{
value: '/^((ht|f)tps?:\\/\\/)?[\\w-]+(\\.[\\w-]+)+:\\d{1,5}\\/?$/',
text: '网址带端口号',
},
{
value:
'/^(((ht|f)tps?):\\/\\/)?[\\w-]+(\\.[\\w-]+)+([\\w.,@?^=%&:/~+#-\\(\\)]*[\\w@?^=%&/~+#-\\(\\)])?$/',
text: '网址带参数',
},
{
value: '/^[0-9A-HJ-NPQRTUWXY]{2}\\d{6}[0-9A-HJ-NPQRTUWXY]{10}$/',
text: '统一社会信用代码',
},
{
value: '/^(s[hz]|S[HZ])(000[\\d]{3}|002[\\d]{3}|300[\\d]{3}|600[\\d]{3}|60[\\d]{4})$/',
text: '股票代码',
},
{
value: '/^([a-f\\d]{32}|[A-F\\d]{32})$/',
text: 'md5格式(32位)',
},
{
value: '/^[a-f\\d]{4}(?:[a-f\\d]{4}-){4}[a-f\\d]{12}$/i',
text: 'GUID/UUID',
},
{
value: '/^\\d+(?:\\.\\d+){2}$/',
text: '版本号(x.y.z)格式',
},
{
value:
'/^https?:\\/\\/(.+\\/)+.+(\\.(swf|avi|flv|mpg|rm|mov|wav|asf|3gp|mkv|rmvb|mp4))$/i',
text: '视频链接地址',
},
{
value: '/^https?:\\/\\/(.+\\/)+.+(\\.(gif|png|jpg|jpeg|webp|svg|psd|bmp|tif))$/i',
text: '图片链接地址',
},
{
value: '/^-?\\d+(,\\d{3})*(\\.\\d{1,2})?$/',
text: '数字/货币金额(支持负数、千分位分隔符)',
},
{
value:
'/(?:^[1-9]([0-9]+)?(?:\\.[0-9]{1,2})?$)|(?:^(?:0)$)|(?:^[0-9]\\.[0-9](?:[0-9])?$)/',
text: '数字/货币金额',
},
{
value: '/^[1-9]\\d{9,29}$/',
text: '银行卡号',
},
{
value: '/^(?:[\u4e00-\u9fa5·]{2,16})$/',
text: '中文姓名',
},
{
value: '/(^[a-zA-Z][a-zA-Z\\s]{0,20}[a-zA-Z]$)/',
text: '英文姓名',
},
{
value:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z](?:((\\d{5}[A-HJK])|([A-HJK][A-HJ-NP-Z0-9][0-9]{4}))|[A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳])$/',
text: '车牌号(新能源)',
},
{
value:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳]$/',
text: '车牌号(非新能源)',
},
{
value:
'/^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-HJ-NP-Z][A-HJ-NP-Z0-9]{4,5}[A-HJ-NP-Z0-9挂学警港澳]$/',
text: '车牌号(新能源+非新能源)',
},
{
value:
'/^(([^<>()[\\]\\\\.,;:\\s@"]+(\\.[^<>()[\\]\\\\.,;:\\s@"]+)*)|(".+"))@((\\[[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\])|(([a-zA-Z\\-0-9]+\\.)+[a-zA-Z]{2,}))$/',
text: 'email(邮箱)',
},
{
value: '/^(?:(?:\\d{3}-)?\\d{8}|^(?:\\d{4}-)?\\d{7,8})(?:-\\d+)?$/',
text: '座机',
},
{
value:
'/^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|10|11|12)(?:0[1-9]|[1-2]\\d|30|31)\\d{3}[\\dXx]$/',
text: '身份证号',
},
{
value:
'/(^[EeKkGgDdSsPpHh]\\d{8}$)|(^(([Ee][a-fA-F])|([DdSsPp][Ee])|([Kk][Jj])|([Mm][Aa])|(1[45]))\\d{7}$)/',
text: '护照',
},
{
value:
'/^(?:[\u3400-\u4DB5\u4E00-\u9FEA\uFA0E\uFA0F\uFA11\uFA13\uFA14\uFA1F\uFA21\uFA23\uFA24\uFA27-\uFA29]|[\uD840-\uD868\uD86A-\uD86C\uD86F-\uD872\uD874-\uD879][\uDC00-\uDFFF]|\uD869[\uDC00-\uDED6\uDF00-\uDFFF]|\uD86D[\uDC00-\uDF34\uDF40-\uDFFF]|\uD86E[\uDC00-\uDC1D\uDC20-\uDFFF]|\uD873[\uDC00-\uDEA1\uDEB0-\uDFFF]|\uD87A[\uDC00-\uDFE0])+$/',
text: '中文汉字',
},
{
value: '/^\\d+\\.\\d+$/',
text: '小数',
},
{
value: '/^\\d{1,}$/',
text: '数字',
},
{
value: '/^[1-9][0-9]{4,10}$/',
text: 'qq号',
},
{
value: '/^[A-Za-z0-9]+$/',
text: '数字字母组合',
},
{
value: '/^[a-zA-Z]+$/',
text: '英文字母',
},
{
value: '/^[a-z]+$/',
text: '小写英文字母',
},
{
value: '/^[A-Z]+$/',
text: '大写英文字母',
},
{
value: '/^[a-zA-Z0-9_-]{4,16}$/',
text: '用户名校验,4到16位(字母,数字,下划线,减号)',
},
{
value: '/^#?([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/',
text: '16进制颜色',
},
{
value: '/^[a-zA-Z][-_a-zA-Z0-9]{5,19}$/',
text: '微信号',
},
{
value: '/^(0[1-7]|1[0-356]|2[0-7]|3[0-6]|4[0-7]|5[1-7]|6[1-7]|7[0-5]|8[013-6])\\d{4}$/',
text: '邮政编码(中国)',
},
{
value: '/^[^A-Za-z]*$/',
text: '不能包含字母',
},
{
value: '/^\\+?[1-9]\\d*$/',
text: '正整数,不包含0',
},
{
value: '/^-[1-9]\\d*$/',
text: '负整数,不包含0',
},
{
value: '/^-?[0-9]\\d*$/',
text: '整数',
},
{
value: '/^(-?\\d+)(\\.\\d+)?$/',
text: '浮点数',
},
{
value: '/^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$/',
text: 'email(支持中文邮箱)',
},
]);
return { addRules, removeRule, formConfig, patternDataSource };
},
});
</script>
<style lang="less" scoped>
:deep(.icon) {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
.rule-props-content {
:deep(.ant-form-item) {
margin-bottom: 0;
}
.rule-props-item {
position: relative;
background-color: #f0eded;
padding: 3px 2px;
border-radius: 5px;
margin-bottom: 5px;
:deep(.ant-form-item) {
border: 0 !important;
}
&-close {
position: absolute;
top: -5px;
right: -5px;
color: #ccc;
cursor: pointer;
border-radius: 7px;
background-color: #a3a0a0;
z-index: 999;
&:hover {
color: #00c;
}
}
}
}
</style>

1241
apps/vue/src/components/FormDesign/src/components/VFormDesign/config/componentPropsConfig.ts

File diff suppressed because it is too large

351
apps/vue/src/components/FormDesign/src/components/VFormDesign/config/formItemPropsConfig.ts

@ -0,0 +1,351 @@
import { IAnyObject } from '../../../typings/base-type';
import { baseComponents, customComponents } from '../../../core/formItemConfig';
export const globalConfigState: { span: number } = {
span: 24,
};
export interface IBaseFormAttrs {
name: string; // 字段名
label: string; // 字段标签
component?: string; // 属性控件
componentProps?: IAnyObject; // 传递给控件的属性
exclude?: string[]; // 需要排除的控件
includes?: string[]; // 符合条件的组件
on?: IAnyObject;
children?: IBaseFormAttrs[];
category?: 'control' | 'input';
}
export interface IBaseFormItemControlAttrs extends IBaseFormAttrs {
target?: 'props' | 'options'; // 绑定到对象下的某个目标key中
}
export const baseItemColumnProps: IBaseFormAttrs[] = [
{
name: 'span',
label: '栅格数',
component: 'Slider',
on: {
change(value: number) {
globalConfigState.span = value;
},
},
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'offset',
label: '栅格左侧的间隔格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'order',
label: '栅格顺序,flex 布局模式下有效',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'pull',
label: '栅格向左移动格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'push',
label: '栅格向右移动格数',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'xs',
label: '<576px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'sm',
label: '≥576px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'md',
label: '≥768p 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'lg',
label: '≥992px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'xl',
label: '≥1200px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: 'xxl',
label: '≥1600px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
{
name: '≥2000px',
label: '≥1600px 响应式栅格',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
},
];
// 控件属性面板的配置项
export const advanceFormItemColProps: IBaseFormAttrs[] = [
{
name: 'labelCol',
label: '标签col',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
exclude: ['Grid'],
},
{
name: 'wrapperCol',
label: '控件-span',
component: 'Slider',
componentProps: {
max: 24,
min: 0,
marks: { 12: '' },
},
exclude: ['Grid'],
},
];
// 控件属性面板的配置项
export const baseFormItemProps: IBaseFormAttrs[] = [
{
// 动态的切换控件的类型
name: 'component',
label: '控件-FormItem',
component: 'Select',
componentProps: {
options: baseComponents
.concat(customComponents)
.map((item) => ({ value: item.component, label: item.label })),
},
},
{
name: 'label',
label: '标签',
component: 'Input',
componentProps: {
type: 'Input',
placeholder: '请输入标签',
},
exclude: ['Grid'],
},
{
name: 'field',
label: '字段标识',
component: 'Input',
componentProps: {
type: 'InputTextArea',
placeholder: '请输入字段标识',
},
exclude: ['Grid'],
},
{
name: 'helpMessage',
label: 'helpMessage',
component: 'Input',
componentProps: {
placeholder: '请输入提示信息',
},
exclude: ['Grid'],
},
];
// 控件属性面板的配置项
export const advanceFormItemProps: IBaseFormAttrs[] = [
{
name: 'labelAlign',
label: '标签对齐',
component: 'RadioGroup',
componentProps: {
options: [
{
label: '靠左',
value: 'left',
},
{
label: '靠右',
value: 'right',
},
],
},
exclude: ['Grid'],
},
{
name: 'help',
label: 'help',
component: 'Input',
componentProps: {
placeholder: '请输入提示信息',
},
exclude: ['Grid'],
},
{
name: 'extra',
label: '额外消息',
component: 'Input',
componentProps: {
type: 'InputTextArea',
placeholder: '请输入额外消息',
},
exclude: ['Grid'],
},
{
name: 'validateTrigger',
label: 'validateTrigger',
component: 'Input',
componentProps: {
type: 'InputTextArea',
placeholder: '请输入validateTrigger',
},
exclude: ['Grid'],
},
{
name: 'validateStatus',
label: '校验状态',
component: 'RadioGroup',
componentProps: {
options: [
{
label: '默认',
value: '',
},
{
label: '成功',
value: 'success',
},
{
label: '警告',
value: 'warning',
},
{
label: '错误',
value: 'error',
},
{
label: '校验中',
value: 'validating',
},
],
},
exclude: ['Grid'],
},
];
export const baseFormItemControlAttrs: IBaseFormItemControlAttrs[] = [
{
name: 'required',
label: '必填项',
component: 'Checkbox',
exclude: ['alert'],
},
{
name: 'hidden',
label: '隐藏',
component: 'Checkbox',
exclude: ['alert'],
},
{
name: 'hiddenLabel',
component: 'Checkbox',
exclude: ['Grid'],
label: '隐藏标签',
},
{
name: 'colon',
label: 'label后面显示冒号',
component: 'Checkbox',
componentProps: {},
exclude: ['Grid'],
},
{
name: 'hasFeedback',
label: '输入反馈',
component: 'Checkbox',
componentProps: {},
includes: ['Input'],
},
{
name: 'autoLink',
label: '自动关联',
component: 'Checkbox',
componentProps: {},
includes: ['Input'],
},
{
name: 'validateFirst',
label: '检验证错误停止',
component: 'Checkbox',
componentProps: {},
includes: ['Input'],
},
];

324
apps/vue/src/components/FormDesign/src/components/VFormDesign/index.vue

@ -0,0 +1,324 @@
<template>
<Layout>
<LayoutSider
:class="`left ${prefixCls}-sider`"
collapsible
collapsedWidth="0"
width="270"
:zeroWidthTriggerStyle="{
'margin-top': '-70px',
'background-color': 'gray',
}"
breakpoint="md"
>
<CollapseContainer title="基础控件">
<CollapseItem
:list="baseComponents"
:handleListPush="handleListPushDrag"
@add-attrs="handleAddAttrs"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
<CollapseContainer title="自定义控件">
<CollapseItem
:list="customComponents"
@add-attrs="handleAddAttrs"
:handleListPush="handleListPushDrag"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
<CollapseContainer title="布局控件">
<CollapseItem
:list="layoutComponents"
:handleListPush="handleListPushDrag"
@add-attrs="handleAddAttrs"
@handle-list-push="handleListPush"
/>
</CollapseContainer>
</LayoutSider>
<LayoutContent>
<Toolbar
@handle-open-json-modal="handleOpenModal(jsonModal!)"
@handle-open-import-json-modal="handleOpenModal(importJsonModal!)"
@handle-preview="handleOpenModal(eFormPreview!)"
@handle-preview2="handleOpenModal(eFormPreview2!)"
@handle-open-code-modal="handleOpenModal(codeModal!)"
@handle-clear-form-items="handleClearFormItems"
/>
<FormComponentPanel
:current-item="formConfig.currentItem"
:data="formConfig"
@handle-set-select-item="handleSetSelectItem"
/>
</LayoutContent>
<LayoutSider
:class="`right ${prefixCls}-sider`"
collapsible
:reverseArrow="true"
collapsedWidth="0"
width="270"
:zeroWidthTriggerStyle="{ 'margin-top': '-70px', 'background-color': 'gray' }"
breakpoint="lg"
>
<PropsPanel ref="propsPanel" :activeKey="formConfig.activeKey">
<template v-for="item of formConfig.schemas" #[`${item.component}Props`]="data">
<slot
:name="`${item.component}Props`"
v-bind="{ formItem: data, props: data.componentProps }"
></slot>
</template>
</PropsPanel>
</LayoutSider>
</Layout>
<JsonModal ref="jsonModal" />
<CodeModal ref="codeModal" />
<ImportJsonModal ref="importJsonModal" />
<VFormPreview ref="eFormPreview" :formConfig="formConfig" />
<VFormPreview2 ref="eFormPreview2" :formConfig="formConfig" />
</template>
<script lang="ts" setup>
import CollapseItem from './modules/CollapseItem.vue';
import FormComponentPanel from './modules/FormComponentPanel.vue';
import JsonModal from './components/JsonModal.vue';
import VFormPreview from '../VFormPreview/index.vue';
import VFormPreview2 from '../VFormPreview/useForm.vue';
import Toolbar from './modules/Toolbar.vue';
import PropsPanel from './modules/PropsPanel.vue';
import ImportJsonModal from './components/ImportJsonModal.vue';
import CodeModal from './components/CodeModal.vue';
import 'codemirror/mode/javascript/javascript';
import { ref, provide, Ref } from 'vue';
import { Layout, LayoutContent, LayoutSider } from 'ant-design-vue';
import { IVFormComponent, IFormConfig, PropsTabKey } from '../../typings/v-form-component';
import { formItemsForEach, generateKey } from '../../utils';
import { cloneDeep } from 'lodash-es';
import { baseComponents, customComponents, layoutComponents } from '../../core/formItemConfig';
import { useRefHistory, UseRefHistoryReturn } from '@vueuse/core';
import { globalConfigState } from './config/formItemPropsConfig';
import { IFormDesignMethods, IPropsPanel, IToolbarMethods } from '../../typings/form-type';
import { useDesign } from '/@/hooks/web/useDesign';
import { CollapseContainer } from '/@/components/Container/index';
defineProps({
title: {
type: String,
default: 'v-form-antd表单设计器',
},
});
const { prefixCls } = useDesign('form-design');
//
const propsPanel = ref<null | IPropsPanel>(null);
const jsonModal = ref<null | IToolbarMethods>(null);
const importJsonModal = ref<null | IToolbarMethods>(null);
const eFormPreview = ref<null | IToolbarMethods>(null);
const eFormPreview2 = ref<null | IToolbarMethods>(null);
const codeModal = ref<null | IToolbarMethods>(null);
const formModel = ref({});
// endregion
const formConfig = ref<IFormConfig>({
//
schemas: [],
layout: 'horizontal',
labelLayout: 'flex',
labelWidth: 100,
labelCol: {},
wrapperCol: {},
currentItem: {
component: '',
componentProps: {},
},
activeKey: 1,
});
const setFormConfig = (config: IFormConfig) => {
//
config.schemas = config.schemas || [];
config.schemas.forEach((item) => {
item.colProps = item.colProps || { span: 24 };
item.componentProps = item.componentProps || {};
item.itemProps = item.itemProps || {};
});
formConfig.value = config;
};
//
const historyReturn = useRefHistory(formConfig, {
deep: true,
capacity: 20,
parse: (val: IFormConfig) => {
// 使lodash.cloneDeepcurrentItem
const formConfig = cloneDeep(val);
const { currentItem, schemas } = formConfig;
// formItems
const item = schemas && schemas.find((item) => item.key === currentItem?.key);
//
if (item) {
formConfig.currentItem = item;
}
return formConfig;
},
});
/**
* 选中表单项
* @param schema 当前选中的表单项
*/
const handleSetSelectItem = (schema: IVFormComponent) => {
formConfig.value.currentItem = schema;
handleChangePropsTabs(
schema.key ? (formConfig.value.activeKey! === 1 ? 2 : formConfig.value.activeKey!) : 1,
);
};
const setGlobalConfigState = (formItem: IVFormComponent) => {
formItem.colProps = formItem.colProps || {};
formItem.colProps.span = globalConfigState.span;
// console.log('setGlobalConfigState', formItem);
};
/**
* 添加属性
* @param schemas
* @param index
*/
const handleAddAttrs = (_formItems: IVFormComponent[], _index: number) => {};
const handleListPushDrag = (item: IVFormComponent) => {
const formItem = cloneDeep(item);
setGlobalConfigState(formItem);
generateKey(formItem);
return formItem;
};
/**
* 单击控件时添加到面板中
* @param item {IVFormComponent} 当前点击的组件
*/
const handleListPush = (item: IVFormComponent) => {
// console.log('handleListPush', item);
const formItem = cloneDeep(item);
setGlobalConfigState(formItem);
generateKey(formItem);
if (!formConfig.value.currentItem?.key) {
handleSetSelectItem(formItem);
formConfig.value.schemas && formConfig.value.schemas.push(formItem);
return;
}
handleCopy(formItem, false);
};
/**
* 复制表单项如果表单项为栅格布局则遍历所有自表单项重新生成key
* @param {IVFormComponent} formItem
* @return {IVFormComponent}
*/
const copyFormItem = (formItem: IVFormComponent) => {
const newFormItem = cloneDeep(formItem);
if (newFormItem.component === 'Grid') {
formItemsForEach([formItem], (item) => {
generateKey(item);
});
}
return newFormItem;
};
/**
* 复制或者添加表单isCopy为true时则复制表单
* @param item {IVFormComponent} 当前点击的组件
* @param isCopy {boolean} 是否复制
*/
const handleCopy = (
item: IVFormComponent = formConfig.value.currentItem as IVFormComponent,
isCopy = true,
) => {
const key = formConfig.value.currentItem?.key;
/**
* 遍历当表单项配置如果是复制则复制一份表单项如果不是复制则直接添加到表单项中
* @param schemas
*/
const traverse = (schemas: IVFormComponent[]) => {
// 使some
schemas.some((formItem: IVFormComponent, index: number) => {
if (formItem.key === key) {
//
isCopy
? schemas.splice(index, 0, copyFormItem(formItem))
: schemas.splice(index + 1, 0, item);
const event = {
newIndex: index + 1,
};
//
handleBeforeColAdd(event, schemas, isCopy);
return true;
}
if (['Grid', 'Tabs'].includes(formItem.component)) {
//
formItem.columns?.forEach((item) => {
traverse(item.children);
});
}
});
};
if (formConfig.value.schemas) {
traverse(formConfig.value.schemas);
}
};
/**
* 添加到表单中
* @param newIndex {object} 事件对象
* @param schemas {IVFormComponent[]} 表单项列表
* @param isCopy {boolean} 是否复制
*/
const handleBeforeColAdd = ({ newIndex }: any, schemas: IVFormComponent[], isCopy = false) => {
const item = schemas[newIndex];
isCopy && generateKey(item);
handleSetSelectItem(item);
};
/**
* 打开模态框
* @param Modal {IToolbarMethods}
*/
const handleOpenModal = (Modal: IToolbarMethods) => {
const config = cloneDeep(formConfig.value);
Modal?.showModal(config);
};
/**
* 切换属性面板
* @param key
*/
const handleChangePropsTabs = (key: PropsTabKey) => {
formConfig.value.activeKey = key;
};
/**
* 清空表单项列表
*/
const handleClearFormItems = () => {
formConfig.value.schemas = [];
handleSetSelectItem({ component: '' });
};
const setFormModel = (key, value) => (formModel.value[key] = value);
provide('formModel', formModel);
// inject
provide<(key: String, value: any) => void>('setFormModelMethod', setFormModel);
// region
// provide('currentItem', formConfig.value.currentItem)
// inject
provide<Ref<IFormConfig>>('formConfig', formConfig);
//
provide<UseRefHistoryReturn<any, any>>('historyReturn', historyReturn);
// inject
provide<IFormDesignMethods>('formDesignMethods', {
handleBeforeColAdd,
handleCopy,
handleListPush,
handleSetSelectItem,
handleAddAttrs,
setFormConfig,
});
// endregion
</script>
<style lang="less" scoped>
@prefix-cls: ~'@{namespace}-form-design';
[data-theme='dark'] {
.@{prefix-cls}-sider{
background-color: #1f1f1f;
}}
[data-theme='light'] {
.@{prefix-cls}-sider{
background-color: #fff;
}
}
</style>

101
apps/vue/src/components/FormDesign/src/components/VFormDesign/modules/CollapseItem.vue

@ -0,0 +1,101 @@
<template>
<div>
<draggable
tag="ul"
:model-value="list"
v-bind="{
group: { name: 'form-draggable', pull: 'clone', put: false },
sort: false,
clone: cloneItem,
animation: 180,
ghostClass: 'moving',
}"
item-key="type"
@start="handleStart($event, list)"
@add="handleAdd"
>
<template #item="{ element, index }">
<li
class="bs-box text-ellipsis"
@dragstart="$emit('add-attrs', list, index)"
@click="$emit('handle-list-push', element)"
>
<!-- <svg v-if="element.icon.indexOf('icon-') > -1" class="icon" aria-hidden="true">
<use :xlink:href="`#${element.icon}`" />
</svg> -->
<Icon :icon="element.icon" />
{{ element.label }}</li
></template
>
</draggable>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
import { IVFormComponent } from '../../../typings/v-form-component';
import draggable from 'vuedraggable';
// import { toRefs } from '@vueuse/core';
import { Icon } from '/@/components/Icon';
export default defineComponent({
name: 'CollapseItem',
components: { draggable, Icon },
props: {
list: {
type: [Array] as PropType<IVFormComponent[]>,
default: () => [],
},
handleListPush: {
type: Function as PropType<(item: IVFormComponent) => void>,
default: null,
},
},
setup(props, { emit }) {
const state = reactive({});
const handleStart = (e: any, list1: IVFormComponent[]) => {
emit('start', list1[e.oldIndex].component);
};
const handleAdd = (e: any) => {
console.log(e);
};
// https://github.com/SortableJS/vue.draggable.next
// https://github.com/SortableJS/vue.draggable.next/blob/master/example/components/custom-clone.vue
const cloneItem = (one) => {
return props.handleListPush(one);
};
return { state, handleStart, handleAdd, cloneItem };
},
});
</script>
<style lang="less" scoped>
@import url(../styles/variable.less);
ul {
padding: 5px;
list-style: none;
display: flex;
margin-bottom: 0;
flex-wrap: wrap;
// background: #efefef;
li {
padding: 8px 12px;
transition: all 0.3s;
width: calc(50% - 6px);
margin: 2.7px;
height: 36px;
line-height: 20px;
cursor: move;
border: 1px solid @border-color;
border-radius: 3px;
&:hover {
color: @primary-color;
border: 1px solid @primary-color;
position: relative;
// z-index: 1;
box-shadow: 0 2px 6px @primary-color;
}
}
}
svg {
display: inline !important;
}
</style>

150
apps/vue/src/components/FormDesign/src/components/VFormDesign/modules/FormComponentPanel.vue

@ -0,0 +1,150 @@
<!--
* @Description: 中间表单布局面板
* https://github.com/SortableJS/vue.draggable.next/issues/138
-->
<template>
<div class="form-panel v-form-container">
<Empty
class="empty-text"
v-show="formConfig.schemas.length === 0"
description="从左侧选择控件添加"
/>
<Form v-bind="formConfig">
<div class="draggable-box">
<draggable
class="list-main ant-row"
group="form-draggable"
:component-data="{ name: 'list', tag: 'div', type: 'transition-group' }"
ghostClass="moving"
:animation="180"
handle=".drag-move"
v-model="formConfig.schemas"
item-key="key"
@add="addItem"
@start="handleDragStart"
>
<template #item="{ element }">
<LayoutItem
class="drag-move"
:schema="element"
:data="formConfig"
:current-item="formConfig.currentItem || {}"
/>
</template>
</draggable>
</div>
</Form>
</div>
</template>
<script lang="ts">
import draggable from 'vuedraggable';
import { defineComponent, computed } from 'vue';
import LayoutItem from '../components/LayoutItem.vue';
import { cloneDeep } from 'lodash-es';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { Form, Empty } from 'ant-design-vue';
export default defineComponent({
name: 'FormComponentPanel',
components: {
LayoutItem,
draggable,
Form,
Empty,
},
emits: ['handleSetSelectItem'],
setup(_, { emit }) {
const { formConfig } = useFormDesignState() as Recordable;
/**
* 拖拽完成事件
* @param newIndex
*/
const addItem = ({ newIndex }: any) => {
formConfig.value.schemas = formConfig.value.schemas || [];
const schemas = formConfig.value.schemas;
schemas[newIndex] = cloneDeep(schemas[newIndex]);
emit('handleSetSelectItem', schemas[newIndex]);
};
/**
* 拖拽开始事件
* @param e {Object} 事件对象
*/
const handleDragStart = (e: any) => {
emit('handleSetSelectItem', formConfig.value.schemas[e.oldIndex]);
};
// currentItem
// AColdiv
const layoutTag = computed(() => {
return formConfig.value.layout === 'horizontal' ? 'Col' : 'div';
});
return {
addItem,
handleDragStart,
formConfig,
layoutTag,
};
},
});
</script>
<style lang="less" scoped>
@import url(../styles/variable.less);
@import url(../styles/drag.less);
.v-form-container {
//
.ant-form-inline {
.list-main {
display: flex;
flex-wrap: wrap;
justify-content: flex-start;
align-content: flex-start;
.layout-width {
width: 100%;
}
}
.ant-form-item-control-wrapper {
min-width: 175px !important;
}
}
}
.form-panel {
position: relative;
height: 100%;
.empty-text {
color: #aaa;
height: 150px;
top: -10%;
left: 0;
right: 0;
bottom: 0;
margin: auto;
position: absolute;
z-index: 100;
}
.draggable-box {
// width: 100%;
.drag-move {
cursor: move;
min-height: 62px;
}
.list-main {
overflow: auto;
height: 100%;
//
.list-enter-active {
transition: all 0.5s;
}
.list-leave-active {
transition: all 0.3s;
}
.list-enter,
.list-leave-to {
opacity: 0;
transform: translateX(-100px);
}
.list-enter {
height: 30px;
}
}
}
}
</style>

91
apps/vue/src/components/FormDesign/src/components/VFormDesign/modules/PropsPanel.vue

@ -0,0 +1,91 @@
<!--
* @Description: 右侧属性配置面板
-->
<template>
<div>
<Tabs v-model:activeKey="formConfig.activeKey" :tabBarStyle="{ margin: 0 }">
<TabPane :key="1" tab="表单">
<FormProps />
</TabPane>
<TabPane :key="2" tab="控件">
<FormItemProps />
</TabPane>
<TabPane :key="3" tab="栅格">
<ComponentColumnProps />
</TabPane>
<TabPane :key="4" tab="组件">
<slot v-if="slotProps" :name="slotProps.component + 'Props'"></slot>
<ComponentProps v-else />
</TabPane>
</Tabs>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import FormProps from '../components/FormProps.vue';
import FormItemProps from '../components/FormItemProps.vue';
import ComponentProps from '../components/ComponentProps.vue';
import ComponentColumnProps from '../components/FormItemColumnProps.vue';
import { useFormDesignState } from '../../../hooks/useFormDesignState';
import { customComponents } from '../../../core/formItemConfig';
import { TabPane, Tabs } from 'ant-design-vue';
type ChangeTabKey = 1 | 2;
export interface IPropsPanel {
changeTab: (key: ChangeTabKey) => void;
}
export default defineComponent({
name: 'PropsPanel',
components: {
FormProps,
FormItemProps,
ComponentProps,
ComponentColumnProps,
Tabs,
TabPane,
},
setup() {
const { formConfig } = useFormDesignState();
const slotProps = computed(() => {
return customComponents.find(
(item) => item.component === formConfig.value.currentItem?.component,
);
});
return { formConfig, customComponents, slotProps };
},
});
</script>
<style lang="less" scoped>
@import url(../styles/variable.less);
:deep(.ant-tabs) {
box-sizing: border-box;
form {
width: 100%;
position: absolute;
height: calc(100% - 50px);
margin-right: 10px;
overflow-y: auto;
overflow-x: hidden;
}
.hint-box {
margin-top: 200px;
}
.ant-form-item,
.ant-slider-with-marks {
margin-left: 10px;
margin-right: 20px;
margin-bottom: 0;
}
.ant-form-item {
// width: 100%;
margin-bottom: 0;
.ant-form-item-label {
line-height: 2;
vertical-align: text-top;
}
}
.ant-input-number {
width: 100%;
}
}
</style>

128
apps/vue/src/components/FormDesign/src/components/VFormDesign/modules/Toolbar.vue

@ -0,0 +1,128 @@
<!--
* @Description: 工具栏
-->
<template>
<div class="operating-area">
<!-- 头部操作按钮区域 start -->
<!-- 操作左侧区域 start -->
<div class="left-btn-box">
<Tooltip v-for="item in toolbarsConfigs" :title="item.title" :key="item.icon">
<a @click="$emit(item.event)" class="toolbar-text">
<Icon :icon="item.icon" />
</a>
</Tooltip>
<Divider type="vertical" />
<Tooltip title="撤销">
<a :class="{ disabled: !canUndo }" :disabled="!canUndo" @click="undo">
<Icon icon="ant-design:undo-outlined" />
</a>
</Tooltip>
<Tooltip title="重做">
<a :class="{ disabled: !canRedo }" :disabled="!canRedo" @click="redo">
<Icon icon="ant-design:redo-outlined" />
</a>
</Tooltip>
</div>
</div>
<!-- 操作区域 start -->
</template>
<script lang="ts">
import { defineComponent, inject, reactive, toRefs } from 'vue';
import { UseRefHistoryReturn } from '@vueuse/core';
import { IFormConfig } from '../../../typings/v-form-component';
import { Tooltip, Divider } from 'ant-design-vue';
import Icon from '/@/components/Icon/index';
interface IToolbarsConfig {
type: string;
title: string;
icon: string;
event: string;
}
export default defineComponent({
name: 'OperatingArea',
components: {
Tooltip,
Icon,
Divider,
},
setup() {
const state = reactive<{
toolbarsConfigs: IToolbarsConfig[];
}>({
toolbarsConfigs: [
{
title: '预览-支持布局',
type: 'preview',
event: 'handlePreview',
icon: 'ant-design:chrome-filled',
},
{
title: '预览-不支持布局',
type: 'preview',
event: 'handlePreview2',
icon: 'ant-design:chrome-filled',
},
{
title: '导入JSON',
type: 'importJson',
event: 'handleOpenImportJsonModal',
icon: 'ant-design:import-outlined',
},
{
title: '生成JSON',
type: 'exportJson',
event: 'handleOpenJsonModal',
icon: 'ant-design:export-outlined',
},
{
title: '生成代码',
type: 'exportCode',
event: 'handleOpenCodeModal',
icon: 'ant-design:code-filled',
},
{
title: '清空',
type: 'reset',
event: 'handleClearFormItems',
icon: 'ant-design:clear-outlined',
},
],
});
const historyRef = inject('historyReturn') as UseRefHistoryReturn<IFormConfig, IFormConfig>;
const { undo, redo, canUndo, canRedo } = historyRef;
return { ...toRefs(state), undo, redo, canUndo, canRedo };
},
});
</script>
<style lang="less" scoped>
//noinspection CssUnknownTarget
@import url('../styles/variable.less');
.operating-area {
border-bottom: 2px solid @border-color;
font-size: 16px;
text-align: left;
height: @operating-area-height;
line-height: @operating-area-height;
padding: 0 12px;
display: flex;
justify-content: space-between;
align-content: center;
padding-left: 30px;
a {
color: #666;
margin: 0 5px;
&.disabled,
&.disabled:hover {
color: #ccc;
}
&:hover {
color: @primary-color;
}
> span {
font-size: 14px;
padding-left: 2px;
}
}
}
</style>

226
apps/vue/src/components/FormDesign/src/components/VFormDesign/styles/drag.less

@ -0,0 +1,226 @@
.draggable-box {
height: 100%;
overflow: auto;
/* stylelint-disable-next-line selector-pseudo-class-no-unknown */
:deep(.list-main) {
overflow: hidden;
padding: 5px;
position: relative;
.moving {
// 拖放移动中;
min-height: 35px;
box-sizing: border-box;
overflow: hidden;
padding: 0 !important;
position: relative;
&::before {
content: '';
height: 5px;
width: 100%;
background-color: @primary-color;
position: absolute;
top: 0;
right: 0;
}
}
.drag-move-box {
position: relative;
box-sizing: border-box;
padding: 8px;
overflow: hidden;
transition: all 0.3s;
min-height: 60px;
&:hover {
background-color: @primary-hover-bg-color;
}
// 选择时 start
&::before {
content: '';
height: 5px;
width: 100%;
background-color: @primary-color;
position: absolute;
top: 0;
right: -100%;
transition: all 0.3s;
}
&.active {
background-color: @primary-hover-bg-color;
outline-offset: 0;
&::before {
right: 0;
}
}
// 选择时 end
.form-item-box {
position: relative;
box-sizing: border-box;
word-wrap: break-word;
&::before {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.ant-form-item {
// 修改ant form-item的margin为padding
margin: 0;
padding-bottom: 6px;
}
}
.show-key-box {
// 显示key
position: absolute;
bottom: 2px;
right: 5px;
font-size: 14px;
// z-index: 999;
color: @primary-color;
}
.copy,
.delete {
position: absolute;
top: 0;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
color: #fff;
// z-index: 989;
transition: all 0.3s;
&.unactivated {
opacity: 0 !important;
pointer-events: none;
}
&.active {
opacity: 1 !important;
}
}
.copy {
border-radius: 0 0 0 8px;
right: 30px;
background-color: @primary-color;
}
.delete {
right: 0;
background-color: @primary-color;
}
}
.grid-box {
position: relative;
box-sizing: border-box;
padding: 5px;
background-color: @layout-background-color;
width: 100%;
transition: all 0.3s;
overflow: hidden;
.form-item-box {
position: relative;
box-sizing: border-box;
.ant-form-item {
// 修改ant form-item的margin为padding
margin: 0;
padding-bottom: 15px;
}
}
.grid-row {
background-color: @layout-background-color;
.grid-col {
.draggable-box {
min-height: 80px;
min-width: 50px;
border: 1px #ccc dashed;
// background: #fff;
.list-main {
min-height: 83px;
position: relative;
border: 1px #ccc dashed;
}
}
}
}
// 选择时 start
&::before {
content: '';
height: 5px;
width: 100%;
background: transparent;
position: absolute;
top: 0;
right: -100%;
transition: all 0.3s;
}
&.active {
background-color: @layout-hover-bg-color;
outline-offset: 0;
&::before {
background-color: @layout-color;
right: 0;
}
}
// 选择时 end
> .copy-delete-box {
> .copy,
> .delete {
position: absolute;
top: 0;
width: 30px;
height: 30px;
line-height: 30px;
text-align: center;
color: #fff;
// z-index: 989;
transition: all 0.3s;
&.unactivated {
opacity: 0 !important;
pointer-events: none;
}
&.active {
opacity: 1 !important;
}
}
> .copy {
border-radius: 0 0 0 8px;
right: 30px;
background-color: @layout-color;
}
> .delete {
right: 0;
background-color: @layout-color;
}
}
}
}
}

15
apps/vue/src/components/FormDesign/src/components/VFormDesign/styles/variable.less

@ -0,0 +1,15 @@
// 表单设计器样式
@primary-color: #13c2c2;
@layout-color: #9867f7;
@primary-background-color: fade(@primary-color, 6%);
@primary-hover-bg-color: fade(@primary-color, 20%);
@layout-background-color: fade(@layout-color, 12%);
@layout-hover-bg-color: fade(@layout-color, 24%);
@title-text-color: #fff;
@border-color: #ccc;
@left-right-width: 280px;
@header-height: 56px;
@operating-area-height: 45px;

0
apps/vue/src/components/FormDesign/src/components/VFormDesign/styles/variable.wxss

204
apps/vue/src/components/FormDesign/src/components/VFormItem/index.vue

@ -0,0 +1,204 @@
<!--
* @Description:
-->
<template>
<Col v-bind="colPropsComputed">
<FormItem v-bind="{ ...formItemProps }">
<template #label v-if="!formItemProps.hiddenLabel && schema.component !== 'Divider'">
<Tooltip>
<span>{{ schema.label }}</span>
<template #title v-if="schema.helpMessage"
><span>{{ schema.helpMessage }}</span></template
>
<Icon v-if="schema.helpMessage" class="ml-5" icon="ant-design:question-circle-outlined" />
</Tooltip>
</template>
<slot
v-if="schema.componentProps && schema.componentProps?.slotName"
:name="schema.componentProps.slotName"
v-bind="schema"
></slot>
<Divider
v-else-if="schema.component == 'Divider' && schema.label && !formItemProps.hiddenLabel"
>{{ schema.label }}</Divider
>
<!-- 部分控件需要一个空div -->
<div
><component
class="v-form-item-wrapper"
:is="componentItem"
v-bind="{ ...cmpProps, ...asyncProps }"
:schema="schema"
:style="schema.width ? { width: schema.width } : {}"
@change="handleChange"
@click="handleClick(schema)"
/></div>
<span v-if="['Button'].includes(schema.component)">{{ schema.label }}</span>
</FormItem>
</Col>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, computed, PropType, unref } from 'vue';
import { componentMap } from '../../core/formItemConfig';
import { IVFormComponent, IFormConfig } from '../../typings/v-form-component';
import { asyncComputed } from '@vueuse/core';
import { handleAsyncOptions } from '../../utils';
import { omit } from 'lodash-es';
import { Tooltip, FormItem, Divider, Col } from 'ant-design-vue';
// import FormItem from '/@/components/Form/src/components/FormItem.vue';
import { Icon } from '/@/components/Icon';
import { useFormModelState } from '../../hooks/useFormDesignState';
export default defineComponent({
name: 'VFormItem',
components: {
Tooltip,
Icon,
FormItem,
Divider,
Col,
},
props: {
formData: {
type: Object,
default: () => ({}),
},
schema: {
type: Object as PropType<IVFormComponent>,
required: true,
},
formConfig: {
type: Object as PropType<IFormConfig>,
required: true,
},
},
emits: ['update:form-data', 'change'],
setup(props, { emit }) {
const state = reactive({
componentMap,
});
const { formModel: formData1, setFormModel } = useFormModelState();
const colPropsComputed = computed(() => {
const { colProps = {} } = props.schema;
return colProps;
});
const formItemProps = computed(() => {
const { formConfig } = unref(props);
let { field, required, rules, labelCol, wrapperCol } = unref(props.schema);
const { colon } = props.formConfig;
const { itemProps } = unref(props.schema);
//<editor-fold desc="">
labelCol = labelCol
? labelCol
: formConfig.layout === 'horizontal'
? formConfig.labelLayout === 'flex'
? { style: `width:${formConfig.labelWidth}px` }
: formConfig.labelCol
: {};
wrapperCol = wrapperCol
? wrapperCol
: formConfig.layout === 'horizontal'
? formConfig.labelLayout === 'flex'
? { style: 'width:auto;flex:1' }
: formConfig.wrapperCol
: {};
const style =
formConfig.layout === 'horizontal' && formConfig.labelLayout === 'flex'
? { display: 'flex' }
: {};
/**
* 将字符串正则格式化成正则表达式
*/
const newConfig = Object.assign(
{},
{
name: field,
style: { ...style },
colon,
required,
rules,
labelCol,
wrapperCol,
},
itemProps,
);
if (!itemProps?.labelCol?.span) {
newConfig.labelCol = labelCol;
}
if (!itemProps?.wrapperCol?.span) {
newConfig.wrapperCol = wrapperCol;
}
if (!itemProps?.rules) {
newConfig.rules = rules;
}
return newConfig;
}) as Recordable;
const componentItem = computed(() => componentMap.get(props.schema.component as string));
// console.log('component change:', props.schema.component, componentItem.value);
const handleClick = (schema: IVFormComponent) => {
if (schema.component === 'Button' && schema.componentProps?.handle)
emit(schema.componentProps?.handle);
};
/**
* 处理异步属性异步属性会导致一些属性渲染错误如defaultValue异步加载会导致渲染不出来故而此处只处理optionstreeData同步属性在cmpProps中处理
*/
const asyncProps = asyncComputed(async () => {
let { options, treeData } = props.schema.componentProps ?? {};
if (options) options = await handleAsyncOptions(options);
if (treeData) treeData = await handleAsyncOptions(treeData);
return {
options,
treeData,
};
});
/**
* 处理同步属性
*/
const cmpProps = computed(() => {
const isCheck =
props.schema && ['Switch', 'Checkbox', 'Radio'].includes(props.schema.component);
let { field } = props.schema;
let { disabled, ...attrs } =
omit(props.schema.componentProps, ['options', 'treeData']) ?? {};
disabled = props.formConfig.disabled || disabled;
return {
...attrs,
disabled,
[isCheck ? 'checked' : 'value']: formData1.value[field!],
};
});
const handleChange = function (e) {
const isCheck = ['Switch', 'Checkbox', 'Radio'].includes(props.schema.component);
const target = e ? e.target : null;
const value = target ? (isCheck ? target.checked : target.value) : e;
setFormModel(props.schema.field!, value);
emit('change', value);
};
return {
...toRefs(state),
componentItem,
formItemProps,
handleClick,
asyncProps,
cmpProps,
handleChange,
colPropsComputed,
};
},
});
</script>
<style lang="less" scoped>
.ml-5 {
margin-left: 5px;
}
// formant-col使width:100%
:deep(.ant-col) {
width: auto;
}
.ant-form-item:not(.ant-form-item-with-help) {
margin-bottom: 20px;
}
// .w-full {
// width: 100% !important;
// }
</style>

64
apps/vue/src/components/FormDesign/src/components/VFormItem/vFormItem.vue

@ -0,0 +1,64 @@
<!--
* @Description:
`<FormItem`
:tableAction="tableAction"
:formActionType="formActionType"
:schema="schema2"
:formProps="getProps"
:allDefaultValues="defaultValueRef"
:formModel="formModel"
:setFormModel="setFormModel"
>
-->
<template>
<FormItem :schema="schemaNew" :formProps="getProps">
<template #[item]="data" v-for="item in Object.keys($slots)">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</FormItem>
</template>
<script lang="ts">
import { computed, defineComponent, unref } from 'vue';
import { IFormConfig, IVFormComponent } from '../../typings/v-form-component';
import { FormProps, FormSchema } from '/@/components/Form';
import FormItem from '/@/components/Form/src/components/FormItem.vue';
export default defineComponent({
name: 'VFormItem',
components: {
FormItem,
},
props: {
formData: {
type: Object,
default: () => ({}),
},
schema: {
type: Object as PropType<IVFormComponent>,
required: true,
},
formConfig: {
type: Object as PropType<IFormConfig>,
required: true,
},
},
setup(props) {
const schema = computed(() => {
const schema: FormSchema = {
...unref(props.schema),
} as FormSchema;
return schema;
});
// Get the basic configuration of the form
const getProps = computed((): FormProps => {
return { ...unref(props.formConfig) } as FormProps;
});
return {
schemaNew: schema,
getProps,
};
},
});
</script>
<style lang="less" scoped></style>

98
apps/vue/src/components/FormDesign/src/components/VFormPreview/index.vue

@ -0,0 +1,98 @@
<!--
* @Description: 渲染组件无法使用Vben的组件
-->
<template>
<Modal
title="预览(支持布局)"
:visible="visible"
@ok="handleGetData"
@cancel="handleCancel"
okText="获取数据"
cancelText="关闭"
style="top: 20px"
:destroyOnClose="true"
:width="900"
>
<VFormCreate
:form-config="formConfig"
v-model:fApi="fApi"
v-model:formModel="formModel"
@submit="onSubmit"
>
<template #slotName="{ formModel, field }">
<a-input v-model:value="formModel[field]" placeholder="我是插槽传递的输入框" />
</template>
</VFormCreate>
<JsonModal ref="jsonModal" />
</Modal>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
import { IFormConfig } from '../../typings/v-form-component';
import { IAnyObject } from '../../typings/base-type';
import VFormCreate from '../VFormCreate/index.vue';
import { formatRules } from '../../utils';
import { IVFormMethods } from '../../hooks/useVFormMethods';
import JsonModal from '../VFormDesign/components/JsonModal.vue';
import { IToolbarMethods } from '../../typings/form-type';
import { Modal } from 'ant-design-vue';
export default defineComponent({
name: 'VFormPreview',
components: {
JsonModal,
VFormCreate,
Modal,
},
setup() {
const jsonModal = ref<IToolbarMethods | null>(null);
const state = reactive<{
formModel: IAnyObject;
visible: boolean;
formConfig: IFormConfig;
fApi: IVFormMethods;
}>({
formModel: {},
formConfig: {} as IFormConfig,
visible: false,
fApi: {} as IVFormMethods,
});
/**
* 显示Json数据弹框
* @param jsonData
*/
const showModal = (jsonData: IFormConfig) => {
// console.log('showModal-', jsonData);
formatRules(jsonData.schemas);
state.formConfig = jsonData;
state.visible = true;
};
/**
* 获取表单数据
* @return {Promise<void>}
*/
const handleCancel = () => {
state.visible = false;
state.formModel = {};
};
const handleGetData = async () => {
const _data = await state.fApi.submit?.();
jsonModal.value?.showModal?.(_data);
};
const onSubmit = (_data: IAnyObject) => {
//
};
const onCancel = () => {
state.formModel = {};
};
return {
handleGetData,
handleCancel,
...toRefs(state),
showModal,
jsonModal,
onSubmit,
onCancel,
};
},
});
</script>

65
apps/vue/src/components/FormDesign/src/components/VFormPreview/useForm.vue

@ -0,0 +1,65 @@
<!--
* @Description: 使用vbenForm的功能进行渲染
-->
<template>
<Modal
title="预览(不支持布局)"
:visible="state.visible"
@ok="handleGetData"
@cancel="handleCancel"
okText="获取数据"
cancelText="关闭"
style="top: 20px"
:destroyOnClose="true"
:width="900"
>
<BasicForm v-bind="attrs" @register="registerForm" />
<JsonModal ref="jsonModal" />
</Modal>
</template>
<script lang="ts" setup>
import { BasicForm, useForm } from '/@/components/Form/index';
import { reactive, ref, computed } from 'vue';
import { IFormConfig } from '../../typings/v-form-component';
import { IAnyObject } from '../../typings/base-type';
import JsonModal from '../VFormDesign/components/JsonModal.vue';
import { IToolbarMethods } from '../../typings/form-type';
import { Modal } from 'ant-design-vue';
const jsonModal = ref<IToolbarMethods | null>(null);
const state = reactive<{
formModel: IAnyObject;
visible: boolean;
formConfig: IFormConfig;
}>({
formModel: {},
formConfig: {} as IFormConfig,
visible: false,
});
const attrs = computed(() => {
return {
...state.formConfig,
} as Recordable;
});
/**
* 显示Json数据弹框
* @param jsonData
*/
const showModal = (jsonData: IFormConfig) => {
state.formConfig = jsonData;
state.visible = true;
};
//
const [registerForm, { validate }] = useForm();
const handleCancel = () => {
state.visible = false;
};
/**
* 获取表单数据
* @return {Promise<void>}
*/
const handleGetData = async () => {
let data = await validate();
jsonModal.value?.showModal?.(data);
};
defineExpose({ showModal });
</script>

74
apps/vue/src/components/FormDesign/src/components/index.ts

@ -0,0 +1,74 @@
import type { Component } from 'vue';
import { ComponentType } from '/@/components/Form/src/types';
import { IconPicker } from '/@/components/Icon/index';
/**
* Component list, register here to setting it in the form
*/
import {
Avatar,
Input,
Button,
Select,
Radio,
Checkbox,
AutoComplete,
Cascader,
DatePicker,
InputNumber,
Switch,
TimePicker,
// ColorPicker,
TreeSelect,
Slider,
Rate,
Divider,
Calendar,
Transfer,
Table,
} from 'ant-design-vue';
//ant-desing本身的Form控件库
const componentMap = new Map<string, Component>();
componentMap.set('Radio', Radio);
componentMap.set('Button', Button);
componentMap.set('Calendar', Calendar);
componentMap.set('Input', Input);
componentMap.set('InputGroup', Input.Group);
componentMap.set('InputPassword', Input.Password);
componentMap.set('InputSearch', Input.Search);
componentMap.set('InputTextArea', Input.TextArea);
componentMap.set('InputNumber', InputNumber);
componentMap.set('AutoComplete', AutoComplete);
componentMap.set('Select', Select);
componentMap.set('TreeSelect', TreeSelect);
componentMap.set('Switch', Switch);
componentMap.set('RadioGroup', Radio.Group);
componentMap.set('Checkbox', Checkbox);
componentMap.set('CheckboxGroup', Checkbox.Group);
componentMap.set('Cascader', Cascader);
componentMap.set('Slider', Slider);
componentMap.set('Rate', Rate);
componentMap.set('Transfer', Transfer);
componentMap.set('DatePicker', DatePicker);
componentMap.set('MonthPicker', DatePicker.MonthPicker);
componentMap.set('RangePicker', DatePicker.RangePicker);
componentMap.set('WeekPicker', DatePicker.WeekPicker);
componentMap.set('TimePicker', TimePicker);
componentMap.set('IconPicker', IconPicker);
componentMap.set('Divider', Divider);
componentMap.set('Avatar', Avatar);
componentMap.set('Table', Table);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: ComponentType) {
componentMap.delete(compName);
}
export { componentMap };

493
apps/vue/src/components/FormDesign/src/core/formItemConfig.ts

@ -0,0 +1,493 @@
/**
* @description
*/
import { IVFormComponent } from '../typings/v-form-component';
import { isArray } from 'lodash-es';
import { componentMap as VbenCmp, add } from '/@/components/Form/src/componentMap';
import { ComponentType } from '/@/components/Form/src/types';
import { componentMap as Cmp } from '../components';
import { Component } from 'vue';
const componentMap = new Map<string, Component>();
//如果有其它控件,可以在这里初始化
//注册Ant控件库
Cmp.forEach((value, key) => {
componentMap.set(key, value);
if (VbenCmp[key] == null) {
add(key as ComponentType, value);
}
});
//注册vben控件库
VbenCmp.forEach((value, key) => {
componentMap.set(key, value);
});
export { componentMap };
/**
*
* @param {IVFormComponent | IVFormComponent[]} config
*/
export function setFormDesignComponents(config: IVFormComponent | IVFormComponent[]) {
if (isArray(config)) {
config.forEach((item) => {
const { componentInstance: component, ...rest } = item;
componentMap[item.component] = component;
customComponents.push(Object.assign({ props: {} }, rest));
});
} else {
const { componentInstance: component, ...rest } = config;
componentMap[config.component] = component;
customComponents.push(Object.assign({ props: {} }, rest));
}
}
//外部设置的自定义控件
export const customComponents: IVFormComponent[] = [];
// 左侧控件列表与初始化的控件属性
// props.slotName,会在formitem级别生成一个slot,并绑定当前record值
// 属性props,类型为对象,不能为undefined或是null。
export const baseComponents: IVFormComponent[] = [
{
component: 'InputCountDown',
label: '倒计时输入',
icon: 'line-md:iconify2',
colProps: { span: 24 },
field: '',
componentProps: {},
},
{
component: 'IconPicker',
label: '图标选择器',
icon: 'line-md:iconify2',
colProps: { span: 24 },
field: '',
componentProps: {},
},
{
component: 'StrengthMeter',
label: '密码强度',
icon: 'wpf:password1',
colProps: { span: 24 },
field: '',
componentProps: {},
},
{
component: 'AutoComplete',
label: '自动完成',
icon: 'wpf:password1',
colProps: { span: 24 },
field: '',
componentProps: {
placeholder: '请输入正则表达式',
options: [
{
value: '/^(?:(?:\\+|00)86)?1[3-9]\\d{9}$/',
label: '手机号码',
},
{
value: '/^((ht|f)tps?:\\/\\/)?[\\w-]+(\\.[\\w-]+)+:\\d{1,5}\\/?$/',
label: '网址带端口号',
},
],
},
},
{
component: 'Divider',
label: '分割线',
icon: 'radix-icons:divider-horizontal',
colProps: { span: 24 },
field: '',
componentProps: {
orientation: 'center',
dashed: true,
},
},
{
component: 'Checkbox',
label: '复选框',
icon: 'ant-design:check-circle-outlined',
colProps: { span: 24 },
field: '',
},
{
component: 'CheckboxGroup',
label: '复选框-组',
icon: 'ant-design:check-circle-filled',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
},
{
component: 'Input',
label: '输入框',
icon: 'bi:input-cursor-text',
field: '',
colProps: { span: 24 },
componentProps: {
type: 'text',
},
},
{
component: 'InputNumber',
label: '数字输入框',
icon: 'ant-design:field-number-outlined',
field: '',
colProps: { span: 24 },
componentProps: { style: 'width:200px' },
},
{
component: 'InputTextArea',
label: '文本域',
icon: 'ant-design:file-text-filled',
field: '',
colProps: { span: 24 },
componentProps: {},
},
{
component: 'Select',
label: '下拉选择',
icon: 'gg:select',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
},
{
component: 'Radio',
label: '单选框',
icon: 'ant-design:check-circle-outlined',
field: '',
colProps: { span: 24 },
componentProps: {},
},
{
component: 'RadioGroup',
label: '单选框-组',
icon: 'carbon:radio-button-checked',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
},
{
component: 'DatePicker',
label: '日期选择',
icon: 'healthicons:i-schedule-school-date-time-outline',
field: '',
colProps: { span: 24 },
componentProps: {},
},
{
component: 'RangePicker',
label: '日期范围',
icon: 'healthicons:i-schedule-school-date-time-outline',
field: '',
colProps: { span: 24 },
componentProps: {
placeholder: ['开始日期', '结束日期'],
},
},
{
component: 'MonthPicker',
label: '月份选择',
icon: 'healthicons:i-schedule-school-date-time-outline',
field: '',
colProps: { span: 24 },
componentProps: {
placeholder: '请选择月份',
},
},
{
component: 'TimePicker',
label: '时间选择',
icon: 'healthicons:i-schedule-school-date-time',
field: '',
colProps: { span: 24 },
componentProps: {},
},
{
component: 'Slider',
label: '滑动输入条',
icon: 'vaadin:slider',
field: '',
colProps: { span: 24 },
componentProps: {},
},
{
component: 'Rate',
label: '评分',
icon: 'ic:outline-star-rate',
field: '',
colProps: { span: 24 },
componentProps: {},
},
{
component: 'Switch',
label: '开关',
icon: 'entypo:switch',
field: '',
colProps: { span: 24 },
componentProps: {},
},
{
component: 'TreeSelect',
label: '树形选择',
icon: 'clarity:tree-view-line',
field: '',
colProps: { span: 24 },
componentProps: {
treeData: [
{
label: '选项1',
value: '1',
children: [
{
label: '选项三',
value: '1-1',
},
],
},
{
label: '选项2',
value: '2',
},
],
},
},
{
component: 'Upload',
label: '上传',
icon: 'ant-design:upload-outlined',
field: '',
colProps: { span: 24 },
componentProps: {
api: () => 1,
},
},
{
component: 'Cascader',
label: '级联选择',
icon: 'ant-design:check-outlined',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1',
children: [
{
label: '选项三',
value: '1-1',
},
],
},
{
label: '选项2',
value: '2',
},
],
},
},
// {
// component: 'Button',
// label: '按钮',
// icon: 'dashicons:button',
// field: '',
// colProps: { span: 24 },
// hiddenLabel: true,
// componentProps: {},
// },
// {
// component: 'ColorPicker',
// label: '颜色选择器',
// icon: 'carbon:color-palette',
// field: '',
// colProps: { span: 24 },
// componentProps: {
// defaultValue: '',
// value: '',
// },
// },
{
component: 'Avatar',
label: '头像',
icon: 'radix-icons:avatar',
field: '',
colProps: { span: 24 },
componentProps: {},
},
{
component: 'Descriptions',
label: '描述列表',
icon: 'cil:description',
field: '',
colProps: { span: 24 },
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
},
{
component: 'slot',
label: '插槽',
icon: 'vs:timeslot-question',
field: '',
colProps: { span: 24 },
componentProps: {
slotName: 'slotName',
},
},
];
// https://next.antdv.com/components/transfer-cn
const transferControl = {
component: 'Transfer',
label: '穿梭框',
icon: 'bx:bx-transfer-alt',
field: '',
colProps: { span: 24 },
componentProps: {
render: (item) => item.title,
dataSource: [
{
key: 'key-1',
title: '标题1',
description: '描述',
disabled: false,
chosen: true,
},
{
key: 'key-2',
title: 'title2',
description: 'description2',
disabled: true,
},
{
key: 'key-3',
title: '标题3',
description: '描述3',
disabled: false,
chosen: true,
},
],
},
};
baseComponents.push(transferControl);
const tableControl = {
component: 'Table',
label: '表格',
icon: 'bx:bx-table',
field: '',
colProps: { span: 24 },
componentProps: {
render: (item) => item.title,
columns: [
{
align: 'left',
title: '列1',
dataIndex: 'col1'
},
{
align: 'left',
title: '列2',
dataIndex: 'col2'
},
{
align: 'left',
title: '列3',
dataIndex: 'col3'
},
],
dataSource: [
{
col1: '数据1-1',
col2: '数据1-2',
col3: '数据1-3',
},
{
col1: '数据2-1',
col2: '数据2-2',
col3: '数据2-3',
},
{
col1: '数据3-1',
col2: '数据3-2',
col3: '数据3-3',
},
],
},
}
baseComponents.push(tableControl);
export const layoutComponents: IVFormComponent[] = [
{
field: '',
component: 'Grid',
label: '栅格布局',
icon: 'icon-grid',
componentProps: {},
columns: [
{
span: 12,
children: [],
},
{
span: 12,
children: [],
},
],
colProps: { span: 24 },
options: {
gutter: 0,
},
},
];

739
apps/vue/src/components/FormDesign/src/core/iconConfig.ts

@ -0,0 +1,739 @@
const iconConfig = {
filled: [
'account-book',
'alert',
'alipay-circle',
'alipay-square',
'aliwangwang',
'amazon-circle',
'android',
'amazon-square',
'api',
'appstore',
'audio',
'apple',
'backward',
'bank',
'behance-circle',
'bell',
'behance-square',
'book',
'box-plot',
'bug',
'bulb',
'calculator',
'build',
'calendar',
'camera',
'car',
'caret-down',
'caret-left',
'caret-right',
'carry-out',
'caret-up',
'check-circle',
'check-square',
'chrome',
'ci-circle',
'clock-circle',
'close-circle',
'cloud',
'close-square',
'code-sandbox-square',
'code-sandbox-circle',
'code',
'codepen-circle',
'compass',
'codepen-square',
'contacts',
'container',
'control',
'copy',
'copyright-circle',
'credit-card',
'crown',
'customer-service',
'dashboard',
'delete',
'diff',
'dingtalk-circle',
'database',
'dingtalk-square',
'dislike',
'dollar-circle',
'down-circle',
'down-square',
'dribbble-circle',
'dribbble-square',
'dropbox-circle',
'dropbox-square',
'environment',
'edit',
'exclamation-circle',
'euro-circle',
'experiment',
'eye-invisible',
'eye',
'facebook',
'fast-backward',
'fast-forward',
'file-add',
'file-excel',
'file-exclamation',
'file-image',
'file-markdown',
'file-pdf',
'file-ppt',
'file-text',
'file-unknown',
'file-word',
'file-zip',
'file',
'filter',
'fire',
'flag',
'folder-add',
'folder',
'folder-open',
'forward',
'frown',
'fund',
'funnel-plot',
'gift',
'github',
'gitlab',
'golden',
'google-circle',
'google-plus-circle',
'google-plus-square',
'google-square',
'hdd',
'heart',
'highlight',
'home',
'hourglass',
'html5',
'idcard',
'ie-circle',
'ie-square',
'info-circle',
'instagram',
'insurance',
'interaction',
'interation',
'layout',
'left-circle',
'left-square',
'like',
'linkedin',
'lock',
'mail',
'medicine-box',
'medium-circle',
'medium-square',
'meh',
'message',
'minus-circle',
'minus-square',
'mobile',
'money-collect',
'pause-circle',
'pay-circle',
'notification',
'phone',
'picture',
'pie-chart',
'play-circle',
'play-square',
'plus-circle',
'plus-square',
'pound-circle',
'printer',
'profile',
'project',
'pushpin',
'property-safety',
'qq-circle',
'qq-square',
'question-circle',
'read',
'reconciliation',
'red-envelope',
'reddit-circle',
'reddit-square',
'rest',
'right-circle',
'rocket',
'right-square',
'safety-certificate',
'save',
'schedule',
'security-scan',
'setting',
'shop',
'shopping',
'sketch-circle',
'sketch-square',
'skin',
'slack-circle',
'skype',
'slack-square',
'sliders',
'smile',
'snippets',
'sound',
'star',
'step-backward',
'step-forward',
'stop',
'switcher',
'tablet',
'tag',
'tags',
'taobao-circle',
'taobao-square',
'tool',
'thunderbolt',
'trademark-circle',
'twitter-circle',
'trophy',
'twitter-square',
'unlock',
'up-circle',
'up-square',
'usb',
'video-camera',
'wallet',
'warning',
'wechat',
'weibo-circle',
'windows',
'yahoo',
'weibo-square',
'yuque',
'youtube',
'zhihu-circle',
'zhihu-square',
],
outlined: [
'account-book',
'alert',
'alipay-circle',
'aliwangwang',
'android',
'api',
'appstore',
'audio',
'apple',
'backward',
'bank',
'bell',
'behance-square',
'book',
'box-plot',
'bug',
'bulb',
'calculator',
'build',
'calendar',
'camera',
'car',
'caret-down',
'caret-left',
'caret-right',
'carry-out',
'caret-up',
'check-circle',
'check-square',
'chrome',
'clock-circle',
'close-circle',
'cloud',
'close-square',
'code',
'codepen-circle',
'compass',
'contacts',
'container',
'control',
'copy',
'credit-card',
'crown',
'customer-service',
'dashboard',
'delete',
'diff',
'database',
'dislike',
'down-circle',
'down-square',
'dribbble-square',
'environment',
'edit',
'exclamation-circle',
'experiment',
'eye-invisible',
'eye',
'facebook',
'fast-backward',
'fast-forward',
'file-add',
'file-excel',
'file-exclamation',
'file-image',
'file-markdown',
'file-pdf',
'file-ppt',
'file-text',
'file-unknown',
'file-word',
'file-zip',
'file',
'filter',
'fire',
'flag',
'folder-add',
'folder',
'folder-open',
'forward',
'frown',
'fund',
'funnel-plot',
'gift',
'github',
'gitlab',
'hdd',
'heart',
'highlight',
'home',
'hourglass',
'html5',
'idcard',
'info-circle',
'instagram',
'insurance',
'interaction',
'interation',
'layout',
'left-circle',
'left-square',
'like',
'linkedin',
'lock',
'mail',
'medicine-box',
'meh',
'message',
'minus-circle',
'minus-square',
'mobile',
'money-collect',
'pause-circle',
'pay-circle',
'notification',
'phone',
'picture',
'pie-chart',
'play-circle',
'play-square',
'plus-circle',
'plus-square',
'printer',
'profile',
'project',
'pushpin',
'property-safety',
'question-circle',
'read',
'reconciliation',
'red-envelope',
'rest',
'right-circle',
'rocket',
'right-square',
'safety-certificate',
'save',
'schedule',
'security-scan',
'setting',
'shop',
'shopping',
'skin',
'skype',
'slack-square',
'sliders',
'smile',
'snippets',
'sound',
'star',
'step-backward',
'step-forward',
'stop',
'switcher',
'tablet',
'tag',
'tags',
'taobao-circle',
'tool',
'thunderbolt',
'trophy',
'unlock',
'up-circle',
'up-square',
'usb',
'video-camera',
'wallet',
'warning',
'wechat',
'weibo-circle',
'windows',
'yahoo',
'weibo-square',
'yuque',
'youtube',
'alibaba',
'align-center',
'align-left',
'align-right',
'alipay',
'aliyun',
'amazon',
'ant-cloud',
'apartment',
'ant-design',
'area-chart',
'arrow-left',
'arrow-down',
'arrow-up',
'arrows-alt',
'arrow-right',
'audit',
'bar-chart',
'barcode',
'bars',
'behance',
'bg-colors',
'block',
'bold',
'border-bottom',
'border-left',
'border-outer',
'border-inner',
'border-right',
'border-horizontal',
'border-top',
'border-verticle',
'border',
'branches',
'check',
'ci',
'close',
'cloud-download',
'cloud-server',
'cloud-sync',
'cloud-upload',
'cluster',
'codepen',
'code-sandbox',
'colum-height',
'column-width',
'column-height',
'coffee',
'copyright',
'dash',
'deployment-unit',
'desktop',
'dingding',
'disconnect',
'dollar',
'double-left',
'dot-chart',
'double-right',
'down',
'drag',
'download',
'dribbble',
'dropbox',
'ellipsis',
'enter',
'euro',
'exception',
'exclamation',
'export',
'fall',
'file-done',
'file-jpg',
'file-protect',
'file-sync',
'file-search',
'font-colors',
'font-size',
'fork',
'form',
'fullscreen-exit',
'fullscreen',
'gateway',
'global',
'google-plus',
'gold',
'google',
'heat-map',
'history',
'ie',
'import',
'inbox',
'info',
'italic',
'key',
'issues-close',
'laptop',
'left',
'line-chart',
'link',
'line-height',
'line',
'loading-3-quarters',
'loading',
'login',
'logout',
'man',
'medium',
'medium-workmark',
'menu-unfold',
'menu-fold',
'menu',
'minus',
'monitor',
'more',
'ordered-list',
'number',
'pause',
'percentage',
'paper-clip',
'pic-center',
'pic-left',
'pic-right',
'plus',
'pound',
'poweroff',
'pull-request',
'qq',
'question',
'radar-chart',
'qrcode',
'radius-bottomleft',
'radius-bottomright',
'radius-upleft',
'radius-setting',
'radius-upright',
'reddit',
'redo',
'reload',
'retweet',
'right',
'rise',
'rollback',
'safety',
'robot',
'scan',
'search',
'scissor',
'select',
'shake',
'share-alt',
'shopping-cart',
'shrink',
'sketch',
'slack',
'small-dash',
'solution',
'sort-descending',
'sort-ascending',
'stock',
'swap-left',
'swap-right',
'strikethrough',
'swap',
'sync',
'table',
'team',
'taobao',
'to-top',
'trademark',
'transaction',
'twitter',
'underline',
'undo',
'unordered-list',
'up',
'upload',
'user-add',
'user-delete',
'usergroup-add',
'user',
'usergroup-delete',
'vertical-align-bottom',
'vertical-align-middle',
'vertical-align-top',
'vertical-left',
'vertical-right',
'weibo',
'wifi',
'zhihu',
'woman',
'zoom-out',
'zoom-in',
],
twoTone: [
'account-book',
'alert',
'api',
'appstore',
'audio',
'bank',
'bell',
'book',
'box-plot',
'bug',
'bulb',
'calculator',
'build',
'calendar',
'camera',
'car',
'carry-out',
'check-circle',
'check-square',
'clock-circle',
'close-circle',
'cloud',
'close-square',
'code',
'compass',
'contacts',
'container',
'control',
'copy',
'credit-card',
'crown',
'customer-service',
'dashboard',
'delete',
'diff',
'database',
'dislike',
'down-circle',
'down-square',
'environment',
'edit',
'exclamation-circle',
'experiment',
'eye-invisible',
'eye',
'file-add',
'file-excel',
'file-exclamation',
'file-image',
'file-markdown',
'file-pdf',
'file-ppt',
'file-text',
'file-unknown',
'file-word',
'file-zip',
'file',
'filter',
'fire',
'flag',
'folder-add',
'folder',
'folder-open',
'frown',
'fund',
'funnel-plot',
'gift',
'hdd',
'heart',
'highlight',
'home',
'hourglass',
'html5',
'idcard',
'info-circle',
'insurance',
'interaction',
'interation',
'layout',
'left-circle',
'left-square',
'like',
'lock',
'mail',
'medicine-box',
'meh',
'message',
'minus-circle',
'minus-square',
'mobile',
'money-collect',
'pause-circle',
'notification',
'phone',
'picture',
'pie-chart',
'play-circle',
'play-square',
'plus-circle',
'plus-square',
'pound-circle',
'printer',
'profile',
'project',
'pushpin',
'property-safety',
'question-circle',
'reconciliation',
'red-envelope',
'rest',
'right-circle',
'rocket',
'right-square',
'safety-certificate',
'save',
'schedule',
'security-scan',
'setting',
'shop',
'shopping',
'skin',
'sliders',
'smile',
'snippets',
'sound',
'star',
'stop',
'switcher',
'tablet',
'tag',
'tags',
'tool',
'thunderbolt',
'trademark-circle',
'trophy',
'unlock',
'up-circle',
'up-square',
'usb',
'video-camera',
'wallet',
'warning',
'ci',
'copyright',
'dollar',
'euro',
'gold',
'canlendar',
],
};
export default iconConfig;

18
apps/vue/src/components/FormDesign/src/hooks/useFormDesignState.ts

@ -0,0 +1,18 @@
import { inject, Ref } from 'vue';
import { IFormDesignMethods } from '../typings/form-type';
import { IFormConfig } from '../typings/v-form-component';
/**
* formDesign状态
*/
export function useFormDesignState() {
const formConfig = inject('formConfig') as Ref<IFormConfig>;
const formDesignMethods = inject('formDesignMethods') as IFormDesignMethods;
return { formConfig, formDesignMethods };
}
export function useFormModelState() {
const formModel = inject('formModel') as Ref<{}>;
const setFormModel = inject('setFormModelMethod') as (key: String, value: any) => void;
return { formModel, setFormModel };
}

62
apps/vue/src/components/FormDesign/src/hooks/useFormInstanceMethods.ts

@ -0,0 +1,62 @@
import { IAnyObject } from '../typings/base-type';
import { Ref, SetupContext } from 'vue';
import { cloneDeep, forOwn, isFunction } from 'lodash-es';
import { AForm, IVFormComponent } from '../typings/v-form-component';
import { getCurrentInstance } from 'vue';
import { Form } from 'ant-design-vue';
import { toRaw } from 'vue';
export function useFormInstanceMethods(
props: IAnyObject,
formdata,
context: Partial<SetupContext>,
_formInstance: Ref<AForm | null>,
) {
/**
* props和on中的上下文为parent
*/
const bindContext = () => {
const instance = getCurrentInstance();
const vm = instance?.parent;
if (!vm) return;
(props.formConfig.schemas as IVFormComponent[]).forEach((item) => {
// 绑定 props 中的上下文
forOwn(item.componentProps, (value: any, key) => {
if (isFunction(value)) {
item.componentProps![key] = value.bind(vm);
}
});
// 绑定事件监听(v-on)的上下文
forOwn(item.on, (value: any, key) => {
if (isFunction(value)) {
item.componentProps![key] = value.bind(vm);
}
});
});
};
bindContext();
const { emit } = context;
const useForm = Form.useForm;
const { resetFields, validate, clearValidate, validateField } = useForm(formdata, []);
const submit = async () => {
//const _result = await validate();
const data = cloneDeep(toRaw(formdata.value));
emit?.('submit', data);
props.formConfig.submit?.(data);
return data;
};
return {
validate,
validateField,
resetFields,
clearValidate,
submit,
};
}

195
apps/vue/src/components/FormDesign/src/hooks/useVFormMethods.ts

@ -0,0 +1,195 @@
import { Ref, SetupContext } from 'vue';
import { IVFormComponent, IFormConfig, AForm } from '../typings/v-form-component';
import { findFormItem, formItemsForEach } from '../utils';
import { cloneDeep, isFunction } from 'lodash-es';
import { IAnyObject } from '../typings/base-type';
interface IFormInstanceMethods extends AForm {
submit: () => Promise<any>;
}
export interface IProps {
formConfig: IFormConfig;
formModel: IAnyObject;
}
type ISet = <T extends keyof IVFormComponent>(
field: string,
key: T,
value: IVFormComponent[T],
) => void;
// 获取当前field绑定的表单项
type IGet = (field: string) => IVFormComponent | undefined;
// 获取field在formData中的值
type IGetValue = (field: string) => any;
// 设置field在formData中的值并且触发校验
type ISetValue = (field: string | IAnyObject, value?: any) => void;
// 隐藏field对应的表单项
type IHidden = (field: string) => void;
// 显示field对应的表单项
type IShow = (field: string) => void;
// 设置field对应的表单项绑定的props属性
type ISetProps = (field: string, key: string, value: any) => void;
// 获取formData中的值
type IGetData = () => Promise<IAnyObject>;
// 禁用表单,如果field为空,则禁用整个表单
type IDisable = (field?: string | boolean) => void;
// 设置表单配置方法
type ISetFormConfig = (key: string, value: any) => void;
interface ILinkOn {
[key: string]: Set<IVFormComponent>;
}
export interface IVFormMethods extends Partial<IFormInstanceMethods> {
set: ISet;
get: IGet;
getValue: IGetValue;
setValue: ISetValue;
hidden: IHidden;
show: IShow;
setProps: ISetProps;
linkOn: ILinkOn;
getData: IGetData;
disable: IDisable;
}
export function useVFormMethods(
props: IProps,
_context: Partial<SetupContext>,
formInstance: Ref<AForm | null>,
formInstanceMethods: Partial<IFormInstanceMethods>,
): IVFormMethods {
/**
* field获取表单项
* @param {string} field
* @return {IVFormComponent | undefined}
*/
const get: IGet = (field) =>
findFormItem(props.formConfig.schemas, (item) => item.field === field);
/**
* field设置表单项字段值
* @param {string} field
* @param {keyof IVFormComponent} key
* @param {never} value
*/
const set: ISet = (field, key, value) => {
const formItem = get(field);
if (formItem) formItem[key] = value;
};
/**
* props
* @param {string} field field
* @param {string} key key
* @param value
*/
const setProps: ISetProps = (field, key, value) => {
const formItem = get(field);
if (formItem?.componentProps) {
['options', 'treeData'].includes(key) && setValue(field, undefined);
formItem.componentProps[key] = value;
}
};
/**
*
* @param {string} field
* @param value
*/
const setValue: ISetValue = (field, value) => {
if (typeof field === 'string') {
// props.formData[field] = value
props.formModel[field] = value;
formInstance.value?.validateField(field, value, []);
} else {
const keys = Object.keys(field);
keys.forEach((key) => {
props.formModel[key] = field[key];
formInstance.value?.validateField(key, field[key], []);
});
}
};
/**
*
* @param {string} key
* @param value
*/
const setFormConfig: ISetFormConfig = (key, value) => {
props.formConfig[key] = value;
};
/**
* field获取字段值field为空
* @param {string} field
*/
const getValue: IGetValue = (field) => {
const formData = cloneDeep(props.formModel);
return formData[field];
};
/**
* formData中的值
* @return {Promise<IAnyObject<any>>}
*/
const getData: IGetData = async () => {
return cloneDeep(props.formModel);
};
/**
*
* @param {string} field field
*/
const hidden: IHidden = (field) => {
set(field, 'hidden', true);
};
/**
*
* @param {string | undefined} field
*/
const disable: IDisable = (field) => {
typeof field === 'string'
? setProps(field, 'disabled', true)
: setFormConfig('disabled', field !== false);
};
/**
*
* @param {string} field field
*/
const show: IShow = (field) => {
set(field, 'hidden', false);
};
/**
*
* @type {ILinkOn}
*/
const linkOn: ILinkOn = {};
const initLink = (schemas: IVFormComponent[]) => {
// 首次遍历,查找需要关联字段的表单
formItemsForEach(schemas, (formItem) => {
// 如果需要关联,则进行第二层遍历,查找表单中关联的字段,存到Set中
formItemsForEach(schemas, (item) => {
if (!linkOn[item.field!]) linkOn[item.field!] = new Set<IVFormComponent>();
if (formItem.link?.includes(item.field!) && isFunction(formItem.update)) {
linkOn[item.field!].add(formItem);
}
});
linkOn[formItem.field!].add(formItem);
});
};
initLink(props.formConfig.schemas);
return {
linkOn,
setValue,
getValue,
hidden,
show,
set,
get,
setProps,
getData,
disable,
...formInstanceMethods,
};
}

10
apps/vue/src/components/FormDesign/src/typings/base-type.ts

@ -0,0 +1,10 @@
export interface IAnyObject<T = any> {
[key: string]: T;
}
export interface IInputEvent {
target: {
value: any;
checked: boolean;
};
}

52
apps/vue/src/components/FormDesign/src/typings/form-type.ts

@ -0,0 +1,52 @@
import { Ref } from 'vue';
import { IAnyObject } from './base-type';
import { IFormConfig, IVFormComponent } from './v-form-component';
export interface IToolbarMethods {
showModal: (jsonData: IAnyObject) => void;
}
type ChangeTabKey = 1 | 2;
export interface IPropsPanel {
changeTab: (key: ChangeTabKey) => void;
}
export interface IState {
// 语言
locale: any;
// 公用组件
baseComponents: IVFormComponent[];
// 自定义组件
customComponents: IVFormComponent[];
// 布局组件
layoutComponents: IVFormComponent[];
// 属性面板实例
propsPanel: Ref<null | IPropsPanel>;
// json模态框实例
jsonModal: Ref<null | IToolbarMethods>;
// 导入json数据模态框
importJsonModal: Ref<null | IToolbarMethods>;
// 代码预览模态框
codeModal: Ref<null | IToolbarMethods>;
// 预览模态框
eFormPreview: Ref<null | IToolbarMethods>;
eFormPreview2: Ref<null | IToolbarMethods>;
}
export interface IFormDesignMethods {
// 设置当前选中的控件
handleSetSelectItem(item: IVFormComponent): void;
// 添加控件到formConfig.formItems中
handleListPush(item: IVFormComponent): void;
// 复制控件
handleCopy(item?: IVFormComponent, isCopy?: boolean): void;
// 添加控件属性
handleAddAttrs(schemas: IVFormComponent[], index: number): void;
setFormConfig(config: IFormConfig): void;
// 添加到表单中之前触发
handleBeforeColAdd(
event: { newIndex: string },
schemas: IVFormComponent[],
isCopy?: boolean,
): void;
}

349
apps/vue/src/components/FormDesign/src/typings/v-form-component.ts

@ -0,0 +1,349 @@
import { IAnyObject } from './base-type';
// import { ComponentOptions } from 'vue/types/options';
import { ComponentOptions } from 'vue';
import { IVFormMethods } from '../hooks/useVFormMethods';
import { ColEx } from '/@/components/Form/src/types';
import { SelectValue } from 'ant-design-vue/lib/select';
import { validateOptions } from 'ant-design-vue/lib/form/useForm';
import { RuleError } from 'ant-design-vue/lib/form/interface';
import { FormItem } from '/@/components/Form';
type LayoutType = 'horizontal' | 'vertical' | 'inline';
type labelLayout = 'flex' | 'Grid';
export type PropsTabKey = 1 | 2 | 3;
type ColSpanType = number | string;
declare type Value = [number, number] | number;
/**
*
*/
export interface IVFormComponent {
// extends Omit<FormSchema, 'component' | 'label' | 'field' | 'rules'> {
// 对应的字段
field?: string;
// 组件类型
component: string;
// 组件label
label?: string;
// 自定义组件控件实例
componentInstance?: ComponentOptions<any>;
// 组件icon
icon?: string;
// 组件校验规则
rules?: Partial<IValidationRule>[];
// 是否隐藏
hidden?: boolean;
// 隐藏label
hiddenLabel?: boolean;
// 组件宽度
width?: string;
// 是否必选
required?: boolean;
// 必选提示
message?: string;
// 提示信息
helpMessage?: string;
// 传给给组件的属性,默认会吧所有的props都传递给控件
componentProps?: IAnyObject;
// 监听组件事件对象,以v-on方式传递给控件
on?: IAnyObject<(...any: []) => void>;
// 组件选项
options?: IAnyObject;
// 唯一标识
key?: string;
// Reference formModelItem
itemProps?: Partial<FormItem>;
colProps?: Partial<ColEx>;
// 联动字段
link?: string[];
// 联动属性变化的回调
update?: (value: any, formItem: IVFormComponent, fApi: IVFormMethods) => void;
// 控件栅格数
// span?: number;
// 标签布局
labelCol?: IAnyObject;
// 组件布局
wrapperCol?: IAnyObject;
// 子控件
columns?: Array<{ span: number; children: any[] }>;
}
declare type namesType = string | string[];
/**
*
*/
export interface IFormConfig {
// 表单项配置列表
// schemas: IVFormComponent[];
// 表单配置
// config: {
layout?: LayoutType;
labelLayout?: labelLayout;
labelWidth?: number;
labelCol?: Partial<IACol>;
wrapperCol?: Partial<IACol>;
hideRequiredMark?: boolean;
// Whether to disable
schemas: IVFormComponent[];
disabled?: boolean;
labelAlign?: 'left' | 'right';
// Internal component size of the form
size?: 'default' | 'small' | 'large';
// };
// 当前选中项
currentItem?: IVFormComponent;
activeKey?: PropsTabKey;
colon?: boolean;
}
export interface AForm {
/**
* Hide required mark of all form items
* @default false
* @type boolean
*/
hideRequiredMark: boolean;
/**
* The layout of label. You can set span offset to something like {span: 3, offset: 12} or sm: {span: 3, offset: 12} same as with <Col>
* @type IACol
*/
labelCol: IACol;
/**
* Define form layout
* @default 'horizontal'
* @type string
*/
layout: 'horizontal' | 'inline' | 'vertical';
/**
* The layout for input controls, same as labelCol
* @type IACol
*/
wrapperCol: IACol;
/**
* change default props colon value of Form.Item (only effective when prop layout is horizontal)
* @type boolean
* @default true
*/
colon: boolean;
/**
* text align of label of all items
* @type 'left' | 'right'
* @default 'left'
*/
labelAlign: 'left' | 'right';
/**
* data of form component
* @type object
*/
model: IAnyObject;
/**
* validation rules of form
* @type object
*/
rules: IAnyObject;
/**
* Default validate message. And its format is similar with newMessages's returned value
* @type any
*/
validateMessages?: any;
/**
* whether to trigger validation when the rules prop is changed
* @type Boolean
* @default true
*/
validateOnRuleChange: boolean;
/**
* validate the whole form. Takes a callback as a param. After validation,
* the callback will be executed with two params: a boolean indicating if the validation has passed,
* and an object containing all fields that fail the validation. Returns a promise if callback is omitted
* @type Function
*/
validate: <T = any>(names?: namesType, option?: validateOptions) => Promise<T>;
/**
* validate one or several form items
* @type Function
*/
validateField: (
name: string,
value: any,
rules: Record<string, unknown>[],
option?: validateOptions,
) => Promise<RuleError[]>;
/**
* reset all the fields and remove validation result
*/
resetFields: () => void;
/**
* clear validation message for certain fields.
* The parameter is prop name or an array of prop names of the form items whose validation messages will be removed.
* When omitted, all fields' validation messages will be cleared
* @type string[] | string
*/
clearValidate: (props: string[] | string) => void;
}
interface IACol {
/**
* raster number of cells to occupy, 0 corresponds to display: none
* @default none (0)
* @type ColSpanType
*/
span: Value;
/**
* raster order, used in flex layout mode
* @default 0
* @type ColSpanType
*/
order: ColSpanType;
/**
* the layout fill of flex
* @default none
* @type ColSpanType
*/
flex: ColSpanType;
/**
* the number of cells to offset Col from the left
* @default 0
* @type ColSpanType
*/
offset: ColSpanType;
/**
* the number of cells that raster is moved to the right
* @default 0
* @type ColSpanType
*/
push: ColSpanType;
/**
* the number of cells that raster is moved to the left
* @default 0
* @type ColSpanType
*/
pull: ColSpanType;
/**
* <576px and also default setting, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xs: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* 576px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
sm: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* 768px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
md: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* 992px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
lg: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* 1200px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xl: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
/**
* 1600px, could be a span value or an object containing above props
* @type { span: ColSpanType, offset: ColSpanType } | ColSpanType
*/
xxl: { span: ColSpanType; offset: ColSpanType } | ColSpanType;
}
export interface IValidationRule {
trigger?: 'change' | 'blur' | ['change', 'blur'];
/**
* validation error message
* @type string | Function
*/
message?: string | number;
/**
* built-in validation type, available options: https://github.com/yiminghe/async-validator#type
* @default 'string'
* @type string
*/
type?: string;
/**
* indicates whether field is required
* @default false
* @type boolean
*/
required?: boolean;
/**
* treat required fields that only contain whitespace as errors
* @default false
* @type boolean
*/
whitespace?: boolean;
/**
* validate the exact length of a field
* @type number
*/
len?: number;
/**
* validate the min length of a field
* @type number
*/
min?: number;
/**
* validate the max length of a field
* @type number
*/
max?: number;
/**
* validate the value from a list of possible values
* @type string | string[]
*/
enum?: string | string[];
/**
* validate from a regular expression
* @type boolean
*/
pattern?: SelectValue;
/**
* transform a value before validation
* @type Function
*/
transform?: (value: any) => any;
/**
* custom validate function (Note: callback must be called)
* @type Function
*/
validator?: (rule: any, value: any, callback: () => void) => any;
}

200
apps/vue/src/components/FormDesign/src/utils/index.ts

@ -0,0 +1,200 @@
// import { VueConstructor } from 'vue';
import { IVFormComponent, IFormConfig, IValidationRule } from '../typings/v-form-component';
import { cloneDeep, isArray, isFunction, isNumber, uniqueId } from 'lodash-es';
// import { del } from '@vue/composition-api';
// import { withInstall } from '/@/utils';
/**
* install方法
* @param comp install方法的组件
*/
// export function withInstall<T extends { name: string }>(comp: T) {
// return Object.assign(comp, {
// install(Vue: VueConstructor) {
// Vue.component(comp.name, comp);
// },
// });
// }
/**
* key
* @param [formItem] key key
* @returns {string|boolean} id false
*/
export function generateKey(formItem?: IVFormComponent): string | boolean {
if (formItem && formItem.component) {
const key = uniqueId(`${toLine(formItem.component)}_`);
formItem.key = key;
formItem.field = key;
return true;
}
return uniqueId('key_');
}
/**
* value可以是一个数字下标true的元素
* @param array {Array<T>}
* @param value {number | ((item: T, index: number, array: Array<T>) => boolean}
* @returns {T}
*/
export function remove<T>(
array: Array<T>,
value: number | ((item: T, index: number, array: Array<T>) => boolean),
): T | undefined {
let removeVal: Array<T | undefined> = [];
if (!isArray(array)) return undefined;
if (isNumber(value)) {
removeVal = array.splice(value, 1);
} else {
const index = array.findIndex(value);
if (index !== -1) {
removeVal = array.splice(index, 1);
}
}
return removeVal.shift();
}
/**
*
* @param value
*/
export function getType(value: any): string {
return Object.prototype.toString.call(value).slice(8, -1);
}
/**
* guid
* @returns {String} id标识符
*/
export function randomUUID(): string {
function S4() {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}
return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4() + S4() + S4()}`;
}
/**
* 线
* @param str
*/
export function toLine(str: string) {
return str.replace(/([A-Z])/g, '_$1').toLowerCase();
}
/**
*
* @param array
* @param cb
*/
export function formItemsForEach(array: IVFormComponent[], cb: (item: IVFormComponent) => void) {
if (!isArray(array)) return;
const traverse = (schemas: IVFormComponent[]) => {
schemas.forEach((formItem: IVFormComponent) => {
if (['Grid'].includes(formItem.component)) {
// 栅格布局
formItem.columns?.forEach((item) => traverse(item.children));
} else {
cb(formItem);
}
});
};
traverse(array);
}
/**
*
*/
export const findFormItem: (
schemas: IVFormComponent[],
cb: (formItem: IVFormComponent) => boolean,
) => IVFormComponent | undefined = (schemas, cb) => {
let res;
const traverse = (schemas: IVFormComponent[]): boolean => {
return schemas.some((formItem: IVFormComponent) => {
const { component: type } = formItem;
// 处理栅格
if (['Grid'].includes(type)) {
return formItem.columns?.some((item) => traverse(item.children));
}
if (cb(formItem)) res = formItem;
return cb(formItem);
});
};
traverse(schemas);
return res;
};
/**
* json模态框时删除当前项属性
* @param formConfig {IFormConfig}
* @returns {IFormConfig}
*/
export const removeAttrs = (formConfig: IFormConfig): IFormConfig => {
const copyFormConfig = cloneDeep(formConfig);
delete copyFormConfig.currentItem;
delete copyFormConfig.activeKey;
copyFormConfig.schemas &&
formItemsForEach(copyFormConfig.schemas, (item) => {
delete item.icon;
delete item.key;
});
return copyFormConfig;
};
/**
* select treeSelect Promise对象
* @param {(() => Promise<any[]>) | any[]} options
* @return {Promise<any[]>}
*/
export const handleAsyncOptions = async (
options: (() => Promise<any[]>) | any[],
): Promise<any[]> => {
try {
if (isFunction(options)) return await options();
return options;
} catch {
return [];
}
};
/**
*
* @param {IVFormComponent[]} schemas
*/
export const formatRules = (schemas: IVFormComponent[]) => {
formItemsForEach(schemas, (item) => {
if ('required' in item) {
!isArray(item.rules) && (item.rules = []);
item.rules.push({ required: true, message: item.message });
delete item['required'];
delete item['message'];
}
});
};
/**
*
* @param {IValidationRule[]} rules
* @return {IValidationRule[]}
*/
export const strToReg = (rules: IValidationRule[]) => {
const newRules = cloneDeep(rules);
return newRules.map((item) => {
if (item.pattern) item.pattern = runCode(item.pattern);
return item;
});
};
/**
*
* @param code
* @return {any}
*/
export const runCode = <T>(code: any): T => {
try {
return new Function(`return ${code}`)();
} catch {
return code;
}
};

18
apps/vue/src/components/FormDesign/src/utils/message.ts

@ -0,0 +1,18 @@
import { useMessage } from '/@/hooks/web/useMessage';
const { createMessage } = useMessage();
const message = Object.assign({
success: (msg: string) => {
createMessage.success(msg);
},
error: (msg: string) => {
createMessage.error(msg);
},
warning: (msg: string) => {
createMessage.warning(msg);
},
info: (msg: string) => {
createMessage.info(msg);
},
});
export default message;

2
apps/vue/src/components/Table/src/components/settings/ColumnSetting.vue

@ -145,6 +145,7 @@
const emits = defineEmits(['columns-change']);
const { t } = useI18n();
const attrs = useAttrs();
const table = useTableContext();
const defaultRowSelection = omit(table.getRowSelection(), 'selectedRowKeys');
@ -379,7 +380,6 @@
}
function getPopupContainer() {
const attrs = useAttrs();
return isFunction(attrs.getPopupContainer)
? attrs.getPopupContainer()
: getParentContainer();

2
apps/vue/src/enums/imEnum.ts

@ -26,4 +26,6 @@ export enum NotifyEventEnum {
NOTIFICATIONS_RECEVIED = 'notifications:recevied',
/** 通知已读 */
NOTIFICATIONS_READ = 'notifications:read',
/** 服务端回调 */
NOTIFICATIONS_SERVICE_CALLBACK = 'notifications:service_callback',
}

2
apps/vue/src/layouts/default/header/components/notify/useMessages.ts

@ -69,7 +69,7 @@ export function useMessages() {
function onMessageReceived(message: ChatMessage) {
// 处理需要本地化的系统消息
if (message.source === MessageSourceTye.System && message.extraProperties.L === true) {
if (message.source === MessageSourceTye.System && (message.extraProperties.L === true || message.extraProperties.L === 'true')) {
message.content = t(
message.extraProperties.content.ResourceName + '.' + message.extraProperties.content.Name,
message.extraProperties.content.Values as Recordable,

68
apps/vue/src/layouts/default/header/components/notify/useNotifications.ts

@ -2,12 +2,13 @@ import { onMounted, onUnmounted, ref } from 'vue';
import { notification } from 'ant-design-vue';
import { getList } from '/@/api/messages/notifications';
import {
NotificationType,
NotificationInfo,
NotificationSeverity,
NotificationReadState,
} from '/@/api/messages/model/notificationsModel';
import { formatToDateTime } from '/@/utils/dateUtil';
import { TabItem, ListItem } from './data';
import { TabItem, ListItem as Notification } from './data';
import { formatPagedRequest } from '/@/utils/http/abp/helper';
import { NotifyEventEnum } from '/@/enums/imEnum';
import { useSignalR } from '/@/hooks/web/useSignalR';
@ -49,7 +50,7 @@ export function useNotifications() {
if (!data.extraProperties) {
return;
}
if (data.extraProperties.L === true) {
if (data.extraProperties.L === true || data.extraProperties.L === 'true') {
// TODO: 后端统一序列化格式
const { L } = useLocalization(
[data.extraProperties.title.resourceName ?? data.extraProperties.title.ResourceName,
@ -70,7 +71,7 @@ export function useNotifications() {
);
}
}
const notifier: ListItem = {
const notifier: Notification = {
id: notificationInfo.id,
avatar: data.extraProperties.avatar,
title: title,
@ -79,51 +80,56 @@ export function useNotifications() {
datetime: formatToDateTime(notificationInfo.creationTime, 'YYYY-MM-DD HH:mm:ss'),
type: String(notificationInfo.type),
};
switch (notificationInfo.severity) {
if (notifer && notificationInfo.type !== NotificationType.ServiceCallback) {
_notification(notifier, notificationInfo.severity);
}
if (notificationInfo.type === NotificationType.ServiceCallback) {
emitter.emit(NotifyEventEnum.NOTIFICATIONS_SERVICE_CALLBACK, notificationInfo);
} else {
emitter.emit(NotifyEventEnum.NOTIFICATIONS_RECEVIED, notificationInfo);
notifierRef.value.list.push(notifier);
}
}
function _notification(notifier: Notification, severity: NotificationSeverity) {
switch (severity) {
case NotificationSeverity.Error:
case NotificationSeverity.Fatal:
notifier.color = 'red';
notifier.avatar = errorAvatar;
if (notifer) {
notification['error']({
message: notifier.title,
description: notifier.description,
});
}
notification['error']({
message: notifier.title,
description: notifier.description,
});
break;
case NotificationSeverity.Warn:
notifier.color = 'gold';
notifier.avatar = warningAvatar;
if (notifer) {
notification['warning']({
message: notifier.title,
description: notifier.description,
});
}
notification['warning']({
message: notifier.title,
description: notifier.description,
});
break;
case NotificationSeverity.Info:
notifier.color = 'gold';
notifier.avatar = infoAvatar;
if (notifer) {
notification['info']({
message: notifier.title,
description: notifier.description,
});
}
notification['info']({
message: notifier.title,
description: notifier.description,
});
break;
case NotificationSeverity.Success:
notifier.color = 'green';
notifier.avatar = successAvatar;
if (notifer) {
notification['success']({
message: notifier.title,
description: notifier.description,
});
}
notification['success']({
message: notifier.title,
description: notifier.description,
});
break;
}
emitter.emit(NotifyEventEnum.NOTIFICATIONS_RECEVIED, notificationInfo);
notifierRef.value.list.push(notifier);
}
function refreshNotifer(page = 1, pageSize = 10) {
@ -145,7 +151,7 @@ export function useNotifications() {
});
}
function readNotifer(notifier: ListItem) {
function readNotifer(notifier: Notification) {
signalR.invoke('change-state', notifier.id, NotificationReadState.Read).then(() => {
notifierRef.value.list = [];
refreshNotifer();

4
apps/vue/src/layouts/page/index.vue

@ -15,9 +15,7 @@
appear
>
<keep-alive v-if="openCache" :include="getCaches">
<div :key="route.name!">
<component :is="Component" :key="route.fullPath" />
</div>
<component :is="Component" :key="route.fullPath" />
</keep-alive>
<div v-else :key="route.name!">
<component :is="Component" :key="route.fullPath" />

3
apps/vue/src/utils/helper/treeHelper.ts

@ -25,6 +25,9 @@ export function listToTree<T = any>(list: any[], config: Partial<TreeHelperConfi
for (const node of list) {
const parent = nodeMap.get(node[pid]);
(parent ? parent[children] : result).push(node);
if (parent) {
parent.hasChildren = true;
}
}
return result;
}

2
apps/vue/types/abp.d.ts

@ -3,7 +3,7 @@ declare interface LocalizableStringInfo {
name: string;
}
declare type ExtraPropertyDictionary = Dictionary<string, any>;
declare type ExtraPropertyDictionary = { [key: string]: any };
declare interface IHasConcurrencyStamp {
concurrencyStamp: string;

14
aspnet-core/modules/common/LINGYUN.Abp.Notifications.Core/LINGYUN/Abp/Notifications/NotificationData.cs

@ -1,5 +1,6 @@
using LINGYUN.Abp.RealTime.Localization;
using System;
using System.Globalization;
using Volo.Abp.Data;
using Volo.Abp.EventBus;
@ -20,6 +21,10 @@ namespace LINGYUN.Abp.Notifications
/// 用来标识是否需要本地化的信息
/// </summary>
public const string LocalizerKey = "L";
/// <summary>
/// 用于本地化文化
/// </summary>
public const string CultureKey = "C";
public virtual string Type => GetType().FullName;
@ -58,13 +63,15 @@ namespace LINGYUN.Abp.Notifications
LocalizableStringInfo message,
DateTime createTime,
string formUser,
LocalizableStringInfo description = null)
LocalizableStringInfo description = null,
string culture = null)
{
TrySetData("title", title);
TrySetData("message", message);
TrySetData("formUser", formUser);
TrySetData("createTime", createTime);
TrySetData(LocalizerKey, true);
TrySetData(CultureKey, culture ?? "en");
if (description != null)
{
TrySetData("description", description);
@ -155,9 +162,10 @@ namespace LINGYUN.Abp.Notifications
public bool NeedLocalizer()
{
var localizer = TryGetData(LocalizerKey);
if (localizer != null && localizer is bool needLocalizer)
if (localizer != null &&
bool.TryParse(localizer.ToString(), out var isLocalizer))
{
return needLocalizer;
return isLocalizer;
}
return false;
}

6
aspnet-core/modules/common/LINGYUN.Abp.Notifications.Core/LINGYUN/Abp/Notifications/NotificationType.cs

@ -16,6 +16,10 @@
/// <summary>
/// 用户(对应用户,受租户控制)
/// </summary>
User = 20
User = 20,
/// <summary>
/// 服务端回调,用户不应进行处理
/// </summary>
ServiceCallback = 30,
}
}

41
aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/DefaultNotificationDataSerializer.cs

@ -0,0 +1,41 @@
using LINGYUN.Abp.RealTime.Localization;
using Newtonsoft.Json;
using Volo.Abp.DependencyInjection;
namespace LINGYUN.Abp.Notifications;
[Dependency(TryRegister = true)]
public class DefaultNotificationDataSerializer : INotificationDataSerializer, ISingletonDependency
{
public NotificationData Serialize(NotificationData source)
{
if (source != null)
{
if (source.NeedLocalizer())
{
// 潜在的空对象引用修复
if (source.ExtraProperties.TryGetValue("title", out var title) && title != null)
{
var titleObj = JsonConvert.DeserializeObject<LocalizableStringInfo>(title.ToString());
source.TrySetData("title", titleObj);
}
if (source.ExtraProperties.TryGetValue("message", out var message) && message != null)
{
var messageObj = JsonConvert.DeserializeObject<LocalizableStringInfo>(message.ToString());
source.TrySetData("message", messageObj);
}
if (source.ExtraProperties.TryGetValue("description", out var description) && description != null)
{
var descriptionObj = JsonConvert.DeserializeObject<LocalizableStringInfo>(description.ToString());
source.TrySetData("description", descriptionObj);
}
}
}
else
{
source = new NotificationData();
}
return source;
}
}

5
aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/INotificationDataSerializer.cs

@ -0,0 +1,5 @@
namespace LINGYUN.Abp.Notifications;
public interface INotificationDataSerializer
{
NotificationData Serialize(NotificationData source);
}

39
aspnet-core/modules/common/LINGYUN.Abp.Notifications/LINGYUN/Abp/Notifications/NotificationDataConverter.cs

@ -1,39 +0,0 @@
using LINGYUN.Abp.RealTime.Localization;
using Newtonsoft.Json;
namespace LINGYUN.Abp.Notifications
{
public class NotificationDataConverter
{
public static NotificationData Convert(NotificationData notificationData)
{
if (notificationData != null)
{
if (notificationData.NeedLocalizer())
{
// 潜在的空对象引用修复
if (notificationData.ExtraProperties.TryGetValue("title", out object title) && title != null)
{
var titleObj = JsonConvert.DeserializeObject<LocalizableStringInfo>(title.ToString());
notificationData.TrySetData("title", titleObj);
}
if (notificationData.ExtraProperties.TryGetValue("message", out object message) && message != null)
{
var messageObj = JsonConvert.DeserializeObject<LocalizableStringInfo>(message.ToString());
notificationData.TrySetData("message", messageObj);
}
if (notificationData.ExtraProperties.TryGetValue("description", out object description) && description != null)
{
notificationData.TrySetData("description", JsonConvert.DeserializeObject<LocalizableStringInfo>(description.ToString()));
}
}
}
else
{
notificationData = new NotificationData();
}
return notificationData;
}
}
}

23
aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks.Core/LINGYUN/Abp/Webhooks/AbpWebhooksOptions.cs

@ -28,6 +28,10 @@ public class AbpWebhooksOptions
public HashSet<string> DeletedWebhooks { get; }
public HashSet<string> DeletedWebhookGroups { get; }
/// <summary>
/// 默认请求头
/// </summary>
public IDictionary<string, string> DefaultHttpHeaders { get; }
public AbpWebhooksOptions()
{
@ -39,5 +43,24 @@ public class AbpWebhooksOptions
DeletedWebhooks = new HashSet<string>();
DeletedWebhookGroups = new HashSet<string>();
DefaultHttpHeaders = new Dictionary<string, string>
{
// 取消响应内容包装
{ "_AbpDontWrapResult", "true" },
// TODO: 可能跨域影响
// { "X-Requested-With", "XMLHttpRequest" },
// 标识来源
{ "X-Requested-From", "abp-webhooks" },
};
}
public void AddHeader(string key, string value)
{
if (value.IsNullOrWhiteSpace())
{
return;
}
DefaultHttpHeaders[key] = value;
}
}

21
aspnet-core/modules/webhooks/LINGYUN.Abp.Webhooks/LINGYUN/Abp/Webhooks/DefaultWebhookSender.cs

@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Linq;
@ -15,6 +16,7 @@ namespace LINGYUN.Abp.Webhooks
{
public ILogger<DefaultWebhookSender> Logger { protected get; set; }
private readonly AbpWebhooksOptions _options;
private readonly IWebhookManager _webhookManager;
private readonly IHttpClientFactory _httpClientFactory;
@ -22,8 +24,10 @@ namespace LINGYUN.Abp.Webhooks
public DefaultWebhookSender(
IWebhookManager webhookManager,
IHttpClientFactory httpClientFactory)
IHttpClientFactory httpClientFactory,
IOptions<AbpWebhooksOptions> options)
{
_options = options.Value;
_webhookManager = webhookManager;
_httpClientFactory = httpClientFactory;
@ -110,6 +114,19 @@ namespace LINGYUN.Abp.Webhooks
protected virtual void AddAdditionalHeaders(HttpRequestMessage request, WebhookSenderArgs webhookSenderArgs)
{
foreach (var header in _options.DefaultHttpHeaders)
{
if (request.Headers.TryAddWithoutValidation(header.Key, header.Value))
{
continue;
}
if (request.Content.Headers.TryAddWithoutValidation(header.Key, header.Value))
{
continue;
}
}
foreach (var header in webhookSenderArgs.Headers)
{
if (request.Headers.TryAddWithoutValidation(header.Key, header.Value))
@ -144,7 +161,7 @@ namespace LINGYUN.Abp.Webhooks
res.Add(header.Key, header.Value.JoinAsString(";"));
}
}
return res;
}
}

10
aspnet-core/services/LINGYUN.Abp.Applications/AbpApplicationsModule.cs

@ -0,0 +1,10 @@
using Volo.Abp.Autofac;
using Volo.Abp.Modularity;
namespace LINGYUN.Abp.Applications;
[DependsOn(
typeof(AbpAutofacModule))]
public class AbpApplicationsModule : AbpModule
{
}

14
aspnet-core/services/LINGYUN.Abp.Applications/App.xaml

@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:LINGYUN.Abp.Applications"
x:Class="LINGYUN.Abp.Applications.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

11
aspnet-core/services/LINGYUN.Abp.Applications/App.xaml.cs

@ -0,0 +1,11 @@
namespace LINGYUN.Abp.Applications;
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}

14
aspnet-core/services/LINGYUN.Abp.Applications/AppShell.xaml

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="LINGYUN.Abp.Applications.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:LINGYUN.Abp.Applications"
Shell.FlyoutBehavior="Disabled">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>

9
aspnet-core/services/LINGYUN.Abp.Applications/AppShell.xaml.cs

@ -0,0 +1,9 @@
namespace LINGYUN.Abp.Applications;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}

64
aspnet-core/services/LINGYUN.Abp.Applications/LINGYUN.Abp.Applications.csproj

@ -0,0 +1,64 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0-android;net7.0-ios;net7.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net7.0-windows10.0.19041.0</TargetFrameworks>
<!-- Uncomment to also build the tizen app. You will need to install tizen by following this: https://github.com/Samsung/Tizen.NET -->
<!-- <TargetFrameworks>$(TargetFrameworks);net7.0-tizen</TargetFrameworks> -->
<OutputType>Exe</OutputType>
<RootNamespace>LINGYUN.Abp.Applications</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Display name -->
<ApplicationTitle>LINGYUN.Abp.Applications</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.lingyun.abp.applications</ApplicationId>
<ApplicationIdGuid>29c575ea-3256-46cb-b070-8ce749673efa</ApplicationIdGuid>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">11.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">13.1</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<None Remove="appsettings.json" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="appsettings.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Volo.Abp.Autofac" Version="$(VoloAbpPackageVersion)" />
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="7.0.0" />
</ItemGroup>
</Project>

41
aspnet-core/services/LINGYUN.Abp.Applications/MainPage.xaml

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="LINGYUN.Abp.Applications.MainPage">
<ScrollView>
<VerticalStackLayout
Spacing="25"
Padding="30,0"
VerticalOptions="Center">
<Image
Source="dotnet_bot.png"
SemanticProperties.Description="Cute dot net bot waving hi to you!"
HeightRequest="200"
HorizontalOptions="Center" />
<Label
Text="Hello, World!"
SemanticProperties.HeadingLevel="Level1"
FontSize="32"
HorizontalOptions="Center" />
<Label
Text="Welcome to .NET Multi-platform App UI"
SemanticProperties.HeadingLevel="Level2"
SemanticProperties.Description="Welcome to dot net Multi platform App U I"
FontSize="18"
HorizontalOptions="Center" />
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>

24
aspnet-core/services/LINGYUN.Abp.Applications/MainPage.xaml.cs

@ -0,0 +1,24 @@
namespace LINGYUN.Abp.Applications;
public partial class MainPage : ContentPage
{
int count = 0;
public MainPage()
{
InitializeComponent();
}
private void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
}
}

44
aspnet-core/services/LINGYUN.Abp.Applications/MauiProgram.cs

@ -0,0 +1,44 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Logging;
using System.Reflection;
using Volo.Abp;
using Volo.Abp.Autofac;
namespace LINGYUN.Abp.Applications;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureContainer(new AbpAutofacServiceProviderFactory(new Autofac.ContainerBuilder()));
ConfigureConfiguration(builder);
builder.Services.AddApplication<AbpApplicationsModule>(options =>
{
options.Services.ReplaceConfiguration(builder.Configuration);
});
var app = builder.Build();
app.Services
.GetRequiredService<IAbpApplicationWithExternalServiceProvider>()
.Initialize(app.Services);
return app;
}
private static void ConfigureConfiguration(MauiAppBuilder builder)
{
var assembly = typeof(App).GetTypeInfo().Assembly;
builder.Configuration.AddJsonFile(new EmbeddedFileProvider(assembly), "appsettings.json", optional: false, false);
}
}

6
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Android/AndroidManifest.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

9
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Android/MainActivity.cs

@ -0,0 +1,9 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace LINGYUN.Abp.Applications;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}

14
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Android/MainApplication.cs

@ -0,0 +1,14 @@
using Android.App;
using Android.Runtime;
namespace LINGYUN.Abp.Applications;
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

6
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Android/Resources/values/colors.xml

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

8
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/MacCatalyst/AppDelegate.cs

@ -0,0 +1,8 @@
using Foundation;
namespace LINGYUN.Abp.Applications;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

30
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/MacCatalyst/Info.plist

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

14
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/MacCatalyst/Program.cs

@ -0,0 +1,14 @@
using ObjCRuntime;
using UIKit;
namespace LINGYUN.Abp.Applications;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

16
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Tizen/Main.cs

@ -0,0 +1,16 @@
using Microsoft.Maui;
using Microsoft.Maui.Hosting;
using System;
namespace LINGYUN.Abp.Applications;
internal class Program : MauiApplication
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
static void Main(string[] args)
{
var app = new Program();
app.Run(args);
}
}

15
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Tizen/tizen-manifest.xml

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="7" xmlns="http://tizen.org/ns/packages">
<profile name="common" />
<ui-application appid="maui-application-id-placeholder" exec="LINGYUN.Abp.Applications.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
<label>maui-application-title-placeholder</label>
<icon>maui-appicon-placeholder</icon>
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
</ui-application>
<shortcut-list />
<privileges>
<privilege>http://tizen.org/privilege/internet</privilege>
</privileges>
<dependencies />
<provides-appdefined-privileges />
</manifest>

8
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Windows/App.xaml

@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="LINGYUN.Abp.Applications.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:LINGYUN.Abp.Applications.WinUI">
</maui:MauiWinUIApplication>

23
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Windows/App.xaml.cs

@ -0,0 +1,23 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace LINGYUN.Abp.Applications.WinUI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

46
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Windows/Package.appxmanifest

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<mp:PhoneIdentity PhoneProductId="BCCDBDF8-97B0-44DE-AD23-E3418D61B861" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate" />
</Resources>
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

15
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/Windows/app.manifest

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="LINGYUN.Abp.Applications.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
</windowsSettings>
</application>
</assembly>

8
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/iOS/AppDelegate.cs

@ -0,0 +1,8 @@
using Foundation;
namespace LINGYUN.Abp.Applications;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

32
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/iOS/Info.plist

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

14
aspnet-core/services/LINGYUN.Abp.Applications/Platforms/iOS/Program.cs

@ -0,0 +1,14 @@
using ObjCRuntime;
using UIKit;
namespace LINGYUN.Abp.Applications;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

8
aspnet-core/services/LINGYUN.Abp.Applications/Properties/launchSettings.json

@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "MsixPackage",
"nativeDebugging": false
}
}
}

4
aspnet-core/services/LINGYUN.Abp.Applications/Resources/AppIcon/appicon.svg

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

After

Width:  |  Height:  |  Size: 231 B

8
aspnet-core/services/LINGYUN.Abp.Applications/Resources/AppIcon/appiconfg.svg

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
aspnet-core/services/LINGYUN.Abp.Applications/Resources/Fonts/OpenSans-Regular.ttf

Binary file not shown.

BIN
aspnet-core/services/LINGYUN.Abp.Applications/Resources/Fonts/OpenSans-Semibold.ttf

Binary file not shown.

93
aspnet-core/services/LINGYUN.Abp.Applications/Resources/Images/dotnet_bot.svg

@ -0,0 +1,93 @@
<svg width="419" height="519" viewBox="0 0 419 519" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M284.432 247.568L284.004 221.881C316.359 221.335 340.356 211.735 355.308 193.336C382.408 159.996 372.893 108.183 372.786 107.659L398.013 102.831C398.505 105.432 409.797 167.017 375.237 209.53C355.276 234.093 324.719 246.894 284.432 247.568Z" fill="#8A6FE8"/>
<path d="M331.954 109.36L361.826 134.245C367.145 138.676 375.055 137.959 379.497 132.639C383.928 127.32 383.211 119.41 377.891 114.969L348.019 90.0842C342.7 85.6531 334.79 86.3702 330.348 91.6896C325.917 97.0197 326.634 104.929 331.954 109.36Z" fill="#8A6FE8"/>
<path d="M407.175 118.062L417.92 94.2263C420.735 87.858 417.856 80.4087 411.488 77.5831C405.12 74.7682 397.67 77.6473 394.845 84.0156L383.831 108.461L407.175 118.062Z" fill="#8A6FE8"/>
<path d="M401.363 105.175L401.234 69.117C401.181 62.1493 395.498 56.541 388.53 56.5945C381.562 56.648 375.954 62.3313 376.007 69.2989L376.018 96.11L401.363 105.175Z" fill="#8A6FE8"/>
<path d="M386.453 109.071L378.137 73.9548C376.543 67.169 369.757 62.9628 362.971 64.5575C356.185 66.1523 351.979 72.938 353.574 79.7237L362.04 115.482L386.453 109.071Z" fill="#8A6FE8"/>
<path d="M381.776 142.261C396.359 142.261 408.181 130.44 408.181 115.857C408.181 101.274 396.359 89.4527 381.776 89.4527C367.194 89.4527 355.372 101.274 355.372 115.857C355.372 130.44 367.194 142.261 381.776 142.261Z" fill="url(#paint0_radial)"/>
<path d="M248.267 406.979C248.513 384.727 245.345 339.561 222.376 301.736L199.922 315.372C220.76 349.675 222.323 389.715 221.841 407.182C221.798 408.627 235.263 409.933 248.267 406.979Z" fill="url(#paint1_linear)"/>
<path d="M221.841 406.936L242.637 406.84L262.052 518.065L220.311 518.258C217.132 518.269 214.724 515.711 214.938 512.532L221.841 406.936Z" fill="#522CD5"/>
<path d="M306.566 488.814C310.173 491.661 310.109 495.782 309.831 500.127L308.964 513.452C308.803 515.839 306.727 517.798 304.34 517.809L260.832 518.012C258.125 518.023 256.08 515.839 256.262 513.142L256.551 499.335C256.883 494.315 255.192 492.474 251.307 487.744C244.649 479.663 224.967 435.62 226.84 406.925L248.256 406.829C249.691 423.858 272.167 461.682 306.566 488.814Z" fill="url(#paint2_linear)"/>
<path d="M309.82 500.127C310.023 497.088 310.077 494.176 308.889 491.715L254.635 491.961C256.134 494.166 256.765 496.092 256.562 499.314L256.273 513.121C256.091 515.828 258.146 518.012 260.843 517.99L304.34 517.798C306.727 517.787 308.803 515.828 308.964 513.442L309.82 500.127Z" fill="url(#paint3_radial)"/>
<path d="M133.552 407.471C133.103 385.22 135.864 340.021 158.49 301.993L181.073 315.425C160.545 349.921 159.346 389.972 159.989 407.428C160.042 408.884 146.578 410.318 133.552 407.471Z" fill="url(#paint4_linear)"/>
<path d="M110.798 497.152C110.765 494.187 111.204 491.575 112.457 487.23C131.882 434.132 133.52 407.364 133.52 407.364L159.999 407.246C159.999 407.246 161.819 433.512 181.716 486.427C183.289 490.195 183.471 493.641 183.674 496.831L183.792 513.816C183.803 516.374 181.716 518.483 179.158 518.494L177.873 518.504L116.781 518.782L115.496 518.793C112.927 518.804 110.83 516.728 110.819 514.159L110.798 497.152Z" fill="url(#paint5_linear)"/>
<path d="M110.798 497.152C110.798 496.67 110.808 496.199 110.83 495.739C110.969 494.262 111.643 492.603 114.875 492.582L180.207 492.282C182.561 492.367 183.343 494.176 183.589 495.311C183.621 495.814 183.664 496.328 183.696 496.82L183.813 513.806C183.824 515.411 183.011 516.824 181.769 517.669C181.031 518.172 180.132 518.472 179.179 518.483L177.895 518.494L116.802 518.772L115.528 518.782C114.244 518.793 113.077 518.269 112.232 517.434C111.386 516.599 110.862 515.432 110.851 514.148L110.798 497.152Z" fill="url(#paint6_radial)"/>
<path d="M314.979 246.348C324.162 210.407 318.008 181.777 318.008 181.777L326.452 181.734L326.656 181.574C314.262 115.75 256.326 66.0987 186.949 66.4198C108.796 66.773 45.7233 130.424 46.0765 208.577C46.4297 286.731 110.08 349.803 188.234 349.45C249.905 349.172 302.178 309.474 321.304 254.343C321.872 251.999 321.797 247.804 314.979 246.348Z" fill="url(#paint7_radial)"/>
<path d="M310.237 279.035L65.877 280.148C71.3998 289.428 77.95 298.012 85.3672 305.761L290.972 304.829C298.336 297.005 304.8 288.368 310.237 279.035Z" fill="#D8CFF7"/>
<path d="M235.062 312.794L280.924 312.585L280.74 272.021L234.877 272.23L235.062 312.794Z" fill="#512BD4"/>
<path d="M243.001 297.626C242.691 297.626 242.434 297.53 242.22 297.327C242.006 297.123 241.899 296.866 241.899 296.588C241.899 296.299 242.006 296.042 242.22 295.839C242.434 295.625 242.691 295.528 243.001 295.528C243.312 295.528 243.568 295.635 243.782 295.839C243.996 296.042 244.114 296.299 244.114 296.588C244.114 296.877 244.007 297.123 243.793 297.327C243.568 297.519 243.312 297.626 243.001 297.626Z" fill="white"/>
<path d="M255.192 297.434H253.212L247.967 289.203C247.839 289 247.721 288.775 247.636 288.55H247.593C247.636 288.786 247.657 289.299 247.657 290.091L247.668 297.444H245.912L245.891 286.228H247.999L253.062 294.265C253.276 294.597 253.415 294.833 253.479 294.95H253.511C253.458 294.651 253.437 294.148 253.437 293.441L253.426 286.217H255.17L255.192 297.434Z" fill="white"/>
<path d="M263.733 297.412L257.589 297.423L257.568 286.206L263.465 286.195V287.779L259.387 287.79L259.398 290.969L263.155 290.958V292.532L259.398 292.542L259.409 295.86L263.733 295.85V297.412Z" fill="white"/>
<path d="M272.445 287.758L269.298 287.769L269.32 297.401H267.5L267.479 287.769L264.343 287.779V286.195L272.434 286.174L272.445 287.758Z" fill="white"/>
<path d="M315.279 246.337C324.355 210.836 318.457 182.483 318.308 181.798L171.484 182.462C171.484 182.462 162.226 181.563 162.268 190.018C162.311 198.463 162.761 222.341 162.878 248.746C162.9 254.172 167.363 256.773 170.863 256.751C170.874 256.751 311.618 252.213 315.279 246.337Z" fill="url(#paint8_radial)"/>
<path d="M227.685 246.798C227.685 246.798 250.183 228.827 254.571 225.499C258.959 222.17 262.812 221.977 266.869 225.445C270.925 228.913 293.616 246.498 293.616 246.498L227.685 246.798Z" fill="#A08BE8"/>
<path d="M320.748 256.141C320.748 256.141 324.943 248.414 315.279 246.348C315.289 246.305 170.927 246.894 170.927 246.894C167.566 246.905 163.232 244.925 162.846 241.671C162.857 244.004 162.878 246.369 162.889 248.756C162.91 253.68 166.582 256.27 169.878 256.698C170.21 256.73 170.542 256.773 170.874 256.773L180.742 256.73L320.748 256.141Z" fill="#512BD4"/>
<path d="M206.4 233.214C212.511 233.095 217.302 224.667 217.102 214.39C216.901 204.112 211.785 195.878 205.674 195.997C199.563 196.116 194.772 204.544 194.973 214.821C195.173 225.099 200.289 233.333 206.4 233.214Z" fill="#512BD4"/>
<path d="M306.249 214.267C306.356 203.989 301.488 195.605 295.377 195.541C289.266 195.478 284.225 203.758 284.118 214.037C284.011 224.315 288.878 232.699 294.99 232.763C301.101 232.826 306.142 224.545 306.249 214.267Z" fill="#512BD4"/>
<path d="M205.905 205.291C208.152 203.022 211.192 202.016 214.157 202.262C215.912 205.495 217.014 209.733 217.111 214.389C217.164 217.3 216.811 220.04 216.158 222.513C212.669 223.519 208.752 222.662 205.979 219.922C201.912 215.909 201.88 209.348 205.905 205.291Z" fill="#8065E0"/>
<path d="M294.996 204.285C297.255 202.016 300.294 200.999 303.259 201.256C305.164 204.628 306.309 209.209 306.256 214.239C306.224 216.808 305.892 219.259 305.303 221.485C301.793 222.523 297.843 221.678 295.061 218.916C291.004 214.892 290.972 208.342 294.996 204.285Z" fill="#8065E0"/>
<path d="M11.6342 357.017C10.9171 354.716 -5.72611 300.141 21.3204 258.903C36.9468 235.078 63.3083 221.035 99.6664 217.15L102.449 243.276C74.3431 246.273 54.4676 256.345 43.3579 273.202C23.0971 303.941 36.5722 348.733 36.7113 349.183L11.6342 357.017Z" fill="url(#paint9_linear)"/>
<path d="M95.1498 252.802C109.502 252.802 121.137 241.167 121.137 226.815C121.137 212.463 109.502 200.828 95.1498 200.828C80.7976 200.828 69.1628 212.463 69.1628 226.815C69.1628 241.167 80.7976 252.802 95.1498 252.802Z" fill="url(#paint10_radial)"/>
<path d="M72.0098 334.434L33.4683 329.307C26.597 328.397 20.2929 333.214 19.3725 340.085C18.4627 346.956 23.279 353.26 30.1504 354.181L68.6919 359.308C75.5632 360.217 81.8673 355.401 82.7878 348.53C83.6975 341.658 78.8705 335.344 72.0098 334.434Z" fill="#8A6FE8"/>
<path d="M3.73535 367.185L7.35297 393.076C8.36975 399.968 14.7702 404.731 21.6629 403.725C28.5556 402.708 33.3185 396.308 32.3124 389.415L28.5984 362.861L3.73535 367.185Z" fill="#8A6FE8"/>
<path d="M15.5194 374.988L34.849 405.427C38.6058 411.292 46.4082 413.005 52.2735 409.248C58.1387 405.491 59.8512 397.689 56.0945 391.823L41.7953 369.144L15.5194 374.988Z" fill="#8A6FE8"/>
<path d="M26.0511 363.739L51.8026 389.019C56.7688 393.911 64.7532 393.846 69.6445 388.88C74.5358 383.914 74.4715 375.929 69.516 371.038L43.2937 345.297L26.0511 363.739Z" fill="#8A6FE8"/>
<path d="M26.4043 381.912C40.987 381.912 52.8086 370.091 52.8086 355.508C52.8086 340.925 40.987 329.104 26.4043 329.104C11.8216 329.104 0 340.925 0 355.508C0 370.091 11.8216 381.912 26.4043 381.912Z" fill="url(#paint11_radial)"/>
<path d="M184.73 63.6308L157.819 66.5892L158.561 38.5412L177.888 36.4178L184.73 63.6308Z" fill="#8A6FE8"/>
<path d="M170.018 41.647C180.455 39.521 187.193 29.3363 185.067 18.8988C182.941 8.46126 172.757 1.72345 162.319 3.84944C151.882 5.97543 145.144 16.1601 147.27 26.5976C149.396 37.0351 159.58 43.773 170.018 41.647Z" fill="#D8CFF7"/>
<path d="M196.885 79.385C198.102 79.2464 198.948 78.091 198.684 76.8997C195.851 64.2818 183.923 55.5375 170.773 56.9926C157.622 58.4371 147.886 69.5735 147.865 82.4995C147.863 83.7232 148.949 84.6597 150.168 84.5316L196.885 79.385Z" fill="url(#paint12_radial)"/>
<defs>
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(382.004 103.457) scale(26.4058)">
<stop stop-color="#8065E0"/>
<stop offset="1" stop-color="#512BD4"/>
</radialGradient>
<linearGradient id="paint1_linear" x1="214.439" y1="303.482" x2="236.702" y2="409.505" gradientUnits="userSpaceOnUse">
<stop stop-color="#522CD5"/>
<stop offset="0.4397" stop-color="#8A6FE8"/>
</linearGradient>
<linearGradient id="paint2_linear" x1="231.673" y1="404.144" x2="297.805" y2="522.048" gradientUnits="userSpaceOnUse">
<stop stop-color="#522CD5"/>
<stop offset="0.4397" stop-color="#8A6FE8"/>
</linearGradient>
<radialGradient id="paint3_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(280.957 469.555) rotate(-0.260742) scale(45.8326)">
<stop offset="0.034" stop-color="#522CD5"/>
<stop offset="0.9955" stop-color="#8A6FE8"/>
</radialGradient>
<linearGradient id="paint4_linear" x1="166.061" y1="303.491" x2="144.763" y2="409.709" gradientUnits="userSpaceOnUse">
<stop stop-color="#522CD5"/>
<stop offset="0.4397" stop-color="#8A6FE8"/>
</linearGradient>
<linearGradient id="paint5_linear" x1="146.739" y1="407.302" x2="147.246" y2="518.627" gradientUnits="userSpaceOnUse">
<stop stop-color="#522CD5"/>
<stop offset="0.4397" stop-color="#8A6FE8"/>
</linearGradient>
<radialGradient id="paint6_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(148.63 470.023) rotate(179.739) scale(50.2476)">
<stop offset="0.034" stop-color="#522CD5"/>
<stop offset="0.9955" stop-color="#8A6FE8"/>
</radialGradient>
<radialGradient id="paint7_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(219.219 153.929) rotate(179.739) scale(140.935)">
<stop offset="0.4744" stop-color="#A08BE8"/>
<stop offset="0.8618" stop-color="#8065E0"/>
</radialGradient>
<radialGradient id="paint8_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(314.861 158.738) rotate(179.739) scale(146.053)">
<stop offset="0.0933" stop-color="#E1DFDD"/>
<stop offset="0.6573" stop-color="white"/>
</radialGradient>
<linearGradient id="paint9_linear" x1="54.1846" y1="217.159" x2="54.1846" y2="357.022" gradientUnits="userSpaceOnUse">
<stop offset="0.3344" stop-color="#9780E6"/>
<stop offset="0.8488" stop-color="#8A6FE8"/>
</linearGradient>
<radialGradient id="paint10_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(90.3494 218.071) rotate(-0.260742) scale(25.9924)">
<stop stop-color="#8065E0"/>
<stop offset="1" stop-color="#512BD4"/>
</radialGradient>
<radialGradient id="paint11_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(25.805 345.043) scale(26.4106)">
<stop stop-color="#8065E0"/>
<stop offset="1" stop-color="#512BD4"/>
</radialGradient>
<radialGradient id="paint12_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(169.113 67.3662) rotate(-32.2025) scale(21.0773)">
<stop stop-color="#8065E0"/>
<stop offset="1" stop-color="#512BD4"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

15
aspnet-core/services/LINGYUN.Abp.Applications/Resources/Raw/AboutAssets.txt

@ -0,0 +1,15 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
These files will be deployed with you package and will be accessible using Essentials:
async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
}

8
aspnet-core/services/LINGYUN.Abp.Applications/Resources/Splash/splash.svg

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

44
aspnet-core/services/LINGYUN.Abp.Applications/Resources/Styles/Colors.xaml

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
<Color x:Key="Yellow100Accent">#F7B548</Color>
<Color x:Key="Yellow200Accent">#FFD590</Color>
<Color x:Key="Yellow300Accent">#FFE5B9</Color>
<Color x:Key="Cyan100Accent">#28C2D1</Color>
<Color x:Key="Cyan200Accent">#7BDDEF</Color>
<Color x:Key="Cyan300Accent">#C3F2F4</Color>
<Color x:Key="Blue100Accent">#3E8EED</Color>
<Color x:Key="Blue200Accent">#72ACF1</Color>
<Color x:Key="Blue300Accent">#A7CBF6</Color>
</ResourceDictionary>

405
aspnet-core/services/LINGYUN.Abp.Applications/Resources/Styles/Styles.xaml

@ -0,0 +1,405 @@
<?xml version="1.0" encoding="UTF-8" ?>
<?xaml-comp compile="true" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
</Style>
<Style TargetType="BoxView">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Primary}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Frame">
<Setter Property="HasShadow" Value="False" />
<Setter Property="BorderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ListView">
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.ForegroundColor" Value="{OnPlatform WinUI={StaticResource Primary}, Default={StaticResource White}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>

3
aspnet-core/services/LINGYUN.Abp.Applications/appsettings.json

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

9
aspnet-core/services/LY.MicroService.RealtimeMessage.HttpApi.Host/EventBus/Distributed/NotificationEventHandler.cs

@ -68,6 +68,10 @@ namespace LY.MicroService.RealtimeMessage.EventBus.Distributed
/// </summary>
protected IStringLocalizerFactory StringLocalizerFactory { get; }
/// <summary>
/// Reference to <see cref="INotificationDataSerializer"/>.
/// </summary>
protected INotificationDataSerializer NotificationDataSerializer { get; }
/// <summary>
/// Reference to <see cref="INotificationDefinitionManager"/>.
/// </summary>
protected INotificationDefinitionManager NotificationDefinitionManager { get; }
@ -92,6 +96,7 @@ namespace LY.MicroService.RealtimeMessage.EventBus.Distributed
IStringLocalizerFactory stringLocalizerFactory,
IOptions<AbpNotificationsPublishOptions> options,
INotificationStore notificationStore,
INotificationDataSerializer notificationDataSerializer,
INotificationDefinitionManager notificationDefinitionManager,
INotificationSubscriptionManager notificationSubscriptionManager,
INotificationPublishProviderManager notificationPublishProviderManager)
@ -104,6 +109,7 @@ namespace LY.MicroService.RealtimeMessage.EventBus.Distributed
BackgroundJobManager = backgroundJobManager;
StringLocalizerFactory = stringLocalizerFactory;
NotificationStore = notificationStore;
NotificationDataSerializer = notificationDataSerializer;
NotificationDefinitionManager = notificationDefinitionManager;
NotificationSubscriptionManager = notificationSubscriptionManager;
NotificationPublishProviderManager = notificationPublishProviderManager;
@ -289,8 +295,7 @@ namespace LY.MicroService.RealtimeMessage.EventBus.Distributed
};
notificationInfo.SetId(eventData.Id);
// TODO: 可以做成一个接口来序列化消息
notificationInfo.Data = NotificationDataConverter.Convert(notificationInfo.Data);
notificationInfo.Data = NotificationDataSerializer.Serialize(notificationInfo.Data);
// 获取用户订阅
var subscriptionUsers = await GerSubscriptionUsersAsync(

Loading…
Cancel
Save