完善页面
6
.env
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
# 全局 API 地址
|
||||||
|
# VITE_GLOBAL_API="http://localhost:6688"
|
||||||
|
VITE_GLOBAL_API="https://api-hot.imsyy.top"
|
||||||
|
|
||||||
|
# ICP 备案号
|
||||||
|
VITE_ICP = "豫ICP备2022018134号-1"
|
||||||
28
.gitignore
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
coverage
|
||||||
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||||
|
}
|
||||||
29
README.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# DailyHot
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vitejs.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
1
dev-dist/registerSW.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
if('serviceWorker' in navigator) navigator.serviceWorker.register('/dev-sw.js?dev-sw', { scope: '/', type: 'classic' })
|
||||||
100
dev-dist/sw.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
/**
|
||||||
|
* Copyright 2018 Google Inc. All Rights Reserved.
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// If the loader is already loaded, just stop.
|
||||||
|
if (!self.define) {
|
||||||
|
let registry = {};
|
||||||
|
|
||||||
|
// Used for `eval` and `importScripts` where we can't get script URL by other means.
|
||||||
|
// In both cases, it's safe to use a global var because those functions are synchronous.
|
||||||
|
let nextDefineUri;
|
||||||
|
|
||||||
|
const singleRequire = (uri, parentUri) => {
|
||||||
|
uri = new URL(uri + ".js", parentUri).href;
|
||||||
|
return registry[uri] || (
|
||||||
|
|
||||||
|
new Promise(resolve => {
|
||||||
|
if ("document" in self) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.src = uri;
|
||||||
|
script.onload = resolve;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
} else {
|
||||||
|
nextDefineUri = uri;
|
||||||
|
importScripts(uri);
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
.then(() => {
|
||||||
|
let promise = registry[uri];
|
||||||
|
if (!promise) {
|
||||||
|
throw new Error(`Module ${uri} didn’t register its module`);
|
||||||
|
}
|
||||||
|
return promise;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.define = (depsNames, factory) => {
|
||||||
|
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
|
||||||
|
if (registry[uri]) {
|
||||||
|
// Module is already loading or loaded.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let exports = {};
|
||||||
|
const require = depUri => singleRequire(depUri, uri);
|
||||||
|
const specialDeps = {
|
||||||
|
module: { uri },
|
||||||
|
exports,
|
||||||
|
require
|
||||||
|
};
|
||||||
|
registry[uri] = Promise.all(depsNames.map(
|
||||||
|
depName => specialDeps[depName] || require(depName)
|
||||||
|
)).then(deps => {
|
||||||
|
factory(...deps);
|
||||||
|
return exports;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
define(['./workbox-25adc094'], (function (workbox) { 'use strict';
|
||||||
|
|
||||||
|
self.skipWaiting();
|
||||||
|
workbox.clientsClaim();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The precacheAndRoute() method efficiently caches and responds to
|
||||||
|
* requests for URLs in the manifest.
|
||||||
|
* See https://goo.gl/S9QRab
|
||||||
|
*/
|
||||||
|
workbox.precacheAndRoute([{
|
||||||
|
"url": "registerSW.js",
|
||||||
|
"revision": "3ca0b8505b4bec776b69afdba2768812"
|
||||||
|
}, {
|
||||||
|
"revision": null,
|
||||||
|
"url": "index.html"
|
||||||
|
}], {});
|
||||||
|
workbox.cleanupOutdatedCaches();
|
||||||
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
allowlist: [/^\/$/]
|
||||||
|
}));
|
||||||
|
workbox.registerRoute(/(.*?)\.(woff2|woff|ttf)/, new workbox.CacheFirst({
|
||||||
|
"cacheName": "file-cache",
|
||||||
|
plugins: []
|
||||||
|
}), 'GET');
|
||||||
|
workbox.registerRoute(/(.*?)\.(webp|png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps)/, new workbox.CacheFirst({
|
||||||
|
"cacheName": "image-cache",
|
||||||
|
plugins: []
|
||||||
|
}), 'GET');
|
||||||
|
|
||||||
|
}));
|
||||||
3495
dev-dist/workbox-25adc094.js
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="ico/favicon.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>今日热榜 - 汇聚全网热点,热门尽览无余</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
35
package.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "dailyhot",
|
||||||
|
"description": "今日热榜",
|
||||||
|
"author": "imsyy",
|
||||||
|
"github": "https://github.com/imsyy",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@icon-park/vue-next": "^1.4.2",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.14",
|
||||||
|
"axios": "^1.3.3",
|
||||||
|
"lunar-calendar": "^0.1.4",
|
||||||
|
"pinia": "^2.0.28",
|
||||||
|
"pinia-plugin-persistedstate": "^3.1.0",
|
||||||
|
"sass": "^1.56.1",
|
||||||
|
"scrollreveal": "^4.0.9",
|
||||||
|
"terser": "^5.16.5",
|
||||||
|
"vue": "^3.2.45",
|
||||||
|
"vue-router": "^4.1.6",
|
||||||
|
"vuedraggable": "^4.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
"naive-ui": "^2.34.3",
|
||||||
|
"unplugin-auto-import": "^0.12.0",
|
||||||
|
"unplugin-vue-components": "^0.22.11",
|
||||||
|
"vite": "^4.0.0",
|
||||||
|
"vite-plugin-pwa": "^0.14.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
3712
pnpm-lock.yaml
generated
Normal file
BIN
public/ico/error.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
public/ico/favicon.png
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/ico/icon_error.png
Normal file
|
After Width: | Height: | Size: 2.9 KiB |
BIN
public/logo/36kr.png
Normal file
|
After Width: | Height: | Size: 9.0 KiB |
BIN
public/logo/baidu.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/logo/bilibili.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
BIN
public/logo/ithome.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/logo/juejin.png
Normal file
|
After Width: | Height: | Size: 6.2 KiB |
BIN
public/logo/newsqq.png
Normal file
|
After Width: | Height: | Size: 7.8 KiB |
BIN
public/logo/sspai.png
Normal file
|
After Width: | Height: | Size: 8.7 KiB |
BIN
public/logo/thepaper.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/logo/tieba.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
public/logo/toutiao.png
Normal file
|
After Width: | Height: | Size: 5.1 KiB |
BIN
public/logo/weibo.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/logo/zhihu.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
103
src/App.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<Provider>
|
||||||
|
<n-layout
|
||||||
|
embedded
|
||||||
|
:native-scrollbar="false"
|
||||||
|
:class="store.headerFixed ? 'fixed' : null"
|
||||||
|
>
|
||||||
|
<n-back-top :visibility-height="2" @update:show="backTopChange" />
|
||||||
|
<Header :class="headerShow ? 'show' : null" />
|
||||||
|
<main>
|
||||||
|
<router-view v-slot="{ Component }">
|
||||||
|
<keep-alive>
|
||||||
|
<transition name="scale" mode="out-in">
|
||||||
|
<component :is="Component" />
|
||||||
|
</transition>
|
||||||
|
</keep-alive>
|
||||||
|
</router-view>
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
</n-layout>
|
||||||
|
</Provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { mainStore } from "@/store";
|
||||||
|
import Provider from "@/components/Provider.vue";
|
||||||
|
import Header from "@/components/Header.vue";
|
||||||
|
import Footer from "@/components/Footer.vue";
|
||||||
|
|
||||||
|
const store = mainStore();
|
||||||
|
|
||||||
|
// 顶栏显隐
|
||||||
|
const headerShow = ref(false);
|
||||||
|
|
||||||
|
// 回顶按钮显隐
|
||||||
|
const backTopChange = (val) => {
|
||||||
|
headerShow.value = val;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.n-layout {
|
||||||
|
height: 100%;
|
||||||
|
&.fixed {
|
||||||
|
.header {
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
&.show {
|
||||||
|
height: 70px;
|
||||||
|
border-bottom: 2px solid var(--n-border-color);
|
||||||
|
background-color: var(--n-color);
|
||||||
|
:deep(section) {
|
||||||
|
.logo {
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
span {
|
||||||
|
&:nth-of-type(1) {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 118px 5vw 0 5vw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.n-scrollbar-rail) {
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
main {
|
||||||
|
padding: 0 5vw;
|
||||||
|
max-width: 1800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
min-height: calc(100vh - 238px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由跳转动画
|
||||||
|
.scale-enter-active,
|
||||||
|
.scale-leave-active {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scale-enter-from,
|
||||||
|
.scale-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
14
src/api/index.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import axios from "@/api/request";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取热榜分类数据
|
||||||
|
* @param {string} type 热榜分类名称
|
||||||
|
* @param {boolean} isNew 是否拉取最新数据
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getHotLists = (type, isNew) => {
|
||||||
|
return axios({
|
||||||
|
method: "GET",
|
||||||
|
url: `/${type}${isNew ? "/new" : "/"}`,
|
||||||
|
});
|
||||||
|
};
|
||||||
69
src/api/request.js
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import axios from "axios";
|
||||||
|
|
||||||
|
switch (process.env.NODE_ENV) {
|
||||||
|
case "production":
|
||||||
|
axios.defaults.baseURL = import.meta.env.VITE_GLOBAL_API;
|
||||||
|
break;
|
||||||
|
case "development":
|
||||||
|
axios.defaults.baseURL = import.meta.env.VITE_GLOBAL_API;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
axios.defaults.baseURL = import.meta.env.VITE_GLOBAL_API;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
axios.defaults.timeout = 30000;
|
||||||
|
axios.defaults.headers = { "Content-Type": "application/json" };
|
||||||
|
|
||||||
|
// 请求拦截
|
||||||
|
axios.interceptors.request.use(
|
||||||
|
(request) => {
|
||||||
|
// if (request.loadingBar != "Hidden") $loadingBar.start();
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (token) {
|
||||||
|
request.headers.Authorization = token;
|
||||||
|
}
|
||||||
|
return request;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
// $loadingBar.error();
|
||||||
|
$message.error("请求失败,请稍后重试");
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
// $loadingBar.finish();
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
$loadingBar.error();
|
||||||
|
if (error.response) {
|
||||||
|
let data = error.response.data;
|
||||||
|
switch (error.response.status) {
|
||||||
|
case 401:
|
||||||
|
$message.error(data.message ? data.message : "请登录后使用");
|
||||||
|
break;
|
||||||
|
case 301:
|
||||||
|
$message.error(data.message ? data.message : "请求路径发生跳转");
|
||||||
|
break;
|
||||||
|
case 404:
|
||||||
|
$message.error(data.message ? data.message : "请求资源不存在");
|
||||||
|
break;
|
||||||
|
case 500:
|
||||||
|
$message.error(data.message ? data.message : "内部服务器错误");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$message.error(data.message ? data.message : "请求失败,请稍后重试");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message.error(data.message ? data.message : "请求失败,请稍后重试");
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export default axios;
|
||||||
68
src/components/Footer.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<footer>
|
||||||
|
<div class="copyright">
|
||||||
|
<n-text class="description" v-html="packageJson.description" />
|
||||||
|
<n-text
|
||||||
|
class="author"
|
||||||
|
:depth="3"
|
||||||
|
v-html="packageJson.author"
|
||||||
|
@click="jumpLink(packageJson.github)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<n-text
|
||||||
|
v-if="icp"
|
||||||
|
:depth="3"
|
||||||
|
class="icp"
|
||||||
|
v-html="icp"
|
||||||
|
@click="jumpLink('https://beian.miit.gov.cn/')"
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import packageJson from "@/../package.json";
|
||||||
|
|
||||||
|
const icp = ref(import.meta.env.VITE_ICP ? import.meta.env.VITE_ICP : null);
|
||||||
|
|
||||||
|
// 链接跳转
|
||||||
|
const jumpLink = (url) => {
|
||||||
|
window.open(url);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
footer {
|
||||||
|
height: 100px;
|
||||||
|
padding: 0 5vw;
|
||||||
|
max-width: 1800px;
|
||||||
|
margin: 20px auto 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
.copyright {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
.description {
|
||||||
|
&::after {
|
||||||
|
content: "@ Copyright By";
|
||||||
|
margin: 0 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.author {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
&:hover {
|
||||||
|
color: var(--n-code-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.icp {
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
&:hover {
|
||||||
|
color: var(--n-code-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
291
src/components/Header.vue
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
<template>
|
||||||
|
<n-card :bordered="false" class="header" content-style="padding: 0">
|
||||||
|
<section>
|
||||||
|
<div class="logo" @click="router.push('/')">
|
||||||
|
<img src="/ico/favicon.png" alt="logo" />
|
||||||
|
<div class="name">
|
||||||
|
<n-text>今日热榜</n-text>
|
||||||
|
<n-text :depth="3">汇聚全网热点,热门尽览无余</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="current-time" v-if="store.timeData">
|
||||||
|
<n-text class="time">{{ store.timeData.time.text }}</n-text>
|
||||||
|
<n-text class="date" :depth="3">
|
||||||
|
{{
|
||||||
|
store.timeData.lunar.GanZhiYear +
|
||||||
|
"年 " +
|
||||||
|
store.timeData.lunar.text +
|
||||||
|
" " +
|
||||||
|
store.timeData.time.weekday
|
||||||
|
}}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
<div class="current-time" v-else>
|
||||||
|
<n-text class="time">时间获取中</n-text>
|
||||||
|
</div>
|
||||||
|
<div class="controls">
|
||||||
|
<n-space justify="end">
|
||||||
|
<n-popover>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button secondary strong round @click="router.go(0)">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Refresh" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
刷新页面
|
||||||
|
</n-popover>
|
||||||
|
<n-popover>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
secondary
|
||||||
|
strong
|
||||||
|
round
|
||||||
|
@click="
|
||||||
|
store.setSiteTheme(
|
||||||
|
store.siteTheme === 'light' ? 'dark' : 'light'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon
|
||||||
|
:component="store.siteTheme === 'light' ? Moon : SunOne"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
{{ store.siteTheme === "light" ? "深色模式" : "浅色模式" }}
|
||||||
|
</n-popover>
|
||||||
|
<n-popover>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button secondary strong round @click="router.push('/setting')">
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="SettingTwo" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
全局设置
|
||||||
|
</n-popover>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
<div class="mobile">
|
||||||
|
<n-dropdown
|
||||||
|
:options="menuOptions"
|
||||||
|
size="large"
|
||||||
|
trigger="click"
|
||||||
|
placement="bottom-end"
|
||||||
|
@select="menuOptionsSelect"
|
||||||
|
>
|
||||||
|
<n-button secondary strong round>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="HamburgerButton" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</n-dropdown>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
SunOne,
|
||||||
|
Moon,
|
||||||
|
Refresh,
|
||||||
|
SettingTwo,
|
||||||
|
HamburgerButton,
|
||||||
|
} from "@icon-park/vue-next";
|
||||||
|
import { getCurrentTime } from "@/utils/getTime.js";
|
||||||
|
import { mainStore } from "@/store";
|
||||||
|
import { NText, NIcon } from "naive-ui";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const store = mainStore();
|
||||||
|
|
||||||
|
const timeInterval = ref(null);
|
||||||
|
|
||||||
|
// 移动端时间模块
|
||||||
|
const timeRender = () => {
|
||||||
|
return h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "6px 18px",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h(NText, null, {
|
||||||
|
default: () =>
|
||||||
|
store.timeData ? store.timeData.time.text : "时间获取失败",
|
||||||
|
}),
|
||||||
|
h(
|
||||||
|
NText,
|
||||||
|
{ depth: 3, style: "font-size: 12px" },
|
||||||
|
{
|
||||||
|
default: () =>
|
||||||
|
store.timeData
|
||||||
|
? store.timeData.lunar.GanZhiYear +
|
||||||
|
"年 " +
|
||||||
|
store.timeData.lunar.text +
|
||||||
|
" " +
|
||||||
|
store.timeData.time.weekday
|
||||||
|
: "日期获取失败",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 移动端菜单
|
||||||
|
const menuOptions = [
|
||||||
|
{
|
||||||
|
key: "header",
|
||||||
|
type: "render",
|
||||||
|
render: timeRender,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "header-divider",
|
||||||
|
type: "divider",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "刷新页面",
|
||||||
|
key: "refresh",
|
||||||
|
icon: () => {
|
||||||
|
return h(NIcon, null, {
|
||||||
|
default: () => h(Refresh),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: () => {
|
||||||
|
return h(NText, null, {
|
||||||
|
default: () => (store.siteTheme === "light" ? "深色模式" : "浅色模式"),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
key: "changeTheme",
|
||||||
|
icon: () => {
|
||||||
|
return h(NIcon, null, {
|
||||||
|
default: () => (store.siteTheme === "light" ? h(Moon) : h(SunOne)),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "全局设置",
|
||||||
|
key: "setting",
|
||||||
|
icon: () => {
|
||||||
|
return h(NIcon, null, {
|
||||||
|
default: () => h(SettingTwo),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 移动端下拉菜单点击事件
|
||||||
|
const menuOptionsSelect = (val) => {
|
||||||
|
if (val === "refresh") {
|
||||||
|
router.go(0);
|
||||||
|
} else if (val === "changeTheme") {
|
||||||
|
store.setSiteTheme(store.siteTheme === "light" ? "dark" : "light");
|
||||||
|
} else if (val === "setting") {
|
||||||
|
router.push("/setting");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.$timeInterval = timeInterval.value = setInterval(() => {
|
||||||
|
store.timeData = getCurrentTime();
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
clearInterval(timeInterval.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.header {
|
||||||
|
height: 118px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px 5vw;
|
||||||
|
z-index: 2;
|
||||||
|
top: 0;
|
||||||
|
background-color: transparent;
|
||||||
|
transition: all 0.3s;
|
||||||
|
section {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
img {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
margin-right: 16px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
span {
|
||||||
|
&:nth-of-type(1) {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
&:nth-of-type(2) {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.current-time {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
.date {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.mobile {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
display: flex;
|
||||||
|
.logo {
|
||||||
|
img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
span {
|
||||||
|
&:nth-of-type(1) {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.current-time,
|
||||||
|
.controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.mobile {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
362
src/components/HotList.vue
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
<template>
|
||||||
|
<n-card
|
||||||
|
hoverable
|
||||||
|
class="hot-list"
|
||||||
|
:header-style="{ padding: '16px' }"
|
||||||
|
:content-style="{ padding: '0 16px' }"
|
||||||
|
:footer-style="{ padding: '16px' }"
|
||||||
|
@click="toList"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<template v-if="!hotListData">
|
||||||
|
<div class="loading">
|
||||||
|
<n-skeleton text round />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="title">
|
||||||
|
<n-avatar
|
||||||
|
class="ico"
|
||||||
|
:src="`/logo/${hotType}.png`"
|
||||||
|
fallback-src="/ico/icon_error.png"
|
||||||
|
/>
|
||||||
|
<n-text class="name">{{ hotListData.title }}</n-text>
|
||||||
|
<n-text class="subtitle" :depth="2">
|
||||||
|
{{ hotListData.subtitle }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
<n-scrollbar class="news-list" ref="scrollbarRef">
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<template v-if="!hotListData || listLoading">
|
||||||
|
<div class="loading">
|
||||||
|
<n-skeleton text round :repeat="10" height="20px" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="lists" :id="hotType + 'Lists'">
|
||||||
|
<div
|
||||||
|
class="item"
|
||||||
|
v-for="(item, index) in hotListData.data.slice(0, 15)"
|
||||||
|
:key="item"
|
||||||
|
>
|
||||||
|
<n-text
|
||||||
|
class="num"
|
||||||
|
:class="
|
||||||
|
index === 0
|
||||||
|
? 'one'
|
||||||
|
: index === 1
|
||||||
|
? 'two'
|
||||||
|
: index === 2
|
||||||
|
? 'three'
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
:depth="2"
|
||||||
|
>{{ index + 1 }}</n-text
|
||||||
|
>
|
||||||
|
<n-text class="text" @click.stop="jumpLink(item)">
|
||||||
|
{{ item.title }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Transition>
|
||||||
|
</n-scrollbar>
|
||||||
|
<template #footer>
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<template v-if="!hotListData">
|
||||||
|
<div class="loading">
|
||||||
|
<n-skeleton text round />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="message">
|
||||||
|
<n-text class="time" :depth="3" v-if="updateTime">
|
||||||
|
{{ updateTime }}
|
||||||
|
</n-text>
|
||||||
|
<n-text class="time" :depth="3" v-else> 获取失败 </n-text>
|
||||||
|
<n-space class="controls">
|
||||||
|
<n-popover v-if="hotListData.data.length > 15">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
secondary
|
||||||
|
strong
|
||||||
|
round
|
||||||
|
@click.stop="toList"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="More" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
查看更多
|
||||||
|
</n-popover>
|
||||||
|
<n-popover>
|
||||||
|
<template #trigger>
|
||||||
|
<n-button
|
||||||
|
size="tiny"
|
||||||
|
secondary
|
||||||
|
strong
|
||||||
|
round
|
||||||
|
@click.stop="getNewData"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Refresh" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</template>
|
||||||
|
获取最新
|
||||||
|
</n-popover>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Refresh, More } from "@icon-park/vue-next";
|
||||||
|
import { getHotLists } from "@/api";
|
||||||
|
import { formatTime } from "@/utils/getTime";
|
||||||
|
import { mainStore } from "@/store";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const store = mainStore();
|
||||||
|
const props = defineProps({
|
||||||
|
// 热榜类别
|
||||||
|
hotType: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新时间
|
||||||
|
const updateTime = ref(null);
|
||||||
|
|
||||||
|
// 刷新按钮数据
|
||||||
|
const lastClickTime = ref(localStorage.getItem(`${props.hotType}Btn`) || 0);
|
||||||
|
|
||||||
|
// 热榜数据
|
||||||
|
const hotListData = ref(null);
|
||||||
|
const scrollbarRef = ref(null);
|
||||||
|
const listLoading = ref(false);
|
||||||
|
|
||||||
|
// 获取热榜数据
|
||||||
|
const getHotListsData = (type, isNew = false) => {
|
||||||
|
// hotListData.value = null;
|
||||||
|
getHotLists(type, isNew).then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
if (res.code === 200) {
|
||||||
|
listLoading.value = false;
|
||||||
|
hotListData.value = res;
|
||||||
|
// 滚动至顶部
|
||||||
|
if (scrollbarRef.value) {
|
||||||
|
scrollbarRef.value.scrollTo({ position: "top", behavior: "smooth" });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$message.error(res.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取最新数据
|
||||||
|
const getNewData = () => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastClickTime.value > 60000) {
|
||||||
|
// 点击事件
|
||||||
|
listLoading.value = true;
|
||||||
|
getHotListsData(props.hotType, true);
|
||||||
|
// 更新最后一次点击时间
|
||||||
|
lastClickTime.value = now;
|
||||||
|
localStorage.setItem(`${props.hotType}Btn`, now);
|
||||||
|
} else {
|
||||||
|
// 不执行点击事件
|
||||||
|
$message.info("请稍后再刷新");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 链接跳转
|
||||||
|
const jumpLink = (data) => {
|
||||||
|
if (!data.url || !data.mobileUrl) return $message.error("链接不存在");
|
||||||
|
const url = window.innerWidth > 680 ? data.url : data.mobileUrl;
|
||||||
|
if (store.linkOpenType === "open") {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
} else if (store.linkOpenType === "href") {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 前往全部列表
|
||||||
|
const toList = () => {
|
||||||
|
if (props.hotType) {
|
||||||
|
router.push({
|
||||||
|
path: "/list",
|
||||||
|
query: {
|
||||||
|
type: props.hotType,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
$message.error("数据出错,请重试");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 实时改变更新时间
|
||||||
|
watch(
|
||||||
|
() => store.timeData,
|
||||||
|
() => {
|
||||||
|
if (hotListData.value) {
|
||||||
|
updateTime.value = formatTime(hotListData.value.updateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.hotType) getHotListsData(props.hotType);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.hot-list {
|
||||||
|
border-radius: 12px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 16px;
|
||||||
|
height: 26px;
|
||||||
|
.n-avatar {
|
||||||
|
background-color: transparent;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
height: 24px;
|
||||||
|
.time {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.news-list) {
|
||||||
|
height: 300px;
|
||||||
|
.n-scrollbar-rail {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 300px;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lists {
|
||||||
|
padding-right: 6px;
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
min-height: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
cursor: pointer;
|
||||||
|
&:nth-last-of-type(1) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
.num {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--n-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--n-close-color-hover);
|
||||||
|
}
|
||||||
|
&.one {
|
||||||
|
background-color: #ea444d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&.two {
|
||||||
|
background-color: #ed702d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&.three {
|
||||||
|
background-color: #eead3f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
&:hover {
|
||||||
|
transform: translateX(4px);
|
||||||
|
&::after {
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
&:active {
|
||||||
|
color: #ea444d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
max-height: 2px;
|
||||||
|
background-color: var(--n-close-color-pressed);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
bottom: -2px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.n-card-header) {
|
||||||
|
.loading {
|
||||||
|
height: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:deep(.n-card__footer) {
|
||||||
|
.loading {
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
src/components/Provider.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<!-- 全局配置组件 -->
|
||||||
|
<template>
|
||||||
|
<n-config-provider
|
||||||
|
abstract
|
||||||
|
inline-theme-disabled
|
||||||
|
:locale="zhCN"
|
||||||
|
:date-locale="dateZhCN"
|
||||||
|
:theme="theme"
|
||||||
|
:theme-overrides="themeOverrides"
|
||||||
|
>
|
||||||
|
<n-loading-bar-provider>
|
||||||
|
<n-dialog-provider>
|
||||||
|
<n-notification-provider>
|
||||||
|
<n-message-provider>
|
||||||
|
<NaiveProviderContent />
|
||||||
|
<slot></slot>
|
||||||
|
</n-message-provider>
|
||||||
|
</n-notification-provider>
|
||||||
|
</n-dialog-provider>
|
||||||
|
</n-loading-bar-provider>
|
||||||
|
</n-config-provider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
zhCN,
|
||||||
|
dateZhCN,
|
||||||
|
darkTheme,
|
||||||
|
useOsTheme,
|
||||||
|
useLoadingBar,
|
||||||
|
useDialog,
|
||||||
|
useMessage,
|
||||||
|
useNotification,
|
||||||
|
} from "naive-ui";
|
||||||
|
import { mainStore } from "@/store";
|
||||||
|
|
||||||
|
const store = mainStore();
|
||||||
|
|
||||||
|
// 明暗切换
|
||||||
|
let theme = ref(null);
|
||||||
|
const changeTheme = () => {
|
||||||
|
if (store.siteTheme === "light") {
|
||||||
|
theme.value = null;
|
||||||
|
} else if (store.siteTheme === "dark") {
|
||||||
|
theme.value = darkTheme;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听明暗变化
|
||||||
|
watch(
|
||||||
|
() => store.siteTheme,
|
||||||
|
() => {
|
||||||
|
changeTheme();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 监听系统明暗变化
|
||||||
|
const osThemeRef = useOsTheme();
|
||||||
|
watch(
|
||||||
|
() => osThemeRef.value,
|
||||||
|
(value) => {
|
||||||
|
value == "dark" ? store.setSiteTheme("dark") : store.setSiteTheme("light");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 配置主题色
|
||||||
|
const themeOverrides = {
|
||||||
|
common: {
|
||||||
|
primaryColor: "#ea444d",
|
||||||
|
primaryColorHover: "#F57B74",
|
||||||
|
primaryColorSuppl: "#F57B74",
|
||||||
|
primaryColorPressed: "#F64B41",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 挂载 naive 组件的方法
|
||||||
|
const setupNaiveTools = () => {
|
||||||
|
window.$loadingBar = useLoadingBar(); // 进度条
|
||||||
|
window.$notification = useNotification(); // 通知
|
||||||
|
window.$message = useMessage(); // 信息
|
||||||
|
window.$dialog = useDialog(); // 对话框
|
||||||
|
};
|
||||||
|
|
||||||
|
const NaiveProviderContent = defineComponent({
|
||||||
|
setup() {
|
||||||
|
setupNaiveTools();
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
return h("div", {
|
||||||
|
class: {
|
||||||
|
tools: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
changeTheme();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
18
src/main.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { createApp } from "vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||||
|
|
||||||
|
import App from "./App.vue";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
|
// 全局样式
|
||||||
|
import "@/style/global.scss";
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
pinia.use(piniaPluginPersistedstate);
|
||||||
|
app.use(pinia);
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
18
src/router/index.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { createRouter, createWebHashHistory } from "vue-router";
|
||||||
|
import routes from "@/router/routes";
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHashHistory(),
|
||||||
|
routes,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach(() => {
|
||||||
|
$loadingBar.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
router.afterEach(() => {
|
||||||
|
$loadingBar.finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
53
src/router/routes.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
const routes = [
|
||||||
|
// 首页
|
||||||
|
{
|
||||||
|
path: "/",
|
||||||
|
name: "home",
|
||||||
|
meta: {
|
||||||
|
title: "首页",
|
||||||
|
},
|
||||||
|
component: () => import("@/views/Home.vue"),
|
||||||
|
},
|
||||||
|
// 新闻列表
|
||||||
|
{
|
||||||
|
path: "/list",
|
||||||
|
name: "list",
|
||||||
|
meta: {
|
||||||
|
title: "新闻列表",
|
||||||
|
},
|
||||||
|
component: () => import("@/views/List.vue"),
|
||||||
|
},
|
||||||
|
// 设置页
|
||||||
|
{
|
||||||
|
path: "/setting",
|
||||||
|
name: "setting",
|
||||||
|
meta: {
|
||||||
|
title: "全局设置",
|
||||||
|
},
|
||||||
|
component: () => import("@/views/Setting.vue"),
|
||||||
|
},
|
||||||
|
// 测试页面
|
||||||
|
{
|
||||||
|
path: "/test",
|
||||||
|
name: "test",
|
||||||
|
meta: {
|
||||||
|
title: "test",
|
||||||
|
},
|
||||||
|
component: () => import("@/views/Test.vue"),
|
||||||
|
},
|
||||||
|
// 404
|
||||||
|
{
|
||||||
|
path: "/404",
|
||||||
|
name: "404",
|
||||||
|
meta: {
|
||||||
|
title: "404",
|
||||||
|
},
|
||||||
|
component: () => import("@/views/404.vue"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/:pathMatch(.*)",
|
||||||
|
redirect: "/404",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default routes;
|
||||||
107
src/store/index.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { defineStore } from "pinia";
|
||||||
|
|
||||||
|
export const mainStore = defineStore("main", {
|
||||||
|
state: () => {
|
||||||
|
return {
|
||||||
|
// 系统主题
|
||||||
|
siteTheme: "light",
|
||||||
|
// 新闻类别
|
||||||
|
newsArr: [
|
||||||
|
{
|
||||||
|
label: "哔哩哔哩",
|
||||||
|
value: "bilibili",
|
||||||
|
order: 0,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "微博",
|
||||||
|
value: "weibo",
|
||||||
|
order: 1,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "知乎",
|
||||||
|
value: "zhihu",
|
||||||
|
order: 2,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "36氪",
|
||||||
|
value: "36kr",
|
||||||
|
order: 3,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "百度",
|
||||||
|
value: "baidu",
|
||||||
|
order: 4,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "少数派",
|
||||||
|
value: "sspai",
|
||||||
|
order: 5,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "IT之家",
|
||||||
|
value: "ithome",
|
||||||
|
order: 6,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "澎湃新闻",
|
||||||
|
value: "thepaper",
|
||||||
|
order: 7,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "今日头条",
|
||||||
|
value: "toutiao",
|
||||||
|
order: 8,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "百度贴吧",
|
||||||
|
value: "tieba",
|
||||||
|
order: 9,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "稀土掘金",
|
||||||
|
value: "juejin",
|
||||||
|
order: 10,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "腾讯新闻",
|
||||||
|
value: "newsqq",
|
||||||
|
order: 11,
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 链接跳转方式
|
||||||
|
linkOpenType: "open",
|
||||||
|
// 页头固定
|
||||||
|
headerFixed: true,
|
||||||
|
// 时间数据
|
||||||
|
timeData: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getters: {},
|
||||||
|
actions: {
|
||||||
|
// 更改系统主题
|
||||||
|
setSiteTheme(val) {
|
||||||
|
$message.info(`已切换至${val === "dark" ? "深色模式" : "浅色模式"}`, {
|
||||||
|
showIcon: false,
|
||||||
|
});
|
||||||
|
this.siteTheme = val;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
persist: [
|
||||||
|
{
|
||||||
|
storage: localStorage,
|
||||||
|
paths: ["siteTheme", "newsArr", "linkOpenType", "headerFixed"],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
14
src/style/global.scss
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// 全局样式
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
#app {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
87
src/utils/getTime.js
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import LunarCalendar from "lunar-calendar";
|
||||||
|
|
||||||
|
export const formatTime = (timestamp) => {
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
const now = new Date();
|
||||||
|
const diffInSeconds = (now.getTime() - date.getTime()) / 1000;
|
||||||
|
const diffInMinutes = diffInSeconds / 60;
|
||||||
|
const diffInHours = diffInMinutes / 60;
|
||||||
|
|
||||||
|
if (diffInSeconds < 60) {
|
||||||
|
return "刚刚更新";
|
||||||
|
} else if (diffInMinutes < 60) {
|
||||||
|
const minutes = Math.floor(diffInMinutes);
|
||||||
|
return `${minutes}分钟前更新`;
|
||||||
|
} else if (diffInHours < 24) {
|
||||||
|
const hours = Math.floor(diffInHours);
|
||||||
|
return `${hours}小时前更新`;
|
||||||
|
} else {
|
||||||
|
const month = date.getMonth() + 1;
|
||||||
|
const day = date.getDate();
|
||||||
|
return `${month}月${day}日`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCurrentTime = () => {
|
||||||
|
const time = new Date();
|
||||||
|
const year = time.getFullYear();
|
||||||
|
const month =
|
||||||
|
time.getMonth() + 1 < 10
|
||||||
|
? "0" + (time.getMonth() + 1)
|
||||||
|
: time.getMonth() + 1;
|
||||||
|
const day = time.getDate() < 10 ? "0" + time.getDate() : time.getDate();
|
||||||
|
const hour = time.getHours() < 10 ? "0" + time.getHours() : time.getHours();
|
||||||
|
const minute =
|
||||||
|
time.getMinutes() < 10 ? "0" + time.getMinutes() : time.getMinutes();
|
||||||
|
const second =
|
||||||
|
time.getSeconds() < 10 ? "0" + time.getSeconds() : time.getSeconds();
|
||||||
|
const weekday = [
|
||||||
|
"星期日",
|
||||||
|
"星期一",
|
||||||
|
"星期二",
|
||||||
|
"星期三",
|
||||||
|
"星期四",
|
||||||
|
"星期五",
|
||||||
|
"星期六",
|
||||||
|
];
|
||||||
|
// 获取农历
|
||||||
|
const lunar = LunarCalendar.solarToLunar(
|
||||||
|
time.getFullYear(),
|
||||||
|
time.getMonth() + 1,
|
||||||
|
time.getDate()
|
||||||
|
);
|
||||||
|
const currentTime = {
|
||||||
|
time: {
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
second,
|
||||||
|
weekday: weekday[time.getDay()],
|
||||||
|
text:
|
||||||
|
year +
|
||||||
|
"-" +
|
||||||
|
month +
|
||||||
|
"-" +
|
||||||
|
day +
|
||||||
|
" " +
|
||||||
|
hour +
|
||||||
|
":" +
|
||||||
|
minute +
|
||||||
|
":" +
|
||||||
|
second,
|
||||||
|
},
|
||||||
|
lunar: {
|
||||||
|
data: lunar,
|
||||||
|
year: lunar.lunarYear,
|
||||||
|
month: lunar.lunarMonthName,
|
||||||
|
day: lunar.lunarDayName,
|
||||||
|
GanZhiYear: lunar.GanZhiYear,
|
||||||
|
GanZhiMonth: lunar.GanZhiMonth,
|
||||||
|
GanZhiDay: lunar.GanZhiDay,
|
||||||
|
text: lunar.lunarMonthName + lunar.lunarDayName,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return currentTime;
|
||||||
|
};
|
||||||
38
src/views/404.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<n-layout
|
||||||
|
embedded
|
||||||
|
class="state"
|
||||||
|
:content-style="{
|
||||||
|
padding: '24px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<n-result
|
||||||
|
class="error"
|
||||||
|
status="404"
|
||||||
|
title="404 资源不存在"
|
||||||
|
description="生活总归带点荒谬"
|
||||||
|
>
|
||||||
|
<template #footer>
|
||||||
|
<n-button @click="goHome">重新载入</n-button>
|
||||||
|
</template>
|
||||||
|
</n-result>
|
||||||
|
</n-layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goHome = () => {
|
||||||
|
router.push("/");
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.state {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
79
src/views/Home.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home">
|
||||||
|
<!-- <n-alert type="info" :show-icon="false" style="margin-bottom: 20px">
|
||||||
|
站点未完工
|
||||||
|
</n-alert> -->
|
||||||
|
<n-grid
|
||||||
|
v-if="store.newsArr[0] && store.newsArr.filter((item) => item.show)[0]"
|
||||||
|
cols="1 560:2 800:3 1100:4 1500:5"
|
||||||
|
:x-gap="24"
|
||||||
|
:y-gap="24"
|
||||||
|
>
|
||||||
|
<n-grid-item
|
||||||
|
class="news-card"
|
||||||
|
v-for="(item, index) in store.newsArr.filter((item) => item.show)"
|
||||||
|
:key="item"
|
||||||
|
:style="{ animationDelay: index / 10 + 0.2 + 's' }"
|
||||||
|
>
|
||||||
|
<HotList :hotType="item.value" />
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
<div class="error" v-else>
|
||||||
|
<n-divider dashed class="tip"> 此处暂无内容 </n-divider>
|
||||||
|
<n-space justify="center">
|
||||||
|
<n-button size="large" secondary strong @click="reset">
|
||||||
|
出错了?点此重置
|
||||||
|
</n-button>
|
||||||
|
</n-space>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { mainStore } from "@/store";
|
||||||
|
import HotList from "@/components/HotList.vue";
|
||||||
|
|
||||||
|
const store = mainStore();
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
const reset = () => {
|
||||||
|
$dialog.warning({
|
||||||
|
title: "重置站点",
|
||||||
|
content:
|
||||||
|
"确认重置站点?你的自定义数据将会恢复为默认状态!(当设置页面能正常进入并显示时请不要执行此操作!)",
|
||||||
|
positiveText: "重置",
|
||||||
|
negativeText: "取消",
|
||||||
|
onPositiveClick: () => {
|
||||||
|
if ($timeInterval) clearInterval($timeInterval);
|
||||||
|
localStorage.clear();
|
||||||
|
location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.home {
|
||||||
|
.news-card {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
animation-timing-function: cubic-bezier(0.42, 0, 0.58, 1);
|
||||||
|
animation: cardShow 0.3s forwards ease-in-out;
|
||||||
|
}
|
||||||
|
.tip {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 出现动画
|
||||||
|
@keyframes cardShow {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
358
src/views/List.vue
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
<template>
|
||||||
|
<div class="list">
|
||||||
|
<n-space class="type" v-if="store.newsArr[0]">
|
||||||
|
<n-tag
|
||||||
|
round
|
||||||
|
size="large"
|
||||||
|
class="tag"
|
||||||
|
v-for="item in store.newsArr.filter((item) => item.show)"
|
||||||
|
:key="item"
|
||||||
|
:type="item.value === listType ? 'primary' : 'default'"
|
||||||
|
@click="changeType(item.value)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
<template #avatar>
|
||||||
|
<img :src="`/logo/${item.value}.png`" alt="logo" class="logo" />
|
||||||
|
</template>
|
||||||
|
</n-tag>
|
||||||
|
</n-space>
|
||||||
|
<n-card class="card">
|
||||||
|
<template #header>
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<template v-if="!listData">
|
||||||
|
<div class="loading" style="height: 60px">
|
||||||
|
<n-skeleton text round height="40px" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="header">
|
||||||
|
<div class="logo">
|
||||||
|
<img :src="`/logo/${listType}.png`" alt="logo" />
|
||||||
|
</div>
|
||||||
|
<div class="name">
|
||||||
|
<n-text class="title">{{ listData.title }}</n-text>
|
||||||
|
<n-text class="subtitle" :depth="3">
|
||||||
|
{{ listData.subtitle }}
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
<div class="data">
|
||||||
|
<n-text
|
||||||
|
v-if="listData.total"
|
||||||
|
:depth="3"
|
||||||
|
class="total"
|
||||||
|
v-html="listData.total"
|
||||||
|
/>
|
||||||
|
<n-text :depth="3" class="time" v-html="updateTime" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Transition>
|
||||||
|
</template>
|
||||||
|
<Transition name="fade" mode="out-in">
|
||||||
|
<template v-if="!listData">
|
||||||
|
<div class="loading" style="flex-direction: column">
|
||||||
|
<n-skeleton
|
||||||
|
text
|
||||||
|
round
|
||||||
|
:repeat="20"
|
||||||
|
height="40px"
|
||||||
|
style="margin-bottom: 20px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="all">
|
||||||
|
<n-list hoverable clickable style="width: 100%">
|
||||||
|
<n-list-item
|
||||||
|
v-for="(item, index) in listData.data.slice(
|
||||||
|
pageNumber * 20 - 20,
|
||||||
|
pageNumber * 20
|
||||||
|
)"
|
||||||
|
:key="item"
|
||||||
|
@click="jumpLink(item)"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<n-text
|
||||||
|
class="num"
|
||||||
|
:class="
|
||||||
|
index + 1 + (pageNumber - 1) * 20 === 1
|
||||||
|
? 'one'
|
||||||
|
: index + 1 + (pageNumber - 1) * 20 === 2
|
||||||
|
? 'two'
|
||||||
|
: index + 1 + (pageNumber - 1) * 20 === 3
|
||||||
|
? 'three'
|
||||||
|
: null
|
||||||
|
"
|
||||||
|
:depth="2"
|
||||||
|
>
|
||||||
|
{{ index + 1 + (pageNumber - 1) * 20 }}
|
||||||
|
</n-text>
|
||||||
|
</template>
|
||||||
|
<div class="text">
|
||||||
|
<n-text class="title" v-html="item.title" />
|
||||||
|
<n-text
|
||||||
|
v-if="item.desc"
|
||||||
|
class="desc"
|
||||||
|
:depth="3"
|
||||||
|
v-html="item.desc"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="message">
|
||||||
|
<div class="hot" v-if="item.hot">
|
||||||
|
<n-icon :depth="3" :component="Fire" />
|
||||||
|
<n-text class="hot-text" :depth="3" v-html="item.hot" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-list-item>
|
||||||
|
</n-list>
|
||||||
|
<n-pagination
|
||||||
|
class="pagination"
|
||||||
|
:page-slot="5"
|
||||||
|
:item-count="listData.data.length"
|
||||||
|
:page-sizes="[20]"
|
||||||
|
v-model:page="pageNumber"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Transition>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Fire } from "@icon-park/vue-next";
|
||||||
|
import { mainStore } from "@/store";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { formatTime } from "@/utils/getTime";
|
||||||
|
import { getHotLists } from "@/api";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const store = mainStore();
|
||||||
|
|
||||||
|
const updateTime = ref(null);
|
||||||
|
const listType = ref(
|
||||||
|
router.currentRoute.value.query.type || store.newsArr[0].value
|
||||||
|
);
|
||||||
|
const pageNumber = ref(
|
||||||
|
router.currentRoute.value.query.page
|
||||||
|
? Number(router.currentRoute.value.query.page)
|
||||||
|
: 1
|
||||||
|
);
|
||||||
|
const listData = ref(null);
|
||||||
|
|
||||||
|
// 获取热榜数据
|
||||||
|
const getHotListsData = (type, isNew = false) => {
|
||||||
|
listData.value = null;
|
||||||
|
getHotLists(type, isNew).then((res) => {
|
||||||
|
console.log(res);
|
||||||
|
if (res.code === 200) {
|
||||||
|
listData.value = res;
|
||||||
|
} else {
|
||||||
|
$message.error(res.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 链接跳转
|
||||||
|
const jumpLink = (data) => {
|
||||||
|
if (!data.url || !data.mobileUrl) return $message.error("链接不存在");
|
||||||
|
const url = window.innerWidth > 680 ? data.url : data.mobileUrl;
|
||||||
|
if (store.linkOpenType === "open") {
|
||||||
|
window.open(url, "_blank");
|
||||||
|
} else if (store.linkOpenType === "href") {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 切换类别
|
||||||
|
const changeType = (type) => {
|
||||||
|
router.push({
|
||||||
|
path: "/list",
|
||||||
|
query: {
|
||||||
|
type,
|
||||||
|
page: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 实时改变更新时间
|
||||||
|
watch(
|
||||||
|
() => store.timeData,
|
||||||
|
() => {
|
||||||
|
if (listData.value) {
|
||||||
|
updateTime.value = formatTime(listData.value.updateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 页数变化
|
||||||
|
watch(
|
||||||
|
() => pageNumber.value,
|
||||||
|
(val) => {
|
||||||
|
router.push({
|
||||||
|
path: "/list",
|
||||||
|
query: {
|
||||||
|
type: listType.value,
|
||||||
|
page: val,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
document.querySelector(".n-back-top")?.click();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 类别变化
|
||||||
|
watch(
|
||||||
|
() => router.currentRoute.value,
|
||||||
|
(val) => {
|
||||||
|
if (val.name === "list") {
|
||||||
|
listType.value = val.query.type;
|
||||||
|
pageNumber.value = Number(val.query.page);
|
||||||
|
getHotListsData(listType.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getHotListsData(listType.value);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.list {
|
||||||
|
.type {
|
||||||
|
width: 100%;
|
||||||
|
.tag {
|
||||||
|
cursor: pointer;
|
||||||
|
.logo {
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-top: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
.fade-enter-active,
|
||||||
|
.fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-enter-from,
|
||||||
|
.fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 60px;
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
img {
|
||||||
|
height: 50px;
|
||||||
|
width: 50px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.name {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
.title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.data {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 14px;
|
||||||
|
.total {
|
||||||
|
&::before {
|
||||||
|
content: "共 ";
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
content: " 条 ·";
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.all {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
.num {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
min-width: 24px;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--n-border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--n-close-color-hover);
|
||||||
|
}
|
||||||
|
&.one {
|
||||||
|
background-color: #ea444d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&.two {
|
||||||
|
background-color: #ed702d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
&.three {
|
||||||
|
background-color: #eead3f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.desc {
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 14px;
|
||||||
|
display: -webkit-inline-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
.hot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 13px;
|
||||||
|
.hot-text {
|
||||||
|
margin-left: 4px;
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
209
src/views/Setting.vue
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
<template>
|
||||||
|
<div class="setting">
|
||||||
|
<div class="title">全局设置</div>
|
||||||
|
<n-h6 prefix="bar"> 基础设置 </n-h6>
|
||||||
|
<n-card class="set-item">
|
||||||
|
<div class="top">
|
||||||
|
<div class="name">
|
||||||
|
<n-text class="text">链接跳转方式</n-text>
|
||||||
|
<n-text class="tip" :depth="3"> 选择榜单列表内容的跳转方式 </n-text>
|
||||||
|
</div>
|
||||||
|
<n-select
|
||||||
|
class="set"
|
||||||
|
v-model:value="linkOpenType"
|
||||||
|
:options="linkOptions"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
<n-card class="set-item">
|
||||||
|
<div class="top">
|
||||||
|
<div class="name">
|
||||||
|
<n-text class="text">固定导航栏</n-text>
|
||||||
|
<n-text class="tip" :depth="3"> 导航栏是否固定 </n-text>
|
||||||
|
</div>
|
||||||
|
<n-switch v-model:value="headerFixed" :round="false" />
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
<n-card class="set-item">
|
||||||
|
<div class="top">
|
||||||
|
<div class="name">
|
||||||
|
<n-text class="text">榜单排序</n-text>
|
||||||
|
<n-text class="tip" :depth="3">
|
||||||
|
拖拽以排序,开关用以控制在页面中的显示状态
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
<n-popconfirm @positive-click="restoreDefault">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button class="control" size="small"> 恢复默认 </n-button>
|
||||||
|
</template>
|
||||||
|
确认将排序恢复到默认状态?
|
||||||
|
</n-popconfirm>
|
||||||
|
</div>
|
||||||
|
<draggable
|
||||||
|
:list="newsArr"
|
||||||
|
:animation="200"
|
||||||
|
class="mews-group"
|
||||||
|
item-key="order"
|
||||||
|
@end="saveSoreData()"
|
||||||
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
|
<n-card
|
||||||
|
class="item"
|
||||||
|
embedded
|
||||||
|
:content-style="{ display: 'flex', alignItems: 'center' }"
|
||||||
|
>
|
||||||
|
<div class="desc" :style="{ opacity: element.show ? null : 0.6 }">
|
||||||
|
<img
|
||||||
|
class="logo"
|
||||||
|
:src="`/logo/${element.value}.png`"
|
||||||
|
alt="logo"
|
||||||
|
/>
|
||||||
|
<n-text class="news-name" v-html="element.label" />
|
||||||
|
</div>
|
||||||
|
<n-switch
|
||||||
|
class="switch"
|
||||||
|
:round="false"
|
||||||
|
v-model:value="element.show"
|
||||||
|
@update:value="saveSoreData(element.label, element.show)"
|
||||||
|
/>
|
||||||
|
</n-card>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</n-card>
|
||||||
|
<n-h6 prefix="bar"> 杂项设置 </n-h6>
|
||||||
|
<n-card class="set-item">
|
||||||
|
<div class="top">
|
||||||
|
<div class="name">
|
||||||
|
<n-text class="text">重置所有数据</n-text>
|
||||||
|
<n-text class="tip" :depth="3">
|
||||||
|
重置所有数据,你的自定义设置都将会丢失
|
||||||
|
</n-text>
|
||||||
|
</div>
|
||||||
|
<n-popconfirm @positive-click="reset">
|
||||||
|
<template #trigger>
|
||||||
|
<n-button type="warning"> 重置 </n-button>
|
||||||
|
</template>
|
||||||
|
确认重置所有数据?你的自定义设置都将会丢失!
|
||||||
|
</n-popconfirm>
|
||||||
|
</div>
|
||||||
|
</n-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from "pinia";
|
||||||
|
import { mainStore } from "@/store";
|
||||||
|
import draggable from "vuedraggable";
|
||||||
|
|
||||||
|
const store = mainStore();
|
||||||
|
const { newsArr, linkOpenType, headerFixed } = storeToRefs(store);
|
||||||
|
|
||||||
|
// 榜单跳转
|
||||||
|
const linkOptions = [
|
||||||
|
{
|
||||||
|
label: "新页面打开",
|
||||||
|
value: "open",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "当前页打开",
|
||||||
|
value: "href",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 恢复默认排序
|
||||||
|
const restoreDefault = () => {
|
||||||
|
newsArr.value = newsArr.value.sort((a, b) => a.order - b.order);
|
||||||
|
$message.success("恢复默认榜单排序成功");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 将排序结果写入
|
||||||
|
const saveSoreData = (name = null, open = false) => {
|
||||||
|
$message.success(
|
||||||
|
name ? `${name}榜单已${open ? "开启" : "关闭"}` : "榜单排序成功"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 重置数据
|
||||||
|
const reset = () => {
|
||||||
|
if ($timeInterval) clearInterval($timeInterval);
|
||||||
|
localStorage.clear();
|
||||||
|
location.reload();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.setting {
|
||||||
|
.title {
|
||||||
|
margin-top: 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 40px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.n-h {
|
||||||
|
padding-left: 16px;
|
||||||
|
font-size: 20px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
.set-item {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
.name {
|
||||||
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
.tip {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.set {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mews-group {
|
||||||
|
margin-top: 16px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(5, minmax(0px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
@media (max-width: 1666px) {
|
||||||
|
grid-template-columns: repeat(4, minmax(0px, 1fr));
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
grid-template-columns: repeat(3, minmax(0px, 1fr));
|
||||||
|
}
|
||||||
|
@media (max-width: 890px) {
|
||||||
|
grid-template-columns: repeat(2, minmax(0px, 1fr));
|
||||||
|
}
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
grid-template-columns: repeat(1, minmax(0px, 1fr));
|
||||||
|
}
|
||||||
|
.item {
|
||||||
|
cursor: pointer;
|
||||||
|
.desc {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
transition: all 0.3s;
|
||||||
|
.logo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-name {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.switch {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
120
src/views/Test.vue
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-2">
|
||||||
|
<button class="btn btn-secondary button" @click="sort">
|
||||||
|
To original order
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-6">
|
||||||
|
<h3>Transition</h3>
|
||||||
|
<draggable
|
||||||
|
class="list-group"
|
||||||
|
tag="transition-group"
|
||||||
|
:component-data="{
|
||||||
|
tag: 'ul',
|
||||||
|
type: 'transition-group',
|
||||||
|
name: !drag ? 'flip-list' : null,
|
||||||
|
}"
|
||||||
|
v-model="list"
|
||||||
|
v-bind="dragOptions"
|
||||||
|
@start="drag = true"
|
||||||
|
@end="drag = false"
|
||||||
|
item-key="order"
|
||||||
|
>
|
||||||
|
<template #item="{ element }">
|
||||||
|
<li class="list-group-item">
|
||||||
|
<i
|
||||||
|
:class="
|
||||||
|
element.fixed ? 'fa fa-anchor' : 'glyphicon glyphicon-pushpin'
|
||||||
|
"
|
||||||
|
@click="element.fixed = !element.fixed"
|
||||||
|
aria-hidden="true"
|
||||||
|
></i>
|
||||||
|
{{ element.name }}
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<rawDisplayer class="col-3" :value="list" title="List" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import draggable from "vuedraggable";
|
||||||
|
|
||||||
|
const message = [
|
||||||
|
"vue.draggable",
|
||||||
|
"draggable",
|
||||||
|
"component",
|
||||||
|
"for",
|
||||||
|
"vue.js 2.0",
|
||||||
|
"based",
|
||||||
|
"on",
|
||||||
|
"Sortablejs",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "transition-example-2",
|
||||||
|
display: "Transitions",
|
||||||
|
order: 7,
|
||||||
|
components: {
|
||||||
|
draggable,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
list: message.map((name, index) => {
|
||||||
|
return { name, order: index + 1 };
|
||||||
|
}),
|
||||||
|
drag: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
sort() {
|
||||||
|
this.list = this.list.sort((a, b) => a.order - b.order);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
dragOptions() {
|
||||||
|
return {
|
||||||
|
animation: 200,
|
||||||
|
group: "description",
|
||||||
|
disabled: false,
|
||||||
|
ghostClass: "ghost",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.button {
|
||||||
|
margin-top: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flip-list-move {
|
||||||
|
transition: transform 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-move {
|
||||||
|
transition: transform 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ghost {
|
||||||
|
opacity: 0.5;
|
||||||
|
background: #c8ebfb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group {
|
||||||
|
min-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item {
|
||||||
|
cursor: move;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-group-item i {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
85
vite.config.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
|
import Components from "unplugin-vue-components/vite";
|
||||||
|
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
AutoImport({
|
||||||
|
imports: [
|
||||||
|
"vue",
|
||||||
|
{
|
||||||
|
"naive-ui": [
|
||||||
|
"useDialog",
|
||||||
|
"useMessage",
|
||||||
|
"useNotification",
|
||||||
|
"useLoadingBar",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
Components({
|
||||||
|
resolvers: [NaiveUiResolver()],
|
||||||
|
}),
|
||||||
|
// PWA
|
||||||
|
VitePWA({
|
||||||
|
registerType: "autoUpdate",
|
||||||
|
devOptions: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
workbox: {
|
||||||
|
cleanupOutdatedCaches: true,
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: /(.*?)\.(woff2|woff|ttf)/,
|
||||||
|
handler: "CacheFirst",
|
||||||
|
options: {
|
||||||
|
cacheName: "file-cache",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
urlPattern: /(.*?)\.(webp|png|jpe?g|svg|gif|bmp|psd|tiff|tga|eps)/,
|
||||||
|
handler: "CacheFirst",
|
||||||
|
options: {
|
||||||
|
cacheName: "image-cache",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
manifest: {
|
||||||
|
name: "今日热榜",
|
||||||
|
short_name: "DailyHot",
|
||||||
|
description: "汇聚全网热点,热门尽览无余",
|
||||||
|
display: "standalone",
|
||||||
|
start_url: "/",
|
||||||
|
theme_color: "#fff",
|
||||||
|
background_color: "#efefef",
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: "/ico/favicon.png",
|
||||||
|
sizes: "200x200",
|
||||||
|
type: "image/png",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
minify: "terser",
|
||||||
|
terserOptions: {
|
||||||
|
compress: {
|
||||||
|
pure_funcs: ["console.log"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||