完善页面
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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||