feat: basic framework

This commit is contained in:
wuyugu
2025-08-01 17:52:35 +08:00
commit 1b2dafb654
24 changed files with 1068 additions and 0 deletions

9
src/App.vue Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,2 @@
@import "tailwindcss";
@plugin "daisyui";

5
src/types/http.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare interface Result<T> {
code: number;
message: string;
data: T;
}

26
src/utils/http.ts Normal file
View 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
View 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
View File

@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_SERVER: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}