chenlei 1 年之前
父节点
当前提交
904ed94e4d

+ 2 - 0
components.d.ts

@@ -10,6 +10,8 @@ declare module 'vue' {
     Accessibility: typeof import('./src/components/Accessibility/index.vue')['default']
     Breadcrumb: typeof import('./src/components/Breadcrumb/index.vue')['default']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCarousel: typeof import('element-plus/es')['ElCarousel']
+    ElCarouselItem: typeof import('element-plus/es')['ElCarouselItem']
     ElCollapse: typeof import('element-plus/es')['ElCollapse']
     ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
     ElDatePicker: typeof import('element-plus/es')['ElDatePicker']

+ 3 - 0
src/api/collections.ts

@@ -3,6 +3,7 @@ import {
   requestByPost,
   type PaginationParams,
 } from "@dage/service";
+import type { FileItem } from ".";
 
 export interface CollectionThumbListItem {
   id: number;
@@ -27,6 +28,8 @@ export interface CollectionListItem {
 export interface CollectionDetail extends CollectionListItem {
   rtf: string;
   size: string;
+  author: string;
+  files: FileItem[];
 }
 
 export const getCollectionThumbListApi = () => {

+ 4 - 0
src/assets/css/base.css

@@ -226,6 +226,10 @@ a:hover {
   -webkit-line-clamp: 2;
 }
 
+.line-4 {
+  -webkit-line-clamp: 4;
+}
+
 .no-more {
   position: absolute;
   top: 50%;

文件差异内容过多而无法显示
+ 1423 - 1421
src/components/Accessibility/index.vue


+ 7 - 1
src/components/Breadcrumb/index.vue

@@ -29,7 +29,7 @@
     >
       {{ item.label }}>
     </RouterLink>
-    <span tabindex="0" :aria-description="curRoute?.name">
+    <span tabindex="0" :aria-description="curRoute?.name" class="limit-line">
       {{ curRoute?.name }}
     </span>
   </div>
@@ -50,6 +50,8 @@ defineProps<{
 
 <style lang="scss" scoped>
 .breadcrumb {
+  display: flex;
+  align-items: center;
   max-width: 1180px;
   height: 28px;
   line-height: 28px;
@@ -61,5 +63,9 @@ defineProps<{
   span {
     color: var(--black-text-color);
   }
+  .limit-line {
+    flex: 1;
+    width: 0;
+  }
 }
 </style>

+ 444 - 438
src/utils/index.js

@@ -1,438 +1,444 @@
-function mapTags(tag) {
-  let ret = "";
-  switch (tag) {
-    case "A":
-      ret = "Link";
-      break;
-    case "BUTTON":
-      ret = "Button";
-      break;
-    case "IMG":
-      ret = "Image";
-      break;
-    case "INPUT":
-      ret = "Textbox";
-      break;
-    case "TEXTAREA":
-      ret = "Textbox";
-      break;
-    default:
-      ret = "";
-      // ret = 'Text'
-      break;
-  }
-  return ret;
-}
-
-function extractTextForFocus(e) {
-  let meaningfulNode = e.target;
-
-  // 如果天然能focus,但没有被加上tabindex属性,比如focus到了第三方组件内部的可focus元素,直接返回。
-  if (
-    ["A", "AREA", "BUTTON", "INPUT", "SELECT", "IFRAME"].includes(
-      meaningfulNode.tagName
-    ) &&
-    !meaningfulNode.getAttribute("tabindex")
-  ) {
-    return;
-  }
-
-  while (
-    !meaningfulNode.getAttribute ||
-    !meaningfulNode.getAttribute("tabindex")
-  ) {
-    meaningfulNode = meaningfulNode.parentNode;
-    if (!meaningfulNode) {
-      return;
-    }
-  }
-
-  // 约定:tabindex属性值为-1的元素只用于在点击有data-aria-xxx-area attribute的区域包裹元素的子孙元素时,避免focus到区域包裹元素。
-  if (meaningfulNode.getAttribute("tabindex") === "-1") {
-    return;
-  }
-
-  let elemType = "";
-  const ariaLabel = meaningfulNode.getAttribute("aria-label");
-  if (ariaLabel !== null) {
-    elemType = ariaLabel;
-  } else {
-    elemType = mapTags(meaningfulNode.tagName);
-  }
-
-  let elemDisc = "";
-  const ariaDescription = meaningfulNode.getAttribute("aria-description");
-  if (ariaDescription !== null) {
-    elemDisc = ariaDescription;
-  } else {
-    elemDisc = meaningfulNode.innerText;
-  }
-
-  let elemAudioId = "";
-  const ariaElemAudioId = meaningfulNode.getAttribute("aria-audio-id");
-  if (ariaElemAudioId !== null) {
-    elemAudioId = ariaElemAudioId;
-  } else {
-    let ariaElemAudioId2 =
-      meaningfulNode.firstElementChild &&
-      meaningfulNode.firstElementChild.getAttribute("aria-audio-id");
-    if (ariaElemAudioId2) {
-      elemAudioId = ariaElemAudioId2;
-    }
-  }
-
-  return {
-    elemType,
-    elemDisc,
-    ariaNode: meaningfulNode,
-    elemAudioId,
-  };
-}
-
-let lastMeaningfulNode = null;
-function extractTextForMouseOver(e) {
-  let meaningfulNode = e.target;
-
-  while (
-    !meaningfulNode.getAttribute ||
-    !meaningfulNode.getAttribute("tabindex")
-  ) {
-    meaningfulNode = meaningfulNode.parentNode;
-    if (!meaningfulNode) {
-      lastMeaningfulNode = null;
-      return;
-    }
-  }
-
-  if (meaningfulNode.getAttribute("tabindex") === "-1") {
-    lastMeaningfulNode = null;
-    return;
-  }
-
-  // mouseover事件冒泡到有data-aria-xxx-area attribute的区域包裹元素时,不应该提取该区域包裹元素的无障碍辅助信息。
-  if (
-    meaningfulNode.getAttribute("data-aria-navigation-area") !== null ||
-    meaningfulNode.getAttribute("data-aria-viewport-area") !== null ||
-    meaningfulNode.getAttribute("data-aria-interaction-area") !== null
-  ) {
-    lastMeaningfulNode = null;
-    return;
-  }
-
-  // 如果只是在需要朗读的子元素之间进进出出,第一次需要朗读,以后就不需要朗读了。
-  let relatedMeaningfulNode = e.relatedTarget;
-  while (
-    relatedMeaningfulNode &&
-    (!relatedMeaningfulNode.getAttribute ||
-      !relatedMeaningfulNode.getAttribute("tabindex"))
-  ) {
-    relatedMeaningfulNode = relatedMeaningfulNode.parentNode;
-  }
-  if (
-    relatedMeaningfulNode === meaningfulNode &&
-    lastMeaningfulNode === meaningfulNode
-  ) {
-    return;
-  }
-
-  lastMeaningfulNode = meaningfulNode;
-
-  let elemType = "";
-  const ariaLabel = meaningfulNode.getAttribute("aria-label");
-  if (ariaLabel !== null) {
-    elemType = ariaLabel;
-  } else {
-    elemType = mapTags(meaningfulNode.tagName);
-  }
-
-  let elemDisc = "";
-  const ariaDescription = meaningfulNode.getAttribute("aria-description");
-  if (ariaDescription !== null) {
-    elemDisc = ariaDescription;
-  } else {
-    elemDisc = meaningfulNode.innerText;
-  }
-
-  let elemAudioId = "";
-  const ariaElemAudioId = meaningfulNode.getAttribute("aria-audio-id");
-  if (ariaElemAudioId !== null) {
-    elemAudioId = ariaElemAudioId;
-  } else {
-    let ariaElemAudioId2 =
-      meaningfulNode.firstElementChild &&
-      meaningfulNode.firstElementChild.getAttribute("aria-audio-id");
-    if (ariaElemAudioId2) {
-      elemAudioId = ariaElemAudioId2;
-    }
-  }
-
-  return {
-    elemType,
-    elemDisc,
-    ariaNode: meaningfulNode,
-    elemAudioId,
-  };
-}
-
-function isObject(p) {
-  return Object.prototype.toString.call(p) === "[object Object]";
-}
-
-// 判断两个对象内容是否相同
-function isSameObject(object1, object2) {
-  const keys1 = Object.keys(object1);
-  const keys2 = Object.keys(object2);
-
-  if (keys1.length !== keys2.length) {
-    return false;
-  }
-
-  for (let index = 0; index < keys1.length; index++) {
-    const val1 = object1[keys1[index]];
-    const val2 = object2[keys2[index]];
-    const areObjects = isObject(val1) && isObject(val2);
-    if (
-      (areObjects && !isSameObject(val1, val2)) ||
-      (!areObjects && val1 !== val2)
-    ) {
-      return false;
-    }
-  }
-  return true;
-}
-
-function getAndFocusNextNodeWithCustomAttribute(attriName) {
-  const startNode = document.activeElement || document.body;
-
-  const treeWalker = document.createTreeWalker(
-    document.body,
-    NodeFilter.SHOW_ELEMENT
-  );
-  treeWalker.currentNode = startNode;
-
-  let targetNode = null;
-
-  // eslint-disable-next-line
-  while (true) {
-    const nextNode = treeWalker.nextNode();
-    if (!nextNode) {
-      console.log("往下没找到");
-      break;
-    }
-    if (nextNode.dataset[attriName] !== undefined) {
-      console.log("往下找到了");
-      targetNode = nextNode;
-      break;
-    }
-  }
-
-  if (!targetNode && startNode !== document.body) {
-    treeWalker.currentNode = document.body;
-    // eslint-disable-next-line
-    while (true) {
-      const nextNode = treeWalker.nextNode();
-      if (!nextNode) {
-        console.log("往上也没找到");
-        break;
-      }
-      if (nextNode.dataset[attriName] !== undefined) {
-        console.log("往上找到了");
-        targetNode = nextNode;
-        break;
-      }
-    }
-  }
-
-  if (targetNode) {
-    // 如果重复选中某一区域,需要重新触发focus,所以先blur一下
-    if (document.activeElement === targetNode) {
-      targetNode.blur();
-    }
-    targetNode.focus();
-
-    // 如果无法focus,就强行focus
-    if (document.activeElement !== targetNode) {
-      targetNode.setAttribute("tabindex", "0");
-      targetNode.focus();
-    }
-  }
-
-  return targetNode;
-}
-
-function __focusNextFocusableNode(treeWalker) {
-  // eslint-disable-next-line
-  while (true) {
-    const nextNode = treeWalker.nextNode();
-    if (!nextNode) {
-      return false;
-    }
-    if (nextNode.focus) {
-      nextNode.focus();
-      if (document.activeElement === nextNode && nextNode.tabIndex !== -1) {
-        return true;
-      }
-    }
-  }
-}
-
-function iterateOnFocusableNode(startNode, focusedNodeHandler) {
-  const treeWalker = document.createTreeWalker(
-    document.body,
-    NodeFilter.SHOW_ELEMENT
-  );
-  treeWalker.currentNode = startNode;
-  treeWalker.currentNode.focus();
-  if (document.activeElement === treeWalker.currentNode) {
-    // console.log('起始节点可以focus')
-  } else {
-    // console.log('起始节点不可以focus,focus到下一节点。')
-    const ret = __focusNextFocusableNode(treeWalker);
-    if (!ret) {
-      return;
-    }
-  }
-
-  const iterator = () => {
-    focusedNodeHandler(treeWalker.currentNode)
-      .then(() => {
-        const result = __focusNextFocusableNode(treeWalker);
-        if (result) {
-          // console.log('遍历到下一个节点!')
-          iterator();
-        } else {
-          // console.log('遍历结束!')
-        }
-      })
-      .catch((e) => {
-        // console.log('遍历中止!', e)
-      });
-  };
-  iterator();
-}
-
-/**
- * 返回一个自带消抖效果的函数,用res表示。
- *
- * fn: 需要被消抖的函数
- * delay: 消抖时长
- * isImmediateCall: 是否在一组操作中的第一次调用时立即执行fn
- * isRememberLastCall:是否在一组中最后一次调用后等delay时长再执行fn
- */
-function debounce(
-  fn,
-  delay,
-  isImmediateCall = false,
-  isRememberLastCall = true
-) {
-  console.assert(
-    isImmediateCall || isRememberLastCall,
-    "isImmediateCall 和 isRememberLastCall 至少应有一个是true,否则没有意义!"
-  );
-  let timer = null;
-  // 上次调用的时刻
-  let lastCallTime = 0;
-
-  if (isImmediateCall && !isRememberLastCall) {
-    return function (...args) {
-      const currentTime = Date.now();
-      if (currentTime - lastCallTime >= delay) {
-        fn.apply(this, args);
-      }
-      lastCallTime = currentTime;
-    };
-  } else if (!isImmediateCall && isRememberLastCall) {
-    return function (...args) {
-      if (timer) {
-        clearTimeout(timer);
-      }
-      timer = setTimeout(() => {
-        fn.apply(this, args);
-      }, delay);
-    };
-  } else if (isImmediateCall && isRememberLastCall) {
-    return function (...args) {
-      const currentTime = Date.now();
-      if (currentTime - lastCallTime >= delay) {
-        // 一组操作中的第一次
-        fn.apply(this, args);
-        lastCallTime = currentTime;
-        return;
-      } else {
-        // 一组中的后续调用
-        if (timer) {
-          // 在此之前存在中间调用
-          lastCallTime = currentTime;
-          clearTimeout(timer);
-        }
-        timer = setTimeout(() => {
-          fn.apply(this, args);
-          lastCallTime = 0;
-          timer = null;
-        }, delay);
-      }
-    };
-  } else {
-    console.error("不应该执行到这里!");
-  }
-}
-
-class DebounceScheduler {
-  constructor(fn, delay, context, isImmediateCall = false) {
-    this.job = fn;
-    this.delay = delay;
-    this.context = context;
-    this.timer = null;
-    this.lastCallTime = 0;
-    this.isImmediateCall = isImmediateCall;
-  }
-  planToDo(...args) {
-    if (!this.isImmediateCall) {
-      if (this.timer) {
-        clearTimeout(this.timer);
-        this.timer = null;
-      }
-      this.timer = setTimeout(() => {
-        this.job.apply(this.context, args);
-      }, this.delay);
-    } else {
-      const currentTime = Date.now();
-      if (currentTime - this.lastCallTime >= this.delay) {
-        // 一组操作中的第一次
-        this.job.apply(this.context, args);
-        this.lastCallTime = currentTime;
-        return;
-      } else {
-        // 一组中的后续调用
-        if (this.timer) {
-          // 在此之前存在中间调用
-          this.lastCallTime = currentTime;
-          clearTimeout(this.timer);
-          this.timer = null;
-        }
-        this.timer = setTimeout(() => {
-          this.job.apply(this.context, args);
-          this.lastCallTime = 0;
-          this.timer = null;
-        }, this.delay);
-      }
-    }
-  }
-  cancel() {
-    if (this.timer) {
-      clearTimeout(this.timer);
-      this.timer = null;
-    }
-  }
-}
-
-export default {
-  mapTags,
-  extractTextForFocus,
-  extractTextForMouseOver,
-  isSameObject,
-  getAndFocusNextNodeWithCustomAttribute,
-  iterateOnFocusableNode,
-  debounce,
-  DebounceScheduler,
-};
+function mapTags(tag) {
+  let ret = "";
+  switch (tag) {
+    case "A":
+      ret = "Link";
+      break;
+    case "BUTTON":
+      ret = "Button";
+      break;
+    case "IMG":
+      ret = "Image";
+      break;
+    case "INPUT":
+      ret = "Textbox";
+      break;
+    case "TEXTAREA":
+      ret = "Textbox";
+      break;
+    default:
+      ret = "";
+      // ret = 'Text'
+      break;
+  }
+  return ret;
+}
+
+function extractTextForFocus(e) {
+  let meaningfulNode = e.target;
+
+  // 如果天然能focus,但没有被加上tabindex属性,比如focus到了第三方组件内部的可focus元素,直接返回。
+  if (
+    ["A", "AREA", "BUTTON", "INPUT", "SELECT", "IFRAME"].includes(
+      meaningfulNode.tagName
+    ) &&
+    !meaningfulNode.getAttribute("tabindex")
+  ) {
+    return;
+  }
+
+  while (
+    !meaningfulNode.getAttribute ||
+    !meaningfulNode.getAttribute("tabindex")
+  ) {
+    meaningfulNode = meaningfulNode.parentNode;
+    if (!meaningfulNode) {
+      return;
+    }
+  }
+
+  // 约定:tabindex属性值为-1的元素只用于在点击有data-aria-xxx-area attribute的区域包裹元素的子孙元素时,避免focus到区域包裹元素。
+  if (meaningfulNode.getAttribute("tabindex") === "-1") {
+    return;
+  }
+
+  let elemType = "";
+  const ariaLabel = meaningfulNode.getAttribute("aria-label");
+  if (ariaLabel !== null) {
+    elemType = ariaLabel;
+  } else {
+    elemType = mapTags(meaningfulNode.tagName);
+  }
+
+  let elemDisc = "";
+  const ariaDescription = meaningfulNode.getAttribute("aria-description");
+  if (ariaDescription !== null) {
+    elemDisc = ariaDescription;
+  } else {
+    elemDisc = meaningfulNode.innerText;
+  }
+
+  let elemAudioId = "";
+  const ariaElemAudioId = meaningfulNode.getAttribute("aria-audio-id");
+  if (ariaElemAudioId !== null) {
+    elemAudioId = ariaElemAudioId;
+  } else {
+    let ariaElemAudioId2 =
+      meaningfulNode.firstElementChild &&
+      meaningfulNode.firstElementChild.getAttribute("aria-audio-id");
+    if (ariaElemAudioId2) {
+      elemAudioId = ariaElemAudioId2;
+    }
+  }
+
+  let eleAudioUrl = meaningfulNode.getAttribute("aria-audio-url");
+
+  return {
+    elemType,
+    elemDisc,
+    ariaNode: meaningfulNode,
+    elemAudioId,
+    eleAudioUrl,
+  };
+}
+
+let lastMeaningfulNode = null;
+function extractTextForMouseOver(e) {
+  let meaningfulNode = e.target;
+
+  while (
+    !meaningfulNode.getAttribute ||
+    !meaningfulNode.getAttribute("tabindex")
+  ) {
+    meaningfulNode = meaningfulNode.parentNode;
+    if (!meaningfulNode) {
+      lastMeaningfulNode = null;
+      return;
+    }
+  }
+
+  if (meaningfulNode.getAttribute("tabindex") === "-1") {
+    lastMeaningfulNode = null;
+    return;
+  }
+
+  // mouseover事件冒泡到有data-aria-xxx-area attribute的区域包裹元素时,不应该提取该区域包裹元素的无障碍辅助信息。
+  if (
+    meaningfulNode.getAttribute("data-aria-navigation-area") !== null ||
+    meaningfulNode.getAttribute("data-aria-viewport-area") !== null ||
+    meaningfulNode.getAttribute("data-aria-interaction-area") !== null
+  ) {
+    lastMeaningfulNode = null;
+    return;
+  }
+
+  // 如果只是在需要朗读的子元素之间进进出出,第一次需要朗读,以后就不需要朗读了。
+  let relatedMeaningfulNode = e.relatedTarget;
+  while (
+    relatedMeaningfulNode &&
+    (!relatedMeaningfulNode.getAttribute ||
+      !relatedMeaningfulNode.getAttribute("tabindex"))
+  ) {
+    relatedMeaningfulNode = relatedMeaningfulNode.parentNode;
+  }
+  if (
+    relatedMeaningfulNode === meaningfulNode &&
+    lastMeaningfulNode === meaningfulNode
+  ) {
+    return;
+  }
+
+  lastMeaningfulNode = meaningfulNode;
+
+  let elemType = "";
+  const ariaLabel = meaningfulNode.getAttribute("aria-label");
+  if (ariaLabel !== null) {
+    elemType = ariaLabel;
+  } else {
+    elemType = mapTags(meaningfulNode.tagName);
+  }
+
+  let elemDisc = "";
+  const ariaDescription = meaningfulNode.getAttribute("aria-description");
+  if (ariaDescription !== null) {
+    elemDisc = ariaDescription;
+  } else {
+    elemDisc = meaningfulNode.innerText;
+  }
+
+  let elemAudioId = "";
+  const ariaElemAudioId = meaningfulNode.getAttribute("aria-audio-id");
+  if (ariaElemAudioId !== null) {
+    elemAudioId = ariaElemAudioId;
+  } else {
+    let ariaElemAudioId2 =
+      meaningfulNode.firstElementChild &&
+      meaningfulNode.firstElementChild.getAttribute("aria-audio-id");
+    if (ariaElemAudioId2) {
+      elemAudioId = ariaElemAudioId2;
+    }
+  }
+
+  let eleAudioUrl = meaningfulNode.getAttribute("aria-audio-url");
+
+  return {
+    elemType,
+    elemDisc,
+    ariaNode: meaningfulNode,
+    elemAudioId,
+    eleAudioUrl,
+  };
+}
+
+function isObject(p) {
+  return Object.prototype.toString.call(p) === "[object Object]";
+}
+
+// 判断两个对象内容是否相同
+function isSameObject(object1, object2) {
+  const keys1 = Object.keys(object1);
+  const keys2 = Object.keys(object2);
+
+  if (keys1.length !== keys2.length) {
+    return false;
+  }
+
+  for (let index = 0; index < keys1.length; index++) {
+    const val1 = object1[keys1[index]];
+    const val2 = object2[keys2[index]];
+    const areObjects = isObject(val1) && isObject(val2);
+    if (
+      (areObjects && !isSameObject(val1, val2)) ||
+      (!areObjects && val1 !== val2)
+    ) {
+      return false;
+    }
+  }
+  return true;
+}
+
+function getAndFocusNextNodeWithCustomAttribute(attriName) {
+  const startNode = document.activeElement || document.body;
+
+  const treeWalker = document.createTreeWalker(
+    document.body,
+    NodeFilter.SHOW_ELEMENT
+  );
+  treeWalker.currentNode = startNode;
+
+  let targetNode = null;
+
+  // eslint-disable-next-line
+  while (true) {
+    const nextNode = treeWalker.nextNode();
+    if (!nextNode) {
+      console.log("往下没找到");
+      break;
+    }
+    if (nextNode.dataset[attriName] !== undefined) {
+      console.log("往下找到了");
+      targetNode = nextNode;
+      break;
+    }
+  }
+
+  if (!targetNode && startNode !== document.body) {
+    treeWalker.currentNode = document.body;
+    // eslint-disable-next-line
+    while (true) {
+      const nextNode = treeWalker.nextNode();
+      if (!nextNode) {
+        console.log("往上也没找到");
+        break;
+      }
+      if (nextNode.dataset[attriName] !== undefined) {
+        console.log("往上找到了");
+        targetNode = nextNode;
+        break;
+      }
+    }
+  }
+
+  if (targetNode) {
+    // 如果重复选中某一区域,需要重新触发focus,所以先blur一下
+    if (document.activeElement === targetNode) {
+      targetNode.blur();
+    }
+    targetNode.focus();
+
+    // 如果无法focus,就强行focus
+    if (document.activeElement !== targetNode) {
+      targetNode.setAttribute("tabindex", "0");
+      targetNode.focus();
+    }
+  }
+
+  return targetNode;
+}
+
+function __focusNextFocusableNode(treeWalker) {
+  // eslint-disable-next-line
+  while (true) {
+    const nextNode = treeWalker.nextNode();
+    if (!nextNode) {
+      return false;
+    }
+    if (nextNode.focus) {
+      nextNode.focus();
+      if (document.activeElement === nextNode && nextNode.tabIndex !== -1) {
+        return true;
+      }
+    }
+  }
+}
+
+function iterateOnFocusableNode(startNode, focusedNodeHandler) {
+  const treeWalker = document.createTreeWalker(
+    document.body,
+    NodeFilter.SHOW_ELEMENT
+  );
+  treeWalker.currentNode = startNode;
+  treeWalker.currentNode.focus();
+  if (document.activeElement === treeWalker.currentNode) {
+    // console.log('起始节点可以focus')
+  } else {
+    // console.log('起始节点不可以focus,focus到下一节点。')
+    const ret = __focusNextFocusableNode(treeWalker);
+    if (!ret) {
+      return;
+    }
+  }
+
+  const iterator = () => {
+    focusedNodeHandler(treeWalker.currentNode)
+      .then(() => {
+        const result = __focusNextFocusableNode(treeWalker);
+        if (result) {
+          // console.log('遍历到下一个节点!')
+          iterator();
+        } else {
+          // console.log('遍历结束!')
+        }
+      })
+      .catch((e) => {
+        // console.log('遍历中止!', e)
+      });
+  };
+  iterator();
+}
+
+/**
+ * 返回一个自带消抖效果的函数,用res表示。
+ *
+ * fn: 需要被消抖的函数
+ * delay: 消抖时长
+ * isImmediateCall: 是否在一组操作中的第一次调用时立即执行fn
+ * isRememberLastCall:是否在一组中最后一次调用后等delay时长再执行fn
+ */
+function debounce(
+  fn,
+  delay,
+  isImmediateCall = false,
+  isRememberLastCall = true
+) {
+  console.assert(
+    isImmediateCall || isRememberLastCall,
+    "isImmediateCall 和 isRememberLastCall 至少应有一个是true,否则没有意义!"
+  );
+  let timer = null;
+  // 上次调用的时刻
+  let lastCallTime = 0;
+
+  if (isImmediateCall && !isRememberLastCall) {
+    return function (...args) {
+      const currentTime = Date.now();
+      if (currentTime - lastCallTime >= delay) {
+        fn.apply(this, args);
+      }
+      lastCallTime = currentTime;
+    };
+  } else if (!isImmediateCall && isRememberLastCall) {
+    return function (...args) {
+      if (timer) {
+        clearTimeout(timer);
+      }
+      timer = setTimeout(() => {
+        fn.apply(this, args);
+      }, delay);
+    };
+  } else if (isImmediateCall && isRememberLastCall) {
+    return function (...args) {
+      const currentTime = Date.now();
+      if (currentTime - lastCallTime >= delay) {
+        // 一组操作中的第一次
+        fn.apply(this, args);
+        lastCallTime = currentTime;
+        return;
+      } else {
+        // 一组中的后续调用
+        if (timer) {
+          // 在此之前存在中间调用
+          lastCallTime = currentTime;
+          clearTimeout(timer);
+        }
+        timer = setTimeout(() => {
+          fn.apply(this, args);
+          lastCallTime = 0;
+          timer = null;
+        }, delay);
+      }
+    };
+  } else {
+    console.error("不应该执行到这里!");
+  }
+}
+
+class DebounceScheduler {
+  constructor(fn, delay, context, isImmediateCall = false) {
+    this.job = fn;
+    this.delay = delay;
+    this.context = context;
+    this.timer = null;
+    this.lastCallTime = 0;
+    this.isImmediateCall = isImmediateCall;
+  }
+  planToDo(...args) {
+    if (!this.isImmediateCall) {
+      if (this.timer) {
+        clearTimeout(this.timer);
+        this.timer = null;
+      }
+      this.timer = setTimeout(() => {
+        this.job.apply(this.context, args);
+      }, this.delay);
+    } else {
+      const currentTime = Date.now();
+      if (currentTime - this.lastCallTime >= this.delay) {
+        // 一组操作中的第一次
+        this.job.apply(this.context, args);
+        this.lastCallTime = currentTime;
+        return;
+      } else {
+        // 一组中的后续调用
+        if (this.timer) {
+          // 在此之前存在中间调用
+          this.lastCallTime = currentTime;
+          clearTimeout(this.timer);
+          this.timer = null;
+        }
+        this.timer = setTimeout(() => {
+          this.job.apply(this.context, args);
+          this.lastCallTime = 0;
+          this.timer = null;
+        }, this.delay);
+      }
+    }
+  }
+  cancel() {
+    if (this.timer) {
+      clearTimeout(this.timer);
+      this.timer = null;
+    }
+  }
+}
+
+export default {
+  mapTags,
+  extractTextForFocus,
+  extractTextForMouseOver,
+  isSameObject,
+  getAndFocusNextNodeWithCustomAttribute,
+  iterateOnFocusableNode,
+  debounce,
+  DebounceScheduler,
+};

+ 9 - 4
src/views/Collections/components/DetailDialog/index.scss

@@ -34,6 +34,15 @@
     background: var(--white-bg);
     box-shadow: 0 0 10px #000;
   }
+  &-carousel {
+    margin: 20px 0;
+    width: 100%;
+
+    &__item {
+      display: flex;
+      justify-content: center;
+    }
+  }
   &-preview {
     width: 540px;
     color: var(--black-text-color);
@@ -43,10 +52,6 @@
       font-size: 36px;
       line-height: 100%;
     }
-    .el-image {
-      margin: 20px 0;
-      width: 100%;
-    }
     &__btn {
       margin: 0 auto;
       width: 160px;

+ 43 - 12
src/views/Collections/components/DetailDialog/index.vue

@@ -6,13 +6,30 @@
       </h3>
 
       <div ref="maskRef" style="position: relative">
-        <ElImage
-          tabindex="0"
-          aria-label="Image"
-          :aria-description="detail?.name"
-          :src="imgUrl"
-          :preview-src-list="[imgUrl]"
-        />
+        <ElCarousel
+          class="collection-detail-carousel"
+          :height="curImgIndex > 0 ? 'auto' : `${firstSliderHeight}px`"
+          :autoplay="false"
+          trigger="click"
+          @change="(i: number) => curImgIndex = i"
+        >
+          <ElCarouselItem
+            v-for="item in previewSrcList"
+            :key="item"
+            style="height: max-content"
+          >
+            <div class="collection-detail-carousel__item">
+              <ElImage
+                tabindex="0"
+                aria-label="Image"
+                fit="contain"
+                :aria-description="detail?.name"
+                :src="item"
+                @click="handlePreview"
+              />
+            </div>
+          </ElCarouselItem>
+        </ElCarousel>
 
         <span class="collection-detail__mask" :style="maskStyle" />
       </div>
@@ -28,6 +45,10 @@
     </div>
 
     <div v-if="detail" class="collection-detail-info">
+      <p v-if="detail.author" style="font-style: italic">
+        {{ detail.author }}
+      </p>
+
       <p v-if="detail.dictAge">{{ detail.dictAge }}</p>
 
       <p v-if="detail.size">{{ detail.size }}</p>
@@ -42,6 +63,7 @@
   <ElImageViewer
     v-if="showViewer"
     :url-list="previewSrcList"
+    :initial-index="curImgIndex"
     @close="closeViewer"
   />
 </template>
@@ -75,21 +97,21 @@ const show = computed({
 let timer: NodeJS.Timeout;
 const maskRef = ref();
 const loading = ref(false);
+const curImgIndex = ref(0);
 const rtf = ref<{ id: number; txt: string }[]>([]);
 const detail = ref<CollectionDetail | null>(null);
-const imgUrl = computed(() =>
-  detail.value ? baseUrl + detail.value.thumb : ""
-);
+const firstSliderHeight = ref(0);
 
 const showViewer = ref(false);
-const previewSrcList = ref<string[]>([]);
+const previewSrcList = computed(() =>
+  detail.value ? detail.value.files.map((item) => baseUrl + item.filePath) : []
+);
 
 const closeViewer = () => {
   showViewer.value = false;
 };
 
 const handlePreview = () => {
-  previewSrcList.value = [imgUrl.value];
   showViewer.value = true;
 };
 
@@ -98,9 +120,18 @@ const getDetail = async () => {
 
   try {
     loading.value = true;
+    firstSliderHeight.value = 0;
     const data = await getCollectionDetailApi(props.id);
     detail.value = data;
     rtf.value = JSON.parse(data.rtf).txtArr;
+
+    if (data.files.length) {
+      const img = new Image();
+      img.src = baseUrl + data.files[0].filePath;
+      img.onload = () => {
+        firstSliderHeight.value = (img.height / img.width) * 500;
+      };
+    }
   } finally {
     loading.value = false;
   }

+ 18 - 18
src/views/Collections/components/Menu.vue

@@ -8,11 +8,6 @@
       :class="{
         active: $route.params.type === item.type,
       }"
-      :style="{
-        backgroundImage: `url(${baseUrl}${
-          list.find((i) => i.type === item.name)?.thumb
-        })`,
-      }"
       @click="
         $router.push({ name: 'Collections', params: { type: item.type } })
       "
@@ -23,22 +18,22 @@
 </template>
 
 <script lang="ts" setup>
-import { getCollectionThumbListApi, type CollectionThumbListItem } from "@/api";
+// import { getCollectionThumbListApi, type CollectionThumbListItem } from "@/api";
 import { NAV_LIST } from "../constants";
-import { onMounted, ref } from "vue";
-import { getBaseURL } from "@dage/service";
+// import { onMounted, ref } from "vue";
+// import { getBaseURL } from "@dage/service";
 
-const baseUrl = getBaseURL();
-const list = ref<CollectionThumbListItem[]>([]);
+// const baseUrl = getBaseURL();
+// const list = ref<CollectionThumbListItem[]>([]);
 
-onMounted(() => {
-  getThumbList();
-});
+// onMounted(() => {
+//   getThumbList();
+// });
 
-const getThumbList = async () => {
-  const data = await getCollectionThumbListApi();
-  list.value = data;
-};
+// const getThumbList = async () => {
+//   const data = await getCollectionThumbListApi();
+//   list.value = data;
+// };
 </script>
 
 <style lang="scss" scoped>
@@ -46,8 +41,13 @@ const getThumbList = async () => {
   li {
     position: relative;
     border-bottom: 1px solid #fff;
-    background: #181818 no-repeat center / cover;
+    background: #181818 no-repeat 100%;
 
+    @for $i from 1 through 11 {
+      &:nth-child(#{$i}) {
+        background-image: url("../images/tab/#{$i}-min.png");
+      }
+    }
     &:last-child {
       border: none;
     }

二进制
src/views/Collections/images/tab/1-min.png


二进制
src/views/Collections/images/tab/10-min.png


二进制
src/views/Collections/images/tab/11-min.png


二进制
src/views/Collections/images/tab/2-min.png


二进制
src/views/Collections/images/tab/3-min.png


二进制
src/views/Collections/images/tab/4-min.png


二进制
src/views/Collections/images/tab/5-min.png


二进制
src/views/Collections/images/tab/6-min.png


二进制
src/views/Collections/images/tab/7-min.png


二进制
src/views/Collections/images/tab/8-min.png


二进制
src/views/Collections/images/tab/9-min.png


+ 9 - 0
src/views/Exhibitions/Detail/components/Objects.vue

@@ -9,6 +9,8 @@
         v-for="item in showMore ? list : list.slice(0, 5)"
         :key="item.id"
         class="exh-detail-object-item"
+        tabindex="0"
+        :aria-description="item.fileName"
       >
         <ElImage
           :src="baseUrl + item.filePath"
@@ -17,6 +19,7 @@
         />
 
         <div class="exh-detail-object-item__mask">
+          <p>{{ item.fileName }}</p>
           <div>
             <SvgIcon
               name="download"
@@ -137,6 +140,12 @@ defineExpose({
         align-items: center;
         gap: 5px;
       }
+      p {
+        padding: 0 5px;
+        color: white;
+        font-size: 18px;
+        line-height: 24px;
+      }
     }
   }
   &__more {

+ 14 - 4
src/views/Exhibitions/Detail/components/Overview.vue

@@ -45,7 +45,13 @@
         class="exh-detail-overview__content"
       >
         <p tabindex="0">{{ item.name }}</p>
-        <div v-html="item.txt" tabindex="0" />
+        <div
+          v-html="item.txt"
+          tabindex="0"
+          :aria-audio-url="
+            item.fileInfo.filePath ? baseUrl + item.fileInfo.filePath : ''
+          "
+        />
       </div>
     </div>
 
@@ -69,18 +75,21 @@ import AddressIcon from "@/assets/images/bg_7.png";
 import { computed, nextTick, ref, watch } from "vue";
 import { type ExhibitionDetail } from "@/api";
 import { MONTHS } from "@/utils/date";
+import { getBaseURL } from "@dage/service";
 
 const props = defineProps<{
   detail: null | ExhibitionDetail;
 }>();
 
+const baseUrl = getBaseURL();
 const wrapRef = ref();
 const mainRef = ref();
 const hasMore = ref(true);
 const height = ref(600);
-const rtf = computed<{ id: number; txt: string; name: string }[]>(() =>
-  props.detail ? JSON.parse(props.detail.rtf).txtArr : []
-);
+const rtf = computed<
+  { id: number; txt: string; name: string; fileInfo: { filePath: string } }[]
+>(() => (props.detail ? JSON.parse(props.detail.rtf).txtArr : []));
+
 const date = computed(() => {
   if (!props.detail || !props.detail.dateStart) return;
 
@@ -145,6 +154,7 @@ defineExpose({
         margin-top: 24px;
         font-size: 18px;
         line-height: 26px;
+        word-break: break-all;
         color: var(--black2-text-color);
       }
     }

+ 1 - 0
src/views/Exhibitions/Detail/index.vue

@@ -5,6 +5,7 @@
       class="exh-detail-banner"
       tabindex="0"
       data-aria-viewport-area
+      fit="cover"
       :aria-description="detail?.name"
       :src="baseUrl + detail?.thumbPc"
     />

+ 1 - 1
src/views/Exhibitions/components/ListItem/index.vue

@@ -19,7 +19,7 @@
 
     <div class="list-item__inner" aria-label="Link">
       <p class="list-item__title">{{ item.name }}</p>
-      <p class="list-item__content">
+      <p class="list-item__content limit-line line-4">
         {{ item.digest }}
       </p>
     </div>

+ 13 - 5
src/views/Publications/Magazines/index.vue

@@ -32,7 +32,7 @@
       <!-- 年份 -->
       <ul class="magazines-our__nav">
         <li
-          v-for="(date, idx) in DATE"
+          v-for="(date, idx) in years"
           :key="date"
           :class="{ active: idx === curDateIndex }"
           tabindex="0"
@@ -67,8 +67,8 @@
             })
           "
         >
-          <ElImage :src="baseUrl + item.thumb" />
-          <p>{{ item.name }}</p>
+          <ElImage :src="baseUrl + item.thumb" fit="cover" />
+          <p class="limit-line line-2">{{ item.name }}</p>
         </li>
 
         <p v-if="!total && !loading" class="no-more">no more</p>
@@ -95,8 +95,16 @@ import { getPublishListApi, type PubicationItem } from "@/api";
 import { getBaseURL } from "@dage/service";
 import { debounce } from "lodash-unified";
 
-const DATE = [2023, 2022, 2021, 2020, 2019, 2018, 2017];
 const PAGE_SIZE = 8;
+
+const MIN_YEAR = 2017;
+const date = new Date();
+const year = date.getFullYear();
+const years = Array.from(
+  { length: year - MIN_YEAR + 1 },
+  (_, index) => year - index
+);
+
 const curInfoIndex = ref(0);
 const curDateIndex = ref(0);
 const baseUrl = getBaseURL();
@@ -122,7 +130,7 @@ const getList = async () => {
     const data = await getPublishListApi({
       pageSize: PAGE_SIZE,
       pageNum: pageNum.value,
-      year: DATE[curDateIndex.value],
+      year: years[curDateIndex.value],
       type: "Magazines",
     });
     total.value = data.total;