首页+登录页

This commit is contained in:
2025-08-07 16:39:37 +08:00
commit a161520e7b
60 changed files with 5456 additions and 0 deletions

30
.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# 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?
*.tsbuildinfo

3
.vscode/extensions.json vendored Normal file
View File

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

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# blog
`重新起航`

88
auto-imports.d.ts vendored Normal file
View File

@ -0,0 +1,88 @@
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue')['EffectScope']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const effectScope: typeof import('vue')['effectScope']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
const mapStores: typeof import('pinia')['mapStores']
const mapWritableState: typeof import('pinia')['mapWritableState']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const storeToRefs: typeof import('pinia')['storeToRefs']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useModel: typeof import('vue')['useModel']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}

34
components.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
/* eslint-disable */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
DAside: typeof import('vue-devui/layout/index.es.js')['Aside']
DAvatar: typeof import('vue-devui/avatar/index.es.js')['Avatar']
DButton: typeof import('vue-devui/button/index.es.js')['Button']
DCard: typeof import('vue-devui/card/index.es.js')['Card']
DContent: typeof import('vue-devui/layout/index.es.js')['Content']
DFooter: typeof import('vue-devui/layout/index.es.js')['Footer']
DForm: typeof import('vue-devui/form/index.es.js')['Form']
DFormItem: typeof import('vue-devui/form/index.es.js')['FormItem']
DHeader: typeof import('vue-devui/layout/index.es.js')['Header']
DIcon: typeof import('vue-devui/icon/index.es.js')['Icon']
DInput: typeof import('vue-devui/input/index.es.js')['Input']
DLayout: typeof import('vue-devui/layout/index.es.js')['Layout']
DMenu: typeof import('vue-devui/menu/index.es.js')['Menu']
DMenuItem: typeof import('vue-devui/menu/index.es.js')['MenuItem']
DModal: typeof import('vue-devui/modal/index.es.js')['Modal']
DSelect: typeof import('vue-devui/select/index.es.js')['Select']
DTab: typeof import('vue-devui/tabs/index.es.js')['Tab']
DTabs: typeof import('vue-devui/tabs/index.es.js')['Tabs']
HomeSide: typeof import('./src/components/homeSide.vue')['default']
Menu: typeof import('./src/components/menu.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}
}

18
env.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
/// <reference types="vite/client" />
/// <reference types="axios" />
declare global {
let $http: any;
let $cookies: any;
let $msg: any;
let $store:any
interface Window {
// 扩展Windows环境下的全局$http对象
$http: any;
$cookies: any;
$msg: any;
$store:any
}
}
export { };

71
index.html Normal file
View File

@ -0,0 +1,71 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<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.ts"></script>
</body>
<script type="text/javascript">
! function (e, t, a) {
function r() {
for (var e = 0; e < s.length; e++) s[e].alpha <= 0 ? (t.body.removeChild(s[e].el), s.splice(e, 1)) : (s[
e].y--, s[e].scale += .004, s[e].alpha -= .013, s[e].el.style.cssText = "left:" + s[e].x +
"px;top:" + s[e].y + "px;opacity:" + s[e].alpha + ";transform:scale(" + s[e].scale + "," + s[e]
.scale + ") rotate(45deg);background:" + s[e].color + ";z-index:99999");
requestAnimationFrame(r)
}
function n() {
var t = "function" == typeof e.onclick && e.onclick;
e.onclick = function (e) {
t && t(), o(e)
}
}
function o(e) {
var a = t.createElement("div");
a.className = "heart", s.push({
el: a,
x: e.clientX - 5,
y: e.clientY - 5,
scale: 1,
alpha: 1,
color: c()
}), t.body.appendChild(a)
}
function i(e) {
var a = t.createElement("style");
a.type = "text/css";
try {
a.appendChild(t.createTextNode(e))
} catch (t) {
a.styleSheet.cssText = e
}
t.getElementsByTagName("head")[0].appendChild(a)
}
function c() {
return "rgb(" + ~~(255 * Math.random()) + "," + ~~(255 * Math.random()) + "," + ~~(255 * Math
.random()) + ")"
}
var s = [];
e.requestAnimationFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e
.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (e) {
setTimeout(e, 1e3 / 60)
}, i(
".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}"
), n(), r()
}(window, document);
</script>
</html>

49
package.json Normal file
View File

