1.下载安装 #

2.页签切换 #

2.1 index.vue #

pages\index\index.vue

<template>
    <view>
        首页
    </view>
</template>

<script>
    export default {
        data() {
            return {

            }
        },
        onLoad() {

        },
        methods: {

        }
    }
</script>

<style>
</style>


2.2 history.vue #

pages\history\history.vue

<template>
    <view>
        学习历史
    </view>
</template>

<script>
    export default {
        data() {
            return {

            };
        }
    }
</script>

<style lang="scss">

</style>

2.3 pages.json #

pages.json

{
    "pages": [
    {
        "path": "pages/index/index",
        "style": {
+            "navigationBarTitleText": "首页",
+            "enablePullDownRefresh": true
        }
    },
    {
+        "path": "pages/history/history",
+        "style": {
+            "navigationBarTitleText": "学习历史",
+            "enablePullDownRefresh": false
+        }
+    }
    ],
    "globalStyle": {
        "navigationBarTextStyle": "black",
+        "navigationBarTitleText": "前端面试",
        "navigationBarBackgroundColor": "#F8F8F8",
        "backgroundColor": "#F8F8F8"
    },
    "uniIdRouter": {},
+    "tabBar": {
+        "color": "#a0a0a0",
+        "selectedColor": "#1baeae",
+        "list": [{
+                "text": "首页",
+                "pagePath": "pages/index/index",
+                "iconPath": "static/images/home.png",
+                "selectedIconPath": "static/images/home-selected.png"
+            },
+            {
+                "text": "学习历史",
+                "pagePath": "pages/history/history",
+                "iconPath": "static/images/history.png",
+                "selectedIconPath": "static/images/history-selected.png"
+            }
+        ]
+    }
}

3.显示题目类型 #

3.1 categories.json #

mock\categories.json

[
  {
    id: 1,
    code: "HTML_BASIC",
    name: "HTML基础",
  },
  {
    id: 2,
    code: "CSS_LAYOUT",
    name: "CSS与布局",
  },
  {
    id: 3,
    code: "JS_CORE",
    name: "JavaScript核心",
  },
  {
    id: 4,
    code: "RESPONSIVE_MOBILE",
    name: "响应式与移动端设计",
  },
  {
    id: 5,
    code: "FRONTEND_FRAMEWORKS",
    name: "前端框架与库",
  },
  {
    id: 6,
    code: "WEB_PERFORMANCE",
    name: "Web性能优化",
  },
  {
    id: 7,
    code: "BROWSER_COMPATIBILITY",
    name: "浏览器兼容性与调试",
  },
  {
    id: 8,
    code: "WEB_SECURITY",
    name: "Web安全",
  },
  {
    id: 9,
    code: "MODULAR_PACKAGING",
    name: "模块化与打包工具",
  },
  {
    id: 10,
    code: "FRONTEND_ENGINEERING",
    name: "前端工程化与自动化",
  },
];

3.2 index.vue #

pages\index\index.vue

<template>
+ <view class="home">
+   <scroll-view scroll-x class="categories">
+     <view
+       v-for="(category, index) in categories"
+       :key="index"
+       class="category"
+     >
+       {{ category.name }}</view
+     >
+   </scroll-view>
+ </view>
</template>
<script>
import categories from "../../mock/categories";
export default {
  data() {
    return {
      categories,
    };
  }
};
</script>
+<style lang="scss" scoped>
+.home {
+  .categories {
+    position: fixed;
+    left: 0;
+    top: var(--window-top);
+    z-index: 100;
+    white-space: nowrap;
+    width: 100%;
+    height: 100rpx;
+    overflow-x: auto;
+    overflow-y: hidden;
+    background-color: #f5f5f5;
+    /deep/ ::-webkit-scrollbar-horizontal {
+      display: none;
+    }
+    .category {
+      display: inline-block;
+      height: 100%;
+      padding: 0 20rpx;
+      line-height: 100rpx;
+      text-align: center;
+      border: 1px solid #c5c5c5;
+      box-sizing: border-box;
+    }
+  }
+}
+</style>

4.请求题目分类 #

4.1 index.vue #

pages\index\index.vue

<template>
  <view class="home">
    <scroll-view scroll-x class="categories">
      <view
        v-for="(category, index) in categories"
        :key="index"
        class="category"
      >
        {{ category.name }}</view
      >
    </scroll-view>
  </view>
</template>
<script>
+import { fetchData } from "../../utils";
export default {
  data() {
    return {
+     categories: [],
    };
  },
  async onLoad() {
+   try {
+     await this.fetchCategories();
+   } catch (error) {
+     console.log(error);
+     uni.showToast({
+       title: "获取数据失败",
+       icon: "error",
+       duration: 2000,
+     });
+   }
  },
  methods: {
+   async fetchCategories() {
+     this.categories = await fetchData({
+       url: "/categories",
+       method: "GET",
+     });
+   },
  },
};
</script>
<style lang="scss" scoped>
.home {
  .categories {
    position: fixed;
    left: 0;
    top: var(--window-top);
    z-index: 100;
    white-space: nowrap;
    width: 100%;
    height: 100rpx;
    overflow-x: auto;
    overflow-y: hidden;
    background-color: #f5f5f5;
    /deep/ ::-webkit-scrollbar-horizontal {
      display: none;
    }
    .category {
      display: inline-block;
      height: 100%;
      padding: 0 20rpx;
      line-height: 100rpx;
      text-align: center;
      border: 1px solid #c5c5c5;
      box-sizing: border-box;
    }
  }
}
</style>

4.2 utils.js #

utils.js

export function fetchData(options) {
  return new Promise((resolve, reject) => {
    uni.request({
      ...options,
      url: `http://localhost:3000${options.url}`,
      success: (res) => {
        if (res.statusCode === 200) {
          resolve(res.data);
        } else {
          reject(new Error("请求失败"));
        }
      },
      fail: (error) => {
        console.log(error);
        reject(error);
      },
    });
  });
}

5.选中高亮 #

5.1 index.vue #

pages\index\index.vue

<template>
  <view class="home">
    <scroll-view scroll-x class="categories">
      <view
        v-for="(category, index) in categories"
        :key="index"
        class="category"
+       :class="{ highlighted: selectedIndex === index }"
+       @click="selectCategory(index)"
      >
        {{ category.name }}</view
      >
    </scroll-view>
  </view>
</template>
<script>
import { fetchData } from "../../utils";
export default {
  data() {
    return {
      categories: [],
+     selectedIndex: 0,
    };
  },
  async onLoad() {
    try {
      await this.fetchCategories();
    } catch (error) {
      console.log(error);
      uni.showToast({
        title: "获取数据失败",
        icon: "error",
        duration: 2000,
      });
    }
  },
  methods: {
    async fetchCategories() {
      this.categories = await fetchData({
        url: "/categories",
        method: "GET",
      });
    },
+   selectCategory(index) {
+     if (index === this.selectedIndex) return;
+     this.selectedIndex = index;
+   },
  },
};
</script>
<style lang="scss" scoped>
.home {
  .categories {
    position: fixed;
    left: 0;
    top: var(--window-top);
    z-index: 100;
    white-space: nowrap;
    width: 100%;
    height: 100rpx;
    overflow-x: auto;
    overflow-y: hidden;
    background-color: #f5f5f5;
    /deep/ ::-webkit-scrollbar-horizontal {
      display: none;
    }
    .category {
      display: inline-block;
      height: 100%;
      padding: 0 20rpx;
      line-height: 100rpx;
      text-align: center;
      border: 1px solid #c5c5c5;
      box-sizing: border-box;
+     &.highlighted {
+       color: #1baeae;
+       border: 1px dashed #1baeae;
+     }
    }
  }
}
</style>

6.高亮居中 #

6.1 index.vue #

pages\index\index.vue

<template>
  <view class="home">
    <scroll-view
      scroll-x
      class="categories"
+     :scroll-left="scrollLeft"
+     scroll-with-animation
    >
      <view
        v-for="(category, index) in categories"
        :key="index"
        class="category"
        :class="{ highlighted: selectedIndex === index }"
        @click="selectCategory(index)"
      >
        {{ category.name }}</view
      >
    </scroll-view>
  </view>
</template>
<script>
+import { fetchData, selectFromQuery } from "../../utils";
export default {
  data() {
    return {
      categories: [],
      selectedIndex: 0,
+     scrollLeft: 0,
    };
  },
  async onLoad() {
    try {
      await this.fetchCategories();
    } catch (error) {
      console.log(error);
      uni.showToast({
        title: "获取数据失败",
        icon: "error",
        duration: 2000,
      });
    }
  },
  methods: {
    async fetchCategories() {
      this.categories = await fetchData({
        url: "/categories",
        method: "GET",
      });
    },
    selectCategory(index) {
      if (index === this.selectedIndex) return;
      this.selectedIndex = index;
+     const query = uni.createSelectorQuery().in(this);
+     const scrollOffset = selectFromQuery(
+       query,
+       ".categories",
+       "scrollOffset"
+     );
+     const elementRect = selectFromQuery(
+       query,
+       `.category:nth-child(${index + 1})`,
+       "boundingClientRect"
+     );
+     Promise.all([scrollOffset, elementRect]).then(
+       ([scrollOffset, elementRect]) => {
+         this.scrollLeft =
+           elementRect.left +
+           scrollOffset.scrollLeft -
+           uni.getSystemInfoSync().windowWidth / 2 + elementRect.width / 2;
+       }
+     );
    },
  },
};
</script>
<style lang="scss" scoped>
.home {
  .categories {
    position: fixed;
    left: 0;
    top: var(--window-top);
    z-index: 100;
    white-space: nowrap;
    width: 100%;
    height: 100rpx;
    overflow-x: auto;
    overflow-y: hidden;
    background-color: #f5f5f5;
    /deep/ ::-webkit-scrollbar-horizontal {
      display: none;
    }
    .category {
      display: inline-block;
      height: 100%;
      padding: 0 20rpx;
      line-height: 100rpx;
      text-align: center;
      border: 1px solid #c5c5c5;
      box-sizing: border-box;
      &.highlighted {
        color: #1baeae;
        border: 1px dashed #1baeae;
      }
    }
  }
}
</style>

7.题目列表 #

7.1 index.vue #

pages\index\index.vue

<template>
  <view class="home">
    <scroll-view
      scroll-x
      class="categories"
      :scroll-left="scrollLeft"
      scroll-with-animation
    >
      <view
        v-for="(category, index) in categories"
        :key="index"
        class="category"
        :class="{ highlighted: selectedIndex === index }"
        @click="selectCategory(index)"
      >
        {{ category.name }}</view
      >
    </scroll-view>
    <view class="topics">
      <topic v-for="topic in topics" :topic="topic" :key="topic.id"></topic>
    </view>
  </view>
</template>
<script>
import { fetchData, selectFromQuery } from "../../utils";
export default {
  data() {
    return {
      categories: [],
      selectedIndex: 0,
      scrollLeft: 0,
+     currentPage: 1,
+     pageSize: 10,
+     topics: [],
    };
  },
  async onLoad() {
    try {
      await this.fetchCategories();
+     await this.fetchTopics();
    } catch (error) {
      console.log(error);
      uni.showToast({
        title: "获取数据失败",
        icon: "error",
        duration: 2000,
      });
    }
  },
  methods: {
    async fetchCategories() {
      this.categories = await fetchData({
        url: "/categories",
        method: "GET",
      });
    },
+   async fetchTopics() {
+     if (this.categories.length <= 0) return;
+     const category_id = this.categories[this.selectedIndex].id;
+     this.isLoading = true;
+     uni.showLoading({
+       title: "加载中...",
+     });
+     try {
+       const response = await fetchData({
+         url: `/topics?category_id=${category_id}&currentPage=${this.currentPage}&pageSize=${this.pageSize}`,
+         method: "GET",
+       });
+       this.topics = response.list;
+     } catch (error) {
+       console.log(error);
+     } finally {
+       this.isLoading = false;
+       uni.hideLoading();
+     }
+   },
    selectCategory(index) {
      if (index === this.selectedIndex) return;
      this.selectedIndex = index;
      const query = uni.createSelectorQuery().in(this);
      const scrollOffset = selectFromQuery(
        query,
        ".categories",
        "scrollOffset"
      );
      const elementRect = selectFromQuery(
        query,
        `.category:nth-child(${index + 1})`,
        "boundingClientRect"
      );
      Promise.all([scrollOffset, elementRect]).then(
        ([scrollOffset, elementRect]) => {
          this.scrollLeft =
            elementRect.left +
            scrollOffset.scrollLeft -
            uni.getSystemInfoSync().windowWidth / 2 +
            elementRect.width / 2;
        }
      );
    },
  },
};
</script>
<style lang="scss" scoped>
.home {
  .categories {
    position: fixed;
    left: 0;
    top: var(--window-top);
    z-index: 100;
    white-space: nowrap;
    width: 100%;
    height: 100rpx;
    overflow-x: auto;
    overflow-y: hidden;
    background-color: #f5f5f5;
    /deep/ ::-webkit-scrollbar-horizontal {
      display: none;
    }
    .category {
      display: inline-block;
      height: 100%;
      padding: 0 20rpx;
      line-height: 100rpx;
      text-align: center;
      border: 1px solid #c5c5c5;
      box-sizing: border-box;
      &.highlighted {
        color: #1baeae;
        border: 1px dashed #1baeae;
      }
    }
  }
+ .topics {
+   padding-top: 100rpx;
+ }
}
</style>

7.2 topic.vue #

components\topic\topic.vue

<template>
  <view class="topic">
    <view class="topic-cover">
      <image :src="topic.cover" mode="aspectFit"></image>
    </view>
    <view class="topic-info">
      <view class="question">
        {{ topic.question }}
      </view>
      <view class="detail">
        <text>{{ topic.difficulty }}</text>
        <text>{{ topic.views }}次学习</text>
      </view>
    </view>
  </view>
</template>
<script>
export default {
  name: "topic",
  props: {
    topic: {
      type: Object,
    },
  },
  methods: {},
  computed: {},
};
</script>
<style lang="scss">
.topic {
  display: flex;
  align-items: center;
  padding: 20rpx;
  .topic-cover {
    flex-shrink: 0;
    width: 200rpx;
    height: 200rpx;
    image {
      width: 100%;
      height: 100%;
      object-fit: cover;
      border-radius: 20rpx;
    }
  }
  .topic-info {
    flex-grow: 1;
    padding-left: 30rpx;
    .question {
      font-size: 32rpx;
      font-weight: bold;
      max-height: 2.4em;
      line-height: 1.2em;
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      text-overflow: ellipsis;
      margin-bottom: 16rpx;
    }
    .detail {
      display: flex;
      justify-content: space-between;
      text {
        font-size: 24rpx;
        &:first-child {
          color: gray;
          margin-right: 24rpx;
        }
      }
    }
  }
}
</style>

8.上拉加载 #

8.1 index.vue #

pages\index\index.vue

<template>
  <view class="home">
    <scroll-view
      scroll-x
      class="categories"
      :scroll-left="scrollLeft"
      scroll-with-animation
    >
      <view
        v-for="(category, index) in categories"
        :key="index"
        class="category"
        :class="{ highlighted: selectedIndex === index }"
        @click="selectCategory(index)"
      >
        {{ category.name }}</view
      >
    </scroll-view>
    <view class="topics">
      <topic v-for="topic in topics" :topic="topic" :key="topic.id"></topic>
    </view>
  </view>
</template>
<script>
import { fetchData, selectFromQuery } from "../../utils";
export default {
  data() {
    return {
      categories: [],
      selectedIndex: 0,
      scrollLeft: 0,
      currentPage: 1,
      pageSize: 10,
      topics: [],
+     totalPages: 0,
+     isLoading: false,
    };
  },
  async onLoad() {
    try {
      await this.fetchCategories();
      await this.fetchTopics();
    } catch (error) {
      console.log(error);
      uni.showToast({
        title: "获取数据失败",
        icon: "error",
        duration: 2000,
      });
    }
  },
+ onReachBottom() {
+   if (this.currentPage <= this.totalPages && !this.isLoading) {
+     this.fetchTopics();
+   } else {
+     uni.showToast({
+       title: "没有更多数据了",
+       icon: "none",
+       duration: 2000,
+     });
+   }
+ },
  methods: {
    async fetchCategories() {
      this.categories = await fetchData({
        url: "/categories",
        method: "GET",
      });
    },
    async fetchTopics() {
      if (this.categories.length <= 0) return;
      const category_id = this.categories[this.selectedIndex].id;
      this.isLoading = true;
      uni.showLoading({
        title: "加载中...",
      });
      try {
        const response = await fetchData({
          url: `/topics?category_id=${category_id}&currentPage=${this.currentPage}&pageSize=${this.pageSize}`,
          method: "GET",
        });
+       this.topics = this.topics.concat(response.list);
+       this.totalPages = response.totalPages;
+       this.currentPage++;
      } catch (error) {
        console.log(error);
      } finally {
        this.isLoading = false;
        uni.hideLoading();
      }
    },
    selectCategory(index) {
      if (index === this.selectedIndex) return;
      this.selectedIndex = index;
      const query = uni.createSelectorQuery().in(this);
      const scrollOffset = selectFromQuery(
        query,
        ".categories",
        "scrollOffset"
      );
      const elementRect = selectFromQuery(
        query,
        `.category:nth-child(${index + 1})`,
        "boundingClientRect"
      );
      Promise.all([scrollOffset, elementRect]).then(
        ([scrollOffset, elementRect]) => {
          this.scrollLeft =
            elementRect.left +
            scrollOffset.scrollLeft -
            uni.getSystemInfoSync().windowWidth / 2 +
            elementRect.width / 2;
        }
      );
    },
  },
};
</script>
<style lang="scss" scoped>
.home {
  .categories {
    position: fixed;
    left: 0;
    top: var(--window-top);
    z-index: 100;
    white-space: nowrap;
    width: 100%;
    height: 100rpx;
    overflow-x: auto;
    overflow-y: hidden;
    background-color: #f5f5f5;
    /deep/ ::-webkit-scrollbar-horizontal {
      display: none;
    }
    .category {
      display: inline-block;
      height: 100%;
      padding: 0 20rpx;
      line-height: 100rpx;
      text-align: center;
      border: 1px solid #c5c5c5;
      box-sizing: border-box;
      &.highlighted {
        color: #1baeae;
        border: 1px dashed #1baeae;
      }
    }
  }
  .topics {
    padding-top: 100rpx;
  }
}
</style>

9.下拉刷新 #

9.1 index.vue #

pages\index\index.vue

<template>
  <view class="home">
    <scroll-view
      scroll-x
      class="categories"
      :scroll-left="scrollLeft"
      scroll-with-animation
    >
      <view
        v-for="(category, index) in categories"
        :key="index"
        class="category"
        :class="{ highlighted: selectedIndex === index }"
        @click="selectCategory(index)"
      >
        {{ category.name }}</view
      >
    </scroll-view>
    <view class="topics">
      <topic v-for="topic in topics" :topic="topic" :key="topic.id"></topic>
    </view>
  </view>
</template>
<script>
import { fetchData, selectFromQuery } from "../../utils";
export default {
  data() {
    return {
      categories: [],
      selectedIndex: 0,
      scrollLeft: 0,
      currentPage: 1,
      pageSize: 10,
      topics: [],
      totalPages: 0,
      isLoading: false,
    };
  },
  async onLoad() {
    try {
      await this.fetchCategories();
      await this.fetchTopics();
    } catch (error) {
      console.log(error);
      uni.showToast({
        title: "获取数据失败",
        icon: "error",
        duration: 2000,
      });
    }
  },
  onReachBottom() {
    if (this.currentPage <= this.totalPages && !this.isLoading) {
      this.fetchTopics();
    } else {
      uni.showToast({
        title: "没有更多数据了",
        icon: "none",
        duration: 2000,
      });
    }
  },
+ async onPullDownRefresh() {
+   try {
+     await this.refreshTopics();
+   } catch (error) {
+     uni.showToast({
+       title: "刷新失败",
+       icon: "error",
+       duration: 2000,
+     });
+   } finally {
+     uni.stopPullDownRefresh();
+   }
+ },
  methods: {
+   async refreshTopics() {
+     this.currentPage = 1;
+     this.topics = [];
+     await this.fetchTopics();
+   },
    async fetchCategories() {
      this.categories = await fetchData({
        url: "/categories",
        method: "GET",
      });
    },
    async fetchTopics() {
      if (this.categories.length <= 0) return;
      const category_id = this.categories[this.selectedIndex].id;
      this.isLoading = true;
      uni.showLoading({
        title: "加载中...",
      });
      try {
        const response = await fetchData({
          url: `/topics?category_id=${category_id}&currentPage=${this.currentPage}&pageSize=${this.pageSize}`,
          method: "GET",
        });
        this.topics = this.topics.concat(response.list);
        this.totalPages = response.totalPages;
        this.currentPage++;
      } catch (error) {
        console.log(error);
      } finally {
        this.isLoading = false;
        uni.hideLoading();
      }
    },
    selectCategory(index) {
      if (index === this.selectedIndex) return;
      this.selectedIndex = index;
      const query = uni.createSelectorQuery().in(this);
      const scrollOffset = selectFromQuery(
        query,
        ".categories",
        "scrollOffset"
      );
      const elementRect = selectFromQuery(
        query,
        `.category:nth-child(${index + 1})`,
        "boundingClientRect"
      );
      Promise.all([scrollOffset, elementRect]).then(
        ([scrollOffset, elementRect]) => {
          this.scrollLeft =
            elementRect.left +
            scrollOffset.scrollLeft -
            uni.getSystemInfoSync().windowWidth / 2 +
            elementRect.width / 2;
+           this.refreshTopics();
        }
      );
    },
  },
};
</script>
<style lang="scss" scoped>
.home {
  .categories {
    position: fixed;
    left: 0;
    top: var(--window-top);
    z-index: 100;
    white-space: nowrap;
    width: 100%;
    height: 100rpx;
    overflow-x: auto;
    overflow-y: hidden;
    background-color: #f5f5f5;
    /deep/ ::-webkit-scrollbar-horizontal {
      display: none;
    }
    .category {
      display: inline-block;
      height: 100%;
      padding: 0 20rpx;
      line-height: 100rpx;
      text-align: center;
      border: 1px solid #c5c5c5;
      box-sizing: border-box;
      &.highlighted {
        color: #1baeae;
        border: 1px dashed #1baeae;
      }
    }
  }
  .topics {
    padding-top: 100rpx;
  }
}
</style>

10.题目详情页 #

10.1 topic.vue #

components\topic\topic.vue

<template>
+ <view class="topic" @click="navigateToDetail(topic)">
    <view class="topic-cover">
      <image :src="topic.cover" mode="aspectFit"></image>
    </view>
    <view class="topic-info">
      <view class="question">
        {{ topic.question }}
      </view>
      <view class="detail">
        <text>{{ topic.difficulty }}</text>
        <text>{{ topic.views }}次学习</text>
      </view>
    </view>
  </view>
</template>
<script>
export default {
  name: "topic",
  props: {
    topic: {
      type: Object,
    },
  },
  methods: {
+   navigateToDetail(topic) {
+     uni.navigateTo({
+       url: `/pages/detail/detail?id=${topic.id}`,
+     });
+   },
  },
  computed: {},
};
</script>
<style lang="scss">
.topic {
  display: flex;
  align-items: center;
  padding: 20rpx;
  .topic-cover {
    flex-shrink: 0;
    width: 200rpx;
    height: 200rpx;
    image {
      width: 100%;
      height: 100%;
      object-fit: cover;
      border-radius: 20rpx;
    }
  }
  .topic-info {
    flex-grow: 1;
    padding-left: 30rpx;
    .question {
      font-size: 32rpx;
      font-weight: bold;
      max-height: 2.4em;
      line-height: 1.2em;
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      text-overflow: ellipsis;
      margin-bottom: 16rpx;
    }
    .detail {
      display: flex;
      justify-content: space-between;
      text {
        font-size: 24rpx;
        &:first-child {
          color: gray;
          margin-right: 24rpx;
        }
      }
    }
  }
}
</style>

10.2 detail.vue #

pages\detail\detail.vue

<template>
  <view class="topic">
    <view class="question">{{ topic.question }}</view>
    <view class="info">
      <text class="difficulty">难度:{{ topic.difficulty }}</text>
      <text class="views">学习次数:{{ topic.views }}</text>
    </view>
    <view class="answer">
      <image :src="topic.cover" mode="aspectFit"></image>
      <rich-text :nodes="htmlAnswer" class="content"></rich-text>
    </view>
  </view>
</template>
<script>
import { fetchData,md } from "../../utils";
export default {
  name: "detail",
  data() {
    return {
      topic: {},
    };
  },
  async onLoad({ id }) {
    const topic = await fetchData({
      url: `/topic/${id}`,
      method: "GET",
    });
    this.topic = topic;
    uni.setNavigationBarTitle({
      title: topic.question,
    });
  },
  methods: {},

  computed: {
    htmlAnswer() {
      return md.render(this.topic.answer || "");
    },
  },
};
</script>

<style lang="scss">
.topic {
  padding: 20rpx;
  background-color: #fafafa;

  .question {
    font-size: 36rpx;
    font-weight: bold;
    margin-bottom: 30rpx;
    color: #333;
    text-align: center;
    background: #fff;
    padding: 20rpx;
    border-radius: 15rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  }

  .info {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 40rpx;
    background: #fff;
    padding: 20rpx;
    border-radius: 15rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);

    .difficulty,
    .views {
      font-size: 28rpx;
      color: #777;
    }
  }

  .answer {
    margin-top: 20rpx;

    image {
      width: 100%;
      height: 400rpx;
      object-fit: cover;
      border-radius: 15rpx;
      margin-bottom: 20rpx;
    }

    .content {
      /deep/ ol,
      /deep/ ul {
        list-style-position: inside;
      }
      /deep/ code.language-html {
        white-space: pre-wrap;
        word-wrap: break-word;
      }
    }
  }
}
</style>

11.学习历史 #

11.1 detail.vue #

pages\detail\detail.vue

<template>
  <view class="topic">
    <view class="question">{{ topic.question }}</view>
    <view class="info">
      <text class="difficulty">难度:{{ topic.difficulty }}</text>
      <text class="views">学习次数:{{ topic.views }}</text>
    </view>
    <view class="answer">
      <image :src="topic.cover" mode="aspectFit"></image>
      <rich-text :nodes="htmlAnswer" class="content"></rich-text>
    </view>
  </view>
</template>
<script>
+import { fetchData, md, STUDY_HISTORY } from "../../utils";
export default {
  name: "detail",
  data() {
    return {
      topic: {},
    };
  },
  async onLoad({ id }) {
    const topic = await fetchData({
      url: `/topic/${id}`,
      method: "GET",
    });
    this.topic = topic;
+   this.addTopicToStudyHistory(topic);
    uni.setNavigationBarTitle({
      title: topic.question,
    });
  },
  methods: {
+   addTopicToStudyHistory(topic) {
+     let studyHistory = uni.getStorageSync(STUDY_HISTORY) || [];
+     topic.studyTime = Date.now();
+     const existingTopicIndex = studyHistory.findIndex(
+       (item) => item.id === topic.id
+     );
+     if (existingTopicIndex !== -1) {
+       studyHistory.splice(existingTopicIndex, 1);
+     }
+     studyHistory.unshift(topic);
+     uni.setStorageSync(STUDY_HISTORY, studyHistory);
+   },
  },

  computed: {
    htmlAnswer() {
      return md.render(this.topic.answer || "");
    },
  },
};
</script>

<style lang="scss">
.topic {
  padding: 20rpx;
  background-color: #fafafa;

  .question {
    font-size: 36rpx;
    font-weight: bold;
    margin-bottom: 30rpx;
    color: #333;
    text-align: center;
    background: #fff;
    padding: 20rpx;
    border-radius: 15rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
  }

  .info {
    display: flex;
    justify-content: space-between;
    align-items: center;
    margin-bottom: 40rpx;
    background: #fff;
    padding: 20rpx;
    border-radius: 15rpx;
    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);

    .difficulty,
    .views {
      font-size: 28rpx;
      color: #777;
    }
  }

  .answer {
    margin-top: 20rpx;

    image {
      width: 100%;
      height: 400rpx;
      object-fit: cover;
      border-radius: 15rpx;
      margin-bottom: 20rpx;
    }

    .content {
      /deep/ ol,
      /deep/ ul {
        list-style-position: inside;
      }
      /deep/ code.language-html {
        white-space: pre-wrap;
        word-wrap: break-word;
      }
    }
  }
}
</style>

11.2 history.vue #

pages\history\history.vue

<template>
  <view class="topics">
    <topic v-for="topic in topics" :topic="topic" :key="topic.id"></topic>
  </view>
</template>

<script>
import { STUDY_HISTORY } from "../../utils";
export default {
  name: "history",
  data() {
    return {
      topics: [],
    };
  },
  async onShow() {
    let studyHistory = uni.getStorageSync(STUDY_HISTORY) || [];
    this.topics = studyHistory;
  },
};
</script>

<style lang="scss"></style>

11.3 utils.js #

utils.js

import MarkdownIt from "markdown-it";
export function fetchData(options) {
  return new Promise((resolve, reject) => {
    uni.request({
      ...options,
      url: `http://localhost:3000${options.url}`,
      success: (res) => {
        if (res.statusCode === 200) {
          resolve(res.data);
        } else {
          reject(new Error("请求失败"));
        }
      },
      fail: (error) => {
        console.log(error);
        reject(error);
      },
    });
  });
}
export function selectFromQuery(query, selector, action) {
  return new Promise((resolve) => {
    query.select(selector)[action](resolve).exec();
  });
}
export const md = new MarkdownIt();
+export const STUDY_HISTORY = "STUDY_HISTORY";
+export function formatTime(time) {
+  const date = new Date(time);
+  return `${date.getFullYear()}年${
+    date.getMonth() + 1
+  }月${date.getDate()}日${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`;
+}

11.4 topic.vue #

components\topic\topic.vue

<template>
  <view class="topic" @click="navigateToDetail(topic)">
    <view class="topic-cover">
      <image :src="topic.cover" mode="aspectFit"></image>
    </view>
    <view class="topic-info">
      <view class="question">
        {{ topic.question }}
      </view>
+     <view class="detail" v-if="topic.studyTime">
+       <text>学习时间: {{ formattedStudyTime }}</text>
+     </view>
+     <view class="detail" v-else>
        <text>{{ topic.difficulty }}</text>
        <text>{{ topic.views }}次学习</text>
      </view>
    </view>
  </view>
</template>
<script>
+import { formatTime } from "../../utils";
export default {
  name: "topic",
  props: {
    topic: {
      type: Object,
    },
  },
  methods: {
    navigateToDetail(topic) {
      uni.navigateTo({
        url: `/pages/detail/detail?id=${topic.id}`,
      });
    },
  },
  computed: {
+   formattedStudyTime() {
+     return formatTime(this.topic.studyTime);
+   },
  },
};
</script>
<style lang="scss">
.topic {
  display: flex;
  align-items: center;
  padding: 20rpx;
  .topic-cover {
    flex-shrink: 0;
    width: 200rpx;
    height: 200rpx;
    image {
      width: 100%;
      height: 100%;
      object-fit: cover;
      border-radius: 20rpx;
    }
  }
  .topic-info {
    flex-grow: 1;
    padding-left: 30rpx;
    .question {
      font-size: 32rpx;
      font-weight: bold;
      max-height: 2.4em;
      line-height: 1.2em;
      overflow: hidden;
      display: -webkit-box;
      -webkit-line-clamp: 2;
      -webkit-box-orient: vertical;
      text-overflow: ellipsis;
      margin-bottom: 16rpx;
    }
    .detail {
      display: flex;
      justify-content: space-between;
      text {
        font-size: 24rpx;
        &:first-child {
          color: gray;
          margin-right: 24rpx;
        }
      }
    }
  }
}
</style>

12.参考 #

1.scroll-view #

scroll-view 是 uni-app(也在微信小程序中)提供的一个组件,允许用户在内容超出屏幕范围时进行滚动查看。它支持垂直滚动、水平滚动以及双向滚动。

下面是 scroll-view 的主要特性和使用方法:

属性

  1. scroll-xscroll-y:这两个布尔属性控制是否允许在水平或垂直方向上滚动。例如,如果你只想允许水平滚动,那么只设置 scroll-xtrue

  2. upper-thresholdlower-threshold:这两个属性允许你定义当滚动到顶部或底部时的触发阈值,常常与滚动事件一起使用,例如实现“下拉刷新”或“上拉加载更多”的功能。

  3. scroll-topscroll-left:允许你定义滚动区域的起始位置。值为数字,单位是 px。

  4. scroll-into-view:该属性接受一个字符串 ID,使得带有此 ID 的元素滚动到视图中。

  5. scroll-with-animation:一个布尔属性,确定是否应该有动画效果地滚动到 scroll-topscroll-leftscroll-into-view 的位置。

事件

  1. bindscrolltoupper:当滚动到顶部时触发的事件。

  2. bindscrolltolower:当滚动到底部时触发的事件。

  3. bindscroll:滚动时触发的事件,可以获取到滚动的具体位置。

示例

  1. 基本用法:
<scroll-view scroll-y style="height: 300px;">
  <view style="height:1500px;">这是一个可滚动的内容</view>
</scroll-view>
  1. 水平滚动:
<scroll-view scroll-x style="white-space: nowrap;">
  <view style="display:inline-block;width:300px;">1</view>
  <view style="display:inline-block;width:300px;">2</view>
  <view style="display:inline-block;width:300px;">3</view>
</scroll-view>
  1. 监听滚动到底部事件,实现“上拉加载更多”:
<scroll-view scroll-y bindscrolltolower="loadMore" style="height: 300px;">
  <view style="height:1500px;">这是一个可滚动的内容</view>
</scroll-view>

在上述示例中,当用户滚动到底部时,loadMore 方法会被调用,你可以在这个方法里面加载更多的内容。

总的来说,scroll-view 提供了一种简单的方式来实现滚动功能,与此同时,它也提供了很多实用的属性和事件来让你可以定制和控制滚动的行为。

2.var(--window-top) #

uni-app 中,var(--window-top) 是一个 CSS 变量,它代表了窗口的上方的安全区域距离。这主要用于适应不同的设备,尤其是那些具有刘海屏、水滴屏等特殊设计的设备。

var(--window-top) 提供了从窗口顶部到安全区域的距离,通常这个距离是设备的状态栏的高度。当你希望页面内容或某个元素距离顶部有一个合适的间距时,特别是不想内容被状态栏遮挡时,这个变量会非常有用。

例如,如果你想设置一个元素的 padding-top 为状态栏的高度,你可以这样使用:

.my-element {
  padding-top: var(--window-top);
}

这样,无论在什么设备上,该元素的上方 padding 都会自动适应到合适的距离,确保内容不会被设备的特殊部分(如刘海)遮挡。

同样的,uni-app 还提供了其它类似的变量,如:

这些变量为开发者提供了更灵活、更直观的方式来处理各种设备屏幕的适配问题。

3.white-space: nowrap; #

uni-app,CSS 属性 white-space: nowrap; 的功能与传统的 Web 开发环境中的功能相同。

white-space 是 CSS 中用于控制元素内文本如何换行的属性。当它的值设置为 nowrap 时,它会阻止文本换行,即使文本的长度超过了容器的宽度。文本会继续在一行内展示,直到文本结束或者遇到一个强制换行的点(如 <br> 标签)。

举个简单的例子:

<view class="text-container">
  这是一段很长的文本,但由于我们设置了white-space: nowrap;,它不会自动换行。
</view>
.text-container {
  white-space: nowrap;
}

在上述示例中,尽管文本的长度可能会超出 text-container 的宽度,但文本会继续在同一行显示,不会自动换行。

uni-app 或任何其他 Web 或移动应用环境中,这种行为通常会导致文本溢出其容器。为了处理这种溢出,你可能会结合其他属性,例如 overflow: hidden;text-overflow: ellipsis;,来隐藏溢出的文本并显示一个省略号(...),表示还有更多的文本没有显示。

总之,white-space: nowrap;uni-app 中的用法与其在传统 Web 开发中的用法相同,用于控制文本的换行行为。

4.::-webkit-scrollbar-horizontal #

uni-app 和其他基于 Vue 的框架中,/deep/ 是一个用来突破 scoped CSS 边界的特殊选择器。当你为一个 Vue 组件使用 <style scoped> 时,该组件的样式会被限定到该组件内部,防止全局污染。但有时,我们需要修改子组件或某个更深层级元素的样式,这时 /deep/ 就派上了用场。

对于 ::-webkit-scrollbar-horizontal,这是一个 Webkit 浏览器和某些移动应用环境下用于定制横向滚动条的伪元素。你可以使用它来更改横向滚动条的外观。

结合起来,当你在 uni-app 的一个组件中使用以下样式:

<style scoped>
.container /deep/ ::-webkit-scrollbar-horizontal {
    height: 8px;
}
.container /deep/ ::-webkit-scrollbar-thumb {
    background-color: rgba(0, 0, 0, 0.5);
}
.container /deep/ ::-webkit-scrollbar-track {
    background-color: rgba(0, 0, 0, 0.1);
}
</style>

上述代码会为 .container 内部的所有横向滚动条设置一个 8px 的高度,并为滚动条的拖动块和轨道设置不同的背景色。

但要注意,::-webkit-scrollbar-horizontal 以及其他相关的滚动条样式伪元素只在 Webkit 浏览器和基于 Webkit 的环境中有效,比如 Chrome 和 Safari。在不支持的环境中,这些样式不会有任何效果。

还需要注意的是,/deep/ 选择器在某些环境中可能需要替换为 >>>::v-deep。确保你正在使用的工具或编译器支持 /deep/ 选择器。

5.uni.request #

uni.request()uni-app 提供的一个 API,用于发送网络请求。它是开发者在 uni-app 项目中进行 HTTP 请求时的主要方法。这个 API 很像其他前端库和框架中的 HTTP 请求方法(例如 jQuery 中的 $.ajax() 或 Axios 中的 axios()),但它特别针对 uni-app 的多端运行环境进行了优化。

使用方法

基本的使用方式如下:

uni.request({
  url: "https://api.example.com/data", // 请求的 URL
  method: "GET", // HTTP 请求方法,默认为 'GET'
  data: {
    key1: "value1",
    key2: "value2",
  }, // 请求的参数
  header: {
    "content-type": "application/json", // 设置请求头
  },
  success: (res) => {
    if (res.statusCode === 200) {
      console.log(res.data); // 输出请求到的数据
    }
  },
  fail: (error) => {
    console.error("request fail:", error);
  },
  complete: () => {
    console.log("request complete");
  },
});

主要参数

返回值

该函数返回一个 RequestTask 对象,可以用于请求的中断:

const requestTask = uni.request({
  url: "https://api.example.com/data",
  success: (res) => {
    console.log(res.data);
  },
});

// 用于中断请求
// requestTask.abort();

注意事项

总的来说,uni.request() 提供了一个简单而强大的方式来在 uni-app 项目中发送网络请求,使开发者能够轻松获取远程数据。

6.uni.createSelectorQuery #

uni.createSelectorQuery() 是 uni-app 提供的一个 API,用于在页面中查询和操作页面节点。这个 API 可以帮助开发者获取页面中的元素信息,例如位置、尺寸等。它与微信小程序中的 wx.createSelectorQuery() 功能类似,但经过调整以适应 uni-app 的跨平台特性。

使用方法

  1. 创建查询对象

首先,你需要创建一个查询对象:

const query = uni.createSelectorQuery();
  1. 选择目标元素

使用查询对象,你可以选择页面中的元素。有几种选择方法:

例如:

query.select("#myElement");
  1. 获取元素信息

选择了元素之后,可以获取其相关信息:

例如:

query.select("#myElement").boundingClientRect();
  1. 执行查询

使用 exec() 方法执行查询并获取结果:

query.exec(function (res) {
  console.log(res[0].top); // 输出元素的 top 位置信息
});

示例

以下是一个简单的示例,用于获取页面中 id 为 myElement 的元素的位置和尺寸信息:

const query = uni.createSelectorQuery();
query.select("#myElement").boundingClientRect();
query.exec(function (res) {
  console.log("元素的宽度:", res[0].width);
  console.log("元素的高度:", res[0].height);
  console.log("元素的左边位置:", res[0].left);
  console.log("元素的顶部位置:", res[0].top);
});

注意事项

总的来说,uni.createSelectorQuery() 提供了一个方便的方法来查询页面中的元素信息,帮助开发者更好地与页面交互。

7.uni.getSystemInfoSync #

在 uni-app 中,uni.getSystemInfoSync() 是一个同步 API,它可以让开发者获取当前设备的系统信息,包括设备型号、屏幕尺寸、系统版本等。与其异步版本 uni.getSystemInfo() 功能相同,但 uni.getSystemInfoSync() 会立即返回结果,而不需要通过回调函数获取。

主要返回的信息包括:

  1. brand: 设备品牌
  2. model: 设备型号
  3. pixelRatio: 设备像素比
  4. screenWidth: 屏幕宽度(单位:px)
  5. screenHeight: 屏幕高度(单位:px)
  6. windowWidth: 窗口宽度(单位:px)
  7. windowHeight: 窗口高度(单位:px)
  8. statusBarHeight: 状态栏的高度(单位:px)
  9. language: 设备语言
  10. version: uni-app 版本
  11. system: 操作系统版本
  12. platform: 客户端平台(如 "android"、"ios")
  13. fontSizeSetting: 用户选择的字体大小(单位:px)
  14. SDKVersion: 客户端基础库版本

... 以及其他一些特定平台相关的信息。

使用示例

以下是一个简单的示例,演示如何使用 uni.getSystemInfoSync() 来获取设备的屏幕宽度和高度:

const systemInfo = uni.getSystemInfoSync();
console.log("屏幕宽度:", systemInfo.screenWidth);
console.log("屏幕高度:", systemInfo.screenHeight);

注意事项

  1. 尽管 uni.getSystemInfoSync() 提供了方便的同步获取设备信息的方式,但在某些情况下,频繁使用同步 API 可能会对性能造成影响。所以,除非有特定的需求,通常建议使用异步 API uni.getSystemInfo()
  2. 返回的设备信息可能因不同的平台和版本而有所不同。因此,建议在多个目标平台上进行测试,以确保获取的信息满足应用程序的需求。

总之,uni.getSystemInfoSync() 是一个非常有用的 API,它可以帮助开发者获取设备的详细系统信息,从而提供更好的用户体验或执行特定的功能逻辑。

8.uni.showLoading #

在 uni-app 中,uni.showLoading() 是一个常用的 API,用于在界面上显示一个加载中的提示,通常是一个带有文字描述的模态的加载圈。这可以帮助提供一个视觉反馈,让用户知道某个操作(如网络请求、数据处理等)正在进行,避免用户在等待过程中感到迷茫。

主要参数

uni.showLoading() 接受一个对象作为参数,该对象的属性包括:

使用示例

uni.showLoading({
  title: "加载中...",
  mask: true,
});

// 模拟一个异步操作,比如网络请求
setTimeout(() => {
  uni.hideLoading(); // 在数据加载完后,隐藏加载提示
}, 2000);

在上面的例子中,当调用 uni.showLoading() 后,会在界面上显示一个带有 "加载中..." 文字的加载提示。然后通过 setTimeout 模拟了一个两秒的异步操作,操作完成后调用 uni.hideLoading() 隐藏加载提示。

注意事项

  1. 避免遮挡:由于 uni.showLoading() 会在界面上显示一个模态的提示,所以使用时要确保不会遮挡到用户需要交互的界面元素。

  2. 及时关闭:一旦加载或异步操作完成,应该立即调用 uni.hideLoading() 关闭加载提示,避免用户长时间看到加载提示。

  3. 避免和 uni.showToast() 同时使用uni.showLoading()uni.showToast() 是两个独立的接口,但它们在界面上的显示位置是重叠的,所以应避免它们同时出现。

  4. mask 的使用:如果设置 masktrue,则会在界面上增加一个半透明的蒙层,这可以防止用户在加载过程中进行其他操作。但如果不希望阻止用户的其他交互,可以不设置此属性或设置为 false

总的来说,uni.showLoading() 是一个简单而实用的 API,它能有效地为用户提供正在进行中的反馈,提高应用的用户体验。

9.uni.showToast #

uni.showToast() 是 uni-app 提供的一个轻量级反馈提示接口。它主要用于显示短暂的文本或图标提示,通常用于提醒用户操作结果或其他非模态的信息提示。

主要参数

当你调用 uni.showToast() 时,你可以传入一个对象,该对象的属性包括:

使用示例

  1. 基础使用
uni.showToast({
  title: "操作成功",
  icon: "success",
});

这会显示一个带有 "成功" 图标和 "操作成功" 文字的提示。

  1. 自定义图标
uni.showToast({
  title: "自定义图标",
  image: "/path/to/your/image.png",
});
  1. 无图标
uni.showToast({
  title: "这是一个纯文本提示",
  icon: "none",
});

注意事项

  1. 持续时间uni.showToast() 默认会在屏幕上持续显示 2 秒,但你可以通过 duration 参数自定义显示时长。

  2. 避免和 uni.showLoading() 同时使用uni.showToast()uni.showLoading() 在界面上的显示位置是重叠的,因此避免它们同时出现。

  3. 简短且有意义的文本:由于 uni.showToast() 仅用于短暂的提示,你应该确保提供的 title 文本简短且对用户有意义。

总的来说,uni.showToast() 是一个很实用的功能,可以在不中断用户当前操作的情况下提供重要反馈。正确使用它可以提高你应用的用户体验。

10.onReachBottom #

onReachBottom 是 uni-app 提供的一个页面生命周期函数,它用于监听用户滑动到页面底部事件。当页面滚动到底部,onReachBottom 会被触发,常常用于实现页面上的“上拉加载更多”功能。

使用场景

最典型的使用场景是列表页。当用户滚动查看列表内容并到达底部时,可以通过 onReachBottom 来自动加载更多内容,从而无缝扩展列表。

参数配置

在页面的配置文件 json 中,你可以通过 onReachBottomDistance 来设置触发 onReachBottom 事件的距离。该值表示距离页面底部多少距离时触发此事件。默认值是 50(单位:px)。

{
  "onReachBottomDistance": 100
}

使用方法

在页面的 <script> 标签中定义 onReachBottom 方法:

export default {
  // ... 其他代码 ...
  onReachBottom() {
    console.log("滚动到底部了!");
    // 你可以在这里请求服务器获取更多的数据,并将它添加到当前的数据列表中
  },
};

注意事项

  1. 频繁触发:用户在滚动时可能会多次触发该事件,所以你应该在实际操作中加入适当的节流或防抖,确保不会因频繁请求服务器导致性能问题。

  2. 加载状态的处理:为了避免在数据还未加载完成时再次触发 onReachBottom,你可能需要设置一个标记表示数据是否正在加载,并在数据加载完成后再将此标记设回初始状态。

  3. 加载完毕处理:如果所有数据已经加载完毕,应该告诉用户没有更多数据了,以避免用户继续无意义的滚动尝试加载更多。

总结,onReachBottom 是 uni-app 提供的一个非常实用的功能,它帮助开发者轻松实现上拉加载更多的交互。但在使用时,需要注意频繁触发和数据状态的处理,确保为用户提供流畅且清晰的用户体验。

11.onPullDownRefresh #

uni-app 中,onPullDownRefresh 是一个页面生命周期函数,用于监听用户的下拉刷新动作。当用户在页面顶部向下滑动时,该事件会被触发,常用于重新获取数据,刷新页面内容。

使用场景 当页面内容需要手动刷新时,例如新闻、社交动态、天气信息等,可以通过用户下拉页面来触发刷新操作。

参数配置 要使页面支持下拉刷新,需要在页面的配置文件 json 中设置 enablePullDownRefresh 属性为 true

{
  "enablePullDownRefresh": true,
  "backgroundTextStyle": "dark"
}

backgroundTextStyle 可以设置为 "dark" 或 "light",表示下拉刷新时的 loading 图标是深色还是浅色。

使用方法

在页面的 <script> 标签中定义 onPullDownRefresh 方法:

export default {
  // ... 其他代码 ...
  onPullDownRefresh() {
    console.log("触发了下拉刷新!");
    // 你可以在这里请求服务器获取最新的数据
    // 一旦数据更新完毕,使用uni.stopPullDownRefresh()来停止下拉刷新动作
    uni.stopPullDownRefresh();
  },
};

注意事项

  1. 主动结束刷新:务必记得在数据加载完毕后调用 uni.stopPullDownRefresh(),否则刷新动画会一直保持,这会影响用户体验。

  2. 下拉距离和阈值:只有当用户下拉一定的距离后,才会触发刷新。这个距离通常是系统或框架预设的,不需要开发者自己配置。

  3. 与其他滚动事件的冲突:如果页面有其他的滚动监听,可能会和下拉刷新产生冲突。要确保各个事件的逻辑处理得当,不会相互干扰。

总结,onPullDownRefresh 提供了一个简单而又直观的方式让用户刷新页面内容。但在使用时,要确保逻辑处理得当,尤其是在异步操作(如网络请求)完成后,正确地停止下拉刷新动作,避免产生不良的用户体验。

12.uni.stopPullDownRefresh #

uni-app 中,uni.stopPullDownRefresh 是一个用于停止当前页面的下拉刷新动作的 API。当页面启用了下拉刷新功能并被用户触发后,你通常需要在数据加载完毕之后,手动调用这个函数来结束下拉刷新的动画和状态,告诉用户刷新已完成。

为什么需要 uni.stopPullDownRefresh

当用户进行下拉刷新操作,通常页面顶部会显示一个旋转的加载图标,提示用户正在进行数据刷新。然而,这个加载图标不会自动消失,因为 uni-app 不知道你何时完成了数据的加载和处理。因此,开发者需要在合适的时机手动调用 uni.stopPullDownRefresh 来结束刷新状态。

如何使用 uni.stopPullDownRefresh

在你的页面或组件的逻辑中,当完成了数据的加载和处理后,直接调用这个函数:

uni.stopPullDownRefresh();

举个例子,在使用 onPullDownRefresh 进行数据更新时:

export default {
  onPullDownRefresh() {
    // 模拟数据请求
    setTimeout(() => {
      // 假设这里数据更新完毕,停止下拉刷新状态
      uni.stopPullDownRefresh();
    }, 2000);
  },
};

注意事项:

  1. 及时调用:确保在数据加载完毕后及时调用 uni.stopPullDownRefresh,否则下拉刷新的加载图标会一直显示,导致用户认为页面仍在刷新。

  2. 错误处理:当数据加载失败或出错时,也应当调用此函数停止刷新状态,并可能通过其他方式(例如 uni.showToast)通知用户。

  3. 不必担心过早调用:即使在数据加载很快,或在未开始下拉刷新的状态下调用此函数,它也不会有任何副作用。但通常,你会在确保有下拉刷新动作后再调用它。

简而言之,uni.stopPullDownRefresh 是用于控制和结束页面的下拉刷新状态的函数,确保在数据加载和处理完毕后正确使用它,以提供良好的用户体验。

13.uni.navigateTo #

uni.navigateTouni-app 中用于页面跳转的 API。当你调用这个方法时,你会打开一个新的页面,并且将当前页面压入页面栈。这种导航方式与很多前端框架和原生应用中的导航行为相似。

使用 uni.navigateTo 的基本语法:

uni.navigateTo({
  url: "/pages/detail/detail",
  success: (res) => {
    console.log("navigate success");
  },
  fail: (err) => {
    console.log("navigate fail", err);
  },
  complete: () => {
    console.log("navigate complete");
  },
});

参数解释:

注意事项:

  1. 页面栈限制uni-app 对页面栈有限制,最多允许 10 个页面同时存在于栈内。当超过这个数量时,最早进入的页面会被销毁。

  2. 页面传值:通过 url 参数可以传递一些简单的参数给下一个页面,这些参数可以在目标页面的 onLoad 生命周期方法的参数中获取。

  3. uni.redirectTo 的区别uni.navigateTo 会保留当前页面,并在上面打开新页面,而 uni.redirectTo 则会关闭当前页面,并打开新页面。所以,使用 navigateTo 后,用户仍可以通过返回操作返回到之前的页面,而使用 redirectTo 则不能。

  4. 路径使用:在 url 参数中可以使用相对路径和绝对路径。使用绝对路径时,需要以 / 开头,如:/pages/detail/detail;使用相对路径时,会相对于当前页面路径,如:../detail/detail

总的来说,uni.navigateTouni-app 中进行页面导航的常用方法。当你需要在当前页面和新页面之间来回切换时,这个方法会非常有用。在设计应用导航时,了解如何使用它,以及何时使用其他导航方法(如 redirectToreLaunch),是很重要的。

14.rich-text #

rich-textuni-app 中用于显示富文本内容的组件。该组件可以解析并显示 HTML、Markdown 或者 JSON 格式的富文本内容。

基本用法:

uni-app 中,你可以使用 rich-text 组件来渲染一个简单的 HTML 或 Markdown 字符串。




属性:

使用 JSON 数据:

除了直接使用 HTML 或 Markdown 字符串外,你还可以使用一个 JSON 格式的数组来描述你的富文本数据。

例如:




注意事项:

  1. 标签和属性支持rich-text 支持的 HTML 标签和属性是有限的。并不是所有的 HTML 标签和 CSS 样式都会被解析和显示。具体支持的标签和属性取决于目标平台。

  2. web-view 的区别:如果你需要显示一个完整的 Web 页面或者需要完整的 HTML/CSS/JS 支持,你应该使用 web-view 组件而不是 rich-textrich-text 更适合用于显示简单的富文本内容,而 web-view 适合用于嵌入完整的 Web 页面。

  3. 事件处理rich-text 不支持事件绑定,也就是说你不能为 rich-text 内的元素绑定点击事件或其他事件。如果需要这种交互,你可能需要考虑其他方式来实现。

  4. 性能考虑:解析和渲染富文本内容可能比较耗费性能,特别是当富文本内容非常复杂时。因此,你应该尽量避免频繁地更新 rich-text 的内容。

总之,rich-textuni-app 提供的一个非常有用的组件,它可以帮助你在应用中展示富文本内容。但同时,你也需要注意它的限制和适用场景,确保在正确的上下文中使用它。

15.uni.setNavigationBarTitle #

uni.setNavigationBarTitle 是 uni-app 提供的一个 API,用于动态设置当前页面的导航条标题。这在你希望根据用户的交互或其他因素动态改变标题时非常有用。

使用方法:

uni.setNavigationBarTitle({
  title: "新的标题文本",
  success: function () {
    console.log("设置标题成功");
  },
  fail: function (err) {
    console.error("设置标题失败:", err);
  },
});

参数说明:

注意事项:

  1. 只能在页面的 .vue 文件中对其导航条使用此 API。
  2. 此 API 只适用于拥有导航条的页面,对于自定义导航的页面无效。
  3. 要确保在页面加载完成之后(例如在 onLoadonReady 或用户交互事件中)调用此 API。

示例应用场景:

  1. 当用户在搜索框中输入查询词并开始搜索时,你可能希望将导航条标题更改为 "搜索结果" 或 "显示与 '查询词' 相关的结果"。
  2. 在内容详情页面,根据内容的标题动态设置导航条的标题。
  3. 在多步骤表单的不同步骤中,根据当前步骤更改导航条的标题,以提供更好的用户体验。

使用 uni.setNavigationBarTitle,你可以轻松地实现这些和其他场景,提供更加动态和响应式的用户界面。

16.markdown-it #

markdown-it 是一个流行的、插件友好的 Markdown 解析器,它可以将 Markdown 格式的文本转换为 HTML 格式。它是用 JavaScript 编写的,因此可以在浏览器和 Node.js 环境中使用。

主要特点:

  1. 高性能:它被设计为与其他同类库相比拥有更快的速度。
  2. 插件系统:可以通过插件轻松地扩展其功能。
  3. 灵活的配置:提供多种配置,以满足不同用户的需求。
  4. CommonMark 兼容:支持 CommonMark 规范,并可选择额外的原生扩展。

基本使用:

安装:

npm install markdown-it

使用:

const MarkdownIt = require("markdown-it");
const md = new MarkdownIt();

const result = md.render("# markdown-it rulezz!");
console.log(result); // 输出:<h1>markdown-it rulezz!</h1>

高级选项:

创建解析器时,你可以为其传递选项:

const md = new MarkdownIt({
  html: true, // 启用 HTML 标签解析
  linkify: true, // 将 URL-like 文本自动转换为链接
  typographer: true, // 将特定的符号替换为打字机友好的字符,如把 (c) 替换为 ©
});

插件系统:

markdown-it 的一个强大之处是其插件体系。有许多现成的插件可供使用,例如插入表情、代码高亮、容器等。

使用插件的例子:

const md = new MarkdownIt()
  .use(require("markdown-it-emoji"))
  .use(require("markdown-it-highlight"));

md.render(":smile: **hello**");

内联与块级渲染:

markdown-it 允许你单独渲染内联内容或块级内容:

定制渲染:

你还可以重写默认的标记渲染方法或添加新的渲染方法,这为高级定制提供了极大的灵活性。

总的来说,markdown-it 是一个功能丰富、易于扩展和定制的 Markdown 解析器,它为开发人员提供了在各种应用程序中处理和展示 Markdown 内容的强大工具。

17.object-fit #

object-fit: cover; 是一个 CSS 属性值,主要应用于 <img><video> 或其他替换元素,用于控制元素的内容如何适应其容器的大小。

object-fit: cover; 的工作方式:

  1. 它保证对象(图像或视频等)完全覆盖容器,而不留任何空白。
  2. 对象将尽可能大地放大,同时保持其原始的宽高比(aspect ratio),以完全填充容器。
  3. 如果对象的宽高比与容器的宽高比不同,那么对象的某些部分可能会被裁剪。

相比于其他 object-fit 属性值:

示例:

想象你有一个正方形的容器和一个宽长的矩形图像。

在实际应用中:

object-fit: cover; 尤其适用于图片或视频背景,确保无论容器的大小或形状如何,内容都能覆盖整个容器,而不留下任何空白或显示不完整。

注意:不是所有浏览器都支持 object-fit。在某些老版本的浏览器中,你可能需要使用备用技巧或 polyfills。

18.list-style-position #

在你提供的代码片段中,/deep/ 是一个深度选择器,这是一个特殊的 CSS 选择器。让我们详细解析它。

/deep/ 选择器

在一些框架中,如 Vue 和其衍生的框架(包括 uni-app)中,当使用组件化的开发模式时,每个组件的样式默认只作用于该组件,不会影响到其他组件或全局样式。这是因为这些框架为每个组件附加了特定的属性,以隔离它们的样式。

但有时,你可能想要为组件内部的子组件或某些深度嵌套的元素应用样式。为此,你可以使用 /deep/ 选择器来确保样式能够“深入”到目标元素,而不受组件样式隔离的影响。

代码片段的含义

/deep/ ol,
/deep/ ul {
  list-style-position: inside;
}

这个代码片段的意思是:在当前组件内,无论有多少层的嵌套,所有的 olul 元素都将应用 list-style-position: inside; 这个样式。

list-style-position: inside; 本身是一个 CSS 属性,它会将列表项标记(例如,有序列表的数字或无序列表的点)放在列表项文本的内部,与文本对齐。

注意点

19.white-space #

当然可以!这段代码在 uni-app(基于 Vue 的框架)中涉及了两部分:/deep/ 选择器和具体的 CSS 属性。我们先从整体上理解这段代码,然后再详细解析每部分。

整体理解

/deep/ code.language-html {
  white-space: pre-wrap;
  word-wrap: break-word;
}

上述代码的含义是:在当前组件内,无论有多少层的嵌套,所有带有 language-html 类的 code 元素都会应用以下的两个 CSS 样式:white-space: pre-wrap;word-wrap: break-word;

/deep/ 选择器

/deep/ 是一个深度选择器,用于确保所选择的样式能够“深入”到 Vue 或 uni-app 组件的子组件中。这是为了克服组件样式的作用域限制,让你能够定义的样式在子组件内生效。

CSS 属性

  1. white-space: pre-wrap;

    这个属性决定了如何处理元素内部的空白字符(空格、换行符、tab 等):

    • pre-wrap 会保留空白字符,并正常换行。
    • pre 类似,但与 pre 不同的是,它会考虑容器的宽度并在必要时换行。
  2. word-wrap: break-word;

    这个属性规定了超出容器边界的长单词或 URL 地址应如何换行:

    • break-word 会在单词的边界上断开并换行。这意味着如果一个单词/URL 过长,超出了容器的宽度,它会被强制换行,以避免溢出。

应用场景

从选择器 code.language-html 可以推测,这个样式可能是为了美化显示 HTML 代码的 code 标签。通过上述的样式,确保代码在容器内正确显示,即使有很长的代码行或 URL,它们也会被适当地换行,而不会超出容器边界。