feat: basic framework
This commit is contained in:
9
src/App.vue
Normal file
9
src/App.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<router-view />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
11
src/main.ts
Normal file
11
src/main.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { createPinia } from "pinia";
|
||||
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
createApp(App).use(router).use(pinia).mount("#app");
|
||||
19
src/pages/AboutPage.vue
Normal file
19
src/pages/AboutPage.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
// About page component
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">关于我们</h1>
|
||||
<p class="py-6">这是一个自动路由注册的示例页面</p>
|
||||
<p>路径将自动转换为 /about</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Component styles */
|
||||
</style>
|
||||
18
src/pages/HomePage.vue
Normal file
18
src/pages/HomePage.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts" setup>
|
||||
// Home page component
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">首页</h1>
|
||||
<p class="py-6">欢迎使用自动路由系统</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Component styles */
|
||||
</style>
|
||||
72
src/router/index.ts
Normal file
72
src/router/index.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
type RouteRecordRaw,
|
||||
} from "vue-router";
|
||||
|
||||
/**
|
||||
* Convert PascalCase to kebab-case and remove 'Page' suffix
|
||||
* @param name PascalCase name (e.g., AboutMePage)
|
||||
* @returns kebab-case path (e.g., about-me)
|
||||
*/
|
||||
function formatPathFromComponentName(name: string): string {
|
||||
// Remove .vue extension if present
|
||||
const baseName = name.endsWith(".vue") ? name.slice(0, -4) : name;
|
||||
|
||||
// Remove Page suffix if present
|
||||
const withoutPageSuffix = baseName.endsWith("Page")
|
||||
? baseName.slice(0, -4)
|
||||
: baseName;
|
||||
|
||||
// Convert PascalCase to kebab-case
|
||||
return withoutPageSuffix
|
||||
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
||||
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract component name from file path
|
||||
* @param filePath Path to the component file
|
||||
* @returns Component name without extension
|
||||
*/
|
||||
function getComponentNameFromPath(filePath: string): string {
|
||||
// Get the file name from the path
|
||||
return filePath.split("/").pop() || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate routes from page components using Vite's import.meta.glob
|
||||
* @returns Array of route configurations
|
||||
*/
|
||||
function generateRoutesFromPages(): RouteRecordRaw[] {
|
||||
const routes: RouteRecordRaw[] = [];
|
||||
|
||||
const pages = import.meta.glob("@/pages/**/*.vue");
|
||||
|
||||
for (const path in pages) {
|
||||
const componentName = getComponentNameFromPath(path);
|
||||
const routePath = `/${formatPathFromComponentName(componentName)}`;
|
||||
|
||||
// Special case for home page
|
||||
const finalPath = routePath.toLowerCase() === "/home" ? "/" : routePath;
|
||||
|
||||
routes.push({
|
||||
path: finalPath,
|
||||
name: componentName,
|
||||
component: pages[path],
|
||||
});
|
||||
}
|
||||
|
||||
return routes;
|
||||
}
|
||||
|
||||
// Generate routes from pages directory
|
||||
const routes: RouteRecordRaw[] = generateRoutesFromPages();
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
export default router;
|
||||
21
src/stores/UserStore.ts
Normal file
21
src/stores/UserStore.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { ref } from "vue";
|
||||
import { defineStore } from "pinia";
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
"user",
|
||||
() => {
|
||||
const token = ref("");
|
||||
|
||||
const setToken = (newToken: string) => {
|
||||
token.value = newToken;
|
||||
};
|
||||
|
||||
return {
|
||||
token,
|
||||
setToken,
|
||||
};
|
||||
},
|
||||
{
|
||||
persist: true,
|
||||
},
|
||||
);
|
||||
2
src/style.css
Normal file
2
src/style.css
Normal file
@@ -0,0 +1,2 @@
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui";
|
||||
5
src/types/http.d.ts
vendored
Normal file
5
src/types/http.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare interface Result<T> {
|
||||
code: number;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
26
src/utils/http.ts
Normal file
26
src/utils/http.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import axios, { type AxiosRequestConfig } from "axios";
|
||||
import { useUserStore } from "@/stores/UserStore.ts";
|
||||
|
||||
const instance = axios.create({
|
||||
baseURL: import.meta.env.VITE_SERVER,
|
||||
});
|
||||
|
||||
instance.interceptors.request.use((config) => {
|
||||
const store = useUserStore();
|
||||
if (!store.token) {
|
||||
return config;
|
||||
}
|
||||
const token = store.token;
|
||||
if (!token) {
|
||||
return config;
|
||||
}
|
||||
config.headers["token"] = token;
|
||||
return config;
|
||||
});
|
||||
|
||||
const http = async <T>(config: AxiosRequestConfig): Promise<Result<T>> => {
|
||||
const { data } = await instance.request<Result<T>>(config);
|
||||
return data;
|
||||
};
|
||||
|
||||
export default http;
|
||||
50
src/utils/toast.tsx
Normal file
50
src/utils/toast.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
import { createApp, ref } from "vue";
|
||||
import { AnimatePresence, motion } from "motion-v";
|
||||
|
||||
export interface ToastOptions {
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export function toast(options: ToastOptions): void {
|
||||
const toastHideFlag = ref(false);
|
||||
|
||||
const toastInstance = () => (
|
||||
<AnimatePresence>
|
||||
{toastHideFlag.value ? null : (
|
||||
<motion.div
|
||||
class="toast toast-top toast-end bg-transparent"
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.25 }}
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
>
|
||||
<div class="alert">
|
||||
<span>{options.message}</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
const toastContainer = document.createElement("div");
|
||||
document.body.appendChild(toastContainer);
|
||||
|
||||
const toastApp = createApp(toastInstance);
|
||||
toastApp.mount(toastContainer);
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
toastHideFlag.value = true;
|
||||
},
|
||||
(options.duration || 3000) + 250, // 250ms for start animation
|
||||
);
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
toastApp.unmount();
|
||||
document.body.removeChild(toastContainer);
|
||||
},
|
||||
(options.duration || 3000) + 500, // 500ms for end animation
|
||||
);
|
||||
}
|
||||
9
src/vite-env.d.ts
vendored
Normal file
9
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_SERVER: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv;
|
||||
}
|
||||
Reference in New Issue
Block a user