@ -0,0 +1,49 @@
{
"name": "blog",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "run-p type-check \"build-only {@}\" --",
"preview": "vite preview",
"build-only": "vite build",
"type-check": "vue-tsc --build"
},
"dependencies": {
"@devui-design/icons": "^1.4.0",
"@heroicons/vue": "^2.2.0",
"@unocss/reset": "^66.3.3",
"axios": "^1.11.0",
"devui-theme": "^0.0.7",
"es-toolkit": "^1.39.8",
"less": "^4.4.0",
"ng-devui": "^18.0.0",
"pinia": "^3.0.3",
"qs": "^6.14.0",
"qweather-icons": "^1.7.0",
"unocss": "^66.3.3",
"unplugin-auto-import": "^19.3.0",
"unplugin-vue-components": "^28.8.0",
"vite-svg-loader": "^5.1.0",
"vue": "^3.5.18",
"vue-devui": "^1.6.33",
"vue-router": "^4.5.1",
"vue3-cookies": "^1.0.6",
"vue3-perfect-scrollbar": "^2.0.0"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.2",
"@types/node": "^22.16.5",
"@vitejs/plugin-vue": "^6.0.1",
"@vue/tsconfig": "^0.7.0",
"npm-run-all2": "^8.0.4",
"typescript": "~5.8.0",
"vite": "^7.0.6",
"vite-plugin-vue-devtools": "^8.0.0",
"vue-tsc": "^3.0.4"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

18
src/App.vue Normal file
View File

@ -0,0 +1,18 @@
<template>
<div>
<PerfectScrollbar>
<router-view></router-view>
</PerfectScrollbar>
</div>
</template>
<script setup lang="ts">
// 空白项目入口
</script>
<style scoped lang="less">
.ps {
width: 100vw;
height: 100vh;
}
</style>

23
src/Layout.vue Normal file
View File

@ -0,0 +1,23 @@
<template>
<d-layout>
<d-header class="dheader-1">
<menu-h />
</d-header>
<d-content class="dcontent-1">
<router-view />
</d-content>
<d-footer class="dfooter-1">footer</d-footer>
</d-layout>
</template>
<script setup lang="ts">
// 空白项目入口
import menuH from '@/components/menu.vue';
</script>
<style scoped>
.dcontent-1 {
background-color: #fbfbfb;
}
</style>

0
src/api/file/index.ts Normal file
View File

56
src/api/mix/index.ts Normal file
View File

@ -0,0 +1,56 @@
import request from "@/util/request";
//getWeather
export function getWeather(params: Record<string, string>) {
return request({
url: "/mix/wea",
method: "get",
params,
});
}
//getLocation
export function getLocation(params: Record<string, string>) {
return request({
url: "/mix/location",
method: "get",
params,
});
}
//getIp
export function getIp() {
return request({
url: "/mix/ip",
method: "get",
});
}
//getIcon
export function getIcon(data: any) {
return request({
url: "/mix/ico",
method: "post",
data,
});
}
//getHoli
export function getHoli() {
return request({
url: "/mix/nextholi",
method: "get",
});
}
//getJq
export function getJq(params: Record<string, string>) {
return request({
url: "/mix/carl",
method: "get",
params,
});
}
//getBdhot
export function getBdhot() {
return request({
url: "/mix/bdhot",
method: "get",
});
}

17
src/api/nav/index.ts Normal file
View File

@ -0,0 +1,17 @@
import request from "@/util/request";
// getNavList
export function getNavList() {
return request({
url: "/navi",
method: "get",
});
}
// addNav
export function addNav(data: any) {
return request({
url: "/navi",
method: "post",
data,
});
}

18
src/api/user/index.ts Normal file
View File

@ -0,0 +1,18 @@
import request from "@/util/request";
//login
export function login(data: Record<string, string>) {
return request({
url: "/user/login",
method: "post",
data,
});
}
// register
export function register(data: Record<string, string>) {
return request({
url: "/user/register",
method: "post",
data,
});
}

12
src/assets/base.css Normal file
View File

@ -0,0 +1,12 @@
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-size: 16px;
font-weight: normal;
}
.ps__thumb-y {
background-color: #f6cbe7 !important;
}

BIN
src/assets/font/LCDML.woff2 Normal file

Binary file not shown.

BIN
src/assets/font/yapi.otf Normal file

Binary file not shown.

BIN
src/assets/images/05.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 854 KiB

6
src/assets/main.css Normal file
View File

@ -0,0 +1,6 @@
@import "./base.css";
@import "@devui-design/icons/icomoon/devui-icon.css";
@import "qweather-icons/font/qweather-icons.css";
@import "@devui-design/icons/icomoon/devui-icon.css";
@import "vue-devui/style.css";
@import "vue3-perfect-scrollbar/style.css";

114
src/components/homeSide.vue Normal file
View File

@ -0,0 +1,114 @@
<template>
<div class="pr-8">
<d-card class="mt-10 bg-white">
<template #title>
<div class="flex items-center">
<icon-time class="w-5 mr-2"></icon-time>
节日天气
</div>
</template>
<template #content>
<div class="w-full text-center text-[#ec66ab] font-500 text-2xl font-[yj]">{{ t }}</div>
<div class="mt-2 text-center">{{ d }}</div>
</template>
</d-card>
<d-card class="mt-10 bg-white">
<template #title>
<div class="flex items-center">
<icon-date class="w-5 mr-2"></icon-date>
农历节气
</div>
</template>
<template #content>
<div class="w-full text-center text-[#ec66ab] font-500">{{ jq.yearTips }} {{ jq.lunarCalendar }}</div>
<div class="w-full flex justify-center">
<img class="mt-2 w-[90%] rounded" :src="jqImg" alt=""></img>
</div>
<div class="mt-2 text-center">{{ jq.solarTerms }}</div>
</template>
</d-card>
<d-card class="mt-10 bg-white">
<template #title>
<div class="flex items-center">
<icon-news class="w-5 mr-2"></icon-news>
百度新闻
</div>
</template>
<template #content>
<div class="py-1" v-for="i in bdNews" :key="i.id">
<a class="devui-link flex justify-between" :href="i.url" target="_blank">
<span>{{ i.index }}. {{ i.title }}</span>
<span class="text-primary">{{ i.hot }}</span>
</a>
</div>
<div class="mt-2 text-center">
<a class="devui-link" href="https://www.baidu.com/s?ie=utf-8&wd=百度新闻" target="_blank">更多</a>
</div>
</template>
</d-card>
</div>
</template>
<script setup lang="ts">
//mark import
import { formatTime } from '@/util/index';
//mark data
const t = ref<any>('')
const d = ref<any>('')
let timer: any = null
const jq = ref<any>("")
const jqImg = ref<any>("")
const bdNews = ref<any>([])
//mark method
function getTime() {
const date = new Date();
t.value = formatTime(date, 'hh:mm:ss')
d.value = formatTime(date, 'YYYY 年 MM 月 DD 日')
}
async function getJq() {
const res = await $http.mix.getJq({
date: formatTime(new Date(), "YYYYMMDD")
})
console.log('>>> --> getJq --> res:', res.data)
jq.value = res.data
const j = res.data.solarTerms.slice(0, 2)
jqImg.value = 'https://www.hxyouzi.com/img/jq/' + j + '.png'
}
async function getBdhot() {
const res = await $http.mix.getBdhot()
console.log('>>> --> getBdhot --> res:', res.data)
// 取前5条
bdNews.value = res.data.slice(0, 5)
}
//mark 周期、内置函数等
onMounted(() => {
getJq()
getBdhot()
timer = setInterval(() => {
getTime()
}, 1000)
})
onUnmounted(() => {
clearInterval(timer)
})
</script>
<style scoped lang="less">
@font-face {
font-family: 'yj';
src: url('@/assets/font/LCDML.woff2');
}
:deep(.devui-card__shadow--hover:hover, .devui-card__shadow--always) {
box-shadow: none !important;
}
</style>

212
src/components/menu.vue Normal file
View File

