整体优化

This commit is contained in:
2026-08-04 17:33:45 +08:00
parent cadc8885f3
commit 2c69b3f1c3
32 changed files with 2212 additions and 643 deletions

44
AGENTS.md Normal file
View File

@ -0,0 +1,44 @@
# AGENTS.md — 博客前端项目
Vue 3 + TypeScript + Vite 7 博客前端(后端 API 为 `https://www.hxyouzi.com/api`。UInaive-ui + UnoCSS + vue-devui(Tag) + md-editor-v3。风格为暖色玻璃拟态主色 `#ec66ab`)。
## 常用命令
| 命令 | 用途 |
| -------------------- | --------------------------------------------------------- |
| `npm run dev` | 开发服务器(端口 8989路径 base `/blog/`,首页 `/home` |
| `npm run type-check` | vue-tsc 类型检查(**改完代码必跑** |
| `npm run b` | type-check + build |
| `npm run build-only` | 仅构建 |
## 全局约定(勿重复 import / 注册)
- 全局变量直接使用、**无需 import**(类型在 `env.d.ts``$http`API`$msg`naive message`$store`pinia`$cookies``$modal`(弹窗,参数 `{title, content, contType: 'text'|'input', ...}`)。
- 组合式 API 已自动导入(`ref/computed/watch/useRoute/defineStore/useMessage` 等,见 `auto-imports.d.ts`**不要重复 import**。
- `src/components/` 下组件与 naive-ui `<n-xxx>` 自动注册(`components.d.ts`),模板直接用。
- 图标:`src/icon/` 顶层 svg 注册为 `<icon-<name> />``icon/menu/``icon/plink/` 子目录**不自动注册**,需手动 importvite-svg-loader
- API 模块:新建 `src/api/<name>/index.ts` 即自动挂到 `$http.<name>``import.meta.glob`)。**新增 icon/api/store 文件后需重启 dev server**glob 构建时静态分析)。
## 架构速览
- **路由**unplugin-vue-router 文件系统路由(`src/views/`),视图用 `definePage()``src/router/index.ts``/` 重定向到 `/home`,带 NProgress 守卫。
- **API 调用**`$http.<模块>.<函数>()` → 请求层 `src/util/request.ts`baseURL `https://www.hxyouzi.com/api`token 从 cookie 自动带 Bearer响应拦截直接返回 `response.data`)。
- **Stores**`src/stores/` setup 风格;`nav.ts` 提供 `navH`(导航栏高度,页面布局需减它)、`log.ts` 提供 `isLogin`。访问:`$store.nav.useNavStore()`
- **UnoCSS**:重度使用 arbitrary values`bg-[...]``shadow-[...]`);卡片类优先用 `uno.config.ts` 的 shortcuts`blog-list-panel``blog-detail-card``blog-detail-card-soft``home-glass-surface` 等)。
- **Less**vite 已全局注入 `@primary: #ec66ab` 等变量less 块内直接用。字体:霞鹜文楷(正文)+ Fira Code代码
- **页面背景模板**:所有视图沿用统一背景 — 暖色径向渐变 + 两个 blur 圆 + 网格 overlay参考 `src/views/Home.vue` 顶部)。
## 常见陷阱
- `console.log` 在 build 时会被 esbuild 删除(`pure: ["console.log"]`),调试用 dev 模式。
- 登录态cookie `userinfo` 存在即视为已登录。
- `$http.mix.getHoli()` 后端 500 不可用,必须 try/catch 防御式处理(失败静默隐藏)。
- 内部滚动容器用 naive-ui `n-scrollbar` 时监听 `@scroll`**不要监听 window scroll**(内部滚动不冒泡)。
- `Waterfall` 组件需传 `row-key="filepath"`GalleryItem 无 id 字段)。
- 阴影规范:三层体系(接触 `rgba(97,121,146,0.05)` + 主体 `0.08-0.13` + 暖色 `rgba(178,128,88,*)` + `inset 0 1px 0 rgba(255,255,255,0.7)` 高光),避免纯冷灰 `rgba(86,111,137,*)`
- 修改 `Home.vue` 导航卡片动画时先阅读 `/memories/repo/blog-nav.md`FLIP 动画有特殊约束)。
## 相关资料
- 导航首页细节FLIP 动画、侧边栏、画廊约定):仓库记忆 `/memories/repo/blog-nav.md`
- UI 截图批量转 Vue 组件:技能 `.agents/skills/ui/SKILL.md`

1
auto-imports.d.ts vendored
View File

