国外MOSE官网
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

230 lines
5.5 KiB

3 months ago
  1. <script setup lang="ts">
  2. import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
  3. import { Icon } from '@iconify/vue';
  4. // 定义弹窗的属性
  5. const props = defineProps({
  6. // 是否显示弹窗
  7. visible: {
  8. type: Boolean,
  9. default: false
  10. },
  11. // 弹窗标题
  12. title: {
  13. type: String,
  14. default: ''
  15. },
  16. // 弹窗内容
  17. content: {
  18. type: String,
  19. default: ''
  20. },
  21. // 弹窗图片
  22. image: {
  23. type: String,
  24. default: ''
  25. },
  26. // 来源信息(适用于媒体报道)
  27. source: {
  28. type: String,
  29. default: ''
  30. },
  31. // 日期信息
  32. date: {
  33. type: String,
  34. default: ''
  35. },
  36. // 弹窗类型(post 或 media)
  37. type: {
  38. type: String,
  39. default: 'post'
  40. }
  41. });
  42. // 弹窗动画状态
  43. const modalOpen = ref(false);
  44. const modalReady = ref(false);
  45. // 监听visible属性变化
  46. watch(() => props.visible, (newVal) => {
  47. if (newVal) {
  48. document.body.classList.add('overflow-hidden');
  49. modalReady.value = true;
  50. setTimeout(() => {
  51. modalOpen.value = true;
  52. }, 50);
  53. } else {
  54. modalOpen.value = false;
  55. setTimeout(() => {
  56. modalReady.value = false;
  57. document.body.classList.remove('overflow-hidden');
  58. }, 300);
  59. }
  60. }, { immediate: true });
  61. // 定义关闭弹窗的事件
  62. const emit = defineEmits(['close']);
  63. const closeModal = () => {
  64. emit('close');
  65. };
  66. // 点击弹窗外部关闭弹窗
  67. const handleOutsideClick = (e: MouseEvent) => {
  68. const modal = document.querySelector('.modal-content');
  69. if (modal && !modal.contains(e.target as Node)) {
  70. closeModal();
  71. }
  72. };
  73. // 按ESC键关闭弹窗
  74. const handleEscKey = (e: KeyboardEvent) => {
  75. if (e.key === 'Escape') {
  76. closeModal();
  77. }
  78. };
  79. // 添加事件监听器
  80. onMounted(() => {
  81. document.addEventListener('mousedown', handleOutsideClick);
  82. document.addEventListener('keydown', handleEscKey);
  83. });
  84. // 移除事件监听器
  85. onBeforeUnmount(() => {
  86. document.removeEventListener('mousedown', handleOutsideClick);
  87. document.removeEventListener('keydown', handleEscKey);
  88. });
  89. // 添加默认导出
  90. defineExpose({
  91. closeModal
  92. });
  93. </script>
  94. <template>
  95. <div
  96. v-if="modalReady"
  97. class="fixed inset-0 z-50 flex items-center justify-center p-4 md:p-6"
  98. :class="{ 'modal-visible': modalOpen }"
  99. >
  100. <!-- 背景遮罩 -->
  101. <div
  102. class="modal-backdrop absolute inset-0 bg-black transition-opacity duration-300"
  103. :class="{ 'opacity-60': modalOpen, 'opacity-0': !modalOpen }"
  104. ></div>
  105. <!-- 弹窗内容 -->
  106. <div
  107. class="modal-content relative bg-background w-full max-w-3xl max-h-[90vh] overflow-y-auto rounded-xl shadow-2xl transform transition-all duration-300"
  108. :class="{ 'opacity-100 scale-100': modalOpen, 'opacity-0 scale-95': !modalOpen }"
  109. >
  110. <!-- 关闭按钮 -->
  111. <button
  112. @click="closeModal"
  113. class="absolute top-4 right-4 z-10 w-8 h-8 flex items-center justify-center rounded-full bg-background-dark/50 hover:bg-background-dark/80 text-text-secondary hover:text-text transition-colors duration-200"
  114. >
  115. <Icon icon="carbon:close" width="20" height="20" />
  116. </button>
  117. <!-- 弹窗头部 - 图片 -->
  118. <div class="relative w-full h-64 md:h-80 overflow-hidden">
  119. <img
  120. :src="image"
  121. :alt="title"
  122. class="w-full h-full object-cover"
  123. />
  124. <div class="absolute inset-0 bg-gradient-to-t from-background to-transparent"></div>
  125. <!-- 来源和日期信息 -->
  126. <div class="absolute bottom-4 left-4 flex flex-wrap gap-2">
  127. <div v-if="source" class="bg-black/50 backdrop-blur-sm px-3 py-1 rounded-lg text-white text-sm flex items-center">
  128. <Icon icon="carbon:document" class="mr-1.5" width="16" height="16" />
  129. {{ source }}
  130. </div>
  131. <div v-if="date" class="bg-black/50 backdrop-blur-sm px-3 py-1 rounded-lg text-white text-sm flex items-center">
  132. <Icon icon="carbon:calendar" class="mr-1.5" width="16" height="16" />
  133. {{ date }}
  134. </div>
  135. </div>
  136. </div>
  137. <!-- 弹窗内容 -->
  138. <div class="p-6 md:p-8">
  139. <h2 class="text-2xl md:text-3xl font-bold text-text mb-6">{{ title }}</h2>
  140. <div class="prose prose-sm md:prose-base max-w-none text-text-secondary" v-html="content"></div>
  141. <!-- 底部操作区 -->
  142. <div class="mt-8 flex justify-end">
  143. <button
  144. @click="closeModal"
  145. class="px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors duration-200"
  146. >
  147. 关闭
  148. </button>
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. </template>
  154. <style scoped>
  155. .modal-visible {
  156. animation: fadeIn 0.3s ease;
  157. }
  158. @keyframes fadeIn {
  159. from { opacity: 0; }
  160. to { opacity: 1; }
  161. }
  162. /* 自定义滚动条 */
  163. .modal-content {
  164. scrollbar-width: thin;
  165. scrollbar-color: var(--color-primary) transparent;
  166. }
  167. .modal-content::-webkit-scrollbar {
  168. width: 6px;
  169. }
  170. .modal-content::-webkit-scrollbar-track {
  171. background: transparent;
  172. }
  173. .modal-content::-webkit-scrollbar-thumb {
  174. background-color: var(--color-primary-light);
  175. border-radius: 20px;
  176. }
  177. /* 富文本内容样式 */
  178. :deep(.prose) {
  179. color: inherit;
  180. }
  181. :deep(.prose a) {
  182. color: var(--color-primary);
  183. text-decoration: none;
  184. }
  185. :deep(.prose a:hover) {
  186. text-decoration: underline;
  187. }
  188. :deep(.prose img) {
  189. border-radius: 0.5rem;
  190. }
  191. :deep(.prose h2) {
  192. color: var(--color-text);
  193. font-weight: 600;
  194. margin-top: 2em;
  195. margin-bottom: 1em;
  196. }
  197. :deep(.prose h3) {
  198. color: var(--color-text);
  199. font-weight: 600;
  200. margin-top: 1.5em;
  201. margin-bottom: 0.75em;
  202. }
  203. </style>