| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| <template> |
| <a-affix v-if="showBanner" class="announcement-banner-container"> |
| <a-alert |
| :type="bannerConfig.type || 'default'" |
| :show-icon="bannerConfig.showIcon !== false" |
| :closable="bannerConfig.closable !== false" |
| :banner="true" |
| @close="handleClose" |
| :style="[ { border: borderColor }]" |
| > |
| <template #message> |
| <div class="banner-content" v-html="sanitizedMessage" :style="[$store.getters.darkMode ? { color: 'rgba(255, 255, 255, 0.65)' } : { color: '#888' }]" /> |
| </template> |
| </a-alert> |
| </a-affix> |
| </template> |
| |
| <script> |
| import DOMPurify from 'dompurify' |
| |
| export default { |
| name: 'AnnouncementBanner', |
| data () { |
| return { |
| showBanner: false, |
| bannerConfig: {}, |
| dismissed: false |
| } |
| }, |
| computed: { |
| sanitizedMessage () { |
| if (!this.bannerConfig.message) return '' |
| const cleanHTML = DOMPurify.sanitize(this.bannerConfig.message, { |
| ALLOWED_TAGS: [ |
| 'p', 'div', 'span', 'br', 'strong', 'b', 'em', 'i', 'u', |
| 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', |
| 'small', 'mark', 'del', 'ins', 'sub', 'sup' |
| ], |
| ALLOWED_ATTR: ['href', 'target', 'rel', 'class', 'id', 'style'], |
| ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp|xxx):|[^a-z]|[a-z+.-]+(?:[^a-z+.\-:]|$))/i, |
| FORBID_TAGS: ['script', 'object', 'embed', 'form', 'input', 'textarea', 'select', 'button'], |
| FORBID_ATTR: ['onclick', 'onload', 'onerror', 'onmouseover', 'onfocus', 'onblur'] |
| }) |
| return cleanHTML |
| }, |
| borderColor () { |
| const colorMap = { |
| error: '#ffa39e', |
| warning: '#ffe58f', |
| success: '#b7eb8f', |
| info: '#b3cde3' |
| } |
| const color = colorMap[this.bannerConfig.type] |
| return color ? `1px solid ${color}` : '0px' |
| } |
| }, |
| mounted () { |
| this.loadBannerConfig() |
| }, |
| methods: { |
| loadBannerConfig () { |
| const config = this.$config?.announcementBanner || {} |
| if (config && config.enabled && config.message) { |
| this.bannerConfig = config |
| if (config.persistDismissal) { |
| const dismissedKey = `cs-banner-dismissed-${this.getBannerHash()}` |
| this.dismissed = this.$localStorage.get(dismissedKey) === 'true' |
| } |
| if (!this.dismissed && this.isWithinDisplayPeriod()) { |
| this.showBanner = true |
| } |
| } |
| }, |
| isWithinDisplayPeriod () { |
| const config = this.bannerConfig |
| const now = new Date() |
| |
| if (config.startDate) { |
| const startDate = new Date(config.startDate) |
| if (now < startDate) return false |
| } |
| |
| if (config.endDate) { |
| const endDate = new Date(config.endDate) |
| if (now > endDate) return false |
| } |
| return true |
| }, |
| handleClose () { |
| this.showBanner = false |
| if (this.bannerConfig.persistDismissal) { |
| const dismissedKey = `cs-banner-dismissed-${this.getBannerHash()}` |
| this.$localStorage.set(dismissedKey, 'true') |
| } |
| if (this.bannerConfig.onClose) { |
| this.bannerConfig.onClose() |
| } |
| }, |
| getBannerHash () { |
| // Create a simple hash of the message content for dismissal tracking |
| let hash = 0 |
| const str = this.bannerConfig.message || '' |
| for (let i = 0; i < str.length; i++) { |
| const char = str.charCodeAt(i) |
| hash = ((hash << 5) - hash) + char |
| hash = hash & hash // Convert to 32bit integer |
| } |
| return Math.abs(hash).toString() |
| } |
| } |
| } |
| </script> |
| |
| <style scoped> |
| .announcement-banner-container { |
| z-index: 1000; |
| top: 0; |
| margin: 0; |
| width: 100%; |
| justify-content: center; |
| align-items: center; |
| } |
| |
| .banner-content { |
| line-height: 1.7; |
| text-align: center |
| } |
| </style> |