BasicInfo.vue 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. <!--
  2. * @since: 2024-12-30
  3. * BasicInfo.vue
  4. -->
  5. <template>
  6. <CardLayout title="基础配置" :isShowWraning="false" :mandatory="false">
  7. <el-form
  8. ref="ruleFormRef"
  9. label-width="auto"
  10. :model="ruleForm"
  11. :rules="formRules"
  12. :label-position="labelPosition"
  13. class="el-form-outer"
  14. >
  15. <el-form-item label="消息样式: " prop="messageType">
  16. <el-radio-group v-model="ruleForm.messageType" :disabled="!pageScopedDisabled">
  17. <el-radio :value="item.value" v-for="item in messageTypeOptions" :key="item.value"
  18. >{{ item.label }}
  19. </el-radio>
  20. </el-radio-group>
  21. </el-form-item>
  22. <el-form-item label="消息标题: " prop="title">
  23. <el-input
  24. v-model="ruleForm.title"
  25. placeholder="请输入20字以内的消息标题"
  26. maxlength="20"
  27. show-word-limit
  28. :disabled="!pageScopedDisabled"
  29. />
  30. </el-form-item>
  31. <el-form-item label="Banner图片: " prop="bannerUrl" v-if="ruleForm.messageType === MessageTypeEnum.BANNER">
  32. <el-upload
  33. ref="upload"
  34. v-model:file-list="fileList"
  35. :action="actionUrl"
  36. :headers="getHeaders()"
  37. :data="{ bizType: 'PUSH_MESSAGE' }"
  38. :on-success="handleAvatarSuccess"
  39. :before-upload="beforeAvatarUpload"
  40. :limit="1"
  41. list-type="picture-card"
  42. :on-exceed="handleExceed"
  43. :on-preview="handlePictureCardPreview"
  44. :disabled="!pageScopedDisabled"
  45. >
  46. <el-icon class="avatar-uploader-icon"><Plus /></el-icon>
  47. </el-upload>
  48. <span class="tip-message">支持格式:.jpg、.jpeg、.png,文件不超过 20MB,建议上传 4:3 比例的图片!</span>
  49. </el-form-item>
  50. <el-form-item label="推送渠道: " prop="pushChannel">
  51. <el-checkbox-group v-model="ruleForm.pushChannel">
  52. <el-checkbox
  53. v-for="item in pushChannelName"
  54. :key="item.value"
  55. :value="item.value"
  56. :label="item.label"
  57. :disabled="!pageScopedDisabled"
  58. />
  59. </el-checkbox-group>
  60. </el-form-item>
  61. <el-form-item label="失效时间: " prop="expirationTime">
  62. <el-date-picker
  63. v-model="ruleForm.expirationTime"
  64. type="datetime"
  65. placeholder="请选择失效时间"
  66. format="YYYY/MM/DD hh:mm:ss"
  67. value-format="YYYY-MM-DD hh:mm:ss"
  68. :disabled="!pageScopedDisabled"
  69. :disabled-date="disabledDate"
  70. />
  71. <span class="tip-message">请注意:超过该日期后,【APP/PC 主页】将不再弹出消息!</span>
  72. </el-form-item>
  73. <PushObject
  74. ref="childFromRef"
  75. :recipientType="ruleForm.recipientType"
  76. :userGroupList="ruleForm.userGroupList"
  77. :customUserList="ruleForm.customUserList"
  78. :disabled="!pageScopedDisabled"
  79. :has-all="true"
  80. />
  81. </el-form>
  82. </CardLayout>
  83. <el-dialog v-model="dialogVisible">
  84. <img w-full :src="dialogImageUrl" alt="Preview Image" />
  85. </el-dialog>
  86. </template>
  87. <script setup lang="ts">
  88. import { ref, watch, computed, unref } from 'vue';
  89. import { useRoute } from 'vue-router';
  90. import type {
  91. FormInstance,
  92. FormProps,
  93. FormRules,
  94. UploadProps,
  95. UploadUserFile,
  96. UploadInstance,
  97. UploadRawFile,
  98. } from 'element-plus';
  99. import { ElMessage, genFileId } from 'element-plus';
  100. import { Plus } from '@element-plus/icons-vue';
  101. import PushObject from '../../components/PushObject.vue';
  102. import CardLayout from './CardLayout.vue';
  103. import { RuleFormView, MessageTypeEnum, RuleFormAdd } from '../type';
  104. import { messageTypeOptions, pushChannelName } from '../../constant';
  105. import urlJoin from 'url-join';
  106. import { useGlobSetting } from '@/hooks/setting';
  107. import { getHeaders } from '@/utils/http/axios';
  108. import { isEmpty } from 'lodash-es';
  109. const { urlPrefix } = useGlobSetting();
  110. interface Props {
  111. dataSoure: RuleFormView;
  112. isDisabled: boolean;
  113. }
  114. const props = defineProps<Props>();
  115. const route = useRoute();
  116. const pageScopedDisabled = computed(() => props.isDisabled === false || route.query.viewModel === 'edit');
  117. const labelPosition = ref<FormProps['labelPosition']>('left');
  118. const childFromRef = ref<InstanceType<typeof PushObject>>();
  119. const actionUrl = computed(() => {
  120. return urlJoin(urlPrefix!, `/admin/minio/uploadFile`);
  121. });
  122. /**
  123. * 表单相关操作
  124. */
  125. type Rule = Omit<RuleFormView, 'introduction' | 'contentType' | 'content' | 'contentUrl' | 'operator'>;
  126. const formRules: FormRules<Rule> = {
  127. messageType: [{ required: true, trigger: 'change', message: '请选择消息样式' }],
  128. title: [{ required: true, trigger: 'change', message: '请输入消息标题' }],
  129. pushChannel: [{ required: true, trigger: 'change', message: '请选择推送渠道' }],
  130. recipientType: [{ required: true, trigger: 'change', message: '请选择推送对象' }],
  131. bannerUrl: [{ required: true, trigger: 'change', message: '请选择banner图片' }],
  132. };
  133. const ruleForm = ref<Rule>({
  134. messageType: MessageTypeEnum.TEXT,
  135. title: '',
  136. bannerUrl: '',
  137. pushChannel: [],
  138. expirationTime: '',
  139. recipientType: 1, // 默认全员
  140. userGroupList: [],
  141. customUserList: [],
  142. });
  143. const fileList = ref<UploadUserFile[]>([]);
  144. watch(
  145. () => props.dataSoure,
  146. (value) => {
  147. if (value) {
  148. ruleForm.value = { ...value };
  149. if (!isEmpty(value.bannerUrl)) {
  150. fileList.value = [
  151. {
  152. name: '',
  153. url: value.bannerUrl,
  154. },
  155. ];
  156. }
  157. }
  158. },
  159. {
  160. immediate: true,
  161. deep: true,
  162. },
  163. );
  164. const ruleFormRef = ref<FormInstance>();
  165. const isValidate = ref<boolean>();
  166. const validate = async () => {
  167. if (!ruleFormRef.value) return;
  168. try {
  169. const isSuccess = await ruleFormRef.value.validate();
  170. return isSuccess;
  171. } catch (error) {
  172. ElMessage.error('请完善信息填写');
  173. throw error;
  174. }
  175. };
  176. const handleAvatarSuccess: UploadProps['onSuccess'] = (response) => {
  177. // 注意,这里 response 是 没有被拦截处理
  178. ruleForm.value.bannerUrl = response.data.url;
  179. };
  180. // 上传前限制
  181. const beforeAvatarUpload: UploadProps['beforeUpload'] = (rawFile) => {
  182. if (!['image/jpeg', 'image/png', 'image/jpg'].includes(rawFile.type)) {
  183. ElMessage.error('文件格式仅支持:.jpg, .jpeg, .png');
  184. return false;
  185. } else if (rawFile.size / 1024 / 1024 > 20) {
  186. ElMessage.error('文件不支持超过 20MB!');
  187. return false;
  188. }
  189. return true;
  190. };
  191. // 图片预览
  192. const dialogImageUrl = ref('');
  193. const dialogVisible = ref(false);
  194. const handlePictureCardPreview: UploadProps['onPreview'] = (uploadFile) => {
  195. dialogImageUrl.value = uploadFile.url!;
  196. dialogVisible.value = true;
  197. };
  198. const upload = ref<UploadInstance>();
  199. const handleExceed: UploadProps['onExceed'] = (files) => {
  200. upload.value!.clearFiles();
  201. const file = files[0] as UploadRawFile;
  202. file.uid = genFileId();
  203. upload.value!.handleStart(file);
  204. };
  205. const buildFromData = () => {
  206. const childValue = childFromRef.value!.getChildValue();
  207. childFromRef.value!.submitForm().then((res) => {
  208. isValidate.value = res;
  209. });
  210. const basicInfo = { ...unref(ruleForm) } as RuleFormAdd;
  211. if (childValue) {
  212. basicInfo.recipientType = childValue.recipientType!;
  213. basicInfo.userGroupList = childValue.userGroupList!;
  214. basicInfo.customUserList = childValue.customUserList!;
  215. }
  216. return basicInfo;
  217. };
  218. const disabledDate = (time: Date) => {
  219. return time.getTime() < Date.now();
  220. };
  221. defineExpose({ validate, buildFromData });
  222. </script>
  223. <style scoped lang="scss">
  224. .avatar-uploader .avatar {
  225. width: 58px;
  226. height: 58px;
  227. display: block;
  228. }
  229. .avatar-uploader .el-upload {
  230. border: 1px dashed var(--el-border-color);
  231. border-radius: 6px;
  232. cursor: pointer;
  233. position: relative;
  234. overflow: hidden;
  235. transition: var(--el-transition-duration-fast);
  236. }
  237. .avatar-uploader .el-upload:hover {
  238. border-color: var(--el-color-primary);
  239. }
  240. .el-icon.avatar-uploader-icon {
  241. font-size: 28px;
  242. color: #8c939d;
  243. width: 58px;
  244. height: 58px;
  245. text-align: center;
  246. }
  247. .tip-message {
  248. margin-left: 20px;
  249. }
  250. </style>