dongyukun
2024-11-08 0d21bc334ff8e175c6bd409f21b3ef8bbb7b2c87
提交 | 用户 | 时间
820397 1 <template>
H 2   <div class="my-process-designer">
3     <div class="my-process-designer__container">
4       <div class="my-process-designer__canvas" style="height: 760px" ref="bpmnCanvas"></div>
5     </div>
6   </div>
7 </template>
8
9 <script lang="ts" setup>
10 import BpmnViewer from 'bpmn-js/lib/Viewer'
11 import DefaultEmptyXML from './plugins/defaultEmpty'
12 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
13 import { formatDate } from '@/utils/formatTime'
14 import { isEmpty } from '@/utils/is'
15
16 defineOptions({ name: 'MyProcessViewer' })
17
18 const props = defineProps({
19   value: {
20     // BPMN XML 字符串
21     type: String,
22     default: ''
23   },
24   prefix: {
25     // 使用哪个引擎
26     type: String,
27     default: 'camunda'
28   },
29   activityData: {
30     // 活动的数据。传递时,可高亮流程
31     type: Array,
32     default: () => []
33   },
34   processInstanceData: {
35     // 流程实例的数据。传递时,可展示流程发起人等信息
36     type: Object,
37     default: () => {}
38   },
39   taskData: {
40     // 任务实例的数据。传递时,可展示 UserTask 审核相关的信息
41     type: Array,
42     default: () => []
43   }
44 })
45
46 provide('configGlobal', props)
47
48 const emit = defineEmits(['destroy'])
49
50 let bpmnModeler
51
52 const xml = ref('')
53 const activityLists = ref<any[]>([])
54 const processInstance = ref<any>(undefined)
55 const taskList = ref<any[]>([])
56 const bpmnCanvas = ref()
57 // const element = ref()
58 const elementOverlayIds = ref<any>(null)
59 const overlays = ref<any>(null)
60
61 const initBpmnModeler = () => {
62   if (bpmnModeler) return
63   bpmnModeler = new BpmnViewer({
64     container: bpmnCanvas.value,
65     bpmnRenderer: {}
66   })
67 }
68
69 /* 创建新的流程图 */
70 const createNewDiagram = async (xml) => {
71   // 将字符串转换成图显示出来
72   let newId = `Process_${new Date().getTime()}`
73   let newName = `业务流程_${new Date().getTime()}`
74   let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
75   try {
76     let { warnings } = await bpmnModeler.importXML(xmlString)
77     if (warnings && warnings.length) {
78       warnings.forEach((warn) => console.warn(warn))
79     }
80     // 高亮流程图
81     await highlightDiagram()
82     const canvas = bpmnModeler.get('canvas')
83     canvas.zoom('fit-viewport', 'auto')
84   } catch (e) {
85     console.error(e)
86     // console.error(`[Process Designer Warn]: ${e?.message || e}`);
87   }
88 }
89
90 /* 高亮流程图 */
91 // TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html
92 const highlightDiagram = async () => {
93   const activityList = activityLists.value
94   if (activityList.length === 0) {
95     return
96   }
97   // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现
98   // 再次基础上,增加不同审批结果的颜色等等
99   let canvas = bpmnModeler.get('canvas')
100   let todoActivity: any = activityList.find((m: any) => !m.endTime) // 找到待办的任务
101   let endActivity: any = activityList[activityList.length - 1] // 获得最后一个任务
102   let findProcessTask = false //是否已经高亮了进行中的任务
103   //进行中高亮之后的任务 key 集合,用于过滤掉 taskList 进行中后面的任务,避免进行中后面的数据 Hover 还有数据
104   let removeTaskDefinitionKeyList = []
105   // debugger
106   bpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach((n: any) => {
107     let activity: any = activityList.find((m: any) => m.key === n.id) // 找到对应的活动
108     if (!activity) {
109       return
110     }
111     if (n.$type === 'bpmn:UserTask') {
112       // 用户任务
113       // 处理用户任务的高亮
114       const task: any = taskList.value.find((m: any) => m.id === activity.taskId) // 找到活动对应的 taskId
115       if (!task) {
116         return
117       }
118       // 进行中的任务已经高亮过了,则不高亮后面的任务了
119       if (findProcessTask) {
120         removeTaskDefinitionKeyList.push(n.id)
121         return
122       }
123       // 高亮任务
124       canvas.addMarker(n.id, getResultCss(task.status))
125       //标记是否高亮了进行中任务
126       if (task.status === 1) {
127         findProcessTask = true
128       }
129       // 如果非通过,就不走后面的线条了
130       if (task.status !== 2) {
131         return
132       }
133       // 处理 outgoing 出线
134       const outgoing = getActivityOutgoing(activity)
135       outgoing?.forEach((nn: any) => {
136         // debugger
137         let targetActivity: any = activityList.find((m: any) => m.key === nn.targetRef.id)
138         // 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置
139         if (targetActivity) {
140           canvas.addMarker(nn.id, targetActivity.endTime ? 'highlight' : 'highlight-todo')
141         } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
142           // TODO 芋艿:这个流程,暂时没走到过
143           canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo')
144           canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo')
145         } else if (nn.targetRef.$type === 'bpmn:EndEvent') {
146           // TODO 芋艿:这个流程,暂时没走到过
147           if (!todoActivity && endActivity.key === n.id) {
148             canvas.addMarker(nn.id, 'highlight')
149             canvas.addMarker(nn.targetRef.id, 'highlight')
150           }
151           if (!activity.endTime) {
152             canvas.addMarker(nn.id, 'highlight-todo')
153             canvas.addMarker(nn.targetRef.id, 'highlight-todo')
154           }
155         }
156       })
157     } else if (n.$type === 'bpmn:ExclusiveGateway') {
158       // 排它网关
159       // 设置【bpmn:ExclusiveGateway】排它网关的高亮
160       canvas.addMarker(n.id, getActivityHighlightCss(activity))
161       // 查找需要高亮的连线
162       let matchNN: any = undefined
163       let matchActivity: any = undefined
164       n.outgoing?.forEach((nn: any) => {
165         let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
166         if (!targetActivity) {
167           return
168         }
169         // 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径:
170         //  1. 一个是 UserTask => EndEvent
171         //  2. 一个是 EndEvent
172         // 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。
173         // 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~
174         if (!matchActivity || matchActivity.type === 'endEvent') {
175           matchNN = nn
176           matchActivity = targetActivity
177         }
178       })
179       if (matchNN && matchActivity) {
180         canvas.addMarker(matchNN.id, getActivityHighlightCss(matchActivity))
181       }
182     } else if (n.$type === 'bpmn:ParallelGateway') {
183       // 并行网关
184       // 设置【bpmn:ParallelGateway】并行网关的高亮
185       canvas.addMarker(n.id, getActivityHighlightCss(activity))
186       n.outgoing?.forEach((nn: any) => {
187         // 获得连线是否有指向目标。如果有,则进行高亮
188         const targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
189         if (targetActivity) {
190           canvas.addMarker(nn.id, getActivityHighlightCss(targetActivity)) // 高亮【bpmn:SequenceFlow】连线
191           // 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。
192           canvas.addMarker(nn.targetRef.id, getActivityHighlightCss(targetActivity))
193         }
194       })
195     } else if (n.$type === 'bpmn:StartEvent') {
196       // 开始节点
197       canvas.addMarker(n.id, 'highlight')
198       n.outgoing?.forEach((nn) => {
199         // outgoing 例如说【bpmn:SequenceFlow】连线
200         // 获得连线是否有指向目标。如果有,则进行高亮
201         let targetActivity = activityList.find((m: any) => m.key === nn.targetRef.id)
202         if (targetActivity) {
203           canvas.addMarker(nn.id, 'highlight') // 高亮【bpmn:SequenceFlow】连线
204           canvas.addMarker(n.id, 'highlight') // 高亮【bpmn:StartEvent】开始节点(自己)
205         }
206       })
207     } else if (n.$type === 'bpmn:EndEvent') {
208       // 结束节点
209       if (!processInstance.value || processInstance.value.status === 1) {
210         return
211       }
212       canvas.addMarker(n.id, getResultCss(processInstance.value.status))
213     } else if (n.$type === 'bpmn:ServiceTask') {
214       //服务任务
215       if (activity.startTime > 0 && activity.endTime === 0) {
216         //进入执行,标识进行色
217         canvas.addMarker(n.id, getResultCss(1))
218       }
219       if (activity.endTime > 0) {
220         // 执行完成,节点标识完成色, 所有outgoing标识完成色。
221         canvas.addMarker(n.id, getResultCss(2))
222         const outgoing = getActivityOutgoing(activity)
223         outgoing?.forEach((out) => {
224           canvas.addMarker(out.id, getResultCss(2))
225         })
226       }
227     } else if (n.$type === 'bpmn:SequenceFlow') {
228       let targetActivity = activityList.find((m: any) => m.key === n.targetRef.id)
229       if (targetActivity) {
230         canvas.addMarker(n.id, getActivityHighlightCss(targetActivity))
231       }
232     }
233   })
234   if (!isEmpty(removeTaskDefinitionKeyList)) {
235     taskList.value = taskList.value.filter(
236       (item) => !removeTaskDefinitionKeyList.includes(item.taskDefinitionKey)
237     )
238   }
239 }
240
241 const getActivityHighlightCss = (activity) => {
242   return activity.endTime ? 'highlight' : 'highlight-todo'
243 }
244
245 const getResultCss = (status) => {
246   if (status === 1) {
247     // 审批中
248     return 'highlight-todo'
249   } else if (status === 2) {
250     // 已通过
251     return 'highlight'
252   } else if (status === 3) {
253     // 不通过
254     return 'highlight-reject'
255   } else if (status === 4) {
256     // 已取消
257     return 'highlight-cancel'
258   } else if (status === 5) {
259     // 退回
260     return 'highlight-return'
261   } else if (status === 6) {
262     // 委派
263     return 'highlight-todo'
264   } else if (status === 7) {
265     // 审批通过中
266     return 'highlight-todo'
267   } else if (status === 0) {
268     // 待审批
269     return 'highlight-todo'
270   }
271   return ''
272 }
273
274 const getActivityOutgoing = (activity) => {
275   // 如果有 outgoing,则直接使用它
276   if (activity.outgoing && activity.outgoing.length > 0) {
277     return activity.outgoing
278   }
279   // 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing
280   const flowElements = bpmnModeler.getDefinitions().rootElements[0].flowElements
281   const outgoing: any[] = []
282   flowElements.forEach((item: any) => {
283     if (item.$type !== 'bpmn:SequenceFlow') {
284       return
285     }
286     if (item.sourceRef.id === activity.key) {
287       outgoing.push(item)
288     }
289   })
290   return outgoing
291 }
292 const initModelListeners = () => {
293   const EventBus = bpmnModeler.get('eventBus')
294   // 注册需要的监听事件
295   EventBus.on('element.hover', function (eventObj) {
296     let element = eventObj ? eventObj.element : null
297     elementHover(element)
298   })
299   EventBus.on('element.out', function (eventObj) {
300     let element = eventObj ? eventObj.element : null
301     elementOut(element)
302   })
303 }
304 // 流程图的元素被 hover
305 const elementHover = (element) => {
306   element.value = element
307   !elementOverlayIds.value && (elementOverlayIds.value = {})
308   !overlays.value && (overlays.value = bpmnModeler.get('overlays'))
309   // 展示信息
310   // console.log(activityLists.value, 'activityLists.value')
311   // console.log(element.value, 'element.value')
312   const activity = activityLists.value.find((m) => m.key === element.value.id)
313   // console.log(activity, 'activityactivityactivityactivity')
314   if (!activity) {
315     return
316   }
317   if (!elementOverlayIds.value[element.value.id] && element.value.type !== 'bpmn:Process') {
318     let html = `<div class="element-overlays">
319             <p>Elemet id: ${element.value.id}</p>
320             <p>Elemet type: ${element.value.type}</p>
321           </div>` // 默认值
322     if (element.value.type === 'bpmn:StartEvent' && processInstance.value) {
323       html = `<p>发起人:${processInstance.value.startUser.nickname}</p>
324                   <p>部门:${processInstance.value.startUser.deptName}</p>
325                   <p>创建时间:${formatDate(processInstance.value.createTime)}`
326     } else if (element.value.type === 'bpmn:UserTask') {
327       let task = taskList.value.find((m) => m.id === activity.taskId) // 找到活动对应的 taskId
328       if (!task) {
329         return
330       }
331       let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
332       let dataResult = ''
333       optionData.forEach((element) => {
334         if (element.value == task.status) {
335           dataResult = element.label
336         }
337       })
338       html = `<p>审批人:${task.assigneeUser.nickname}</p>
339                   <p>部门:${task.assigneeUser.deptName}</p>
340                   <p>结果:${dataResult}</p>
341                   <p>创建时间:${formatDate(task.createTime)}</p>`
342       // html = `<p>审批人:${task.assigneeUser.nickname}</p>
343       //             <p>部门:${task.assigneeUser.deptName}</p>
344       //             <p>结果:${getIntDictOptions(
345       //               DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
346       //               task.status
347       //             )}</p>
348       //             <p>创建时间:${formatDate(task.createTime)}</p>`
349       if (task.endTime) {
350         html += `<p>结束时间:${formatDate(task.endTime)}</p>`
351       }
352       if (task.reason) {
353         html += `<p>审批建议:${task.reason}</p>`
354       }
355     } else if (element.value.type === 'bpmn:ServiceTask' && processInstance.value) {
356       if (activity.startTime > 0) {
357         html = `<p>创建时间:${formatDate(activity.startTime)}</p>`
358       }
359       if (activity.endTime > 0) {
360         html += `<p>结束时间:${formatDate(activity.endTime)}</p>`
361       }
362       console.log(html)
363     } else if (element.value.type === 'bpmn:EndEvent' && processInstance.value) {
364       let optionData = getIntDictOptions(DICT_TYPE.BPM_TASK_STATUS)
365       let dataResult = ''
366       optionData.forEach((element) => {
367         if (element.value == processInstance.value.status) {
368           dataResult = element.label
369         }
370       })
371       html = `<p>结果:${dataResult}</p>`
372       // html = `<p>结果:${getIntDictOptions(
373       //   DICT_TYPE.BPM_PROCESS_INSTANCE_RESULT,
374       //   processInstance.value.status
375       // )}</p>`
376       if (processInstance.value.endTime) {
377         html += `<p>结束时间:${formatDate(processInstance.value.endTime)}</p>`
378       }
379     }
380     // console.log(html, 'html111111111111111')
381     elementOverlayIds.value[element.value.id] = toRaw(overlays.value)?.add(element.value, {
382       position: { left: 0, bottom: 0 },
383       html: `<div class="element-overlays">${html}</div>`
384     })
385   }
386 }
387
388 // 流程图的元素被 out
389 const elementOut = (element) => {
390   toRaw(overlays.value).remove({ element })
391   elementOverlayIds.value[element.id] = null
392 }
393
394 onMounted(() => {
395   xml.value = props.value
396   activityLists.value = props.activityData
397   // 初始化
398   initBpmnModeler()
399   createNewDiagram(xml.value)
400   // 初始模型的监听器
401   initModelListeners()
402 })
403
404 onBeforeUnmount(() => {
405   // this.$once('hook:beforeDestroy', () => {
406   // })
407   if (bpmnModeler) bpmnModeler.destroy()
408   emit('destroy', bpmnModeler)
409   bpmnModeler = null
410 })
411
412 watch(
413   () => props.value,
414   (newValue) => {
415     xml.value = newValue
416     createNewDiagram(xml.value)
417   }
418 )
419 watch(
420   () => props.activityData,
421   (newActivityData) => {
422     activityLists.value = newActivityData
423     createNewDiagram(xml.value)
424   }
425 )
426 watch(
427   () => props.processInstanceData,
428   (newProcessInstanceData) => {
429     processInstance.value = newProcessInstanceData
430     createNewDiagram(xml.value)
431   }
432 )
433 watch(
434   () => props.taskData,
435   (newTaskListData) => {
436     taskList.value = newTaskListData
437     createNewDiagram(xml.value)
438   }
439 )
440 </script>
441
442 <style lang="scss">
443 /** 处理中 */
444 .highlight-todo.djs-connection > .djs-visual > path {
445   stroke: #1890ff !important;
446   stroke-dasharray: 4px !important;
447   fill-opacity: 0.2 !important;
448 }
449
450 .highlight-todo.djs-shape .djs-visual > :nth-child(1) {
451   fill: #1890ff !important;
452   stroke: #1890ff !important;
453   stroke-dasharray: 4px !important;
454   fill-opacity: 0.2 !important;
455 }
456
457 :deep(.highlight-todo.djs-connection > .djs-visual > path) {
458   stroke: #1890ff !important;
459   stroke-dasharray: 4px !important;
460   fill-opacity: 0.2 !important;
461   marker-end: url('#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr');
462 }
463
464 :deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
465   fill: #1890ff !important;
466   stroke: #1890ff !important;
467   stroke-dasharray: 4px !important;
468   fill-opacity: 0.2 !important;
469 }
470
471 /** 通过 */
472 .highlight.djs-shape .djs-visual > :nth-child(1) {
473   fill: green !important;
474   stroke: green !important;
475   fill-opacity: 0.2 !important;
476 }
477
478 .highlight.djs-shape .djs-visual > :nth-child(2) {
479   fill: green !important;
480 }
481
482 .highlight.djs-shape .djs-visual > path {
483   fill: green !important;
484   fill-opacity: 0.2 !important;
485   stroke: green !important;
486 }
487
488 .highlight.djs-connection > .djs-visual > path {
489   stroke: green !important;
490 }
491
492 .highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
493   fill: green !important; /* color elements as green */
494 }
495
496 :deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
497   fill: green !important;
498   stroke: green !important;
499   fill-opacity: 0.2 !important;
500 }
501
502 :deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
503   fill: green !important;
504 }
505
506 :deep(.highlight.djs-shape .djs-visual > path) {
507   fill: green !important;
508   fill-opacity: 0.2 !important;
509   stroke: green !important;
510 }
511
512 :deep(.highlight.djs-connection > .djs-visual > path) {
513   stroke: green !important;
514 }
515
516 .djs-element.highlight > .djs-visual > path {
517   stroke: green !important;
518 }
519
520 /** 不通过 */
521 .highlight-reject.djs-shape .djs-visual > :nth-child(1) {
522   fill: red !important;
523   stroke: red !important;
524   fill-opacity: 0.2 !important;
525 }
526
527 .highlight-reject.djs-shape .djs-visual > :nth-child(2) {
528   fill: red !important;
529 }
530
531 .highlight-reject.djs-shape .djs-visual > path {
532   fill: red !important;
533   fill-opacity: 0.2 !important;
534   stroke: red !important;
535 }
536
537 .highlight-reject.djs-connection > .djs-visual > path {
538   stroke: red !important;
539   marker-end: url(#sequenceflow-end-white-success) !important;
540 }
541
542 .highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
543   fill: red !important; /* color elements as green */
544 }
545
546 :deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) {
547   fill: red !important;
548   stroke: red !important;
549   fill-opacity: 0.2 !important;
550 }
551
552 :deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) {
553   fill: red !important;
554 }
555
556 :deep(.highlight-reject.djs-shape .djs-visual > path) {
557   fill: red !important;
558   fill-opacity: 0.2 !important;
559   stroke: red !important;
560 }
561
562 :deep(.highlight-reject.djs-connection > .djs-visual > path) {
563   stroke: red !important;
564 }
565
566 /** 已取消 */
567 .highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
568   fill: grey !important;
569   stroke: grey !important;
570   fill-opacity: 0.2 !important;
571 }
572
573 .highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
574   fill: grey !important;
575 }
576
577 .highlight-cancel.djs-shape .djs-visual > path {
578   fill: grey !important;
579   fill-opacity: 0.2 !important;
580   stroke: grey !important;
581 }
582
583 .highlight-cancel.djs-connection > .djs-visual > path {
584   stroke: grey !important;
585 }
586
587 .highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) {
588   fill: grey !important; /* color elements as green */
589 }
590
591 :deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) {
592   fill: grey !important;
593   stroke: grey !important;
594   fill-opacity: 0.2 !important;
595 }
596
597 :deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) {
598   fill: grey !important;
599 }
600
601 :deep(.highlight-cancel.djs-shape .djs-visual > path) {
602   fill: grey !important;
603   fill-opacity: 0.2 !important;
604   stroke: grey !important;
605 }
606
607 :deep(.highlight-cancel.djs-connection > .djs-visual > path) {
608   stroke: grey !important;
609 }
610
611 /** 回退 */
612 .highlight-return.djs-shape .djs-visual > :nth-child(1) {
613   fill: #e6a23c !important;
614   stroke: #e6a23c !important;
615   fill-opacity: 0.2 !important;
616 }
617
618 .highlight-return.djs-shape .djs-visual > :nth-child(2) {
619   fill: #e6a23c !important;
620 }
621
622 .highlight-return.djs-shape .djs-visual > path {
623   fill: #e6a23c !important;
624   fill-opacity: 0.2 !important;
625   stroke: #e6a23c !important;
626 }
627
628 .highlight-return.djs-connection > .djs-visual > path {
629   stroke: #e6a23c !important;
630 }
631
632 .highlight-return:not(.djs-connection) .djs-visual > :nth-child(1) {
633   fill: #e6a23c !important; /* color elements as green */
634 }
635
636 :deep(.highlight-return.djs-shape .djs-visual > :nth-child(1)) {
637   fill: #e6a23c !important;
638   stroke: #e6a23c !important;
639   fill-opacity: 0.2 !important;
640 }
641
642 :deep(.highlight-return.djs-shape .djs-visual > :nth-child(2)) {
643   fill: #e6a23c !important;
644 }
645
646 :deep(.highlight-return.djs-shape .djs-visual > path) {
647   fill: #e6a23c !important;
648   fill-opacity: 0.2 !important;
649   stroke: #e6a23c !important;
650 }
651
652 :deep(.highlight-return.djs-connection > .djs-visual > path) {
653   stroke: #e6a23c !important;
654 }
655
656 .element-overlays {
657   width: 200px;
658   padding: 8px;
659   color: #fafafa;
660   background: rgb(0 0 0 / 60%);
661   border-radius: 4px;
662   box-sizing: border-box;
663 }
664 </style>