PageModal.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284
  1. <template>
  2. <div>
  3. <!-- drawer -->
  4. <template v-if="modalConfig.component === 'drawer'">
  5. <el-drawer
  6. v-model="modalVisible"
  7. v-bind="{ destroyOnClose: true, ...modalConfig.drawer }"
  8. @close="handleClose"
  9. >
  10. <el-form ref="formRef" v-bind="modalConfig.form" :model="formData" :rules="formRules">
  11. <el-row :gutter="20">
  12. <template v-for="item in formItems" :key="item.prop">
  13. <el-col v-show="!item.hidden" v-bind="item.col">
  14. <el-form-item :label="item.label" :prop="item.prop">
  15. <!-- Label -->
  16. <template #label>
  17. <span>
  18. {{ item?.label || "" }}
  19. <el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
  20. <QuestionFilled class="w-4 h-4 mx-1" />
  21. </el-tooltip>
  22. <span v-if="modalConfig.colon" class="ml-0.5">:</span>
  23. </span>
  24. </template>
  25. <!-- components -->
  26. <template v-if="item.type === 'custom'">
  27. <slot
  28. :name="item.slotName ?? item.prop"
  29. :prop="item.prop"
  30. :form-data="formData"
  31. :attrs="item.attrs"
  32. ></slot>
  33. </template>
  34. <component
  35. :is="componentMap.get(item.type)"
  36. v-else
  37. v-model.trim="formData[item.prop]"
  38. v-bind="{ style: { width: '100%' }, ...item.attrs }"
  39. >
  40. <template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
  41. <component
  42. :is="childrenMap.get(item.type)"
  43. v-for="opt in item.options"
  44. :key="opt.value"
  45. :label="opt.label"
  46. :value="opt.value"
  47. ></component>
  48. </template>
  49. <template v-if="item?.slotName && $slots[item.slotName]" #[item.slotName]>
  50. <slot :name="item.slotName" />
  51. </template>
  52. </component>
  53. </el-form-item>
  54. </el-col>
  55. </template>
  56. </el-row>
  57. </el-form>
  58. <template #footer>
  59. <el-button v-if="!formDisable" type="primary" @click="handleSubmit">确 定</el-button>
  60. <el-button @click="handleClose">{{ !formDisable ? "取 消" : "关闭" }}</el-button>
  61. </template>
  62. </el-drawer>
  63. </template>
  64. <!-- dialog -->
  65. <template v-else>
  66. <EnhancedDialog
  67. v-model="modalVisible"
  68. :title="String(modalConfig.dialog?.title ?? '')"
  69. :width="modalConfig.dialog?.width ?? '600px'"
  70. :draggable="modalConfig.dialog?.draggable !== false"
  71. v-bind="dialogRestAttrs"
  72. @close="handleClose"
  73. >
  74. <el-form ref="formRef" v-bind="modalConfig.form" :model="formData" :rules="formRules">
  75. <el-scrollbar max-height="70vh" :view-style="{ overflowX: 'hidden' }">
  76. <el-row :gutter="20">
  77. <template v-for="item in formItems" :key="item.prop">
  78. <el-col v-show="!item.hidden" v-bind="item.col">
  79. <el-form-item :label="item.label" :prop="item.prop">
  80. <template #label>
  81. <span>
  82. {{ item?.label || "" }}
  83. <el-tooltip v-if="item?.tips" v-bind="getTooltipProps(item.tips)">
  84. <QuestionFilled class="w-4 h-4 mx-1" />
  85. </el-tooltip>
  86. <span v-if="modalConfig.colon" class="ml-0.5">:</span>
  87. </span>
  88. </template>
  89. <template v-if="item.type === 'custom'">
  90. <slot
  91. :name="item.slotName ?? item.prop"
  92. :prop="item.prop"
  93. :form-data="formData"
  94. :attrs="item.attrs"
  95. ></slot>
  96. </template>
  97. <component
  98. :is="componentMap.get(item.type)"
  99. v-else
  100. v-model.trim="formData[item.prop]"
  101. v-bind="{ style: { width: '100%' }, ...item.attrs }"
  102. >
  103. <template v-if="['select', 'radio', 'checkbox'].includes(item.type)">
  104. <component
  105. :is="childrenMap.get(item.type)"
  106. v-for="opt in item.options"
  107. :key="opt.value"
  108. :label="opt.label"
  109. :value="opt.value"
  110. ></component>
  111. </template>
  112. <template v-if="item?.slotName && $slots[item.slotName]" #[item.slotName]>
  113. <slot :name="item.slotName" />
  114. </template>
  115. </component>
  116. </el-form-item>
  117. </el-col>
  118. </template>
  119. </el-row>
  120. <slot name="bottom" :form-data="formData"></slot>
  121. </el-scrollbar>
  122. </el-form>
  123. <template #footer>
  124. <el-button v-if="!formDisable" type="primary" @click="handleSubmit">确 定</el-button>
  125. <el-button @click="handleClose">{{ !formDisable ? "取 消" : "关闭" }}</el-button>
  126. </template>
  127. </EnhancedDialog>
  128. </template>
  129. </div>
  130. </template>
  131. <script setup lang="ts">
  132. import { computed, markRaw, onMounted, reactive, ref } from "vue";
  133. import { useThrottleFn } from "@vueuse/core";
  134. import type { FormInstance, FormRules } from "element-plus";
  135. import type { IComponentType, IModalConfig, IObject } from "./types";
  136. import InputTag from "@/components/InputTag/index.vue";
  137. import IconSelect from "@/components/IconSelect/index.vue";
  138. import EnhancedDialog from "./EnhancedDialog.vue";
  139. defineSlots<{ [key: string]: (_args: any) => any }>();
  140. // 定义接收的属性
  141. const props = defineProps<{ modalConfig: IModalConfig }>();
  142. // 自定义事件
  143. const emit = defineEmits<{ submitClick: []; customSubmit: [queryParams: IObject] }>();
  144. // 组件映射表
  145. const componentMap = new Map<IComponentType, any>([
  146. // @ts-ignore
  147. ["input", markRaw(ElInput)], // @ts-ignore
  148. ["select", markRaw(ElSelect)], // @ts-ignore
  149. ["switch", markRaw(ElSwitch)], // @ts-ignore
  150. ["cascader", markRaw(ElCascader)], // @ts-ignore
  151. ["input-number", markRaw(ElInputNumber)], // @ts-ignore
  152. ["input-tag", markRaw(InputTag)], // @ts-ignore
  153. ["time-picker", markRaw(ElTimePicker)], // @ts-ignore
  154. ["time-select", markRaw(ElTimeSelect)], // @ts-ignore
  155. ["date-picker", markRaw(ElDatePicker)], // @ts-ignore
  156. ["tree-select", markRaw(ElTreeSelect)], // @ts-ignore"
  157. ["custom-tag", markRaw(InputTag)], // @ts-ignore
  158. ["text", markRaw(ElText)], // @ts-ignore
  159. ["radio", markRaw(ElRadioGroup)], // @ts-ignore"
  160. ["checkbox", markRaw(ElCheckboxGroup)], // @ts-ignore"
  161. ["icon-select", markRaw(IconSelect)], // @ts-ignore"
  162. ["custom", ""],
  163. ]);
  164. const childrenMap = new Map<IComponentType, any>([
  165. // @ts-ignore
  166. ["select", markRaw(ElOption)], // @ts-ignore
  167. ["radio", markRaw(ElRadio)], // @ts-ignore"
  168. ["checkbox", markRaw(ElCheckbox)],
  169. ]);
  170. const pk = props.modalConfig.pk ?? "id"; // 主键名,用于表单数据处理
  171. const dialogRestAttrs = computed(() => {
  172. const d = props.modalConfig.dialog ?? {};
  173. const { title: _t, width: _w, draggable: _d, ...rest } = d;
  174. return { destroyOnClose: true, ...rest };
  175. });
  176. const modalVisible = ref(false); // 弹窗显示状态
  177. const formRef = ref<FormInstance>(); // 表单实例
  178. const formItems = reactive(props.modalConfig.formItems ?? []); // 表单配置项
  179. const formData = reactive<IObject>({}); // 表单数据
  180. const formRules: FormRules = {}; // 表单验证规则
  181. const formDisable = ref(false); // 表单禁用状态
  182. // 获取tooltip提示框属性
  183. const getTooltipProps = (tips: string | IObject) => {
  184. return typeof tips === "string" ? { content: tips } : tips;
  185. };
  186. // 隐藏弹窗
  187. const handleClose = () => {
  188. modalVisible.value = false;
  189. formRef.value?.resetFields();
  190. };
  191. // 设置表单值
  192. const setFormData = (data: IObject) => {
  193. for (const key in formData) {
  194. if (Object.prototype.hasOwnProperty.call(formData, key) && key in data) {
  195. formData[key] = data[key];
  196. }
  197. }
  198. if (Object.prototype.hasOwnProperty.call(data, pk)) {
  199. formData[pk] = data[pk];
  200. }
  201. };
  202. // 表单提交
  203. const handleSubmit = useThrottleFn(() => {
  204. formRef.value?.validate((valid: boolean) => {
  205. if (!valid) return;
  206. if (typeof props.modalConfig.beforeSubmit === "function") {
  207. props.modalConfig.beforeSubmit(formData);
  208. }
  209. if (!props.modalConfig?.formAction) {
  210. emit("customSubmit", formData);
  211. handleClose();
  212. return;
  213. }
  214. props.modalConfig.formAction(formData).then(() => {
  215. if (props.modalConfig.component === "drawer") {
  216. ElMessage.success(`${props.modalConfig.drawer?.title}成功`);
  217. } else {
  218. ElMessage.success(`${props.modalConfig.dialog?.title}成功`);
  219. }
  220. emit("submitClick");
  221. handleClose();
  222. });
  223. });
  224. }, 3000);
  225. onMounted(() => {
  226. formItems.forEach((item) => {
  227. if (item.initFn) {
  228. item.initFn(item);
  229. }
  230. formRules[item.prop] = item?.rules ?? [];
  231. props.modalConfig.form = { labelWidth: "auto", ...props.modalConfig?.form };
  232. if (["input-tag", "custom-tag", "cascader"].includes(item.type)) {
  233. formData[item.prop] = Array.isArray(item.initialValue) ? item.initialValue : [];
  234. } else if (item.type === "input-number") {
  235. formData[item.prop] = item.initialValue ?? null;
  236. } else {
  237. formData[item.prop] = item.initialValue ?? "";
  238. }
  239. });
  240. });
  241. // 暴露的属性和方法
  242. defineExpose({
  243. setFormData,
  244. // 展示/因此 modal
  245. setModalVisible: (visible: boolean = true) => (modalVisible.value = visible),
  246. // 获取表单数据
  247. getFormData: (key: string) => formData[key] ?? formData,
  248. // 设置表单项值
  249. setFormItemData: (key: string, value: any) => (formData[key] = value),
  250. // 禁用表单
  251. handleDisabled: (disable: boolean) => {
  252. formDisable.value = disable;
  253. props.modalConfig.form = {
  254. ...props.modalConfig.form,
  255. disabled: disable,
  256. };
  257. },
  258. });
  259. </script>
  260. <style lang="scss" scoped>
  261. :deep(.el-input-number .el-input__inner) {
  262. text-align: left;
  263. }
  264. :deep(.el-input-number.is-without-controls .el-input__wrapper) {
  265. padding-right: 11px !important ;
  266. padding-left: 11px !important;
  267. }
  268. </style>