@ -0,0 +1,212 @@
<template>
<div ref="nav"
class="main-nav flex justify-between bg-white border-b border-gray-100 shadow-[0_2px_10px_rgba(173,21,21,0.05)]">
<!-- 网站Logo -->
<div class="px-5 flex items-center" slot="brand" @click="goHome">
<img src="@/logo/红色字体.png" alt="柚子的网站" class="h-9 align-middle" />
</div>
<!-- 主导航菜单 -->
<d-menu mode="horizontal" router class="ml-5 h-14 text-[16px]" :default-select-keys="[key]">
<d-menu-item key="home">
<d-icon :component="homeSvg" class="w-5 mr-1"></d-icon>
首页
</d-menu-item>
<d-menu-item key="gallery">
<d-icon :component="picSvg" class="w-5 mr-1"></d-icon>
画廊
</d-menu-item>
<d-menu-item key="article">
<d-icon :component="artiSvg" class="w-5 mr-1 "></d-icon>
文章
</d-menu-item>
<d-menu-item key="widget">
<d-icon :component="settingSvg" class="w-5 mr-1 "></d-icon>
工具
</d-menu-item>
<d-menu-item key="appshare">
<d-icon :component="downSvg" class="w-5 mr-1 "></d-icon>
软件分享
</d-menu-item>
<d-menu-item key="plink">
<d-icon :component="linkSvg" class="w-5 mr-1 "></d-icon>
友链
</d-menu-item>
</d-menu>
<!-- 用户区域 -->
<div class="!text-[#ec66ab] user-area flex items-center px-3">
<span class="flex items-center location-info ">
<d-icon color="#ec66ab" class="mr-1" name="location-new"></d-icon>
{{ locationInfo }}
</span>
<span class="mx-3 text-gray-300">|</span>
<span class="weather-info mr-2">{{ wea }}</span>
<i :class="'qiIcon qi-' + weaIcon + '-fill'"></i>
<span class="weather-info ml-4">{{ temp }}°C</span>
<d-avatar v-if="userinfo" :img-src="userinfo.ava_url" class="ml-20 cursor-pointer" alt="用户的头" />
<d-avatar v-else class="ml-20 cursor-pointer" @click="toLogin"></d-avatar>
</div>
</div>
</template>
<script setup lang="ts">
// 从@/icon/menu引入所有的svg文件
import artiSvg from '@/icon/menu/arti.svg';
import downSvg from '@/icon/menu/download.svg';
import homeSvg from '@/icon/menu/home.svg';
import linkSvg from '@/icon/menu/link.svg';
import picSvg from '@/icon/menu/pic.svg';
import settingSvg from '@/icon/menu/setting.svg';
import { onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
const route = useRoute();
const router = useRouter();
const key = ref("home");
const locationInfo = ref("获取位置中...");
const latitude = ref<number | null>(null);
const longitude = ref<number | null>(null);
const wea = ref("")
const weaIcon = ref<string>("")
const temp = ref<number>(0);
const userinfo: any = ref(null)
const nav: any = useTemplateRef('nav')
const navx = $store.nav.useNavStore()
// 获取地理位置
const getLocation = () => {
if (!navigator.geolocation) {
locationInfo.value = "您的浏览器不支持地理位置定位";
return;
}
navigator.geolocation.getCurrentPosition(
async (position) => {
latitude.value = position.coords.latitude;
longitude.value = position.coords.longitude;
const jw = `${longitude.value.toFixed(2)},${latitude.value.toFixed(2)}`;
// 这里可以添加调用后端API获取具体位置名称的逻辑
handdleJw(jw);
},
async (error) => {
switch (error.code) {
case error.PERMISSION_DENIED:
locationInfo.value = "用户拒绝了位置请求";
break;
case error.POSITION_UNAVAILABLE:
locationInfo.value = "位置信息不可用";
break;
case error.TIMEOUT:
locationInfo.value = "获取位置超时";
break;
}
const res = await $http.mix.getIp();
latitude.value = res.data.lat;
longitude.value = res.data.lon;
const jw = `${longitude.value?.toFixed(2)},${latitude.value?.toFixed(2)}`;
// 这里可以添加调用后端API获取具体位置名称的逻辑
handdleJw(jw);
},
{
enableHighAccuracy: false,
timeout: 5000,
maximumAge: 0
}
);
};
async function handdleJw(jw: string) {
const zxs = ['北京', '重庆', '天津', '上海']
// 根据经纬度获取物理位置
const loc = await $http.mix.getLocation({ location: jw });
console.log(loc);
if (loc.code == 200) {
const data = loc.data;
if (zxs.includes(data.adm2)) {
locationInfo.value = `${data.country}${data.adm1}${data.name}`;
} else {
locationInfo.value = `${data.adm1}${data.adm2}${data.name}`;
}
} else {
$msg.console.error(loc.msg);
return
}
const res = await $http.mix.getWeather({ location: jw });
console.log(res);
if (res.code == 200) {
wea.value = res.data.text;
weaIcon.value = res.data.icon;
temp.value = res.data.temp;
} else {
$msg.console.error(loc.msg);
}
}
watch(() => route.name, (newVal) => {
key.value = newVal as string
})
function goHome() {
if (route.name == 'home') return
router.push({ name: 'home' });
}
function toLogin() {
if ($cookies.get('token')) return
router.push('/login');
}
onMounted(() => {
console.log(route.name);
userinfo.value = $cookies.get('userinfo');
console.log('>>>>>>>>>>', userinfo.value);
key.value = route.name as string;
getLocation(); // 组件挂载时获取位置
const h: number = nav.value.clientHeight
navx.setNavH(h)
});
</script>
<style scoped>
/* :deep(svg) {
color: red;
} */
:deep(.devui-menu-horizontal .devui-menu-item:hover span .icon) {
color: var(--devui-brand, #5e7ce0) !important;
fill: var(--devui-brand, #5e7ce0) !important;
}
:deep(.devui-menu-item-select span svg) {
color: var(--devui-brand, #5e7ce0) !important;
fill: var(--devui-brand, #5e7ce0) !important;
line-height: 100% !important;
}
:deep(.devui-menu-item-select span) {
color: var(--devui-brand, #5e7ce0) !important;
}
:deep(.devui-menu-horizontal) {
padding: 14px 20px 6px;
}
:deep(.devui-menu-item span) {
display: flex;
align-items: center;
.devui-icon__container {
display: flex;
align-items: center;
}
}
</style>

4
src/config/cookies.ts Normal file
View File

@ -0,0 +1,4 @@
import { useCookies } from "vue3-cookies";
const { cookies } = useCookies();
const ck = cookies;
export default ck;

12
src/config/http.ts Normal file
View File

@ -0,0 +1,12 @@
const files = import.meta.glob("@/api/*/index.ts", {
eager: true,
});
const http: Record<string, any> = {};
// console.log(files);
for (const i in files) {
const t = i.split("/");
const name = t[t.length - 2];
http[name] = files[i];
}
export default http;

7
src/config/index.ts Normal file
View File

@ -0,0 +1,7 @@
export function useConfig() {
const files = import.meta.glob("./*.ts", { eager: true }) as Record<string, any>;
Object.keys(files).forEach(key => {
const name = key.replace(/(\.\/|\.ts)/g, "");
(window as Record<string, any>)["$" + name] = files[key].default as any;
});
}

5
src/config/msg.ts Normal file
View File

@ -0,0 +1,5 @@
import { Message } from 'vue-devui';
const msg:any = Message;
export default msg;

12
src/config/store.ts Normal file
View File

@ -0,0 +1,12 @@
const files = import.meta.glob("@/stores/*.ts", {
eager: true,
});
const store: Record<string, any> = {};
// console.log("stores封装",files);
for (const i in files) {
const t = i.split("/");
const name = t[t.length - 1].split(".")[0];
store[name] = files[i];
}
export default store;

3
src/icon/date.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" />
</svg>

After

Width:  |  Height:  |  Size: 754 B

12
src/icon/index.ts Normal file
View File

@ -0,0 +1,12 @@
const icons = import.meta.glob("./*.svg", {
eager: true,
});
const icon: Record<string, string> = {};
// console.log(icons);
for (const i in icons) {
const t = i.split("/");
const name = t[1].split(".")[0];
icon[name] = icons[i] as string;
}
export default icon;

5
src/icon/menu/arti.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M5.625 1.5c-1.036 0-1.875.84-1.875 1.875v17.25c0 1.035.84 1.875 1.875 1.875h12.75c1.035 0 1.875-.84 1.875-1.875V12.75A3.75 3.75 0 0 0 16.5 9h-1.875a1.875 1.875 0 0 1-1.875-1.875V5.25A3.75 3.75 0 0 0 9 1.5H5.625ZM7.5 15a.75.75 0 0 1 .75-.75h7.5a.75.75 0 0 1 0 1.5h-7.5A.75.75 0 0 1 7.5 15Zm.75 2.25a.75.75 0 0 0 0 1.5H12a.75.75 0 0 0 0-1.5H8.25Z" clip-rule="evenodd" />
<path d="M12.971 1.816A5.23 5.23 0 0 1 14.25 5.25v1.875c0 .207.168.375.375.375H16.5a5.23 5.23 0 0 1 3.434 1.279 9.768 9.768 0 0 0-6.963-6.963Z" />
</svg>

After

Width:  |  Height:  |  Size: 653 B

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M10.5 3.75a6 6 0 0 0-5.98 6.496A5.25 5.25 0 0 0 6.75 20.25H18a4.5 4.5 0 0 0 2.206-8.423 3.75 3.75 0 0 0-4.133-4.303A6.001 6.001 0 0 0 10.5 3.75Zm2.25 6a.75.75 0 0 0-1.5 0v4.94l-1.72-1.72a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l3-3a.75.75 0 1 0-1.06-1.06l-1.72 1.72V9.75Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 436 B

5
src/icon/menu/home.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path d="M11.47 3.841a.75.75 0 0 1 1.06 0l8.69 8.69a.75.75 0 1 0 1.06-1.061l-8.689-8.69a2.25 2.25 0 0 0-3.182 0l-8.69 8.69a.75.75 0 1 0 1.061 1.06l8.69-8.689Z" />
<path d="m12 5.432 8.159 8.159c.03.03.06.058.091.086v6.198c0 1.035-.84 1.875-1.875 1.875H15a.75.75 0 0 1-.75-.75v-4.5a.75.75 0 0 0-.75-.75h-3a.75.75 0 0 0-.75.75V21a.75.75 0 0 1-.75.75H5.625a1.875 1.875 0 0 1-1.875-1.875v-6.198a2.29 2.29 0 0 0 .091-.086L12 5.432Z" />
</svg>

After

Width:  |  Height:  |  Size: 539 B

6
src/icon/menu/link.svg Normal file
View File

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path d="M11.7 2.805a.75.75 0 0 1 .6 0A60.65 60.65 0 0 1 22.83 8.72a.75.75 0 0 1-.231 1.337 49.948 49.948 0 0 0-9.902 3.912l-.003.002c-.114.06-.227.119-.34.18a.75.75 0 0 1-.707 0A50.88 50.88 0 0 0 7.5 12.173v-.224c0-.131.067-.248.172-.311a54.615 54.615 0 0 1 4.653-2.52.75.75 0 0 0-.65-1.352 56.123 56.123 0 0 0-4.78 2.589 1.858 1.858 0 0 0-.859 1.228 49.803 49.803 0 0 0-4.634-1.527.75.75 0 0 1-.231-1.337A60.653 60.653 0 0 1 11.7 2.805Z" />
<path d="M13.06 15.473a48.45 48.45 0 0 1 7.666-3.282c.134 1.414.22 2.843.255 4.284a.75.75 0 0 1-.46.711 47.87 47.87 0 0 0-8.105 4.342.75.75 0 0 1-.832 0 47.87 47.87 0 0 0-8.104-4.342.75.75 0 0 1-.461-.71c.035-1.442.121-2.87.255-4.286.921.304 1.83.634 2.726.99v1.27a1.5 1.5 0 0 0-.14 2.508c-.09.38-.222.753-.397 1.11.452.213.901.434 1.346.66a6.727 6.727 0 0 0 .551-1.607 1.5 1.5 0 0 0 .14-2.67v-.645a48.549 48.549 0 0 1 3.44 1.667 2.25 2.25 0 0 0 2.12 0Z" />
<path d="M4.462 19.462c.42-.419.753-.89 1-1.395.453.214.902.435 1.347.662a6.742 6.742 0 0 1-1.286 1.794.75.75 0 0 1-1.06-1.06Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

3
src/icon/menu/pic.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M1.5 6a2.25 2.25 0 0 1 2.25-2.25h16.5A2.25 2.25 0 0 1 22.5 6v12a2.25 2.25 0 0 1-2.25 2.25H3.75A2.25 2.25 0 0 1 1.5 18V6ZM3 16.06V18c0 .414.336.75.75.75h16.5A.75.75 0 0 0 21 18v-1.94l-2.69-2.689a1.5 1.5 0 0 0-2.12 0l-.88.879.97.97a.75.75 0 1 1-1.06 1.06l-5.16-5.159a1.5 1.5 0 0 0-2.12 0L3 16.061Zm10.125-7.81a1.125 1.125 0 1 1 2.25 0 1.125 1.125 0 0 1-2.25 0Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 517 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-6">
<path fill-rule="evenodd" d="M11.828 2.25c-.916 0-1.699.663-1.85 1.567l-.091.549a.798.798 0 0 1-.517.608 7.45 7.45 0 0 0-.478.198.798.798 0 0 1-.796-.064l-.453-.324a1.875 1.875 0 0 0-2.416.2l-.243.243a1.875 1.875 0 0 0-.2 2.416l.324.453a.798.798 0 0 1 .064.796 7.448 7.448 0 0 0-.198.478.798.798 0 0 1-.608.517l-.55.092a1.875 1.875 0 0 0-1.566 1.849v.344c0 .916.663 1.699 1.567 1.85l.549.091c.281.047.508.25.608.517.06.162.127.321.198.478a.798.798 0 0 1-.064.796l-.324.453a1.875 1.875 0 0 0 .2 2.416l.243.243c.648.648 1.67.733 2.416.2l.453-.324a.798.798 0 0 1 .796-.064c.157.071.316.137.478.198.267.1.47.327.517.608l.092.55c.15.903.932 1.566 1.849 1.566h.344c.916 0 1.699-.663 1.85-1.567l.091-.549a.798.798 0 0 1 .517-.608 7.52 7.52 0 0 0 .478-.198.798.798 0 0 1 .796.064l.453.324a1.875 1.875 0 0 0 2.416-.2l.243-.243c.648-.648.733-1.67.2-2.416l-.324-.453a.798.798 0 0 1-.064-.796c.071-.157.137-.316.198-.478.1-.267.327-.47.608-.517l.55-.091a1.875 1.875 0 0 0 1.566-1.85v-.344c0-.916-.663-1.699-1.567-1.85l-.549-.091a.798.798 0 0 1-.608-.517 7.507 7.507 0 0 0-.198-.478.798.798 0 0 1 .064-.796l.324-.453a1.875 1.875 0 0 0-.2-2.416l-.243-.243a1.875 1.875 0 0 0-2.416-.2l-.453.324a.798.798 0 0 1-.796.064 7.462 7.462 0 0 0-.478-.198.798.798 0 0 1-.517-.608l-.091-.55a1.875 1.875 0 0 0-1.85-1.566h-.344ZM12 15.75a3.75 3.75 0 1 0 0-7.5 3.75 3.75 0 0 0 0 7.5Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

3
src/icon/news.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 0 1-2.25 2.25M16.5 7.5V18a2.25 2.25 0 0 0 2.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 0 0 2.25 2.25h13.5M6 7.5h3v3H6v-3Z" />
</svg>

After

Width:  |  Height:  |  Size: 477 B

3
src/icon/time.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>

After

Width:  |  Height:  |  Size: 247 B

BIN
src/logo/柚子娘-绿.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 KiB

BIN
src/logo/柚子娘.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
src/logo/红色字体.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
src/logo/绿色字体.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

28
src/main.ts Normal file
View File

@ -0,0 +1,28 @@
import "@/assets/main.css";
import { useConfig } from "@/config";
import icon from "@/icon/index.ts";
import { createPinia } from "pinia";
import "virtual:uno.css";
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
// 自定义主题配置 - 设置主色和二级色
import { ThemeServiceInit, infinityTheme, sweetTheme } from "devui-theme";
import { PerfectScrollbarPlugin } from "vue3-perfect-scrollbar";
// ThemeServiceInit({ customTheme }, "customTheme");
const themeService = ThemeServiceInit({ infinityTheme }, "infinityTheme");
themeService?.applyTheme(sweetTheme);
const app = createApp(App);
app.use(createPinia());
app.use(router);
useConfig();
for (const key in icon) {
// console.log(key, icon[key]);
app.component("icon-" + key, icon[key] as any);
}
app.use(PerfectScrollbarPlugin);
app.mount("#app");

100
src/router/index.ts Normal file
View File

@ -0,0 +1,100 @@
import { createRouter, createWebHistory } from "vue-router";
import layout from "@/Layout.vue";
import Home from '@/views/Home.vue';
// 导入视图组件 - 修改为@开头的懒加载路径
const Gallery = () => import("@/views/Gallery.vue");
const Article = () => import("@/views/Article.vue");
const Widget = () => import("@/views/Widget.vue");
const AppShare = () => import("@/views/AppShare.vue");
const Plink = () => import("@/views/Plink.vue");
const Login = () => import("@/views/login/Login.vue");
const NotFound = () => import("@/views/NotFound.vue");
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
meta: {},
children: [
{
path: "/",
component: layout,
redirect: {name:'home'},
children: [
{
path: "/home",
name: "home",
component: Home,
meta: {
title:'首页'
},
},
{
path: "/gallery",
name: "gallery",
component: Gallery,
meta: {
title:'画廊'
},
},
{
path: "/article",
name: "article",
component: Article,
meta: {
title:'文章'
},
},
{
path: "/widget",
name: "widget",
component: Widget,
meta: {
title:'工具'
},
},
{
path: "/appshare",
name: "appShare",
component: AppShare,
meta: {
title:'软件分享'
},
},
{
path: "/plink",
name: "plink",
component: Plink,
meta: {
title:'友链'
},
},
],
},
{
path: "/login",
name: "login",
component: Login,
meta: {
title:'登录'
},
},
{
path: "/404",
name: "404",
component: NotFound,
meta: {
title:'错误页'
},
},
],
},
],
});
export default router;

10
src/stores/nav.ts Normal file
View File

@ -0,0 +1,10 @@
export const useNavStore = defineStore('nav', () => {
const navH:Ref<number,number> = ref(0)
function setNavH(v:number) {
navH.value = v
}
return { navH , setNavH}
})

161
src/util/index.ts Normal file
View File

@ -0,0 +1,161 @@
// 将res的key对应的值复制给dzdata的同名key
/**
* 将源对象res的属性复制到目标对象dzdata中。
* @param {Object} res - 源对象,其属性将被复制。
* @param {Object} dzdata - 目标对象,将接收源对象的属性。
* @example
* ObjectCopy({a: 1, b: 2}, {c: 3}); // dzdata 变为 {a: 1, b: 2, c: 3}
*/
export function ObjectCopy<T extends Record<string, any>>(res: Partial<T>, dzdata: T): void {
Object.keys(dzdata).forEach(key => {
if (res.hasOwnProperty(key)) {
(dzdata as Record<string, any>)[key] = res[key];
}
});
}
/**
* @description: 时间格式化
* @param {*} time 传入时间参数,支持字符串和时间戳
* @param {*} f 对应格式 "YYYY-MM-DD hh:mm:ss" "YYYY年MM月DD日hh时mm分ss秒"
* 格式可以自定义,对应的字符串对应的时间会更新,输入单个字符表示可以不补零
* @return {*} 返回格式化的日期
*/
export function formatTime(time: string | number | Date, f: string): string | undefined {
const date = new Date(time);
if (!(date instanceof Date && !isNaN(date.getTime()))) {
console.error("不合法的日期!");
return;
}
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
const pad = (num: number): string => num.toString().padStart(2, "0");
const monthAdd0 = pad(month);
const dayAdd0 = pad(day);
const hourAdd0 = pad(hour);
const minuteAdd0 = pad(minute);
const secondAdd0 = pad(second);
let str = f.toString();
str = str.replace("YYYY", year.toString());
if (f.includes("M")) {
str = f.includes("MM") ? str.replace("MM", monthAdd0) : str.replace("M", month.toString());
}
if (f.includes("D")) {
str = f.includes("DD") ? str.replace("DD", dayAdd0) : str.replace("D", day.toString());
}
if (f.includes("h")) {
str = f.includes("hh") ? str.replace("hh", hourAdd0) : str.replace("h", hour.toString());
}
if (f.includes("m")) {
str = f.includes("mm") ? str.replace("mm", minuteAdd0) : str.replace("m", minute.toString());
}
if (f.includes("s")) {
str = f.includes("ss") ? str.replace("ss", secondAdd0) : str.replace("s", second.toString());
}
return str;
}
/**
* @description: 补零函数 为时间服务 不足两位的自动在数字前面加上0
* @param {*} n 待补零数字
* @return {*} 补零后数字
*/
function timeAdd0(n: number): string {
return n.toString().padStart(2, "0");
}
export function deepclone<T>(obj: T): T {
if (obj === null || typeof obj !== "object") {
return obj;
}
let newobj: any = obj instanceof Array ? [] : {};
if (window.JSON) {
newobj = JSON.parse(JSON.stringify(obj));
} else {
for (const i in obj) {
newobj[i] = typeof obj[i] === "object" ? deepclone(obj[i]) : obj[i];
}
}
return newobj as T;
}
/**
* 根据日期格式化时间。
* @param {Date} date - 需要格式化的日期对象。
* @param {string} format - 时间格式字符串。
* @returns {string} 格式化后的时间字符串。
* @example
* formatTimeBydate(new Date(), 'yyyy-MM-dd HH:mm:ss');
*/
export function formatTimeBydate(this: Date, f: string): string | undefined {
return formatTime(this, f);
}
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
const _this = this;
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(() => {
fn.apply(_this, args);
}, delay);
};
}
export function throttle<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout> | null = null;
return function (this: ThisParameterType<T>, ...args: Parameters<T>): void {
const _this = this;
if (!timer) {
timer = setTimeout(() => {
fn.apply(_this, args);
timer = null;
}, delay);
}
};
}
// 字典查询 通个一个字段返回另一个字段的值
/**
* @description 字典查询
* @param {*} dict 字典数组
* @param {*} ckey 查询字段
* @param {*} cvalue 查询值
* @param {*} rkey 返回字段
* @return {*} 返回值
*/
export function getDictValue<T extends Record<string, any>>(dict: T[], ckey: keyof T, cvalue: T[keyof T], rkey: keyof T): T[keyof T] | string {
let result = "";
dict.forEach(item => {
if (item[ckey] === cvalue) {
result = item[rkey];
}
});
return result;
}
// 分割数组
export function chunkArrayInGroups<T>(arr: T[], size: number): T[][] {
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));
}
// 重置对象
export function resetObject<T extends Record<string, any>>(obj: T): void {
Object.keys(obj).forEach(key => {
(obj as Record<string, any>)[key] = null;
});
}

