DialogForm.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. <script setup lang="ts">
  2. import { computed, onMounted, PropType, ref, toRefs, watch } from 'vue';
  3. import { ElMessage } from 'element-plus';
  4. import { Plus, Delete } from '@element-plus/icons-vue';
  5. import { useI18n } from 'vue-i18n';
  6. import _ from 'lodash';
  7. import { perm } from '@/store/useCurrentUser';
  8. import { actionType, ACTION } from '@/constant/common';
  9. const CONTINUOUS_SETTINGS = 'cms_continuous_settings';
  10. function fetchContinuous(): Record<string, boolean> {
  11. const settings = localStorage.getItem(CONTINUOUS_SETTINGS);
  12. return settings ? JSON.parse(settings) : {};
  13. }
  14. function storeContinuous(settings: Record<string, boolean>) {
  15. localStorage.setItem(CONTINUOUS_SETTINGS, JSON.stringify(settings));
  16. }
  17. function getContinuous(name: string) {
  18. const settings = fetchContinuous();
  19. return settings[name] ?? false;
  20. }
  21. function setContinuous(name: string, continuous: boolean) {
  22. const settings = fetchContinuous();
  23. settings[name] = continuous;
  24. storeContinuous(settings);
  25. }
  26. const props = defineProps({
  27. modelValue: { type: Boolean, required: true },
  28. name: { type: String, required: true },
  29. beanId: { type: [Number, String], default: null },
  30. beanIds: { type: Array as PropType<string[] | number[]>, required: true },
  31. values: { type: Object, required: true },
  32. initValues: { type: Function as PropType<(bean?: any) => any>, required: true },
  33. toValues: { type: Function as PropType<(bean: any) => any>, required: true },
  34. queryBean: { type: Function as PropType<(id: any) => Promise<any>>, required: true },
  35. createBean: { type: Function as PropType<(bean: any) => Promise<any>>, required: true },
  36. updateBean: { type: Function as PropType<(bean: any) => Promise<any>>, required: true },
  37. disableDelete: { type: Function as PropType<(bean: any) => boolean>, default: null },
  38. disableEdit: { type: Function as PropType<(bean: any) => boolean>, default: null },
  39. addable: { type: Boolean, default: true },
  40. action: { type: String as PropType<actionType>, default: ACTION.EDIT },
  41. showId: { type: Boolean, default: true },
  42. perms: { type: String, default: null },
  43. focus: { type: Object, default: null },
  44. large: { type: Boolean, default: false },
  45. labelPosition: { type: String as PropType<'top' | 'right' | 'left'>, default: 'right' },
  46. labelWidth: { type: String, default: '150px' },
  47. });
  48. const emit = defineEmits({
  49. 'update:modelValue': null,
  50. 'update:values': null,
  51. finished: null,
  52. beanChange: null,
  53. beforeSubmit: null,
  54. });
  55. const { name, beanId, beanIds, focus, values, action, modelValue: visible } = toRefs(props);
  56. const { t } = useI18n();
  57. const loading = ref<boolean>(false);
  58. const buttonLoading = ref<boolean>(false);
  59. const continuous = ref<boolean>(getContinuous(name.value));
  60. const form = ref();
  61. const bean = ref(props.initValues());
  62. const origValues = ref();
  63. const id = ref();
  64. const ids = ref<Array<any>>([]);
  65. const isEdit = computed(() => id.value != null && action.value === ACTION.EDIT);
  66. const unsaved = computed(() => {
  67. // 调试 未保存
  68. // if (!_.isEqual(origValues.value, values.value)) {
  69. // console.log(JSON.stringify(origValues.value));
  70. // console.log(JSON.stringify(values.value));
  71. // }
  72. return !loading.value && !_.isEqual(origValues.value, values.value);
  73. });
  74. const disabled = computed(() => props.disableEdit?.(bean.value) ?? false);
  75. const title = computed(() => `${name.value} - ${isEdit.value ? `${t(disabled.value ? ACTION.DETAIL : ACTION.EDIT)} (ID: ${id.value})` : `${t('add')}`}`);
  76. const loadBean = async () => {
  77. loading.value = true;
  78. try {
  79. bean.value = id.value != null ? await props.queryBean(id.value) : props.initValues(values.value);
  80. origValues.value = id.value != null ? props.toValues(bean.value) : bean.value;
  81. emit('update:values', _.cloneDeep(origValues.value));
  82. emit('beanChange', bean.value);
  83. form.value?.resetFields();
  84. } finally {
  85. loading.value = false;
  86. }
  87. };
  88. onMounted(() => emit('update:values', props.initValues()));
  89. watch(visible, () => {
  90. if (visible.value) {
  91. ids.value = beanIds.value;
  92. if (id.value !== beanId.value) {
  93. id.value = beanId.value;
  94. } else {
  95. loadBean();
  96. }
  97. }
  98. });
  99. watch(id, () => {
  100. loadBean();
  101. });
  102. watch(continuous, () => setContinuous(name.value, continuous.value));
  103. const index = computed(() => ids.value.indexOf(id.value));
  104. const hasPrev = computed(() => index.value > 0);
  105. const hasNext = computed(() => index.value < ids.value.length - 1);
  106. const handlePrev = () => {
  107. if (hasPrev.value) {
  108. id.value = ids.value[index.value - 1];
  109. }
  110. };
  111. const handleNext = () => {
  112. if (hasNext.value) {
  113. id.value = ids.value[index.value + 1];
  114. }
  115. };
  116. // const handleAdd = () => {
  117. // focus.value?.focus?.();
  118. // id.value = undefined;
  119. // };
  120. // const handleCancel = () => {
  121. // emit('update:modelValue', false);
  122. // };
  123. const handleSubmit = () => {
  124. form.value.validate(async (valid: boolean) => {
  125. if (!valid) return;
  126. buttonLoading.value = true;
  127. try {
  128. emit('beforeSubmit', values.value);
  129. if (isEdit.value) {
  130. await props.updateBean(values.value);
  131. } else {
  132. await props.createBean(values.value);
  133. // eslint-disable-next-line no-unused-expressions
  134. focus.value?.focus?.();
  135. emit('update:values', props.initValues(values.value));
  136. form.value.resetFields();
  137. }
  138. ElMessage.success(t('success'));
  139. if (!continuous.value) emit('update:modelValue', false);
  140. emit('finished', bean.value);
  141. } finally {
  142. buttonLoading.value = false;
  143. }
  144. });
  145. };
  146. // const handleDelete = async () => {
  147. // buttonLoading.value = true;
  148. // try {
  149. // await props.deleteBean([id.value]);
  150. // if (!continuous.value) emit('update:modelValue', false);
  151. // if (hasNext.value) {
  152. // handleNext();
  153. // ids.value.splice(index.value - 1, 1);
  154. // } else if (hasPrev.value) {
  155. // handlePrev();
  156. // ids.value.splice(index.value + 1, 1);
  157. // } else {
  158. // emit('update:modelValue', false);
  159. // }
  160. // ElMessage.success(t('success'));
  161. // emit('finished');
  162. // } finally {
  163. // buttonLoading.value = false;
  164. // }
  165. // };
  166. const submit = (
  167. executor: (values: any, payload: { isEdit: boolean; continuous: boolean; form: any; props: any; focus: any; loadBean: () => Promise<any>; emit: any }) => Promise<any>,
  168. ) => {
  169. form.value.validate(async (valid: boolean) => {
  170. if (!valid) return;
  171. buttonLoading.value = true;
  172. try {
  173. emit('beforeSubmit', values.value);
  174. await executor(values.value, { isEdit: isEdit.value, continuous: continuous.value, form: form.value, props, focus: focus.value, loadBean, emit });
  175. if (!continuous.value) emit('update:modelValue', false);
  176. emit('finished', bean.value);
  177. } finally {
  178. buttonLoading.value = false;
  179. }
  180. });
  181. };
  182. const remove = async (
  183. executor: (values: any, payload: { isEdit: boolean; continuous: boolean; form: any; props: any; focus: any; loadBean: () => Promise<any>; emit: any }) => Promise<any>,
  184. ) => {
  185. buttonLoading.value = true;
  186. try {
  187. await executor(values.value, { isEdit: isEdit.value, continuous: continuous.value, form: form.value, props, focus: focus.value, loadBean, emit });
  188. if (!continuous.value) emit('update:modelValue', false);
  189. if (hasNext.value) {
  190. handleNext();
  191. ids.value.splice(index.value - 1, 1);
  192. } else if (hasPrev.value) {
  193. handlePrev();
  194. ids.value.splice(index.value + 1, 1);
  195. } else {
  196. emit('update:modelValue', false);
  197. }
  198. ElMessage.success(t('success'));
  199. emit('finished');
  200. } finally {
  201. buttonLoading.value = false;
  202. }
  203. };
  204. defineExpose({ form, submit, remove });
  205. </script>
  206. <template>
  207. <el-dialog
  208. :title="title"
  209. :close-on-click-modal="!unsaved"
  210. :model-value="modelValue"
  211. :width="large ? '98%' : '768px'"
  212. :top="large ? '16px' : '8vh'"
  213. @update:model-value="(event) => $emit('update:modelValue', event)"
  214. @opened="() => !isEdit && focus?.focus()"
  215. >
  216. <template #header>
  217. {{ name }} -
  218. <span v-if="isEdit">
  219. {{ $t(disabled ? 'detail' : 'edit') }}
  220. <span v-if="showId">(ID: {{ id }})</span>
  221. </span>
  222. <span v-else>{{ $t('add') }}</span>
  223. </template>
  224. <!-- <div v-loading="loading || buttonLoading" class="space-x-2">
  225. <el-button v-if="isEdit && addable" :disabled="perm(`${perms}:create`)" type="primary" :icon="Plus" @click="handleAdd">{{ $t('add') }}</el-button>
  226. <slot name="header-action" :bean="bean" :is-edit="isEdit" :disabled="disabled" :unsaved="unsaved" :disable-delete="disableDelete" :handle-delete="handleDelete">
  227. <el-popconfirm v-if="isEdit" :title="$t('confirmDelete')" @confirm="() => handleDelete()">
  228. <template #reference>
  229. <el-button :disabled="disableDelete?.(bean) || perm(`${perms}:delete`)" :icon="Delete">{{ $t('delete') }}</el-button>
  230. </template>
  231. </el-popconfirm>
  232. </slot>
  233. <el-button-group v-if="isEdit">
  234. <el-button :disabled="!hasPrev" @click="handlePrev">{{ $t('form.prev') }}</el-button>
  235. <el-button :disabled="!hasNext" @click="handleNext">{{ $t('form.next') }}</el-button>
  236. </el-button-group>
  237. <el-button type="primary" @click="handleCancel">{{ $t('back') }}</el-button>
  238. <el-tooltip :content="$t('form.continuous')" placement="top">
  239. <el-switch v-model="continuous" size="small"></el-switch>
  240. </el-tooltip>
  241. <el-tag v-if="unsaved" type="danger">{{ $t('form.unsaved') }}</el-tag>
  242. <slot name="header-status" :bean="bean" :is-edit="isEdit" :disabled="disabled"></slot>
  243. </div> -->
  244. <el-form ref="form" :class="['mt-5', 'pr-5']" :model="values" :disabled="disabled" :label-width="labelWidth" :label-position="labelPosition" scroll-to-error>
  245. <slot :bean="bean" :is-edit="isEdit" :disabled="disabled"></slot>
  246. <div v-if="!disabled" v-loading="buttonLoading" class="w-full text-center footer-action">
  247. <slot name="footer-action" :bean="bean" :is-edit="isEdit" :disabled="disabled" :handle-submit="handleSubmit">
  248. <el-button :disabled="perm(isEdit ? `${perms}:update` : `${perms}:create`)" type="primary" native-type="submit" @click.prevent="() => handleSubmit()">
  249. {{ $t('save') }}
  250. </el-button>
  251. </slot>
  252. </div>
  253. </el-form>
  254. </el-dialog>
  255. </template>