edit.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  1. <template>
  2. <div class="plan-edit-container layout-pd">
  3. <el-card shadow="never" style="padding: 0 50px">
  4. <el-form :model="state.ruleForm" ref="ruleFormRef" label-width="120px" scroll-to-error>
  5. <el-row :gutter="35">
  6. <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
  7. <el-skeleton :loading="state.loading" animated>
  8. <template #template>
  9. <el-form-item label="预案分类">
  10. <el-skeleton-item variant="h1" />
  11. </el-form-item>
  12. </template>
  13. <template #default>
  14. <el-form-item label="预案分类" prop="planTypeId" :rules="[{ required: true, message: '请选择预案分类', trigger: 'change' }]">
  15. <VTreeDrop
  16. :data="state.typeData"
  17. checkable
  18. keyField="id"
  19. titleField="name"
  20. v-model="state.ruleForm.planTypeId"
  21. @checked-change="selectType"
  22. :dropHeight="400"
  23. dropPlaceholder="预案分类"
  24. dropdownWidthFixed
  25. clearable
  26. searchPlaceholder="预案分类名称"
  27. checkedButtonText="查看已选"
  28. :show-footer="false"
  29. :cascade="false"
  30. >
  31. </VTreeDrop>
  32. </el-form-item>
  33. </template>
  34. </el-skeleton>
  35. </el-col>
  36. <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
  37. <el-skeleton :loading="state.loading" animated>
  38. <template #template>
  39. <el-form-item>
  40. <template #label>
  41. <div style="height: 34px; display: flex; align-items: center">
  42. 失效时间
  43. <el-tooltip placement="top-start">
  44. <SvgIcon name="ele-QuestionFilled" size="18px" class="ml3" />
  45. <template #content> 不设置则代表永久有效;到达预设失效时间,预案将自动下架 </template>
  46. </el-tooltip>
  47. </div>
  48. </template>
  49. <el-skeleton-item variant="h1" />
  50. </el-form-item>
  51. </template>
  52. <template #default>
  53. <el-form-item label="失效时间" prop="expiredTime" :rules="[{ required: false, message: '请选择失效时间', trigger: 'change' }]">
  54. <template #label>
  55. <div style="height: 34px; display: flex; align-items: center">
  56. 失效时间
  57. <el-tooltip placement="top-start">
  58. <SvgIcon name="ele-QuestionFilled" size="18px" class="ml3" />
  59. <template #content> 不设置则代表永久有效;到达预设失效时间,预案将自动下架 </template>
  60. </el-tooltip>
  61. </div>
  62. </template>
  63. <el-date-picker
  64. v-model="state.ruleForm.expiredTime"
  65. type="datetime"
  66. placeholder="请选择失效时间"
  67. class="w100"
  68. value-format="YYYY-MM-DD[T]HH:mm:ss"
  69. :disabled-date="disabledDate"
  70. popper-class="no-atTheMoment"
  71. />
  72. </el-form-item>
  73. </template>
  74. </el-skeleton>
  75. </el-col>
  76. <el-col :xs="24" :sm="24" :md="12" :lg="12" :xl="8">
  77. <el-skeleton :loading="state.loading" animated>
  78. <template #template>
  79. <el-form-item label="热点分类">
  80. <el-skeleton-item variant="h1" />
  81. </el-form-item>
  82. </template>
  83. <template #default>
  84. <el-form-item label="热点分类" prop="hotspotId" :rules="[{ required: false, message: '请选择热点分类', trigger: 'change' }]">
  85. <hot-spot-select v-model="state.ruleForm.hotspotId" v-model:externalArr="state.hotspotExternal" @change="chooseHotSpot" clearable />
  86. </el-form-item>
  87. </template>
  88. </el-skeleton>
  89. </el-col>
  90. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  91. <el-skeleton :loading="state.loading" animated>
  92. <template #template>
  93. <el-form-item label="预案标题">
  94. <el-skeleton-item variant="h1" />
  95. </el-form-item>
  96. </template>
  97. <template #default>
  98. <el-form-item label="预案标题" prop="title" :rules="[{ required: true, validator: validatePassTitle, trigger: 'blur' }]">
  99. <el-input v-model="state.ruleForm.title" placeholder="请填写预案标题" clearable @blur="isRepeat('title')"></el-input>
  100. </el-form-item>
  101. </template>
  102. </el-skeleton>
  103. </el-col>
  104. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  105. <el-skeleton :loading="state.loading" animated>
  106. <template #template>
  107. <el-form-item label="预案内容">
  108. <el-skeleton-item variant="h1" />
  109. </el-form-item>
  110. </template>
  111. <template #default>
  112. <el-form-item label="预案内容" prop="content" :rules="[{ required: true, validator: validatePassContent, trigger: 'blur' }]">
  113. <editor
  114. v-model:get-html="state.ruleForm.content"
  115. :disable="state.disable"
  116. placeholder="请填写预案内容"
  117. :defaultContent="defaultContent"
  118. ref="editorRef"
  119. @blur="isRepeat('content')"
  120. />
  121. </el-form-item>
  122. </template>
  123. </el-skeleton>
  124. </el-col>
  125. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  126. <el-skeleton :loading="state.loading" animated>
  127. <template #template>
  128. <el-form-item label="附件">
  129. <el-skeleton-item variant="h1" />
  130. </el-form-item>
  131. </template>
  132. <template #default>
  133. <el-form-item label="附件" prop="files" :rules="[{ required: false, message: '请选择附件', trigger: 'change' }]">
  134. <annex-list
  135. name="预案附件"
  136. v-model="state.ruleForm.files"
  137. v-model:format="filesFormat"
  138. :businessId="state.ruleForm.id"
  139. classify="预案上传"
  140. />
  141. </el-form-item>
  142. </template>
  143. </el-skeleton>
  144. </el-col>
  145. <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
  146. <el-form-item>
  147. <el-button type="primary" @click="onSubmitReview(ruleFormRef)" :loading="state.loading">提交审核</el-button>
  148. <el-button class="default-button" @click="onPreview" :loading="state.loading">预览 </el-button>
  149. <el-button @click="onSaveOnly(ruleFormRef)" class="default-button" :loading="state.loading">保存为草稿</el-button>
  150. <el-button class="default-button" @click="onCancel" :loading="state.loading">取消 </el-button>
  151. </el-form-item>
  152. </el-col>
  153. </el-row>
  154. </el-form>
  155. </el-card>
  156. <!-- 删除或者更新提交审核 -->
  157. <edit-submit-audit ref="editSubmitAuditRef" @updateList="processSuccess" />
  158. </div>
  159. </template>
  160. <script setup lang="ts" name="planEdit">
  161. import { defineAsyncComponent, onMounted, reactive, ref } from 'vue';
  162. import type { FormInstance } from 'element-plus';
  163. import { ElMessage } from 'element-plus';
  164. import mittBus from '@/utils/mitt';
  165. import { useRoute, useRouter } from 'vue-router';
  166. import { storeToRefs } from 'pinia';
  167. import { useUserInfo } from '@/stores/userInfo';
  168. import { Local } from '@/utils/storage';
  169. import other from '@/utils/other';
  170. import { throttle, transformFile } from '@/utils/tools';
  171. import { disabledDate } from '@/utils/constants';
  172. import { VTreeDrop } from '@wsfe/vue-tree';
  173. import { planTreeList } from '@/api/plan/type';
  174. import { addPlanDraft, addPlanDraftAudit, editPlanDraft, planDetail, planRepeat } from '@/api/plan';
  175. // 引入组件
  176. const Editor = defineAsyncComponent(() => import('@/components/Editor/index.vue')); // 富文本编辑器
  177. const AnnexList = defineAsyncComponent(() => import('@/components/AnnexList/index.vue')); // 附件组件
  178. const EditSubmitAudit = defineAsyncComponent(() => import('@/views/plan/index/components/Edit-submit-audit.vue')); // 删除或者更新 提交审核
  179. const HotSpotSelect = defineAsyncComponent(() => import('@/components/Hotspot/index.vue')); // 选择热点
  180. const stores = useUserInfo(); // 用户信息
  181. const { userInfos } = storeToRefs(stores); // 用户信息
  182. // 定义变量内容
  183. const state = reactive<any>({
  184. dialogVisible: false,
  185. ruleForm: {
  186. attribution: '中心预案库', // 预案归属
  187. isPublic: true, // 是否公开
  188. files: [], // 附件
  189. content: '', // 内容
  190. title: null, // 标题
  191. },
  192. typeData: [], // 分类
  193. loading: false,
  194. hotspotExternal: [],
  195. });
  196. const ruleFormRef = ref<any>(); // 表单ref
  197. const validatePassTitle = (rule: any, value: any, callback: any) => {
  198. if (value === '' || value === null) {
  199. callback(new Error('请填写案例标题'));
  200. } else if (Repeat.value) {
  201. callback(new Error('有相似标题,请检查!'));
  202. } else {
  203. callback();
  204. }
  205. };
  206. const validatePassContent = (rule: any, value: any, callback: any) => {
  207. if (value === '' || value === null || value === '<p style="line-height: 2;"><br></p>') {
  208. callback(new Error('请填写案例内容'));
  209. } else if (Repeat.value) {
  210. callback(new Error('有相似内容,请检查!'));
  211. } else {
  212. callback();
  213. }
  214. };
  215. // 校验标题/摘要/内容是否重复 根据输入标题检测出关键词填入
  216. const Repeat = ref<boolean>(false);
  217. const isRepeat = (type: string) => {
  218. if (state.ruleForm[type]) {
  219. planRepeat({ [type]: state.ruleForm[type], id: state.ruleForm.id })
  220. .then((res: any) => {
  221. Repeat.value = res.result;
  222. ruleFormRef.value.validateField(type);
  223. })
  224. .catch(() => {
  225. state.ruleForm[type] = '';
  226. });
  227. }
  228. };
  229. // 默认配置
  230. const defaultContent = ref([
  231. {
  232. type: 'paragraph',
  233. children: [{ text: '', fontFamily: '仿宋', fontSize: '20px' }],
  234. lineHeight: '2',
  235. },
  236. ]);
  237. const selectType = (value: any) => {
  238. state.ruleForm.planTypes = value.map((item: any) => {
  239. return { planTypeName: item.name, planTypeId: item.id, planTypeSpliceName: item.spliceName };
  240. // return { name: item.name, id: item.id, spliceName: item.spliceName };
  241. });
  242. state.ruleForm.planTypeId = value.map((item: any) => item.id);
  243. ruleFormRef.value.validateField('planTypeId');
  244. };
  245. // 提交审核
  246. const route = useRoute(); // 获取路由
  247. const router = useRouter(); // 路由跳转
  248. const filesFormat = ref<EmptyArrayType>([]); // 附件列表格式化
  249. const editSubmitAuditRef = ref<RefType>(); // 删除或者更新 提交审核
  250. const onSubmitReview = async (formEl: FormInstance | undefined) => {
  251. if (!formEl) return;
  252. await formEl.validate((valid: boolean) => {
  253. if (!valid) return;
  254. state.ruleForm.files = filesFormat.value;
  255. const submitObj = other.deepClone(state.ruleForm);
  256. Reflect.deleteProperty(submitObj, 'creationTime');
  257. //如果已经有ID 说明是已经提交过的数据 更新审核
  258. if (route.params.id) {
  259. editSubmitAuditRef.value.openDialog(submitObj, 'update');
  260. } else {
  261. state.loading = true;
  262. addPlanDraftAudit(submitObj)
  263. .then(() => {
  264. ElMessage.success('新增预案审核提交成功');
  265. processSuccess();
  266. })
  267. .catch(() => {
  268. state.loading = false;
  269. });
  270. state.loading = false;
  271. }
  272. });
  273. };
  274. // 流程提交成功
  275. const processSuccess = () => {
  276. // 关闭当前 tagsView
  277. mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
  278. mittBus.emit('clearCache', 'planManage');
  279. router.push({
  280. path: '/plan/index',
  281. });
  282. };
  283. // 预览
  284. const onPreview = () => {
  285. if (route.params.id) {
  286. /* empty */
  287. } else {
  288. state.ruleForm.creatorName = userInfos.value?.name ?? '';
  289. state.ruleForm.creationTime = new Date();
  290. state.ruleForm.creatorOrgName = userInfos.value?.orgName ?? '';
  291. }
  292. Local.set('planPreviewForm', state.ruleForm);
  293. router.push({
  294. name: 'planPreview',
  295. params: {
  296. tagsViewName: '案例预览',
  297. id: '0',
  298. },
  299. });
  300. };
  301. // 选择热点分类
  302. const chooseHotSpot = (val: any, node: any, externalArr: any) => {
  303. if (val) {
  304. state.ruleForm.hotspotSpliceName = val.hotSpotFullName; // 热点分类拼接名称
  305. state.ruleForm.hotspotName = val.hotSpotName; // 热点分类名称
  306. state.ruleForm.hotspotCode = val.provinceCode; // 热点分类code
  307. state.ruleForm.hotspotExternal = externalArr.join(',') ?? ''; // 热点分类默认展开项
  308. } else {
  309. state.ruleForm.hotspotSpliceName = null;
  310. state.ruleForm.hotspotName = null;
  311. state.ruleForm.hotspotCode = null;
  312. state.ruleForm.hotspotExternal = null;
  313. }
  314. };
  315. // 保存到草稿箱
  316. const onSaveOnly = throttle(async (formEl: FormInstance | undefined) => {
  317. if (!formEl) return;
  318. await formEl.validate((valid: boolean) => {
  319. if (!valid) return;
  320. state.loading = true;
  321. state.ruleForm.files = filesFormat.value;
  322. const submitObj = other.deepClone(state.ruleForm);
  323. Reflect.deleteProperty(submitObj, 'creationTime');
  324. if (route.params.id) {
  325. // 更新
  326. editPlanDraft(submitObj)
  327. .then(handleSuccess)
  328. .catch(() => {
  329. state.loading = false;
  330. });
  331. } else {
  332. // 新增
  333. addPlanDraft(submitObj)
  334. .then(handleSuccess)
  335. .catch(() => {
  336. state.loading = false;
  337. });
  338. }
  339. });
  340. }, 300);
  341. // 取消
  342. const onCancel = () => {
  343. state.loading = true;
  344. mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
  345. mittBus.emit('clearCache', 'planManage');
  346. router.push({
  347. path: '/plan/index',
  348. });
  349. state.loading = false;
  350. };
  351. const handleSuccess = () => {
  352. state.loading = false;
  353. ElMessage.success('操作成功');
  354. // 关闭当前 tagsView
  355. mittBus.emit('onCurrentContextmenuClick', Object.assign({}, { contextMenuClickId: 1, ...route }));
  356. mittBus.emit('clearCache', 'planManage');
  357. router.push({
  358. path: '/plan/index',
  359. });
  360. };
  361. // 获取分类
  362. const getType = async () => {
  363. state.loading = true;
  364. try {
  365. const [typeDataRes] = await Promise.all([planTreeList({ IsEnable: true })]);
  366. state.typeData = typeDataRes.result ?? [];
  367. state.loading = false;
  368. } catch (error) {
  369. console.log(error);
  370. state.loading = false;
  371. }
  372. };
  373. const editorRef = ref<RefType>();
  374. const getDetail = async () => {
  375. if (route.params.id) {
  376. const res: any = await planDetail({ id: route.params.id }); //详情
  377. state.ruleForm = res.result ?? {};
  378. if (state.ruleForm.hotspotExternal) {
  379. //热点分类默认展开
  380. state.hotspotExternal = state.ruleForm.hotspotExternal.split(',');
  381. }
  382. state.ruleForm.files = transformFile(state.ruleForm.files);
  383. state.ruleForm.planTypeId = state.ruleForm.planTypes?.map((item: any) => item.planTypeId);
  384. }
  385. };
  386. onMounted(async () => {
  387. await getType();
  388. await getDetail();
  389. });
  390. </script>
  391. <style lang="scss">
  392. .vtree-tree-drop__wrapper {
  393. width: 100%;
  394. }
  395. </style>