diff --git a/JWT.py b/JWT.py index b10db97..f0e57da 100644 --- a/JWT.py +++ b/JWT.py @@ -3,6 +3,8 @@ from model import database import asyncio oauth2_scheme = OAuth2PasswordBearer( + scheme_name='获取 JWT Bearer 令牌', + description='用于获取 JWT Bearer 令牌,需要以表单的形式提交', tokenUrl="/api/token" ) diff --git a/README.md b/README.md index 26dbe70..b8aa88d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ * @Author: 于小丘 海枫 * @Date: 2024-11-29 20:06:02 * @LastEditors: Yuerchu admin@yuxiaoqiu.cn - * @LastEditTime: 2024-11-29 20:28:54 * @FilePath: /Findreve/README.md * @Description: Findreve * @@ -28,7 +27,10 @@

## 介绍 Introduction -Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,并确保丢失后能够安全找回。每个物品都会被分配一个 `唯一 ID` ,并生成一个 `安全链接` ,可轻松嵌入到 `二维码` 或 `NFC 标签` 中。当扫描该代码时,会将拾得者引导至一个专门的网页,上面显示物品详情和您的联系信息,既保障隐私又便于沟通。无论您是在管理个人物品还是专业资产,Findreve 都能以高效、简便的方式弥合丢失与找回之间的距离。 +Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,并确保丢失后能够安全找回。 +每个物品都会被分配一个 `唯一 ID` ,并生成一个 `安全链接` ,可轻松嵌入到 `二维码` 或 `NFC 标签` 中。 +当扫描该代码时,会将拾得者引导至一个专门的网页,上面显示物品详情和您的联系信息,既保障隐私又便于沟通。 +无论您是在管理个人物品还是专业资产,Findreve 都能以高效、简便的方式弥合丢失与找回之间的距离。 Findreve is a powerful and intuitive solution, an enhanced version of Findreve, designed to help you manage your personal items and ensure their safe recovery in case of loss. Each @@ -69,4 +71,6 @@ Open Source Free Version: Licensed under the `GPLv3`. 基于赞助的专业版:基于您的赞助,您可获得附加功能和源代码的版本,允许进一步开发用于个人或内部使用。 然而,不允许重新分发修改后的或原始的源代码。 -Donation-Based Premium Version: By making a donation, you can access a version with additional features and source code, which allows further development for personal or internal use. However, redistribution of the modified or original source code is not permitted. \ No newline at end of file +Donation-Based Premium Version: By making a donation, you can access a version with additional features +and source code, which allows further development for personal or internal use. However, redistribution +of the modified or original source code is not permitted. \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..c18b262 --- /dev/null +++ b/app.py @@ -0,0 +1,29 @@ +from fastapi import FastAPI +from contextlib import asynccontextmanager +import model.database + +# 定义程序参数 +APP_NAME: str = 'Findreve' +VERSION: str = '2.0.0' +summary='标记、追踪与找回 —— 就这么简单。' +description='Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,'\ + '并确保丢失后能够安全找回。每个物品都会被分配一个 唯一 ID ,'\ + '并生成一个 安全链接 ,可轻松嵌入到 二维码 或 NFC 标签 中。'\ + '当扫描该代码时,会将拾得者引导至一个专门的网页,上面显示物品详情和您的联系信息,'\ + '既保障隐私又便于沟通。无论您是在管理个人物品还是专业资产,'\ + 'Findreve 都能以高效、简便的方式弥合丢失与找回之间的距离。' + +# Findreve 的生命周期 +@asynccontextmanager +async def lifespan(app: FastAPI): + await model.database.Database().init_db() + yield + +# 定义 Findreve 服务器 +app = FastAPI( + title=APP_NAME, + version=VERSION, + summary=summary, + description=description, + lifespan=lifespan +) \ No newline at end of file diff --git a/frontend/.browserslistrc b/frontend/.browserslistrc new file mode 100644 index 0000000..dc3bc09 --- /dev/null +++ b/frontend/.browserslistrc @@ -0,0 +1,4 @@ +> 1% +last 2 versions +not dead +not ie 11 diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 0000000..7053c49 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,5 @@ +[*.{js,jsx,ts,tsx,vue}] +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/frontend/.github/copilot-instructions.md b/frontend/.github/copilot-instructions.md new file mode 100644 index 0000000..4147695 --- /dev/null +++ b/frontend/.github/copilot-instructions.md @@ -0,0 +1,41 @@ +# Findreve Frontend 项目指南 - GitHub Copilot 指令 + +## 项目概述 +Findreve 是一款强大且直观的解决方案,旨在帮助您管理个人物品,并确保丢失后能够安全找回。每个物品都会被分配一个 `唯一 ID` ,并生成一个 `安全链接` ,可轻松嵌入到 `二维码` 或 `NFC 标签` 中。当扫描该代码时,会将拾得者引导至一个专门的网页,上面显示物品详情和您的联系信息,既保障隐私又便于沟通。无论您是在管理个人物品还是专业资产,Findreve 都能以高效、简便的方式弥合丢失与找回之间的距离。 +而 Findreve Frontend 作为 Findreve 的前端,采用 Vue + Vuetify 3 开发。 + +## 项目规划 +[ ] 追平 Findreve 早期基于 NiceGUI 开发的前端 + +## 代码规范 +- 使用类型提示增强代码可读性 +- 所有函数和类都应有reST风格的文档字符串(docstring) +- 项目的日志模块使用英语输出 +- 使用异步编程模式处理并发 +- 尽可能写出弹性可扩展、可维护的代码 + +## 项目结构 +- `.github/` : Github 相关 +- `public/` : 纯静态文件 +- `src/` +- `.browserslistrc` +- `.editorconfig` +- `.gitignore` +- `README.md` +- `index.html` +- `jsconfig.json` +- `package.json` +- `vite.config.mjs` +- `yarn.lock` + +## 回复用户规则 +- 当用户提出了产品的问题或者解决问题的思路时,应当在适时且随机的时候回答前肯定用户的想法 +- 如 `你的理解非常到位,抓住了问题的核心`、`这个想法非常不错` 等等 +- 每次鼓励尽可能用不同的词语和语法,但也不要次次都鼓励 + +## 命名约定 +- 类名: className +- 函数和变量: getInfo +- 常量: UPPER_SNAKE_CASE +- 文件名: snake_case.vue +- 模块名: snake_case \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..11f5d71 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,22 @@ +.DS_Store +node_modules +/dist + +# local env files +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Editor directories and files +.idea +.vscode +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..f58f676 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,79 @@ +# Vuetify (Default) + +This is the official scaffolding tool for Vuetify, designed to give you a head start in building your new Vuetify application. It sets up a base template with all the necessary configurations and standard directory structure, enabling you to begin development without the hassle of setting up the project from scratch. + +## ❗️ Important Links + +- 📄 [Docs](https://vuetifyjs.com/) +- 🚨 [Issues](https://issues.vuetifyjs.com/) +- 🏬 [Store](https://store.vuetifyjs.com/) +- 🎮 [Playground](https://play.vuetifyjs.com/) +- 💬 [Discord](https://community.vuetifyjs.com) + +## 💿 Install + +Set up your project using your preferred package manager. Use the corresponding command to install the dependencies: + +| Package Manager | Command | +|---------------------------------------------------------------|----------------| +| [yarn](https://yarnpkg.com/getting-started) | `yarn install` | +| [npm](https://docs.npmjs.com/cli/v7/commands/npm-install) | `npm install` | +| [pnpm](https://pnpm.io/installation) | `pnpm install` | +| [bun](https://bun.sh/#getting-started) | `bun install` | + +After completing the installation, your environment is ready for Vuetify development. + +## ✨ Features + +- 🖼️ **Optimized Front-End Stack**: Leverage the latest Vue 3 and Vuetify 3 for a modern, reactive UI development experience. [Vue 3](https://v3.vuejs.org/) | [Vuetify 3](https://vuetifyjs.com/en/) +- 🗃️ **State Management**: Integrated with [Pinia](https://pinia.vuejs.org/), the intuitive, modular state management solution for Vue. +- 🚦 **Routing and Layouts**: Utilizes Vue Router for SPA navigation and vite-plugin-vue-layouts for organizing Vue file layouts. [Vue Router](https://router.vuejs.org/) | [vite-plugin-vue-layouts](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) +- ⚡ **Next-Gen Tooling**: Powered by Vite, experience fast cold starts and instant HMR (Hot Module Replacement). [Vite](https://vitejs.dev/) +- 🧩 **Automated Component Importing**: Streamline your workflow with unplugin-vue-components, automatically importing components as you use them. [unplugin-vue-components](https://github.com/antfu/unplugin-vue-components) + +These features are curated to provide a seamless development experience from setup to deployment, ensuring that your Vuetify application is both powerful and maintainable. + +## 💡 Usage + +This section covers how to start the development server and build your project for production. + +### Starting the Development Server + +To start the development server with hot-reload, run the following command. The server will be accessible at [http://localhost:3000](http://localhost:3000): + +```bash +yarn dev +``` + +(Repeat for npm, pnpm, and bun with respective commands.) + +> Add NODE_OPTIONS='--no-warnings' to suppress the JSON import warnings that happen as part of the Vuetify import mapping. If you are on Node [v21.3.0](https://nodejs.org/en/blog/release/v21.3.0) or higher, you can change this to NODE_OPTIONS='--disable-warning=5401'. If you don't mind the warning, you can remove this from your package.json dev script. + +### Building for Production + +To build your project for production, use: + +```bash +yarn build +``` + +(Repeat for npm, pnpm, and bun with respective commands.) + +Once the build process is completed, your application will be ready for deployment in a production environment. + +## 💪 Support Vuetify Development + +This project is built with [Vuetify](https://vuetifyjs.com/en/), a UI Library with a comprehensive collection of Vue components. Vuetify is an MIT licensed Open Source project that has been made possible due to the generous contributions by our [sponsors and backers](https://vuetifyjs.com/introduction/sponsors-and-backers/). If you are interested in supporting this project, please consider: + +- [Requesting Enterprise Support](https://support.vuetifyjs.com/) +- [Sponsoring John on Github](https://github.com/users/johnleider/sponsorship) +- [Sponsoring Kael on Github](https://github.com/users/kaelwd/sponsorship) +- [Supporting the team on Open Collective](https://opencollective.com/vuetify) +- [Becoming a sponsor on Patreon](https://www.patreon.com/vuetify) +- [Becoming a subscriber on Tidelift](https://tidelift.com/subscription/npm/vuetify) +- [Making a one-time donation with Paypal](https://paypal.me/vuetify) + +## 📑 License +[MIT](http://opensource.org/licenses/MIT) + +Copyright (c) 2016-present Vuetify, LLC diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..0a84a1c --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + Welcome to Vuetify 3 + + +
+ + + diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..dad0634 --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "allowJs": true, + "target": "es5", + "module": "esnext", + "baseUrl": "./", + "moduleResolution": "bundler", + "paths": { + "@/*": [ + "src/*" + ] + }, + "lib": [ + "esnext", + "dom", + "dom.iterable", + "scripthost" + ] + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..75b609c --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "frontend", + "private": true, + "type": "module", + "version": "0.0.0", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@mdi/font": "7.4.47", + "qrcode": "^1.5.3", + "roboto-fontface": "*", + "vue": "^3.5.13", + "vue-router": "4", + "vuetify": "^3.8.1" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.2.3", + "globals": "^16.0.0", + "sass-embedded": "^1.86.3", + "unplugin-fonts": "^1.3.1", + "unplugin-vue-components": "^28.4.1", + "vite": "^6.2.2", + "vite-plugin-vuetify": "^2.1.1" + }, + "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e" +} diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..45077d8 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 0000000..e5373b1 --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,92 @@ + + + + + + + + Findreve + + + + + + + + + + +
+
+ +
+
+
+ +
+ + + + + + + + + diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..ee385eb --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,86 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000..a5f23ae Binary files /dev/null and b/frontend/src/assets/logo.png differ diff --git a/frontend/src/assets/logo.svg b/frontend/src/assets/logo.svg new file mode 100644 index 0000000..d57771c --- /dev/null +++ b/frontend/src/assets/logo.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/src/assets/styles/global.css b/frontend/src/assets/styles/global.css new file mode 100644 index 0000000..d0a0af7 --- /dev/null +++ b/frontend/src/assets/styles/global.css @@ -0,0 +1,23 @@ +/* 全局样式定义 */ +.hover-scale { + transition: transform 0.3s ease; +} + +.hover-scale:hover { + transform: scale(1.1); +} + +.max-width-7xl { + max-width: 1280px; +} + +/* 响应式调整 */ +@media (max-width: 600px) { + .text-h3 { + font-size: 1.75rem !important; + } + + .text-h4 { + font-size: 1.5rem !important; + } +} diff --git a/frontend/src/components/AppFooter.vue b/frontend/src/components/AppFooter.vue new file mode 100644 index 0000000..7444827 --- /dev/null +++ b/frontend/src/components/AppFooter.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/components/CacheStatus.vue b/frontend/src/components/CacheStatus.vue new file mode 100644 index 0000000..3f85db2 --- /dev/null +++ b/frontend/src/components/CacheStatus.vue @@ -0,0 +1,136 @@ + + + diff --git a/frontend/src/components/HelloWorld.vue b/frontend/src/components/HelloWorld.vue new file mode 100644 index 0000000..f74d894 --- /dev/null +++ b/frontend/src/components/HelloWorld.vue @@ -0,0 +1,13 @@ + diff --git a/frontend/src/components/README.md b/frontend/src/components/README.md new file mode 100644 index 0000000..ab0e8ff --- /dev/null +++ b/frontend/src/components/README.md @@ -0,0 +1,35 @@ +# 组件 + +此文件夹中的 Vue 模板文件会被自动导入。 + +## 🚀 使用方法 + +自动导入功能由 [unplugin-vue-components](https://github.com/unplugin/unplugin-vue-components) 实现。该插件会自动导入 `src/components` 目录下创建的 `.vue` 文件,并将它们注册为全局组件。这意味着你可以在应用程序中直接使用任何组件而无需手动导入。 + +以下示例假设存在一个位于 `src/components/MyComponent.vue` 的组件: + +```vue + + + +``` + +当模板渲染时,组件的导入语句会被自动内联,最终呈现为: + +```vue + + + +``` \ No newline at end of file diff --git a/frontend/src/main.js b/frontend/src/main.js new file mode 100644 index 0000000..1788c87 --- /dev/null +++ b/frontend/src/main.js @@ -0,0 +1,72 @@ +/** + * main.js + * + * Bootstraps Vuetify and other plugins then mounts the App` + */ + +// Components +import App from './App.vue' + +// Composables +import { createApp } from 'vue' + +// 先导入样式,确保在应用挂载前已加载CSS +import './assets/styles/global.css' // 导入全局样式 + +// 添加预加载完成标志以避免闪屏 +document.addEventListener('DOMContentLoaded', () => { + document.documentElement.setAttribute('data-app-loaded', 'true') +}) + +// 创建应用实例 +const app = createApp(App) + +// 异步导入其他依赖以优化初始加载 +Promise.all([ + import('./plugins/vuetify'), // Vuetify + import('./router'), // 路由 + import('./services/api_service'), // API服务 + import('./services/storage_service') // 本地存储服务 +]).then(([{ default: vuetify }, { default: router }, { default: apiService }, { default: storageService }]) => { + + // 添加全局事件总线功能 + app.config.globalProperties.$root = { + $on: (event, callback) => { + if (!app.config.globalProperties._eventBus) app.config.globalProperties._eventBus = {} + if (!app.config.globalProperties._eventBus[event]) app.config.globalProperties._eventBus[event] = [] + app.config.globalProperties._eventBus[event].push(callback) + }, + $off: (event, callback) => { + if (!app.config.globalProperties._eventBus || !app.config.globalProperties._eventBus[event]) return + if (!callback) { + app.config.globalProperties._eventBus[event] = [] + } else { + app.config.globalProperties._eventBus[event] = app.config.globalProperties._eventBus[event].filter(cb => cb !== callback) + } + }, + $emit: (event, ...args) => { + if (!app.config.globalProperties._eventBus || !app.config.globalProperties._eventBus[event]) return + app.config.globalProperties._eventBus[event].forEach(callback => callback(...args)) + } + } + + // 将API服务注册为全局属性 + app.config.globalProperties.$api = apiService + + // 将存储服务注册为全局属性 + app.config.globalProperties.$storage = storageService + + // 定期清理过期缓存 + setInterval(() => { + storageService.cleanExpiredCache(); + }, 30 * 60 * 1000); // 每30分钟执行一次 + + // 使用插件 + app.use(router) + app.use(vuetify) + + // 确保所有资源都加载完毕后再挂载应用 + setTimeout(() => { + app.mount('#app') + }, 0) +}) diff --git a/frontend/src/plugins/README.md b/frontend/src/plugins/README.md new file mode 100644 index 0000000..62201c7 --- /dev/null +++ b/frontend/src/plugins/README.md @@ -0,0 +1,3 @@ +# Plugins + +Plugins are a way to extend the functionality of your Vue application. Use this folder for registering plugins that you want to use globally. diff --git a/frontend/src/plugins/index.js b/frontend/src/plugins/index.js new file mode 100644 index 0000000..705f228 --- /dev/null +++ b/frontend/src/plugins/index.js @@ -0,0 +1,12 @@ +/** + * plugins/index.js + * + * Automatically included in `./src/main.js` + */ + +// Plugins +import vuetify from './vuetify' + +export function registerPlugins (app) { + app.use(vuetify) +} diff --git a/frontend/src/plugins/vuetify.js b/frontend/src/plugins/vuetify.js new file mode 100644 index 0000000..c73600f --- /dev/null +++ b/frontend/src/plugins/vuetify.js @@ -0,0 +1,61 @@ +/** + * plugins/vuetify.js + * + * Framework documentation: https://vuetifyjs.com` + */ + +// Styles +import '@mdi/font/css/materialdesignicons.css' +import 'vuetify/styles' + +// Composables +import { createVuetify } from 'vuetify' + +// 预设主题以防止闪烁 +const setInitialTheme = () => { + // 检查本地存储中的主题首选项 + const savedTheme = localStorage.getItem('vuetify-theme-preference') || 'dark' + + // 在DOM加载前应用主题类,避免闪烁 + document.documentElement.classList.add(`v-theme--${savedTheme}`) + + return savedTheme +} + +const defaultTheme = setInitialTheme() + +// https://vuetifyjs.com/en/introduction/why-vuetify/#feature-guides +export default createVuetify({ + theme: { + defaultTheme, + themes: { + light: { + dark: false, + colors: { + primary: '#1867C0', + secondary: '#5CBBF6', + } + }, + dark: { + dark: true, + colors: { + primary: '#2196F3', + secondary: '#03A9F4', + } + } + }, + options: { + // 启用自定义属性以提高渲染性能 + customProperties: true, + // 缓存主题以避免重新计算 + cspNonce: 'findreve-theme', + // 减少主题变化时的闪烁 + variations: false + } + }, + defaults: { + VBtn: { + variant: 'flat' + }, + } +}) diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js new file mode 100644 index 0000000..556b715 --- /dev/null +++ b/frontend/src/router/index.js @@ -0,0 +1,66 @@ +// src/router/index.js +import { createRouter, createWebHistory } from 'vue-router' +import Home from '@/views/Home.vue' +import Found from '@/views/Found.vue' +import Admin from '@/views/Admin.vue' +import Login from '@/views/Login.vue' +import NotFound from '@/views/NotFound.vue' + +const routes = [ + { + path: '/', + name: 'Home', + meta: { title: '主页'}, + component: Home + }, + { + path: '/found', + name: 'Found', + meta: { title: '关于此物品'}, + component: Found + }, + { + path: '/admin', + name: 'Admin', + component: Admin, + meta: { + requiresAuth: true, + title: 'Findreve 仪表盘' + } + }, + { + path: '/login', + name: 'Login', + meta: { title: '登录 Findreve'}, + component: Login + }, + // 添加404路由,必须放在最后以匹配所有未定义的路径 + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: NotFound, + meta: { title: '404 - 页面未找到' } + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// 路由守卫,用于检查用户是否已登录并更新页面标题 +router.beforeEach((to, from, next) => { + // 更新页面标题 + document.title = to.meta.title || 'Findreve' + + const isAuthenticated = localStorage.getItem('user-token') // 简单的认证检查,实际应用中可能更复杂 + + if (to.meta.requiresAuth && !isAuthenticated) { + // 如果路由需要认证但用户未登录,重定向到登录页 + next({ name: 'Login', query: { redirect: to.fullPath } }) + } else { + next() + } +}) + +export default router \ No newline at end of file diff --git a/frontend/src/services/api_service.js b/frontend/src/services/api_service.js new file mode 100644 index 0000000..c8e1bfc --- /dev/null +++ b/frontend/src/services/api_service.js @@ -0,0 +1,315 @@ +/** + * API 服务 + * + * 提供统一的 HTTP 请求处理,包括认证令牌管理、错误处理等功能。 + * 自动处理令牌过期情况,在令牌失效时重定向到登录页面。 + * 集成了本地缓存功能,支持优先使用缓存数据。 + */ + +import router from '@/router'; +import storageService from './storage_service'; + +class ApiService { + /** + * 发送 HTTP 请求 + * + * @param {string} url - 请求地址 + * @param {Object} options - 请求选项 + * @returns {Promise} 响应数据 + */ + async request(url, options = {}) { + // 默认请求头 + const headers = { + 'accept': 'application/json', + ...options.headers + }; + + // 添加认证令牌 + const token = localStorage.getItem('user-token'); + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } + + try { + const response = await fetch(url, { + ...options, + headers + }); + + // 处理认证错误 + if (response.status === 401) { + console.log('认证失败,可能是令牌已过期'); + // 清除过期的令牌 + localStorage.removeItem('user-token'); + + // 保存当前路径,用于登录后跳转回来 + const currentPath = router.currentRoute.value.fullPath; + if (currentPath !== '/login') { + // 跳转到登录页面,带上重定向参数 + router.push({ + path: '/login', + query: { redirect: currentPath, expired: 'true' } + }); + + // 抛出错误,中断后续处理 + throw new Error('认证已过期,请重新登录'); + } + } + + // 处理其他错误 + if (!response.ok) { + // 尝试解析错误信息 + let errorMessage; + try { + const errorData = await response.json(); + errorMessage = errorData.msg || errorData.detail || `请求失败: ${response.status}`; + } catch (e) { + errorMessage = `请求失败: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); + } + + return await response.json(); + } catch (error) { + console.error('API 请求错误:', error); + throw error; + } + } + + /** + * GET 请求 + * + * @param {string} url - 请求地址 + * @param {Object} options - 请求选项 + * @returns {Promise} 响应数据 + */ + get(url, options = {}) { + return this.request(url, { + method: 'GET', + ...options + }); + } + + /** + * POST 请求 + * + * @param {string} url - 请求地址 + * @param {Object|FormData|string} data - 请求数据 + * @param {Object} options - 请求选项 + * @returns {Promise} 响应数据 + */ + post(url, data, options = {}) { + const requestOptions = { + method: 'POST', + ...options + }; + + // 根据数据类型设置请求体和内容类型 + if (data) { + if (data instanceof FormData) { + requestOptions.body = data; + } else if (typeof data === 'string') { + requestOptions.body = data; + } else { + requestOptions.body = JSON.stringify(data); + requestOptions.headers = { + 'Content-Type': 'application/json', + ...options.headers + }; + } + } + + return this.request(url, requestOptions); + } + + /** + * PATCH 请求 + * + * @param {string} url - 请求地址 + * @param {Object|FormData|string} data - 请求数据 + * @param {Object} options - 请求选项 + * @returns {Promise} 响应数据 + */ + patch(url, data, options = {}) { + const requestOptions = { + method: 'PATCH', + ...options + }; + + // 处理请求体 + if (data) { + if (data instanceof FormData || typeof data === 'string') { + requestOptions.body = data; + } else { + requestOptions.body = JSON.stringify(data); + requestOptions.headers = { + 'Content-Type': 'application/json', + ...options.headers + }; + } + } else { + requestOptions.body = ''; + } + + return this.request(url, requestOptions); + } + + /** + * DELETE 请求 + * + * @param {string} url - 请求地址 + * @param {Object} options - 请求选项 + * @returns {Promise} 响应数据 + */ + delete(url, options = {}) { + return this.request(url, { + method: 'DELETE', + ...options + }); + } + + /** + * 提交表单数据 + * + * @param {string} url - 请求地址 + * @param {Object} formData - 表单数据对象 + * @param {Object} options - 请求选项 + * @returns {Promise} 响应数据 + */ + submitForm(url, formData, options = {}) { + const urlSearchParams = new URLSearchParams(); + + // 将对象转换为 URLSearchParams + Object.keys(formData).forEach(key => { + if (formData[key] !== undefined && formData[key] !== null) { + urlSearchParams.append(key, formData[key]); + } + }); + + return this.post(url, urlSearchParams.toString(), { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + ...options + }); + } + + /** + * 登录请求 + * + * @param {string} username - 用户名 + * @param {string} password - 密码 + * @returns {Promise} 登录结果 + */ + async login(username, password) { + try { + const formData = new URLSearchParams(); + formData.append('username', username); + formData.append('password', password); + formData.append('grant_type', 'password'); + + const response = await fetch('/api/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'accept': 'application/json' + }, + body: formData + }); + + if (!response.ok) { + let errorMessage = '登录失败'; + if (response.status === 401) { + errorMessage = '账号或密码错误'; + } else { + try { + const errorData = await response.json(); + errorMessage = errorData.detail || '登录失败'; + } catch (e) { + console.error('解析错误响应失败:', e); + } + } + throw new Error(errorMessage); + } + + const data = await response.json(); + localStorage.setItem('user-token', data.access_token); + + return { success: true, data }; + } catch (error) { + console.error('登录错误:', error); + return { success: false, error: error.message }; + } + } + + /** + * 验证当前令牌是否有效 + * + * 通过调用 /api/admin/ 接口验证当前令牌的有效性 + * + * @returns {Promise} 令牌是否有效 + */ + async validateToken() { + try { + // 检查是否有令牌 + const token = localStorage.getItem('user-token'); + if (!token) { + console.log('没有找到认证令牌'); + return false; + } + + // 调用验证接口 + const response = await this.get('/api/admin/'); + return response === true; + } catch (error) { + console.log('令牌验证失败:', error); + return false; + } + } + + /** + * 获取物品详情 + * + * 根据物品标识码获取物品信息,支持缓存机制 + * + * @param {string} key - 物品标识码 + * @param {boolean} useCache - 是否优先使用缓存数据 + * @returns {Promise} 物品详情 + */ + async getObject(key, useCache = true) { + try { + // 1. 如果允许使用缓存,先检查是否有缓存数据 + if (useCache) { + const cachedItem = storageService.getItemFromCache(key); + if (cachedItem) { + console.log('Using cached item data:', key); + return cachedItem; + } + } + + // 2. 没有缓存或不使用缓存,从API获取数据 + const data = await this.get(`/api/object/${encodeURIComponent(key)}`); + + if (data.code === 0) { + // 3. 获取数据成功后,保存到缓存 + storageService.saveItemToCache(key, data.data); + return data.data; + } else { + throw new Error(data.msg || '获取物品信息失败'); + } + } catch (error) { + console.error('获取物品错误:', error); + throw error; + } + } + + /** + * 清除API结果缓存 + * 可用于强制刷新数据或在用户登出时清除敏感信息 + */ + clearCache() { + storageService.clearAllCache(); + } +} + +export default new ApiService(); diff --git a/frontend/src/services/storage_service.js b/frontend/src/services/storage_service.js new file mode 100644 index 0000000..0dc4e55 --- /dev/null +++ b/frontend/src/services/storage_service.js @@ -0,0 +1,205 @@ +/** + * 本地存储服务 + * + * 提供本地数据的存储和获取功能,支持缓存物品详情和其他应用数据 + * 包括缓存过期时间控制和数据版本管理 + */ + +const STORAGE_KEYS = { + ITEMS_CACHE: 'findreve-items-cache', + CACHE_VERSION: 'findreve-cache-version' +}; + +// 当前缓存版本号,当数据结构变更时修改此值使旧缓存失效 +const CURRENT_CACHE_VERSION = '1.0'; + +// 缓存默认过期时间(24小时) +const DEFAULT_CACHE_EXPIRY = 24 * 60 * 60 * 1000; + +class StorageService { + constructor() { + // 初始化时检查缓存版本,清除过期缓存 + this.initializeCache(); + } + + /** + * 初始化缓存 + * + * 检查缓存版本,如果版本不匹配则清除所有缓存 + */ + initializeCache() { + try { + const cachedVersion = localStorage.getItem(STORAGE_KEYS.CACHE_VERSION); + + // 如果版本号不匹配,清除所有缓存 + if (cachedVersion !== CURRENT_CACHE_VERSION) { + console.log('Cache version mismatch, clearing cache...'); + this.clearAllCache(); + localStorage.setItem(STORAGE_KEYS.CACHE_VERSION, CURRENT_CACHE_VERSION); + } + } catch (error) { + console.error('Error initializing cache:', error); + // 出错时尝试清除缓存 + this.clearAllCache(); + } + } + + /** + * 保存物品数据到本地缓存 + * + * @param {string} key - 物品唯一标识 + * @param {Object} itemData - 物品详情数据 + * @param {number} expiryTime - 缓存过期时间(毫秒),默认24小时 + */ + saveItemToCache(key, itemData, expiryTime = DEFAULT_CACHE_EXPIRY) { + try { + // 获取现有缓存 + const itemsCache = this.getAllCachedItems() || {}; + + // 更新缓存,添加时间戳 + itemsCache[key] = { + data: itemData, + timestamp: Date.now(), + expiry: expiryTime + }; + + // 保存回本地存储 + localStorage.setItem(STORAGE_KEYS.ITEMS_CACHE, JSON.stringify(itemsCache)); + + console.log(`Item cached: ${key}`); + } catch (error) { + console.error('Error saving item to cache:', error); + } + } + + /** + * 从缓存获取物品数据 + * + * @param {string} key - 物品唯一标识 + * @returns {Object|null} 缓存的物品数据,如果不存在或已过期则返回null + */ + getItemFromCache(key) { + try { + const itemsCache = this.getAllCachedItems() || {}; + const cachedItem = itemsCache[key]; + + // 检查是否存在缓存 + if (!cachedItem) { + return null; + } + + // 检查缓存是否过期 + const now = Date.now(); + if (now - cachedItem.timestamp > cachedItem.expiry) { + console.log(`Cache expired for item: ${key}`); + this.removeItemFromCache(key); + return null; + } + + console.log(`Cache hit for item: ${key}`); + return cachedItem.data; + } catch (error) { + console.error('Error retrieving item from cache:', error); + return null; + } + } + + /** + * 获取缓存项的时间戳 + * + * @param {string} key - 缓存项的唯一标识 + * @returns {number|null} 缓存项的时间戳,如果不存在则返回null + */ + getCacheTimestamp(key) { + try { + const itemsCache = this.getAllCachedItems() || {}; + const cachedItem = itemsCache[key]; + + if (cachedItem && cachedItem.timestamp) { + return cachedItem.timestamp; + } + return null; + } catch (error) { + console.error('Error getting cache timestamp:', error); + return null; + } + } + + /** + * 从缓存中移除物品数据 + * + * @param {string} key - 物品唯一标识 + */ + removeItemFromCache(key) { + try { + const itemsCache = this.getAllCachedItems() || {}; + + if (itemsCache[key]) { + delete itemsCache[key]; + localStorage.setItem(STORAGE_KEYS.ITEMS_CACHE, JSON.stringify(itemsCache)); + console.log(`Removed item from cache: ${key}`); + } + } catch (error) { + console.error('Error removing item from cache:', error); + } + } + + /** + * 获取所有缓存的物品数据 + * + * @returns {Object|null} 包含所有缓存物品的对象 + */ + getAllCachedItems() { + try { + const cachedData = localStorage.getItem(STORAGE_KEYS.ITEMS_CACHE); + return cachedData ? JSON.parse(cachedData) : {}; + } catch (error) { + console.error('Error getting all cached items:', error); + return {}; + } + } + + /** + * 清理过期的缓存项目 + * + * 遍历所有缓存项目并移除已过期的条目 + */ + cleanExpiredCache() { + try { + const now = Date.now(); + const itemsCache = this.getAllCachedItems() || {}; + let hasExpired = false; + + // 检查每个缓存项是否过期 + Object.keys(itemsCache).forEach(key => { + const item = itemsCache[key]; + if (now - item.timestamp > item.expiry) { + delete itemsCache[key]; + hasExpired = true; + console.log(`Expired cache removed: ${key}`); + } + }); + + // 如果有过期项,更新缓存 + if (hasExpired) { + localStorage.setItem(STORAGE_KEYS.ITEMS_CACHE, JSON.stringify(itemsCache)); + } + } catch (error) { + console.error('Error cleaning expired cache:', error); + } + } + + /** + * 清除所有缓存数据 + */ + clearAllCache() { + try { + localStorage.removeItem(STORAGE_KEYS.ITEMS_CACHE); + console.log('All cache cleared'); + } catch (error) { + console.error('Error clearing cache:', error); + } + } +} + +export default new StorageService(); diff --git a/frontend/src/views/Admin.vue b/frontend/src/views/Admin.vue new file mode 100644 index 0000000..5d372fb --- /dev/null +++ b/frontend/src/views/Admin.vue @@ -0,0 +1,858 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/views/Found.vue b/frontend/src/views/Found.vue new file mode 100644 index 0000000..224c7ec --- /dev/null +++ b/frontend/src/views/Found.vue @@ -0,0 +1,286 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue new file mode 100644 index 0000000..00a3dcb --- /dev/null +++ b/frontend/src/views/Home.vue @@ -0,0 +1,600 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..b63184b --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,219 @@ + + + + + + \ No newline at end of file diff --git a/frontend/src/views/NotFound.vue b/frontend/src/views/NotFound.vue new file mode 100644 index 0000000..a2e325a --- /dev/null +++ b/frontend/src/views/NotFound.vue @@ -0,0 +1,39 @@ + + + diff --git a/frontend/vite.config.mjs b/frontend/vite.config.mjs new file mode 100644 index 0000000..7dfeb6b --- /dev/null +++ b/frontend/vite.config.mjs @@ -0,0 +1,70 @@ +// Plugins +import Components from 'unplugin-vue-components/vite' +import Vue from '@vitejs/plugin-vue' +import Vuetify, { transformAssetUrls } from 'vite-plugin-vuetify' +import ViteFonts from 'unplugin-fonts/vite' + +// Utilities +import { defineConfig } from 'vite' +import { fileURLToPath, URL } from 'node:url' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + Vue({ + template: { transformAssetUrls }, + }), + // https://github.com/vuetifyjs/vuetify-loader/tree/master/packages/vite-plugin#readme + Vuetify(), + Components(), + ViteFonts({ + google: { + families: [{ + name: 'Roboto', + styles: 'wght@100;300;400;500;700;900', + }], + }, + }), + ], + optimizeDeps: { + exclude: ['vuetify'], + }, + define: { 'process.env': {} }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + extensions: [ + '.js', + '.json', + '.jsx', + '.mjs', + '.ts', + '.tsx', + '.vue', + ], + }, + server: { + port: 3000, + proxy: { + // 配置代理 + '/api': { + target: 'http://127.0.0.1:8080', + changeOrigin: true, + secure: false, + // 如果后端API不包含/api前缀,可以使用下面的配置移除它 + // rewrite: (path) => path.replace(/^\/api/, '') + } + } + }, + css: { + preprocessorOptions: { + sass: { + api: 'modern-compiler', + }, + scss: { + api:'modern-compiler', + }, + }, + }, +}) diff --git a/frontend/yarn.lock b/frontend/yarn.lock new file mode 100644 index 0000000..f235a5c --- /dev/null +++ b/frontend/yarn.lock @@ -0,0 +1,1244 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/helper-string-parser@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" + integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== + +"@babel/helper-validator-identifier@^7.25.9": + version "7.25.9" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" + integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== + +"@babel/parser@^7.25.3": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.0.tgz#3d7d6ee268e41d2600091cbd4e145ffee85a44ec" + integrity sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg== + dependencies: + "@babel/types" "^7.27.0" + +"@babel/types@^7.27.0": + version "7.27.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.0.tgz#ef9acb6b06c3173f6632d993ecb6d4ae470b4559" + integrity sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg== + dependencies: + "@babel/helper-string-parser" "^7.25.9" + "@babel/helper-validator-identifier" "^7.25.9" + +"@bufbuild/protobuf@^2.0.0": + version "2.2.5" + resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.2.5.tgz#8e82c0af292113b4a89f8b658c71c4636c8d2e36" + integrity sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ== + +"@esbuild/aix-ppc64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz#b87036f644f572efb2b3c75746c97d1d2d87ace8" + integrity sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag== + +"@esbuild/android-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz#5ca7dc20a18f18960ad8d5e6ef5cf7b0a256e196" + integrity sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w== + +"@esbuild/android-arm@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.2.tgz#3c49f607b7082cde70c6ce0c011c362c57a194ee" + integrity sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA== + +"@esbuild/android-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.2.tgz#8a00147780016aff59e04f1036e7cb1b683859e2" + integrity sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg== + +"@esbuild/darwin-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz#486efe7599a8d90a27780f2bb0318d9a85c6c423" + integrity sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA== + +"@esbuild/darwin-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz#95ee222aacf668c7a4f3d7ee87b3240a51baf374" + integrity sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA== + +"@esbuild/freebsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz#67efceda8554b6fc6a43476feba068fb37fa2ef6" + integrity sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w== + +"@esbuild/freebsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz#88a9d7ecdd3adadbfe5227c2122d24816959b809" + integrity sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ== + +"@esbuild/linux-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz#87be1099b2bbe61282333b084737d46bc8308058" + integrity sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g== + +"@esbuild/linux-arm@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz#72a285b0fe64496e191fcad222185d7bf9f816f6" + integrity sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g== + +"@esbuild/linux-ia32@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz#337a87a4c4dd48a832baed5cbb022be20809d737" + integrity sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ== + +"@esbuild/linux-loong64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz#1b81aa77103d6b8a8cfa7c094ed3d25c7579ba2a" + integrity sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w== + +"@esbuild/linux-mips64el@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz#afbe380b6992e7459bf7c2c3b9556633b2e47f30" + integrity sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q== + +"@esbuild/linux-ppc64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz#6bf8695cab8a2b135cca1aa555226dc932d52067" + integrity sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g== + +"@esbuild/linux-riscv64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz#43c2d67a1a39199fb06ba978aebb44992d7becc3" + integrity sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw== + +"@esbuild/linux-s390x@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz#419e25737ec815c6dce2cd20d026e347cbb7a602" + integrity sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q== + +"@esbuild/linux-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz#22451f6edbba84abe754a8cbd8528ff6e28d9bcb" + integrity sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg== + +"@esbuild/netbsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz#744affd3b8d8236b08c5210d828b0698a62c58ac" + integrity sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw== + +"@esbuild/netbsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz#dbbe7521fd6d7352f34328d676af923fc0f8a78f" + integrity sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg== + +"@esbuild/openbsd-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz#f9caf987e3e0570500832b487ce3039ca648ce9f" + integrity sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg== + +"@esbuild/openbsd-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz#d2bb6a0f8ffea7b394bb43dfccbb07cabd89f768" + integrity sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw== + +"@esbuild/sunos-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz#49b437ed63fe333b92137b7a0c65a65852031afb" + integrity sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA== + +"@esbuild/win32-arm64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz#081424168463c7d6c7fb78f631aede0c104373cf" + integrity sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q== + +"@esbuild/win32-ia32@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz#3f9e87143ddd003133d21384944a6c6cadf9693f" + integrity sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg== + +"@esbuild/win32-x64@0.25.2": + version "0.25.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz#839f72c2decd378f86b8f525e1979a97b920c67d" + integrity sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA== + +"@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@mdi/font@7.4.47": + version "7.4.47" + resolved "https://registry.yarnpkg.com/@mdi/font/-/font-7.4.47.tgz#2ae522867da3a5c88b738d54b403eb91471903af" + integrity sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@rollup/rollup-android-arm-eabi@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.0.tgz#d964ee8ce4d18acf9358f96adc408689b6e27fe3" + integrity sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg== + +"@rollup/rollup-android-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.0.tgz#9b5e130ecc32a5fc1e96c09ff371743ee71a62d3" + integrity sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w== + +"@rollup/rollup-darwin-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.0.tgz#ef439182c739b20b3c4398cfc03e3c1249ac8903" + integrity sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ== + +"@rollup/rollup-darwin-x64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.0.tgz#d7380c1531ab0420ca3be16f17018ef72dd3d504" + integrity sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA== + +"@rollup/rollup-freebsd-arm64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.0.tgz#cbcbd7248823c6b430ce543c59906dd3c6df0936" + integrity sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg== + +"@rollup/rollup-freebsd-x64@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.0.tgz#96bf6ff875bab5219c3472c95fa6eb992586a93b" + integrity sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw== + +"@rollup/rollup-linux-arm-gnueabihf@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.0.tgz#d80cd62ce6d40f8e611008d8dbf03b5e6bbf009c" + integrity sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA== + +"@rollup/rollup-linux-arm-musleabihf@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.0.tgz#75440cfc1e8d0f87a239b4c31dfeaf4719b656b7" + integrity sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg== + +"@rollup/rollup-linux-arm64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.0.tgz#ac527485ecbb619247fb08253ec8c551a0712e7c" + integrity sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg== + +"@rollup/rollup-linux-arm64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.0.tgz#74d2b5cb11cf714cd7d1682e7c8b39140e908552" + integrity sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ== + +"@rollup/rollup-linux-loongarch64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.0.tgz#a0a310e51da0b5fea0e944b0abd4be899819aef6" + integrity sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg== + +"@rollup/rollup-linux-powerpc64le-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.0.tgz#4077e2862b0ac9f61916d6b474d988171bd43b83" + integrity sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw== + +"@rollup/rollup-linux-riscv64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.0.tgz#5812a1a7a2f9581cbe12597307cc7ba3321cf2f3" + integrity sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA== + +"@rollup/rollup-linux-riscv64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.0.tgz#973aaaf4adef4531375c36616de4e01647f90039" + integrity sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ== + +"@rollup/rollup-linux-s390x-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.0.tgz#9bad59e907ba5bfcf3e9dbd0247dfe583112f70b" + integrity sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw== + +"@rollup/rollup-linux-x64-gnu@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.0.tgz#68b045a720bd9b4d905f462b997590c2190a6de0" + integrity sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ== + +"@rollup/rollup-linux-x64-musl@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.0.tgz#8e703e2c2ad19ba7b2cb3d8c3a4ad11d4ee3a282" + integrity sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw== + +"@rollup/rollup-win32-arm64-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.0.tgz#c5bee19fa670ff5da5f066be6a58b4568e9c650b" + integrity sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ== + +"@rollup/rollup-win32-ia32-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.0.tgz#846e02c17044bd922f6f483a3b4d36aac6e2b921" + integrity sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA== + +"@rollup/rollup-win32-x64-msvc@4.40.0": + version "4.40.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.0.tgz#fd92d31a2931483c25677b9c6698106490cbbc76" + integrity sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ== + +"@types/estree@1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@vitejs/plugin-vue@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz#71a8fc82d4d2e425af304c35bf389506f674d89b" + integrity sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg== + +"@vue/compiler-core@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05" + integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/shared" "3.5.13" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58" + integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA== + dependencies: + "@vue/compiler-core" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/compiler-sfc@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46" + integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/compiler-core" "3.5.13" + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + estree-walker "^2.0.2" + magic-string "^0.30.11" + postcss "^8.4.48" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba" + integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/devtools-api@^6.6.4": + version "6.6.4" + resolved "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" + integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== + +"@vue/reactivity@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f" + integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg== + dependencies: + "@vue/shared" "3.5.13" + +"@vue/runtime-core@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455" + integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw== + dependencies: + "@vue/reactivity" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/runtime-dom@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215" + integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog== + dependencies: + "@vue/reactivity" "3.5.13" + "@vue/runtime-core" "3.5.13" + "@vue/shared" "3.5.13" + csstype "^3.1.3" + +"@vue/server-renderer@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7" + integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA== + dependencies: + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/shared@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f" + integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ== + +"@vuetify/loader-shared@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@vuetify/loader-shared/-/loader-shared-2.1.0.tgz#29410dce04a78fa9cd40c4d9bc417b8d61ce5103" + integrity sha512-dNE6Ceym9ijFsmJKB7YGW0cxs7xbYV8+1LjU6jd4P14xOt/ji4Igtgzt0rJFbxu+ZhAzqz853lhB0z8V9Dy9cQ== + dependencies: + upath "^2.0.1" + +acorn@^8.14.0, acorn@^8.14.1: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^4.0.0: + version "4.3.0" + resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +buffer-builder@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/buffer-builder/-/buffer-builder-0.2.0.tgz#3322cd307d8296dab1f604618593b261a3fade8f" + integrity sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg== + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.npmmirror.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cliui@^6.0.0: + version "6.0.0" + resolved "https://registry.npmmirror.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1" + integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^6.2.0" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +colorjs.io@^0.5.0: + version "0.5.2" + resolved "https://registry.yarnpkg.com/colorjs.io/-/colorjs.io-0.5.2.tgz#63b20139b007591ebc3359932bef84628eb3fcef" + integrity sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw== + +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +confbox@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110" + integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ== + +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +debug@^4.3.3, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== + +dijkstrajs@^1.0.1: + version "1.0.3" + resolved "https://registry.npmmirror.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" + integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +esbuild@^0.25.0: + version "0.25.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.2.tgz#55a1d9ebcb3aa2f95e8bba9e900c1a5061bc168b" + integrity sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ== + optionalDependencies: + "@esbuild/aix-ppc64" "0.25.2" + "@esbuild/android-arm" "0.25.2" + "@esbuild/android-arm64" "0.25.2" + "@esbuild/android-x64" "0.25.2" + "@esbuild/darwin-arm64" "0.25.2" + "@esbuild/darwin-x64" "0.25.2" + "@esbuild/freebsd-arm64" "0.25.2" + "@esbuild/freebsd-x64" "0.25.2" + "@esbuild/linux-arm" "0.25.2" + "@esbuild/linux-arm64" "0.25.2" + "@esbuild/linux-ia32" "0.25.2" + "@esbuild/linux-loong64" "0.25.2" + "@esbuild/linux-mips64el" "0.25.2" + "@esbuild/linux-ppc64" "0.25.2" + "@esbuild/linux-riscv64" "0.25.2" + "@esbuild/linux-s390x" "0.25.2" + "@esbuild/linux-x64" "0.25.2" + "@esbuild/netbsd-arm64" "0.25.2" + "@esbuild/netbsd-x64" "0.25.2" + "@esbuild/openbsd-arm64" "0.25.2" + "@esbuild/openbsd-x64" "0.25.2" + "@esbuild/sunos-x64" "0.25.2" + "@esbuild/win32-arm64" "0.25.2" + "@esbuild/win32-ia32" "0.25.2" + "@esbuild/win32-x64" "0.25.2" + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +exsolve@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.5.tgz#1f5b6b4fe82ad6b28a173ccb955a635d77859dcf" + integrity sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg== + +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +fdir@^6.4.3, fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.npmmirror.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +globals@^16.0.0: + version "16.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-16.0.0.tgz#3d7684652c5c4fbd086ec82f9448214da49382d8" + integrity sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +immutable@^5.0.2: + version "5.1.1" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.1.tgz#d4cb552686f34b076b3dcf23c4384c04424d8354" + integrity sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +local-pkg@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-1.1.1.tgz#f5fe74a97a3bd3c165788ee08ca9fbe998dc58dd" + integrity sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg== + dependencies: + mlly "^1.7.4" + pkg-types "^2.0.1" + quansync "^0.2.8" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.npmmirror.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +magic-string@^0.30.11, magic-string@^0.30.17: + version "0.30.17" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" + integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mlly@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.4.tgz#3d7295ea2358ec7a271eaa5d000a0f84febe100f" + integrity sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw== + dependencies: + acorn "^8.14.0" + pathe "^2.0.1" + pkg-types "^1.3.0" + ufo "^1.5.4" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.8: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.npmmirror.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.npmmirror.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.npmmirror.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +pkg-types@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== + dependencies: + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +pkg-types@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.1.0.tgz#70c9e1b9c74b63fdde749876ee0aa007ea9edead" + integrity sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A== + dependencies: + confbox "^0.2.1" + exsolve "^1.0.1" + pathe "^2.0.3" + +pngjs@^5.0.0: + version "5.0.0" + resolved "https://registry.npmmirror.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb" + integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw== + +postcss@^8.4.48, postcss@^8.5.3: + version "8.5.3" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +qrcode@^1.5.4: + version "1.5.4" + resolved "https://registry.npmmirror.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88" + integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg== + dependencies: + dijkstrajs "^1.0.1" + pngjs "^5.0.0" + yargs "^15.3.1" + +quansync@^0.2.8: + version "0.2.10" + resolved "https://registry.yarnpkg.com/quansync/-/quansync-0.2.10.tgz#32053cf166fa36511aae95fc49796116f2dc20e1" + integrity sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +roboto-fontface@*: + version "0.10.0" + resolved "https://registry.yarnpkg.com/roboto-fontface/-/roboto-fontface-0.10.0.tgz#7eee40cfa18b1f7e4e605eaf1a2740afb6fd71b0" + integrity sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g== + +rollup@^4.34.9: + version "4.40.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.0.tgz#13742a615f423ccba457554f006873d5a4de1920" + integrity sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w== + dependencies: + "@types/estree" "1.0.7" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.40.0" + "@rollup/rollup-android-arm64" "4.40.0" + "@rollup/rollup-darwin-arm64" "4.40.0" + "@rollup/rollup-darwin-x64" "4.40.0" + "@rollup/rollup-freebsd-arm64" "4.40.0" + "@rollup/rollup-freebsd-x64" "4.40.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.40.0" + "@rollup/rollup-linux-arm-musleabihf" "4.40.0" + "@rollup/rollup-linux-arm64-gnu" "4.40.0" + "@rollup/rollup-linux-arm64-musl" "4.40.0" + "@rollup/rollup-linux-loongarch64-gnu" "4.40.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.40.0" + "@rollup/rollup-linux-riscv64-gnu" "4.40.0" + "@rollup/rollup-linux-riscv64-musl" "4.40.0" + "@rollup/rollup-linux-s390x-gnu" "4.40.0" + "@rollup/rollup-linux-x64-gnu" "4.40.0" + "@rollup/rollup-linux-x64-musl" "4.40.0" + "@rollup/rollup-win32-arm64-msvc" "4.40.0" + "@rollup/rollup-win32-ia32-msvc" "4.40.0" + "@rollup/rollup-win32-x64-msvc" "4.40.0" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@^7.4.0: + version "7.8.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b" + integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA== + dependencies: + tslib "^2.1.0" + +sass-embedded-android-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.86.3.tgz#daa4658a383e4834a511fd00321841b5da71fd7d" + integrity sha512-q+XwFp6WgAv+UgnQhsB8KQ95kppvWAB7DSoJp+8Vino8b9ND+1ai3cUUZPE5u4SnLZrgo5NtrbPvN5KLc4Pfyg== + +sass-embedded-android-arm@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.86.3.tgz#adf63d572e972aaba07b6dc3a006828ed745b4d1" + integrity sha512-UyeXrFzZSvrGbvrWUBcspbsbivGgAgebLGJdSqJulgSyGbA6no3DWQ5Qpdd6+OAUC39BlpPu74Wx9s4RrVuaFw== + +sass-embedded-android-ia32@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.86.3.tgz#daca4191cf0e4625e79e6765ced132106ff2641e" + integrity sha512-gTJjVh2cRzvGujXj5ApPk/owUTL5SiO7rDtNLrzYAzi1N5HRuLYXqk3h1IQY3+eCOBjGl7mQ9XyySbJs/3hDvg== + +sass-embedded-android-riscv64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.86.3.tgz#b62bc4ca759c3d3bff545bd1eaa85e462392bfd4" + integrity sha512-Po3JnyiCS16kd6REo1IMUbFGYtvL9O0rmKaXx5vOuBaJD1LPy2LiSSp7TU7wkJ9IxsTDGzFaSeP1I9qb6D8VVg== + +sass-embedded-android-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.86.3.tgz#5440c91eae7db2b281e414f27e331d7556dac0d4" + integrity sha512-+7h3jdDv/0kUFx0BvxYlq2fa7CcHiDPlta6k5OxO5K6jyqJwo9hc0Z052BoYEauWTqZ+vK6bB5rv2BIzq4U9nA== + +sass-embedded-darwin-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.86.3.tgz#a538082a6fa59f15b1b0ecaba192e3a40ffa979d" + integrity sha512-EgLwV4ORm5Hr0DmIXo0Xw/vlzwLnfAiqD2jDXIglkBsc5czJmo4/IBdGXOP65TRnsgJEqvbU3aQhuawX5++x9A== + +sass-embedded-darwin-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.86.3.tgz#ea9a7c694ede309b3daf95262dda0681e9de973c" + integrity sha512-dfKhfrGPRNLWLC82vy/vQGmNKmAiKWpdFuWiePRtg/E95pqw+sCu6080Y6oQLfFu37Iq3MpnXiSpDuSo7UnPWA== + +sass-embedded-linux-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.86.3.tgz#0472365e17f57086f5006056d19e597a1b147fec" + integrity sha512-tYq5rywR53Qtc+0KI6pPipOvW7a47ETY69VxfqI9BR2RKw2hBbaz0bIw6OaOgEBv2/XNwcWb7a4sr7TqgkqKAA== + +sass-embedded-linux-arm@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.86.3.tgz#fcc85a2ad5bf335197a16c33992fe4c9c59807ed" + integrity sha512-+fVCIH+OR0SMHn2NEhb/VfbpHuUxcPtqMS34OCV3Ka99LYZUJZqth4M3lT/ppGl52mwIVLNYzR4iLe6mdZ6mYA== + +sass-embedded-linux-ia32@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.86.3.tgz#80ccbf951c1a9a816ce460595208686f13d078f0" + integrity sha512-CmQ5OkqnaeLdaF+bMqlYGooBuenqm3LvEN9H8BLhjkpWiFW8hnYMetiqMcJjhrXLvDw601KGqA5sr/Rsg5s45g== + +sass-embedded-linux-musl-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.86.3.tgz#664d7178017b2b47983fcf7bcdad03d90ec9109a" + integrity sha512-4zOr2C/eW89rxb4ozTfn7lBzyyM5ZigA1ZSRTcAR26Qbg/t2UksLdGnVX9/yxga0d6aOi0IvO/7iM2DPPRRotg== + +sass-embedded-linux-musl-arm@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.86.3.tgz#d3eace3ac4804541372ed61ce9aee384e3f22945" + integrity sha512-SEm65SQknI4pl+mH5Xf231hOkHJyrlgh5nj4qDbiBG6gFeutaNkNIeRgKEg3cflXchCr8iV/q/SyPgjhhzQb7w== + +sass-embedded-linux-musl-ia32@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.86.3.tgz#755eb08baa6da277bcd8b642710c7ffa16930586" + integrity sha512-84Tcld32LB1loiqUvczWyVBQRCChm0wNLlkT59qF29nxh8njFIVf9yaPgXcSyyjpPoD9Tu0wnq3dvVzoMCh9AQ== + +sass-embedded-linux-musl-riscv64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.86.3.tgz#d6e9b0c45b23be340999cc384eda04ae9fe34043" + integrity sha512-IxEqoiD7vdNpiOwccybbV93NljBy64wSTkUOknGy21SyV43C8uqESOwTwW9ywa3KufImKm8L3uQAW/B0KhJMWg== + +sass-embedded-linux-musl-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.86.3.tgz#88d6e6dcf1d9ac76c7e9949e2613310918a02617" + integrity sha512-ePeTPXUxPK6JgHcUfnrkIyDtyt+zlAvF22mVZv6y1g/PZFm1lSfX+Za7TYHg9KaYqaaXDiw6zICX4i44HhR8rA== + +sass-embedded-linux-riscv64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.86.3.tgz#624725ba3322f49b2401df6abb912e55879da526" + integrity sha512-NuXQ72dwfNLe35E+RaXJ4Noq4EkFwM65eWwCwxEWyJO9qxOx1EXiCAJii6x8kkOh5daWuMU0VAI1B9RsJaqqQQ== + +sass-embedded-linux-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.86.3.tgz#ac61d48784f794c0ee752a25d7105b1cb3c3a979" + integrity sha512-t8be9zJ5B82+og9bQmIQ83yMGYZMTMrlGA+uGWtYacmwg6w3093dk91Fx0YzNSZBp3Tk60qVYjCZnEIwy60x0g== + +sass-embedded-win32-arm64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.86.3.tgz#d71186bfbf16e2051ae145ea53f4cdc0f1db231d" + integrity sha512-4ghuAzjX4q8Nksm0aifRz8hgXMMxS0SuymrFfkfJlrSx68pIgvAge6AOw0edoZoe0Tf5ZbsWUWamhkNyNxkTvw== + +sass-embedded-win32-ia32@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.86.3.tgz#5e85820c515fce300d770950d776e0c68d72001e" + integrity sha512-tCaK4zIRq9mLRPxLzBAdYlfCuS/xLNpmjunYxeWkIwlJo+k53h1udyXH/FInnQ2GgEz0xMXyvH3buuPgzwWYsw== + +sass-embedded-win32-x64@1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.86.3.tgz#4bfd3e6969823487ee9497923a033f2456ce9f65" + integrity sha512-zS+YNKfTF4SnOfpC77VTb0qNZyTXrxnAezSoRV0xnw6HlY+1WawMSSB6PbWtmbvyfXNgpmJUttoTtsvJjRCucg== + +sass-embedded@^1.86.3: + version "1.86.3" + resolved "https://registry.yarnpkg.com/sass-embedded/-/sass-embedded-1.86.3.tgz#33358bfc13108c5b59b9904fb55ed56773b73037" + integrity sha512-3pZSp24ibO1hdopj+W9DuiWsZOb2YY6AFRo/jjutKLBkqJGM1nJjXzhAYfzRV+Xn5BX1eTI4bBTE09P0XNHOZg== + dependencies: + "@bufbuild/protobuf" "^2.0.0" + buffer-builder "^0.2.0" + colorjs.io "^0.5.0" + immutable "^5.0.2" + rxjs "^7.4.0" + supports-color "^8.1.1" + sync-child-process "^1.0.2" + varint "^6.0.0" + optionalDependencies: + sass-embedded-android-arm "1.86.3" + sass-embedded-android-arm64 "1.86.3" + sass-embedded-android-ia32 "1.86.3" + sass-embedded-android-riscv64 "1.86.3" + sass-embedded-android-x64 "1.86.3" + sass-embedded-darwin-arm64 "1.86.3" + sass-embedded-darwin-x64 "1.86.3" + sass-embedded-linux-arm "1.86.3" + sass-embedded-linux-arm64 "1.86.3" + sass-embedded-linux-ia32 "1.86.3" + sass-embedded-linux-musl-arm "1.86.3" + sass-embedded-linux-musl-arm64 "1.86.3" + sass-embedded-linux-musl-ia32 "1.86.3" + sass-embedded-linux-musl-riscv64 "1.86.3" + sass-embedded-linux-musl-x64 "1.86.3" + sass-embedded-linux-riscv64 "1.86.3" + sass-embedded-linux-x64 "1.86.3" + sass-embedded-win32-arm64 "1.86.3" + sass-embedded-win32-ia32 "1.86.3" + sass-embedded-win32-x64 "1.86.3" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + +source-map-js@^1.2.0, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +supports-color@^8.1.1: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +sync-child-process@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/sync-child-process/-/sync-child-process-1.0.2.tgz#45e7c72e756d1243e80b547ea2e17957ab9e367f" + integrity sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA== + dependencies: + sync-message-port "^1.0.0" + +sync-message-port@^1.0.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sync-message-port/-/sync-message-port-1.1.3.tgz#6055c565ee8c81d2f9ee5aae7db757e6d9088c0c" + integrity sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg== + +tinyglobby@^0.2.12: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tslib@^2.1.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +ufo@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== + +unplugin-fonts@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/unplugin-fonts/-/unplugin-fonts-1.3.1.tgz#84f2e446976d47d6d5bf9bed4bfa71d9adb1809e" + integrity sha512-GmaJWPAWH6lBI4fP8xKdbMZJwTgsnr8PGJOfQE52jlod8QkqSO4M529Nox2L8zYapjB5hox2wCu4N3c/LOal/A== + dependencies: + fast-glob "^3.3.2" + unplugin "2.0.0-beta.1" + +unplugin-utils@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/unplugin-utils/-/unplugin-utils-0.2.4.tgz#56e4029a6906645a10644f8befc404b06d5d24d0" + integrity sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA== + dependencies: + pathe "^2.0.2" + picomatch "^4.0.2" + +unplugin-vue-components@^28.4.1: + version "28.5.0" + resolved "https://registry.yarnpkg.com/unplugin-vue-components/-/unplugin-vue-components-28.5.0.tgz#33585a24c98939d1abe56bd69217bc7187ba329f" + integrity sha512-o7fMKU/uI8NiP+E0W62zoduuguWqB0obTfHFtbr1AP2uo2lhUPnPttWUE92yesdiYfo9/0hxIrj38FMc1eaySg== + dependencies: + chokidar "^3.6.0" + debug "^4.4.0" + local-pkg "^1.1.1" + magic-string "^0.30.17" + mlly "^1.7.4" + tinyglobby "^0.2.12" + unplugin "^2.3.2" + unplugin-utils "^0.2.4" + +unplugin@2.0.0-beta.1: + version "2.0.0-beta.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.0.0-beta.1.tgz#3f8c9ecfae03fc9e22d9821ba68d52aa46a13aeb" + integrity sha512-2qzQo5LN2DmUZXkWDHvGKLF5BP0WN+KthD6aPnPJ8plRBIjv4lh5O07eYcSxgO2znNw9s4MNhEO1sB+JDllDbQ== + dependencies: + acorn "^8.14.0" + webpack-virtual-modules "^0.6.2" + +unplugin@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.2.tgz#36c93a1662b70c97a2e2fc45c0e78fa09f7a4984" + integrity sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w== + dependencies: + acorn "^8.14.1" + picomatch "^4.0.2" + webpack-virtual-modules "^0.6.2" + +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + +varint@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0" + integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg== + +vite-plugin-vuetify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/vite-plugin-vuetify/-/vite-plugin-vuetify-2.1.1.tgz#31c958f0c64c436a3165462b81196a7c2ae3a2ff" + integrity sha512-Pb7bKhQH8qPMzURmEGq2aIqCJkruFNsyf1NcrrtnjsOIkqJPMcBbiP0oJoO8/uAmyB5W/1JTbbUEsyXdMM0QHQ== + dependencies: + "@vuetify/loader-shared" "^2.1.0" + debug "^4.3.3" + upath "^2.0.1" + +vite@^6.2.2: + version "6.3.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-6.3.2.tgz#4c1bb01b1cea853686a191657bbc14272a038f0a" + integrity sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg== + dependencies: + esbuild "^0.25.0" + fdir "^6.4.3" + picomatch "^4.0.2" + postcss "^8.5.3" + rollup "^4.34.9" + tinyglobby "^0.2.12" + optionalDependencies: + fsevents "~2.3.3" + +vue-router@4: + version "4.5.0" + resolved "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.0.tgz#58fc5fe374e10b6018f910328f756c3dae081f14" + integrity sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w== + dependencies: + "@vue/devtools-api" "^6.6.4" + +vue@^3.5.13: + version "3.5.13" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a" + integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-sfc" "3.5.13" + "@vue/runtime-dom" "3.5.13" + "@vue/server-renderer" "3.5.13" + "@vue/shared" "3.5.13" + +vuetify@^3.8.1: + version "3.8.2" + resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-3.8.2.tgz#59799811a6e97154ee238981b2926b166ff14ae2" + integrity sha512-UJNFP4egmKJTQ3V3MKOq+7vIUKO7/Fko5G6yUsOW2Rm0VNBvAjgO6VY6EnK3DTqEKN6ugVXDEPw37NQSTGLZvw== + +webpack-virtual-modules@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" + integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== + +which-module@^2.0.0: + version "2.0.1" + resolved "https://registry.npmmirror.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409" + integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ== + +wrap-ansi@^6.2.0: + version "6.2.0" + resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" + integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +y18n@^4.0.0: + version "4.0.3" + resolved "https://registry.npmmirror.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" + integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== + +yargs-parser@^18.1.2: + version "18.1.3" + resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^15.3.1: + version "15.4.1" + resolved "https://registry.npmmirror.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" + integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A== + dependencies: + cliui "^6.0.0" + decamelize "^1.2.0" + find-up "^4.1.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^4.2.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^18.1.2" diff --git a/main.py b/main.py index 9b8e962..c074fa8 100644 --- a/main.py +++ b/main.py @@ -2,61 +2,35 @@ Author: 于小丘 海枫 Date: 2024-10-02 15:23:34 LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-11-29 20:04:41 FilePath: /Findreve/main.py -Description: Findreve +Description: 标记、追踪与找回 —— 就这么简单。 -Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved. +Copyright (c) 2018-2025 by 于小丘Yuerchu, All Rights Reserved. ''' -from nicegui import app, ui -import model.database -import asyncio -asyncio.run(model.database.Database().init_db()) -import notfound -from routes.frontend import main_page -from routes.frontend import found -from routes.frontend import login -from routes.frontend.admin import home -from routes.frontend.admin import auth -from routes.frontend.admin import about -from routes.frontend.admin import items -from routes.backend import session -from routes.backend import admin -from routes.backend import object -import model +# 导入库 +from app import app +from fastapi.staticfiles import StaticFiles + +from routes import (session, admin, object) import logging -notfound.create() -main_page.create() -found.create() -login.create() -home.create() -auth.create() -about.create() -items.create() - +# 挂载路由 app.include_router(admin.Router) app.include_router(session.Router) app.include_router(object.Router) # 添加静态文件目录 try: - app.add_static_files(url_path='/static', local_directory='static') + app.mount("/static", StaticFiles(directory="static"), name="static") except RuntimeError: logging.error('无法挂载静态目录') -# 启动函数 Startup function -def startup(): - asyncio.run(model.database.Database().init_db()) - ui.run( +# 作为主程序启动时 +if __name__ == '__main__': + import uvicorn + uvicorn.run( + 'main:app', host='0.0.0.0', - favicon='🚀', port=8080, - title='Findreve', - native=False, - language='zh-CN', - fastapi_docs=True) - -if __name__ in {"__main__", "__mp_main__"}: - startup() \ No newline at end of file + reload=True) \ No newline at end of file diff --git a/model/database.py b/model/database.py index e44c99f..d8eba9b 100644 --- a/model/database.py +++ b/model/database.py @@ -15,6 +15,7 @@ import tool import logging from typing import Optional +# 数据库类 class Database: def __init__(self, db_path: str = "data.db"): self.db_path = db_path @@ -103,7 +104,8 @@ class Database: logging.info("数据库初始化完成并提交更改") async def add_object(self, key: str, name: str, icon: str = None, phone: str = None): - """添加新对象 + """ + 添加新对象 :param key: 序列号 :param name: 名称 @@ -134,7 +136,8 @@ class Database: lost_description: Optional[str] = None, find_ip: Optional[str] = None, lost_time: Optional[str] = None): - """更新对象信息 + """ + 更新对象信息 :param id: 对象ID :param key: 序列号 @@ -171,7 +174,8 @@ class Database: await db.commit() async def get_object(self, id: int = None, key: str = None): - """获取对象 + """ + 获取对象 :param id: 对象ID :param key: 序列号 @@ -187,7 +191,8 @@ class Database: return await cursor.fetchall() async def delete_object(self, id: int): - """删除对象 + """ + 删除对象 :param id: 对象ID """ @@ -196,7 +201,8 @@ class Database: await db.commit() async def set_setting(self, name: str, value: str): - """设置配置项 + """ + 设置配置项 :param name: 配置项名称 :param value: 配置项值 @@ -209,7 +215,8 @@ class Database: await db.commit() async def get_setting(self, name: str): - """获取配置项 + """ + 获取配置项 :param name: 配置项名称 """ diff --git a/model/response.py b/model/response.py index 23bb85a..f6211f8 100644 --- a/model/response.py +++ b/model/response.py @@ -1,6 +1,16 @@ from pydantic import BaseModel +from typing import Literal, Optional class DefaultResponse(BaseModel): code: int = 0 data: dict | list | None = None - msg: str = "" \ No newline at end of file + msg: str = "" + +class ObjectData(BaseModel): + id: int + key: str + name: str + icon: str + status: Literal['ok', 'lost'] + phone: str + context: Optional[str] = None \ No newline at end of file diff --git a/notfound.py b/notfound.py deleted file mode 100644 index 4a99618..0000000 --- a/notfound.py +++ /dev/null @@ -1,19 +0,0 @@ -''' -Author: 于小丘 海枫 -Date: 2024-10-02 15:23:34 -LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-11-29 20:05:13 -FilePath: /Findreve/notfound.py -Description: Findreve 404 - -Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved. -''' - -from nicegui import app -from fastapi import Request -from fastapi.responses import HTMLResponse - -def create() -> None: - @app.get('/404') - async def not_found_page(request: Request) -> HTMLResponse: - return HTMLResponse(status_code=404) diff --git a/routes/backend/__init__.py b/routes/__init__.py similarity index 100% rename from routes/backend/__init__.py rename to routes/__init__.py diff --git a/routes/admin.py b/routes/admin.py new file mode 100644 index 0000000..ece3000 --- /dev/null +++ b/routes/admin.py @@ -0,0 +1,177 @@ +from fastapi import APIRouter +from typing import Annotated, Literal, Optional +from fastapi import Depends, Query +from fastapi import HTTPException +import JWT +from model import database +from model.response import DefaultResponse +from model.items import Item + +# 验证是否为管理员 +async def is_admin(token: Annotated[str, Depends(JWT.oauth2_scheme)]) -> Literal[True]: + ''' + 验证是否为管理员。 + + 使用方法: + >>> APIRouter(dependencies=[Depends(is_admin)]) + ''' + return True + +Router = APIRouter( + prefix='/api/admin', + tags=['管理员 Admin'], + dependencies=[Depends(is_admin)] +) + +@Router.get( + path='/', + summary='验证管理员身份', + description='返回管理员身份验证结果', + response_model=DefaultResponse, + response_description='当前为管理员' +) +async def verity_admin( + is_admin: Annotated[str, Depends(is_admin)] +) -> Literal[True]: + ''' + 使用 API 验证是否为管理员。 + + - 若为管理员,返回 `True` + - 若不是管理员,抛出 `401` 错误 + ''' + return is_admin + +@Router.get( + path='/items', + summary='获取物品信息', + description='返回物品信息列表', + response_model=DefaultResponse, + response_description='物品信息列表' +) +async def get_items( + id: Optional[int] = Query(default=None, ge=1, description='物品ID'), + key: Optional[str] = Query(default=None, description='物品序列号')): + ''' + 获得物品信息。 + + 不传参数返回所有信息,否则可传入 `id` 或 `key` 进行筛选。 + ''' + results = await database.Database().get_object(id=id, key=key) + + if results is not None: + if not isinstance(results, list): + items = [results] + else: + items = results + item = [] + for i in items: + item.append(Item( + id=i[0], + key=i[1], + name=i[2], + icon=i[3], + status=i[4], + phone=i[5], + lost_description=i[6], + find_ip=i[7], + create_time=i[8], + lost_time=i[9] + )) + return DefaultResponse(data=item) + else: + return DefaultResponse(data=[]) + +@Router.post( + path='/items', + summary='添加物品信息', + description='添加新的物品信息', + response_model=DefaultResponse, + response_description='添加物品成功' +) +async def add_items( + key: str, + name: str, + icon: str, + phone: str) -> DefaultResponse: + ''' + 添加物品信息。 + + - **key**: 物品的关键字 + - **name**: 物品的名称 + - **icon**: 物品的图标 + - **phone**: 联系电话 + ''' + + try: + await database.Database().add_object( + key=key, name=name, icon=icon, phone=phone) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + else: + return DefaultResponse() + +@Router.patch( + path='/items', + summary='更新物品信息', + description='更新现有物品的信息', + response_model=DefaultResponse, + response_description='更新物品成功' +) +async def update_items( + id: int = Query(ge=1), + key: Optional[str] = None, + name: Optional[str] = None, + icon: Optional[str] = None, + status: Optional[str] = None, + phone: Optional[int] = None, + lost_description: Optional[str] = None, + find_ip: Optional[str] = None, + lost_time: Optional[str] = None) -> DefaultResponse: + ''' + 更新物品信息。 + + 只有 `id` 是必填参数,其余参数都是可选的,在不传入任何值的时候将不做任何更改。 + + - **id**: 物品的ID + - **key**: 物品的序列号 **不建议修改此项,这样会导致生成的物品二维码直接失效** + - **name**: 物品的名称 + - **icon**: 物品的图标 + - **status**: 物品的状态 + - **phone**: 联系电话 + - **lost_description**: 物品丢失描述 + - **find_ip**: 找到物品的IP + - **lost_time**: 物品丢失时间 + + ''' + try: + await database.Database().update_object( + id=id, + key=key, name=name, icon=icon, status=status, phone=phone, + lost_description=lost_description, find_ip=find_ip, + lost_time=lost_time + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + else: + return DefaultResponse() + +@Router.delete( + path='/items', + summary='删除物品信息', + description='删除指定的物品信息', + response_model=DefaultResponse, + response_description='删除物品成功' +) +async def delete_items( + id: int) -> DefaultResponse: + ''' + 删除物品信息。 + + - **id**: 物品的ID + ''' + try: + await database.Database().delete_object(id=id) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + else: + return DefaultResponse() \ No newline at end of file diff --git a/routes/backend/admin.py b/routes/backend/admin.py deleted file mode 100644 index 6c7d821..0000000 --- a/routes/backend/admin.py +++ /dev/null @@ -1,109 +0,0 @@ -from fastapi import APIRouter -from typing import Annotated, Optional -from fastapi import Depends -from fastapi import HTTPException, status -from jwt import InvalidTokenError -import jwt, JWT -from model import database -from model.response import DefaultResponse -from model.items import Item - -async def is_admin(token: Annotated[str, Depends(JWT.oauth2_scheme)]): - credentials_exception = HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Login required", - headers={"WWW-Authenticate": "Bearer"}, - ) - try: - payload = jwt.decode(token, JWT.SECRET_KEY, algorithms=["HS256"]) - username = payload.get("sub") - if username is None: - raise credentials_exception - except InvalidTokenError: - raise credentials_exception - if not username == await database.Database().get_setting('account'): - raise credentials_exception - return True - -Router = APIRouter( - prefix='/api/admin', - tags=['admin'], - dependencies=[Depends(is_admin)] -) - -@Router.get('/') -async def verity_admin( - is_admin: Annotated[str, Depends(is_admin)] -): - return is_admin - -@Router.get('/items') -async def get_items( - id: Optional[int] = None, - key: Optional[str] = None): - results = await database.Database().get_object(id=id, key=key) - - if results is not None: - if not isinstance(results, list): - items = [results] - else: - items = results - item = [] - for i in items: - item.append(Item( - id=i[0], - key=i[1], - name=i[2], - icon=i[3], - status=i[4], - phone=i[5], - lost_description=i[6], - find_ip=i[7], - create_time=i[8], - lost_time=i[9] - )) - return DefaultResponse(data=item) - else: - return DefaultResponse(data=[]) - -@Router.post('/items') -async def add_items( - key: str, - name: str, - icon: str, - phone: str): - await database.Database().add_object( - key=key, name=name, icon=icon, phone=phone) - -@Router.patch('/items') -async def update_items( - id: int, - key: Optional[str] = None, - name: Optional[str] = None, - icon: Optional[str] = None, - status: Optional[str] = None, - phone: Optional[int] = None, - lost_description: Optional[str] = None, - find_ip: Optional[str] = None, - lost_time: Optional[str] = None): - try: - await database.Database().update_object( - id=id, - key=key, name=name, icon=icon, status=status, phone=phone, - lost_description=lost_description, find_ip=find_ip, - lost_time=lost_time - ) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - else: - return DefaultResponse() - -@Router.delete('/items') -async def delete_items( - id: int): - try: - await database.Database().delete_object(id=id) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - else: - return DefaultResponse() \ No newline at end of file diff --git a/routes/frontend/admin/__init__.py b/routes/frontend/admin/__init__.py deleted file mode 100644 index 60a2590..0000000 --- a/routes/frontend/admin/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from . import about -from . import auth -from . import home -from . import items \ No newline at end of file diff --git a/routes/frontend/admin/about.py b/routes/frontend/admin/about.py deleted file mode 100644 index 736bbb0..0000000 --- a/routes/frontend/admin/about.py +++ /dev/null @@ -1,43 +0,0 @@ -''' -Author: 于小丘 海枫 -Date: 2024-10-02 15:23:34 -LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-12-14 20:03:49 -FilePath: /Findreve/admin.py -Description: Findreve 后台管理 admin - -Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved. -''' - -from nicegui import ui -from fastapi import Request -from tool import * -from ..framework import frame - - -def create(): - @ui.page('/admin/about') - async def admin_about(request: Request): - ui.add_head_html(""" - - - """) - - async with frame(request=request): - - # 关于 Findreve - with ui.tab_panel('about'): - ui.label('关于 Findreve').classes('text-2xl font-bold') - about = ui.markdown('''加载中...''') - - try: - # 延长超时时间到10秒 - about_text = await ui.run_javascript('get_about()', timeout=10.0) - if isinstance(about_text, dict) and 'status' in about_text and about_text['status'] == 'failed': - about.set_content(f'加载失败: {about_text.get("detail", "未知错误")}') - else: - about.set_content(about_text) - except Exception as e: - ui.notify(f'加载失败: {str(e)}', color='negative') - about.set_content(f'### 无法加载内容\n\n出现错误: {str(e)}') - \ No newline at end of file diff --git a/routes/frontend/admin/auth.py b/routes/frontend/admin/auth.py deleted file mode 100644 index b948cd4..0000000 --- a/routes/frontend/admin/auth.py +++ /dev/null @@ -1,41 +0,0 @@ -''' -Author: 于小丘 海枫 -Date: 2024-10-02 15:23:34 -LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-12-14 20:03:49 -FilePath: /Findreve/admin.py -Description: Findreve 后台管理 admin - -Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved. -''' - -from nicegui import ui -from tool import * -from fastapi import Request -from ..framework import frame - - -def create(): - @ui.page('/admin/auth') - async def admin_auth(request: Request): - # Findreve 授权 - async with frame(request=request): - - ui.label('Findreve 授权').classes('text-2xl text-bold') - - with ui.element('div').classes('p-2 bg-orange-100 w-full'): - with ui.row(align_items='center'): - ui.icon('favorite').classes('text-rose-500 text-2xl') - ui.label('感谢您使用 Findreve').classes('text-rose-500 text-bold') - with ui.column(): - ui.markdown('> 使用付费版本请在下方进行授权验证' - '
' - 'Findreve 是一款良心、厚道的好产品!创作不易,支持正版,从我做起!' - '
' - '如需在生产环境部署请前往 `auth.yxqi.cn` 购买正版' - ).classes('text-rose-500') - ui.markdown('- Findreve 官网:[https://auth.yxqi.cn](https://auth.yxqi.cn)\n' - '- 作者联系方式:QQ 2372526808\n' - '- 管理我的授权:[https://auth.yxqi.cn/product/5](https://auth.yxqi.cn/product/5)\n' - ).classes('text-rose-500') - ui.label('您正在使用免费版本,无需授权可体验完整版Findreve。').classes('text-bold') \ No newline at end of file diff --git a/routes/frontend/admin/home.py b/routes/frontend/admin/home.py deleted file mode 100644 index f9bd8c6..0000000 --- a/routes/frontend/admin/home.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Author: 于小丘 海枫 -Date: 2024-10-02 15:23:34 -LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-12-14 20:03:49 -FilePath: /Findreve/admin.py -Description: Findreve 后台管理 admin - -Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved. -''' - -from nicegui import ui, app -from fastapi import Request -from fastapi.responses import RedirectResponse -from tool import * -from ..framework import frame - - -def create(): - @app.get('/admin') - async def jump(): - return RedirectResponse(url='/admin/home') - - @ui.page('/admin/home') - async def admin_home(request: Request): - async with frame(request=request): - with ui.tab_panel('main_page'): - ui.label('首页配置').classes('text-2xl text-bold') - ui.label('暂不支持,请直接修改main_page.py').classes('text-md text-gray-600').classes('w-full') \ No newline at end of file diff --git a/routes/frontend/admin/items.py b/routes/frontend/admin/items.py deleted file mode 100644 index 046d97a..0000000 --- a/routes/frontend/admin/items.py +++ /dev/null @@ -1,453 +0,0 @@ -''' -Author: 于小丘 海枫 -Date: 2024-10-02 15:23:34 -LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-12-14 20:03:49 -FilePath: /Findreve/admin.py -Description: Findreve 后台管理 admin - -Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved. -''' - -import asyncio -from nicegui import ui -from typing import Dict -import qrcode -import base64 -import json -from io import BytesIO -from fastapi import Request -from tool import * -from ..framework import frame, loding_process - - -def create(): - @ui.page('/admin/items') - async def admin_items(request: Request): - - ui.add_head_html(""" - - """) - - dark_mode = ui.dark_mode(value=True) - - async with frame(): - # 表格列的显示隐藏开关 - def tableToggle(column: Dict, visible: bool, table) -> None: - column['classes'] = '' if visible else 'hidden' - column['headerClasses'] = '' if visible else 'hidden' - table.update() - - # 列表选择函数 - async def objectTableOnSelect(): - try: - status = str(object_table.selected[0]['status']) - except: - status = None - # 刷新FAB按钮状态 - if status: - # 选中正常物品,显示编辑按钮 - addObjectFAB.set_visibility(False) - editObjectFAB.set_visibility(True) - else: - addObjectFAB.set_visibility(True) - editObjectFAB.set_visibility(False) - - try: - # 预填充编辑表单 - if object_table.selected: - selected_item = object_table.selected[0] - edit_object_name.set_value(selected_item.get('name', '')) - edit_object_icon.set_value(selected_item.get('icon', '')) - edit_object_phone.set_value(selected_item.get('phone', '')) - edit_object_key.set_value(selected_item.get('key', '')) - # 设置丢失状态开关 - edit_set_object_lost.set_value(selected_item.get('status') == '丢失') - # 设置物主留言 - lostReason.set_value(selected_item.get('lost_description', '')) - except: - # 当物品列表未选中,显示添加物品按钮,其他按钮不显示 - addObjectFAB.set_visibility(True) - return - - # 添加物品 - async def addObject(): - dialogAddObjectIcon.disable() - - async def on_success(): - await reloadTable(tips=False) - - # 清空输入框 - object_name.set_value('') - object_icon.set_value('') - object_phone.set_value('') - object_key.set_value('') - - with ui.dialog() as addObjectSuccessDialog, ui.card().style('width: 90%; max-width: 500px'): - ui.button(icon='done').props('outline round').classes('mx-auto w-auto shadow-sm w-fill') - ui.label('添加成功').classes('w-full text-h5 text-center') - - ui.label('你可以使用下面的链接来访问这个物品') - ui.code(request.base_url.hostname+ '/found?key=' + object_key.value).classes('w-full') - - # 生成二维码 - qr_data = request.base_url.hostname + '/found?key=' + object_key.value - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, - ) - qr.add_data(qr_data) - qr.make(fit=True) - img = qr.make_image(fill='black', back_color='white') - - # 将二维码转换为Base64 - buffered = BytesIO() - img.save(buffered, format="PNG") - img_str = base64.b64encode(buffered.getvalue()).decode() - - # 展示二维码 - with ui.row(align_items='center').classes('w-full'): - ui.space() - ui.image(f'data:image/png;base64,{img_str}').classes('w-1/3') - ui.space() - - with ui.row(align_items='center').classes('w-full'): - ui.space() - - ui.button("下载二维码", on_click=lambda: ui.download(buffered.getvalue(), 'qrcode.png')) \ - .props('flat rounded') - - ui.button("返回", on_click=lambda: (addObjectDialog.close(), addObjectSuccessDialog.close(), addObjectSuccessDialog.delete())) \ - .props('flat rounded') - - addObjectSuccessDialog.open() - - if object_name.value == "" or object_icon == "" or object_phone == "": - ui.notify('必填字段不能为空', color='negative') - dialogAddObjectIcon.enable() - return - - if not object_phone.validate(): - ui.notify('号码输入有误,请检查!', color='negative') - dialogAddObjectIcon.enable() - return - - if object_key.value == "": - object_key.set_value(generate_password()) - - async with loding_process( - success_content='添加成功', - on_success=on_success, - on_finally=dialogAddObjectIcon.enable() - ): - # 正确序列化字符串参数 - key = json.dumps(object_key.value) - name = json.dumps(object_name.value) - icon = json.dumps(object_icon.value) - phone = json.dumps(object_phone.value) - - result = await ui.run_javascript( - f'addItems({key}, {name}, {icon}, {phone})' - ) - - if result.get('status') == 'failed': - raise Exception(f"添加失败: {result.get('detail', '未知错误')}") - - # 添加物品对话框 - with ui.dialog() as addObjectDialog, ui.card().style('width: 90%; max-width: 500px'): - ui.button(icon='add_circle').props('outline round').classes('mx-auto w-auto shadow-sm w-fill') - ui.label('添加物品').classes('w-full text-h5 text-center') - - with ui.scroll_area().classes('w-full'): - object_name = ui.input('物品名称').classes('w-full') - ui.label('显示的物品名称').classes('-mt-3') - with ui.row(align_items='center').classes('w-full'): - with ui.column().classes('w-1/2 flex-grow'): - object_icon = ui.input('物品图标').classes('w-full') - with ui.row(align_items='center').classes('-mt-3'): - ui.label('将在右侧实时预览图标') - ui.link('图标列表', 'https://fonts.google.com/icons?icon.set=Material+Icons') - ui.icon('').classes('text-2xl flex-grow').bind_name_from(object_icon, 'value') - object_phone = ui.input('物品绑定手机号', validation={'请输入中国大陆格式的11位手机号': lambda value: len(value) == 11 and value.isdigit()}).classes('w-full') - ui.label('目前仅支持中国大陆格式的11位手机号').classes('-mt-3') - object_key = ui.input('物品Key(可选,不填自动生成)').classes('w-full') - ui.label('物品Key为物品的唯一标识,可用于物品找回').classes('-mt-3') - - async def handle_add_object(): - await addObject() - - dialogAddObjectIcon = ui.button("添加并生成二维码", icon='qr_code', on_click=handle_add_object) \ - .classes('items-center w-full').props('rounded') - ui.button("返回", on_click=addObjectDialog.close) \ - .classes('w-full').props('flat rounded') - - async def editObjectPrepare(): - ''' - 读取选中物品的ID,并预填充编辑表单 - ''' - try: - # 获取选中物品的ID - item_id = str(object_table.selected[0]['id']) - id_json = json.dumps(item_id) - - result: dict = await ui.run_javascript(f'getItem({id_json})') - - if result.get('status') == 'failed': - ui.notify(f"获取物品信息失败: {result.get('detail', '未知错误')}", color='negative') - return - - except Exception as e: - ui.notify(f"操作失败: {str(e)}", color='negative') - return - else: - result = result['data']['data'][0] - - # 预填充编辑表单 - edit_object_name.set_value(result['name']) - edit_object_icon.set_value(result['icon']) - edit_object_phone.set_value(result['phone']) - edit_object_key.set_value(result['key']) - edit_set_object_lost.set_value(result['status'] == 'lost') - lostReason.set_value(result['lost_description']) - editObjectDialog.open() - - async def editObject(): - dialogEditObjectIcon.disable() - - async def on_success(): - await reloadTable(tips=False) - - edit_object_name.set_value('') - edit_object_icon.set_value('') - edit_object_phone.set_value('') - edit_object_key.set_value('') - lostReason.set_value('') - edit_set_object_lost.set_value(False) - - editObjectDialog.close() - - if edit_object_name.value == "" or edit_object_icon.value == "" or edit_object_phone.value == "": - ui.notify('必填字段不能为空', color='negative') - dialogEditObjectIcon.enable() - return - - if not edit_object_phone.validate(): - ui.notify('号码输入有误,请检查!', color='negative') - dialogEditObjectIcon.enable() - return - - if edit_object_key.value == "": - ui.notify('物品Key不能为空', color='negative') - dialogEditObjectIcon.enable() - return - - async with loding_process( - success_content='更新成功', - on_success=on_success, - on_error=dialogEditObjectIcon.enable(), - on_finally=dialogEditObjectIcon.enable() - ): - # 获取选中物品的ID - item_id = str(object_table.selected[0]['id']) - - # 正确序列化字符串参数 - id_json = json.dumps(item_id) - key = json.dumps(edit_object_key.value) - name = json.dumps(edit_object_name.value) - icon = json.dumps(edit_object_icon.value) - phone = json.dumps(edit_object_phone.value) - - # 处理状态和物主留言 - status = json.dumps('lost' if edit_set_object_lost.value else 'ok') - context = json.dumps(lostReason.value if edit_set_object_lost.value else '') - - result = await ui.run_javascript( - f'updateItems({id_json}, {key}, {name}, {icon}, {phone}, {status}, {context})' - ) - - if result.get('status') == 'failed': - raise Exception(f"更新失败: {result.get('detail', '未知错误')}") - - - # 编辑物品对话框 - with ui.dialog() as editObjectDialog, ui.card().style('width: 90%; max-width: 500px'): - ui.button(icon='edit').props('outline round').classes('mx-auto w-auto shadow-sm w-fill') - ui.label('编辑物品信息').classes('w-full text-h5 text-center') - - with ui.scroll_area().classes('w-full'): - edit_object_name = ui.input('物品名称').classes('w-full') - ui.label('显示的物品名称').classes('-mt-3') - with ui.row(align_items='center').classes('w-full'): - with ui.column().classes('w-1/2 flex-grow'): - edit_object_icon = ui.input('物品图标').classes('w-full') - with ui.row(align_items='center').classes('-mt-3'): - ui.label('将在右侧实时预览图标') - ui.link('图标列表', 'https://fonts.google.com/icons?icon.set=Material+Icons') - ui.icon('').classes('text-2xl flex-grow').bind_name_from(edit_object_icon, 'value') - edit_object_phone = ui.input('物品绑定手机号').classes('w-full') - ui.label('目前仅支持中国大陆格式的11位手机号').classes('-mt-3') - edit_object_key = ui.input('物品Key').classes('w-full').props('readonly') - ui.label('物品Key为物品的唯一标识,不可修改').classes('-mt-3') - - edit_set_object_lost = ui.switch('设置物品状态为丢失').classes('w-full') - ui.label('确定要设置这个物品为丢失吗?').bind_visibility_from(edit_set_object_lost, 'value').classes('-mt-3') - ui.html('设置为丢失以后,你的电话号码将会被完整地显示在物品页面(不是“*** **** 8888”而是“188 8888 8888”),以供拾到者能够记下你的电话号码。此外,在页面底部将会显示一个按钮,这个按钮能够一键拨打预先设置好的电话。').bind_visibility_from(edit_set_object_lost, 'value').classes('-mt-3') - lostReason = ui.input('物主留言') \ - .classes('block w-full text-gray-900').bind_visibility_from(edit_set_object_lost, 'value').classes('-mt-3') - ui.label('非必填,但建议填写,以方便拾到者联系你').classes('-mt-3').bind_visibility_from(edit_set_object_lost, 'value').classes('-mt-3') - - ui.separator().classes('my-4') - with ui.card().classes('w-full bg-red-50 dark:bg-red-900 q-pa-md'): - ui.label('危险区域').classes('text-red-500 font-bold') - ui.button('删除物品', icon='delete_forever') \ - .classes('w-full text-red-500').props('flat').on_click(lambda: delete_confirmation_dialog.open()) - - async def handle_edit_object(): - await editObject() - - dialogEditObjectIcon = ui.button("确认提交", on_click=handle_edit_object) \ - .classes('items-center w-full').props('rounded') - ui.button("返回", on_click=editObjectDialog.close) \ - .classes('w-full').props('flat rounded') - - # 删除确认对话框 - with ui.dialog() as delete_confirmation_dialog, ui.card().style('width: 90%; max-width: 500px'): - ui.button(icon='warning').props('outline round').classes('mx-auto w-auto shadow-sm w-fill text-red-500') - ui.label('确认删除物品').classes('w-full text-h5 text-center text-red-500') - ui.label('此操作不可撤销,删除后物品数据将永久丢失!').classes('w-full text-center text-red-500') - - async def handle_delete_item(): - - async def on_success(): - await reloadTable(tips=False) - delete_confirmation_dialog.close() - editObjectDialog.close() - - async with loding_process( - success_content='物品已删除', - on_success=on_success - ): - # 获取选中物品的ID - item_id = str(object_table.selected[0]['id']) - id_json = json.dumps(item_id) - - result = await ui.run_javascript(f'deleteItem({id_json})') - - if result.get('status') == 'failed': - raise Exception(f"删除失败: {result.get('detail', '未知错误')}") - - with ui.row().classes('w-full'): - ui.space() - ui.button('取消', icon='close', on_click=delete_confirmation_dialog.close).props('flat') - ui.button('确认删除', icon='delete_forever', on_click=handle_delete_item).classes('text-red-500').props('flat') - - async def fetch_and_process_objects(): - """获取并处理所有物品数据""" - try: - # 调用前端JavaScript获取数据 - response = await ui.run_javascript('getItems()') - - if response['status'] == 'failed': - if str(response['detail']).find('Unauthorized'): - ui.notify('未登录或登录已过期,请重新登录', color='negative') - await asyncio.sleep(2) - ui.navigate.to('/login?redirect_to=/admin/items') - ui.notify(response['detail'], color='negative') - return [] - - # 从response中提取数据 - raw_data = response.get('data', {}) - objects = raw_data.get('data', []) if isinstance(raw_data, dict) else raw_data - - # 进行数据处理,类似旧版本的逻辑 - status_map = {'ok': '正常', 'lost': '丢失'} - processed_objects = [] - - for obj in objects: - # 确保obj是字典 - if isinstance(obj, dict): - # 复制对象避免修改原始数据 - processed_obj = obj.copy() - - # 状态映射 - if 'status' in processed_obj: - processed_obj['status'] = status_map.get(processed_obj['status'], processed_obj['status']) - - # 时间格式化 - if 'create_time' in processed_obj and processed_obj['create_time']: - processed_obj['create_time'] = format_time_diff(processed_obj['create_time']) - - if 'lost_time' in processed_obj and processed_obj['lost_time']: - processed_obj['lost_time'] = format_time_diff(processed_obj['lost_time']) - - processed_objects.append(processed_obj) - - return processed_objects - except Exception as e: - ui.notify(f"获取数据失败: {str(e)}", color='negative') - print(f"Error in fetch_and_process_objects: {str(e)}") - return [] - - async def reloadTable(tips: bool = True): - objects = await fetch_and_process_objects() - object_table.update_rows(objects) - if tips: - ui.notify('刷新成功') - - object_columns = [ - {'name': 'id', 'label': '内部ID', 'field': 'id', 'required': True, 'align': 'left'}, - {'name': 'key', 'label': '物品Key', 'field': 'key', 'required': True, 'align': 'left'}, - {'name': 'name', 'label': '物品名称', 'field': 'name', 'required': True, 'align': 'left'}, - {'name': 'icon', 'label': '物品图标', 'field': 'icon', 'required': True, 'align': 'left'}, - {'name': 'status', 'label': '物品状态', 'field': 'status', 'required': True, 'align': 'left'}, - {'name': 'phone', 'label': '物品绑定手机', 'field': 'phone', 'required': True, 'align': 'left'}, - {'name': 'context', 'label': '丢失描述', 'field': 'context', 'required': True, 'align': 'left'}, - {'name': 'find_ip', 'label': '物品发现IP', 'field': 'find_ip', 'required': True, 'align': 'left'}, - {'name': 'create_time', 'label': '物品创建时间', 'field': 'create_time', 'required': True, 'align': 'left'}, - {'name': 'lost_time', 'label': '物品丢失时间', 'field': 'lost_time', 'required': True, 'align': 'left'} - ] - - objects = await fetch_and_process_objects() - object_table = ui.table( - title='物品 & 库存', - row_key='id', - pagination=10, - selection='single', - columns=object_columns, - rows=objects, - # on_select=lambda: objectTableOnSelect() - ).classes('w-full').props('flat') - - object_table.add_slot('body-cell-status', ''' - - - {{ props.value }} - - - ''') - - - with object_table.add_slot('top-right'): - - ui.input('搜索物品').classes('px-2') \ - .bind_value(object_table, 'filter') \ - .props('rounded outlined dense clearable') - - ui.button(icon='refresh', on_click=lambda: reloadTable()).classes('px-2').props('flat fab-mini') - - with ui.button(icon='menu').classes('px-2').props('flat fab-mini'): - with ui.menu(), ui.column().classes('gap-0 p-4'): - for column in object_columns: - ui.switch(column['label'], value=True, on_change=lambda e, - column=column: tableToggle(column=column, visible=e.value, table=object_table)) - # FAB按钮 - with ui.page_sticky(x_offset=24, y_offset=24) \ - .bind_visibility_from(object_table, 'selected', backward=lambda x: not x) as addObjectFAB: - ui.button(icon='add', on_click=addObjectDialog.open) \ - .props('fab') - with ui.page_sticky(x_offset=24, y_offset=24) \ - .bind_visibility_from(addObjectFAB, 'visible', backward=lambda x: not x) as editObjectFAB: - ui.button(icon='edit', on_click=editObjectPrepare) \ - .props('fab') \ No newline at end of file diff --git a/routes/frontend/found.py b/routes/frontend/found.py deleted file mode 100644 index 2d796c0..0000000 --- a/routes/frontend/found.py +++ /dev/null @@ -1,124 +0,0 @@ -''' -Author: 于小丘 海枫 -Date: 2024-10-02 15:23:34 -LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-11-29 20:03:58 -FilePath: /Findreve/found.py -Description: Findreve 物品详情页 found - -Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved. -''' - -from nicegui import ui -from fastapi import Request -from .framework import frame -from tool import format_phone - -def create_header(object_data, status): - """创建卡片标题部分""" - icon_color = 'red' if status == 'lost' else None - ui.button(icon=object_data[3], color=icon_color).props('outline round').classes('mx-auto w-auto shadow-sm w-fill max-md:hidden') - - title = ui.label('关于此 '+ object_data[2]).classes('text-h5 w-full text-center') - if status == 'lost': - with title: - ui.badge('已被标记为丢失 Already lost', color='red').classes('text-lg -right-10').props('floating') - - ui.label('About this '+ object_data[2]).classes('text-xs w-full text-center text-gray-500 -mt-3') - -def create_basic_info(object_data): - """创建基本信息部分""" - ui.label('序列号(Serial number):'+ object_data[1]).classes('text-md w-full text-center -mt-1') - - # 根据状态决定是否隐藏手机号 - is_private = object_data[4] != 'ok' and object_data[4] != 'lost' - ui.label('物主(Owner):'+ format_phone(object_data[5], private=is_private)).classes('text-md w-full text-center -mt-3') - - # 丢失时间(如果有) - if object_data[4] == 'lost' and len(object_data) > 9 and object_data[9]: - ui.label('丢失时间(Lost time):'+ object_data[9]).classes('text-md w-full text-center -mt-3') - -def create_status_message(object_data, status): - """根据状态创建提示信息""" - ui.space() - - # 如果是丢失状态且有留言,显示留言 - if status == 'lost' and len(object_data) > 6 and object_data[6]: - ui.label('物主留言(Owner message):'+ object_data[6]).classes('text-md w-full text-center') - ui.space() - - messages = { - 'ok': ('此物品尚未标记为丢失状态。如果你意外捡到了此物品,请尽快联系物主。', - 'This item has not been marked as lost. If you accidentally picked it up, please contact the owner as soon as possible.'), - 'lost': ('此物品已被物主标记为丢失。您可以通过上面的电话号码来联系物主。', - 'This item has been marked as lost by the owner. You can contact the owner through the phone number above.'), - 'default': ('此物品状态信息已丢失。如果您捡到了这个物品,请尽快联系物主。如果你是物主,请修改物品信息状态。', - 'The item status information has been lost. If you have found this item, please contact the owner as soon as possible. If you are the owner, please modify the item status information.') - } - - msg = messages.get(status, messages['default']) - ui.label(msg[0]).classes('text-md w-full text-center') - ui.label(msg[1]).classes('text-xs w-full text-center text-gray-500 -mt-3') - -def create_contact_button(phone_number): - """创建联系按钮""" - if phone_number: - ui.button('联系物主', - on_click=lambda: ui.navigate.to('tel:' + phone_number)) \ - .classes('items-center w-full').props('rounded') - -def display_item_card(object_data): - """显示物品信息卡片""" - status = object_data[4] - - with ui.card().classes('absolute-center w-3/4 h-3/4'): - # 创建卡片各部分 - create_header(object_data, status) - create_basic_info(object_data) - create_status_message(object_data, status) - - # 只有状态为'ok'或'lost'时显示联系按钮 - if status in ['ok', 'lost']: - create_contact_button(object_data[5]) - -def create() -> None: - @ui.page('/found') - async def found_page(request: Request, key: str = "") -> None: - - ui.add_head_html( - ''' - - - ''' - ) - - await ui.context.client.connected() - - async with frame(page='found', request=request): - if key == "" or key == None: - ui.navigate.to('/404') - return - - # 加载dialog - with ui.dialog().props('persistent') as loading, ui.card(): - with ui.row(align_items='center'): - ui.spinner(size='lg') - with ui.column(): - ui.label('数据加载中...') - ui.label('Loading...').classes('text-xs text-gray-500 -mt-3') - - loading.open() - - try: - object_data = await ui.run_javascript(f'getObject("{key}")') - - if object_data['status'] != 'success': - ui.navigate.to('/404') - else: - object_data = object_data['data'] - display_item_card(object_data) - except Exception as e: - ui.notify(f'加载失败: {str(e)}', color='negative') - ui.navigate.to('/404') - finally: - loading.close() \ No newline at end of file diff --git a/routes/frontend/framework.py b/routes/frontend/framework.py deleted file mode 100644 index bb0d67e..0000000 --- a/routes/frontend/framework.py +++ /dev/null @@ -1,114 +0,0 @@ -from contextlib import asynccontextmanager -from nicegui import ui -import asyncio -from fastapi import Request -from typing import Optional, Literal - -@asynccontextmanager -async def frame( - request: Request = None, - page: Literal['admin', 'session', 'found'] = 'admin', - redirect_to: str = None -): - - ui.add_head_html(""" - - """) - - await ui.context.client.connected() - - is_login = await ui.run_javascript('is_login()', timeout=3) - if str(is_login).lower() != 'true': - if page not in ['session', 'found']: - ui.navigate.to(f'/login?redirect_to={request.url.path}') - else: - if page == 'session': - ui.navigate.to(redirect_to) - - if page != 'found': - ui.dark_mode(value=True) - - with ui.header() \ - .classes('items-center py-2 px-5 no-wrap').props('elevated'): - ui.button(icon='menu', on_click=lambda: left_drawer.toggle()).props('flat color=white round') - ui.button(text="Findreve 仪表盘" if page == 'admin' else "Findreve").classes('text-lg').props('flat color=white no-caps') - - ui.space() - - if str(is_login).lower() == 'true': - ui.button(icon='logout', on_click=lambda: ui.run_javascript('logout()')) \ - .props('flat color=white fab-mini').tooltip('退出登录') - - with ui.left_drawer() as left_drawer: - with ui.column(align_items='center').classes('w-full'): - ui.image('/static/Findreve.png').classes('w-1/3 mx-auto') - ui.label('Findreve').classes('text-2xl text-bold') - ui.label("免费版,无需授权").classes('text-sm text-gray-500') - - - if page == 'admin': - ui.button('首页 & 信息', icon='fingerprint', on_click=lambda: ui.navigate.to('/admin/home')) \ - .classes('w-full').props('flat no-caps') - ui.button('物品 & 库存', icon='settings', on_click=lambda: ui.navigate.to('/admin/items')) \ - .classes('w-full').props('flat no-caps') - ui.button('产品 & 授权', icon='settings', on_click=lambda: ui.navigate.to('/admin/auth')) \ - .classes('w-full').props('flat no-caps') - ui.button('关于 & 反馈', icon='settings', on_click=lambda: ui.navigate.to('/admin/about')) \ - .classes('w-full').props('flat no-caps') - - if page == 'found': - left_drawer.hide() - - with ui.column().classes('w-full'): - yield - -@asynccontextmanager -async def loding_process( - content: str = '正在处理,请稍后...', - success_content: str = '操作成功', - error_content: str = '操作失败', - on_success: Optional[callable] = None, - on_error: Optional[callable] = None, - on_finally: Optional[callable] = None - ): - """ - 加载提示框 - - :param content: 提示内容 - :param success_content: 成功提示内容 - :param error_content: 失败提示内容 - :param on_success: 成功回调函数 - :param on_error: 失败回调函数 - - ~~~ - 使用方法 - >>> async with loding_process(): - # 处理代码 - """ - notify = ui.notification( - message=content, - timeout=None - ) - notify.spinner = True - - try: - yield - except Exception as e: - notify.spinner = False - notify.type = 'negative' - notify.message = error_content + ':' + str(e) - await asyncio.sleep(3) - notify.dismiss() - if on_error: - await on_error(e) - else: - notify.spinner = False - notify.type = 'positive' - notify.message = success_content - if on_success: - await on_success() - await asyncio.sleep(3) - notify.dismiss() - finally: - if on_finally: - await on_finally() \ No newline at end of file diff --git a/routes/frontend/login.py b/routes/frontend/login.py deleted file mode 100644 index c00b467..0000000 --- a/routes/frontend/login.py +++ /dev/null @@ -1,56 +0,0 @@ -''' -Author: 于小丘 海枫 -Date: 2024-10-02 15:23:34 -LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-11-29 20:29:26 -FilePath: /Findreve/login.py -Description: Findreve 登录界面 Login - -Copyright (c) 2018-2024 by 于小丘Yuerchu, All Rights Reserved. -''' - -from nicegui import ui -from typing import Optional -from fastapi.responses import RedirectResponse -from .framework import frame - -def create() -> Optional[RedirectResponse]: - @ui.page('/login') - async def session(redirect_to: str = "/"): - ui.add_head_html(""" - - """) - - ui.page_title('登录 Findreve') - - async with frame(page='session', redirect_to=redirect_to): - - await ui.context.client.connected() - - async def login(): - if username.value == "" or password.value == "": - ui.notify('账号或密码不能为空', color='negative') - return - - try: - result = await ui.run_javascript(f"login('{username.value}', '{password.value}')") - if result['status'] == 'success': - ui.navigate.to(redirect_to) - else: - ui.notify(f"登录失败: {result['detail']}", type="negative") - except Exception as e: - ui.notify(f"登录失败: {str(e)}", type="negative") - - # 创建一个绝对中心的登录卡片 - with ui.card().classes('absolute-center round-lg').style('width: 70%; max-width: 500px'): - # 登录标签 - ui.button(icon='lock').props('outline round').classes('mx-auto w-auto shadow-sm w-fill') - ui.label('登录 Findreve').classes('text-h5 w-full text-center') - # 用户名/密码框 - username = ui.input('账号').on('keydown.enter', login) \ - .classes('block w-full text-gray-900').props('filled') - password = ui.input('密码', password=True, password_toggle_button=True) \ - .on('keydown.enter', login).classes('block w-full text-gray-900').props('filled') - - # 按钮布局 - ui.button('登录', on_click=lambda: login()).classes('items-center w-full').props('rounded') diff --git a/routes/frontend/main_page.py b/routes/frontend/main_page.py deleted file mode 100644 index d5b731d..0000000 --- a/routes/frontend/main_page.py +++ /dev/null @@ -1,151 +0,0 @@ -''' -Author: 于小丘 海枫 -Date: 2024-10-02 15:23:34 -LastEditors: Yuerchu admin@yuxiaoqiu.cn -LastEditTime: 2024-11-29 20:04:24 -FilePath: /Findreve/main_page.py -Description: Findreve 个人主页 main_page - -Copyright (c) 2018-2025 by 于小丘Yuerchu, All Rights Reserved. -''' - -from nicegui import ui -from fastapi import Request - -def create_chip(name: str, color: str, tooltip: str) -> ui.chip: - """Create a UI chip with tooltip""" - return ui.chip(name, color=color).classes('p-4').props('floating').tooltip(tooltip) - -def create() -> None: - @ui.page('/') - async def main_page(request: Request) -> None: - - dark_mode = ui.dark_mode(value=True) - - # 添加页面过渡动画 - ui.add_head_html(''' - - ''') - - with ui.row(align_items='center').classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl p-24'): - with ui.column(align_items='center').classes('px-2 max-md:hidden'): - ui.chip('🐍 Python 是最好的语言').classes('text-xs -mt-1 -right-3').props('floating outline') - ui.chip('🎹 精通 FL Studio Mobile').classes('text-xs -mt-1').props('floating outline') - ui.chip('🎨 熟悉 Ps/Pr/Ae/Au/Ai').classes('text-xs -mt-1').props('floating outline') - ui.chip('🏎 热爱竞速(如地平线5)').classes('text-xs -mt-1 -right-3').props('floating outline') - with ui.avatar().classes('w-32 h-32 transition-transform duration-300 hover:scale-110 cursor-pointer'): - ui.image('/static/heyfun.jpg').classes('w-32 h-32') - with ui.column().classes('px-2 max-md:hidden'): - ui.chip('喜欢去广州图书馆看书 📕').classes('text-xs -mt-1 -left-3').props('floating outline') - ui.chip('致力做安卓苹果开发者 📱').classes('text-xs -mt-1').props('floating outline') - ui.chip('正在自研全链个人生态 🔧').classes('text-xs -mt-1').props('floating outline') - ui.chip('致力与开源社区同发展 🤝').classes('text-xs -mt-1 -left-3').props('floating outline') - - ui.label('关于本站').classes('w-full text-4xl text-bold text-center py-6 subpixel-antialiased') - - with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl py-4'): - with ui.card().classes('w-full sm:w-1/5 lg:w-1/7 flex-grow p-8 bg-gradient-to-br from-indigo-700 to-blue-500'): - ui.label('你好,很高兴认识你👋').classes('text-md text-white') - with ui.row(align_items='center'): - ui.label('我叫').classes('text-4xl text-bold text-white -mt-1 subpixel-antialiased') - ui.label('于小丘').classes('text-4xl text-bold text-white -mt-1 subpixel-antialiased').tooltip('英文名叫Yuerchu,也可以叫我海枫') - ui.label('是一名 开发者、音乐人').classes('text-md text-white -mt-1') - with ui.card().classes('w-full sm:w-1/2 lg:w-1/4 flex-grow flex flex-col justify-center'): - ui.code('void main() {\n printf("为了尚未完成的未来");\n}', language='c').classes('text-3xl max-[768px]:text-xl text-bold text-white flex-grow w-full h-full') - - with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl -mt-3'): - with ui.card().classes('w-full sm:w-1/2 lg:w-1/4 flex-grow p-4'): - ui.label('技能').classes('text-md text-gray-500') - ui.label('开启创造力').classes('text-4xl text-bold -mt-1 right-4') - - with ui.row().classes('items-center'): - create_chip('Python', 'amber-400', 'Python是世界上最好的语言') - create_chip('Kotlin', 'violet-400', 'Kotlin给安卓开发APP') - create_chip('Golang', 'sky-400', 'Golang写后端') - create_chip('Lua', 'blue-900', '用aLua给安卓开发,给罗技鼠标写鼠标宏') - create_chip('c', 'red-400', 'C写嵌入式开发') - create_chip('FL Studio', 'orange-600', 'FL Studio是世界上最好的宿主') - create_chip('Photoshop', 'blue-950', '修图/抠图/画画一站通') - create_chip('Premiere', 'indigo-900', '剪视频比较顺手,但是一开风扇狂转') - create_chip('After Effects', 'indigo-950', '制作特效,电脑太烂了做不了太花的') - create_chip('Audition', 'purple-900', '写歌做母带挺好用的') - create_chip('Illustrator', 'amber-800', '自制字体和画动态SVG') - create_chip('HTML', 'red-900', '前端入门三件套,不学这玩意其他学了没用') - create_chip('CSS3', 'cyan-900', '. window{ show: none; }') - create_chip('JavaScript', 'lime-900', '还在努力学习中,只会一些简单的') - create_chip('git', 'amber-700', '版本管理是真好用') - create_chip('Docker', 'sky-600', '容器化部署') - create_chip('chatGPT', 'emerald-600', '文本助驾,写代码/写文章/写论文') - create_chip('SAI2', 'gray-950', '入门绘画') - create_chip('ips Draw', 'gray-900', '自认为是iOS端最佳绘画软件') - create_chip('AutoCAD', 'gray-950', '画图/绘制电路图') - create_chip('SolidWorks', 'gray-900', '画图/绘制3D模型') - create_chip('EasyEDA', 'gray-950', '画图/绘制电路图') - create_chip('KiCad', 'gray-900', '画图/绘制电路图') - create_chip('Altium Designer', 'gray-950', '画图/绘制电路图') - ui.label('...').classes('text-md text-gray-500') - with ui.card().classes('w-full sm:w-1/3 lg:w-1/6 flex-grow flex flex-col justify-center'): - ui.label('生涯').classes('text-md text-gray-500') - ui.label('无限进步').classes('text-4xl text-bold -mt-1 right-4') - - with ui.timeline(side='right', layout='comfortable'): - ui.timeline_entry('那天,我买了第一台服务器,并搭建了我第一个Wordpress站点', - title='梦开始的地方', - subtitle='2022年1月21日') - ui.timeline_entry('准备从Cloudreve项目脱离,自建网盘系统DiskNext', - title='自建生态计划开始', - subtitle='2024年3月1日') - ui.timeline_entry('目前正在开发HeyAuth、Findreve、DiskNext', - title='项目框架仍在研发中', - subtitle='现在', - icon='rocket') - - - - ui.label('我的作品').classes('w-full text-center text-2xl text-bold p-4') - - with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl'): - with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'): - with ui.row().classes('items-center w-full -mt-2'): - ui.label('DiskNext').classes('text-lg text-bold') - ui.chip('B端程序').classes('text-xs').props('floating') - ui.space() - ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://pan.yxqi.cn'))).props('flat fab-mini') - ui.label('一个基于NiceGUI的网盘系统,性能与Golang媲美').classes('text-sm -mt-3') - with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'): - with ui.row().classes('items-center w-full -mt-2'): - ui.label('Findreve').classes('text-lg text-bold') - ui.chip('C端程序').classes('text-xs').props('floating') - ui.space() - ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://i.yxqi.cn'))).props('flat fab-mini') - ui.label('一个基于NiceGUI的个人主页配合物品丢失找回系统').classes('text-sm -mt-3') - with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'): - with ui.row().classes('items-center w-full -mt-2'): - ui.label('HeyAuth').classes('text-lg text-bold') - ui.chip('B端程序').classes('text-xs').props('floating') - ui.space() - ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://auth.yxqi.cn'))).props('flat fab-mini') - ui.label('一个基于NiceGUI的B+C端多应用授权系统').classes('text-sm -mt-3') - - with ui.row().classes('w-full items-center justify-center items-stretch mx-auto mx-8 max-w-7xl'): - with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'): - with ui.row().classes('items-center w-full -mt-2'): - ui.label('与枫同奔 Run With Fun').classes('text-lg text-bold') - ui.chip('词曲').classes('text-xs').props('floating') - ui.space() - ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=2148944359'))).props('flat fab-mini') - ui.label('我愿如流星赶月那样飞奔').classes('text-sm -mt-3') - with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'): - with ui.row().classes('items-center w-full -mt-2'): - ui.label('HeyFun\'s Story').classes('text-lg text-bold') - ui.chip('自设印象曲').classes('text-xs').props('floating') - ui.space() - ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=1889436124'))).props('flat fab-mini') - ui.label('飞奔在星辰大海之间的少年').classes('text-sm -mt-3') - with ui.card().classes('w-full sm:w-1/3 lg:w-1/5 flex-grow p-4'): - with ui.row().classes('items-center w-full -mt-2'): - ui.label('2020Fall').classes('text-lg text-bold') - ui.chip('年度纯音乐').classes('text-xs').props('floating') - ui.space() - ui.button(icon='open_in_new', on_click=lambda: (ui.navigate.to('https://music.163.com/#/song?id=1863630345'))).props('flat fab-mini') - ui.label('耗时6个月完成的年度纯音乐').classes('text-sm -mt-3') diff --git a/routes/backend/object.py b/routes/object.py similarity index 61% rename from routes/backend/object.py rename to routes/object.py index d9a3cce..cf209f8 100644 --- a/routes/backend/object.py +++ b/routes/object.py @@ -2,12 +2,18 @@ import random from fastapi import APIRouter, Request from fastapi.responses import JSONResponse from model.database import Database -from model.response import DefaultResponse +from model.response import DefaultResponse, ObjectData import asyncio -Router = APIRouter(prefix='/api/object', tags=['object']) +Router = APIRouter(prefix='/api/object', tags=['物品 Object']) -@Router.get('/{item_key}') +@Router.get( + path='/{item_key}', + summary="获取物品信息", + description="根据物品键获取物品信息", + response_model=DefaultResponse, + response_description="物品信息" +) async def get_object(item_key: str, request: Request): """ 获取物品信息 / Get object information @@ -27,9 +33,15 @@ async def get_object(item_key: str, request: Request): else: await asyncio.sleep(random.uniform(0.10, 0.30)) - return DefaultResponse( - data=object_data - ) + return DefaultResponse(data=ObjectData( + id=object_data[0], + key=object_data[1], + name=object_data[2], + icon=object_data[3], + status=object_data[4], + phone=object_data[5], + context=object_data[6] + ).model_dump()) else: return JSONResponse( status_code=404, content=DefaultResponse( diff --git a/routes/backend/session.py b/routes/session.py similarity index 85% rename from routes/backend/session.py rename to routes/session.py index 3ae230f..7cb52d2 100644 --- a/routes/backend/session.py +++ b/routes/session.py @@ -1,4 +1,4 @@ -from nicegui import app +# 导入库 from typing import Annotated from datetime import datetime, timedelta, timezone from fastapi import Depends, HTTPException, status @@ -10,8 +10,9 @@ from model.token import Token from model import database from tool import verify_password -Router = APIRouter() +Router = APIRouter(tags=["令牌 session"]) +# 创建令牌 def create_access_token(data: dict, expires_delta: timedelta | None = None): to_encode = data.copy() if expires_delta: @@ -22,6 +23,7 @@ def create_access_token(data: dict, expires_delta: timedelta | None = None): encoded_jwt = jwt.encode(to_encode, JWT.SECRET_KEY, algorithm='HS256') return encoded_jwt +# 验证账号密码 async def authenticate_user(username: str, password: str): # 验证账号和密码 account = await database.Database().get_setting('account') @@ -33,7 +35,13 @@ async def authenticate_user(username: str, password: str): return {'is_authenticated': True} # FastAPI 登录路由 / FastAPI login route -@app.post("/api/token") +@Router.post( + path="/api/token", + summary="获取访问令牌", + description="使用用户名和密码获取访问令牌", + response_model=Token, + response_description="访问令牌" +) async def login_for_access_token( form_data: Annotated[OAuth2PasswordRequestForm, Depends()], ) -> Token: