41
README.md
@@ -1,29 +1,36 @@
|
|||||||
# DailyHot
|
<div align="center">
|
||||||
|
<img alt="logo" height="120" src="./public/favicon.png" width="120"/>
|
||||||
|
<h2>今日热榜</h2>
|
||||||
|
<p>汇聚全网热点,热门尽览无余</p>
|
||||||
|
<br />
|
||||||
|
<img src="./screenshots/main.jpg" style="border-radius: 16px" />
|
||||||
|
</div>
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
|
||||||
|
|
||||||
## Recommended IDE Setup
|
## 示例
|
||||||
|
|
||||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
> 这里是示例站点
|
||||||
|
|
||||||
## Customize configuration
|
- [今日热榜 - https://hot.imsyy.top/](https://hot.imsyy.top/)
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
|
||||||
|
|
||||||
## Project Setup
|
## 部署
|
||||||
|
|
||||||
```sh
|
```bash
|
||||||
npm install
|
// 安装依赖
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
// 开发
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
// 打包
|
||||||
|
pnpm build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
## Vercel 部署
|
||||||
|
|
||||||
```sh
|
现已支持 Vercel 一键部署,无需服务器
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Minify for Production
|
> 请注意,需要修改环境变量中的 API 地址
|
||||||
|
|
||||||
```sh
|

|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"description": "今日热榜",
|
"description": "今日热榜",
|
||||||
"author": "imsyy",
|
"author": "imsyy",
|
||||||
"github": "https://github.com/imsyy",
|
"github": "https://github.com/imsyy",
|
||||||
"version": "0.2.1",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
BIN
public/favicon.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
6
public/ico/powered-by-vercel.svg
Normal file
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo/douban_new.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
BIN
public/logo/genshin.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
public/logo/kuaishou.png
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
public/logo/lol.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
public/logo/netease.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/logo/weread.png
Normal file
|
After Width: | Height: | Size: 4.9 KiB |
BIN
screenshots/main.jpg
Normal file
|
After Width: | Height: | Size: 280 KiB |
10
src/App.vue
@@ -36,6 +36,16 @@ const headerShow = ref(false);
|
|||||||
const backTopChange = (val) => {
|
const backTopChange = (val) => {
|
||||||
headerShow.value = val;
|
headerShow.value = val;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
store.checkNewsUpdate();
|
||||||
|
// 写入默认
|
||||||
|
nextTick(() => {
|
||||||
|
if (store.newsArr.length === 0) {
|
||||||
|
store.newsArr = store.defaultNewsArr;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -8,36 +8,39 @@
|
|||||||
@click="toList"
|
@click="toList"
|
||||||
>
|
>
|
||||||
<template #header>
|
<template #header>
|
||||||
<Transition name="fade" mode="out-in">
|
<n-space class="title" justify="space-between">
|
||||||
<template v-if="!hotListData">
|
<div class="name">
|
||||||
<div class="loading">
|
<n-avatar
|
||||||
<n-skeleton text round />
|
class="ico"
|
||||||
</div>
|
:src="`/logo/${hotData.name}.png`"
|
||||||
</template>
|
fallback-src="/ico/icon_error.png"
|
||||||
<template v-else>
|
/>
|
||||||
<div class="title">
|
<n-text class="name-text">{{ hotData.label }}</n-text>
|
||||||
<n-avatar
|
</div>
|
||||||
class="ico"
|
<n-text v-if="hotListData?.subtitle" class="subtitle" :depth="2">
|
||||||
:src="`/logo/${hotType}.png`"
|
{{ hotListData.subtitle }}
|
||||||
fallback-src="/ico/icon_error.png"
|
</n-text>
|
||||||
/>
|
<n-skeleton v-else width="60px" text round />
|
||||||
<n-text class="name">{{ hotListData.title }}</n-text>
|
</n-space>
|
||||||
<n-text class="subtitle" :depth="2">
|
|
||||||
{{ hotListData.subtitle }}
|
|
||||||
</n-text>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Transition>
|
|
||||||
</template>
|
</template>
|
||||||
<n-scrollbar class="news-list" ref="scrollbarRef">
|
<n-scrollbar class="news-list" ref="scrollbarRef">
|
||||||
<Transition name="fade" mode="out-in">
|
<Transition name="fade" mode="out-in">
|
||||||
<template v-if="!hotListData || listLoading">
|
<template v-if="loadingError">
|
||||||
|
<n-result
|
||||||
|
size="small"
|
||||||
|
status="500"
|
||||||
|
title="哎呀,加载失败了"
|
||||||
|
description="生活总会遇到不如意的事情"
|
||||||
|
style="margin-top: 40px"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!hotListData || listLoading">
|
||||||
<div class="loading">
|
<div class="loading">
|
||||||
<n-skeleton text round :repeat="10" height="20px" />
|
<n-skeleton text round :repeat="10" height="20px" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="lists" :id="hotType + 'Lists'">
|
<div class="lists" :id="hotData.name + 'Lists'">
|
||||||
<div
|
<div
|
||||||
class="item"
|
class="item"
|
||||||
v-for="(item, index) in hotListData.data.slice(0, 15)"
|
v-for="(item, index) in hotListData.data.slice(0, 15)"
|
||||||
@@ -129,10 +132,10 @@ import { useRouter } from "vue-router";
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const store = mainStore();
|
const store = mainStore();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
// 热榜类别
|
// 热榜数据
|
||||||
hotType: {
|
hotData: {
|
||||||
type: String,
|
type: Object,
|
||||||
default: null,
|
default: {},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -140,44 +143,38 @@ const props = defineProps({
|
|||||||
const updateTime = ref(null);
|
const updateTime = ref(null);
|
||||||
|
|
||||||
// 刷新按钮数据
|
// 刷新按钮数据
|
||||||
const lastClickTime = ref(localStorage.getItem(`${props.hotType}Btn`) || 0);
|
const lastClickTime = ref(
|
||||||
|
localStorage.getItem(`${props.hotData.name}Btn`) || 0
|
||||||
|
);
|
||||||
|
|
||||||
// 热榜数据
|
// 热榜数据
|
||||||
const hotListData = ref(null);
|
const hotListData = ref(null);
|
||||||
const scrollbarRef = ref(null);
|
const scrollbarRef = ref(null);
|
||||||
const listLoading = ref(false);
|
const listLoading = ref(false);
|
||||||
|
const loadingError = ref(false);
|
||||||
|
|
||||||
// 获取热榜数据
|
// 获取热榜数据
|
||||||
const getHotListsData = (type, isNew = false) => {
|
const getHotListsData = async (type, isNew = false) => {
|
||||||
// hotListData.value = null;
|
try {
|
||||||
getHotLists(type, isNew)
|
// hotListData.value = null;
|
||||||
.then((res) => {
|
loadingError.value = false;
|
||||||
console.log(res);
|
const result = await getHotLists(type, isNew);
|
||||||
if (res.code === 200) {
|
// console.log(result);
|
||||||
listLoading.value = false;
|
if (result.code === 200) {
|
||||||
hotListData.value = res;
|
listLoading.value = false;
|
||||||
// 滚动至顶部
|
hotListData.value = result;
|
||||||
if (scrollbarRef.value) {
|
// 滚动至顶部
|
||||||
scrollbarRef.value.scrollTo({ position: "top", behavior: "smooth" });
|
if (scrollbarRef.value) {
|
||||||
}
|
scrollbarRef.value.scrollTo({ position: "top", behavior: "smooth" });
|
||||||
} else {
|
|
||||||
$message.error(res.title + res.message);
|
|
||||||
}
|
}
|
||||||
})
|
} else {
|
||||||
.catch((error) => {
|
loadingError.value = true;
|
||||||
console.error("资源请求失败:" + error);
|
$message.error(result.title + result.message);
|
||||||
switch (error?.response.status) {
|
}
|
||||||
case 403:
|
} catch (error) {
|
||||||
router.push("/403");
|
loadingError.value = true;
|
||||||
break;
|
$message.error("热榜加载失败,请重试");
|
||||||
case 500:
|
}
|
||||||
router.push("/500");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
router.push("/404");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取最新数据
|
// 获取最新数据
|
||||||
@@ -186,10 +183,10 @@ const getNewData = () => {
|
|||||||
if (now - lastClickTime.value > 60000) {
|
if (now - lastClickTime.value > 60000) {
|
||||||
// 点击事件
|
// 点击事件
|
||||||
listLoading.value = true;
|
listLoading.value = true;
|
||||||
getHotListsData(props.hotType, true);
|
getHotListsData(props.hotData.name, true);
|
||||||
// 更新最后一次点击时间
|
// 更新最后一次点击时间
|
||||||
lastClickTime.value = now;
|
lastClickTime.value = now;
|
||||||
localStorage.setItem(`${props.hotType}Btn`, now);
|
localStorage.setItem(`${props.hotData.name}Btn`, now);
|
||||||
} else {
|
} else {
|
||||||
// 不执行点击事件
|
// 不执行点击事件
|
||||||
$message.info("请稍后再刷新");
|
$message.info("请稍后再刷新");
|
||||||
@@ -209,11 +206,11 @@ const jumpLink = (data) => {
|
|||||||
|
|
||||||
// 前往全部列表
|
// 前往全部列表
|
||||||
const toList = () => {
|
const toList = () => {
|
||||||
if (props.hotType) {
|
if (props.hotData.name) {
|
||||||
router.push({
|
router.push({
|
||||||
path: "/list",
|
path: "/list",
|
||||||
query: {
|
query: {
|
||||||
type: props.hotType,
|
type: props.hotData.name,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@@ -232,7 +229,7 @@ watch(
|
|||||||
);
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (props.hotType) getHotListsData(props.hotType);
|
if (props.hotData.name) getHotListsData(props.hotData.name);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -255,11 +252,15 @@ onMounted(() => {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
.n-avatar {
|
.name {
|
||||||
background-color: transparent;
|
display: flex;
|
||||||
width: 20px;
|
align-items: center;
|
||||||
height: 20px;
|
.n-avatar {
|
||||||
margin-right: 8px;
|
background-color: transparent;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.subtitle {
|
.subtitle {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|||||||
@@ -1,92 +1,129 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
export const mainStore = defineStore("main", {
|
export const mainStore = defineStore("mainData", {
|
||||||
state: () => {
|
state: () => {
|
||||||
return {
|
return {
|
||||||
// 系统主题
|
// 系统主题
|
||||||
siteTheme: "light",
|
siteTheme: "light",
|
||||||
siteThemeAuto: true,
|
siteThemeAuto: true,
|
||||||
// 新闻类别
|
// 新闻类别
|
||||||
newsArr: [
|
defaultNewsArr: [
|
||||||
{
|
{
|
||||||
label: "哔哩哔哩",
|
label: "哔哩哔哩",
|
||||||
value: "bilibili",
|
name: "bilibili",
|
||||||
order: 0,
|
order: 0,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "微博",
|
label: "微博",
|
||||||
value: "weibo",
|
name: "weibo",
|
||||||
order: 1,
|
order: 1,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "抖音",
|
label: "抖音",
|
||||||
value: "douyin",
|
name: "douyin",
|
||||||
order: 2,
|
order: 2,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "知乎",
|
label: "知乎",
|
||||||
value: "zhihu",
|
name: "zhihu",
|
||||||
order: 3,
|
order: 3,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "36氪",
|
label: "36氪",
|
||||||
value: "36kr",
|
name: "36kr",
|
||||||
order: 4,
|
order: 4,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "百度",
|
label: "百度",
|
||||||
value: "baidu",
|
name: "baidu",
|
||||||
order: 5,
|
order: 5,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "少数派",
|
label: "少数派",
|
||||||
value: "sspai",
|
name: "sspai",
|
||||||
order: 6,
|
order: 6,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "IT之家",
|
label: "IT之家",
|
||||||
value: "ithome",
|
name: "ithome",
|
||||||
order: 7,
|
order: 7,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "澎湃新闻",
|
label: "澎湃新闻",
|
||||||
value: "thepaper",
|
name: "thepaper",
|
||||||
order: 8,
|
order: 8,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "今日头条",
|
label: "今日头条",
|
||||||
value: "toutiao",
|
name: "toutiao",
|
||||||
order: 9,
|
order: 9,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "百度贴吧",
|
label: "百度贴吧",
|
||||||
value: "tieba",
|
name: "tieba",
|
||||||
order: 10,
|
order: 10,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "稀土掘金",
|
label: "稀土掘金",
|
||||||
value: "juejin",
|
name: "juejin",
|
||||||
order: 11,
|
order: 11,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "腾讯新闻",
|
label: "腾讯新闻",
|
||||||
value: "newsqq",
|
name: "newsqq",
|
||||||
order: 12,
|
order: 12,
|
||||||
show: true,
|
show: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "豆瓣",
|
||||||
|
name: "douban_new",
|
||||||
|
order: 13,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "原神",
|
||||||
|
name: "genshin",
|
||||||
|
order: 14,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "LOL",
|
||||||
|
name: "lol",
|
||||||
|
order: 15,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "快手",
|
||||||
|
name: "kuaishou",
|
||||||
|
order: 16,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "网易新闻",
|
||||||
|
name: "netease",
|
||||||
|
order: 17,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "微信读书",
|
||||||
|
name: "weread",
|
||||||
|
order: 18,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
|
newsArr: [],
|
||||||
// 链接跳转方式
|
// 链接跳转方式
|
||||||
linkOpenType: "open",
|
linkOpenType: "open",
|
||||||
// 页头固定
|
// 页头固定
|
||||||
@@ -105,6 +142,30 @@ export const mainStore = defineStore("main", {
|
|||||||
this.siteTheme = val;
|
this.siteTheme = val;
|
||||||
this.siteThemeAuto = false;
|
this.siteThemeAuto = false;
|
||||||
},
|
},
|
||||||
|
// 检查更新
|
||||||
|
checkNewsUpdate() {
|
||||||
|
const mainData = JSON.parse(localStorage.getItem("mainData"));
|
||||||
|
let updatedNum = 0;
|
||||||
|
if (!mainData) return false;
|
||||||
|
console.log("列表尝试更新", this.defaultNewsArr, this.newsArr);
|
||||||
|
// 执行比较并迁移
|
||||||
|
if (this.newsArr.length > 0) {
|
||||||
|
for (const newItem of this.defaultNewsArr) {
|
||||||
|
const exists = this.newsArr.some(
|
||||||
|
(news) => newItem.label === news.label && newItem.name === news.name
|
||||||
|
);
|
||||||
|
if (!exists) {
|
||||||
|
console.log("列表有更新:", newItem);
|
||||||
|
updatedNum++;
|
||||||
|
this.newsArr.push(newItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updatedNum) $message.success(`成功更新 ${updatedNum} 个榜单数据`);
|
||||||
|
} else {
|
||||||
|
console.log("列表无内容,写入默认");
|
||||||
|
this.newsArr = this.defaultNewsArr;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
persist: [
|
persist: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
:key="item"
|
:key="item"
|
||||||
:style="{ animationDelay: index / 10 + 0.2 + 's' }"
|
:style="{ animationDelay: index / 10 + 0.2 + 's' }"
|
||||||
>
|
>
|
||||||
<HotList :hotType="item.value" />
|
<HotList :hotData="item" />
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
<div class="error" v-else>
|
<div class="error" v-else>
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
class="tag"
|
class="tag"
|
||||||
v-for="item in store.newsArr.filter((item) => item.show)"
|
v-for="item in store.newsArr.filter((item) => item.show)"
|
||||||
:key="item"
|
:key="item"
|
||||||
:type="item.value === listType ? 'primary' : 'default'"
|
:type="item.name === listType ? 'primary' : 'default'"
|
||||||
@click="changeType(item.value)"
|
@click="changeType(item.name)"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
<template #avatar>
|
<template #avatar>
|
||||||
<img :src="`/logo/${item.value}.png`" alt="logo" class="logo" />
|
<img :src="`/logo/${item.name}.png`" alt="logo" class="logo" />
|
||||||
</template>
|
</template>
|
||||||
</n-tag>
|
</n-tag>
|
||||||
</n-space>
|
</n-space>
|
||||||
@@ -131,7 +131,7 @@ const store = mainStore();
|
|||||||
|
|
||||||
const updateTime = ref(null);
|
const updateTime = ref(null);
|
||||||
const listType = ref(
|
const listType = ref(
|
||||||
router.currentRoute.value.query.type || store.newsArr[0].value
|
router.currentRoute.value.query.type || store.newsArr[0].name
|
||||||
);
|
);
|
||||||
const pageNumber = ref(
|
const pageNumber = ref(
|
||||||
router.currentRoute.value.query.page
|
router.currentRoute.value.query.page
|
||||||
|
|||||||
@@ -79,11 +79,7 @@
|
|||||||
:content-style="{ display: 'flex', alignItems: 'center' }"
|
:content-style="{ display: 'flex', alignItems: 'center' }"
|
||||||
>
|
>
|
||||||
<div class="desc" :style="{ opacity: element.show ? null : 0.6 }">
|
<div class="desc" :style="{ opacity: element.show ? null : 0.6 }">
|
||||||
<img
|
<img class="logo" :src="`/logo/${element.name}.png`" alt="logo" />
|
||||||
class="logo"
|
|
||||||
:src="`/logo/${element.value}.png`"
|
|
||||||
alt="logo"
|
|
||||||
/>
|
|
||||||
<n-text class="news-name" v-html="element.label" />
|
<n-text class="news-name" v-html="element.label" />
|
||||||
</div>
|
</div>
|
||||||
<n-switch
|
<n-switch
|
||||||
@@ -174,7 +170,7 @@ const saveSoreData = (name = null, open = false) => {
|
|||||||
|
|
||||||
// 重置数据
|
// 重置数据
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
if ($timeInterval) clearInterval($timeInterval);
|
if (typeof $timeInterval !== "undefined") clearInterval($timeInterval);
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
location.reload();
|
location.reload();
|
||||||
};
|
};
|
||||||
|
|||||||
3
vercel.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"rewrites": [{ "source": "/:path*", "destination": "/index.html" }]
|
||||||
|
}
|
||||||