58
src/util/request.ts Normal file
View File

@ -0,0 +1,58 @@
import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from "axios";
import axios from "axios";
import { useCookies } from "vue3-cookies";
const { cookies } = useCookies();
// const baseURL: string = "http://127.0.0.1:7777" + "/api";
const baseURL: string = "https://www.hxyouzi.com" + "/api";
const headers: Record<string, string> = {
"Content-Type": "application/x-www-form-urlencoded",
};
const request = axios.create({
baseURL,
headers,
});
// 添加请求拦截器
request.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const token = cookies.get("token");
if (token && typeof token === "string" && token.trim() !== "") {
config.headers["Authorization"] = "Bearer " + token;
} else delete config.headers["Authorization"];
return config;
},
function (error: AxiosError): string {
// 对请求错误做些什么
return error.message || "Request error";
}
);
// 添加响应拦截器
request.interceptors.response.use(
function (response: AxiosResponse): any {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data;
},
async function (error: AxiosError): Promise<string> {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
console.log("Response error", error);
// if (error.response?.status === 401) {
// window.$msg.warning("无效的token");
// cookies.remove("token");
// cookies.remove("userinfo");
// router.replace("/login");
// return "Unauthorized";
// }
return error.message || "Response error";
}
);
export default request;

14
src/views/AppShare.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="appshare-page">
<h1>软件分享页</h1>
<!-- 软件分享内容 -->
</div>
</template>
<script setup lang="ts">
// 软件分享页逻辑
</script>
<style scoped>
/* 软件分享页样式 */
</style>

