first init
This commit is contained in:
1
.env.development
Normal file
1
.env.development
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_SERVER=http://localhost:8080
|
||||||
1
.env.production
Normal file
1
.env.production
Normal file
@@ -0,0 +1 @@
|
|||||||
|
VITE_SERVER=http://localhost:8080
|
||||||
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
11
.prettierrc
Normal file
11
.prettierrc
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": false,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"objectWrap": "preserve",
|
||||||
|
"bracketSameLine": false,
|
||||||
|
"arrowParens": "always"
|
||||||
|
}
|
||||||
78
auto-imports.d.ts
vendored
Normal file
78
auto-imports.d.ts
vendored
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/* prettier-ignore */
|
||||||
|
// @ts-nocheck
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
|
// Generated by unplugin-auto-import
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
export {}
|
||||||
|
declare global {
|
||||||
|
const EffectScope: typeof import('vue')['EffectScope']
|
||||||
|
const computed: typeof import('vue')['computed']
|
||||||
|
const createApp: typeof import('vue')['createApp']
|
||||||
|
const customRef: typeof import('vue')['customRef']
|
||||||
|
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||||
|
const defineComponent: typeof import('vue')['defineComponent']
|
||||||
|
const effectScope: typeof import('vue')['effectScope']
|
||||||
|
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||||
|
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||||
|
const getCurrentWatcher: typeof import('vue')['getCurrentWatcher']
|
||||||
|
const h: typeof import('vue')['h']
|
||||||
|
const inject: typeof import('vue')['inject']
|
||||||
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const isShallow: typeof import('vue')['isShallow']
|
||||||
|
const markRaw: typeof import('vue')['markRaw']
|
||||||
|
const nextTick: typeof import('vue')['nextTick']
|
||||||
|
const onActivated: typeof import('vue')['onActivated']
|
||||||
|
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||||
|
const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave']
|
||||||
|
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
|
||||||
|
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||||
|
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||||
|
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||||
|
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||||
|
const onMounted: typeof import('vue')['onMounted']
|
||||||
|
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||||
|
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||||
|
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||||
|
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||||
|
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||||
|
const onUpdated: typeof import('vue')['onUpdated']
|
||||||
|
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||||
|
const provide: typeof import('vue')['provide']
|
||||||
|
const reactive: typeof import('vue')['reactive']
|
||||||
|
const readonly: typeof import('vue')['readonly']
|
||||||
|
const ref: typeof import('vue')['ref']
|
||||||
|
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||||
|
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||||
|
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||||
|
const shallowRef: typeof import('vue')['shallowRef']
|
||||||
|
const toRaw: typeof import('vue')['toRaw']
|
||||||
|
const toRef: typeof import('vue')['toRef']
|
||||||
|
const toRefs: typeof import('vue')['toRefs']
|
||||||
|
const toValue: typeof import('vue')['toValue']
|
||||||
|
const triggerRef: typeof import('vue')['triggerRef']
|
||||||
|
const unref: typeof import('vue')['unref']
|
||||||
|
const useAttrs: typeof import('vue')['useAttrs']
|
||||||
|
const useCssModule: typeof import('vue')['useCssModule']
|
||||||
|
const useCssVars: typeof import('vue')['useCssVars']
|
||||||
|
const useId: typeof import('vue')['useId']
|
||||||
|
const useLink: typeof import('vue-router')['useLink']
|
||||||
|
const useModel: typeof import('vue')['useModel']
|
||||||
|
const useRoute: typeof import('vue-router')['useRoute']
|
||||||
|
const useRouter: typeof import('vue-router')['useRouter']
|
||||||
|
const useSlots: typeof import('vue')['useSlots']
|
||||||
|
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||||
|
const watch: typeof import('vue')['watch']
|
||||||
|
const watchEffect: typeof import('vue')['watchEffect']
|
||||||
|
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||||
|
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||||
|
}
|
||||||
|
// for type re-export
|
||||||
|
declare global {
|
||||||
|
// @ts-ignore
|
||||||
|
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||||
|
import('vue')
|
||||||
|
}
|
||||||
28
eslint.config.js
Normal file
28
eslint.config.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import eslint from "@eslint/js";
|
||||||
|
import eslintPluginVue from "eslint-plugin-vue";
|
||||||
|
import globals from "globals";
|
||||||
|
import typescriptEslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default typescriptEslint.config(
|
||||||
|
{ ignores: ["*.d.ts", "**/coverage", "**/dist"] },
|
||||||
|
{
|
||||||
|
extends: [
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...typescriptEslint.configs.recommended,
|
||||||
|
...eslintPluginVue.configs["flat/recommended"],
|
||||||
|
],
|
||||||
|
files: ["**/*.{ts,vue}"],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: "latest",
|
||||||
|
sourceType: "module",
|
||||||
|
globals: globals.browser,
|
||||||
|
parserOptions: {
|
||||||
|
parser: typescriptEslint.parser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"vue/singleline-html-element-content-newline": "off",
|
||||||
|
"vue/max-attributes-per-line": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
35
github/workflows/deploy.yml
Normal file
35
github/workflows/deploy.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: 构建并部署前端项目
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["master"]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: 拉取代码
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: 安装Bun
|
||||||
|
uses: oven-sh/setup-bun@v1
|
||||||
|
with:
|
||||||
|
bun-version: latest
|
||||||
|
|
||||||
|
- name: 安装依赖
|
||||||
|
run: bun install
|
||||||
|
|
||||||
|
- name: 构建项目
|
||||||
|
run: bun run build
|
||||||
|
|
||||||
|
- name: 部署到服务器
|
||||||
|
uses: appleboy/scp-action@master
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SERVER_HOST }}
|
||||||
|
port: 22
|
||||||
|
username: ${{ secrets.SERVER_USER }}
|
||||||
|
password: ${{ secrets.SERVER_PASSWORD }}
|
||||||
|
source: "dist/*"
|
||||||
|
target: ${{ secrets.SERVER_TARGET_DIR }}
|
||||||
|
rm: true
|
||||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/logo.png" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Hucky</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
45
package.json
Normal file
45
package.json
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "template-vue-hucky",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"generate:page": "node --no-warnings=ExperimentalWarning --experimental-strip-types scripts/generatePage.ts",
|
||||||
|
"gp": "node --no-warnings=ExperimentalWarning --experimental-strip-types scripts/generatePage.ts",
|
||||||
|
"generate:component": "node --no-warnings=ExperimentalWarning --experimental-strip-types scripts/generateComponent.ts",
|
||||||
|
"gc": "node --no-warnings=ExperimentalWarning --experimental-strip-types scripts/generateComponent.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@vueuse/core": "^13.9.0",
|
||||||
|
"axios": "^1.11.0",
|
||||||
|
"daisyui": "^5.0.50",
|
||||||
|
"motion-v": "^1.6.1",
|
||||||
|
"pinia": "^3.0.3",
|
||||||
|
"pinia-plugin-persistedstate": "^4.4.1",
|
||||||
|
"radash": "^12.1.1",
|
||||||
|
"vue": "^3.5.17",
|
||||||
|
"vue-router": "^4.5.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/vite": "^4.1.11",
|
||||||
|
"@types/node": "^24.1.0",
|
||||||
|
"@vitejs/plugin-vue": "^6.0.0",
|
||||||
|
"@vitejs/plugin-vue-jsx": "^5.0.1",
|
||||||
|
"@vue/babel-plugin-jsx": "^1.4.0",
|
||||||
|
"@vue/tsconfig": "^0.7.0",
|
||||||
|
"eslint": "^9.35.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-vue": "^10.4.0",
|
||||||
|
"prettier": "^3.6.2",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.43.0",
|
||||||
|
"unplugin-auto-import": "^20.1.0",
|
||||||
|
"vite": "^7.0.0",
|
||||||
|
"vue-tsc": "^2.2.10"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 888 KiB |
132
scripts/generateComponent.ts
Normal file
132
scripts/generateComponent.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as readline from "node:readline";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const PAGES_DIR = path.resolve(process.cwd(), "src/components");
|
||||||
|
const TEMPLATE = `<script lang="ts" setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the pages directory exists
|
||||||
|
*/
|
||||||
|
function ensurePagesDirectory() {
|
||||||
|
if (!fs.existsSync(PAGES_DIR)) {
|
||||||
|
console.log(`检测到 ${PAGES_DIR} 目录不存在, 正在为您创建该目录`);
|
||||||
|
fs.mkdirSync(PAGES_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the component name to PascalCase and ensures it ends with .vue
|
||||||
|
* @param name The input name to format
|
||||||
|
* @returns Properly formatted component name
|
||||||
|
*/
|
||||||
|
function formatComponentName(name: string): string {
|
||||||
|
// .vue extension if present
|
||||||
|
if (name.endsWith(".vue")) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page suffix if present
|
||||||
|
if (name.endsWith("Component")) {
|
||||||
|
return `${name}.vue`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to PascalCase
|
||||||
|
let baseName = name
|
||||||
|
.split(/[-_\s.]+/)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Ensure name is not empty
|
||||||
|
if (!baseName) {
|
||||||
|
throw new Error("组件名称不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Page suffix if not already present
|
||||||
|
if (!baseName.endsWith("Component")) {
|
||||||
|
baseName += "Component";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add .vue extension
|
||||||
|
return `${baseName}.vue`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Vue component file
|
||||||
|
* @param componentName The name of the component to create
|
||||||
|
*/
|
||||||
|
async function createComponent(componentName: string) {
|
||||||
|
const formattedName = formatComponentName(componentName);
|
||||||
|
const filePath = path.join(PAGES_DIR, formattedName);
|
||||||
|
|
||||||
|
// Check if file already exists
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
console.error(`❌ 失败: 组件 ${formattedName} 已存在于 ${filePath}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write the file
|
||||||
|
fs.writeFileSync(filePath, TEMPLATE);
|
||||||
|
console.log(`✅ 成功: 创建组件 ${formattedName} at ${filePath}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`❌ 失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to run the script
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
ensurePagesDirectory();
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\n🚀 组件生成器");
|
||||||
|
console.log("==========================");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const componentName = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
"✨ 请输入组件名称, 程序将自动转换为 PascalCase: (例如: about-me => AboutMeComponent)\n",
|
||||||
|
(answer: string) => {
|
||||||
|
resolve(answer.trim());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!componentName) {
|
||||||
|
console.error("❌ 失败: 组件名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createComponent(componentName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`❌ 失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
main().catch(console.error);
|
||||||
132
scripts/generatePage.ts
Normal file
132
scripts/generatePage.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import * as fs from "node:fs";
|
||||||
|
import * as path from "node:path";
|
||||||
|
import * as readline from "node:readline";
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const PAGES_DIR = path.resolve(process.cwd(), "src/pages");
|
||||||
|
const TEMPLATE = `<script lang="ts" setup>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the pages directory exists
|
||||||
|
*/
|
||||||
|
function ensurePagesDirectory() {
|
||||||
|
if (!fs.existsSync(PAGES_DIR)) {
|
||||||
|
console.log(`检测到 ${PAGES_DIR} 目录不存在, 正在为您创建该目录`);
|
||||||
|
fs.mkdirSync(PAGES_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the component name to PascalCase and ensures it ends with .vue
|
||||||
|
* @param name The input name to format
|
||||||
|
* @returns Properly formatted component name
|
||||||
|
*/
|
||||||
|
function formatComponentName(name: string): string {
|
||||||
|
// .vue extension if present
|
||||||
|
if (name.endsWith(".vue")) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page suffix if present
|
||||||
|
if (name.endsWith("Page")) {
|
||||||
|
return `${name}.vue`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to PascalCase
|
||||||
|
let baseName = name
|
||||||
|
.split(/[-_\s.]+/)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Ensure name is not empty
|
||||||
|
if (!baseName) {
|
||||||
|
throw new Error("页面名称不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Page suffix if not already present
|
||||||
|
if (!baseName.endsWith("Page")) {
|
||||||
|
baseName += "Page";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add .vue extension
|
||||||
|
return `${baseName}.vue`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Vue component file
|
||||||
|
* @param componentName The name of the component to create
|
||||||
|
*/
|
||||||
|
async function createComponent(componentName: string) {
|
||||||
|
const formattedName = formatComponentName(componentName);
|
||||||
|
const filePath = path.join(PAGES_DIR, formattedName);
|
||||||
|
|
||||||
|
// Check if file already exists
|
||||||
|
if (fs.existsSync(filePath)) {
|
||||||
|
console.error(`❌ 失败: 组件 ${formattedName} 已存在于 ${filePath}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Write the file
|
||||||
|
fs.writeFileSync(filePath, TEMPLATE);
|
||||||
|
console.log(`✅ 成功: 创建组件 ${formattedName} at ${filePath}`);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`❌ 失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to run the script
|
||||||
|
*/
|
||||||
|
async function main() {
|
||||||
|
ensurePagesDirectory();
|
||||||
|
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\n🚀 页面生成器");
|
||||||
|
console.log("==========================");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const componentName = await new Promise<string>((resolve) => {
|
||||||
|
rl.question(
|
||||||
|
"✨ 请输入页面名称, 程序将自动转换为 PascalCase: (例如: about-me => AboutMePage)\n",
|
||||||
|
(answer: string) => {
|
||||||
|
resolve(answer.trim());
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!componentName) {
|
||||||
|
console.error("❌ 失败: 页面名称不能为空");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await createComponent(componentName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`❌ 失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
rl.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the script
|
||||||
|
main().catch(console.error);
|
||||||
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>
|
||||||
19
src/apis/user.ts
Normal file
19
src/apis/user.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import http from "../utils/http";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an example, please remove if not needed
|
||||||
|
*/
|
||||||
|
export const getUserList = () => {
|
||||||
|
return http({
|
||||||
|
url: "/user/list",
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const insertUser = (data: { account: string; passowrd: string }) => {
|
||||||
|
return http({
|
||||||
|
url: "/user/insert",
|
||||||
|
method: "post",
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
};
|
||||||
BIN
src/assets/hucky.png
Normal file
BIN
src/assets/hucky.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 888 KiB |
16
src/hooks/bus/changeUserBus.ts
Normal file
16
src/hooks/bus/changeUserBus.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { createEventHook } from "@vueuse/core";
|
||||||
|
|
||||||
|
export interface ChangeUserBus {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const changeUserEvent = createEventHook<ChangeUserBus>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is an example, please remove if not needed
|
||||||
|
*/
|
||||||
|
export const changeUserBus = {
|
||||||
|
emit: changeUserEvent.emit,
|
||||||
|
on: changeUserEvent.on,
|
||||||
|
off: changeUserEvent.off,
|
||||||
|
};
|
||||||
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>
|
||||||
77
src/pages/HelpPage.vue
Normal file
77
src/pages/HelpPage.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<script lang="ts" setup>
|
||||||
|
import { range } from "radash";
|
||||||
|
import { usePagination } from "@/utils/pagination";
|
||||||
|
|
||||||
|
const merchandise = ref([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
name: "商品1",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { currentPage, changePage, pageNumbers, totalPages, getPaginatedData } =
|
||||||
|
usePagination(() => merchandise.value);
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
changePage(page);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await new Promise(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
merchandise.value = Array.from(range(1, 50)).map((item) => ({
|
||||||
|
id: item,
|
||||||
|
name: `商品${item}`,
|
||||||
|
}));
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
<li v-for="item in getPaginatedData" :key="item.id">
|
||||||
|
{{ item.name }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="join-item btn btn-sm"
|
||||||
|
:disabled="currentPage === 1"
|
||||||
|
@click="handlePageChange(currentPage - 1)"
|
||||||
|
>
|
||||||
|
«
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-for="page in pageNumbers"
|
||||||
|
:key="page"
|
||||||
|
class="join-item btn btn-sm"
|
||||||
|
:class="{
|
||||||
|
'btn-active': page === currentPage,
|
||||||
|
'btn-disabled': page === '...',
|
||||||
|
}"
|
||||||
|
@click="page !== '...' && handlePageChange(Number(page))"
|
||||||
|
>
|
||||||
|
{{ page }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="join-item btn btn-sm"
|
||||||
|
:disabled="currentPage === totalPages"
|
||||||
|
@click="handlePageChange(currentPage + 1)"
|
||||||
|
>
|
||||||
|
»
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
15
src/pages/panel/MePage.vue
Normal file
15
src/pages/panel/MePage.vue
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<script lang="ts" setup></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>路径将自动转换为 /panel/me</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
98
src/router/index.ts
Normal file
98
src/router/index.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import {
|
||||||
|
createRouter,
|
||||||
|
createWebHashHistory,
|
||||||
|
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 route path from full file path, preserving directory structure
|
||||||
|
* @param filePath Full path to the component file (e.g., "@/pages/test/AbcPage.vue")
|
||||||
|
* @returns Route path (e.g., "/test/abc")
|
||||||
|
*/
|
||||||
|
function generateRoutePathFromFilePath(filePath: string): string {
|
||||||
|
// Remove the "@/pages" prefix and get the relative path
|
||||||
|
const relativePath = filePath.replace(/^@\/pages\//, "");
|
||||||
|
|
||||||
|
// Split into directory parts and filename
|
||||||
|
const pathParts = relativePath.split("/");
|
||||||
|
const fileName = pathParts.pop() || "";
|
||||||
|
const directories = pathParts.splice(3);
|
||||||
|
|
||||||
|
// Transform the filename using existing logic
|
||||||
|
const transformedFileName = formatPathFromComponentName(fileName);
|
||||||
|
|
||||||
|
// Combine directory path with transformed filename
|
||||||
|
const fullPath =
|
||||||
|
directories.length > 0
|
||||||
|
? `/${directories.join("/")}/${transformedFileName}`
|
||||||
|
: `/${transformedFileName}`;
|
||||||
|
|
||||||
|
return fullPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = generateRoutePathFromFilePath(path);
|
||||||
|
|
||||||
|
// 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: createWebHashHistory(),
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
);
|
||||||
29
src/style.css
Normal file
29
src/style.css
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@plugin "daisyui";
|
||||||
|
|
||||||
|
/* Custom Scrollbar Styles */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
border-radius: 10px;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox scrollbar styles */
|
||||||
|
* {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
92
src/utils/confirm.tsx
Normal file
92
src/utils/confirm.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import { createApp, ref } from "vue";
|
||||||
|
import { AnimatePresence, motion } from "motion-v";
|
||||||
|
|
||||||
|
export interface ConfirmOptions {
|
||||||
|
title?: string;
|
||||||
|
message: string;
|
||||||
|
confirmText?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
onConfirm?: () => void;
|
||||||
|
onCancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function confirm(options: ConfirmOptions): void {
|
||||||
|
const confirmHideFlag = ref(false);
|
||||||
|
const defaultOptions = {
|
||||||
|
title: "确认",
|
||||||
|
confirmText: "确定",
|
||||||
|
cancelText: "取消",
|
||||||
|
onConfirm: () => {},
|
||||||
|
onCancel: () => {},
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
confirmHideFlag.value = true;
|
||||||
|
defaultOptions.onConfirm?.();
|
||||||
|
setTimeout(() => {
|
||||||
|
confirmApp.unmount();
|
||||||
|
document.body.removeChild(confirmContainer);
|
||||||
|
}, 300); // 动画结束后移除
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
confirmHideFlag.value = true;
|
||||||
|
defaultOptions.onCancel?.();
|
||||||
|
setTimeout(() => {
|
||||||
|
confirmApp.unmount();
|
||||||
|
document.body.removeChild(confirmContainer);
|
||||||
|
}, 300); // 动画结束后移除
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmInstance = () => (
|
||||||
|
<AnimatePresence>
|
||||||
|
{confirmHideFlag.value ? null : (
|
||||||
|
<div class="fixed inset-0 flex items-center justify-center z-50 bg-[#00000050]">
|
||||||
|
<motion.div
|
||||||
|
class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4 px-2"
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.25 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.95 }}
|
||||||
|
initial={{ opacity: 0, scale: 0.95 }}
|
||||||
|
>
|
||||||
|
{defaultOptions.title && (
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h3 class="text-lg font-medium text-gray-900">
|
||||||
|
{defaultOptions.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="px-6 py-4">
|
||||||
|
<p class="text-gray-700">{defaultOptions.message}</p>
|
||||||
|
</div>
|
||||||
|
<div class="px-6 py-3 flex justify-end space-x-3">
|
||||||
|
{defaultOptions.cancelText && (
|
||||||
|
<button
|
||||||
|
onClick={handleCancel}
|
||||||
|
class="px-4 py-2 bg-gray-100 text-gray-700 rounded hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
{defaultOptions.cancelText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{defaultOptions.confirmText && (
|
||||||
|
<button
|
||||||
|
onClick={handleConfirm}
|
||||||
|
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
{defaultOptions.confirmText}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
|
||||||
|
const confirmContainer = document.createElement("div");
|
||||||
|
document.body.appendChild(confirmContainer);
|
||||||
|
|
||||||
|
const confirmApp = createApp(confirmInstance);
|
||||||
|
confirmApp.mount(confirmContainer);
|
||||||
|
}
|
||||||
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;
|
||||||
158
src/utils/pagination.ts
Normal file
158
src/utils/pagination.ts
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import { ref, computed, ComputedRef, Ref } from "vue";
|
||||||
|
|
||||||
|
export interface PaginationOptions {
|
||||||
|
pageSize?: number;
|
||||||
|
initialPage?: number;
|
||||||
|
maxVisiblePages?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginationResult<T> {
|
||||||
|
// Pagination state
|
||||||
|
currentPage: Ref<number>;
|
||||||
|
pageSize: Ref<number>;
|
||||||
|
totalPages: ComputedRef<number>;
|
||||||
|
pageNumbers: ComputedRef<(number | string)[]>;
|
||||||
|
|
||||||
|
// Pagination methods
|
||||||
|
changePage: (page: number) => void;
|
||||||
|
nextPage: () => void;
|
||||||
|
prevPage: () => void;
|
||||||
|
|
||||||
|
// Data methods
|
||||||
|
getPaginatedData: ComputedRef<T[]>;
|
||||||
|
setTotalItems: (count: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create pagination functionality for a list of items
|
||||||
|
*
|
||||||
|
* @param data Ref to the full data array or a function that returns the full data
|
||||||
|
* @param options Pagination options
|
||||||
|
* @returns Pagination state and methods
|
||||||
|
*/
|
||||||
|
export function usePagination<T>(
|
||||||
|
data: Ref<T[]> | (() => T[]),
|
||||||
|
options: PaginationOptions = {},
|
||||||
|
): PaginationResult<T> {
|
||||||
|
// Initialize pagination state
|
||||||
|
const currentPage = ref(options.initialPage || 1);
|
||||||
|
const pageSize = ref(options.pageSize || 10);
|
||||||
|
const maxVisiblePages = options.maxVisiblePages || 5;
|
||||||
|
|
||||||
|
// Calculate if data is a ref or a function
|
||||||
|
const isDataRef = "value" in data;
|
||||||
|
|
||||||
|
// Calculate total items
|
||||||
|
const totalItems = computed(() => {
|
||||||
|
if (isDataRef) {
|
||||||
|
return (data as Ref<T[]>).value.length;
|
||||||
|
} else {
|
||||||
|
return (data as () => T[])().length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate total pages
|
||||||
|
const totalPages = computed(() => {
|
||||||
|
return Math.ceil(totalItems.value / pageSize.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get paginated data
|
||||||
|
const getPaginatedData = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value;
|
||||||
|
const end = start + pageSize.value;
|
||||||
|
|
||||||
|
if (isDataRef) {
|
||||||
|
return (data as Ref<T[]>).value.slice(start, end);
|
||||||
|
} else {
|
||||||
|
const fullData = (data as () => T[])();
|
||||||
|
return fullData.slice(start, end);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Generate array of page numbers for pagination
|
||||||
|
const pageNumbers = computed(() => {
|
||||||
|
const pages: (number | string)[] = [];
|
||||||
|
|
||||||
|
if (totalPages.value <= maxVisiblePages) {
|
||||||
|
// Show all pages if total pages are less than max visible
|
||||||
|
for (let i = 1; i <= totalPages.value; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Always show first page
|
||||||
|
pages.push(1);
|
||||||
|
|
||||||
|
// Calculate start and end of visible page range
|
||||||
|
let startPage = Math.max(2, currentPage.value - 1);
|
||||||
|
let endPage = Math.min(totalPages.value - 1, currentPage.value + 1);
|
||||||
|
|
||||||
|
// Adjust if we're near the beginning
|
||||||
|
if (currentPage.value <= 3) {
|
||||||
|
endPage = Math.min(totalPages.value - 1, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust if we're near the end
|
||||||
|
if (currentPage.value >= totalPages.value - 2) {
|
||||||
|
startPage = Math.max(2, totalPages.value - 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ellipsis if needed before visible pages
|
||||||
|
if (startPage > 2) {
|
||||||
|
pages.push("...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add visible page numbers
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
pages.push(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add ellipsis if needed after visible pages
|
||||||
|
if (endPage < totalPages.value - 1) {
|
||||||
|
pages.push("...");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always show last page
|
||||||
|
pages.push(totalPages.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change page
|
||||||
|
const changePage = (page: number) => {
|
||||||
|
if (page >= 1 && page <= totalPages.value) {
|
||||||
|
currentPage.value = page;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Next page
|
||||||
|
const nextPage = () => {
|
||||||
|
if (currentPage.value < totalPages.value) {
|
||||||
|
currentPage.value++;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Previous page
|
||||||
|
const prevPage = () => {
|
||||||
|
if (currentPage.value > 1) {
|
||||||
|
currentPage.value--;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set total items (useful for API pagination)
|
||||||
|
const setTotalItems = (count: number) => {
|
||||||
|
totalItems.value = count;
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
pageNumbers,
|
||||||
|
changePage,
|
||||||
|
nextPage,
|
||||||
|
prevPage,
|
||||||
|
getPaginatedData,
|
||||||
|
setTotalItems,
|
||||||
|
};
|
||||||
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
19
tsconfig.app.json
Normal file
19
tsconfig.app.json
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "auto-imports.d.ts"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
26
tsconfig.node.json
Normal file
26
tsconfig.node.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["node"],
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts", "scripts/**/*.ts", "auto-imports.d.ts"]
|
||||||
|
}
|
||||||
24
vite.config.ts
Normal file
24
vite.config.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import vue from "@vitejs/plugin-vue";
|
||||||
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import vueJsx from "@vitejs/plugin-vue-jsx";
|
||||||
|
import AutoImport from "unplugin-auto-import/vite";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
tailwindcss(),
|
||||||
|
vueJsx(),
|
||||||
|
AutoImport({
|
||||||
|
imports: ["vue", "vue-router"],
|
||||||
|
dts: true,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user