完善页面

This commit is contained in:
imsyy
2023-03-20 09:54:29 +08:00
parent e596d9ea85
commit dc94feb2a6
44 changed files with 9615 additions and 0 deletions

6
.env Normal file
View 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
View 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
View File

@@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

29
README.md Normal file
View 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
View 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
View 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} didnt 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

File diff suppressed because it is too large Load Diff

13
index.html Normal file
View 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
View 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

File diff suppressed because it is too large Load Diff

BIN
public/ico/error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
public/ico/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

BIN
public/ico/icon_error.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
public/logo/36kr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
public/logo/baidu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
public/logo/bilibili.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
public/logo/ithome.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/logo/juejin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
public/logo/newsqq.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
public/logo/sspai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

BIN
public/logo/thepaper.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
public/logo/tieba.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
public/logo/toutiao.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

BIN
public/logo/weibo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
public/logo/zhihu.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

103
src/App.vue Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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"],
},
},
},
});