14
src/views/Article.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="article-page">
<h1>文章页</h1>
<!-- 文章内容 -->
</div>
</template>
<script setup lang="ts">
// 文章页逻辑
</script>
<style scoped>
/* 文章页样式 */
</style>

14
src/views/Gallery.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="gallery-page">
<h1>画廊页</h1>
<!-- 画廊内容 -->
</div>
</template>
<script setup lang="ts">
// 画廊页逻辑
</script>
<style scoped>
/* 画廊页样式 */
</style>

240
src/views/Home.vue Normal file
View File

@ -0,0 +1,240 @@
<template>
<div class="home-page" :style="contentStyle">
<d-layout>
<d-content class="main-content">
<div class="pt-8 px-12">
<d-input class="devui-input-demo__mt" size="lg" v-model="searchWord" @keyup.enter="search" placeholder="请输入">
<template #prepend>
<d-select class="w-48" size="lg" v-model="broswer" :options="options"></d-select>
</template>
<template #append>
<d-icon name="search" style="font-size: inherit;" @click="search" />
</template>
</d-input>
</div>
<!-- 图片网格展示区域 -->
<PerfectScrollbar class="" :style="navStyle">
<div class="navcard grid-cols-4 gap-6 p-12">
<d-card class="bg-[white] h-25" v-for="(item, index) in navlist" :key="index"
@click="goExtra(item.menu_link)">
<template #content>
<div class="mt-2 w-full flex flex-col items-center cursor-pointer">
<div :style="{ background: item.color }"
class="w-8 h-8 rounded-full text-white flex items-center justify-center" v-if="item.icon_error">
{{ item.first }}</div>
<img v-else width="32" :src="item.menu_icon" @error="imgErr(index)" class="grid-image" />
<div class="mt-1 w-full text-center">{{ item.menu_name || "" }}</div>
</div>
</template>
</d-card>
<d-card class="bg-[white] h-25">
<div @click="addNav" class="w-full h-full flex flex-col items-center justify-center cursor-pointer">
<div :style="{ background: getRandomDarkColor() }"
class="w-12 h-12 rounded-full text-2xl text-white flex items-center justify-center">
+
</div>
</div>
</d-card>
</div>
</PerfectScrollbar>
</d-content>
<d-aside class="daside w-120">
<homeSide></homeSide>
</d-aside>
</d-layout>
<!-- 新增导航弹窗 -->
<d-modal class="!w-120" v-model="visible" title="新增导航">
<d-form ref="formNav" layout="vertical" :data="navData">
<d-form-item field="username">
<d-input @blur="getIcon" v-model="navData.menu_link" placeholder="请输入单行链接(必填)" />
</d-form-item>
<d-form-item field="password">
<d-input v-model="navData.menu_name" placeholder="请输入导航名称(必填)" />
</d-form-item>
<d-form-item class="form-operation-wrap">
<div class="flex">
<d-input v-model="navData.menu_icon" placeholder="请输入图标链接" />
<img class="ml-5" v-if="navData.menu_icon" width="30" height="30" :src="navData.menu_icon" alt="">
<div v-else class="ml-5 w-[30px] h-[30px]"></div>
</div>
</d-form-item>
</d-form>
<div class="mt-10 w-full flex justify-between">
<d-button @click="navCancel" variant="text" class="w-[49%] hover:bg-[#8a6684] hover:!text-white">取消</d-button>
<span class="text-[20px]"> | </span>
<d-button @click="navSubmit" variant="text" class="w-[49%] hover:bg-[#5c866a] hover:!text-white"
color="primary">确定</d-button>
</div>
</d-modal>
</div>
</template>
<script setup lang="ts">
import homeSide from '@/components/homeSide.vue'
import { getDictValue } from '@/util/index.ts'
// 新增导航弹窗
const visible: any = ref(false)
const navData: any = reactive({
menu_link: '',
menu_name: '',
menu_icon: ''
})
const formNav: any = ref(null)
// 首页逻辑
const nav: any = $store.nav.useNavStore()
const contentStyle: any = ref({})
const navStyle: any = ref({})
const searchWord: any = ref('')
const broswer: any = ref('bing')
const options = ref([
{
name: '必应',
value: 'bing',
url: 'https://cn.bing.com/search?q='
},
{
name: '百度',
value: 'baidu',
url: 'https://www.baidu.com/s?wd='
},
{
name: '谷歌',
value: 'google',
url: 'https://www.google.com/search?q='
},
{
name: '翻译',
value: 'trans',
url: 'https://translate.volcengine.com?text='
},
])
// 图片数据
const navlist: any = ref([])
async function getNavList() {
const res = await $http.nav.getNavList()
res.data.forEach((i: any) => {
i.icon_error = false
i.first = i.menu_name.at(0)
i.color = getRandomDarkColor()
});
navlist.value = res.data
console.log("&&&&&&&&&&&&&&", navlist.value);
}
function imgErr(index: number) {
navlist.value[index].icon_error = true
}
function goExtra(link: string) {
window.open(link, "_BLANK")
}
function search() {
const res = getDictValue(options.value, "value", broswer.value, "url")
if (res) {
window.open(res + searchWord.value, "_BLANK")
}
}
const getRandomDarkColor = (min: number = 30, max: number = 128): string => {
// 确保最大值不超过255且最小值不小于0
min = Math.max(0, min);
max = Math.min(255, max);
// 确保最大值大于最小值
if (max <= min) {
max = min + 30;
}
// 生成随机RGB值 (深色)
const r = Math.floor(Math.random() * (max - min + 1)) + min;
const g = Math.floor(Math.random() * (max - min + 1)) + min;
const b = Math.floor(Math.random() * (max - min + 1)) + min;
// 转换为十六进制
const toHex = (c: number) => {
const hex = c.toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
function addNav() {
if (!$cookies.get("token")) {
$msg.error("请先登录");
return
}
visible.value = true
}
async function getIcon() {
if (!navData.menu_link) return
const res = await $http.mix.getIcon({
url: navData.menu_link
})
console.log('>>> --> getIcon --> res:', res)
if (res.code == 200) {
navData.menu_icon = res.data.url
}
}
function navCancel() {
visible.value = false
formNav.value.resetFields()
}
async function navSubmit() {
console.log('>>> --> navSubmit --> navData:', navData)
if (!navData.menu_link || !navData.menu_name) {
$msg.error('请输入链接和名称')
return
}
const res = await $http.nav.addNav(navData)
console.log('>>> --> navSubmit --> res:', res)
if (res.code == 200) {
$msg.success('添加成功')
visible.value = false
formNav.value.resetFields()
getNavList()
}
}
onMounted(() => {
// console.log("&&&&&&&&&&&&&&", nav.navH);
contentStyle.value = {
height: `calc(100vh - ${nav.navH}px)`
}
navStyle.value = {
height: `calc(100vh - ${nav.navH}px - 110px)`
}
getNavList()
})
</script>
<style scoped lang="less">
/* 首页样式 */
.navcard {
display: grid;
}
:deep(.devui-input-slot__prepend) {
width: 90px;
}
</style>

11
src/views/NotFound.vue Normal file
View File

@ -0,0 +1,11 @@
<template>
<div>
404
</div>
</template>
<script setup lang="ts">
// 404页面
</script>
<style scoped></style>

14
src/views/Plink.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="plink-page">
<h1>友链页</h1>
<!-- 友链内容 -->
</div>
</template>
<script setup lang="ts">
// 友链页逻辑
</script>
<style scoped>
/* 友链页样式 */
</style>

14
src/views/Widget.vue Normal file
View File

@ -0,0 +1,14 @@
<template>
<div class="widget-page">
<h1>工具页</h1>
<!-- 工具内容 -->
</div>
</template>
<script setup lang="ts">
// 工具页逻辑
</script>
<style scoped>
/* 工具页样式 */
</style>

145
src/views/login/Login.vue Normal file
View File

@ -0,0 +1,145 @@
<template>
<div class="login-page w-[100vw] h-[100vh] relative">
<d-card class="w-120 !absolute top-[12%] right-8 bg-[#ffffff60] rounded-[10px]">
<d-tabs v-model="tid" type="pills">
<d-tab id="tab1" title="登录">
<d-form ref="formLogin" layout="vertical" :data="loginData" :rules="rules">
<d-form-item field="username" >
<d-input v-model="loginData.username" placeholder="请输入用户名" />
</d-form-item>
<d-form-item field="password" >
<d-input v-model="loginData.password" show-password placeholder="请输入密码" />
</d-form-item>
<d-form-item class="form-operation-wrap">
<d-button class="w-full" variant="solid" @click="login"> </d-button>
</d-form-item>
</d-form>
</d-tab>
<d-tab id="tab2" title="注册">
<d-form ref="formReg" layout="vertical" :data="regData" :rules="rrules">
<d-form-item field="username" >
<d-input v-model="regData.username" placeholder="请输入用户名" />
</d-form-item>
<d-form-item field="password" >
<d-input v-model="regData.password" show-password placeholder="请输入用密码" />
</d-form-item>
<d-form-item field="nickname" >
<d-input v-model="regData.nickname" placeholder="请输入昵称" />
</d-form-item>
<d-form-item class="form-operation-wrap">
<d-button class="w-full" variant="solid" @click="register">注册</d-button>
</d-form-item>
</d-form>
</d-tab>
</d-tabs>
</d-card>
</div>
</template>
<script setup lang="ts">
const router = useRouter()
// 登录注册逻辑
const tid = ref("tab1");
const formLogin: any = ref(null);
const loginData = reactive({
username: "",
password: ""
});
const formReg: any = ref(null);
const regData = reactive({
username: "",
password: "",
nickname: ""
});
const rules: any = reactive({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ validator: validateUsername, trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ validator: validatePassword, trigger: 'blur' }
]
})
const rrules: any = reactive({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ validator: validateUsername, trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ validator: validatePassword, trigger: 'blur' }
]
})
function login() {
formLogin.value.validate(async (is: boolean,b: any) => {
if (!is) {
$msg.error('信息填写不正确,请检查后再提交')
} else {
const res = await $http.user.login(loginData)
if (res?.code !== 200) {
$msg.error('登录失败')
return
}
$cookies.set('token',res.data.token,'7d')
$cookies.set('userinfo',res.data.userinfo,'7d')
$msg.success(res.msg)
router.push({ path: "/" })
}
})
}
function register() {
formReg.value.validate(async (is: boolean,b: any) => {
if (!is) {
$msg.error('信息填写不正确,请检查后再提交')
} else {
const res = await $http.user.register(regData)
if (res?.code !== 200) {
$msg.error('注册失败')
return
}
$msg.success(res.msg)
tid.value = 'tab1'
formReg.value.resetForm()
loginData.username = regData.username
}
})
}
function validateUsername(rule: any, value: string, callback: Function) {
if (/^[a-zA-Z][a-zA-Z0-9]{3,15}$/.test(value)) return callback()
else return callback(new Error('请输入4-16位字母或数字,且以字母开头'))
}
function validatePassword(rule: any, value: string, callback: Function) {
if (/^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{6,12}$/.test(value)) return callback()
else return callback(new Error('密码长度为6-12位且必须包含数字和字母'))
}
</script>
<style scoped lang="less">
/* 登录页面样式 */
.login-page {
background: transparent url("@/assets/images/05.jpeg") 0 0/cover no-repeat;
}
:deep(.devui-tabs__nav) {
display: flex;
justify-content: center;
li a span {
// width: 20%;
font-size: 18px !important;
font-weight: 500;
}
}
:deep(.devui-form__item--horizontal) {
margin-top: 30px;
}
</style>

12
tsconfig.app.json Normal file
View File

@ -0,0 +1,12 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue","auto-imports.d.ts", "components.d.ts"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"paths": {
"@/*": ["./src/*"]
}
}
}

11
tsconfig.json Normal file
View File

@ -0,0 +1,11 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
}
]
}

19
tsconfig.node.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": {
"noEmit": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["node"]
}
}

10
uno.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from "unocss";
export default defineConfig({
// ...UnoCSS options
theme: {
colors: {
primary: "#ec66ab",
},
},
});

40
vite.config.ts Normal file
View File

@ -0,0 +1,40 @@
import vue from "@vitejs/plugin-vue";
import { fileURLToPath, URL } from "node:url";
import UnoCSS from "unocss/vite";
import AutoImport from "unplugin-auto-import/vite";
import { defineConfig } from "vite";
import vueDevTools from "vite-plugin-vue-devtools";
import svgLoader from "vite-svg-loader";
import Components from "unplugin-vue-components/vite";
import { DevUiResolver } from "unplugin-vue-components/resolvers";
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
UnoCSS(),
svgLoader(),
AutoImport({
include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/, /\.md$/],
imports: ["vue", "pinia", "vue-router"],
}),
Components({
resolvers: [DevUiResolver()],
}),
],
esbuild: {
pure: ["console.log"], // 删除 console.log
drop: ["debugger"], // 删除 debugger
},
base: "/blog/",
server: {
host: "0.0.0.0",
port: 8080,
},
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", import.meta.url)),
},
},
});

3682
yarn.lock Normal file

File diff suppressed because it is too large Load Diff