@ -70,6 +70,7 @@ declare global {
const useCssVars: typeof import('vue')['useCssVars']
const useDialog: typeof import('naive-ui')['useDialog']
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router')['useLink']
const useLoadingBar: typeof import('naive-ui')['useLoadingBar']
const useMessage: typeof import('naive-ui')['useMessage']
const useModel: typeof import('vue')['useModel']

1
components.d.ts vendored
View File

@ -45,6 +45,7 @@ declare module 'vue' {
NPagination: typeof import('naive-ui')['NPagination']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpin: typeof import('naive-ui')['NSpin']
NTabPane: typeof import('naive-ui')['NTabPane']
NTabs: typeof import('naive-ui')['NTabs']
NUpload: typeof import('naive-ui')['NUpload']

19
env.d.ts vendored
View File

@ -1,24 +1,29 @@
/// <reference types="vite/client" />
/// <reference types="axios" />
/// <reference types="vue-router/auto" />
// 全局变量:由 src/config/index.ts 的 useConfig() 与 src/Index.vue 注册,
// 使用时不需 import均挂载在 window 上)
declare global {
// API 聚合对象:$http.<模块>.<函数>(),模块见 src/api/*/index.ts
let $http: any;
// vue3-cookies 封装:$cookies.get/set/remove
let $cookies: any;
// naive-ui useMessage 实例:$msg.success/warning/error/info
let $msg: any;
let $store:any;
let $modal :any
// pinia store 聚合:$store.nav.useNavStore() / $store.log.useLogStore()
let $store: any;
// 全局弹窗:$modal({ title, content, contType: 'text'|'input', ... })
let $modal: any;
interface Window {
// 扩展Windows环境下的全局$http对象
$http: any;
$cookies: any;
$msg: any;
$store:any
$modal:any
$store: any;
$modal: any;
}
}
export { };

View File

@ -31,7 +31,7 @@
"unplugin-vue-router": "^0.15.0",
"vite-svg-loader": "^5.1.0",
"vue": "^3.6.0-alpha.2",
"vue-router": "^4.5.1",
"vue-router": "^5.2.0",
"vue3-cookies": "^1.0.6"
},
"devDependencies": {

View File

@ -7,7 +7,7 @@
<router-view />
</n-layout-content>
<n-layout-footer v-show="route.path == '/home'" class="flex justify-center">
<div class="beian" ref="footer">
<div class="beian">
<a class="text-primary" href="https://beian.miit.gov.cn" target="_blank">皖ICP备2021017362号-1</a>
<a class="swag text-primary" target="_blank" href="https://www.hxyouzi.com/swag">api文档</a>
</div>
@ -36,8 +36,20 @@
</template>
<script setup lang="ts">
interface NmodalItem {
visible: boolean
title: string
content: string
contType?: string
placeholder?: string
cancelText?: string
submitText?: string
handdleCancel?: () => void
handdleSubmit?: () => void
handdleInputChange: (e: any) => void
}
const route = useRoute()
const footer = ref<HTMLElement | null>(null)
window.$msg = useMessage()
const modal: NmodalItem = reactive({
visible: false,
@ -51,18 +63,6 @@ const modal: NmodalItem = reactive({
handdleSubmit: () => { },
handdleInputChange: (e: any) => { }
})
interface NmodalItem {
visible: boolean
title: string
content: string
contType?: string
placeholder?: string
cancelText?: string
submitText?: string
handdleCancel?: () => void
handdleSubmit?: () => void
handdleInputChange: (e: any) => void
}
//mark method
function comemodal(mv: NmodalItem) {
if (!mv) return
@ -93,15 +93,6 @@ function comemodal(mv: NmodalItem) {
}
window.$modal = comemodal
onMounted(() => {
// 计算footer的高度
const footerHeight = footer.value?.clientHeight || 0;
console.log('footerHeight', footerHeight);
document.addEventListener('resize', () => {
})
})
</script>
<style scoped lang="less">

View File

@ -83,7 +83,6 @@ function resetWaterfall() {
columnHeights.value = Array(columnCount.value).fill(0);
// 重新分配图片
fileList.value.forEach(async (item:any) =>await addToWaterfall(item));
console.log('>>> --> resetWaterfall --> fileList:', fileList.value)
}
// 添加图片到瀑布流
@ -94,8 +93,6 @@ async function addToWaterfall(item: any) {
// 找到高度最小的列
let minHeight = Math.min(...columnHeights.value);
let minIndex = columnHeights.value.indexOf(minHeight);
console.log('>>> --> addToWaterfall --> item:', item)
console.log('>>> --> addToWaterfall --> minIndex:', minIndex)
// 添加到该列
columns.value[minIndex].push(item);
// 估算列高 - 实际高度会在图片加载后更新
@ -181,18 +178,13 @@ async function getFileList() {
// 监听滚动事件
const handleScroll: any = throttle((e: any) => {
console.log('>>> --> handleScroll --> loading:', e)
if (loading.value) return;
const scrollTop = e.target.scrollTop
console.log('>>> --> handleScroll --> scrollTop:', scrollTop)
const scrollHeight = e.target.scrollHeight
// console.log('>>> --> handleScroll --> scrollHeight:', scrollHeight)
const clientHeight = e.target.offsetHeight;
// 当滚动到距离底部20%时加载更多
// console.log('>>> --> clientHeight --> clientHeight:', clientHeight)
if (scrollTop + clientHeight >= scrollHeight * 0.8) {
console.log('>>> --> handleScroll --> 加载更多')
pn.value++;
getFileList();
}
@ -203,7 +195,6 @@ function onSearch() {
}
function downloadFile(url: string) {
console.log('>>> --> downloadFile --> url:', url)
// 创建临时a标签
const link = document.createElement('a');
// 设置下载链接

View File

@ -178,30 +178,38 @@ async function login() {
const isValid = await validateForm(formLogin.value)
if (!isValid) return
const res = await $http.user.login(loginData) as ApiResponse<UserInfo>
if (res?.code !== 200) {
$msg.error('登录失败')
return
}
try {
const res = await $http.user.login(loginData) as ApiResponse<UserInfo>
if (res?.code !== 200) {
$msg.error('登录失败')
return
}
persistLogin(res.data)
$msg.success(res.msg)
persistLogin(res.data)
$msg.success(res.msg)
} catch {
$msg.error('登录失败,请检查网络或账号密码')
}
}
async function register() {
const isValid = await validateForm(formReg.value)
if (!isValid) return
const res = await $http.user.register(regData) as ApiResponse
if (res?.code !== 200) {
$msg.error('注册失败')
return
}
try {
const res = await $http.user.register(regData) as ApiResponse
if (res?.code !== 200) {
$msg.error('注册失败')
return
}
$msg.success(res.msg)
tid.value = 'tab1'
loginData.username = regData.username
resetRegister()
$msg.success(res.msg)
tid.value = 'tab1'
loginData.username = regData.username
resetRegister()
} catch {
$msg.error('注册失败,请检查网络或稍后再试')
}
}
function validateUsername(_rule: unknown, value: string) {

View File

@ -5,10 +5,8 @@
<script setup lang="ts">
// https://aplayer.js.org/#/zh-Hans/
// import Hls from 'hls.js/dist/hls.min.js';
import APlayer from "aplayer"; // 引入音乐插件
import "aplayer/dist/APlayer.min.css"; // 引入音乐插件的样式
import { onMounted, reactive } from "vue";
// window.Hls = Hls;
const data: any = reactive({
@ -29,64 +27,61 @@ const data: any = reactive({
onMounted(async () => {
// 8834041785
// const res = await fetch("https://met.hxyouzi.com/meting-api/?server=tencent&type=playlist&id=7567066822")
// const res = await fetch("https://api.moeyao.cn/meting/?server=tencent&type=playlist&id=8834041785")
// const mlist = await res.json()
const metto = 'youzi'
const server = 'tencent'
const gdid = '7567066822'
let ck = 'RK=beuVmBI+x3; ptcz=08924c0c66c35b66361f002fb0b3005ff0f2144404f548f8728b167ffb3dc25d; pgv_pvid=2605897556; fqm_pvqid=c38eee7f-1f49-4867-8fb4-8dbb764cb93b; ts_uid=4584087650; pac_uid=0_xXEQBtbyPnKSj; omgid=0_SK8mZi6ZyjtN4; _qimei_q32=d611a6803057686110470945b0c17a21; _qimei_q36=fcef018f916225c22be032db30001e219617; _qimei_h38=e8d664cf2171af215382857302000000719801; eas_sid=I1j7m5e9o2O188P6Y4P7D745Z4; fqm_sessionid=f2c44c43-4d85-4dcb-99c0-cef6d6e2b0cb; pgv_info=ssid=s1311374400; ts_last=y.qq.com/; ts_refer=cn.bing.com/; _qpsvr_localtk=0.1390152059728047; login_type=1; psrf_access_token_expiresAt=1765271149; psrf_qqopenid=DE4C1520CB81DEEF7C0BE8E9B0FD2783; wxrefresh_token=; music_ignore_pskey=202306271436Hn@vBj; wxunionid=; euin=owv5oeCioiEAoz**; wxopenid=; tmeLoginType=2; psrf_qqrefresh_token=A9707E145813679F3654C6E051AA3E2F; psrf_qqaccess_token=C0591D34DF4A719D28815DBA71122DC5; psrf_musickey_createtime=1760087149; psrf_qqunionid=AD81CE40E407AFA9A2F984D05F7F5CD6; uin=2410633923; qm_keyst=Q_H_L_63k3NMFc5RtmTBDyGhskoc5AjaymIKYUZThRS2bJfh0AHztt8D10MRy1XN-MEAUtIkdF_oauym2Y51IbgjNvw1Wu8hZECDA; qqmusic_key=Q_H_L_63k3NMFc5RtmTBDyGhskoc5AjaymIKYUZThRS2bJfh0AHztt8D10MRy1XN-MEAUtIkdF_oauym2Y51IbgjNvw1Wu8hZECDA'
const res = await fetch(`https://met.hxyouzi.com/api?server=${server}&type=playlist&id=${gdid}`)
let mlist: any = await res.json()
data.audio = []
mlist.forEach((i: any) => {
data.audio.push({
title: i.title,
author: i.author,
url: i.url,
pic: i.pic,
lrc: i.lrc,
try {
const server = 'tencent'
const gdid = '7567066822'
const res = await fetch(`https://met.hxyouzi.com/api?server=${server}&type=playlist&id=${gdid}`)
const mlist: any = await res.json()
data.audio = []
mlist.forEach((i: any) => {
data.audio.push({
title: i.title,
author: i.author,
url: i.url,
pic: i.pic,
lrc: i.lrc,
});
});
// console.log('>>>>>',data.audio);
});
// 创建一个音乐播放器实例并挂载到DOM上同时进行相关配置
const ap = new APlayer({
container: document.getElementById("aplayer"),
audio: data.audio, // 音乐信息
...data.info, // 其他配置信息
});
const d: HTMLElement | null = document.querySelector("#aplayer > div.aplayer-body > div.aplayer-miniswitcher > button");
let flag = true;
const app: HTMLElement | null = document.querySelector("#aplayer > div.aplayer-lrc");
const apic: HTMLElement | null = document.querySelector("#aplayer div.aplayer-body");
if (!d || !app || !apic) return;
app.style.display = "block";
app.style.transform = "translateY(50px)";
apic.style.transform = "translateX(-66px)";
d.addEventListener("click", () => {
if (flag) {
app.style.transform = "translateY(0)";
ap.lrc.show();
apic.style.transform = "translateX(0)";
flag = false;
} else {
app.style.transform = "translateY(50px)";
ap.lrc.hide();
ap.list.hide();
apic.style.transform = "translateX(-66px)";
flag = true;
}
});
document.querySelector(".aplayer-list > ol")?.addEventListener("mousewheel", (e) => {
e.stopPropagation();
})
// 创建一个音乐播放器实例并挂载到DOM上同时进行相关配置
const ap = new APlayer({
container: document.getElementById("aplayer"),
audio: data.audio, // 音乐信息
...data.info, // 其他配置信息
});
const d: any = document.querySelector("#aplayer > div.aplayer-body > div.aplayer-miniswitcher > button");
let flag = true;
const app: any = document.querySelector("#aplayer > div.aplayer-lrc");
const apic: any = document.querySelector("#aplayer div.aplayer-body");
app.style.display = "block";
app.style.transform = "translateY(50px)";
apic.style.transform = "translateX(-66px)";
d.addEventListener("click", () => {
if (flag) {
app.style.transform = "translateY(0)";
ap.lrc.show();
apic.style.transform = "translateX(0)";
flag = false;
} else {
app.style.transform = "translateY(50px)";
ap.lrc.hide();
ap.list.hide();
apic.style.transform = "translateX(-66px)";
flag = true;
}
});
document.querySelector(".aplayer-list > ol")?.addEventListener("mousewheel", (e) => {
e.stopPropagation();
})
// 获取当前屏幕宽度
const width = document.documentElement.clientWidth; // 获取屏幕宽度
if (width < 767) d.click();
// 获取当前屏幕宽度
const width = document.documentElement.clientWidth; // 获取屏幕宽度
if (width < 767) d.click();
} catch (error) {
console.error('音乐列表加载失败:', error)
}
});
</script>
<style lang="less" scoped>

View File

@ -10,37 +10,35 @@
<script setup lang="ts">
import { throttle } from 'es-toolkit/function';
const scrollTop: any = ref(null)
const scrollTop = ref(0)
const visiable = ref(false)
const scrollElement: any = document.querySelector('.n-scrollbar .n-scrollbar-container')
const scrollElement = document.querySelector<HTMLElement>('.n-scrollbar .n-scrollbar-container')
const handleScroll = throttle(() => {
scrollTop.value = scrollElement.scrollTop
scrollTop.value = scrollElement?.scrollTop || 0
visiable.value = scrollTop.value > 200;
},1000)
function handleScrollTop() {
let timer: any = null;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(function fn() {
let timer = 0
const fn = () => {
if (scrollTop.value > 0) {
scrollTop.value -= 50;
scrollElement.scrollTop = document.documentElement.scrollTop = scrollTop.value;
if (scrollElement) scrollElement.scrollTop = scrollTop.value;
timer = requestAnimationFrame(fn);
} else {
cancelAnimationFrame(timer);
visiable.value = false;
}
});
};
timer = requestAnimationFrame(fn);
}
onMounted(() => {
scrollElement?.addEventListener('scroll', handleScroll);
})
onBeforeMount(() => {
window.removeEventListener('scroll', handleScroll);
// 修复:原来用 onBeforeMount 且移除的是 window 的监听,导致卸载时滚动监听泄漏
onUnmounted(() => {
scrollElement?.removeEventListener('scroll', handleScroll);
})
</script>
<style scoped lang="less">

View File

@ -2,7 +2,8 @@
<div class="pl-6 pr-6 !bg-transparent">
<n-card
embedded
class="side-glass-card side-glass-card--tint mt-4 overflow-hidden rounded-[24px]"
class="side-glass-card side-glass-card--tint mt-2 overflow-hidden rounded-[24px]"
:style="{ '--side-i': 0 }"
>
<template #header>
<div class="flex items-center font-semibold text-slate-700">
@ -11,20 +12,49 @@
</div>
</template>
<template #default>
<div class="w-full pr-20 text-center text-4xl font-[yj] font-[500] tracking-[0.04em] text-[#ec66ab] drop-shadow-[0_8px_20px_rgba(236,102,171,0.14)]">
{{ t }} </div>
<div class="mt-3 flex justify-between text-[15px] text-slate-500">
<span>今年已过 {{ jq.dayOfYear }} </span>
{{ d }}
<div class="flex items-center justify-between gap-3">
<div class="min-w-0">
<div
class="text-3xl font-[yj] font-[500] leading-[1.15] tracking-[0.04em] text-[#ec66ab] drop-shadow-[0_8px_20px_rgba(236,102,171,0.14)]"
>
{{ t }}
</div>
<div class="mt-1 text-[14px] text-slate-500">{{ d }}</div>
</div>
<img
v-if="jq.type != 0"
class="pointer-events-none w-[88px] shrink-0 drop-shadow-[0_10px_22px_rgba(236,160,102,0.18)]"
src="@/assets/images/offwork.png"
alt="offwork"
>
<img
v-else
class="pointer-events-none w-[88px] shrink-0 drop-shadow-[0_10px_22px_rgba(102,160,236,0.18)]"
src="@/assets/images/onwork.png"
alt="onwork"
>
</div>
<div class="mt-2">
<div class="flex items-center justify-between text-[13px] text-slate-500">
<span>今年已过 {{ jq.dayOfYear }} </span>
<span>{{ yearProgress }}%</span>
</div>
<div
class="mt-1 h-1 overflow-hidden rounded-full bg-white/70 shadow-[inset_0_1px_2px_rgba(97,121,146,0.14)]"
>
<div
class="h-full rounded-full bg-[linear-gradient(90deg,#f2b077,#ec66ab)] shadow-[0_0_10px_rgba(236,102,171,0.4)] transition-[width] duration-700"
:style="{ width: yearProgress + '%' }"
></div>
</div>
</div>
<img v-if="jq.type != 0" class="absolute right-4 -top-15" width="120" src="@/assets/images/offwork.png" alt="offwork">
<img v-else class="absolute right-4 -top-15" width="120" src="@/assets/images/onwork.png" alt="onwork">
</template>
</n-card>
<n-card
embedded
class="side-glass-card mt-4 overflow-hidden rounded-[24px]"
class="side-glass-card mt-2 overflow-hidden rounded-[24px]"
:style="{ '--side-i': 1 }"
>
<template #header>
<div class="flex items-center font-semibold text-slate-700">
@ -35,21 +65,27 @@
<template #default>
<div class="py-1" v-for="i in bdNews" :key="i.id">
<a
class="flex justify-between truncate rounded-xl border border-transparent px-2 text-[#526ecc] no-underline transition-colors duration-200 hover:border-white/80 hover:bg-white/65 hover:text-primary"
class="group flex items-center justify-between gap-2 rounded-xl border border-transparent px-2 py-0.5 text-[#526ecc] no-underline transition-[background-color,border-color,box-shadow,transform] duration-200 hover:-translate-y-px hover:border-white/80 hover:bg-white/65 hover:shadow-[0_2px_8px_rgba(97,121,146,0.08),inset_0_1px_0_rgba(255,255,255,0.6)]"
:href="i.url"
target="_blank"
>
<span class="flex items-center truncate">
{{ i.index }}. {{ i.title }}
<icon-hot v-show="i.index < 4" class="ml-2 inline-block w-4 text-[red]"></icon-hot>
<span class="flex min-w-0 items-center truncate">
<span
class="mr-2 inline-flex h-5 w-5 shrink-0 items-center justify-center rounded-md text-[11px] font-bold"
:class="i.index <= 3
? 'bg-[linear-gradient(135deg,#ec66ab,#f48a6f)] text-white shadow-[0_3px_8px_rgba(236,102,171,0.28)]'
: 'bg-slate-700/6 text-slate-400'"
>{{ i.index }}</span>
<span class="truncate transition-colors duration-200 group-hover:text-primary">{{ i.title }}</span>
<icon-hot v-show="i.index < 4" class="ml-2 inline-block w-4 shrink-0 text-[red]"></icon-hot>
</span>
<span class="ml-3 shrink-0 text-primary">{{ i.hot }}</span>
<span class="shrink-0 text-[13px] font-medium text-primary/85">{{ i.hot }}</span>
</a>
</div>
<div class="mt-2 flex items-center justify-between">
<div class="mt-1.5 flex items-center justify-between">
<div class="h-px w-2/5 bg-gradient-to-r from-transparent via-[#ec66ab] to-[#ec66ab]/40"></div>
<a
class="flex items-center font-medium text-[#ec66ab] no-underline"
class="flex items-center font-medium text-[#ec66ab] no-underline transition-transform duration-200 hover:scale-105"
href="https://www.baidu.com/s?ie=utf-8&wd=百度新闻"
target="_blank"
>
@ -63,26 +99,41 @@
<n-card
embedded
class="side-glass-card mt-4 overflow-hidden rounded-[24px]"
class="side-glass-card mt-2 overflow-hidden rounded-[24px]"
:style="{ '--side-i': 2 }"
>
<template #header>
<div class="flex items-center font-semibold text-slate-700">
<icon-date class="mr-2 h-[20px] w-5"></icon-date>
农历节气
<div class="ml-12 font-semibold text-[#ec66ab]">{{ jq.yearTips }} {{ jq.lunarCalendar }}</div>
<div class="ml-8 font-semibold text-[#ec66ab]">{{ jq.yearTips }} {{ jq.lunarCalendar }}</div>
</div>
</template>
<template #default>
<div class="mx-[5%] truncate text-sm text-slate-600">
{{ jq.suit }}
<div class="mx-1 flex items-center gap-1.5 text-sm text-slate-600">
<icon-star class="w-4 shrink-0 text-[#7fb69a]"></icon-star>
<span class="truncate">{{ jq.suit }}</span>
</div>
<div class="mx-[5%] truncate text-sm text-slate-600">
{{ jq.avoid }}
<div class="mx-1 mt-1.5 flex items-center gap-1.5 text-sm text-slate-600">
<icon-lock class="w-4 shrink-0 text-[#d99a9a]"></icon-lock>
<span class="truncate">{{ jq.avoid }}</span>
</div>
<div class="flex w-full justify-center">
<img class="mt-3 w-[90%] rounded-2xl border border-white/80 bg-white/70 shadow-[0_12px_26px_rgba(102,129,156,0.12)]" :src="jqImg" alt="节气图">
<img
class="mt-2 w-[82%] rounded-2xl border border-white/80 bg-white/70 shadow-[0_1px_3px_rgba(97,121,146,0.05),0_10px_22px_rgba(97,121,146,0.1),0_28px_54px_rgba(178,128,88,0.07),inset_0_1px_0_rgba(255,255,255,0.7)]"
:src="jqImg"
alt="节气图"
>
</div>
<div class="mt-1.5 text-center font-medium text-slate-600">{{ jq.solarTerms }}</div>
<div
v-if="holiName && holiDays !== null"
class="mx-auto mt-2 flex w-fit items-center gap-1.5 rounded-full border border-white/70 bg-[linear-gradient(135deg,rgba(255,240,246,0.85),rgba(255,233,223,0.8))] px-3.5 py-0.5 text-[13px] font-medium text-[#c2558e] shadow-[0_2px_8px_rgba(236,102,171,0.12),inset_0_1px_0_rgba(255,255,255,0.7)]"
>
<icon-date class="w-3.5"></icon-date>
<b class="font-bold">{{ holiName }}</b> 还有
<b class="font-bold">{{ holiDays }}</b>
</div>
<div class="mt-3 text-center font-medium text-slate-600">{{ jq.solarTerms }}</div>
</template>
</n-card>
</div>
@ -97,6 +148,25 @@ let timer: any = null
const jq = ref<any>('')
const jqImg = ref<any>('')
const bdNews = ref<any>([])
const holi = ref<any>(null)
// 今年进度百分比
const yearProgress = computed(() => {
const now = new Date()
const start = new Date(now.getFullYear(), 0, 1)
const end = new Date(now.getFullYear() + 1, 0, 1)
const pct = ((now.getTime() - start.getTime()) / (end.getTime() - start.getTime())) * 100
return Math.min(100, Math.max(0, pct)).toFixed(1)
})
// 下一个节假日(防御式解析多种字段名)
const holiName = computed(() => holi.value?.name || holi.value?.holiday || '')
const holiDays = computed(() => {
const v = holi.value
if (!v) return null
const raw = v.days ?? v.countdown ?? v.day ?? null
return typeof raw === 'number' ? raw : null
})
function getTime() {
const date = new Date()
@ -105,24 +175,43 @@ function getTime() {
}
async function getJq() {
const res = await $http.mix.getJq({
date: formatTime(new Date(), 'YYYYMMDD')
})
jq.value = res.data
const j = res.data.solarTerms.slice(0, 2)
const timestamp = new Date().getTime()
jqImg.value = `https://www.hxyouzi.com/img/jq/${j}.png?${timestamp}`
try {
const res = await $http.mix.getJq({
date: formatTime(new Date(), 'YYYYMMDD')
})
jq.value = res.data
const j = res.data.solarTerms.slice(0, 2)
const timestamp = new Date().getTime()
jqImg.value = `https://www.hxyouzi.com/img/jq/${j}.png?${timestamp}`
} catch {
// 接口失败时静默处理,保持空状态
}
}
async function getBdhot() {
const res = await $http.mix.getBdhot()
bdNews.value = res.data.slice(0, 5)
try {
const res = await $http.mix.getBdhot()
bdNews.value = res.data.slice(0, 5)
} catch {
bdNews.value = []
}
}
async function getHoli() {
try {
const res = await $http.mix.getHoli()
const data = Array.isArray(res.data) ? res.data[0] : res.data
holi.value = data || null
} catch {
holi.value = null
}
}
onMounted(() => {
getTime()
getJq()
getBdhot()
getHoli()
timer = setInterval(() => {
getTime()
@ -147,11 +236,40 @@ onUnmounted(() => {
linear-gradient(135deg, rgba(255, 255, 255, 0.34), rgba(255, 255, 255, 0.16)),
linear-gradient(180deg, rgba(241, 248, 252, 0.2), rgba(224, 236, 245, 0.08)) !important;
box-shadow:
0 22px 40px rgba(86, 111, 137, 0.14),
inset 0 1px 0 rgba(255, 255, 255, 0.7),
inset 0 -1px 0 rgba(255, 255, 255, 0.16) !important;
0 1px 3px rgba(97, 121, 146, 0.05),
0 14px 30px rgba(97, 121, 146, 0.09),
0 36px 72px rgba(178, 128, 88, 0.07),
inset 0 1px 0 rgba(255, 255, 255, 0.72),
inset 0 -1px 0 rgba(255, 255, 255, 0.14) !important;
backdrop-filter: blur(24px) saturate(165%);
-webkit-backdrop-filter: blur(24px) saturate(165%);
transition:
transform 0.3s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.3s ease;
animation: side-card-in 0.55s cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: calc(var(--side-i, 0) * 110ms);
}
.side-glass-card:hover {
transform: translateY(-2px);
box-shadow:
0 2px 6px rgba(97, 121, 146, 0.06),
0 20px 44px rgba(97, 121, 146, 0.13),
0 52px 100px rgba(178, 128, 88, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.8),
inset 0 -1px 0 rgba(255, 255, 255, 0.18) !important;
}
@keyframes side-card-in {
from {
opacity: 0;
transform: translateY(20px) scale(0.96);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
.side-glass-card::before,
@ -187,11 +305,11 @@ onUnmounted(() => {
:deep(.n-card > .n-card__content),
:deep(.n-card > .n-card__footer) {
padding: 12px 24px !important;
padding: 8px 20px !important;
}
:deep(.n-card > .n-card-header) {
padding: 16px 24px 8px !important;
padding: 10px 20px 4px !important;
}
:deep(.n-card) {

View File

@ -126,7 +126,6 @@ import linkSvg from '@/icon/menu/link.svg';
import picSvg from '@/icon/menu/pic.svg';
import type { DropdownOption, UploadFileInfo } from 'naive-ui';
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
import { RouterLink, useRoute, useRouter } from 'vue-router';
interface UserInfo {
@ -279,21 +278,26 @@ function handleUpload(f: any) {
}
async function autoLogin() {
const res = await $http.user.autologin()
if (res?.code !== 200) {
$msg.error('登录失败')
usrLog.setIsLogin(false)
$cookies.remove('token')
$cookies.remove('userinfo')
syncUserFromCookies()
return
}
try {
const res = await $http.user.autologin()
if (res?.code !== 200) {
$msg.error('登录失败')
usrLog.setIsLogin(false)
$cookies.remove('token')
$cookies.remove('userinfo')
syncUserFromCookies()
return
}
$cookies.set('token', res.data.token, '1d')
$cookies.set('userinfo', res.data.userinfo, '1d')
$msg.success(res.msg)
usrLog.setIsLogin(true)
syncUserFromCookies()
$cookies.set('token', res.data.token, '1d')
$cookies.set('userinfo', res.data.userinfo, '1d')
$msg.success(res.msg)
usrLog.setIsLogin(true)
syncUserFromCookies()
} catch {
usrLog.setIsLogin(false)
syncUserFromCookies()
}
}
function handdleItemCancel() {

View File

@ -1,7 +1,10 @@
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;
});
Object.keys(files)
// 排除自身 index.ts避免挂载出无意义的 $index
.filter(key => !key.endsWith("/index.ts"))
.forEach(key => {
const name = key.replace(/(\.\/|\.ts)/g, "");
(window as Record<string, any>)["$" + name] = files[key].default as any;
});
}

View File

@ -1,6 +1,6 @@
<template>
<div class="image-shell" :style="shellStyle">
<n-image ref="lazyRef" class="lazy__img" :src="url" @load="handleLoad" @error="handleError">
<n-image ref="lazyRef" class="lazy__img" :src="url" :preview-src="preUrl" @load="handleLoad" @error="handleError">
<template #placeholder>
<div class="lazy-placeholder">
<span class="lazy-placeholder__ring">
@ -32,6 +32,10 @@ const props = defineProps({
type: String,
default: "",
},
preUrl: {
type: String,
default: "",
},
loading: {
type: String,
default: loadingImg,
@ -93,7 +97,7 @@ nextTick(() => {
height: 100%;
overflow: hidden;
display: flex;
border-radius: 18px;
border-radius: 11px;
background:
radial-gradient(circle at top left, rgba(255, 198, 105, 0.18), transparent 32%),
linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(244, 249, 255, 0.72));
@ -118,7 +122,7 @@ nextTick(() => {
align-items: center;
justify-content: center;
overflow: hidden;
border-radius: 18px;
border-radius: 11px;
}
:deep(.n-image-placeholder),

View File

@ -201,7 +201,7 @@ defineExpose({
width: 100%;
position: relative;
overflow: hidden;
--waterfall-radius: 22px;
--waterfall-radius: 12px;
--waterfall-border: rgba(255, 255, 255, 0.72);
--waterfall-surface: linear-gradient(
180deg,

View File

@ -2,6 +2,7 @@ import "@/assets/main.css";
import { useConfig } from "@/config";
import icon from "@/icon/index.ts";
import { createPinia } from "pinia";
import { RouterLink, RouterView } from 'vue-router';
import "virtual:uno.css";
import 'vue-devui/tag/style.css';
import App from "./App.vue";
@ -20,6 +21,9 @@ app.use(createPinia());
app.use(router);
// app.use(VMdPreview)
useConfig();
// resolver 模式的实验性路由器不会自动注册 RouterLink / RouterView需手动全局注册
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
for (const key in icon) {
// console.log(key, icon[key]);
app.component("icon-" + key, icon[key] as any);

View File

@ -1,24 +1,35 @@
import NProgress from 'nprogress';
import NProgress from "nprogress";
import { createRouter, createWebHistory } from "vue-router";
// 文件系统路由vue-router/auto-routes 提供自动生成的 routes
import { routes } from "vue-router/auto-routes";
routes.unshift({
path: "/",
redirect: "/home",
});
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: routes as any,
routes: [
// 根路径重定向到 /home
{ path: "/", redirect: "/home" },
...routes,
],
});
// 开发时热更新路由,无需整页刷新
// if (import.meta.hot) {
// handleHotUpdate(router);
// }
// 前置守卫
router.beforeEach((to, from, next) => {
NProgress.start()
next();
router.beforeEach(to => {
NProgress.start();
});
// 后置守卫
router.afterEach(() => {
NProgress.done()
NProgress.done();
});
// 路由错误时也要停止进度条
router.onError(() => {
NProgress.done();
});
export default router;

View File

@ -1,10 +1,10 @@
export const useLogStore = defineStore('log', () => {
export const useLogStore = defineStore("log", () => {
// 登录状态
const isLogin:Ref<boolean,boolean> = ref(false)
function setIsLogin(v:boolean) {
isLogin.value = v
const isLogin = ref(false);
function setIsLogin(v: boolean) {
isLogin.value = v;
}
return { isLogin, setIsLogin }
})
return { isLogin, setIsLogin };
});

View File

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

View File

@ -1,14 +1,15 @@
// 将res的key对应的值复制给dzdata的同名key
/**
* 将源对象res的属性复制到目标对象dzdata
* 将源对象 res 中「目标对象 dzdata 已存在的 key」对应的值复制过去只覆盖既有键
* @param {Object} res - 源对象,其属性将被复制。
* @param {Object} dzdata - 目标对象,将接收源对象的属性
* @param {Object} dzdata - 目标对象,仅其已存在的键会被源对象值覆盖
* @example
* ObjectCopy({a: 1, b: 2}, {c: 3}); // dzdata 变为 {a: 1, b: 2, c: 3}
* ObjectCopy({a: 1, b: 2}, {a: 3}); // dzdata 变为 {a: 1}
*/
export function ObjectCopy<T extends Record<string, any>>(res: Partial<T>, dzdata: T): void {
Object.keys(dzdata).forEach(key => {
if (res.hasOwnProperty(key)) {
// 用 Object.prototype.hasOwnProperty 防御 res 为 null 原型对象的情况
if (Object.prototype.hasOwnProperty.call(res, key)) {
(dzdata as Record<string, any>)[key] = res[key];
}
});
@ -64,30 +65,18 @@ export function formatTime(time: string | number | Date, f: string): string | un
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];
}
// 优先使用 structuredClone支持循环引用、Date/Map/Set且性能优于 JSON 序列化
if (typeof structuredClone === "function") {
return structuredClone(obj);
}
return newobj as T;
// 降级方案JSON 序列化(会丢失 undefined/函数/符号,循环引用会抛错)
return JSON.parse(JSON.stringify(obj)) as T;
}
/**
@ -98,8 +87,8 @@ export function deepclone<T>(obj: T): T {
* @example
* formatTimeBydate(new Date(), 'yyyy-MM-dd HH:mm:ss');
*/
export function formatTimeBydate(this: string, f: string): string | undefined {
return formatTime(this, f);
export function formatTimeBydate(date: string | number | Date, f: string): string | undefined {
return formatTime(date, f);
}
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
@ -139,18 +128,19 @@ export function throttle<T extends (...args: any[]) => any>(fn: T, delay: number
* @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 getDictValue<T extends Record<string, any>>(dict: T[], ckey: keyof T, cvalue: T[keyof T], rkey: keyof T): T[keyof T] | undefined {
// find 命中即返回,避免 forEach 遍历完整字典;未命中返回 undefined
const item = dict.find(item => item[ckey] === cvalue);
return item ? item[rkey] : undefined;
}
// 分割数组
export function chunkArrayInGroups<T>(arr: T[], size: number): T[][] {
// 防御非法 size<= 0 时 Math.ceil 会产生 InfinityArray.from 将抛 RangeError
if (size < 1) {
console.warn("chunkArrayInGroups: size 必须大于 0已返回空数组");
return [];
}
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));
}
// 重置对象
@ -159,5 +149,3 @@ export function resetObject<T extends Record<string, any>>(obj: T): void {
(obj as Record<string, any>)[key] = null;
});
}

View File

@ -25,42 +25,30 @@ request.interceptors.request.use(
} else delete config.headers["Authorization"];
return config;
},
function (error: AxiosError): string {
(error: AxiosError) => {
// 对请求错误做些什么
return error.message || "Request error";
}
return Promise.reject(error);
},
);
// 添加响应拦截器
request.interceptors.response.use(
function (response: AxiosResponse): any {
(response: AxiosResponse): any => {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data;
},
async function (error: AxiosError): Promise<string> {
(error: AxiosError) => {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
console.log("Response error", error);
// 对响应错误做点什么(统一 reject保证调用方 try/catch 能捕获到错误)
if (error.response?.status === 401) {
window.$msg.warning("无效的token");
cookies.remove("token");
cookies.remove("userinfo");
// window.$modal({
// title: "无效的token",
// content: "token已失效需要登录请登录 =>",
// handdleSubmit: () => {
// router.replace("/login");
// },
// });
// router.replace("/login");
return "Unauthorized";
}
return error.message || "Response error";
}
return Promise.reject(error);
},
);
export default request;

View File

@ -1,15 +1,15 @@
/**
* js 文件下使用这个做类型提示
* @type import('naive-ui').GlobalThemeOverrides
*/
const themeOverrides = {
common: {
primaryColor: '#ec66ab',
primaryColorHover: '#ec66ab',
primaryColorPressed: '#ec66ab',
primaryColorSuppl: '#ec66ab',
},
// ...
}
/**
* js 文件下使用这个做类型提示
* @type import('naive-ui').GlobalThemeOverrides
*/
const themeOverrides = {
common: {
primaryColor: "#ec66ab",
primaryColorHover: "#ec66ab",
primaryColorPressed: "#ec66ab",
primaryColorSuppl: "#ec66ab",
},
// ...
};
export default themeOverrides;
export default themeOverrides;

View File

@ -13,7 +13,7 @@
<n-scrollbar ref="virtualListInst" class="relative z-[1] py-6" :style="boxStyle" @scroll="handleScroll">
<section
class="relative mx-auto w-[82%] rounded-[28px] border border-white/62 bg-[linear-gradient(145deg,rgba(255,251,246,0.68),rgba(237,246,244,0.56))] p-6 shadow-[0_20px_50px_rgba(110,124,112,0.11)] backdrop-blur-[20px] after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-[linear-gradient(120deg,rgba(255,255,255,0.24),transparent_45%)] after:content-['']">
class="relative mx-auto w-[82%] rounded-[28px] border border-white/62 bg-[linear-gradient(145deg,rgba(255,251,246,0.68),rgba(237,246,244,0.56))] p-6 shadow-[0_1px_3px_rgba(97,121,146,0.05),0_16px_38px_rgba(97,121,146,0.09),0_44px_84px_rgba(178,128,88,0.07),inset_0_1px_0_rgba(255,255,255,0.7)] backdrop-blur-[20px] after:pointer-events-none after:absolute after:inset-0 after:rounded-[inherit] after:bg-[linear-gradient(120deg,rgba(255,255,255,0.24),transparent_45%)] after:content-['']">
<div class="max-w-[48rem]">
<h1 class="m-0 text-[clamp(1.8rem,2.5vw,2.8rem)] leading-[1.2] text-[#2c3a4a]">
把喜欢的画面收进一片更柔和的光里
@ -24,11 +24,11 @@
</div>
<div
class="mt-6 rounded-full border border-white/62 bg-[linear-gradient(135deg,rgba(255,249,241,0.76),rgba(236,245,243,0.72)),radial-gradient(circle_at_top_right,rgba(255,255,255,0.28),transparent_34%)] shadow-[inset_0_1px_0_rgba(255,255,255,0.55)]">
<n-input class="gallery-search" round size="large" v-model:value="kw" @keyup.enter="onSearch"
placeholder="输入关键词,按回车开始搜索">
class="mt-6 rounded-full border border-white/62 bg-[linear-gradient(135deg,rgba(255,249,241,0.76),rgba(236,245,243,0.72)),radial-gradient(circle_at_top_right,rgba(255,255,255,0.28),transparent_34%)] shadow-[inset_0_1px_0_rgba(255,255,255,0.55)] transition-[box-shadow,border-color] duration-300">
<n-input class="gallery-search" round size="large" clearable v-model:value="kw" @keyup.enter="onSearch"
placeholder="输入关键词,按回车开始搜索">
<template #suffix>
<n-icon class="cursor-pointer text-slate-500" size="large">
<n-icon class="cursor-pointer text-slate-500 transition-colors duration-200 hover:text-primary" size="large" @click="onSearch">
<icon-search />
</n-icon>
</template>
@ -37,43 +37,62 @@
</section>
<section
class="mx-auto mt-6 w-[82%] rounded-[30px] border border-white/62 bg-white/42 p-6 shadow-[0_18px_42px_rgba(109,126,117,0.1)] backdrop-blur-[18px]">
class="mx-auto mt-6 w-[82%] rounded-[30px] border border-white/62 bg-white/42 p-6 shadow-[0_1px_3px_rgba(97,121,146,0.05),0_14px_30px_rgba(97,121,146,0.09),0_36px_72px_rgba(178,128,88,0.07),inset_0_1px_0_rgba(255,255,255,0.7)] backdrop-blur-[18px]">
<div class="mb-5 flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div>
<div class="text-[1.6rem] font-semibold text-[#2f4050]">图片列表</div>
<div class="mt-1 flex items-center gap-2 text-[13px] text-slate-500">
<span v-if="searchKeyword" class="inline-flex items-center rounded-full bg-[rgba(236,102,171,0.1)] px-2.5 py-0.5 font-semibold text-[rgb(212,88,154)]">
关键词{{ searchKeyword }}
</span>
<span>{{ galleryStats.status }}</span>
</div>
</div>
<div
class="inline-flex items-center whitespace-nowrap rounded-full border border-white/80 bg-white/68 px-4 py-[0.65rem] text-[#526376]">
<div class="inline-flex items-center gap-2 self-start rounded-full bg-white/68 px-5 py-2 text-[#526376] shadow-[0_2px_8px_rgba(97,121,146,0.08),inset_0_1px_0_rgba(255,255,255,0.7)]">
<span class="inline-block h-2 w-2 rounded-full" :class="loading ? 'animate-pulse bg-[#ec66ab]' : 'bg-[#7fb69a]'"></span>
{{ galleryStats.loaded }}
</div>
</div>
<Waterfall ref="waterfall" :list="fileList" :gutter="gutter" :columns="column" class="w-full" img-selector="url"
<Waterfall ref="waterfall" :list="fileList" :gutter="gutter" :columns="column" row-key="filepath" class="w-full" img-selector="url"
animation-effect="fadeIn" :animation-duration="1000" :animation-delay="300" backgroundColor="transparent">
<template #item="{ item }">
<div
class="group relative flex h-full items-center justify-center overflow-hidden rounded-[24px] border border-white/82 bg-[linear-gradient(180deg,rgba(255,252,248,0.92),rgba(239,244,241,0.78))] shadow-[0_16px_34px_rgba(116,132,124,0.14)] backdrop-blur-[14px] transition-[transform,box-shadow,border-color] duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] hover:border-white/196 hover:shadow-[0_22px_42px_rgba(102,123,112,0.18)]">
<LazyImg class="overflow-hidden rounded-[20px]" :Pwidth="item.width" :Pheight="item.height"
:url="item.filepath" />
class="gallery-card group relative flex h-full items-center justify-center overflow-hidden rounded-[12px] border border-white/82 bg-[linear-gradient(180deg,rgba(255,252,248,0.92),rgba(239,244,241,0.78))] backdrop-blur-[14px] transition-[transform,box-shadow,border-color] duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] hover:-translate-y-1">
<LazyImg class="gallery-img overflow-hidden rounded-[12px]" :Pwidth="item.width" :Pheight="item.height"
:url="item.thumbnail" :preUrl="item.filepath" />
<div
class="absolute left-0 right-0 top-0 z-10 overflow-hidden text-ellipsis whitespace-nowrap rounded-[14px] border border-white/22 bg-[rgba(39,50,65,0.1)] px-3 py-1 items-center justify-center text-sm text-white hidden backdrop-blur-[12px] group-hover:flex">
class="pointer-events-none absolute left-0 right-0 top-0 z-10 truncate rounded-[12px] border-b border-white/22 bg-gradient-to-b from-[rgba(39,50,65,0.55)] to-transparent px-3 pb-3 pt-1.5 text-sm text-white opacity-0 backdrop-blur-[6px] transition-opacity duration-300 group-hover:opacity-100">
{{ item.filename }}
</div>
<div
class="absolute bottom-0 left-0 right-0 z-10 flex items-center justify-between gap-3 rounded-[14px] border border-white/22 bg-[rgba(39,50,65,0.5)] px-3 py-1 text-white backdrop-blur-[12px]">
class="absolute bottom-0 left-0 right-0 z-10 flex translate-y-[102%] items-center justify-between gap-3 rounded-[12px] border border-white/22 bg-[rgba(39,50,65,0.55)] px-3 py-1.5 text-white backdrop-blur-[12px] transition-transform duration-[320ms] [transition-timing-function:cubic-bezier(0.22,1,0.36,1)] group-hover:translate-y-0">
<div class="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap text-sm text-[#eef4f8]">
<span class="font-semibold text-[#ffd8cb]">{{ item.nickname }}</span> 分享
</div>
<div
class="inline-flex flex-shrink-0 cursor-pointer items-center rounded-full bg-white/18 px-4 py-1 text-[0.86rem] font-semibold text-[#ffe3d3] transition-colors duration-200 hover:bg-white/24 hover:text-white"
@click="downloadFile(item.filepath)">
<icon-download class="mr-1 h-5 w-5" />
class="inline-flex flex-shrink-0 cursor-pointer items-center rounded-full bg-white/18 px-3.5 py-1 text-[0.86rem] font-semibold text-[#ffe3d3] transition-[background-color,transform] duration-200 hover:scale-105 hover:bg-white/28 hover:text-white active:scale-95"
@click.stop="downloadFile(item.filepath)">
<icon-download class="mr-1 h-4 w-4" />
下载
</div>
</div>
</div>
</template>
</Waterfall>
<div v-if="fileList.length === 0 && !loading" class="flex flex-col items-center justify-center py-16 text-slate-400">
<icon-search class="mb-3 h-10 w-10 opacity-50" />
<p class="text-[15px]">{{ searchKeyword ? '未找到相关图片,换个关键词试试' : '暂无图片' }}</p>
</div>
<div v-if="loading" class="flex items-center justify-center gap-2 py-6 text-sm text-slate-400">
<icon-loading class="zhuan h-5 w-5 text-primary" />
<span>{{ fileList.length ? '加载中...' : '首次加载中...' }}</span>
</div>
<div v-else-if="isLoadAll && fileList.length" class="py-5 text-center text-[13px] text-slate-400">
已加载全部 {{ fileList.length }} 张图片
</div>
</section>
</n-scrollbar>
</div>
@ -120,6 +139,7 @@ const cwidth = ref(240)
const column = ref(5)
const gutter = ref(18)
const isLoadAll = ref(false)
let requestSeq = 0
const searchKeyword = computed(() => kw.value.trim())
@ -180,6 +200,7 @@ function resetGalleryState() {
async function getFileList() {
if (loading.value) return
loading.value = true
const seq = ++requestSeq
try {
const res = await $http.file.getFileList({
@ -189,6 +210,9 @@ async function getFileList() {
})
const list = res.data as GalleryItem[]
// 过期请求直接丢弃,避免搜索时旧结果覆盖新结果
if (seq !== requestSeq) return
if (list.length < ps.value) {
isLoadAll.value = true
}
@ -197,8 +221,10 @@ async function getFileList() {
} catch (error) {
console.error('获取图片列表失败:', error)
} finally {
loading.value = false
renderWaterfall()
if (seq === requestSeq) {
loading.value = false
renderWaterfall()
}
}
}
@ -216,12 +242,17 @@ const handleScroll = throttle((e: Event) => {
pn.value += 1
getFileList()
}
}, 1000)
}, 300)
function onSearch() {
pn.value = 1
kw.value = kw.value.trim()
isLoadAll.value = false
// 立即清空旧列表,避免搜索时看到上一批结果
fileList.value = []
// 若正在加载中,先解除守卫,让新搜索请求可以发起
loading.value = false
requestSeq += 1
getFileList()
}
@ -240,16 +271,17 @@ onMounted(() => {
calculateColumns()
updateBoxStyle()
getFileList()
window.addEventListener('resize', calculateColumns)
window.addEventListener('resize', updateBoxStyle)
window.addEventListener('scroll', handleScroll)
window.addEventListener('resize', handleResize)
})
function handleResize() {
calculateColumns()
updateBoxStyle()
}
onUnmounted(() => {
resetGalleryState()
window.removeEventListener('resize', calculateColumns)
window.removeEventListener('resize', updateBoxStyle)
window.removeEventListener('scroll', handleScroll)
window.removeEventListener('resize', handleResize)
})
// 防抖函数
@ -260,8 +292,14 @@ onUnmounted(() => {
:deep(.gallery-search .n-input) {
background: rgba(255, 255, 255, 0.74) !important;
border: 1px solid rgba(255, 255, 255, 0.7) !important;
box-shadow: 0 10px 28px rgba(109, 138, 132, 0.1) !important;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.6),
0 2px 6px rgba(97, 121, 146, 0.05),
0 10px 24px rgba(97, 121, 146, 0.09) !important;
backdrop-filter: blur(18px);
transition:
border-color 0.25s ease,
box-shadow 0.25s ease;
}
:deep(.gallery-search .n-input__input-el) {
@ -274,7 +312,52 @@ onUnmounted(() => {
:deep(.gallery-search .n-input.n-input--focus) {
border-color: rgba(122, 160, 148, 0.7) !important;
box-shadow: 0 16px 38px rgba(118, 149, 139, 0.16) !important;
box-shadow:
0 0 0 4px rgba(122, 160, 148, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.6),
0 2px 6px rgba(97, 121, 146, 0.06),
0 16px 36px rgba(97, 121, 146, 0.14) !important;
}
/* 图片卡片 hover 增强 */
.gallery-card {
box-shadow:
0 1px 3px rgba(97, 121, 146, 0.05),
0 12px 26px rgba(97, 121, 146, 0.09),
0 32px 60px rgba(178, 128, 88, 0.06),
inset 0 1px 0 rgba(255, 255, 255, 0.72) !important;
}
.gallery-card:hover {
border-color: rgba(255, 255, 255, 0.88) !important;
box-shadow:
0 2px 6px rgba(97, 121, 146, 0.06),
0 18px 38px rgba(97, 121, 146, 0.14),
0 48px 90px rgba(178, 128, 88, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.8) !important;
}
.gallery-img {
transition: transform 0.45s cubic-bezier(0.22, 1, 0.36, 1);
}
.gallery-card:hover .gallery-img {
transform: scale(1.045);
}
/* 加载图标旋转 */
.zhuan {
animation: gallery-spin 1.5s linear infinite;
}
@keyframes gallery-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
:deep(.n-image img) {

View File

@ -356,10 +356,14 @@ function getTagColor(tag: string) {
}
async function getPlinkList() {
const res = await $http.plink.getPlinkList() as PlinkListResponse
const uniqueTags = Array.from(new Set(res.data.flatMap((item) => getTags(item.tagname))))
tagColorList.value = buildTagColorMap(uniqueTags)
pList.value = res.data
try {
const res = await $http.plink.getPlinkList() as PlinkListResponse
const uniqueTags = Array.from(new Set(res.data.flatMap((item) => getTags(item.tagname))))
tagColorList.value = buildTagColorMap(uniqueTags)
pList.value = res.data
} catch {
pList.value = []
}
}
function gotoWebsite(url: string) {
@ -404,15 +408,19 @@ async function submitPut() {
return
}
const res = await $http.plink.addPlink(plinkData)
if (res?.code !== 200) {
$msg.error(res.msg)
return
}
try {
const res = await $http.plink.addPlink(plinkData)
if (res?.code !== 200) {
$msg.error(res.msg)
return
}
$msg.success(res.msg)
resetFormData()
await getPlinkList()
$msg.success(res.msg)
resetFormData()
await getPlinkList()
} catch {
$msg.error('提交失败,请稍后再试')
}
}
onMounted(() => {

View File

@ -4,7 +4,7 @@
</div>
</template>
<script setup lang="js">
<script setup lang="ts">
definePage({
name: 'widget',

View File

@ -12,12 +12,21 @@
class="pointer-events-none absolute inset-0 opacity-14 [background-image:linear-gradient(rgba(255,255,255,0.18)_1px,transparent_1px),linear-gradient(90deg,rgba(255,255,255,0.18)_1px,transparent_1px)] [background-size:36px_36px] [mask-image:linear-gradient(180deg,rgba(0,0,0,0.8),transparent_85%)]">
</div>
<div class="relative z-[1] mx-auto flex h-full w-[86%] gap-6 py-6">
<aside class="hidden w-[21rem] shrink-0 lg:block">
<!-- 阅读进度条 -->
<div class="pointer-events-none absolute inset-x-0 top-0 z-[60] h-[3px] bg-white/10">
<div
class="h-full rounded-r-full bg-gradient-to-r from-[#ec66ab] via-[#c98a5d] to-[#76b0d6] transition-[width] duration-150 ease-out"
:style="{ width: `${progress}%` }"></div>
</div>
<div class="relative z-[1] mx-auto flex h-full w-[92%] max-w-[1500px] gap-6 py-6 xl:w-[86%]">
<aside v-if="!loading && !loadError" class="hidden w-[21rem] shrink-0 lg:block">
<div class="sticky top-6">
<section class="blog-detail-card p-6">
<div class="relative z-[1]">
<h1 class="mt-3 text-[2rem] text-center leading-[1.2] text-[#2c3a4a]">阅读目录</h1>
<h1 class="mt-3 text-center text-[2rem] leading-[1.2] text-[#2c3a4a]">阅读目录</h1>
<div class="mx-auto mt-2 h-1 w-16 rounded-full bg-gradient-to-r from-[#ec66ab] via-[#c98a5d] to-[#76b0d6] opacity-70">
</div>
<div class="blog-detail-card-soft mt-6 p-4">
<div class="mb-3 flex items-center justify-between">
@ -25,11 +34,11 @@
<span class="rounded-full cursor-pointer bg-primary px-3 py-1 text-xs text-white">正文导航</span>
</div>
<div class="text-[13px] leading-5 text-[#526376]">
<div class="toc-list text-[13px] leading-5 text-[#526376]">
<template v-for="item in tocItems" :key="item.id">
<details v-if="item.children.length" class="group" :open="item.open">
<summary
class="flex cursor-pointer list-none items-center gap-2 py-1 text-[#526376] marker:hidden select-none">
class="flex cursor-pointer list-none items-center gap-2 rounded-lg py-1 text-[#526376] marker:hidden select-none">
<span
class="text-[11px] text-[#8a98a1] transition-transform duration-200 group-open:rotate-90"></span>
<span class="truncate transition-colors duration-200"
@ -39,9 +48,10 @@
</span>
</summary>
<div class="ml-4 border-l border-[#d7dfdf] pl-3">
<div v-for="child in item.children" :key="child.id" class="py-1">
<div class="cursor-pointer truncate transition-colors duration-200"
:class="activeHeadingId === child.id ? 'text-primary font-semibold' : 'text-[#687983] hover:text-[#2f4050]'"
<div v-for="child in item.children" :key="child.id" class="py-1"
:style="{ paddingLeft: `${Math.min(child.level - 2, 3) * 10}px` }">
<div class="toc-child cursor-pointer truncate rounded-md transition-colors duration-200"
:class="activeHeadingId === child.id ? 'toc-active text-primary font-semibold' : 'text-[#687983] hover:text-[#2f4050]'"
@click="scrollToHeading(child.id)">
{{ child.text }}
</div>
@ -49,8 +59,8 @@
</div>
</details>
<div v-else class="cursor-pointer py-1 transition-colors duration-200"
:class="activeHeadingId === item.id ? 'text-primary font-semibold' : 'text-[#526376] hover:text-[#2f4050]'"
<div v-else class="toc-leaf cursor-pointer rounded-md py-1 transition-colors duration-200"
:class="activeHeadingId === item.id ? 'toc-active text-primary font-semibold' : 'text-[#526376] hover:text-[#2f4050]'"
@click="scrollToHeading(item.id)">
{{ item.text }}
</div>
@ -67,79 +77,172 @@
</aside>
<section class="blog-list-panel min-w-0 flex-1 overflow-hidden p-4 sm:p-6">
<n-scrollbar ref="detailScrollbar" :style="contentStyle" trigger="none" class="pr-1 mb-4 rounded-[3em] overflow-hidden">
<div class="space-y-5 pr-1">
<section class="blog-detail-card p-6">
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div class="min-w-0">
<div class="text-[0.82rem] font-semibold tracking-[0.16em] text-[#718195]">文章详情</div>
<h1
class="my-4 !w-full ml-1/2 text-center text-[1.9rem] font-semibold leading-[1.3] text-[#2f4050] sm:text-[2.15rem]">
{{ blogData.title || '文章内容' }}
</h1>
</div>
<n-breadcrumb class="shrink-0 rounded-full border border-white/52 bg-white/16 px-4 text-[#526376]"
separator="|">
<n-breadcrumb-item href="/blog/blog">文章</n-breadcrumb-item>
<n-breadcrumb-item>{{ blogData.title }}</n-breadcrumb-item>
</n-breadcrumb>
</div>
<div class="flex flex-wrap items-center justify-right gap-x-5 gap-y-2 text-sm text-[#708091]">
<em class="flex items-center gap-1 not-italic text-primary">
<n-icon><icon-author /></n-icon>
{{ blogData.nickname }}
</em>
<em class="flex items-center gap-1 not-italic">
<n-icon><icon-date /></n-icon>
{{ formatTime(blogData.updated_at, 'YYYY年MM月DD日 hh时') }}
</em>
<!-- 加载骨架屏 -->
<div v-if="loading" class="flex h-full flex-col gap-5">
<section class="blog-detail-card p-6">
<div class="flex items-center justify-between">
<div class="blog-skeleton h-5 w-24 rounded-full"></div>
<div class="blog-skeleton h-7 w-40 rounded-full"></div>
</div>
<div class="mt-6 space-y-3">
<div class="blog-skeleton mx-auto h-9 w-2/3 rounded-xl"></div>
<div class="blog-skeleton mx-auto h-4 w-1/3 rounded-full"></div>
<div class="mx-auto mt-5 flex gap-3">
<div class="blog-skeleton h-6 w-16 rounded-full"></div>
<div class="blog-skeleton h-6 w-20 rounded-full"></div>
<div class="blog-skeleton h-6 w-14 rounded-full"></div>
</div>
</div>
</section>
<section class="blog-detail-card p-6">
<div class="blog-skeleton h-10 w-36 rounded-xl"></div>
<div class="mt-6 space-y-3">
<div class="blog-skeleton h-4 w-full rounded-full"></div>
<div class="blog-skeleton h-4 w-11/12 rounded-full"></div>
<div class="blog-skeleton h-4 w-4/5 rounded-full"></div>
<div class="blog-skeleton h-4 w-full rounded-full"></div>
<div class="blog-skeleton h-4 w-2/3 rounded-full"></div>
</div>
</section>
</div>
<div class="mt-4 flex flex-wrap justify-center gap-3">
<div v-for="(item, index) in tags" :key="index" :style="{ backgroundColor: getTagColor(item) }"
class="inline-flex cursor-pointer items-center gap-1 rounded-full px-3 py-1 text-sm text-white shadow-[0_8px_20px_rgba(88,108,125,0.16)]">
<n-icon><icon-tag /></n-icon>
{{ item }}
</div>
</div>
</section>
<section class="blog-detail-card p-4 sm:p-5">
<n-collapse @item-header-click="AISum" arrow-placement="right">
<n-collapse-item>
<template #header>
<div class="flex items-center text-primary">
<icon-star class="mx-2 w-6" />
<div class="text-[20px] font-bold">AI 摘要</div>
</div>
</template>
<template #header-extra>
<div class="flex text-primary">
<icon-star class="mx-2 w-6" />
</div>
</template>
<div class="whitespace-pre-line indent-[2em] text-[#66778b]">
{{ msg }}
</div>
</n-collapse-item>
</n-collapse>
</section>
<section class="blog-detail-card px-5 py-6 sm:px-8 sm:py-8">
<MdPreview class="relative article-preview !bg-transparent" :modelValue="markdown" />
</section>
<!-- 加载失败 -->
<div v-else-if="loadError" class="flex h-full flex-col items-center justify-center gap-4 text-center">
<div class="blog-detail-card px-10 py-12">
<div class="text-[1.2rem] font-semibold text-[#2f4050]">文章加载失败</div>
<p class="mt-2 text-sm text-[#8a98a1]">{{ loadError }}</p>
<button type="button" @click="reload"
class="mt-6 cursor-pointer rounded-full bg-primary px-6 py-2 text-sm text-white shadow-[0_10px_24px_rgba(88,108,125,0.24)] transition-transform hover:-translate-y-0.5">
重新加载
</button>
</div>
</n-scrollbar>
</div>
<button
v-show="showBackToTop"
type="button"
aria-label="回到顶部"
class="absolute bottom-6 right-6 z-[3] flex h-11 w-11 items-center justify-center rounded-full outline-none border-none bg-white/24 text-primary shadow-[0_16px_28px_rgba(109,126,117,0.14),inset_0_1px_0_rgba(255,255,255,0.24)] backdrop-blur-[16px] transition-all duration-300 hover:-translate-y-1 hover:bg-white/36"
@click="scrollToTop"
>
<icon-backtop class="h-5 w-5" />
</button>
<template v-else>
<n-scrollbar ref="detailScrollbar" :style="contentStyle" trigger="none"
class="pr-1 mb-4 rounded-[3em] overflow-hidden">
<div class="space-y-5 pr-1">
<section class="blog-detail-card p-6">
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
<div class="min-w-0">
<div class="text-[0.82rem] font-semibold tracking-[0.16em] text-[#718195]">文章详情</div>
<h1
class="my-4 text-center text-[1.9rem] font-semibold leading-[1.3] text-[#2f4050] sm:text-[2.15rem]">
{{ blogData.title || '文章内容' }}
</h1>
<div
class="mx-auto mb-1 h-1 w-24 rounded-full bg-gradient-to-r from-[#ec66ab] via-[#c98a5d] to-[#76b0d6] opacity-70">
</div>
</div>
<n-breadcrumb class="shrink-0 rounded-full border border-white/52 bg-white/16 px-4 text-[#526376]"
separator="|">
<n-breadcrumb-item href="/blog/blog">文章</n-breadcrumb-item>
<n-breadcrumb-item>{{ blogData.title }}</n-breadcrumb-item>
</n-breadcrumb>
</div>
<div
class="mt-2 flex flex-wrap items-center justify-center gap-x-5 gap-y-2 text-sm text-[#708091]">
<em
class="flex items-center gap-1.5 rounded-full bg-white/46 px-3 py-1 not-italic text-primary shadow-[0_4px_12px_rgba(88,108,125,0.08)]">
<n-icon><icon-author /></n-icon>
{{ blogData.nickname }}
</em>
<em class="flex items-center gap-1.5 not-italic">
<n-icon><icon-date /></n-icon>
{{ formatTime(blogData.updated_at, 'YYYY年MM月DD日 hh时') }}
</em>
</div>
<div class="mt-5 flex flex-wrap justify-center gap-3">
<div v-for="(item, index) in tags" :key="index" :style="{ backgroundColor: getTagColor(item) }"
class="inline-flex cursor-pointer items-center gap-1 rounded-full px-3 py-1 text-sm text-white shadow-[0_8px_20px_rgba(88,108,125,0.16)] transition-all duration-300 hover:-translate-y-0.5 hover:shadow-[0_12px_26px_rgba(88,108,125,0.28)]">
<n-icon><icon-tag /></n-icon>
{{ item }}
</div>
</div>
</section>
<section class="blog-detail-card p-4 sm:p-5">
<n-collapse :expanded-names="aiExpanded ? ['ai-summary'] : []" arrow-placement="right"
@update:expanded-names="onAiExpand">
<n-collapse-item name="ai-summary">
<template #header>
<div class="flex items-center gap-2.5">
<div
class="flex h-8 w-8 items-center justify-center rounded-xl bg-gradient-to-br from-[#ec66ab] to-[#c98a5d] text-white shadow-[0_6px_14px_rgba(236,102,171,0.28)]">
<n-icon :class="aiState === 'loading' || aiState === 'streaming' ? 'animate-spin' : ''">
<icon-star class="h-4 w-4" />
</n-icon>
</div>
<div class="text-[20px] font-bold text-[#2f4050]">AI 摘要</div>
</div>
</template>
<template #header-extra>
<div class="flex items-center gap-2 text-[0.78rem] text-[#8a98a1]">
<span v-if="aiState === 'loading'">正在思考</span>
<span v-else-if="aiState === 'streaming'">生成中</span>
<span v-else-if="aiState === 'done'">已完成</span>
<span v-else-if="aiState === 'error'">生成失败</span>
<span v-else>点击展开生成</span>
</div>
</template>
<div class="whitespace-pre-line pt-1 text-[#66778b]">
<!-- 加载中 -->
<div v-if="aiState === 'loading'" class="flex items-center gap-3 py-2 text-[#8a98a1]">
<n-spin :size="18" />
<span class="text-sm">AI 正在阅读文章请稍候</span>
</div>
<!-- 流式输出 / 已完成 -->
<div v-else-if="aiState === 'streaming' || (aiState === 'done' && aiMsg)"
class="indent-[2em] leading-7">
{{ aiMsg }}<span v-if="aiState === 'streaming'" class="ai-cursor"></span>
</div>
<!-- 生成失败 -->
<div v-else-if="aiState === 'error'"
class="flex flex-col items-start gap-3 rounded-2xl bg-[#fff5f5] px-4 py-3 text-sm text-[#c96a6a]">
<span>摘要生成失败{{ aiError || '未知错误' }}</span>
<n-button @click="triggerAi"
class="cursor-pointer rounded-full bg-primary px-4 py-1 text-xs text-white transition-opacity hover:opacity-85">
重新生成
</n-button>
</div>
<!-- 初始提示 -->
<div v-else class="py-2 text-sm text-[#8a98a1]">
点击上方标题展开AI 将为你生成这篇文章的摘要
</div>
<!-- 完成后重新生成 -->
<div v-if="aiState === 'done' && aiMsg" class="mt-3 flex justify-end">
<n-button @click="triggerAi"
class="cursor-pointer rounded-full bg-white/60 px-4 py-1 text-xs text-[#7a8797] shadow-[0_8px_20px_rgba(88,108,125,0.1)] transition-all hover:text-primary">
重新生成
</n-button>
</div>
</div>
</n-collapse-item>
</n-collapse>
</section>
<section class="blog-detail-card px-5 py-6 sm:px-8 sm:py-8">
<div
class="pointer-events-none absolute inset-x-10 top-0 h-[3px] rounded-b-full bg-gradient-to-r from-[#ec66ab] via-[#c98a5d] to-[#76b0d6] opacity-60">
</div>
<MdPreview class="relative article-preview !bg-transparent" :modelValue="markdown"
:onHtmlChanged="scheduleSyncHeadings" />
</section>
</div>
</n-scrollbar>
<button v-show="showBackToTop" type="button" aria-label="回到顶部"
class="absolute bottom-6 right-6 z-[3] flex h-11 w-11 items-center justify-center rounded-full outline-none border-none bg-white/24 text-primary shadow-[0_16px_28px_rgba(109,126,117,0.14),inset_0_1px_0_rgba(255,255,255,0.24)] backdrop-blur-[16px] transition-all duration-300 hover:-translate-y-1 hover:bg-white/36 hover:shadow-[0_20px_36px_rgba(109,126,117,0.22),inset_0_1px_0_rgba(255,255,255,0.32)]"
@click="scrollToTop">
<icon-backtop class="h-5 w-5" />
</button>
</template>
</section>
</div>
</div>
@ -229,6 +332,7 @@ const detailScrollbar = ref<DetailScrollbarInst | null>(null)
const scrollElement = ref<HTMLElement | undefined>(undefined)
const activeHeadingId = ref('')
const showBackToTop = ref(false)
const progress = ref(0)
const headingElementMap = shallowRef(new Map<string, HTMLElement>())
const route = useRoute()
const nav = $store.nav.useNavStore() as { navH: number }
@ -236,8 +340,6 @@ const nav = $store.nav.useNavStore() as { navH: number }
const boxStyle = ref<Record<string, string>>({})
const contentStyle = ref<Record<string, string>>({})
const tags = ref<string[]>([])
const msg = ref('展开后可生成摘要...')
const aimask = ref(false)
const markdown = ref('')
const blogData = ref<BlogDetail>({
aid: 'preview-only',
@ -248,6 +350,18 @@ const blogData = ref<BlogDetail>({
cont: '',
})
/** 页面加载状态 */
const loading = ref(true)
const loadError = ref('')
/** AI 摘要状态机 */
type AiState = 'idle' | 'loading' | 'streaming' | 'done' | 'error'
const aiState = ref<AiState>('idle')
const aiExpanded = ref(false)
const aiMsg = ref('')
const aiError = ref('')
let aiAbortController: AbortController | null = null
function getTagColor(tag: string) {
const normalized = tag.trim()
if (!normalized) return SOFT_TAG_PALETTE[0]
@ -260,16 +374,41 @@ function getTagColor(tag: string) {
return SOFT_TAG_PALETTE[Math.abs(hash) % SOFT_TAG_PALETTE.length]
}
/** 文章字数与预估阅读时间(按 300 字/分钟估算) */
const stats = computed(() => {
const text = markdown.value
.replace(/```[\s\S]*?```/g, ' ')
.replace(/!\[[^\]]*\]\([^)]*\)/g, ' ')
.replace(/\[[^\]]*\]\([^)]*\)/g, ' ')
.replace(/[#>*`~\-_|]/g, ' ')
.trim()
const chars = text.replace(/\s+/g, '').length
const minutes = Math.max(1, Math.ceil(chars / 300))
return { chars, minutes }
})
const tocItems = computed<TocItem[]>(() => {
const lines = (blogData.value.cont || '').split(/\r?\n/)
const flatItems: TocChildItem[] = []
let inFence = false
lines.forEach((line) => {
const trimmed = line.trim()
// 跳过围栏代码块,避免其中的 # 行被误解析为标题
if (/^(`{3,}|~{3,})/.test(trimmed)) {
inFence = !inFence
return
}
if (inFence) return
// 跳过引用块内的伪标题
if (/^>/.test(trimmed)) return
const match = line.match(/^(#{1,6})\s+(.+?)\s*$/)
if (!match) return
const level = match[1].length
const text = match[2].replace(/[`*_~]/g, '').trim()
// 去除 Markdown 标记与转义反斜杠(如 1\. 引言 → 1. 引言)
const text = match[2].replace(/[`*_~\\]/g, '').trim()
if (!text) return
flatItems.push({
@ -329,10 +468,37 @@ function getPreviewRoot() {
}
async function syncScrollElement() {
await nextTick()
// n-scrollbar 内部容器可能跨多个帧才挂载完成,带重试等待
for (let i = 0; i < 10; i++) {
await nextTick()
const container = getScrollbarContainer()
if (container) {
scrollElement.value = container
bindScrollListener()
scheduleSyncHeadings()
return
}
await new Promise(resolve => requestAnimationFrame(resolve))
}
scrollElement.value = getScrollbarContainer() || undefined
requestAnimationFrame(() => {
syncPreviewHeadings()
}
/** 绑定滚动监听(幂等,避免重复绑定) */
function bindScrollListener() {
const el = scrollElement.value
if (!el || el.dataset.blogScrollBound === '1') return
el.dataset.blogScrollBound = '1'
el.addEventListener('scroll', updateActiveHeading)
}
/** 等待渲染完成后再同步标题映射md-editor 渲染可能跨多个帧) */
function scheduleSyncHeadings() {
nextTick(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
syncPreviewHeadings()
})
})
})
}
@ -343,14 +509,22 @@ function syncPreviewHeadings() {
const headings = previewRoot.querySelectorAll<HTMLElement>('h1, h2, h3, h4, h5, h6')
const flatTocItems = tocItems.value.flatMap(item => [item, ...item.children])
const nextHeadingMap = new Map<string, HTMLElement>()
const used = new Set<number>()
headings.forEach((heading, index) => {
const item = flatTocItems[index]
if (item) {
heading.id = item.id
heading.style.scrollMarginTop = '24px'
nextHeadingMap.set(item.id, heading)
headings.forEach((heading) => {
// 优先按标题文本匹配(渲染结果),其次按顺序兜底,避免解析与渲染不一致导致错位
const headingText = (heading.textContent || '').trim()
let itemIndex = flatTocItems.findIndex((item, i) => !used.has(i) && item.text === headingText)
if (itemIndex === -1) {
itemIndex = flatTocItems.findIndex((item, i) => !used.has(i))
}
if (itemIndex === -1) return
used.add(itemIndex)
const item = flatTocItems[itemIndex]
heading.id = item.id
heading.style.scrollMarginTop = '24px'
nextHeadingMap.set(item.id, heading)
})
headingElementMap.value = nextHeadingMap
@ -360,6 +534,10 @@ function syncPreviewHeadings() {
function updateActiveHeading() {
const container = scrollElement.value
showBackToTop.value = !!container && container.scrollTop > 240
if (container) {
const max = container.scrollHeight - container.clientHeight
progress.value = max > 0 ? Math.min(100, Math.round((container.scrollTop / max) * 100)) : 0
}
const previewRoot = getPreviewRoot()
if (!container || !previewRoot) return
@ -385,33 +563,39 @@ function updateActiveHeading() {
function scrollToHeading(id: string) {
const container = scrollElement.value
const target = headingElementMap.value.get(id) || null
if (!container || !target) return
if (!container) return
// 优先取内存映射,兜底直接按 id 查找 DOM防止映射未就绪
let target = headingElementMap.value.get(id) || null
if (!target) {
target = document.getElementById(id)
}
if (!target) return
const containerRect = container.getBoundingClientRect()
const targetRect = target.getBoundingClientRect()
const nextTop = container.scrollTop + (targetRect.top - containerRect.top) - 24
detailScrollbar.value?.scrollTo?.({
top: Math.max(0, nextTop),
behavior: 'smooth',
})
smoothScrollTo(container, Math.max(0, nextTop))
activeHeadingId.value = id
}
function scrollToTop() {
detailScrollbar.value?.scrollTo?.({
top: 0,
behavior: 'smooth',
})
const container = scrollElement.value
if (container) smoothScrollTo(container, 0)
activeHeadingId.value = tocItems.value[0]?.id || ''
}
/** 平滑滚动(原生实现,兼容不支持 options 的环境) */
function smoothScrollTo(container: HTMLElement, top: number) {
try {
container.scrollTo({ top, behavior: 'smooth' })
} catch {
container.scrollTop = top
}
}
watch([markdown, tocItems], async () => {
await nextTick()
requestAnimationFrame(() => {
syncPreviewHeadings()
})
scheduleSyncHeadings()
})
function parseTags(str: string) {
@ -421,58 +605,130 @@ function parseTags(str: string) {
async function getBlogDetail() {
const bid = (route.params as RouteParams).bid || '0'
const res = await $http.blog.getBlogDetail(bid) as BlogDetailResponse
blogData.value = res.data
tags.value = parseTags(res.data.tags)
markdown.value = res.data.cont
await syncScrollElement()
try {
loading.value = true
loadError.value = ''
const res = await $http.blog.getBlogDetail(bid) as BlogDetailResponse
blogData.value = res.data
tags.value = parseTags(res.data.tags)
markdown.value = res.data.cont
document.title = res.data.title ? `${res.data.title} - 文章详情` : '文章详情'
// 先结束 loading让 v-else 分支的 n-scrollbar 真正挂载,再同步滚动容器
loading.value = false
await syncScrollElement()
} catch (err) {
loadError.value = (err as Error)?.message || '网络异常,请稍后重试'
window.$msg?.error('文章加载失败')
loading.value = false
}
}
async function AISum() {
if (aimask.value || !blogData.value.cont) return
function reload() {
loadError.value = ''
getBlogDetail()
}
const response = await fetch('https://api.siliconflow.cn/v1/chat/completions', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer sk-jwwmhmxsjtseyekknqmamlvzmrkmwfvuacnssbwfufogrkdg'
},
body: JSON.stringify({
model: 'Qwen/Qwen3-8B',
messages: [
{
role: 'system',
content: '你是一个摘要生成工具请用中文总结我发送给你的文章内容不要换行不要超过300字只介绍文章内容语气需要俏皮一些不需要提出建议。开头固定为“这篇文章介绍了”。'
},
{ role: 'user', content: blogData.value.cont }
],
stream: true
})
})
const reader = response.body?.getReader()
const decoder = new TextDecoder('utf-8')
if (!reader) return
msg.value = ''
let done = false
while (!done) {
const { value, done: doneReading } = await reader.read()
done = doneReading
const chunk = decoder.decode(value ?? new Uint8Array())
const match = chunk.match(/"content":"(.*?)"/)
if (match?.[1]) {
msg.value += match[1]
aimask.value = true
async function copyLink() {
const url = window.location.href
try {
await navigator.clipboard.writeText(url)
window.$msg?.success('文章链接已复制')
} catch {
// 兼容非安全上下文http
try {
const textarea = document.createElement('textarea')
textarea.value = url
textarea.style.position = 'fixed'
textarea.style.opacity = '0'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
window.$msg?.success('文章链接已复制')
} catch {
window.$msg?.warning('复制失败,请手动复制地址栏链接')
}
}
}
function onAiExpand(names: string | string[] | null) {
const expanded = Array.isArray(names) ? names.includes('ai-summary') : names === 'ai-summary'
aiExpanded.value = expanded
if (expanded) triggerAi()
}
async function triggerAi() {
if (!blogData.value.cont) {
aiState.value = 'error'
aiError.value = '文章内容为空,无法生成摘要'
return
}
if (aiState.value === 'loading' || aiState.value === 'streaming') return
aiState.value = 'loading'
aiMsg.value = ''
aiError.value = ''
aiAbortController?.abort()
aiAbortController = new AbortController()
try {
const bid = (route.params as RouteParams).bid || '0'
const baseURL = 'https://www.hxyouzi.com'
const response = await fetch(`${baseURL}/api/art/ai/${bid}`, {
method: 'POST',
signal: aiAbortController.signal,
})
if (!response.ok) throw new Error(`请求失败HTTP ${response.status}`)
const reader = response.body?.getReader()
if (!reader) throw new Error('当前浏览器不支持流式读取')
const decoder = new TextDecoder('utf-8')
let buffer = ''
aiState.value = 'streaming'
while (true) {
const { value, done } = await reader.read()
if (done) break
// 累积解码,避免 SSE 事件被 chunk 边界截断
buffer += decoder.decode(value ?? new Uint8Array(), { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() ?? ''
lines.forEach(line => handleSseLine(line.trim()))
}
if (buffer.trim()) handleSseLine(buffer.trim())
if (!aiMsg.value) {
aiState.value = 'error'
aiError.value = '未获取到摘要内容,请重试'
} else {
aiState.value = 'done'
}
} catch (err) {
if ((err as Error)?.name === 'AbortError') return
aiState.value = 'error'
aiError.value = (err as Error)?.message || '生成失败,请稍后重试'
}
}
function handleSseLine(line: string) {
if (!line.startsWith('data: ')) return
const payload = line.slice(6).trim()
if (!payload || payload === '[DONE]') return
// 服务端错误事件,如 [ERROR]xxx
if (/^\[(error|ERROR)\]/.test(payload)) {
aiState.value = 'error'
aiError.value = payload.replace(/^\[(error|ERROR)\]/, '') || '服务端返回错误'
return
}
aiMsg.value += payload
}
onMounted(async () => {
updateLayout()
await getBlogDetail()
scrollElement.value?.addEventListener('scroll', updateActiveHeading)
window.addEventListener('resize', updateLayout)
// getBlogDetail 内部会在 n-scrollbar 挂载后调用 syncScrollElement 绑定滚动事件
await getBlogDetail()
})
onBeforeUnmount(() => {
@ -482,6 +738,93 @@ onBeforeUnmount(() => {
</script>
<style scoped lang="less">
/* 目录列表:激活指示条与悬停反馈 */
.toc-list {
max-height: 60vh;
overflow-y: auto;
padding-right: 4px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
border-radius: 9999px;
background: rgba(139, 158, 166, 0.35);
}
&::-webkit-scrollbar-track {
background: transparent;
}
}
.toc-leaf,
.toc-child {
position: relative;
padding-left: 10px;
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
width: 3px;
height: 0;
border-radius: 9999px;
background: #ec66ab;
transform: translateY(-50%);
transition: height 0.25s ease;
}
&:hover {
background: rgba(255, 255, 255, 0.5);
}
}
.toc-active {
background: linear-gradient(90deg, rgba(236, 102, 171, 0.08), rgba(236, 102, 171, 0));
box-shadow: inset 3px 0 0 rgba(236, 102, 171, 0.85);
&::before {
height: 14px;
}
}
/* 加载骨架屏 */
.blog-skeleton {
background: linear-gradient(90deg, rgba(210, 220, 225, 0.35) 25%, rgba(255, 255, 255, 0.65) 50%, rgba(210, 220, 225, 0.35) 75%);
background-size: 200% 100%;
animation: blog-skeleton-wave 1.4s ease-in-out infinite;
}
@keyframes blog-skeleton-wave {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* AI 摘要流式输出光标 */
.ai-cursor {
display: inline-block;
width: 2px;
height: 1.1em;
margin-left: 2px;
vertical-align: -2px;
background: #ec66ab;
animation: ai-cursor-blink 0.9s steps(2, start) infinite;
}
@keyframes ai-cursor-blink {
to {
visibility: hidden;
}
}
:deep(.article-preview .md-editor-preview) {
background: transparent !important;
}

View File

@ -341,21 +341,29 @@ function getRandomFromPalette(index = 0): string {
}
async function getBlogList() {
const res = await $http.blog.getBlogList({
category: category.value,
page_size: page_size.value,
page_num: page_num.value,
}) as BlogListResponse
try {
const res = await $http.blog.getBlogList({
category: category.value,
page_size: page_size.value,
page_num: page_num.value,
}) as BlogListResponse
const uniqueTags = Array.from(new Set(res.data.flatMap((item) => getTags(item.tags))))
tagColorList.value = buildTagColorMap(uniqueTags)
blogList.value = res.data
const uniqueTags = Array.from(new Set(res.data.flatMap((item) => getTags(item.tags))))
tagColorList.value = buildTagColorMap(uniqueTags)
blogList.value = res.data
} catch {
blogList.value = []
}
}
async function getCateList() {
const res = await $http.blog.getCateList() as CateListResponse
cateList.value = res.data
total.value = allTotal.value
try {
const res = await $http.blog.getCateList() as CateListResponse
cateList.value = res.data
total.value = allTotal.value
} catch {
cateList.value = []
}
}
onMounted(() => {

File diff suppressed because it is too large Load Diff

View File

@ -13,7 +13,7 @@
"compilerOptions": {
"target": "es2021",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types":["unplugin-vue-router/client"],
"types":["vue-router/auto"],
"paths": {
"@/*": ["./src/*"]
}

173
typed-router.d.ts vendored
View File

@ -1,36 +1,97 @@
/* eslint-disable */
/* prettier-ignore */
// oxfmt-ignore
// @ts-nocheck
// Generated by unplugin-vue-router. ‼️ DO NOT MODIFY THIS FILE ‼️
// noinspection ES6UnusedImports
// Generated by vue-router. !! DO NOT MODIFY THIS FILE !!
// It's recommended to commit this file.
// Make sure to add this file to your tsconfig.json file as an "includes" or "files" entry.
declare module 'vue-router/auto-routes' {
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
import type {
RouteRecordInfo,
ParamValue,
ParamValueOneOrMore,
ParamValueZeroOrMore,
ParamValueZeroOrOne,
} from 'vue-router'
import type {
_ExtractParamParserType,
} from 'vue-router/experimental'
declare module 'vue-router' {
interface TypesConfig {
_ParamParsers: {}
RouteNamedMap: import('vue-router/auto-routes').RouteNamedMap
_RouteFileInfoMap: import('vue-router/auto-routes')._RouteFileInfoMap
}
}
declare module 'vue-router/auto-routes' {
/**
* Route name map generated by unplugin-vue-router
* Route name map generated by vue-router
*/
export interface RouteNamedMap {
'apps': RouteRecordInfo<'apps', '/Apps', Record<never, never>, Record<never, never>>,
'blog': RouteRecordInfo<'blog', '/blog', Record<never, never>, Record<never, never>>,
'blog/:bid': RouteRecordInfo<'blog/:bid', '/blog/:bid', { bid: ParamValue<true> }, { bid: ParamValue<false> }>,
'gallery': RouteRecordInfo<'gallery', '/Gallery', Record<never, never>, Record<never, never>>,
'home': RouteRecordInfo<'home', '/Home', Record<never, never>, Record<never, never>>,
'404': RouteRecordInfo<'404', '/NotFound', Record<never, never>, Record<never, never>>,
'plink': RouteRecordInfo<'plink', '/Plink', Record<never, never>, Record<never, never>>,
'widget': RouteRecordInfo<'widget', '/Widget', Record<never, never>, Record<never, never>>,
'home': RouteRecordInfo<
'home',
'/',
Record<never, never>,
Record<never, never>,
| never
>,
'apps': RouteRecordInfo<
'apps',
'/Apps',
Record<never, never>,
Record<never, never>,
| never
>,
'blog': RouteRecordInfo<
'blog',
'/blog',
Record<never, never>,
Record<never, never>,
| never
>,
'blog/:bid': RouteRecordInfo<
'blog/:bid',
'/blog/:bid',
{ bid: ParamValue<true> },
{ bid: ParamValue<false> },
| never
>,
'gallery': RouteRecordInfo<
'gallery',
'/Gallery',
Record<never, never>,
Record<never, never>,
| never
>,
'404': RouteRecordInfo<
'404',
'/NotFound',
Record<never, never>,
Record<never, never>,
| never
>,
'plink': RouteRecordInfo<
'plink',
'/Plink',
Record<never, never>,
Record<never, never>,
| never
>,
'widget': RouteRecordInfo<
'widget',
'/Widget',
Record<never, never>,
Record<never, never>,
| never
>,
}
/**
* Route file to route info map by unplugin-vue-router.
* Used by the volar plugin to automatically type useRoute()
* Route file to route info map by vue-router.
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
*
* Each key is a file path relative to the project root with 2 properties:
* - routes: union of route names of the possible routes when in this page (passed to useRoute<...>())
@ -39,43 +100,75 @@ declare module 'vue-router/auto-routes' {
* @internal
*/
export interface _RouteFileInfoMap {
'src/views/index.vue': {
routes:
| 'home'
views:
| never
pathParamNames:
| never
}
'src/views/Apps.vue': {
routes: 'apps'
views: never
routes:
| 'apps'
views:
| never
pathParamNames:
| never
}
'src/views/blog/index.vue': {
routes: 'blog'
views: never
routes:
| 'blog'
views:
| never
pathParamNames:
| never
}
'src/views/blog/[bid].vue': {
routes: 'blog/:bid'
views: never
routes:
| 'blog/:bid'
views:
| never
pathParamNames:
| 'bid'
}
'src/views/Gallery.vue': {
routes: 'gallery'
views: never
}
'src/views/Home.vue': {
routes: 'home'
views: never
routes:
| 'gallery'
views:
| never
pathParamNames:
| never
}
'src/views/NotFound.vue': {
routes: '404'
views: never
routes:
| '404'
views:
| never
pathParamNames:
| never
}
'src/views/Plink.vue': {
routes: 'plink'
views: never
routes:
| 'plink'
views:
| never
pathParamNames:
| never
}
'src/views/Widget.vue': {
routes: 'widget'
views: never
routes:
| 'widget'
views:
| never
pathParamNames:
| never
}
}
/**
* Get a union of possible route names in a certain route component file.
* Used by the volar plugin to automatically type useRoute()
* Used by the \`sfc-typed-router\` Volar plugin to automatically type \`useRoute()\`.
*
* @internal
*/
@ -84,3 +177,5 @@ declare module 'vue-router/auto-routes' {
? Info['routes']
: keyof RouteNamedMap
}
export {}

View File

@ -5,9 +5,8 @@ import UnoCSS from "unocss/vite";
import AutoImport from "unplugin-auto-import/vite";
import { NaiveUiResolver } from "unplugin-vue-components/resolvers";
import Components from "unplugin-vue-components/vite";
import { VueRouterAutoImports } from "unplugin-vue-router";
import VueRouter from "unplugin-vue-router/vite";
import { defineConfig } from "vite";
import VueRouter from "vue-router/vite";
// import vueDevTools from "vite-plugin-vue-devtools";
import svgLoader from "vite-svg-loader";
// https://vite.dev/config/
@ -23,7 +22,7 @@ export default defineConfig({
imports: [
"vue",
"pinia",
VueRouterAutoImports,
"vue-router",
{
"naive-ui": ["useDialog", "useMessage", "useNotification", "useLoadingBar"],
},

274
yarn.lock
View File

@ -56,6 +56,18 @@
"@jridgewell/trace-mapping" "^0.3.28"
jsesc "^3.0.2"
"@babel/generator@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-8.0.0.tgz#24b7b53a74fa8e74cc2377e054fecef4dcdada96"
integrity sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==
dependencies:
"@babel/parser" "^8.0.0"
"@babel/types" "^8.0.0"
"@jridgewell/gen-mapping" "^0.3.12"
"@jridgewell/trace-mapping" "^0.3.28"
"@types/jsesc" "^2.5.0"
jsesc "^3.0.2"
"@babel/helper-annotate-as-pure@^7.27.3":
version "7.27.3"
resolved "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz#f31fd86b915fc4daf1f3ac6976c59be7084ed9c5"
@ -158,11 +170,31 @@
resolved "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687"
integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
"@babel/helper-string-parser@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz#7f0871d99824d23137d60f86fcf6130fd5a1b51f"
integrity sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==
"@babel/helper-string-parser@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-8.0.0.tgz#4d47d9ad35d0f5bd7d202b348bf558328a347977"
integrity sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==
"@babel/helper-validator-identifier@^7.27.1", "@babel/helper-validator-identifier@^7.28.5":
version "7.28.5"
resolved "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4"
integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==
"@babel/helper-validator-identifier@^7.29.7":
version "7.29.7"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz#bd87084ced0c796ec46bda492de6e83d29e89fc2"
integrity sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==
"@babel/helper-validator-identifier@^8.0.4":
version "8.0.4"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.4.tgz#60e427a9be7a101af52f14588def2dad8a9f9881"
integrity sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg==
"@babel/helper-validator-option@^7.27.1":
version "7.27.1"
resolved "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f"
@ -190,6 +222,20 @@
dependencies:
"@babel/types" "^7.28.5"
"@babel/parser@^7.29.2", "@babel/parser@^7.29.7":
version "7.29.8"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.8.tgz#9653716a2f10c677b98fbc63d4bfb000c302cf17"
integrity sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==
dependencies:
"@babel/types" "^7.29.8"
"@babel/parser@^8.0.0":
version "8.0.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-8.0.4.tgz#244e21ca23ab54c6f5373af2122cb3a618f9dd39"
integrity sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==
dependencies:
"@babel/types" "^8.0.4"
"@babel/plugin-proposal-decorators@^7.23.0":
version "7.28.0"
resolved "https://registry.npmmirror.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.28.0.tgz#419c8acc31088e05a774344c021800f7ddc39bf0"
@ -288,6 +334,22 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.28.5"
"@babel/types@^7.29.0", "@babel/types@^7.29.8":
version "7.29.8"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.8.tgz#1229eef31d85156d70fa3f4cd859376d0eaf6863"
integrity sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==
dependencies:
"@babel/helper-string-parser" "^7.29.7"
"@babel/helper-validator-identifier" "^7.29.7"
"@babel/types@^8.0.0", "@babel/types@^8.0.4":
version "8.0.4"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-8.0.4.tgz#8c208701320c9f0323b5b6a233d093e52c1e3b41"
integrity sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==
dependencies:
"@babel/helper-string-parser" "^8.0.0"
"@babel/helper-validator-identifier" "^8.0.4"
"@braintree/sanitize-url@^6.0.0":
version "6.0.4"
resolved "https://registry.npmmirror.com/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz#923ca57e173c6b232bbbb07347b1be982f03e783"
@ -1175,6 +1237,11 @@
resolved "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz#958b91c991b1867ced318bedea0e215ee050726e"
integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
"@types/jsesc@^2.5.0":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@types/jsesc/-/jsesc-2.5.1.tgz#c34defc608ec94b68dc6a12a581b440942c6d503"
integrity sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==
"@types/katex@^0.16.2":
version "0.16.7"
resolved "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz#03ab680ab4fa4fbc6cb46ecf987ecad5d8019868"
@ -1492,6 +1559,17 @@
magic-string-ast "^1.0.0"
unplugin-utils "^0.2.4"
"@vue-macros/common@^3.1.3":
version "3.1.4"
resolved "https://registry.yarnpkg.com/@vue-macros/common/-/common-3.1.4.tgz#cb56f0a84bdbfacbfa640ac34d9f805667adac8c"
integrity sha512-/5Fv+6DgIcM9ajY05ZmKBv+LMX1M9A0X+IUwDRVdt67ciw8OV9bvG2r34p3RiEadlsQybjhKPRKNXDC8Bp23cw==
dependencies:
"@vue/compiler-sfc" "^3.5.22"
ast-kit "^2.1.2"
local-pkg "^1.1.2"
magic-string-ast "^1.0.2"
unplugin-utils "^0.3.0"
"@vue/babel-helper-vue-transform-on@1.5.0":
version "1.5.0"
resolved "https://registry.npmmirror.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.5.0.tgz#b7e99d37eeb144d7b9757d7a1f40cd977fde748a"
@ -1534,6 +1612,17 @@
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-core@3.5.40":
version "3.5.40"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.40.tgz#a3e9e280ef3dccb6de4c7707ba50d7e10fd598a5"
integrity sha512-39E8IgOhTbVDnoJFMKc2DvYnypcZwUqgUhQkccva/0m6FUwtIKSGV7n1hpVmYcFaoRAwf9pBcwnKlCEsN63ZEQ==
dependencies:
"@babel/parser" "^7.29.7"
"@vue/shared" "3.5.40"
entities "^7.0.1"
estree-walker "^2.0.2"
source-map-js "^1.2.1"
"@vue/compiler-core@3.6.0-beta.1":
version "3.6.0-beta.1"
resolved "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.6.0-beta.1.tgz#9a19a73fcb1ea8a4add071af2a3308514a7baebb"
@ -1553,6 +1642,14 @@
"@vue/compiler-core" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/compiler-dom@3.5.40":
version "3.5.40"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.40.tgz#ac0579d4dc257bf5e10ce324a7ce0a7f9fc5c338"
integrity sha512-pwkx4vqlqOspFstrcmzwkKLePVMD3PT65imRzLhanU2V1Fj4K13g6OXjanOyzw3aTAuRk84BOmY8f3rEHqPaVA==
dependencies:
"@vue/compiler-core" "3.5.40"
"@vue/shared" "3.5.40"
"@vue/compiler-dom@3.6.0-beta.1":
version "3.6.0-beta.1"
resolved "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.6.0-beta.1.tgz#0c45d0faf7949916953d511f15a382bf893025d0"
@ -1592,6 +1689,21 @@
postcss "^8.5.6"
source-map-js "^1.2.1"
"@vue/compiler-sfc@^3.5.22":
version "3.5.40"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.40.tgz#22252b2e5a58a6b6a4f71565cfa91271bddbe782"
integrity sha512-gIf497P4kpuALcvs5n3AEg1Vdn0pSY4XbjASIfHNYF1/MP3T2Mf2STERTubysBxCRxzJGJYtF/O7vwJrxFB3Vw==
dependencies:
"@babel/parser" "^7.29.7"
"@vue/compiler-core" "3.5.40"
"@vue/compiler-dom" "3.5.40"
"@vue/compiler-ssr" "3.5.40"
"@vue/shared" "3.5.40"
estree-walker "^2.0.2"
magic-string "^0.30.21"
postcss "^8.5.19"
source-map-js "^1.2.1"
"@vue/compiler-ssr@3.5.26":
version "3.5.26"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.26.tgz#f6e94bccbb5339180779036ddfb614f998a197ea"
@ -1600,6 +1712,14 @@
"@vue/compiler-dom" "3.5.26"
"@vue/shared" "3.5.26"
"@vue/compiler-ssr@3.5.40":
version "3.5.40"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.40.tgz#c8c80efd22f5d85ea775d6ead787a108dc86cd58"
integrity sha512-rrE5xiXG663+vHCHa3J9p2z5OcBRjXmoqenprJxAFQxg5pSshzeBiCE6pu46axapRJ2Adk0YDA2BRZVjiHXnhg==
dependencies:
"@vue/compiler-dom" "3.5.40"
"@vue/shared" "3.5.40"
"@vue/compiler-ssr@3.6.0-beta.1":
version "3.6.0-beta.1"
resolved "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.6.0-beta.1.tgz#37db007c64697864c70d183e956c908a474c0d2b"
@ -1631,6 +1751,13 @@
dependencies:
"@vue/devtools-kit" "^7.7.9"
"@vue/devtools-api@^8.1.5":
version "8.2.1"
resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-8.2.1.tgz#9d95de2b908aa80b9957d737ddca51790e70c3b5"
integrity sha512-6u4vXBlIBAC1wMplIZgpyPn7uh/s4Bf6F5bMzvLv+EdJ0aHs/+4B7Ygv864EStQSjRbsRzTko/kUG1A1IejQ3A==
dependencies:
"@vue/devtools-kit" "^8.2.1"
"@vue/devtools-core@^8.0.5":
version "8.0.5"
resolved "https://registry.npmmirror.com/@vue/devtools-core/-/devtools-core-8.0.5.tgz#ef3803cb22a5ab6e55728adfd564668418b670d9"
@ -1669,6 +1796,16 @@
speakingurl "^14.0.1"
superjson "^2.2.2"
"@vue/devtools-kit@^8.2.1":
version "8.2.1"
resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-8.2.1.tgz#ad45babb12c51931d32d1b67ffaebc251f550ce9"
integrity sha512-FIGIuq3AWReEpbAHY/cRGeHDfI0qOb8OCQ3YjbEAX04uaxIDbGc9rhkbVcG7rnfHPXE3RsU5KrWOu9V/okd8AQ==
dependencies:
"@vue/devtools-shared" "^8.2.1"
birpc "^2.6.1"
hookable "^5.5.3"
perfect-debounce "^2.0.0"
"@vue/devtools-shared@^7.7.9":
version "7.7.9"
resolved "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.9.tgz#fa4c096b744927081a7dda5fcf05f34b1ae6ca14"
@ -1683,6 +1820,11 @@
dependencies:
rfdc "^1.4.1"
"@vue/devtools-shared@^8.2.1":
version "8.2.1"
resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-8.2.1.tgz#26524e9e12fd205bcd5477cf3b5e9a17b876aeac"
integrity sha512-Fkac7lUdGReh6pVOi3AYPRGe82LQqRmAfThW7RRligOAP0ZA/Z1z9XLHDM9dv34pV2HRc79DK8uKPeG2fLnA/g==
"@vue/language-core@3.2.1", "@vue/language-core@^3.0.1":
version "3.2.1"
resolved "https://registry.npmmirror.com/@vue/language-core/-/language-core-3.2.1.tgz#fe70cb54e76d1ac0a8e10ed2b695dd333022a75e"
@ -1775,6 +1917,11 @@
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.26.tgz#1e02ef2d64aced818cd31d81ce5175711dc90a9f"
integrity sha512-7Z6/y3uFI5PRoKeorTOSXKcDj0MSasfNNltcslbFrPpcw6aXRUALq4IfJlaTRspiWIUOEZbrpM+iQGmCOiWe4A==
"@vue/shared@3.5.40":
version "3.5.40"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.40.tgz#fc09d1871d66cd9b01959a597b41d7cb61f716a8"
integrity sha512-WxnBtruIqOoV3rA4jeKDWzrYI5h7Cp4+pjwDi8kWGHz+IslhiN+wguLVVhtv2l8VoU02rzDCVfDjgCl1lNpZVg==
"@vue/shared@3.6.0-beta.1":
version "3.6.0-beta.1"
resolved "https://registry.npmmirror.com/@vue/shared/-/shared-3.6.0-beta.1.tgz#e4e13a2d79cdb65a98a84628c11e39b84ae1c4ec"
@ -1817,6 +1964,11 @@ acorn@^8.14.1, acorn@^8.15.0:
resolved "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816"
integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
acorn@^8.16.0:
version "8.18.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.18.0.tgz#4faf01b2d6d326bfeed97aea1f52220b5f4c1940"
integrity sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==
alien-signals@^3.0.0:
version "3.1.2"
resolved "https://registry.npmmirror.com/alien-signals/-/alien-signals-3.1.2.tgz#26e623e3ed81e401df1a7c503f726e2288a4fa02"
@ -1854,7 +2006,7 @@ argparse@^2.0.1:
resolved "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
ast-kit@^2.1.1, ast-kit@^2.1.3:
ast-kit@^2.1.1, ast-kit@^2.1.2, ast-kit@^2.1.3, ast-kit@^2.2.0:
version "2.2.0"
resolved "https://registry.npmmirror.com/ast-kit/-/ast-kit-2.2.0.tgz#6d9a298acefef5bdfc5a0fa51d94d1334ef2e671"
integrity sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==
@ -1870,6 +2022,15 @@ ast-walker-scope@^0.8.1:
"@babel/parser" "^7.28.4"
ast-kit "^2.1.3"
ast-walker-scope@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/ast-walker-scope/-/ast-walker-scope-0.9.0.tgz#3bdcba80c488dd4c7fc1de055d645a0f95dff8fe"
integrity sha512-IJdzo2vLiElBxKzwS36VsCue/62d6IdWjnPB2v3nuPKeWGynp6FF/CYoLa5i/3jXH/z97ZDdsXz6abpgM6w07A==
dependencies:
"@babel/parser" "^7.29.2"
"@babel/types" "^7.29.0"
ast-kit "^2.2.0"
async-validator@^4.0.7, async-validator@^4.2.5:
version "4.2.5"
resolved "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz#c96ea3332a521699d0afaaceed510a54656c6339"
@ -2827,6 +2988,11 @@ entities@^7.0.0:
resolved "https://registry.npmmirror.com/entities/-/entities-7.0.0.tgz#2ae4e443f3f17d152d3f5b0f79b932c1e59deb7a"
integrity sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==
entities@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b"
integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==
entities@~2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5"
@ -3284,6 +3450,15 @@ local-pkg@^1.1.1:
pkg-types "^2.3.0"
quansync "^0.2.11"
local-pkg@^1.1.2, local-pkg@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-1.2.1.tgz#9628389399851d78f3b50c9236eddb02f0c31b2b"
integrity sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==
dependencies:
mlly "^1.7.4"
pkg-types "^2.3.0"
quansync "^0.2.11"
lodash-es@^4.17.20, lodash-es@^4.17.21:
version "4.17.22"
resolved "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.22.tgz#eb7d123ec2470d69b911abe34f85cb694849b346"
@ -3316,7 +3491,7 @@ lxgw-wenkai-webfont@^1.7.0:
resolved "https://registry.npmmirror.com/lxgw-wenkai-webfont/-/lxgw-wenkai-webfont-1.7.0.tgz#72ef4ef9d00fd185a2ed8898d1a60f73ab732f97"
integrity sha512-kVJmT4xMAyOLhG6q0w63wJkVU7RP8UMQRGoAWj9tnpzL7sWFFYiEHBdUQyW9myqBQFa5SQfWfa2VLYF4IJBL5g==
magic-string-ast@^1.0.0:
magic-string-ast@^1.0.0, magic-string-ast@^1.0.2:
version "1.0.3"
resolved "https://registry.npmmirror.com/magic-string-ast/-/magic-string-ast-1.0.3.tgz#51ef7832fd5c70a0188fb94627caa3b8c74ff9bf"
integrity sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==
@ -3505,6 +3680,16 @@ mlly@^1.7.4, mlly@^1.8.0:
pkg-types "^1.3.1"
ufo "^1.6.1"
mlly@^1.8.2:
version "1.8.2"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.8.2.tgz#e7f7919a82d13b174405613117249a3f449d78bb"
integrity sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==
dependencies:
acorn "^8.16.0"
pathe "^2.0.3"
pkg-types "^1.3.1"
ufo "^1.6.3"
moment-mini@^2.24.0:
version "2.29.4"
resolved "https://registry.npmmirror.com/moment-mini/-/moment-mini-2.29.4.tgz#cbbcdc58ce1b267506f28ea6668dbe060a32758f"
@ -3560,6 +3745,11 @@ nanoid@^3.3.11:
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
nanoid@^3.3.16:
version "3.3.17"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.17.tgz#f1c3aa253c52547956a52c50bff754316f61037a"
integrity sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==
nanoid@^5.1.5:
version "5.1.6"
resolved "https://registry.npmmirror.com/nanoid/-/nanoid-5.1.6.tgz#30363f664797e7d40429f6c16946d6bd7a3f26c9"
@ -3595,6 +3785,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
nostics@^1.1.4:
version "1.2.0"
resolved "https://registry.yarnpkg.com/nostics/-/nostics-1.2.0.tgz#1362df6de2ca8456b521914501abf6c52c4d7fb5"
integrity sha512-FGqEfhQjrvo1lL8KFifdTQiNwwQHJxC1jtYE1Rc54qF/jxONUNL+kC9gS1krX8Q65PgrQ5fCqH/I4NhWBvdSqg==
npm-normalize-package-bin@^4.0.0:
version "4.0.0"
resolved "https://registry.npmmirror.com/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz#df79e70cd0a113b77c02d1fe243c96b8e618acb1"
@ -3700,6 +3895,11 @@ picomatch@^4.0.2, picomatch@^4.0.3:
resolved "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
picomatch@^4.0.4, picomatch@^4.0.5:
version "4.0.5"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab"
integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==
pidtree@^0.6.0:
version "0.6.0"
resolved "https://registry.npmmirror.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c"
@ -3735,6 +3935,15 @@ pkg-types@^2.1.0, pkg-types@^2.3.0:
exsolve "^1.0.7"
pathe "^2.0.3"
postcss@^8.5.19:
version "8.5.25"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.25.tgz#5012a598eaaa897f21bbe8553be3cb7bd2bd78cb"
integrity sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==
dependencies:
nanoid "^3.3.16"
picocolors "^1.1.1"
source-map-js "^1.2.1"
postcss@^8.5.6:
version "8.5.6"
resolved "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz#2825006615a619b4f62a9e7426cc120b349a8f3c"
@ -3998,6 +4207,14 @@ tinyglobby@^0.2.12, tinyglobby@^0.2.14, tinyglobby@^0.2.15:
fdir "^6.5.0"
picomatch "^4.0.3"
tinyglobby@^0.2.17:
version "0.2.17"
resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631"
integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==
dependencies:
fdir "^6.5.0"
picomatch "^4.0.4"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
@ -4045,6 +4262,11 @@ ufo@^1.6.1:
resolved "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b"
integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==
ufo@^1.6.3:
version "1.6.4"
resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.4.tgz#7a8fb875fcc6382d2c7d0b3692738b0500a92467"
integrity sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==
unconfig-core@7.4.2:
version "7.4.2"
resolved "https://registry.npmmirror.com/unconfig-core/-/unconfig-core-7.4.2.tgz#20b676d6b57a04bade4b96da65c783d25e8b286c"
@ -4147,6 +4369,14 @@ unplugin-utils@^0.3.0, unplugin-utils@^0.3.1:
pathe "^2.0.3"
picomatch "^4.0.3"
unplugin-utils@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/unplugin-utils/-/unplugin-utils-0.3.2.tgz#22f6856408880b0b2d7dd974084faf94094613ef"
integrity sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==
dependencies:
pathe "^2.0.3"
picomatch "^4.0.4"
unplugin-vue-components@^28.8.0:
version "28.8.0"
resolved "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-28.8.0.tgz#3ccabfee1f4d2bdec38ef76634b6924001a5b671"
@ -4193,6 +4423,15 @@ unplugin@^2.2.2, unplugin@^2.3.4, unplugin@^2.3.5:
picomatch "^4.0.3"
webpack-virtual-modules "^0.6.2"
unplugin@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-3.3.0.tgz#18de476b6130a6cde94db87f70ac32b97757f3c9"
integrity sha512-qa66K+crbfyE6JK10GjvbJeRrOsuC/JpbnHctfyp/i4oBTxWOzJfRZyDiOk1PtErMFRu8JhsU/wPvOdBNWe5Rg==
dependencies:
"@jridgewell/remapping" "^2.3.5"
picomatch "^4.0.4"
webpack-virtual-modules "^0.6.2"
update-browserslist-db@^1.2.0:
version "1.2.3"
resolved "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz#64d76db58713136acbeb4c49114366cc6cc2e80d"
@ -4353,13 +4592,37 @@ vue-flow-layout@^0.2.0:
resolved "https://registry.npmmirror.com/vue-flow-layout/-/vue-flow-layout-0.2.0.tgz#3ea4e388a213f23d4653b45255b75b760701cd23"
integrity sha512-zKgsWWkXq0xrus7H4Mc+uFs1ESrmdTXlO0YNbR6wMdPaFvosL3fMB8N7uTV308UhGy9UvTrGhIY7mVz9eN+L0Q==
vue-router@^4.0.3, vue-router@^4.5.1:
vue-router@^4.0.3:
version "4.6.4"
resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.6.4.tgz#a0a9cb9ef811a106d249e4bb9313d286718020d8"
integrity sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==
dependencies:
"@vue/devtools-api" "^6.6.4"
vue-router@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-5.2.0.tgz#34f03b1012729c33bb23184d96ad8e1b1b5c4546"
integrity sha512-QAC5i0LEb1GLG0LXDQmHu8L7FX12j0KwU/JTKmLQUJMrn04gQdKP6Du+p0QwpHb3iy71vBlqnHQ8WAfOSAWhqw==
dependencies:
"@babel/generator" "^8.0.0"
"@vue-macros/common" "^3.1.3"
"@vue/devtools-api" "^8.1.5"
ast-walker-scope "^0.9.0"
chokidar "^5.0.0"
json5 "^2.2.3"
local-pkg "^1.2.1"
magic-string "^0.30.21"
mlly "^1.8.2"
muggle-string "^0.4.1"
nostics "^1.1.4"
pathe "^2.0.3"
picomatch "^4.0.5"
scule "^1.3.0"
tinyglobby "^0.2.17"
unplugin "^3.3.0"
unplugin-utils "^0.3.2"
yaml "^2.9.0"
vue-tsc@^3.0.4:
version "3.2.1"
resolved "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-3.2.1.tgz#c11785765dd8ba08213e77cdc8be69e4a06b115c"
@ -4460,6 +4723,11 @@ yaml@^2.8.0:
resolved "https://registry.npmmirror.com/yaml/-/yaml-2.8.2.tgz#5694f25eca0ce9c3e7a9d9e00ce0ddabbd9e35c5"
integrity sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==
yaml@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.9.0.tgz#78274afd93598a1dfdd6130df6a566defcbf9aa4"
integrity sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==
zrender@5.3.2:
version "5.3.2"
resolved "https://registry.npmmirror.com/zrender/-/zrender-5.3.2.tgz#f67b11d36d3d020d62411d3bb123eb1c93cccd69"