整体优化
This commit is contained in:
44
AGENTS.md
Normal file
44
AGENTS.md
Normal file
@ -0,0 +1,44 @@
|
||||
# AGENTS.md — 博客前端项目
|
||||
|
||||
Vue 3 + TypeScript + Vite 7 博客前端(后端 API 为 `https://www.hxyouzi.com/api`)。UI:naive-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/` 子目录**不自动注册**,需手动 import(vite-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
1
auto-imports.d.ts
vendored
@ -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
1
components.d.ts
vendored
@ -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
19
env.d.ts
vendored
@ -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 { };
|
||||
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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');
|
||||
// 设置下载链接
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
// 创建一个音乐播放器实例,并挂载到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();
|
||||
})
|
||||
|
||||
} 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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
export const useLogStore = defineStore('log', () => {
|
||||
export const useLogStore = defineStore("log", () => {
|
||||
// 登录状态
|
||||
const isLogin:Ref<boolean,boolean> = ref(false)
|
||||
const isLogin = ref(false);
|
||||
|
||||
function setIsLogin(v:boolean) {
|
||||
isLogin.value = v
|
||||
function setIsLogin(v: boolean) {
|
||||
isLogin.value = v;
|
||||
}
|
||||
|
||||
return { isLogin, setIsLogin }
|
||||
})
|
||||
return { isLogin, setIsLogin };
|
||||
});
|
||||
|
||||
@ -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 };
|
||||
});
|
||||
|
||||
@ -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 会产生 Infinity,Array.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;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="js">
|
||||
<script setup lang="ts">
|
||||
|
||||
definePage({
|
||||
name: 'widget',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
@ -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
173
typed-router.d.ts
vendored
@ -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 {}
|
||||
|
||||
@ -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
274
yarn.lock
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user