优化
18
.claude/settings.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(curl -s -X POST http://localhost:5010/admin/login -H \"Content-Type: application/json\" -d '{\"\"username\"\":\"\"admin\"\",\"\"password\"\":\"\"admin123\"\"}')",
|
||||
"mcp__server-mysql__connect_db",
|
||||
"mcp__server-mysql__query",
|
||||
"Bash(python3 -c \":*)",
|
||||
"Bash(pip3 install:*)",
|
||||
"mcp__server-mysql__execute",
|
||||
"Bash(git rm:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(npx tsc:*)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"/Users/sion/Desktop/projects/monisuo/monisuo-admin/.git"
|
||||
]
|
||||
}
|
||||
}
|
||||
26
.gitignore
vendored
@@ -4,6 +4,11 @@
|
||||
# Log file
|
||||
*.log
|
||||
logs/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# Package Files
|
||||
*.jar
|
||||
@@ -31,13 +36,18 @@ buildNumber.properties
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
.vscode/
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/mcp.json
|
||||
.settings/
|
||||
.project
|
||||
.classpath
|
||||
|
||||
# Node
|
||||
node_modules/
|
||||
dist/
|
||||
dist-ssr/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@@ -46,3 +56,17 @@ Thumbs.db
|
||||
# Local config
|
||||
*.local
|
||||
application-local.yml
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
|
||||
# Editor files
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
stats.html
|
||||
|
||||
79
monisuo-admin/.agents/skills/shadcn-vue-admin/SKILL.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
name: shadcn-vue-admin
|
||||
description: Build and maintain the shadcn-vue-admin Vue 3 + Vite + TypeScript admin dashboard with shadcn-vue, Tailwind, Pinia, Vue Router, i18n, and TanStack Query. Use for UI/layout changes, page additions, routing updates, theme/auth work, and component integration in this repo.
|
||||
license: MIT
|
||||
metadata:
|
||||
repository: Whbbit1999/shadcn-vue-admin
|
||||
package-manager: pnpm
|
||||
framework: vue
|
||||
language: typescript
|
||||
---
|
||||
|
||||
## Purpose and scope
|
||||
|
||||
Maintain this Vue 3 admin dashboard repository: pages and layouts, component integration, routing/auth, theming and i18n, data tables, and form validation.
|
||||
|
||||
## Codebase map
|
||||
|
||||
- App entry: `src/main.ts`, `src/App.vue`
|
||||
- Routing: `src/router/`
|
||||
- Layouts and pages: `src/layouts/`, `src/pages/`
|
||||
- Components: `src/components/` (including shadcn-vue style UI)
|
||||
- State: `src/stores/`
|
||||
- Composables: `src/composables/`
|
||||
- Utils and constants: `src/utils/`, `src/lib/`, `src/constants/`
|
||||
- Plugins: `src/plugins/`
|
||||
|
||||
## References
|
||||
|
||||
- System knowledge map: [references/SYSTEM_KNOWLEDGE_MAP.md](references/SYSTEM_KNOWLEDGE_MAP.md)
|
||||
- Testing strategy: [references/testing-strategy.md](references/testing-strategy.md)
|
||||
|
||||
## Standard workflow
|
||||
|
||||
1. Read existing implementations in the target directory and reuse established patterns and styles.
|
||||
2. Prefer existing shadcn-vue components and shared utilities to avoid duplication.
|
||||
3. Only change public APIs when necessary; avoid large-scale formatting unrelated code.
|
||||
|
||||
## Commands and checks
|
||||
|
||||
- Dev server: `pnpm dev`
|
||||
- Build (CI-like check): `pnpm build`
|
||||
- Lint fix: `pnpm lint:fix`
|
||||
|
||||
Requirements:
|
||||
|
||||
- Run `pnpm build` for any non-copy-only change.
|
||||
- Run `pnpm lint:fix` after code changes.
|
||||
- If you modify core logic (`src/lib/**`, `src/utils/**`, `src/composables/**`, `src/services/**`, `src/router/**`, `src/stores/**`):
|
||||
- If test scripts exist (e.g. `pnpm test`/`pnpm test:unit`), add/update tests and run them.
|
||||
- If no test scripts exist, tests are optional but recommended; include “Testing Notes” in the change description.
|
||||
|
||||
## Design and implementation conventions
|
||||
|
||||
- Use Vue 3 Composition API with TypeScript.
|
||||
- Prefer vee-validate + zod for form validation.
|
||||
- Follow existing theming strategy in `src/assets/` and `src/stores/theme.ts`.
|
||||
- Follow the existing structure for i18n in `src/plugins/i18n/`.
|
||||
|
||||
## Common task guides
|
||||
|
||||
### Add a page
|
||||
|
||||
1. Create a page component under `src/pages/`.
|
||||
2. Register routing/menu via `src/router/` if needed.
|
||||
3. Use existing layouts and shared components for consistent spacing and typography.
|
||||
|
||||
### Add a component
|
||||
|
||||
1. Reuse `src/components/ui/` and existing shadcn-vue components first.
|
||||
2. If it should be shared, place it under `src/components/` to avoid page-level duplication.
|
||||
|
||||
### Update theme/styles
|
||||
|
||||
1. Prefer Tailwind and theme files in `src/assets/`.
|
||||
2. Avoid heavy inline styles; keep components maintainable.
|
||||
|
||||
### Output requirements
|
||||
|
||||
After changes, provide a concise summary and list any commands run (if any).
|
||||
@@ -0,0 +1,118 @@
|
||||
# System Knowledge Map (for agents)
|
||||
|
||||
> This is a “navigation index”. It only keeps the high-level structure and key entry files so AI can locate things quickly.
|
||||
|
||||
## Project Overview
|
||||
|
||||
- Stack: Vue 3 + Vite + TypeScript + TailwindCSS
|
||||
- Routing: `vue-router` (v5+ with automatic routes from `src/pages`) + `vite-plugin-vue-layouts`
|
||||
- State: Pinia (with persistedstate)
|
||||
- Data: Axios + @tanstack/vue-query
|
||||
- Forms: vee-validate + zod
|
||||
- UI: shadcn-vue / reka-ui / lucide-vue-next / vue-sonner
|
||||
|
||||
## Startup Flow
|
||||
|
||||
- `index.html`
|
||||
- `src/main.ts`: creates the app, registers plugins, imports global CSS, loads `src/utils/env`
|
||||
- `src/App.vue`: `<router-view />` + `<Suspense>`, initializes `useSystemTheme()`
|
||||
|
||||
## Build / Generation (Vite)
|
||||
|
||||
- `vite.config.ts`
|
||||
- Alias: `@` -> `src/`
|
||||
- Route generation: `vue-router/vite` (types: `src/types/route-map.d.ts`)
|
||||
- Layouts: `vite-plugin-vue-layouts` (default: `default`)
|
||||
- Auto-import: `src/composables` / `src/constants` / `src/stores` (types: `src/types/auto-import.d.ts`)
|
||||
- Components: `src/components` (types: `src/types/auto-import-components.d.ts`)
|
||||
|
||||
## Routing & Layouts
|
||||
|
||||
- Pages (route source): `src/pages/**`
|
||||
- Router (assembly / scroll behavior / HMR): `src/router/index.ts`
|
||||
- Guards: `src/router/guard/*` (includes auth + nprogress)
|
||||
- Layouts: `src/layouts/*.vue` (default / blank / marketing)
|
||||
|
||||
In page files you can use `<route lang="yaml">` to define meta (commonly: layout/auth). Example YAML:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
# layout can be: false | blank | marketing
|
||||
layout: blank
|
||||
auth: true
|
||||
```
|
||||
|
||||
## State & Theme
|
||||
|
||||
- Stores: `src/stores/*` (`auth.ts`, `theme.ts`)
|
||||
- Theme: `src/composables/use-system-theme.ts` + `src/assets/themes.css`
|
||||
- Dark/Light/System: `src/components/toggle-theme.vue`
|
||||
|
||||
## Data Fetching / API
|
||||
|
||||
- Axios: `src/composables/use-axios.ts`
|
||||
- Vue Query plugin: `src/plugins/tanstack-vue-query/setup.ts`
|
||||
- API modules: `src/services/api/*.api.ts`
|
||||
- Shared response types: `src/services/types/response.type.ts`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
When adding environment variables, make sure to validate/types them in `src/utils/env.ts`.
|
||||
|
||||
## Third-party Plugin Setup
|
||||
|
||||
Plugin initialization entry: `src/plugins/index.ts`
|
||||
|
||||
1. When introducing a third-party plugin that needs configuration, put the setup in `src/plugins/[plugin-name]/setup.ts`.
|
||||
2. Import/register it from `src/plugins/index.ts`.
|
||||
|
||||
## Form Validation
|
||||
|
||||
- Validators: `src/pages/**/validators/*.validator.ts` (zod)
|
||||
- Forms: `src/pages/**/components/*-form.vue` (commonly: `toTypedSchema` + `useForm`)
|
||||
|
||||
## UI Component Directories
|
||||
|
||||
- Base UI: `src/components/ui/**`
|
||||
- Layout components: `src/components/global-layout/**`
|
||||
- Sidebar: `src/components/app-sidebar/**`
|
||||
- Command palette: `src/components/command-menu-panel/**`
|
||||
|
||||
## Page / Module Directory Convention
|
||||
|
||||
> Routes are generated automatically from the file structure.
|
||||
|
||||
- Pages: `src/pages/**/*.vue`
|
||||
- Page components: `src/pages/**/components/**/*.vue`
|
||||
- Validators: `src/pages/**/validators/*.validator.ts`
|
||||
- For data-display pages, table configuration should live in: `src/pages/**/data/**`
|
||||
|
||||
## Key Conventions
|
||||
|
||||
- Routing is file-based: do NOT hand-edit route tables; add/rename/remove pages under `src/pages/**`.
|
||||
- Prefer `<route lang="yaml">` meta over ad-hoc logic (commonly: `meta.layout`, `meta.auth`).
|
||||
- Keep env vars strictly typed/validated in `src/utils/env.ts` before use.
|
||||
- Prefer `@/` (alias to `src/`) imports to avoid brittle relative paths.
|
||||
|
||||
## Common Tasks (Where to Change)
|
||||
|
||||
- Add a new page/route: create `src/pages/<name>.vue` (or `src/pages/<name>/index.vue`) + optional `<route lang="yaml">` meta.
|
||||
- Add/modify a layout: edit `src/layouts/*.vue`, then set `meta.layout` in the page.
|
||||
- Add a plugin: create `src/plugins/<plugin>/setup.ts`, then register it in `src/plugins/index.ts`.
|
||||
- Add an API module: create `src/services/api/*.api.ts`; put shared request/response types in `src/services/types/*` or `src/services/api/types/*`.
|
||||
- Add data fetching: use Axios (`src/composables/use-axios.ts`) + Vue Query (setup: `src/plugins/tanstack-vue-query/setup.ts`).
|
||||
- Add a form: define a zod validator in `src/pages/**/validators/*.validator.ts`, then use it from `src/pages/**/components/*-form.vue`.
|
||||
- Add a store: create `src/stores/*.ts` (Pinia; persistedstate is enabled).
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Auto-generated types/routes: when pages change, TypeScript/IDE may need a restart to pick up updated generated types (e.g. `src/types/route-map.d.ts`).
|
||||
- Auto-imported symbols: composables/constants/stores are auto-imported; name collisions can silently change which symbol you get.
|
||||
- Layout meta values: ensure `meta.layout` matches an actual layout filename (and understand what `layout: false` does in this project).
|
||||
- Env vars: Vite uses `import.meta.env`; do not bypass `src/utils/env.ts` validation.
|
||||
|
||||
## Quick Verification
|
||||
|
||||
- Dev: `pnpm dev`
|
||||
- Lint: `pnpm lint:fix`
|
||||
- Build: `pnpm build`
|
||||
@@ -0,0 +1,36 @@
|
||||
# Testing Strategy
|
||||
|
||||
## Current State
|
||||
|
||||
- This repo currently has no dedicated test runner configured (no `pnpm test` script in `package.json`).
|
||||
- For now, treat `pnpm build` (typecheck + Vite build) as the primary safety net.
|
||||
|
||||
## Policy (Strong Constraints)
|
||||
|
||||
- If you change logic in any of the following areas:
|
||||
- `src/lib/**`, `src/utils/**`
|
||||
- `src/composables/**`
|
||||
- `src/services/**`
|
||||
- `src/router/**`
|
||||
- `src/stores/**`
|
||||
- With a test runner available: automated tests are required in the same change, and you must run the relevant test command.
|
||||
- Without a test runner: tests are optional but strongly recommended; you must include “Testing Notes” in the PR/commit description explaining risk and manual/alternative checks.
|
||||
- Pure UI layout/styling changes may skip tests, but must still pass `pnpm build`.
|
||||
|
||||
## Agent Checklist (When Changing Code)
|
||||
|
||||
1. Run `pnpm lint:fix`.
|
||||
2. Run `pnpm build` to catch TypeScript + build-time issues.
|
||||
3. If a test script exists (e.g. `test`, `test:unit`, `test:e2e`), run the relevant command(s).
|
||||
4. For core logic changes, add/adjust tests (see Policy).
|
||||
|
||||
## What To Test (If Adding Tests Later)
|
||||
|
||||
- Pure logic/utils: unit tests (fast, deterministic).
|
||||
- Composables: unit tests with mocked dependencies.
|
||||
- UI components/pages: component tests only for critical interactions; prefer testing behavior over implementation details.
|
||||
|
||||
## Recommended Tooling (Optional)
|
||||
|
||||
- Unit/component: Vitest + @vue/test-utils
|
||||
- E2E (only if needed): Playwright
|
||||
5
monisuo-admin/.env.example
Normal file
@@ -0,0 +1,5 @@
|
||||
# This is api base url
|
||||
# VITE_API_BASE_URL=https://api.example.com
|
||||
VITE_SERVER_API_URL=http://localhost:3000
|
||||
VITE_SERVER_API_PREFIX=/api
|
||||
VITE_SERVER_API_TIMEOUT=5000
|
||||
35
monisuo-admin/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
stats.html
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
!.vscode/settings.json
|
||||
!.vscode/mcp.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.test
|
||||
.env.production
|
||||
477
monisuo-admin/CHANGELOG.md
Normal file
@@ -0,0 +1,477 @@
|
||||
# Changelog
|
||||
|
||||
## v0.7.7
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.7.6...v0.7.7)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Unify routing metadata format and adjust YAML indentation ([#59](https://github.com/Whbbit1999/shadcn-vue-admin/pull/59))
|
||||
- Update VSCode extensions and settings for improved spell checking and add package manager version update ([86188e7](https://github.com/Whbbit1999/shadcn-vue-admin/commit/86188e7))
|
||||
- Update environment error handling and toast notification delay; remove markdown support from Vite config ([9071161](https://github.com/Whbbit1999/shadcn-vue-admin/commit/9071161))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Refactor layout components and improve sidebar navigation ([#60](https://github.com/Whbbit1999/shadcn-vue-admin/pull/60))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.7.6 ([e5a53ce](https://github.com/Whbbit1999/shadcn-vue-admin/commit/e5a53ce))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.7.6
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.7.5...v0.7.6)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Enhance authentication flow with redirect handling ([#54](https://github.com/Whbbit1999/shadcn-vue-admin/pull/54))
|
||||
- Add pagination constants and integrate into data table components ([#56](https://github.com/Whbbit1999/shadcn-vue-admin/pull/56))
|
||||
- Enhance talk footer with dropdown menu and improved input group layout ([b29a41a](https://github.com/Whbbit1999/shadcn-vue-admin/commit/b29a41a))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- When the sidebar is collapsed, the user is redirected to another page, and the collapsed state is lost. #57 ([#58](https://github.com/Whbbit1999/shadcn-vue-admin/pull/58), [#57](https://github.com/Whbbit1999/shadcn-vue-admin/issues/57))
|
||||
- Update pagination handling for server-side pagination support ([d190ae0](https://github.com/Whbbit1999/shadcn-vue-admin/commit/d190ae0))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Pressing command + k or ctrl + k brings up the command-menu-panel for faster and more intuitive operation. ([#53](https://github.com/Whbbit1999/shadcn-vue-admin/pull/53))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.7.5 ([38fb489](https://github.com/Whbbit1999/shadcn-vue-admin/commit/38fb489))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.7.5
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.7.4...v0.7.5)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add InlineTip component and integrate into SVA Components page ([#51](https://github.com/Whbbit1999/shadcn-vue-admin/pull/51))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Remove unused styles and improve component structure ([#52](https://github.com/Whbbit1999/shadcn-vue-admin/pull/52))
|
||||
|
||||
### 📦 Build
|
||||
|
||||
- Update shadcn-vue components ([#50](https://github.com/Whbbit1999/shadcn-vue-admin/pull/50))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.7.4 ([1d7f3e2](https://github.com/Whbbit1999/shadcn-vue-admin/commit/1d7f3e2))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.7.4
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.7.3...v0.7.4)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add server pagination support to data table components ([#46](https://github.com/Whbbit1999/shadcn-vue-admin/pull/46))
|
||||
- Implement task management API functions and response types ([#47](https://github.com/Whbbit1999/shadcn-vue-admin/pull/47))
|
||||
- Use shadcn-vue chart,remove vue-charts ([#49](https://github.com/Whbbit1999/shadcn-vue-admin/pull/49))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Typo Update README.md ([#48](https://github.com/Whbbit1999/shadcn-vue-admin/pull/48))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.7.3 ([1808df2](https://github.com/Whbbit1999/shadcn-vue-admin/commit/1808df2))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
- WuMingDao ([@WuMingDao](https://github.com/WuMingDao))
|
||||
|
||||
## v0.7.3
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.7.2...v0.7.3)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- The issue with no dynamic updates when deleting multiple rows in a batch demo task or when selecting multiple rows. ([435b0aa](https://github.com/Whbbit1999/shadcn-vue-admin/commit/435b0aa))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Update CHANGELOG.md ([b4eb09b](https://github.com/Whbbit1999/shadcn-vue-admin/commit/b4eb09b))
|
||||
- Reorganize plugin setup and improve imports ([b5cd1be](https://github.com/Whbbit1999/shadcn-vue-admin/commit/b5cd1be))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.7.2
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.7.1...v0.7.2)
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.7.1
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.7.0...v0.7.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Add TwoLayout in global layouts, example in settings/components… ([#38](https://github.com/Whbbit1999/shadcn-vue-admin/pull/38))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.7.0
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.6.1...v0.7.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- StatusBadge component And Copy component #25,#26 ([#29](https://github.com/Whbbit1999/shadcn-vue-admin/pull/29), [#25](https://github.com/Whbbit1999/shadcn-vue-admin/issues/25), [#26](https://github.com/Whbbit1999/shadcn-vue-admin/issues/26))
|
||||
- Table bulk-actions #27 ([#36](https://github.com/Whbbit1999/shadcn-vue-admin/pull/36), [#27](https://github.com/Whbbit1999/shadcn-vue-admin/issues/27))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- The pinia plugin persistedstate plugin is invalid ([#35](https://github.com/Whbbit1999/shadcn-vue-admin/pull/35))
|
||||
- Pinia register in router guard ([fb95179](https://github.com/Whbbit1999/shadcn-vue-admin/commit/fb95179))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.6.1 ([f9bfce8](https://github.com/Whbbit1999/shadcn-vue-admin/commit/f9bfce8))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.6.1
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.6.0...v0.6.1)
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Release v0.6.0 ([9122d34](https://github.com/Whbbit1999/shadcn-vue-admin/commit/9122d34))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.5.1
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.5.0...v0.5.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Toggle content layout block #22 ([#23](https://github.com/Whbbit1999/shadcn-vue-admin/pull/23), [#22](https://github.com/Whbbit1999/shadcn-vue-admin/issues/22))
|
||||
- **command-menu-panel:** Use button in mobile ([47cb816](https://github.com/Whbbit1999/shadcn-vue-admin/commit/47cb816))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.5.0 ([32d2939](https://github.com/Whbbit1999/shadcn-vue-admin/commit/32d2939))
|
||||
|
||||
### 🎨 Styles
|
||||
|
||||
- Add a border to the sidebar after it is selected ([#24](https://github.com/Whbbit1999/shadcn-vue-admin/pull/24))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.5.0
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.4.1...v0.5.0)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- **view-options.vue:** Add table toggle columns status reset action ([#20](https://github.com/Whbbit1999/shadcn-vue-admin/pull/20))
|
||||
- **data-table:** Now we can use this component to quickly generate a… ([#21](https://github.com/Whbbit1999/shadcn-vue-admin/pull/21))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.4.1 ([83c9078](https://github.com/Whbbit1999/shadcn-vue-admin/commit/83c9078))
|
||||
|
||||
### 🎨 Styles
|
||||
|
||||
- Remove basic-header component shadow-sm and padding-x style ([13c6ecf](https://github.com/Whbbit1999/shadcn-vue-admin/commit/13c6ecf))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.4.1
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.4.0...v0.4.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Auth-title component ([ff170c7](https://github.com/Whbbit1999/shadcn-vue-admin/commit/ff170c7))
|
||||
- Add tooltip to sidebar menu button ([febc221](https://github.com/Whbbit1999/shadcn-vue-admin/commit/febc221))
|
||||
- Replace button with sidebar menu button for dropdown trigger ([1c4d6fd](https://github.com/Whbbit1999/shadcn-vue-admin/commit/1c4d6fd))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.4.0 ([a9c5355](https://github.com/Whbbit1999/shadcn-vue-admin/commit/a9c5355))
|
||||
- Packages update and lint ([a45d17a](https://github.com/Whbbit1999/shadcn-vue-admin/commit/a45d17a))
|
||||
|
||||
### 🎨 Styles
|
||||
|
||||
- Sidebar popup style change ([4a20c31](https://github.com/Whbbit1999/shadcn-vue-admin/commit/4a20c31))
|
||||
- Settings/components/profile-form CSS style fine-tuning ([9030b3b](https://github.com/Whbbit1999/shadcn-vue-admin/commit/9030b3b))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
- Onur Köse <kose.onur@gmail.com>
|
||||
- Unknown <whbbit@qq.com>
|
||||
|
||||
## v0.4.0
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.3.4...v0.4.0)
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.3.4 ([6a3b12d](https://github.com/Whbbit1999/shadcn-vue-admin/commit/6a3b12d))
|
||||
- Update packages ([9bb3353](https://github.com/Whbbit1999/shadcn-vue-admin/commit/9bb3353))
|
||||
- Update packages ([7ef29d6](https://github.com/Whbbit1999/shadcn-vue-admin/commit/7ef29d6))
|
||||
|
||||
### 🎨 Styles
|
||||
|
||||
- Language-change button and toggle-theme button use size=button ([3e46f30](https://github.com/Whbbit1999/shadcn-vue-admin/commit/3e46f30))
|
||||
- Replace w-* h-* with the new size-* utility ([6a33e42](https://github.com/Whbbit1999/shadcn-vue-admin/commit/6a33e42))
|
||||
- Settings module page style adjustment ([98b687e](https://github.com/Whbbit1999/shadcn-vue-admin/commit/98b687e))
|
||||
- Auth-title icon change ([f383221](https://github.com/Whbbit1999/shadcn-vue-admin/commit/f383221))
|
||||
- Billing/transaction-catd remove backgroun image ([e55e5c1](https://github.com/Whbbit1999/shadcn-vue-admin/commit/e55e5c1))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Unknown <whbbit@qq.com>
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.3.4
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.3.3...v0.3.4)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Does not overflow the default layout width on desktop screens ([#12](https://github.com/Whbbit1999/shadcn-vue-admin/pull/12))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.3.3 ([0552b7e](https://github.com/Whbbit1999/shadcn-vue-admin/commit/0552b7e))
|
||||
- Packages update ([dabfc66](https://github.com/Whbbit1999/shadcn-vue-admin/commit/dabfc66))
|
||||
- Razor-plan ([#13](https://github.com/Whbbit1999/shadcn-vue-admin/pull/13))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.3.3
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.3.2...v0.3.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- New custom-theme component ([6dfcd56](https://github.com/Whbbit1999/shadcn-vue-admin/commit/6dfcd56))
|
||||
- When the sidebar is collapsed, click the menu to display the secondary menu using dropdown and save the sidebar collapsed state ([#11](https://github.com/Whbbit1999/shadcn-vue-admin/pull/11))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.3.2 ([f42338c](https://github.com/Whbbit1999/shadcn-vue-admin/commit/f42338c))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.3.2
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.3.1...v0.3.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Search-menu dialog error ([0fd2d7e](https://github.com/Whbbit1999/shadcn-vue-admin/commit/0fd2d7e))
|
||||
- Build error, use unplugin-vue-router,router name type ([010122f](https://github.com/Whbbit1999/shadcn-vue-admin/commit/010122f))
|
||||
- Remove unplugin-vue-router, pending migrate-to-unplugin-vue-router branch accomplish ([b2f4f64](https://github.com/Whbbit1999/shadcn-vue-admin/commit/b2f4f64))
|
||||
- Index page remove default layout ([8524cf4](https://github.com/Whbbit1999/shadcn-vue-admin/commit/8524cf4))
|
||||
|
||||
### 📦 Build
|
||||
|
||||
- Use unplugin-vue-router instead of vite-plugin-pages ([c9aaf4a](https://github.com/Whbbit1999/shadcn-vue-admin/commit/c9aaf4a))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.3.0 ([fc500d1](https://github.com/Whbbit1999/shadcn-vue-admin/commit/fc500d1))
|
||||
- **release:** V0.3.1 ([3f80295](https://github.com/Whbbit1999/shadcn-vue-admin/commit/3f80295))
|
||||
- Vite-env.d.ts add unplugin-vue-router/client ([631e5db](https://github.com/Whbbit1999/shadcn-vue-admin/commit/631e5db))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 ([@Whbbit1999](https://github.com/Whbbit1999))
|
||||
|
||||
## v0.3.1
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.3.0...v0.3.1)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use vue-sonner instead of shadcn-vue/toast ([8d020fd](https://github.com/Whbbit1999/shadcn-vue-admin/commit/8d020fd))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.3.0 ([fc500d1](https://github.com/Whbbit1999/shadcn-vue-admin/commit/fc500d1))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.3.0
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.3.0...v0.3.0)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Use vue-sonner instead of shadcn-vue/toast ([8d020fd](https://github.com/Whbbit1999/shadcn-vue-admin/commit/8d020fd))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.2.5
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.2.4...v0.2.5)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- **vite.config.ts:** Build error ([8e3620b](https://github.com/Whbbit1999/shadcn-vue-admin/commit/8e3620b))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Add description and keywords in index.html ([054d626](https://github.com/Whbbit1999/shadcn-vue-admin/commit/054d626))
|
||||
- Update vite ([8a00544](https://github.com/Whbbit1999/shadcn-vue-admin/commit/8a00544))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.2.4
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.2.3...v0.2.4)
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- **release:** V0.2.3 ([2c22989](https://github.com/Whbbit1999/shadcn-vue-admin/commit/2c22989))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.2.3
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.2.2...v0.2.3)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- User module CRUD ([f1cbf66](https://github.com/Whbbit1999/shadcn-vue-admin/commit/f1cbf66))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fix the browser warning of the billing block ([7c77903](https://github.com/Whbbit1999/shadcn-vue-admin/commit/7c77903))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Update packages ([e8b1a23](https://github.com/Whbbit1999/shadcn-vue-admin/commit/e8b1a23))
|
||||
- Update shadcn-vue checkbox component ([e329434](https://github.com/Whbbit1999/shadcn-vue-admin/commit/e329434))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.2.2
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/v0.2.1...v0.2.2)
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Billing-detail card in dark-mode background style ([277b24b](https://github.com/Whbbit1999/shadcn-vue-admin/commit/277b24b))
|
||||
- Data-table view-options component drop-down-menu-checkbox-item status error ([0b7d653](https://github.com/Whbbit1999/shadcn-vue-admin/commit/0b7d653))
|
||||
- Data-table table-columns component checkbox status error ([d0d53fb](https://github.com/Whbbit1999/shadcn-vue-admin/commit/d0d53fb))
|
||||
- Change settings block notifications-form component checkbox component attributes ([ab5a9e9](https://github.com/Whbbit1999/shadcn-vue-admin/commit/ab5a9e9))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Remove release-it and release-it-pnpm, change release to unjs/changelogen ([0c14dcd](https://github.com/Whbbit1999/shadcn-vue-admin/commit/0c14dcd))
|
||||
- Remove package.json file git block ([d28bb67](https://github.com/Whbbit1999/shadcn-vue-admin/commit/d28bb67))
|
||||
- Update vueuse/core package ([b007f3d](https://github.com/Whbbit1999/shadcn-vue-admin/commit/b007f3d))
|
||||
- Update radix-vue to reak-ui ([83ad1ee](https://github.com/Whbbit1999/shadcn-vue-admin/commit/83ad1ee))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
## v0.2.1
|
||||
|
||||
[compare changes](https://github.com/Whbbit1999/shadcn-vue-admin/compare/0.2.0...v0.2.1)
|
||||
|
||||
### 🚀 Enhancements
|
||||
|
||||
- Invite User And Create User demo use Dialog on desktop, use Drawer on mobile. ([f1831e8](https://github.com/Whbbit1999/shadcn-vue-admin/commit/f1831e8))
|
||||
- The App module is synchronized with the app module of Shadcn Admin ([9b2606a](https://github.com/Whbbit1999/shadcn-vue-admin/commit/9b2606a))
|
||||
- Add change theme, like shadcn vue document ([6dc355b](https://github.com/Whbbit1999/shadcn-vue-admin/commit/6dc355b))
|
||||
- Update LICENSE ([221c4ca](https://github.com/Whbbit1999/shadcn-vue-admin/commit/221c4ca))
|
||||
- Change hash history mode to webHistory ([a91c44c](https://github.com/Whbbit1999/shadcn-vue-admin/commit/a91c44c))
|
||||
- The auth module synchronized with the auth module of Shadcn Admin ([3a91093](https://github.com/Whbbit1999/shadcn-vue-admin/commit/3a91093))
|
||||
- Add nprogress and global router guard ([8b6d4b7](https://github.com/Whbbit1999/shadcn-vue-admin/commit/8b6d4b7))
|
||||
- Settings module use shadcn-vue example forms ([0149afa](https://github.com/Whbbit1999/shadcn-vue-admin/commit/0149afa))
|
||||
- When the system color changes, the website icon changes ([6569aa4](https://github.com/Whbbit1999/shadcn-vue-admin/commit/6569aa4))
|
||||
- **Search/Menu:** Search Menu component style change, use stone primary color ([34a6cf8](https://github.com/Whbbit1999/shadcn-vue-admin/commit/34a6cf8))
|
||||
- Search menu component when command item click close the panel ([07abd20](https://github.com/Whbbit1999/shadcn-vue-admin/commit/07abd20))
|
||||
- **DataTable:** Global table components change icon ([c29fdd5](https://github.com/Whbbit1999/shadcn-vue-admin/commit/c29fdd5))
|
||||
- Search menu add 'plans & billings page' ([d8f5286](https://github.com/Whbbit1999/shadcn-vue-admin/commit/d8f5286))
|
||||
- Billings page design change ([6886f3e](https://github.com/Whbbit1999/shadcn-vue-admin/commit/6886f3e))
|
||||
- New scrollbar ([0d6de56](https://github.com/Whbbit1999/shadcn-vue-admin/commit/0d6de56))
|
||||
- Added a example module to talk to ai ([571dd4a](https://github.com/Whbbit1999/shadcn-vue-admin/commit/571dd4a))
|
||||
- Tasks CRUD Demo ([5fd8052](https://github.com/Whbbit1999/shadcn-vue-admin/commit/5fd8052))
|
||||
|
||||
### 🩹 Fixes
|
||||
|
||||
- Fix auto-import-components names error, now shadcn-vue components can auto import ,prefix is UI ([a884695](https://github.com/Whbbit1999/shadcn-vue-admin/commit/a884695))
|
||||
- Shadcn-vue calendar eslint error ([5abac39](https://github.com/Whbbit1999/shadcn-vue-admin/commit/5abac39))
|
||||
- Help-center in darkmode style error ([92d170d](https://github.com/Whbbit1999/shadcn-vue-admin/commit/92d170d))
|
||||
- Ai-talk module type error ([b8c708c](https://github.com/Whbbit1999/shadcn-vue-admin/commit/b8c708c))
|
||||
|
||||
### 💅 Refactors
|
||||
|
||||
- Code format ([62acdbc](https://github.com/Whbbit1999/shadcn-vue-admin/commit/62acdbc))
|
||||
|
||||
### 📦 Build
|
||||
|
||||
- Upgrade packages ([88896c5](https://github.com/Whbbit1999/shadcn-vue-admin/commit/88896c5))
|
||||
|
||||
### 🏡 Chore
|
||||
|
||||
- Fix netlify reload page 404 ([a296949](https://github.com/Whbbit1999/shadcn-vue-admin/commit/a296949))
|
||||
- Error module sidebar name change ([c0f6a05](https://github.com/Whbbit1999/shadcn-vue-admin/commit/c0f6a05))
|
||||
- When deploying vercel you need to add a file profile ([9b841fb](https://github.com/Whbbit1999/shadcn-vue-admin/commit/9b841fb))
|
||||
- Change package.json info ([a413bde](https://github.com/Whbbit1999/shadcn-vue-admin/commit/a413bde))
|
||||
- Update packages ([7f7c872](https://github.com/Whbbit1999/shadcn-vue-admin/commit/7f7c872))
|
||||
- Remove VueQueryDevtools, you can find it in vue-devtools or add it yourself ([67d2c4c](https://github.com/Whbbit1999/shadcn-vue-admin/commit/67d2c4c))
|
||||
- Update packages ([1b8234d](https://github.com/Whbbit1999/shadcn-vue-admin/commit/1b8234d))
|
||||
|
||||
### 🎨 Styles
|
||||
|
||||
- Layout Page components add py-4 ([442f52b](https://github.com/Whbbit1999/shadcn-vue-admin/commit/442f52b))
|
||||
|
||||
### ❤️ Contributors
|
||||
|
||||
- Whbbit1999 <luckyboy_dong@sina.com>
|
||||
|
||||
170
monisuo-admin/IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# Monisuo Admin 实施计划
|
||||
|
||||
## 项目信息
|
||||
|
||||
- 项目名称: Monisuo 管理后台
|
||||
- 技术栈: Vue 3 + shadcn-vue + Tailwind CSS
|
||||
- 开始时间: 2026-03-22
|
||||
- 完成时间: 2026-03-22
|
||||
- 状态: COMPLETE
|
||||
|
||||
## 任务清单
|
||||
|
||||
### 阶段 1: 布局组件封装 ✅
|
||||
|
||||
- [x] BasicPage 组件 - 已有组件可直接使用
|
||||
- [x] 使用现有的 UiTable、UiCard 等组件
|
||||
- [x] 使用现有的 UiDialog 组件
|
||||
- [x] 使用现有的 UiSpinner 组件
|
||||
|
||||
### 阶段 2: 页面优化 ✅
|
||||
|
||||
- [x] 登录页响应式优化
|
||||
- 添加表单验证
|
||||
- 添加密码显示/隐藏
|
||||
- 响应式布局
|
||||
- 移动端 Logo
|
||||
- [x] 资金总览页
|
||||
- 统计卡片展示
|
||||
- 快捷操作入口
|
||||
- 待审批订单预览
|
||||
- [x] 用户管理页
|
||||
- Toast 提示
|
||||
- 分页组件
|
||||
- 用户详情弹窗
|
||||
- 响应式布局(PC表格/移动端卡片)
|
||||
- [x] 币种管理页
|
||||
- Toast 提示
|
||||
- 表单验证
|
||||
- 响应式布局
|
||||
- [x] 订单审批页
|
||||
- Toast 提示
|
||||
- 订单详情弹窗
|
||||
- 分页组件
|
||||
- 筛选功能
|
||||
- 响应式布局
|
||||
|
||||
### 阶段 3: 全局优化 ✅
|
||||
|
||||
- [x] Toast 提示(使用 vue-sonner)
|
||||
- [x] 路由守卫优化(Monisuo 路由保护)
|
||||
- [x] TypeScript 类型检查通过
|
||||
- [x] 构建测试通过
|
||||
|
||||
### 阶段 4: 测试与完善 ✅
|
||||
|
||||
- [x] TypeScript 类型检查
|
||||
- [x] 构建测试
|
||||
- [x] 功能完整性检查
|
||||
|
||||
## 已实现的页面
|
||||
|
||||
### 1. 登录页 (`/auth/monisuo-sign-in.vue`)
|
||||
|
||||
- 响应式布局(PC左右分栏,移动端单列)
|
||||
- 表单验证(用户名、密码必填,密码长度检查)
|
||||
- 密码显示/隐藏切换
|
||||
- 加载状态和错误提示
|
||||
- 美观的品牌展示
|
||||
|
||||
### 2. 资金总览 (`/monisuo/dashboard.vue`)
|
||||
|
||||
- 6个统计卡片(充值、提现、在管资金、交易总值、待审批、用户数)
|
||||
- 快捷操作入口
|
||||
- 待审批订单预览
|
||||
- 响应式布局
|
||||
|
||||
### 3. 用户管理 (`/monisuo/users.vue`)
|
||||
|
||||
- 用户列表(PC表格/移动端卡片)
|
||||
- 搜索功能(用户名、状态筛选)
|
||||
- 分页功能
|
||||
- 用户状态切换(启用/禁用)
|
||||
- 用户详情弹窗
|
||||
- Toast 操作提示
|
||||
|
||||
### 4. 币种管理 (`/monisuo/coins.vue`)
|
||||
|
||||
- 币种列表(PC表格/移动端卡片)
|
||||
- 新增/编辑币种弹窗
|
||||
- 调价功能(仅手动价格类型)
|
||||
- 上下架功能
|
||||
- 表单验证
|
||||
- Toast 操作提示
|
||||
|
||||
### 5. 订单审批 (`/monisuo/orders.vue`)
|
||||
|
||||
- 待审批/全部订单 Tab 切换
|
||||
- 订单列表(PC表格/移动端卡片)
|
||||
- 订单详情弹窗
|
||||
- 审批功能(通过/驳回)
|
||||
- 筛选功能(类型、状态)
|
||||
- 分页功能
|
||||
- Toast 操作提示
|
||||
|
||||
## API 对接情况
|
||||
|
||||
所有 API 已完成对接:
|
||||
|
||||
- ✅ POST /admin/login - 管理员登录
|
||||
- ✅ GET /admin/user/list - 用户列表
|
||||
- ✅ GET /admin/user/detail - 用户详情
|
||||
- ✅ POST /admin/user/status - 禁用/启用用户
|
||||
- ✅ GET /admin/coin/list - 币种列表
|
||||
- ✅ POST /admin/coin/save - 新增/编辑币种
|
||||
- ✅ POST /admin/coin/price - 调整币种价格
|
||||
- ✅ POST /admin/coin/status - 币种上下架
|
||||
- ✅ GET /admin/order/pending - 待审批订单
|
||||
- ✅ GET /admin/order/list - 所有订单
|
||||
- ✅ POST /admin/order/approve - 审批订单
|
||||
- ✅ GET /admin/finance/overview - 资金总览
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 使用的库
|
||||
|
||||
- Vue 3 + TypeScript
|
||||
- shadcn-vue (Radix UI)
|
||||
- Tailwind CSS 4
|
||||
- TanStack Query (vue-query)
|
||||
- Vue Router
|
||||
- Pinia
|
||||
- vue-sonner (Toast)
|
||||
- @iconify/vue (图标)
|
||||
- lucide-vue-next (图标)
|
||||
|
||||
### 响应式设计
|
||||
|
||||
- PC端:表格布局,充分利用大屏幕空间
|
||||
- 移动端:卡片列表布局,简化信息展示
|
||||
- 使用 Tailwind CSS 响应式断点 (md:, lg:, xl:)
|
||||
|
||||
### 路由守卫
|
||||
|
||||
- `/monisuo/*` 路由需要登录认证
|
||||
- 未登录自动重定向到 `/auth/monisuo-sign-in`
|
||||
|
||||
## 进度日志
|
||||
|
||||
### 2026-03-22 03:48
|
||||
|
||||
- ✅ 项目初始化完成
|
||||
- ✅ API 服务层实现
|
||||
- ✅ 基础页面创建
|
||||
- ✅ 认证逻辑实现
|
||||
|
||||
### 2026-03-22 (完成)
|
||||
|
||||
- ✅ 登录页响应式优化和表单验证
|
||||
- ✅ 资金总览页统计卡片和快捷操作
|
||||
- ✅ 用户管理页分页、详情弹窗、响应式布局
|
||||
- ✅ 币种管理页表单验证、响应式布局
|
||||
- ✅ 订单审批页详情弹窗、筛选、分页、响应式布局
|
||||
- ✅ Toast 提示集成
|
||||
- ✅ 路由守卫优化
|
||||
- ✅ TypeScript 类型检查通过
|
||||
- ✅ 构建测试通过
|
||||
|
||||
## 状态
|
||||
|
||||
STATUS: COMPLETE
|
||||
21
monisuo-admin/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Whbbit1999
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
133
monisuo-admin/PROMPT.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# 目标
|
||||
|
||||
完善 Monisuo 管理后台前端,对接所有后端 API。
|
||||
|
||||
## 核心要求
|
||||
|
||||
### 1. 统一风格与布局
|
||||
|
||||
- 所有页面使用统一的设计语言
|
||||
- 相似业务模块使用相同的布局结构
|
||||
- 封装可复用的布局组件(如 PageLayout、DataTable、FormDialog 等)
|
||||
|
||||
### 2. 响应式设计
|
||||
|
||||
- PC 端:充分利用大屏幕空间,展示更多信息
|
||||
- 移动端:优化触摸体验,简化布局,隐藏次要信息
|
||||
- 使用 Tailwind CSS 的响应式断点
|
||||
|
||||
### 3. 现代化 UI
|
||||
|
||||
- 简洁、清晰的视觉层次
|
||||
- 适当的留白和间距
|
||||
- 使用 shadcn-vue 组件库保持一致性
|
||||
- 暗色模式支持
|
||||
|
||||
### 4. 完整功能对接
|
||||
|
||||
已实现的 API 接口(见 src/services/api/monisuo-admin.api.ts):
|
||||
|
||||
- POST /admin/login - 管理员登录
|
||||
- GET /admin/user/list - 用户列表
|
||||
- GET /admin/user/detail - 用户详情
|
||||
- POST /admin/user/status - 禁用/启用用户
|
||||
- GET /admin/coin/list - 币种列表
|
||||
- POST /admin/coin/save - 新增/编辑币种
|
||||
- POST /admin/coin/price - 调整币种价格
|
||||
- POST /admin/coin/status - 币种上下架
|
||||
- GET /admin/order/pending - 待审批订单
|
||||
- GET /admin/order/list - 所有订单
|
||||
- POST /admin/order/approve - 审批订单
|
||||
- GET /admin/finance/overview - 资金总览
|
||||
|
||||
### 5. 需要完成的任务
|
||||
|
||||
#### 页面优化
|
||||
|
||||
1. **登录页** (`/auth/monisuo-sign-in.vue`)
|
||||
- 已完成基础实现
|
||||
- 需要添加表单验证
|
||||
- 响应式布局优化
|
||||
|
||||
2. **资金总览** (`/monisuo/dashboard.vue`)
|
||||
- 已完成基础统计卡片
|
||||
- 添加图表展示(可选:使用 Chart.js 或 ECharts)
|
||||
- 添加快捷操作入口
|
||||
|
||||
3. **用户管理** (`/monisuo/users.vue`)
|
||||
- 已完成列表和状态切换
|
||||
- 添加用户详情弹窗
|
||||
- 添加分页组件
|
||||
- 响应式表格优化
|
||||
|
||||
4. **币种管理** (`/monisuo/coins.vue`)
|
||||
- 已完成 CRUD 功能
|
||||
- 优化表单验证
|
||||
- 响应式布局
|
||||
|
||||
5. **订单审批** (`/monisuo/orders.vue`)
|
||||
- 已完成基础审批功能
|
||||
- 添加订单详情查看
|
||||
- 优化审批流程交互
|
||||
- 添加批量操作(可选)
|
||||
|
||||
#### 布局组件封装
|
||||
|
||||
1. **PageLayout** - 统一页面布局
|
||||
- 标题、描述区域
|
||||
- 操作按钮区域
|
||||
- 内容区域
|
||||
- 支持响应式
|
||||
|
||||
2. **DataTable** - 数据表格组件
|
||||
- 统一的表格样式
|
||||
- 分页支持
|
||||
- 加载状态
|
||||
- 空状态
|
||||
- 响应式(移动端改为卡片列表)
|
||||
|
||||
3. **FormDialog** - 表单弹窗
|
||||
- 统一的弹窗样式
|
||||
- 表单验证
|
||||
- 提交状态
|
||||
|
||||
4. **StatsCard** - 统计卡片
|
||||
- 统一的统计展示
|
||||
- 支持图标、颜色主题
|
||||
- 响应式
|
||||
|
||||
#### 其他优化
|
||||
|
||||
1. 添加全局加载状态
|
||||
2. 添加错误提示 Toast
|
||||
3. 优化路由守卫和权限控制
|
||||
4. 添加页面切换动画
|
||||
5. 移动端侧边栏优化
|
||||
|
||||
## 技术栈
|
||||
|
||||
- Vue 3 + TypeScript
|
||||
- shadcn-vue (Radix UI)
|
||||
- Tailwind CSS 4
|
||||
- TanStack Query (vue-query)
|
||||
- Vue Router
|
||||
- Pinia
|
||||
|
||||
## 参考文件
|
||||
|
||||
- src/services/api/monisuo-admin.api.ts - API 定义
|
||||
- src/pages/monisuo/\*.vue - 已有页面
|
||||
- src/composables/use-auth.ts - 认证逻辑
|
||||
- src/stores/auth.ts - 认证状态
|
||||
|
||||
## 完成标准
|
||||
|
||||
1. 所有 API 接口已对接
|
||||
2. 所有页面响应式布局完成
|
||||
3. PC 和移动端体验良好
|
||||
4. 布局组件已封装并复用
|
||||
5. 代码风格统一
|
||||
6. 无 TypeScript 错误
|
||||
7. 可以正常运行和构建
|
||||
|
||||
完成后在 IMPLEMENTATION_PLAN.md 添加:STATUS: COMPLETE
|
||||
156
monisuo-admin/README-CN.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Shadcn Vue Admin
|
||||
|
||||
[](https://github.com/antfu/eslint-config)
|
||||
[](https://github.com/Whbbit1999/shadcn-vue-admin/blob/main/LICENSE)
|
||||
[](https://vuejs.org/)
|
||||
[](https://vitejs.dev/)
|
||||
[](https://pnpm.io/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
|
||||
[English](./README.md) | 简体中文
|
||||
|
||||
基于 **Shadcn-vue**、**Vue 3.5+** 和 **Vite** 构建的企业级管理仪表板 UI,专注于响应式设计、可访问性与开发者体验。
|
||||
本项目 Fork 自 [shadcn-admin](https://github.com/satnaing/shadcn-admin)
|
||||
|
||||

|
||||
|
||||
> ⚠️ 版本说明:本项目为可直接使用的起始模板,后续将持续新增组件与功能。
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
- ✅ 亮/暗色模式切换,支持 Pinia 持久化存储
|
||||
- ✅ 全局搜索命令面板
|
||||
- ✅ 符合可访问性标准的 shadcn-ui 侧边栏导航
|
||||
- ✅ 8+ 个预构建的功能页面
|
||||
- ✅ 基于 shadcn-vue 扩展的自定义组件库
|
||||
- ✅ 基于文件结构的自动路由生成系统
|
||||
- ✅ 国际化支持(vue-i18n v11+)
|
||||
- ✅ VeeValidate + Zod 表单验证
|
||||
- ✅ TanStack Table/Query & Unovis 数据可视化
|
||||
- ✅ 流畅动画支持(AutoAnimate、Motion-V、TW Animate CSS)
|
||||
|
||||
## 🛠️ 技术栈与版本约束
|
||||
|
||||
| 分类 | 工具与库(主版本号) |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 核心框架 | [Vue 3.5+](https://vuejs.org/), [TypeScript 5.9+](https://www.typescriptlang.org/) |
|
||||
| UI 组件 | [shadcn-vue](https://www.shadcn-vue.com), [reka-ui 2+](https://www.reka-ui.com/), [lucide-vue-next 0+](https://lucide.dev/) |
|
||||
| 构建工具 | [Vite](https://vitejs.dev/), [@vitejs/plugin-vue 6+](https://github.com/vitejs/vite-plugin-vue) |
|
||||
| 状态管理 | [Pinia 3+](https://pinia.vuejs.org/), [pinia-plugin-persistedstate 4+](https://prazdevs.github.io/pinia-plugin-persistedstate/) |
|
||||
| 路由管理 | [vue-router 5+](https://router.vuejs.org/), [vite-plugin-vue-layouts 0.11+](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) |
|
||||
| 样式系统 | [Tailwind CSS 4+](https://tailwindcss.com/), [tailwindcss-animate 1+](https://github.com/jamiebuilds/tailwindcss-animate) |
|
||||
| 数据处理 | [TanStack Vue Query 5+](https://tanstack.com/query/latest), [TanStack Vue Table 8+](https://tanstack.com/table/latest) |
|
||||
| 表单验证 | [VeeValidate 4+](https://vee-validate.logaretm.com/), [Zod 4+](https://zod.dev/) |
|
||||
| 动画效果 | [@formkit/auto-animate 0.9+](https://auto-animate.formkit.com/), [motion-v 1+](https://motion-v.vercel.app/) |
|
||||
| 国际化 | [vue-i18n 11+](https://vue-i18n.intlify.dev/) |
|
||||
| HTTP 客户端 | [axios 1+](https://axios-http.com/) |
|
||||
| 代码规范与格式化 | [ESLint 9+](https://eslint.org/), [@antfu/eslint-config 7+](https://github.com/antfu/eslint-config) |
|
||||
| 开发工具 | [vite-plugin-vue-devtools 8+](https://github.com/webfansplz/vite-plugin-vue-devtools) |
|
||||
| 自动导入 | [unplugin-auto-import 20+](https://github.com/antfu/unplugin-auto-import), [unplugin-vue-components 30+](https://github.com/antfu/unplugin-vue-components) |
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 前置依赖(严格版本要求)
|
||||
|
||||
- Node.js ≥ 22.x(推荐 LTS 版本)
|
||||
- **pnpm 10+**(项目指定包管理器)
|
||||
- TypeScript ≥ 5.9.0
|
||||
|
||||
### 安装步骤
|
||||
|
||||
1. 克隆仓库到本地
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Whbbit1999/shadcn-vue-admin.git
|
||||
```
|
||||
|
||||
2. 进入项目目录
|
||||
|
||||
```bash
|
||||
cd shadcn-vue-admin
|
||||
```
|
||||
|
||||
3. 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
4. 启动开发服务器
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 可用脚本
|
||||
|
||||
```bash
|
||||
pnpm dev # 启动开发服务器
|
||||
pnpm build # 生产构建(包含 TypeScript 类型检查)
|
||||
pnpm preview # 预览生产构建产物
|
||||
pnpm lint # 执行 ESLint 代码检查
|
||||
pnpm lint:fix # 自动修复代码规范问题
|
||||
pnpm release # 使用 bumpp 升级版本
|
||||
```
|
||||
|
||||
## 📖 高级指南
|
||||
|
||||
### 依赖维护
|
||||
|
||||
- 所有项目依赖每周二(UTC/GMT +8:00)更新,以确保安全性与兼容性。
|
||||
- 关键依赖版本严格锁定,避免兼容性问题。
|
||||
- 通过 `simple-git-hooks` + `lint-staged` 启用 Git 钩子(pre-commit),保障代码质量。
|
||||
|
||||
### 主题定制
|
||||
|
||||
如需自定义网站样式,可使用 [tweakcn](https://tweakcn.com/editor/theme) 提供的预设样式:
|
||||
|
||||
1. 从 tweakcn 复制生成的 CSS 变量
|
||||
2. 将其粘贴到项目的 `index.css` 文件中
|
||||
3. 修改 `:root`、`:dark` 和 `@theme inline` 部分即可应用自定义样式
|
||||
|
||||
### 布局定制(嵌套目录无 `index.vue` 场景)
|
||||
|
||||
如果希望 `pages/errors/` 和 `pages/auth/` 等目录下的页面不使用默认布局,可按以下步骤操作:
|
||||
|
||||
#### 步骤 1:创建目录级布局文件
|
||||
|
||||
在 `pages/` 目录下创建与子目录同名的文件,如 `src/pages/errors.vue` 和 `src/pages/auth.vue`,内容如下:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<route lang="yml">
|
||||
meta:
|
||||
layout: false # 禁用默认布局,适用于所有子路由
|
||||
</route>
|
||||
```
|
||||
|
||||
#### 步骤 2:解决冗余路由问题
|
||||
|
||||
上述操作会生成空的父路由(如 `/errors/`、`/auth/`),可通过以下方式修复:
|
||||
|
||||
1. 在目标目录中创建 `index.vue`(如 `pages/errors/index.vue`)
|
||||
2. 在该文件中添加重定向逻辑(基于 Vue 3.5+ 组合式 API):
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
// 示例:重定向至 404 页面
|
||||
router.replace({ name: '/errors/404' })
|
||||
</script>
|
||||
```
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
本项目采用 **MIT 许可证**,详情请参阅 [LICENSE](https://github.com/Whbbit1999/shadcn-vue-admin/blob/main/LICENSE) 文件。
|
||||
|
||||
## 🤝 致谢
|
||||
|
||||
- **开发者**:[Whbbit1999](https://github.com/Whbbit1999)
|
||||
- **原始设计**:[shadcn-admin](https://github.com/satnaing/shadcn-admin)
|
||||
- **核心依赖**:shadcn-vue、Vue.js、Vite、Tailwind CSS
|
||||
144
monisuo-admin/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Shadcn Vue Admin
|
||||
|
||||
[](https://github.com/antfu/eslint-config)
|
||||
[](https://github.com/Whbbit1999/shadcn-vue-admin/blob/main/LICENSE)
|
||||
[](https://vuejs.org/)
|
||||
[](https://vitejs.dev/)
|
||||
[](https://pnpm.io/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
|
||||
[简体中文](./README-CN.md) | English
|
||||
|
||||
A production-grade admin dashboard UI built with **Shadcn-vue**, **Vue 3.5+** and **Vite**, focusing on responsiveness, accessibility and developer experience.
|
||||
Forked from [shadcn-admin](https://github.com/satnaing/shadcn-admin)
|
||||
|
||||

|
||||
|
||||
> ⚠️ Version Note: This is a starter template. New components and features will be continuously added to the project.
|
||||
|
||||
## ✨ Key Features
|
||||
|
||||
- ✅ Light/Dark mode toggle with Pinia persistent state
|
||||
- ✅ Global search command palette
|
||||
- ✅ Accessible shadcn-ui sidebar navigation
|
||||
- ✅ 8+ pre-built functional pages
|
||||
- ✅ Custom component library with shadcn-vue extensions
|
||||
- ✅ Auto-generated routing system (based on file structure)
|
||||
- ✅ Internationalization support (vue-i18n v11+)
|
||||
- ✅ Form validation with VeeValidate + Zod
|
||||
- ✅ Data visualization with TanStack Table/Query & Unovis
|
||||
- ✅ Animation support (AutoAnimate, Motion-V, TW Animate CSS)
|
||||
|
||||
## 🛠️ Tech Stack & Version Constraints
|
||||
|
||||
| Category | Tools & Libraries (Major Versions) |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Core Framework | [Vue 3.5+](https://vuejs.org/), [TypeScript 5.9+](https://www.typescriptlang.org/) |
|
||||
| UI Components | [shadcn-vue](https://www.shadcn-vue.com), [reka-ui 2+](https://www.reka-ui.com/), [lucide-vue-next 0+](https://lucide.dev/) |
|
||||
| Build Tool | [Vite](https://vitejs.dev/), [@vitejs/plugin-vue 6+](https://github.com/vitejs/vite-plugin-vue) |
|
||||
| State Management | [Pinia 3+](https://pinia.vuejs.org/), [pinia-plugin-persistedstate 4+](https://prazdevs.github.io/pinia-plugin-persistedstate/) |
|
||||
| Routing | [vue-router 5+](https://router.vuejs.org/), [vite-plugin-vue-layouts 0.11+](https://github.com/JohnCampionJr/vite-plugin-vue-layouts) |
|
||||
| Styling | [Tailwind CSS 4+](https://tailwindcss.com/), [tailwindcss-animate 1+](https://github.com/jamiebuilds/tailwindcss-animate) |
|
||||
| Data Handling | [TanStack Vue Query 5+](https://tanstack.com/query/latest), [TanStack Vue Table 8+](https://tanstack.com/table/latest) |
|
||||
| Form Validation | [VeeValidate 4+](https://vee-validate.logaretm.com/), [Zod 4+](https://zod.dev/) |
|
||||
| Animation | [@formkit/auto-animate 0.9+](https://auto-animate.formkit.com/), [motion-v 1+](https://motion-v.vercel.app/) |
|
||||
| Internationalization | [vue-i18n 11+](https://vue-i18n.intlify.dev/) |
|
||||
| HTTP Client | [axios 1+](https://axios-http.com/) |
|
||||
| Linting & Formatting | [ESLint 9+](https://eslint.org/), [@antfu/eslint-config 7+](https://github.com/antfu/eslint-config) |
|
||||
| Dev Tools | [vite-plugin-vue-devtools 8+](https://github.com/webfansplz/vite-plugin-vue-devtools) |
|
||||
| Auto Import | [unplugin-auto-import 20+](https://github.com/antfu/unplugin-auto-import), [unplugin-vue-components 30+](https://github.com/antfu/unplugin-vue-components) |
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Prerequisites (Strict Version Requirements)
|
||||
|
||||
- Node.js ≥ 22.x (LTS recommended)
|
||||
- **pnpm 10+** (Project-specified package manager)
|
||||
- TypeScript ≥ 5.9.0
|
||||
|
||||
### Installation
|
||||
|
||||
1. Clone the repository
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Whbbit1999/shadcn-vue-admin.git
|
||||
```
|
||||
|
||||
2. Navigate to project directory
|
||||
|
||||
```bash
|
||||
cd shadcn-vue-admin
|
||||
```
|
||||
|
||||
3. Install dependencies
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
4. Start development server
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Available Scripts
|
||||
|
||||
```bash
|
||||
pnpm dev # Start development server
|
||||
pnpm build # Build for production (vue-tsc + vite build)
|
||||
pnpm preview # Preview production build
|
||||
pnpm lint # ESLint check
|
||||
pnpm lint:fix # Auto fix lint issues
|
||||
pnpm release # Bump version with bumpp
|
||||
```
|
||||
|
||||
## 📖 Advanced Guides
|
||||
|
||||
### Dependency Maintenance
|
||||
|
||||
- All project dependencies are updated every Tuesday (UTC/GMT +8:00) to ensure security and compatibility.
|
||||
- Version constraints are strictly managed (pinned versions for critical dependencies).
|
||||
- Git hooks (pre-commit) are enabled via `simple-git-hooks` + `lint-staged` for code quality.
|
||||
|
||||
### Theme Customization
|
||||
|
||||
If you need to change the website style, you can use the preset styles provided by [tweakcn](https://tweakcn.com/editor/theme). You only need to copy the css variables provided by tweakcn to `index.css` and change the `:root` `:dark` and `@theme inline` parts.
|
||||
|
||||
### Layout Customization (No `index.vue` in nested directories)
|
||||
|
||||
For pages in `pages/errors/` and `pages/auth/` that you don’t want to use the default layout:
|
||||
|
||||
Create a same-name file in `pages/`:
|
||||
`src/pages/errors.vue`
|
||||
`src/pages/auth.vue`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<route lang="yml">
|
||||
meta:
|
||||
layout: false
|
||||
</route>
|
||||
```
|
||||
|
||||
This will generate extra empty routes like `/errors/` and `/auth/`.
|
||||
To fix it, create `index.vue` inside the directory and add redirect:
|
||||
|
||||
```vue
|
||||
<script lang="ts" setup>
|
||||
const router = useRouter()
|
||||
router.replace({ name: '/errors/404' })
|
||||
</script>
|
||||
```
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License** - see the [LICENSE](https://github.com/Whbbit1999/shadcn-vue-admin/blob/main/LICENSE) file for details.
|
||||
|
||||
## 🤝 Credits
|
||||
|
||||
- **Developer**: [Whbbit1999](https://github.com/Whbbit1999)
|
||||
- **Original Design**: [shadcn-admin](https://github.com/satnaing/shadcn-admin)
|
||||
125
monisuo-admin/agents.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Monisuo Admin - Vue 3 管理后台
|
||||
|
||||
## 项目说明
|
||||
|
||||
基于 shadcn-vue-admin 模板的 Monisuo 模拟交易系统管理后台。
|
||||
|
||||
### 技术栈
|
||||
|
||||
- Vue 3 + TypeScript
|
||||
- shadcn-vue (Radix UI primitives)
|
||||
- Tailwind CSS 4
|
||||
- TanStack Query (vue-query)
|
||||
- Vue Router
|
||||
- Pinia
|
||||
|
||||
### 后端 API
|
||||
|
||||
- Base URL: http://localhost:8080
|
||||
- 认证方式: JWT Bearer Token
|
||||
- 接口定义: src/services/api/monisuo-admin.api.ts
|
||||
|
||||
### 功能模块
|
||||
|
||||
1. 登录认证 (/auth/monisuo-sign-in)
|
||||
2. 资金总览 (/monisuo/dashboard)
|
||||
3. 用户管理 (/monisuo/users)
|
||||
4. 币种管理 (/monisuo/coins)
|
||||
5. 订单审批 (/monisuo/orders)
|
||||
|
||||
## 开发命令
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
访问: http://localhost:5173
|
||||
|
||||
### 类型检查
|
||||
|
||||
```bash
|
||||
pnpm type-check
|
||||
```
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 代码格式化
|
||||
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
### 响应式断点
|
||||
|
||||
- sm: 640px
|
||||
- md: 768px
|
||||
- lg: 1024px
|
||||
- xl: 1280px
|
||||
- 2xl: 1536px
|
||||
|
||||
移动端优化重点:
|
||||
|
||||
- 表格改为卡片列表
|
||||
- 隐藏次要信息
|
||||
- 增大触摸区域
|
||||
- 简化导航
|
||||
|
||||
### 组件规范
|
||||
|
||||
1. 使用 Composition API + `<script setup>`
|
||||
2. TypeScript 类型定义
|
||||
3. 使用 shadcn-vue 组件
|
||||
4. Tailwind CSS 样式
|
||||
5. TanStack Query 管理数据
|
||||
|
||||
### Git 提交规范
|
||||
|
||||
- feat: 新功能
|
||||
- fix: 修复
|
||||
- refactor: 重构
|
||||
- style: 样式调整
|
||||
- docs: 文档
|
||||
|
||||
## 后端启动
|
||||
|
||||
在另一个终端运行:
|
||||
|
||||
```bash
|
||||
cd ~/Desktop/projects/monisuo
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
默认管理员账号:
|
||||
|
||||
- 用户名: admin
|
||||
- 密码: admin123
|
||||
|
||||
## 当前状态
|
||||
|
||||
✅ 已完成:
|
||||
|
||||
- 项目初始化
|
||||
- API 服务层
|
||||
- 基础页面框架
|
||||
- 认证逻辑
|
||||
|
||||
⏳ 待完成:
|
||||
|
||||
- 布局组件封装
|
||||
- 响应式优化
|
||||
- 完整功能对接
|
||||
- 用户体验优化
|
||||
20
monisuo-admin/components.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "new-york",
|
||||
"typescript": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "src/assets/index.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"composables": "@/composables",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
39
monisuo-admin/eslint.config.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
import antfu from '@antfu/eslint-config'
|
||||
import pluginQuery from '@tanstack/eslint-plugin-query'
|
||||
|
||||
export default antfu({
|
||||
type: 'app',
|
||||
vue: {
|
||||
overrides: {
|
||||
'vue/block-lang': ['warn', {
|
||||
script: { lang: ['ts', 'tsx'] },
|
||||
}],
|
||||
},
|
||||
},
|
||||
typescript: true,
|
||||
formatters: {
|
||||
css: true,
|
||||
html: true,
|
||||
markdown: 'prettier',
|
||||
},
|
||||
|
||||
ignores: [
|
||||
'**/build/**',
|
||||
'**/components/ui/**',
|
||||
],
|
||||
settings: {
|
||||
'import/core-modules': ['vue-router/auto-routes'],
|
||||
},
|
||||
globals: {
|
||||
definePage: 'readonly',
|
||||
},
|
||||
|
||||
imports: {
|
||||
overrides: {
|
||||
'perfectionist/sort-imports': ['error', {
|
||||
tsconfig: { rootDir: '.' },
|
||||
}],
|
||||
},
|
||||
},
|
||||
...pluginQuery.configs['flat/recommended'],
|
||||
})
|
||||
22
monisuo-admin/index.html
Normal file
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" media="(prefers-color-scheme: dark)" href="/logo.svg" />
|
||||
<link rel="icon" type="image/svg+xml" media="(prefers-color-scheme: light)" href="/logo-black.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>shadcn-vue-admin</title>
|
||||
<meta
|
||||
name="description"
|
||||
content="Admin Dashboard UI crafted with Shadcn-vue, Vue3 and Vite. Built with responsiveness and accessibility in mind."
|
||||
/>
|
||||
<meta
|
||||
name="keywords"
|
||||
content="vue,vue-router,vite,typescript,tailwindcss,shadcn-vue,tanstack-vue-query,tanstack-table,eslint,pinia,pnpm"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
112
monisuo-admin/package.json
Normal file
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"name": "shadcn-vue-admin",
|
||||
"type": "module",
|
||||
"version": "0.13.1",
|
||||
"private": false,
|
||||
"packageManager": "pnpm@10.32.1",
|
||||
"description": "Admin Dashboard UI crafted with Shadcn-vue, Vue3 and Vite. Built with responsiveness and accessibility in mind.",
|
||||
"author": "Whbbit1999",
|
||||
"license": "MIT",
|
||||
"homepage": "https://shadcn-vue-admin.netlify.app/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Whbbit1999/shadcn-vue-admin"
|
||||
},
|
||||
"keywords": [
|
||||
"vue",
|
||||
"vue-router",
|
||||
"vite",
|
||||
"typescript",
|
||||
"tailwindcss",
|
||||
"shadcn-vue",
|
||||
"tanstack-vue-query",
|
||||
"tanstack-table",
|
||||
"eslint",
|
||||
"pinia",
|
||||
"pnpm"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=22.15",
|
||||
"pnpm": ">=10"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "simple-git-hooks",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"release": "npx bumpp"
|
||||
},
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tanstack/vue-query": "^5.92.9",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unovis/ts": "^1.6.4",
|
||||
"@unovis/vue": "^1.6.4",
|
||||
"@vee-validate/zod": "^4.15.1",
|
||||
"@vueuse/core": "^14.2.1",
|
||||
"@vueuse/integrations": "^14.2.1",
|
||||
"axios": "^1.13.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"dayjs": "^1.11.20",
|
||||
"echarts": "^6.0.0",
|
||||
"embla-carousel-autoplay": "^8.6.0",
|
||||
"embla-carousel-vue": "^8.6.0",
|
||||
"lucide-vue-next": "0.577.0",
|
||||
"motion-v": "2.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"reka-ui": "^2.9.2",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"universal-cookie": "^8.0.1",
|
||||
"vaul-vue": "^0.4.1",
|
||||
"vee-validate": "^4.15.1",
|
||||
"vue": "^3.5.30",
|
||||
"vue-echarts": "^8.0.1",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-input-otp": "^0.3.2",
|
||||
"vue-router": "^5.0.3",
|
||||
"vue-sonner": "^2.0.9",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^7.7.2",
|
||||
"@faker-js/faker": "^10.3.0",
|
||||
"@iconify-json/simple-icons": "^1.2.74",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.91.4",
|
||||
"@tanstack/vue-query-devtools": "^6.1.5",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@vitejs/plugin-vue-jsx": "^5.1.5",
|
||||
"autoprefixer": "^10.4.27",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-format": "^2.0.1",
|
||||
"lint-staged": "^16.4.0",
|
||||
"rollup-plugin-visualizer": "^7.0.1",
|
||||
"simple-git-hooks": "^2.13.1",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"typescript": "~5.9.3",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^8.0.0",
|
||||
"vite-plugin-vue-devtools": "^8.1.0",
|
||||
"vite-plugin-vue-layouts": "^0.11.0",
|
||||
"vue-tsc": "^3.2.5"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "pnpm lint-staged",
|
||||
"pre-push": "pnpm build"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "eslint --fix"
|
||||
}
|
||||
}
|
||||
14097
monisuo-admin/pnpm-lock.yaml
generated
Normal file
1
monisuo-admin/public/_redirects
Normal file
@@ -0,0 +1 @@
|
||||
/* /index.html 200
|
||||
1
monisuo-admin/public/logo-black.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-satellite"><path d="M13 7 9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4Z"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
1
monisuo-admin/public/logo.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-satellite"><path d="M13 7 9 3 5 7l4 4"/><path d="m17 11 4 4-4 4-4-4"/><path d="m8 12 4 4 6-6-4-4Z"/><path d="m16 8 3-3"/><path d="M9 21a6 6 0 0 0-6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
BIN
monisuo-admin/public/placeholder.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
monisuo-admin/public/robot.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
monisuo-admin/public/shadcn-vue-admin.png
Normal file
|
After Width: | Height: | Size: 552 KiB |
21
monisuo-admin/src/App.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import Loading from '@/components/loading.vue'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { useSystemTheme } from '@/composables/use-system-theme'
|
||||
|
||||
useSystemTheme()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toaster />
|
||||
|
||||
<Suspense>
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<component :is="Component" :key="route" />
|
||||
</router-view>
|
||||
|
||||
<template #fallback>
|
||||
<Loading />
|
||||
</template>
|
||||
</Suspense>
|
||||
</template>
|
||||
118
monisuo-admin/src/assets/chart-theme.css
Normal file
@@ -0,0 +1,118 @@
|
||||
/* theme red */
|
||||
.theme-red {
|
||||
--chart-1: oklch(0.808 0.114 19.571);
|
||||
--chart-2: oklch(0.637 0.237 25.331);
|
||||
--chart-3: oklch(0.577 0.245 27.325);
|
||||
--chart-4: oklch(0.505 0.213 27.518);
|
||||
--chart-5: oklch(0.444 0.177 26.899);
|
||||
}
|
||||
|
||||
.theme-red.dark {
|
||||
--chart-1: oklch(0.808 0.114 19.571);
|
||||
--chart-2: oklch(0.637 0.237 25.331);
|
||||
--chart-3: oklch(0.577 0.245 27.325);
|
||||
--chart-4: oklch(0.505 0.213 27.518);
|
||||
--chart-5: oklch(0.444 0.177 26.899);
|
||||
}
|
||||
|
||||
/* theme rose */
|
||||
.theme-rose {
|
||||
--chart-1: oklch(0.81 0.117 11.638);
|
||||
--chart-2: oklch(0.645 0.246 16.439);
|
||||
--chart-3: oklch(0.586 0.253 17.585);
|
||||
--chart-4: oklch(0.514 0.222 16.935);
|
||||
--chart-5: oklch(0.455 0.188 13.697);
|
||||
}
|
||||
|
||||
.theme-rose.dark {
|
||||
--chart-1: oklch(0.81 0.117 11.638);
|
||||
--chart-2: oklch(0.645 0.246 16.439);
|
||||
--chart-3: oklch(0.586 0.253 17.585);
|
||||
--chart-4: oklch(0.514 0.222 16.935);
|
||||
--chart-5: oklch(0.455 0.188 13.697);
|
||||
}
|
||||
|
||||
/* theme orange */
|
||||
.theme-orange {
|
||||
--chart-1: oklch(0.837 0.128 66.29);
|
||||
--chart-2: oklch(0.705 0.213 47.604);
|
||||
--chart-3: oklch(0.646 0.222 41.116);
|
||||
--chart-4: oklch(0.553 0.195 38.402);
|
||||
--chart-5: oklch(0.47 0.157 37.304);
|
||||
}
|
||||
|
||||
.theme-orange.dark {
|
||||
--chart-1: oklch(0.837 0.128 66.29);
|
||||
--chart-2: oklch(0.705 0.213 47.604);
|
||||
--chart-3: oklch(0.646 0.222 41.116);
|
||||
--chart-4: oklch(0.553 0.195 38.402);
|
||||
--chart-5: oklch(0.47 0.157 37.304);
|
||||
}
|
||||
|
||||
/* theme green */
|
||||
.theme-green {
|
||||
--chart-1: oklch(0.871 0.15 154.449);
|
||||
--chart-2: oklch(0.723 0.219 149.579);
|
||||
--chart-3: oklch(0.627 0.194 149.214);
|
||||
--chart-4: oklch(0.527 0.154 150.069);
|
||||
--chart-5: oklch(0.448 0.119 151.328);
|
||||
}
|
||||
|
||||
.theme-green.dark {
|
||||
--chart-1: oklch(0.871 0.15 154.449);
|
||||
--chart-2: oklch(0.723 0.219 149.579);
|
||||
--chart-3: oklch(0.627 0.194 149.214);
|
||||
--chart-4: oklch(0.527 0.154 150.069);
|
||||
--chart-5: oklch(0.448 0.119 151.328);
|
||||
}
|
||||
|
||||
/* theme blue */
|
||||
.theme-blue {
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
}
|
||||
|
||||
.theme-blue.dark {
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
}
|
||||
|
||||
/* theme yellow */
|
||||
.theme-yellow {
|
||||
--chart-1: oklch(0.905 0.182 98.111);
|
||||
--chart-2: oklch(0.795 0.184 86.047);
|
||||
--chart-3: oklch(0.681 0.162 75.834);
|
||||
--chart-4: oklch(0.554 0.135 66.442);
|
||||
--chart-5: oklch(0.476 0.114 61.907);
|
||||
}
|
||||
|
||||
.theme-yellow.dark {
|
||||
--chart-1: oklch(0.905 0.182 98.111);
|
||||
--chart-2: oklch(0.795 0.184 86.047);
|
||||
--chart-3: oklch(0.681 0.162 75.834);
|
||||
--chart-4: oklch(0.554 0.135 66.442);
|
||||
--chart-5: oklch(0.476 0.114 61.907);
|
||||
}
|
||||
|
||||
/* theme violet */
|
||||
.theme-violet {
|
||||
--chart-1: oklch(0.811 0.111 293.571);
|
||||
--chart-2: oklch(0.606 0.25 292.717);
|
||||
--chart-3: oklch(0.541 0.281 293.009);
|
||||
--chart-4: oklch(0.491 0.27 292.581);
|
||||
--chart-5: oklch(0.432 0.232 292.759);
|
||||
}
|
||||
|
||||
.theme-violet.dark {
|
||||
--chart-1: oklch(0.811 0.111 293.571);
|
||||
--chart-2: oklch(0.606 0.25 292.717);
|
||||
--chart-3: oklch(0.541 0.281 293.009);
|
||||
--chart-4: oklch(0.491 0.27 292.581);
|
||||
--chart-5: oklch(0.432 0.232 292.759);
|
||||
}
|
||||
4
monisuo-admin/src/assets/icons/arrow-dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="144" height="141" viewBox="0 0 144 141" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M129.189 0.0490494C128.744 0.119441 126.422 0.377545 124.03 0.635648C114.719 1.6446 109.23 2.4893 108.058 3.09936C107.119 3.56864 106.674 4.34295 106.674 5.44576C106.674 6.71281 107.424 7.51058 109.043 7.97986C110.403 8.37875 110.825 8.42567 118.87 9.52847C121.778 9.92736 124.288 10.3028 124.475 10.3732C124.663 10.4436 122.951 11.1006 120.676 11.8749C110.028 15.4414 100.412 20.7677 91.7339 27.9242C88.38 30.7164 81.6957 37.4271 79.2096 40.5009C73.8387 47.2116 69.6874 54.8139 66.5681 63.7302C65.9348 65.4665 65.3484 66.8978 65.2546 66.8978C65.1374 66.8978 63.7771 66.7336 62.2291 66.5693C52.9649 65.5134 43.1847 68.1649 34.1316 74.2186C24.7735 80.46 18.5349 87.7338 10.5371 101.742C2.53943 115.726 -1.0959 127.482 0.287874 135.014C0.89767 138.463 2.0469 140.035 3.97011 140.082C5.28352 140.105 5.37733 139.659 4.20465 139.049C3.05541 138.463 2.6567 137.9 2.32835 136.281C0.616228 128.021 6.24512 113.028 17.4325 96.1104C23.2725 87.241 28.362 81.9147 35.5622 77.1046C43.8649 71.5437 52.7069 69.033 61.1737 69.8308C64.9967 70.1828 64.6917 69.9247 64.1992 72.4822C62.2525 82.5013 63.8005 92.6378 67.9753 97.354C73.1116 103.079 81.9771 102 85.0027 95.2657C86.3395 92.2858 86.3864 87.7103 85.1434 83.9796C83.1498 78.0901 80.007 73.8197 75.4335 70.8163C73.8152 69.7604 70.4848 68.1883 69.875 68.1883C69.359 68.1883 69.4294 67.6487 70.2268 65.3257C72.3377 59.2486 75.457 52.7021 78.4122 48.244C83.2436 40.9232 91.4524 32.5701 99.1687 27.103C105.806 22.4102 113.241 18.5386 120.512 16.0045C123.772 14.8548 129.87 13.1889 130.081 13.3766C130.128 13.447 129.541 14.362 128.791 15.4414C124.78 21.0258 122.716 26.0706 122.388 30.998C122.224 33.7198 122.341 34.588 122.88 34.2595C122.998 34.1891 123.678 32.969 124.405 31.5611C126.281 27.8069 131.722 20.6738 139.579 11.6402C141.127 9.85697 142.652 7.86254 143.027 7.08823C144.552 4.03792 143.52 1.48035 140.377 0.471397C139.439 0.166366 138.102 0.0490408 134.584 0.0255769C132.074 -0.021351 129.635 0.00212153 129.189 0.0490494ZM137.117 4.92955C137.187 5.0234 136.718 5.63346 136.061 6.29045L134.865 7.48712L131.042 6.73627C128.931 6.33739 126.727 5.9385 126.14 5.8681C124.827 5.68039 124.123 5.32843 124.968 5.28151C125.296 5.28151 126.868 5.11725 128.486 4.953C131.3 4.64797 136.812 4.62451 137.117 4.92955ZM71.5168 72.5292C76.2075 74.899 79.4441 78.8175 81.3204 84.355C83.6189 91.1361 81.2266 96.8378 76.0433 96.8847C73.3227 96.9082 70.9773 95.2188 69.5936 92.2389C68.2802 89.4232 67.6938 86.5606 67.5765 82.1259C67.4593 78.3248 67.6 76.4242 68.2333 72.7403L68.4912 71.2856L69.359 71.5906C69.8515 71.7548 70.8132 72.1772 71.5168 72.5292Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
5
monisuo-admin/src/assets/icons/arrow-light.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="144" height="141" viewBox="0 0 144 141" fill="white" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M129.189 0.0490494C128.744 0.119441 126.422 0.377545 124.03 0.635648C114.719 1.6446 109.23 2.4893 108.058 3.09936C107.119 3.56864 106.674 4.34295 106.674 5.44576C106.674 6.71281 107.424 7.51058 109.043 7.97986C110.403 8.37875 110.825 8.42567 118.87 9.52847C121.778 9.92736 124.288 10.3028 124.475 10.3732C124.663 10.4436 122.951 11.1006 120.676 11.8749C110.028 15.4414 100.412 20.7677 91.7339 27.9242C88.38 30.7164 81.6957 37.4271 79.2096 40.5009C73.8387 47.2116 69.6874 54.8139 66.5681 63.7302C65.9348 65.4665 65.3484 66.8978 65.2546 66.8978C65.1374 66.8978 63.7771 66.7336 62.2291 66.5693C52.9649 65.5134 43.1847 68.1649 34.1316 74.2186C24.7735 80.46 18.5349 87.7338 10.5371 101.742C2.53943 115.726 -1.0959 127.482 0.287874 135.014C0.89767 138.463 2.0469 140.035 3.97011 140.082C5.28352 140.105 5.37733 139.659 4.20465 139.049C3.05541 138.463 2.6567 137.9 2.32835 136.281C0.616228 128.021 6.24512 113.028 17.4325 96.1104C23.2725 87.241 28.362 81.9147 35.5622 77.1046C43.8649 71.5437 52.7069 69.033 61.1737 69.8308C64.9967 70.1828 64.6917 69.9247 64.1992 72.4822C62.2525 82.5013 63.8005 92.6378 67.9753 97.354C73.1116 103.079 81.9771 102 85.0027 95.2657C86.3395 92.2858 86.3864 87.7103 85.1434 83.9796C83.1498 78.0901 80.007 73.8197 75.4335 70.8163C73.8152 69.7604 70.4848 68.1883 69.875 68.1883C69.359 68.1883 69.4294 67.6487 70.2268 65.3257C72.3377 59.2486 75.457 52.7021 78.4122 48.244C83.2436 40.9232 91.4524 32.5701 99.1687 27.103C105.806 22.4102 113.241 18.5386 120.512 16.0045C123.772 14.8548 129.87 13.1889 130.081 13.3766C130.128 13.447 129.541 14.362 128.791 15.4414C124.78 21.0258 122.716 26.0706 122.388 30.998C122.224 33.7198 122.341 34.588 122.88 34.2595C122.998 34.1891 123.678 32.969 124.405 31.5611C126.281 27.8069 131.722 20.6738 139.579 11.6402C141.127 9.85697 142.652 7.86254 143.027 7.08823C144.552 4.03792 143.52 1.48035 140.377 0.471397C139.439 0.166366 138.102 0.0490408 134.584 0.0255769C132.074 -0.021351 129.635 0.00212153 129.189 0.0490494ZM137.117 4.92955C137.187 5.0234 136.718 5.63346 136.061 6.29045L134.865 7.48712L131.042 6.73627C128.931 6.33739 126.727 5.9385 126.14 5.8681C124.827 5.68039 124.123 5.32843 124.968 5.28151C125.296 5.28151 126.868 5.11725 128.486 4.953C131.3 4.64797 136.812 4.62451 137.117 4.92955ZM71.5168 72.5292C76.2075 74.899 79.4441 78.8175 81.3204 84.355C83.6189 91.1361 81.2266 96.8378 76.0433 96.8847C73.3227 96.9082 70.9773 95.2188 69.5936 92.2389C68.2802 89.4232 67.6938 86.5606 67.5765 82.1259C67.4593 78.3248 67.6 76.4242 68.2333 72.7403L68.4912 71.2856L69.359 71.5906C69.8515 71.7548 70.8132 72.1772 71.5168 72.5292Z"
|
||||
fill="white" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
143
monisuo-admin/src/assets/index.css
Normal file
@@ -0,0 +1,143 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.145 0 0);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.145 0 0);
|
||||
--primary: oklch(0.205 0 0);
|
||||
--primary-foreground: oklch(0.985 0 0);
|
||||
--secondary: oklch(0.97 0 0);
|
||||
--secondary-foreground: oklch(0.205 0 0);
|
||||
--muted: oklch(0.97 0 0);
|
||||
--muted-foreground: oklch(0.556 0 0);
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--radius: 0.625rem;
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.145 0 0);
|
||||
--sidebar-primary: oklch(0.205 0 0);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(1 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||
--sidebar-border: oklch(0.922 0 0);
|
||||
--sidebar-ring: oklch(0.708 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.269 0 0);
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.205 0 0);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--reka-accordion-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--reka-accordion-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
61
monisuo-admin/src/assets/nprogress.css
Normal file
@@ -0,0 +1,61 @@
|
||||
@reference './index.css';
|
||||
|
||||
/* Make clicks pass-through */
|
||||
#nprogress {
|
||||
@apply pointer-events-none;
|
||||
}
|
||||
|
||||
#nprogress .bar {
|
||||
@apply bg-primary fixed left-0 top-0 z-[1031] h-[2px] w-full;
|
||||
}
|
||||
|
||||
/* Fancy blur effect */
|
||||
#nprogress .peg {
|
||||
@apply absolute right-0 block h-full w-[100px];
|
||||
|
||||
box-shadow:
|
||||
0 0 10px hsl(var(--primary)),
|
||||
0 0 5px hsl(var(--primary));
|
||||
opacity: 1;
|
||||
transform: rotate(3deg) translate(0, -4px);
|
||||
}
|
||||
|
||||
/* Remove these to get rid of the spinner */
|
||||
#nprogress .spinner {
|
||||
@apply fixed right-4 top-4 z-[1031] block;
|
||||
}
|
||||
|
||||
#nprogress .spinner-icon {
|
||||
@apply border-t-primary border-l-primary size-4 rounded-full border-[2px] border-solid border-transparent;
|
||||
|
||||
animation: nprogress-spinner 400ms linear infinite;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent {
|
||||
@apply relative overflow-hidden;
|
||||
}
|
||||
|
||||
.nprogress-custom-parent #nprogress .spinner,
|
||||
.nprogress-custom-parent #nprogress .bar {
|
||||
@apply absolute;
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes nprogress-spinner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
25
monisuo-admin/src/assets/scrollbar.css
Normal file
@@ -0,0 +1,25 @@
|
||||
* {
|
||||
scrollbar-color: #8885 var(--c-border);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar:horizontal {
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--c-border);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #8885;
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #8886;
|
||||
}
|
||||
559
monisuo-admin/src/assets/themes.css
Normal file
@@ -0,0 +1,559 @@
|
||||
/* theme yellow */
|
||||
.theme-yellow {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.795 0.184 86.047);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.795 0.184 86.047);
|
||||
}
|
||||
|
||||
.theme-yellow.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.554 0.135 66.442);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.554 0.135 66.442);
|
||||
}
|
||||
|
||||
/* theme red */
|
||||
.theme-red {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.637 0.237 25.331);
|
||||
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.637 0.237 25.331);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||
}
|
||||
|
||||
.theme-red.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.637 0.237 25.331);
|
||||
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.637 0.237 25.331);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||
}
|
||||
|
||||
/* theme rose */
|
||||
.theme-rose {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.645 0.246 16.439);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.645 0.246 16.439);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
|
||||
.theme-rose.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.645 0.246 16.439);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.645 0.246 16.439);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
|
||||
/* theme orange */
|
||||
.theme-orange {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.705 0.213 47.604);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.705 0.213 47.604);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.705 0.213 47.604);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.213 47.604);
|
||||
}
|
||||
|
||||
.theme-orange.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.646 0.222 41.116);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.646 0.222 41.116);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.646 0.222 41.116);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.646 0.222 41.116);
|
||||
}
|
||||
|
||||
/* theme green */
|
||||
.theme-green {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.723 0.219 149.579);
|
||||
--primary-foreground: oklch(0.982 0.018 155.826);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.723 0.219 149.579);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.723 0.219 149.579);
|
||||
--sidebar-primary-foreground: oklch(0.982 0.018 155.826);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.723 0.219 149.579);
|
||||
}
|
||||
|
||||
.theme-green.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.696 0.17 162.48);
|
||||
--primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.527 0.154 150.069);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.696 0.17 162.48);
|
||||
--sidebar-primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.527 0.154 150.069);
|
||||
}
|
||||
|
||||
/* theme blue */
|
||||
.theme-blue {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.623 0.214 259.815);
|
||||
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.623 0.214 259.815);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||
}
|
||||
|
||||
.theme-blue.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.546 0.245 262.881);
|
||||
--primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.488 0.243 264.376);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.488 0.243 264.376);
|
||||
}
|
||||
|
||||
/* theme yellow */
|
||||
.theme-yellow {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.795 0.184 86.047);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.795 0.184 86.047);
|
||||
}
|
||||
|
||||
.theme-yellow.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.554 0.135 66.442);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.554 0.135 66.442);
|
||||
}
|
||||
|
||||
/* theme violet */
|
||||
.theme-violet {
|
||||
--radius: 0.65rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.606 0.25 292.717);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.606 0.25 292.717);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.606 0.25 292.717);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.606 0.25 292.717);
|
||||
}
|
||||
|
||||
.theme-violet.dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.541 0.281 293.009);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.541 0.281 293.009);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
AudioWaveform,
|
||||
Command,
|
||||
GalleryVerticalEnd,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/composables/use-sidebar'
|
||||
|
||||
import type { SidebarData, Team, User } from '../types'
|
||||
|
||||
const user: User = {
|
||||
name: 'shadcn',
|
||||
email: 'm@example.com',
|
||||
avatar: '/avatars/shadcn.jpg',
|
||||
}
|
||||
|
||||
const teams: Team[] = [
|
||||
{
|
||||
name: 'Acme Inc',
|
||||
logo: GalleryVerticalEnd,
|
||||
plan: 'Enterprise',
|
||||
},
|
||||
{
|
||||
name: 'Acme Corp.',
|
||||
logo: AudioWaveform,
|
||||
plan: 'Startup',
|
||||
},
|
||||
{
|
||||
name: 'Evil Corp.',
|
||||
logo: Command,
|
||||
plan: 'Free',
|
||||
},
|
||||
]
|
||||
|
||||
const { navData } = useSidebar()
|
||||
|
||||
export const sidebarData: SidebarData = {
|
||||
user,
|
||||
teams,
|
||||
navMain: navData.value!,
|
||||
}
|
||||
24
monisuo-admin/src/components/app-sidebar/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
import { sidebarData } from './data/sidebar-data'
|
||||
import NavFooter from './nav-footer.vue'
|
||||
import NavTeam from './nav-team.vue'
|
||||
import TeamSwitcher from './team-switcher.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebar collapsible="icon" class="z-50">
|
||||
<UiSidebarHeader>
|
||||
<TeamSwitcher :teams="sidebarData.teams" />
|
||||
</UiSidebarHeader>
|
||||
|
||||
<UiSidebarContent>
|
||||
<NavTeam :nav-main="sidebarData.navMain" />
|
||||
</UiSidebarContent>
|
||||
|
||||
<UiSidebarFooter>
|
||||
<NavFooter :user="sidebarData.user" />
|
||||
</UiSidebarFooter>
|
||||
|
||||
<UiSidebarRail />
|
||||
</UiSidebar>
|
||||
</template>
|
||||
108
monisuo-admin/src/components/app-sidebar/nav-footer.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
BadgeCheck,
|
||||
Bell,
|
||||
ChevronsUpDown,
|
||||
CreditCard,
|
||||
LogOut,
|
||||
Sparkles,
|
||||
UserRoundCog,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { User } from './types'
|
||||
|
||||
const { user } = defineProps<
|
||||
{ user: User }
|
||||
>()
|
||||
|
||||
const { logout } = useAuth()
|
||||
const { isMobile, open } = useSidebar()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarMenu>
|
||||
<UiSidebarMenuItem>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<UiAvatar class="size-8 rounded-lg">
|
||||
<UiAvatarImage :src="user.avatar" :alt="user.name" />
|
||||
<UiAvatarFallback class="rounded-lg">
|
||||
CN
|
||||
</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ user.name }}</span>
|
||||
<span class="text-xs truncate">{{ user.email }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto size-4" />
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent
|
||||
class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
:side="(isMobile || open) ? 'bottom' : 'right'"
|
||||
align="start"
|
||||
:side-offset="4"
|
||||
>
|
||||
<UiDropdownMenuLabel class="p-0 font-normal">
|
||||
<div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
|
||||
<UiAvatar class="size-8 rounded-lg">
|
||||
<UiAvatarImage :src="user.avatar" :alt="user.name" />
|
||||
<UiAvatarFallback class="rounded-lg">
|
||||
CN
|
||||
</UiAvatarFallback>
|
||||
</UiAvatar>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ user.name }}</span>
|
||||
<span class="text-xs truncate">{{ user.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</UiDropdownMenuLabel>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/billing/')">
|
||||
<Sparkles />
|
||||
Upgrade to Pro
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/billing?type=billing')">
|
||||
<CreditCard />
|
||||
Billing
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuGroup>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/')">
|
||||
<UserRoundCog />
|
||||
Profile
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/account')">
|
||||
<BadgeCheck />
|
||||
Account
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="$router.push('/settings/notifications')">
|
||||
<Bell />
|
||||
Notifications
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuGroup>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem @click="logout">
|
||||
<LogOut />
|
||||
{{ $t('logout') }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</UiSidebarMenuItem>
|
||||
</UiSidebarMenu>
|
||||
</template>
|
||||
91
monisuo-admin/src/components/app-sidebar/nav-team-add.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'
|
||||
|
||||
import { teamAddValidator } from './validators/team.validator'
|
||||
|
||||
const emits = defineEmits(['close'])
|
||||
|
||||
const teamAddFormSchema = toTypedSchema(teamAddValidator)
|
||||
|
||||
const { handleSubmit } = useForm({
|
||||
validationSchema: teamAddFormSchema,
|
||||
initialValues: {},
|
||||
})
|
||||
|
||||
const onSubmit = handleSubmit((values) => {
|
||||
toast('You submitted the following values:', {
|
||||
position: 'top-center',
|
||||
description: h('pre', { class: 'mt-2 w-[340px] rounded-md bg-slate-950 p-4' }, h('code', { class: 'text-white' }, JSON.stringify(values, null, 2))),
|
||||
})
|
||||
|
||||
emits('close')
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UiDialogHeader>
|
||||
<UiDialogTitle>
|
||||
Add New Team
|
||||
</UiDialogTitle>
|
||||
<UiDialogDescription>
|
||||
Add a new team by your self.
|
||||
</UiDialogDescription>
|
||||
</UiDialogHeader>
|
||||
|
||||
<form class="space-y-4" @submit="onSubmit">
|
||||
<FormField v-slot="{ componentField }" name="name">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Name
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the name for the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="slug">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Slug
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the slug for the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
<FormField v-slot="{ componentField }" name="logo">
|
||||
<FormItem>
|
||||
<FormLabel class="text-base">
|
||||
Logo
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<UiInput v-bind="componentField" />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Set the logo of the team.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<div class="flex justify-start mt-4">
|
||||
<UiButton type="submit">
|
||||
Add team
|
||||
</UiButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
108
monisuo-admin/src/components/app-sidebar/nav-team.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ChevronRight,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { NavGroup, NavItem } from './types'
|
||||
|
||||
const { navMain } = defineProps<{
|
||||
navMain: NavGroup[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { state, isMobile } = useSidebar()
|
||||
|
||||
function isCollapsed(menu: NavItem): boolean {
|
||||
const pathname = route.path
|
||||
navMain.forEach((group) => {
|
||||
group.items.forEach((item) => {
|
||||
if (item.url === pathname) {
|
||||
return true
|
||||
}
|
||||
})
|
||||
})
|
||||
return !!menu.items?.some(item => item.url === pathname)
|
||||
}
|
||||
|
||||
function isActive(menu: NavItem): boolean {
|
||||
const pathname = route.path
|
||||
if (menu.url) {
|
||||
return pathname === menu.url
|
||||
}
|
||||
return !!menu.items?.some(item => item.url === pathname)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarGroup v-for="group in navMain" :key="group.title">
|
||||
<UiSidebarGroupLabel>{{ group.title }}</UiSidebarGroupLabel>
|
||||
<UiSidebarMenu>
|
||||
<template v-for="menu in group.items" :key="menu.title">
|
||||
<UiSidebarMenuItem v-if="!menu.items">
|
||||
<UiSidebarMenuButton as-child :is-active="isActive(menu)" :tooltip="menu.title">
|
||||
<router-link :to="menu.url">
|
||||
<component :is="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
</router-link>
|
||||
</UiSidebarMenuButton>
|
||||
</UiSidebarMenuItem>
|
||||
|
||||
<UiSidebarMenuItem v-else>
|
||||
<!-- sidebar expanded -->
|
||||
<UiCollapsible
|
||||
v-if="state !== 'collapsed' || isMobile"
|
||||
as-child :default-open="isCollapsed(menu)"
|
||||
class="group/collapsible"
|
||||
>
|
||||
<UiSidebarMenuItem>
|
||||
<UiCollapsibleTrigger as-child>
|
||||
<UiSidebarMenuButton :tooltip="menu.title">
|
||||
<component :is="menu.icon" v-if="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
<ChevronRight
|
||||
class="ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90"
|
||||
/>
|
||||
</UiSidebarMenuButton>
|
||||
</UiCollapsibleTrigger>
|
||||
</UiSidebarMenuItem>
|
||||
<UiCollapsibleContent>
|
||||
<UiSidebarMenuSub>
|
||||
<UiSidebarMenuSubItem v-for="subItem in menu.items" :key="subItem.title">
|
||||
<UiSidebarMenuSubButton as-child :is-active="isActive(subItem as NavItem)">
|
||||
<router-link :to="subItem?.url || '/'">
|
||||
<component :is="subItem.icon" v-if="subItem.icon" />
|
||||
<span>{{ subItem.title }}</span>
|
||||
</router-link>
|
||||
</UiSidebarMenuSubButton>
|
||||
</UiSidebarMenuSubItem>
|
||||
</UiSidebarMenuSub>
|
||||
</UiCollapsibleContent>
|
||||
</UiCollapsible>
|
||||
|
||||
<!-- sidebar collapsed -->
|
||||
<UiDropdownMenu v-else>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton :tooltip="menu.title">
|
||||
<component :is="menu.icon" v-if="menu.icon" />
|
||||
<span>{{ menu.title }}</span>
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="start" side="right">
|
||||
<UiDropdownMenuLabel>{{ menu.title }}</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem v-for="subItem in menu.items" :key="subItem.title" as-child>
|
||||
<router-link :to="subItem?.url || '/'">
|
||||
<component :is="subItem.icon" v-if="subItem.icon" />
|
||||
<span>{{ subItem.title }}</span>
|
||||
</router-link>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</UiSidebarMenuItem>
|
||||
</template>
|
||||
</UiSidebarMenu>
|
||||
</UiSidebarGroup>
|
||||
</template>
|
||||
100
monisuo-admin/src/components/app-sidebar/team-switcher.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts" setup>
|
||||
import {
|
||||
ChevronsUpDown,
|
||||
Plus,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
import { useSidebar } from '@/components/ui/sidebar'
|
||||
|
||||
import type { Team } from './types'
|
||||
|
||||
const { teams } = defineProps<{
|
||||
teams: Team[]
|
||||
}>()
|
||||
|
||||
const { isMobile, open } = useSidebar()
|
||||
|
||||
const activeTeam = ref<Team>(teams[0])
|
||||
function setActiveTeam(team: Team) {
|
||||
activeTeam.value = team
|
||||
}
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const showComponent = shallowRef<Component | null>(null)
|
||||
type TComponent = 'team-add'
|
||||
|
||||
function handleSelect(command: TComponent) {
|
||||
switch (command) {
|
||||
case 'team-add':
|
||||
showComponent.value = defineAsyncComponent(() => import('./nav-team-add.vue'))
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiSidebarMenu>
|
||||
<UiSidebarMenuItem>
|
||||
<UiDialog v-model:open="isOpen">
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiSidebarMenuButton
|
||||
size="lg"
|
||||
class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center rounded-lg aspect-square size-8 bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<component :is="activeTeam.logo" class="size-4" />
|
||||
</div>
|
||||
<div class="grid flex-1 text-sm leading-tight text-left">
|
||||
<span class="font-semibold truncate">{{ activeTeam.name }}</span>
|
||||
<span class="text-xs truncate">{{ activeTeam.plan }}</span>
|
||||
</div>
|
||||
<ChevronsUpDown class="ml-auto" />
|
||||
</UiSidebarMenuButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent
|
||||
class="w-(--radix-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
align="start"
|
||||
:side="(isMobile || open) ? 'bottom' : 'right'"
|
||||
:side-offset="4"
|
||||
>
|
||||
<UiDropdownMenuLabel class="text-xs text-muted-foreground">
|
||||
Teams
|
||||
</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuItem
|
||||
v-for="(team, index) in teams"
|
||||
:key="team.name"
|
||||
class="gap-2 p-2"
|
||||
@click="setActiveTeam(team)"
|
||||
>
|
||||
<div class="flex items-center justify-center border rounded-sm size-6">
|
||||
<component :is="team.logo" class="size-4 shrink-0" />
|
||||
</div>
|
||||
{{ team.name }}
|
||||
<UiDropdownMenuShortcut>⌘{{ index + 1 }}</UiDropdownMenuShortcut>
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuSeparator />
|
||||
|
||||
<UiDialogTrigger as-child>
|
||||
<UiDropdownMenuItem class="gap-2 p-2" @click.stop="handleSelect('team-add')">
|
||||
<div class="flex items-center justify-center border rounded-md size-6 bg-background">
|
||||
<Plus class="size-4" />
|
||||
</div>
|
||||
<div class="font-medium text-muted-foreground">
|
||||
Add team
|
||||
</div>
|
||||
</UiDropdownMenuItem>
|
||||
</UiDialogTrigger>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
|
||||
<UiDialogContent>
|
||||
<component :is="showComponent" @close="isOpen = false" />
|
||||
</UiDialogContent>
|
||||
</UiDialog>
|
||||
</UiSidebarMenuItem>
|
||||
</UiSidebarMenu>
|
||||
</template>
|
||||
42
monisuo-admin/src/components/app-sidebar/types.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { LucideProps } from 'lucide-vue-next'
|
||||
import type { FunctionalComponent } from 'vue'
|
||||
|
||||
type NavIcon = FunctionalComponent<LucideProps, Record<any, any>, any, Record<any, any>>
|
||||
|
||||
interface BaseNavItem {
|
||||
title: string
|
||||
icon?: NavIcon
|
||||
}
|
||||
|
||||
export type NavItem
|
||||
= | BaseNavItem & {
|
||||
items: (BaseNavItem & { url?: string })[]
|
||||
url?: never
|
||||
isActive?: boolean
|
||||
} | BaseNavItem & {
|
||||
url: string
|
||||
items?: never
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
title: string
|
||||
items: NavItem[]
|
||||
}
|
||||
|
||||
export interface User {
|
||||
name: string
|
||||
avatar: string
|
||||
email: string
|
||||
}
|
||||
|
||||
export interface Team {
|
||||
name: string
|
||||
logo: NavIcon
|
||||
plan: string
|
||||
}
|
||||
|
||||
export interface SidebarData {
|
||||
user: User
|
||||
teams: Team[]
|
||||
navMain: NavGroup[]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const teamAddValidator = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, { error: 'Group name is required' })
|
||||
.max(50, { error: 'Group name must be less than 50 characters' }),
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, { error: 'Group name is required' })
|
||||
.max(50, { error: 'Group name must be less than 50 characters' }),
|
||||
logo: z
|
||||
.string()
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type TeamAddValidator = z.infer<typeof teamAddValidator>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { Moon, Sun, SunMoon } from 'lucide-vue-next'
|
||||
|
||||
import CommandItemHasIcon from './command-item-has-icon.vue'
|
||||
|
||||
defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
|
||||
const mode = useColorMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiCommandGroup heading="Theme">
|
||||
<UiCommandItem value="light" @click="mode = 'light', $emit('click')">
|
||||
<CommandItemHasIcon name="Light" :icon="Sun" />
|
||||
</UiCommandItem>
|
||||
<UiCommandItem value="dark" @click="mode = 'dark', $emit('click')">
|
||||
<CommandItemHasIcon name="Dark" :icon="Moon" />
|
||||
</UiCommandItem>
|
||||
<UiCommandItem value="system" @click="mode = 'auto', $emit('click')">
|
||||
<CommandItemHasIcon name="System" :icon="SunMoon" />
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import { Milestone } from 'lucide-vue-next'
|
||||
|
||||
const { icon } = defineProps<{
|
||||
name: string
|
||||
icon?: Component
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center gap-2">
|
||||
<component :is="icon" v-if="icon" class="size-4" />
|
||||
<Milestone v-else class="size-4" />
|
||||
{{ name }}
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { useSidebar } from '@/composables/use-sidebar'
|
||||
|
||||
import type { NavGroup, NavItem } from '../app-sidebar/types'
|
||||
|
||||
import CommandItemHasIcon from './command-item-has-icon.vue'
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'click'): void
|
||||
}>()
|
||||
|
||||
const { navData, otherPages } = useSidebar()
|
||||
|
||||
function getFlatNavItems(navData: NavGroup[]): NavItem[] {
|
||||
const flatItems: NavItem[] = []
|
||||
navData.forEach((group) => {
|
||||
group.items.forEach((item) => {
|
||||
if (item.items) {
|
||||
flatItems.push(...getFlatNavItems([item as unknown as NavGroup]))
|
||||
}
|
||||
else {
|
||||
flatItems.push(item)
|
||||
}
|
||||
})
|
||||
})
|
||||
return flatItems
|
||||
}
|
||||
|
||||
const commands = getFlatNavItems([...navData.value!, ...otherPages.value!])
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
function commandItemClick(url: string) {
|
||||
emit('click')
|
||||
if (route.fullPath !== url) {
|
||||
router.push(url)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiCommandGroup heading="Pages">
|
||||
<UiCommandItem
|
||||
v-for="command in commands"
|
||||
:key="command.title"
|
||||
:value="command.title"
|
||||
@click="commandItemClick(command.url!)"
|
||||
>
|
||||
<CommandItemHasIcon :name="command.title" :icon="command.icon" />
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</template>
|
||||
66
monisuo-admin/src/components/command-menu-panel/index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import { MenuIcon, SearchIcon } from 'lucide-vue-next'
|
||||
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
|
||||
|
||||
import CommandChangeTheme from './command-change-theme.vue'
|
||||
import CommandToPage from './command-to-page.vue'
|
||||
|
||||
const open = ref(false)
|
||||
|
||||
useEventListener('keydown', (event: KeyboardEvent) => {
|
||||
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault()
|
||||
handleOpenChange()
|
||||
}
|
||||
})
|
||||
|
||||
function handleOpenChange() {
|
||||
open.value = !open.value
|
||||
}
|
||||
|
||||
const firstKey = computed(() => navigator?.userAgent.includes('Mac OS') ? '⌘' : 'Ctrl')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="text-sm items-center justify-between text-muted-foreground border border-border bg-muted/5 px-4 py-2 rounded-md md:min-w-[220px] cursor-pointer hidden md:flex"
|
||||
@click="handleOpenChange"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<SearchIcon class="size-4" />
|
||||
<span class="text-xs font-semibold text-muted-foreground">{{ $t('homePage.searchKeyWords') }}</span>
|
||||
</div>
|
||||
<UiKbd>{{ firstKey }} + k</UiKbd>
|
||||
</div>
|
||||
|
||||
<UiButton variant="outline" size="icon" class="md:hidden" @click="handleOpenChange">
|
||||
<SearchIcon />
|
||||
</UiButton>
|
||||
|
||||
<UiCommandDialog v-model:open="open">
|
||||
<UiCommandInput placeholder="Type a command or search..." />
|
||||
<UiCommandList>
|
||||
<UiCommandEmpty>
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<MenuIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No menu found.</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Try searching for a command or check the spelling.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</UiCommandEmpty>
|
||||
|
||||
<CommandToPage @click="handleOpenChange" />
|
||||
<UiCommandSeparator />
|
||||
<CommandChangeTheme @click="handleOpenChange" />
|
||||
</UiCommandList>
|
||||
</UiCommandDialog>
|
||||
</div>
|
||||
</template>
|
||||
71
monisuo-admin/src/components/confirm-dialog.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<script lang='ts' setup>
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog'
|
||||
|
||||
interface ConfirmDialogProps {
|
||||
isLoading?: boolean
|
||||
disabled?: boolean
|
||||
cancelButtonText?: string
|
||||
confirmButtonText?: string
|
||||
destructive?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
isLoading = false,
|
||||
disabled = false,
|
||||
destructive = false,
|
||||
cancelButtonText = 'Cancel',
|
||||
confirmButtonText = 'Continue',
|
||||
} = defineProps<ConfirmDialogProps>()
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'confirm'): void
|
||||
}>()
|
||||
|
||||
const openModel = defineModel<boolean>('open', {
|
||||
default: false,
|
||||
})
|
||||
|
||||
function handleConfirm() {
|
||||
emits('confirm')
|
||||
openModel.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AlertDialog :open="openModel">
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader class="text-start">
|
||||
<AlertDialogTitle>
|
||||
<slot name="title" />
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription as-child>
|
||||
<slot name="description" />
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<slot />
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel :disabled="isLoading" @click="openModel = false">
|
||||
{{ cancelButtonText }}
|
||||
</AlertDialogCancel>
|
||||
|
||||
<UiButton
|
||||
:variant="destructive ? 'destructive' : 'default'"
|
||||
:disabled="disabled || isLoading"
|
||||
@click="handleConfirm"
|
||||
>
|
||||
{{ confirmButtonText }}
|
||||
</UiButton>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</template>
|
||||
34
monisuo-admin/src/components/custom-error.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
code: number
|
||||
subtitle: string
|
||||
error: string
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="max-w-2xl mx-auto text-center">
|
||||
<h1 class="font-bold text-8xl">
|
||||
{{ code }}
|
||||
</h1>
|
||||
<h2 class="mt-4 text-2xl font-bold">
|
||||
{{ subtitle }}
|
||||
</h2>
|
||||
<p class="text-stone-400">
|
||||
{{ error }}
|
||||
</p>
|
||||
|
||||
<footer class="mt-8">
|
||||
<slot>
|
||||
<div class="flex justify-center gap-2">
|
||||
<UiButton variant="outline" @click="$router.back()">
|
||||
Go Back
|
||||
</UiButton>
|
||||
<UiButton @click="$router.push('/')">
|
||||
Back to Home
|
||||
</UiButton>
|
||||
</div>
|
||||
</slot>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
30
monisuo-admin/src/components/custom-theme/content-layout.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { CONTENT_LAYOUTS } from '@/constants/themes'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { setContentLayout } = themeStore
|
||||
const { contentLayout } = storeToRefs(themeStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1.5 pt-6">
|
||||
<UiLabel for="radius" class="text-xs">
|
||||
Content Layout
|
||||
</UiLabel>
|
||||
<div class="grid grid-cols-2 gap-2 py-1.5">
|
||||
<UiButton
|
||||
v-for="layout in CONTENT_LAYOUTS" :key="layout.label"
|
||||
variant="outline"
|
||||
class="justify-center h-8 px-3"
|
||||
:class="contentLayout === layout.value ? 'border-foreground border-2' : ''"
|
||||
@click="setContentLayout(layout.value)"
|
||||
>
|
||||
<component :is="layout.icon" />
|
||||
{{ layout.label }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
40
monisuo-admin/src/components/custom-theme/custom-color.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { THEME_PRIMARY_COLORS, THEMES } from '@/constants/themes'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { setTheme } = themeStore
|
||||
const { theme: t } = storeToRefs(themeStore)
|
||||
|
||||
watchEffect(() => {
|
||||
document.documentElement.classList.remove(...THEMES.map(theme => `theme-${theme}`))
|
||||
document.documentElement.classList.add(`theme-${t.value}`)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1.5 pt-6">
|
||||
<UiLabel for="radius" class="text-xs">
|
||||
Color
|
||||
</UiLabel>
|
||||
<div class="grid grid-cols-2 gap-2 py-1.5">
|
||||
<UiButton
|
||||
v-for="theme in THEME_PRIMARY_COLORS" :key="theme.theme"
|
||||
variant="outline"
|
||||
class="justify-center h-8 px-3"
|
||||
:class="t === theme.theme ? 'border-foreground border-2' : ''"
|
||||
@click="setTheme(theme.theme)"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
'--theme-primary': theme.primaryColor,
|
||||
}"
|
||||
class="size-2 rounded-full bg-(--theme-primary)"
|
||||
/>
|
||||
<span class="text-xs">{{ theme.theme[0].toUpperCase() }}{{ theme.theme.slice(1) }}</span>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
33
monisuo-admin/src/components/custom-theme/custom-radius.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { RADIUS } from '@/constants/themes'
|
||||
import { useThemeStore } from '@/stores/theme'
|
||||
|
||||
const themeStore = useThemeStore()
|
||||
const { setRadius } = themeStore
|
||||
const { radius } = storeToRefs(themeStore)
|
||||
|
||||
watchEffect(() => {
|
||||
document.documentElement.style.setProperty('--radius', `${radius.value}rem`)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1.5 pt-6">
|
||||
<UiLabel for="radius" class="text-xs">
|
||||
Radius
|
||||
</UiLabel>
|
||||
<div class="grid grid-cols-5 gap-2 py-1.5">
|
||||
<UiButton
|
||||
v-for="rayon in RADIUS" :key="rayon"
|
||||
variant="outline"
|
||||
class="justify-center h-8 px-3"
|
||||
:class="rayon === radius ? 'border-foreground border-2' : ''"
|
||||
@click="setRadius(rayon)"
|
||||
>
|
||||
<span class="text-xs">{{ rayon }}</span>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="grid space-y-1">
|
||||
<h1 class="font-semibold text-md text-foreground">
|
||||
Customize
|
||||
</h1>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Pick a style and color for your components.
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
33
monisuo-admin/src/components/custom-theme/theme-popover.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts" setup>
|
||||
import { Paintbrush } from 'lucide-vue-next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover'
|
||||
|
||||
import ContentLayout from './content-layout.vue'
|
||||
import CustomColor from './custom-color.vue'
|
||||
import CustomRadius from './custom-radius.vue'
|
||||
import CustomThemeTitle from './custom-theme-title.vue'
|
||||
import ToggleColorMode from './toggle-color-mode.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<Paintbrush />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end">
|
||||
<CustomThemeTitle />
|
||||
<CustomColor />
|
||||
<CustomRadius />
|
||||
<ToggleColorMode />
|
||||
<ContentLayout />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</template>
|
||||
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BasicColorSchema } from '@vueuse/core'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
import { Moon, Sun, SunMoon } from 'lucide-vue-next'
|
||||
|
||||
const mode = useColorMode()
|
||||
|
||||
const colorModes: {
|
||||
colorMode: BasicColorSchema
|
||||
icon: Component
|
||||
}[] = [
|
||||
{ colorMode: 'light', icon: Sun },
|
||||
{ colorMode: 'dark', icon: Moon },
|
||||
{ colorMode: 'auto', icon: SunMoon },
|
||||
]
|
||||
|
||||
function setColorMode(colorMode: BasicColorSchema) {
|
||||
mode.value = colorMode
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-1.5 pt-6">
|
||||
<UiLabel for="radius" class="text-xs">
|
||||
Color Mode
|
||||
</UiLabel>
|
||||
<div class="grid grid-cols-3 gap-2 py-1.5">
|
||||
<UiButton
|
||||
v-for="item in colorModes" :key="item.colorMode"
|
||||
variant="outline"
|
||||
class="justify-center items-center h-8 px-3"
|
||||
:class="item.colorMode === mode ? 'border-foreground border-2' : ''"
|
||||
@click="setColorMode(item.colorMode)"
|
||||
>
|
||||
<component :is="item.icon" />
|
||||
<span class="text-xs">{{ item.colorMode }}</span>
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
86
monisuo-admin/src/components/data-table/bulk-actions.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script lang='ts' setup generic="T">
|
||||
import type { Table as VueTable } from '@tanstack/vue-table'
|
||||
|
||||
import { XIcon } from 'lucide-vue-next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface BulkActionsProps<T> {
|
||||
table: VueTable<T>
|
||||
entityName: string
|
||||
}
|
||||
|
||||
const { table, entityName } = defineProps<BulkActionsProps<T>>()
|
||||
|
||||
const selectedRows = computed(() => table.getSelectedRowModel().rows)
|
||||
const selectedCount = computed(() => selectedRows.value.length || 0)
|
||||
|
||||
function handleClearSelection() {
|
||||
table.resetRowSelection()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn(
|
||||
'fixed bottom-6 left-1/2 z-50 -translate-x-1/2 rounded-xl',
|
||||
'transition-all delay-100 duration-300 ease-out hover:scale-105',
|
||||
'focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none',
|
||||
)"
|
||||
>
|
||||
<section
|
||||
v-if="selectedCount" :class="cn(
|
||||
'p-2 shadow-xl',
|
||||
'rounded-xl border',
|
||||
'bg-background/95 supports-backdrop-filter:bg-background/60 backdrop-blur-lg',
|
||||
'flex items-center gap-x-2',
|
||||
)"
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
class="size-6 rounded-full"
|
||||
aria-label="Clear selection"
|
||||
title="Clear selection (Escape)"
|
||||
@click="handleClearSelection"
|
||||
>
|
||||
<XIcon />
|
||||
<span class="sr-only">Clear selection</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Clear selection (Escape)</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Separator
|
||||
class="h-5"
|
||||
orientation="vertical"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<section id="bulk-actions-description" class="flex items-center gap-x-1 text-sm">
|
||||
<UiBadge
|
||||
class="min-w-8 rounded-lg"
|
||||
:aria-label="`${selectedCount} selected`"
|
||||
>
|
||||
{{ selectedCount }}
|
||||
</UiBadge>
|
||||
{{ entityName }} selected
|
||||
</section>
|
||||
|
||||
<Separator
|
||||
class="h-5"
|
||||
orientation="vertical"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
93
monisuo-admin/src/components/data-table/column-header.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Column } from '@tanstack/vue-table'
|
||||
|
||||
import { ArrowDownIcon, ArrowLeftIcon, ArrowRightIcon, ArrowUpIcon, ChevronsUpDownIcon, EyeOffIcon, PinIcon, PinOffIcon } from 'lucide-vue-next'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface DataTableColumnHeaderProps {
|
||||
column: Column<T, any>
|
||||
title: string
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableColumnHeaderProps>()
|
||||
|
||||
const canPinned = computed(() => props.column.getCanPin())
|
||||
const canSorted = computed(() => props.column.getCanSort())
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="canSorted || canPinned" :class="cn('flex items-center space-x-2', $attrs.class ?? '')">
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
>
|
||||
<template v-if="canPinned">
|
||||
<PinIcon v-if="props.column.getIsPinned()" class="ml-2 size-4 text-primary" />
|
||||
</template>
|
||||
|
||||
<span>{{ title }}</span>
|
||||
|
||||
<template v-if="canSorted">
|
||||
<ArrowDownIcon v-if="props.column.getIsSorted() === 'desc'" class="ml-2 size-4" />
|
||||
<ArrowUpIcon v-else-if="props.column.getIsSorted() === 'asc'" class="ml-2 size-4" />
|
||||
<ChevronsUpDownIcon v-else class="ml-2 size-4" />
|
||||
</template>
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
|
||||
<UiDropdownMenuContent align="start">
|
||||
<template v-if="canSorted">
|
||||
<UiDropdownMenuItem @click="props.column.toggleSorting(false)">
|
||||
<ArrowUpIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Asc
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="props.column.toggleSorting(true)">
|
||||
<ArrowDownIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Desc
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="props.column.clearSorting()">
|
||||
<ChevronsUpDownIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Clear Sorting
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuSeparator />
|
||||
</template>
|
||||
|
||||
<UiDropdownMenuItem @click="props.column.toggleVisibility(false)">
|
||||
<EyeOffIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Hide
|
||||
</UiDropdownMenuItem>
|
||||
|
||||
<template v-if="canPinned">
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem @click="props.column.pin('left')">
|
||||
<ArrowLeftIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Pin Left
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="props.column.pin('right')">
|
||||
<ArrowRightIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Pin Right
|
||||
</UiDropdownMenuItem>
|
||||
<UiDropdownMenuItem @click="props.column.pin(false)">
|
||||
<PinOffIcon class="mr-2 size-4 text-muted-foreground/70" />
|
||||
Unpin
|
||||
</UiDropdownMenuItem>
|
||||
</template>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div v-else :class="$attrs?.class ?? ''">
|
||||
{{ title }}
|
||||
</div>
|
||||
</template>
|
||||
82
monisuo-admin/src/components/data-table/data-table.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Column, Table as VueTable } from '@tanstack/vue-table'
|
||||
import type { CSSProperties } from 'vue'
|
||||
|
||||
import { FlexRender } from '@tanstack/vue-table'
|
||||
|
||||
import NoResultFound from '@/components/no-result-found.vue'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
|
||||
import type { DataTableProps } from './types'
|
||||
|
||||
import DataTableLoading from './table-loading.vue'
|
||||
import DataTablePagination from './table-pagination.vue'
|
||||
|
||||
defineProps<DataTableProps<T> & {
|
||||
table: VueTable<T>
|
||||
}>()
|
||||
|
||||
function getCommonPinningStyles(column: Column<T>): CSSProperties {
|
||||
const isPinned = column.getIsPinned()
|
||||
return {
|
||||
left: isPinned === 'left' ? `${column.getStart('left')}px` : undefined,
|
||||
right: isPinned === 'right' ? `${column.getAfter('right')}px` : undefined,
|
||||
position: isPinned ? 'sticky' : 'relative',
|
||||
width: `${column.getSize()}px`,
|
||||
zIndex: isPinned ? 1 : 0,
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<slot name="toolbar" />
|
||||
|
||||
<div class="border rounded-md">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
||||
<TableHead
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
:style="getCommonPinningStyles(header.column)"
|
||||
:class="{ 'bg-background': header.column.getIsPinned() }"
|
||||
>
|
||||
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.header" :props="header.getContext()" />
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody v-if="!loading">
|
||||
<template v-if="table.getRowModel().rows?.length">
|
||||
<TableRow
|
||||
v-for="row in table.getRowModel().rows"
|
||||
:key="row.id"
|
||||
:data-state="row.getIsSelected() && 'selected'"
|
||||
>
|
||||
<TableCell
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
:key="cell.id"
|
||||
:style="getCommonPinningStyles(cell.column)"
|
||||
:class="{ 'bg-background': cell.column.getIsPinned() }"
|
||||
>
|
||||
<FlexRender :render="cell.column.columnDef.cell" :props="cell.getContext()" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
|
||||
<TableRow v-else>
|
||||
<TableCell
|
||||
:colspan="columns.length"
|
||||
class="h-24 text-center"
|
||||
>
|
||||
<NoResultFound />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<DataTableLoading v-if="loading" />
|
||||
</div>
|
||||
|
||||
<DataTablePagination v-if="!loading" :table="table" :server-pagination="serverPagination" />
|
||||
</div>
|
||||
</template>
|
||||
121
monisuo-admin/src/components/data-table/faceted-filter.vue
Normal file
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Column } from '@tanstack/vue-table'
|
||||
|
||||
import { Check, CirclePlus } from 'lucide-vue-next'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { FacetedFilterOption } from './types'
|
||||
|
||||
interface DataTableFacetedFilter {
|
||||
column?: Column<T, any>
|
||||
title?: string
|
||||
options: FacetedFilterOption[]
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableFacetedFilter>()
|
||||
|
||||
const facets = computed(() => props.column?.getFacetedUniqueValues())
|
||||
const selectedValues = computed(() => new Set(props.column?.getFilterValue() as string[]))
|
||||
const filterFunction = (list: DataTableFacetedFilter['options'], term: string) => list.filter(i => i.label.toLowerCase()?.includes(term))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiPopover>
|
||||
<UiPopoverTrigger as-child>
|
||||
<UiButton variant="outline" size="sm" class="h-8 border-dashed">
|
||||
<CirclePlus class="size-4 mr-2" />
|
||||
{{ title }}
|
||||
<template v-if="selectedValues.size > 0">
|
||||
<UiSeparator orientation="vertical" class="h-4 mx-2" />
|
||||
<UiBadge
|
||||
variant="secondary"
|
||||
class="px-1 font-normal rounded-sm lg:hidden"
|
||||
>
|
||||
{{ selectedValues.size }}
|
||||
</UiBadge>
|
||||
<div class="hidden space-x-1 lg:flex">
|
||||
<UiBadge
|
||||
v-if="selectedValues.size > 2"
|
||||
variant="secondary"
|
||||
class="px-1 font-normal rounded-sm"
|
||||
>
|
||||
{{ selectedValues.size }} selected
|
||||
</UiBadge>
|
||||
|
||||
<template v-else>
|
||||
<UiBadge
|
||||
v-for="option in options
|
||||
.filter((option) => selectedValues.has(option.value))"
|
||||
:key="option.value"
|
||||
variant="secondary"
|
||||
class="px-1 font-normal rounded-sm"
|
||||
>
|
||||
{{ option.label }}
|
||||
</UiBadge>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</UiButton>
|
||||
</UiPopoverTrigger>
|
||||
<UiPopoverContent class="w-[200px] p-0" align="start">
|
||||
<UiCommand
|
||||
:filter-function="filterFunction as unknown as any"
|
||||
>
|
||||
<UiCommandInput :placeholder="title" />
|
||||
<UiCommandList>
|
||||
<UiCommandEmpty>No results found.</UiCommandEmpty>
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
v-for="option in options"
|
||||
:key="option.value"
|
||||
:value="option"
|
||||
@select="(_e) => {
|
||||
const isSelected = selectedValues.has(option.value)
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.value)
|
||||
}
|
||||
else {
|
||||
selectedValues.add(option.value)
|
||||
}
|
||||
const filterValues = Array.from(selectedValues)
|
||||
column?.setFilterValue(
|
||||
filterValues.length ? filterValues : undefined,
|
||||
)
|
||||
}"
|
||||
>
|
||||
<div
|
||||
:class="cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
selectedValues.has(option.value)
|
||||
? 'bg-primary'
|
||||
: 'opacity-50 [&_svg]:invisible',
|
||||
)"
|
||||
>
|
||||
<Check :class="cn('h-4 w-4', selectedValues.has(option.value) ? 'text-primary-foreground' : '')" />
|
||||
</div>
|
||||
<component :is="option.icon" v-if="option.icon" class="size-4 mr-2 text-muted-foreground" />
|
||||
<span>{{ option.label }}</span>
|
||||
<span v-if="facets?.get(option.value)" class="flex items-center justify-center size-4 ml-auto font-mono text-xs">
|
||||
{{ facets.get(option.value) }}
|
||||
</span>
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
|
||||
<template v-if="selectedValues.size > 0">
|
||||
<UiCommandSeparator />
|
||||
<UiCommandGroup>
|
||||
<UiCommandItem
|
||||
:value="{ label: 'Clear filters' }"
|
||||
class="justify-center text-center"
|
||||
@select="column?.setFilterValue(undefined)"
|
||||
>
|
||||
Clear filters
|
||||
</UiCommandItem>
|
||||
</UiCommandGroup>
|
||||
</template>
|
||||
</UiCommandList>
|
||||
</UiCommand>
|
||||
</UiPopoverContent>
|
||||
</UiPopover>
|
||||
</template>
|
||||
10
monisuo-admin/src/components/data-table/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export { default as DataTableBulkActions } from './bulk-actions.vue'
|
||||
export { default as DataTableColumnHeader } from './column-header.vue'
|
||||
export { default as DataTable } from './data-table.vue'
|
||||
export { default as DataTableFacetedFilter } from './faceted-filter.vue'
|
||||
export { RadioSelectColumn, SelectColumn } from './table-columns'
|
||||
export { default as DataTableLoading } from './table-loading.vue'
|
||||
export { default as DataTablePagination } from './table-pagination.vue'
|
||||
export type * from './types'
|
||||
export { useGenerateVueTable } from './use-generate-vue-table'
|
||||
export { default as DataTableViewOptions } from './view-options.vue'
|
||||
35
monisuo-admin/src/components/data-table/radio-cell.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { CircleIcon } from 'lucide-vue-next'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
defineProps<{
|
||||
checked: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
role="radio"
|
||||
:aria-checked="checked"
|
||||
:class="
|
||||
cn(
|
||||
'border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'hover:border-ring cursor-pointer',
|
||||
)
|
||||
"
|
||||
@click="emit('click', $event)"
|
||||
>
|
||||
<span
|
||||
v-if="checked"
|
||||
class="relative flex items-center justify-center"
|
||||
>
|
||||
<CircleIcon class="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
49
monisuo-admin/src/components/data-table/table-columns.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
import { h } from 'vue'
|
||||
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
|
||||
import RadioCell from './radio-cell.vue'
|
||||
|
||||
const FIXED_WIDTH_COLUMN = {
|
||||
size: 32,
|
||||
minSize: 32,
|
||||
maxSize: 32,
|
||||
enableResizing: false,
|
||||
} as const
|
||||
|
||||
export const SelectColumn: ColumnDef<any> = {
|
||||
id: 'select',
|
||||
...FIXED_WIDTH_COLUMN,
|
||||
header: ({ table }) => h(Checkbox, {
|
||||
'modelValue': table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate'),
|
||||
'onUpdate:modelValue': value => table.toggleAllPageRowsSelected(!!value),
|
||||
'ariaLabel': 'Select all',
|
||||
}),
|
||||
cell: ({ row }) => h(Checkbox, {
|
||||
'modelValue': row.getIsSelected(),
|
||||
'onUpdate:modelValue': value => row.toggleSelected(!!value),
|
||||
'ariaLabel': 'Select row',
|
||||
}),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}
|
||||
|
||||
export const RadioSelectColumn: ColumnDef<any> = {
|
||||
id: 'radio-select',
|
||||
...FIXED_WIDTH_COLUMN,
|
||||
header: () => null,
|
||||
cell: ({ row, table }) => h(RadioCell, {
|
||||
checked: row.getIsSelected(),
|
||||
onClick: (event: MouseEvent) => {
|
||||
event.stopPropagation()
|
||||
// cancel selection of all rows
|
||||
table.toggleAllRowsSelected(false)
|
||||
// select the current row
|
||||
row.toggleSelected(true)
|
||||
},
|
||||
}),
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="h-120 w-full flex items-center justify-center">
|
||||
<UiSpinner class="size-10" />
|
||||
</div>
|
||||
</template>
|
||||
167
monisuo-admin/src/components/data-table/table-pagination.vue
Normal file
@@ -0,0 +1,167 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
|
||||
import { ChevronLeftIcon, ChevronRightIcon, ChevronsLeft, ChevronsRight } from 'lucide-vue-next'
|
||||
|
||||
import { PAGE_SIZES } from '@/constants/pagination'
|
||||
|
||||
import type { ServerPagination } from './types'
|
||||
|
||||
interface DataTablePaginationProps {
|
||||
table: Table<T>
|
||||
serverPagination?: ServerPagination
|
||||
}
|
||||
const props = defineProps<DataTablePaginationProps>()
|
||||
|
||||
const isServerPagination = computed(() => !!props.serverPagination)
|
||||
|
||||
const currentPage = computed(() => {
|
||||
if (isServerPagination.value && props.serverPagination) {
|
||||
return props.serverPagination.page
|
||||
}
|
||||
return props.table.getState().pagination.pageIndex + 1
|
||||
})
|
||||
|
||||
const currentPageSize = computed(() => {
|
||||
if (isServerPagination.value && props.serverPagination) {
|
||||
return props.serverPagination.pageSize
|
||||
}
|
||||
return props.table.getState().pagination.pageSize
|
||||
})
|
||||
|
||||
const totalPages = computed(() => {
|
||||
if (isServerPagination.value && props.serverPagination) {
|
||||
return Math.ceil(props.serverPagination.total / props.serverPagination.pageSize)
|
||||
}
|
||||
return props.table.getPageCount()
|
||||
})
|
||||
|
||||
const canPreviousPage = computed(() => {
|
||||
if (isServerPagination.value) {
|
||||
return currentPage.value > 1
|
||||
}
|
||||
return props.table.getCanPreviousPage()
|
||||
})
|
||||
|
||||
const canNextPage = computed(() => {
|
||||
if (isServerPagination.value) {
|
||||
return currentPage.value < totalPages.value
|
||||
}
|
||||
return props.table.getCanNextPage()
|
||||
})
|
||||
|
||||
function handlePageSizeChange(value: any) {
|
||||
if (!value)
|
||||
return
|
||||
const newPageSize = Number(value)
|
||||
if (isServerPagination.value && props.serverPagination?.onPageSizeChange) {
|
||||
props.serverPagination.onPageSizeChange(newPageSize)
|
||||
}
|
||||
else {
|
||||
props.table.setPageSize(newPageSize)
|
||||
}
|
||||
}
|
||||
|
||||
function goToFirstPage() {
|
||||
if (isServerPagination.value && props.serverPagination?.onPageChange) {
|
||||
props.serverPagination.onPageChange(1)
|
||||
}
|
||||
else {
|
||||
props.table.setPageIndex(0)
|
||||
}
|
||||
}
|
||||
|
||||
function goToPreviousPage() {
|
||||
if (isServerPagination.value && props.serverPagination?.onPageChange) {
|
||||
props.serverPagination.onPageChange(currentPage.value - 1)
|
||||
}
|
||||
else {
|
||||
props.table.previousPage()
|
||||
}
|
||||
}
|
||||
|
||||
function goToNextPage() {
|
||||
if (isServerPagination.value && props.serverPagination?.onPageChange) {
|
||||
props.serverPagination.onPageChange(currentPage.value + 1)
|
||||
}
|
||||
else {
|
||||
props.table.nextPage()
|
||||
}
|
||||
}
|
||||
|
||||
function goToLastPage() {
|
||||
if (isServerPagination.value && props.serverPagination?.onPageChange) {
|
||||
props.serverPagination.onPageChange(totalPages.value)
|
||||
}
|
||||
else {
|
||||
props.table.setPageIndex(props.table.getPageCount() - 1)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex items-center justify-between px-2 py-2 bg-background">
|
||||
<div class="flex-1" />
|
||||
<div class="flex items-center space-x-6 lg:space-x-8">
|
||||
<div class="flex items-center space-x-2">
|
||||
<p class="hidden text-sm font-medium line-clamp-1 md:block">
|
||||
Rows per page
|
||||
</p>
|
||||
<UiSelect
|
||||
:model-value="`${currentPageSize}`"
|
||||
@update:model-value="handlePageSizeChange"
|
||||
>
|
||||
<UiSelectTrigger class="h-8 w-[70px]">
|
||||
<UiSelectValue :placeholder="`${currentPageSize}`" />
|
||||
</UiSelectTrigger>
|
||||
<UiSelectContent side="top">
|
||||
<UiSelectItem v-for="pageSize in PAGE_SIZES" :key="pageSize" :value="`${pageSize}`">
|
||||
{{ pageSize }}
|
||||
</UiSelectItem>
|
||||
</UiSelectContent>
|
||||
</UiSelect>
|
||||
</div>
|
||||
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||
Page {{ currentPage }} of {{ totalPages }}
|
||||
</div>
|
||||
<div class="flex items-center space-x-2">
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="hidden size-8 p-0 lg:flex"
|
||||
:disabled="!canPreviousPage"
|
||||
@click="goToFirstPage"
|
||||
>
|
||||
<span class="sr-only">Go to first page</span>
|
||||
<ChevronsLeft class="size-4" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="size-8 p-0"
|
||||
:disabled="!canPreviousPage"
|
||||
@click="goToPreviousPage"
|
||||
>
|
||||
<span class="sr-only">Go to previous page</span>
|
||||
<ChevronLeftIcon class="size-4" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="size-8 p-0"
|
||||
:disabled="!canNextPage"
|
||||
@click="goToNextPage"
|
||||
>
|
||||
<span class="sr-only">Go to next page</span>
|
||||
<ChevronRightIcon class="size-4" />
|
||||
</UiButton>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
class="hidden size-8 p-0 lg:flex"
|
||||
:disabled="!canNextPage"
|
||||
@click="goToLastPage"
|
||||
>
|
||||
<span class="sr-only">Go to last page</span>
|
||||
<ChevronsRight class="size-4" />
|
||||
</UiButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
22
monisuo-admin/src/components/data-table/types.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { ColumnDef } from '@tanstack/vue-table'
|
||||
|
||||
export interface FacetedFilterOption {
|
||||
label: string
|
||||
value: string
|
||||
icon?: Component
|
||||
}
|
||||
|
||||
export interface ServerPagination {
|
||||
page: number
|
||||
pageSize: number
|
||||
total: number
|
||||
onPageChange: (page: number) => void
|
||||
onPageSizeChange: (pageSize: number) => void
|
||||
}
|
||||
|
||||
export interface DataTableProps<T> {
|
||||
loading?: boolean
|
||||
columns: ColumnDef<T, any>[]
|
||||
data: T[]
|
||||
serverPagination?: ServerPagination
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { ColumnFiltersState, ColumnPinningState, PaginationState, SortingState, TableOptionsWithReactiveData, VisibilityState } from '@tanstack/vue-table'
|
||||
|
||||
import { getCoreRowModel, getFacetedRowModel, getFacetedUniqueValues, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useVueTable } from '@tanstack/vue-table'
|
||||
|
||||
import { DEFAULT_PAGE_SIZE } from '@/constants/pagination'
|
||||
import { valueUpdater } from '@/lib/utils'
|
||||
|
||||
import type { DataTableProps } from './types'
|
||||
|
||||
export function useGenerateVueTable<T>(props: DataTableProps<T>) {
|
||||
const sorting = ref<SortingState>([])
|
||||
const columnFilters = ref<ColumnFiltersState>([])
|
||||
const columnVisibility = ref<VisibilityState>({})
|
||||
const columnPinning = ref<ColumnPinningState>({ left: [], right: [] })
|
||||
const rowSelection = ref({})
|
||||
const pagination = ref<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE,
|
||||
})
|
||||
|
||||
const useServerPagination = !!props.serverPagination
|
||||
|
||||
const pageIndex = computed(() => {
|
||||
if (useServerPagination && props.serverPagination) {
|
||||
return props.serverPagination.page - 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
const pageSize = computed(() => {
|
||||
if (useServerPagination && props.serverPagination) {
|
||||
return props.serverPagination.pageSize
|
||||
}
|
||||
return DEFAULT_PAGE_SIZE
|
||||
})
|
||||
|
||||
const pageCount = computed(() => {
|
||||
if (useServerPagination && props.serverPagination) {
|
||||
return Math.ceil(props.serverPagination.total / props.serverPagination.pageSize)
|
||||
}
|
||||
return -1
|
||||
})
|
||||
|
||||
const tableConfig: TableOptionsWithReactiveData<T> = {
|
||||
get data() { return props.data },
|
||||
get columns() { return props.columns },
|
||||
state: {
|
||||
get sorting() { return sorting.value },
|
||||
get columnFilters() { return columnFilters.value },
|
||||
get columnVisibility() { return columnVisibility.value },
|
||||
get columnPinning() { return columnPinning.value },
|
||||
get rowSelection() { return rowSelection.value },
|
||||
get pagination() {
|
||||
if (useServerPagination) {
|
||||
return {
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value,
|
||||
}
|
||||
}
|
||||
return pagination.value
|
||||
},
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onSortingChange: updaterOrValue => valueUpdater(updaterOrValue, sorting),
|
||||
onColumnFiltersChange: updaterOrValue => valueUpdater(updaterOrValue, columnFilters),
|
||||
onColumnVisibilityChange: updaterOrValue => valueUpdater(updaterOrValue, columnVisibility),
|
||||
onColumnPinningChange: updaterOrValue => valueUpdater(updaterOrValue, columnPinning),
|
||||
onRowSelectionChange: updaterOrValue => valueUpdater(updaterOrValue, rowSelection),
|
||||
onPaginationChange: updaterOrValue => valueUpdater(updaterOrValue, pagination),
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
}
|
||||
|
||||
if (useServerPagination) {
|
||||
tableConfig.pageCount = pageCount.value
|
||||
tableConfig.manualPagination = true
|
||||
}
|
||||
else {
|
||||
tableConfig.getPaginationRowModel = getPaginationRowModel()
|
||||
}
|
||||
|
||||
const table = useVueTable<T>(tableConfig)
|
||||
|
||||
return table
|
||||
}
|
||||
59
monisuo-admin/src/components/data-table/view-options.vue
Normal file
@@ -0,0 +1,59 @@
|
||||
<script setup lang="ts" generic="T">
|
||||
import type { Table } from '@tanstack/vue-table'
|
||||
|
||||
import { RefreshCcw, Settings2 } from 'lucide-vue-next'
|
||||
|
||||
interface DataTableViewOptionsProps {
|
||||
table: Table<T>
|
||||
}
|
||||
|
||||
const props = defineProps<DataTableViewOptionsProps>()
|
||||
|
||||
const columns = computed(() => props.table.getAllColumns()
|
||||
.filter(
|
||||
column =>
|
||||
typeof column.accessorFn !== 'undefined' && column.getCanHide(),
|
||||
))
|
||||
|
||||
function resetColumnVisible() {
|
||||
columns.value.forEach(column => column.toggleVisibility(true))
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton
|
||||
variant="outline"
|
||||
size="sm"
|
||||
class="hidden h-8 ml-auto lg:flex"
|
||||
>
|
||||
<Settings2 class="size-4 mr-2" />
|
||||
Columns View
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent align="end" class="w-[150px]">
|
||||
<UiDropdownMenuLabel>Toggle columns</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
|
||||
<UiDropdownMenuCheckboxItem
|
||||
v-for="column in columns"
|
||||
:key="column.id"
|
||||
class="capitalize"
|
||||
:model-value="column.getIsVisible()"
|
||||
@update:model-value="(value:boolean) => column.toggleVisibility(!!value)"
|
||||
>
|
||||
{{ column.id }}
|
||||
</UiDropdownMenuCheckboxItem>
|
||||
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuItem
|
||||
class="capitalize"
|
||||
@click="resetColumnVisible"
|
||||
>
|
||||
<RefreshCcw />
|
||||
Reset
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
29
monisuo-admin/src/components/global-layout/basic-header.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { LayoutHeaderProps } from './types'
|
||||
|
||||
defineProps<LayoutHeaderProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
:class="cn(
|
||||
'flex flex-col md:flex-row gap-2 justify-between py-2',
|
||||
sticky ? 'sticky top-0 z-40 bg-background' : '',
|
||||
)"
|
||||
>
|
||||
<main>
|
||||
<h1 class="text-2xl font-bold">
|
||||
{{ title }}
|
||||
</h1>
|
||||
<p v-if="description" class="text-muted-foreground">
|
||||
{{ description }}
|
||||
</p>
|
||||
</main>
|
||||
|
||||
<aside class="flex items-center gap-2 flex-wrap">
|
||||
<slot name="actions" />
|
||||
</aside>
|
||||
</header>
|
||||
</template>
|
||||
25
monisuo-admin/src/components/global-layout/basic-page.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts" setup>
|
||||
import type { LayoutHeaderProps } from './types'
|
||||
|
||||
import BasicHeader from './basic-header.vue'
|
||||
|
||||
defineProps<LayoutHeaderProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<BasicHeader
|
||||
:title="title"
|
||||
:description="description"
|
||||
:sticky="sticky"
|
||||
>
|
||||
<template #actions>
|
||||
<slot name="actions" />
|
||||
</template>
|
||||
</BasicHeader>
|
||||
|
||||
<main class="py-4">
|
||||
<slot />
|
||||
</main>
|
||||
</main>
|
||||
</template>
|
||||
6
monisuo-admin/src/components/global-layout/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export { default as BasicHeader } from './basic-header.vue'
|
||||
export { default as BasicPage } from './basic-page.vue'
|
||||
export { default as TwoColAside } from './two-col-aside.vue'
|
||||
export { default as TwoColLayout } from './two-col.vue'
|
||||
|
||||
export type * from './types'
|
||||
48
monisuo-admin/src/components/global-layout/two-col-aside.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script setup lang="ts">
|
||||
import { ChevronsUpDownIcon } from 'lucide-vue-next'
|
||||
|
||||
import type { TwoColAsideNavItem } from './types'
|
||||
|
||||
const props = defineProps<{
|
||||
nav: TwoColAsideNavItem[]
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const currentPath = computed(() => route.path)
|
||||
const activeClass = 'text-primary font-semibold bg-primary/5'
|
||||
|
||||
const currentLink = computed(() => props.nav.find(link => link.url === currentPath.value))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="flex flex-col gap-2">
|
||||
<router-link
|
||||
v-for="link in props.nav" :key="link.url"
|
||||
:to="link.url"
|
||||
class="items-center hidden px-2 py-1 rounded-md lg:flex hover:bg-primary/5"
|
||||
:class="link.url === currentPath ? activeClass : ''"
|
||||
>
|
||||
<component :is="link.icon" class="size-4 mr-1" />
|
||||
<span>{{ link.title }}</span>
|
||||
</router-link>
|
||||
|
||||
<UiDropdownMenu class="lg:hidden">
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton variant="outline" class="w-48 lg:hidden">
|
||||
<component :is="currentLink?.icon" class="size-4 mr-1" />
|
||||
<span>{{ currentLink?.title }}</span>
|
||||
<ChevronsUpDownIcon class="size-4 ml-auto" />
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent class="w-48" align="start">
|
||||
<UiDropdownMenuItem
|
||||
v-for="link in props.nav" :key="link.url"
|
||||
@click="$router.push(link.url)"
|
||||
>
|
||||
<component :is="link.icon" class="size-4 mr-1" />
|
||||
{{ link.title }}
|
||||
</UiDropdownMenuItem>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</nav>
|
||||
</template>
|
||||
19
monisuo-admin/src/components/global-layout/two-col.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang='ts' setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn(
|
||||
`grid grid-cols-1 lg:grid-cols-[200px_1fr] gap-4 w-full`,
|
||||
)"
|
||||
>
|
||||
<aside>
|
||||
<slot name="aside" />
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
<slot name="default" />
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
13
monisuo-admin/src/components/global-layout/types.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Component } from 'vue'
|
||||
|
||||
export interface LayoutHeaderProps {
|
||||
title: string
|
||||
description: string
|
||||
sticky?: boolean
|
||||
}
|
||||
|
||||
export interface TwoColAsideNavItem {
|
||||
title: string
|
||||
url: string
|
||||
icon?: Component
|
||||
}
|
||||
185
monisuo-admin/src/components/inspira-ui/flickering-grid.vue
Normal file
@@ -0,0 +1,185 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface FlickeringGridProps {
|
||||
squareSize?: number
|
||||
gridGap?: number
|
||||
flickerChance?: number
|
||||
color?: string
|
||||
width?: number
|
||||
height?: number
|
||||
class?: string
|
||||
maxOpacity?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FlickeringGridProps>(), {
|
||||
squareSize: 4,
|
||||
gridGap: 6,
|
||||
flickerChance: 0.3,
|
||||
color: 'rgb(0, 0, 0)',
|
||||
maxOpacity: 0.3,
|
||||
})
|
||||
|
||||
const { squareSize, gridGap, flickerChance, color, maxOpacity, width, height } = toRefs(props)
|
||||
|
||||
const containerRef = useTemplateRef<HTMLDivElement>('containerRef')
|
||||
const canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')
|
||||
const context = ref<CanvasRenderingContext2D>()
|
||||
|
||||
const isInView = ref(false)
|
||||
const canvasSize = ref({ width: 0, height: 0 })
|
||||
|
||||
const hexColorRegex = /^#/
|
||||
const computedColor = computed(() => {
|
||||
if (!context.value)
|
||||
return 'rgba(255, 0, 0,'
|
||||
|
||||
const hex = color.value.replace(hexColorRegex, '')
|
||||
const bigint = Number.parseInt(hex, 16)
|
||||
const r = (bigint >> 16) & 255
|
||||
const g = (bigint >> 8) & 255
|
||||
const b = bigint & 255
|
||||
return `rgba(${r}, ${g}, ${b},`
|
||||
})
|
||||
|
||||
function setupCanvas(
|
||||
canvas: HTMLCanvasElement,
|
||||
width: number,
|
||||
height: number,
|
||||
): {
|
||||
cols: number
|
||||
rows: number
|
||||
squares: Float32Array
|
||||
dpr: number
|
||||
} {
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
canvas.style.width = `${width}px`
|
||||
canvas.style.height = `${height}px`
|
||||
|
||||
const cols = Math.floor(width / (squareSize.value + gridGap.value))
|
||||
const rows = Math.floor(height / (squareSize.value + gridGap.value))
|
||||
|
||||
const squares = new Float32Array(cols * rows)
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
squares[i] = Math.random() * maxOpacity.value
|
||||
}
|
||||
return { cols, rows, squares, dpr }
|
||||
}
|
||||
|
||||
function updateSquares(squares: Float32Array, deltaTime: number) {
|
||||
for (let i = 0; i < squares.length; i++) {
|
||||
if (Math.random() < flickerChance.value * deltaTime) {
|
||||
squares[i] = Math.random() * maxOpacity.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawGrid(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
width: number,
|
||||
height: number,
|
||||
cols: number,
|
||||
rows: number,
|
||||
squares: Float32Array,
|
||||
dpr: number,
|
||||
) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
ctx.fillStyle = 'transparent'
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
for (let i = 0; i < cols; i++) {
|
||||
for (let j = 0; j < rows; j++) {
|
||||
const opacity = squares[i * rows + j]
|
||||
ctx.fillStyle = `${computedColor.value}${opacity})`
|
||||
ctx.fillRect(
|
||||
i * (squareSize.value + gridGap.value) * dpr,
|
||||
j * (squareSize.value + gridGap.value) * dpr,
|
||||
squareSize.value * dpr,
|
||||
squareSize.value * dpr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const gridParams = ref<ReturnType<typeof setupCanvas>>()
|
||||
|
||||
function updateCanvasSize() {
|
||||
const newWidth = width.value || containerRef.value!.clientWidth
|
||||
const newHeight = height.value || containerRef.value!.clientHeight
|
||||
|
||||
canvasSize.value = { width: newWidth, height: newHeight }
|
||||
gridParams.value = setupCanvas(canvasRef.value!, newWidth, newHeight)
|
||||
}
|
||||
|
||||
let animationFrameId: number | undefined
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let intersectionObserver: IntersectionObserver | undefined
|
||||
let lastTime = 0
|
||||
|
||||
function animate(time: number) {
|
||||
if (!isInView.value)
|
||||
return
|
||||
|
||||
const deltaTime = (time - lastTime) / 1000
|
||||
lastTime = time
|
||||
|
||||
updateSquares(gridParams.value!.squares, deltaTime)
|
||||
drawGrid(
|
||||
context.value!,
|
||||
canvasRef.value!.width,
|
||||
canvasRef.value!.height,
|
||||
gridParams.value!.cols,
|
||||
gridParams.value!.rows,
|
||||
gridParams.value!.squares,
|
||||
gridParams.value!.dpr,
|
||||
)
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!canvasRef.value || !containerRef.value)
|
||||
return
|
||||
context.value = canvasRef.value.getContext('2d')!
|
||||
if (!context.value)
|
||||
return
|
||||
|
||||
updateCanvasSize()
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
updateCanvasSize()
|
||||
})
|
||||
intersectionObserver = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
isInView.value = entry.isIntersecting
|
||||
animationFrameId = requestAnimationFrame(animate)
|
||||
},
|
||||
{ threshold: 0 },
|
||||
)
|
||||
|
||||
resizeObserver.observe(containerRef.value)
|
||||
intersectionObserver.observe(canvasRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
}
|
||||
resizeObserver?.disconnect()
|
||||
intersectionObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:class="cn('w-full h-full', props.class)"
|
||||
>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="pointer-events-none"
|
||||
:width="canvasSize.width"
|
||||
:height="canvasSize.height"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
197
monisuo-admin/src/components/inspira-ui/glowing-effect.vue
Normal file
@@ -0,0 +1,197 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { animate } from 'motion-v'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
blur?: number
|
||||
inactiveZone?: number
|
||||
proximity?: number
|
||||
spread?: number
|
||||
variant?: 'default' | 'white'
|
||||
glow?: boolean
|
||||
class?: HTMLAttributes['class']
|
||||
disabled?: boolean
|
||||
movementDuration?: number
|
||||
borderWidth?: number
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
blur: 0,
|
||||
inactiveZone: 0.7,
|
||||
proximity: 0,
|
||||
spread: 20,
|
||||
variant: 'default',
|
||||
glow: false,
|
||||
movementDuration: 2,
|
||||
borderWidth: 1,
|
||||
disabled: true,
|
||||
})
|
||||
|
||||
const containerRef = useTemplateRef('containerRef')
|
||||
const lastPosition = ref({
|
||||
x: 0,
|
||||
y: 0,
|
||||
})
|
||||
const animationFrame = ref(0)
|
||||
|
||||
const containerStyles = computed(() => {
|
||||
return {
|
||||
'--blur': `${props.blur}px`,
|
||||
'--spread': props.spread,
|
||||
'--start': '0',
|
||||
'--active': '0',
|
||||
'--glowingeffect-border-width': `${props.borderWidth}px`,
|
||||
'--repeating-conic-gradient-times': '5',
|
||||
'--gradient':
|
||||
props.variant === 'white'
|
||||
? `repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
var(--black),
|
||||
var(--black) calc(25% / var(--repeating-conic-gradient-times))
|
||||
)`
|
||||
: `radial-gradient(circle, #dd7bbb 10%, #dd7bbb00 20%),
|
||||
radial-gradient(circle at 40% 40%, #d79f1e 5%, #d79f1e00 15%),
|
||||
radial-gradient(circle at 60% 60%, #5a922c 10%, #5a922c00 20%),
|
||||
radial-gradient(circle at 40% 60%, #4c7894 10%, #4c789400 20%),
|
||||
repeating-conic-gradient(
|
||||
from 236.84deg at 50% 50%,
|
||||
#dd7bbb 0%,
|
||||
#d79f1e calc(25% / var(--repeating-conic-gradient-times)),
|
||||
#5a922c calc(50% / var(--repeating-conic-gradient-times)),
|
||||
#4c7894 calc(75% / var(--repeating-conic-gradient-times)),
|
||||
#dd7bbb calc(100% / var(--repeating-conic-gradient-times))
|
||||
)`,
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.disabled)
|
||||
return
|
||||
|
||||
window.addEventListener('scroll', handleScroll, { passive: true })
|
||||
document.body.addEventListener('pointermove', handlePointerMove, {
|
||||
passive: true,
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationFrame.value) {
|
||||
cancelAnimationFrame(animationFrame.value)
|
||||
}
|
||||
|
||||
window.removeEventListener('scroll', handleScroll)
|
||||
document.body.removeEventListener('pointermove', handlePointerMove)
|
||||
})
|
||||
|
||||
function handlePointerMove(e: PointerEvent) {
|
||||
handleMove(e)
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
handleMove()
|
||||
}
|
||||
|
||||
function handleMove(e?: MouseEvent | PointerEvent | { x: number, y: number }) {
|
||||
if (!containerRef.value)
|
||||
return
|
||||
|
||||
if (animationFrame.value) {
|
||||
cancelAnimationFrame(animationFrame.value)
|
||||
}
|
||||
|
||||
animationFrame.value = requestAnimationFrame(() => {
|
||||
const element = containerRef.value
|
||||
|
||||
if (!element)
|
||||
return
|
||||
|
||||
const { left, top, width, height } = element.getBoundingClientRect()
|
||||
|
||||
const mouseX = e?.x ?? lastPosition.value.x
|
||||
const mouseY = e?.y ?? lastPosition.value.y
|
||||
|
||||
if (e) {
|
||||
lastPosition.value = { x: mouseX, y: mouseY }
|
||||
}
|
||||
|
||||
const center = [left + width * 0.5, top + height * 0.5]
|
||||
const distanceFromCenter = Math.hypot(mouseX - center[0], mouseY - center[1])
|
||||
const inactiveRadius = 0.5 * Math.min(width, height) * props.inactiveZone
|
||||
|
||||
if (distanceFromCenter < inactiveRadius) {
|
||||
element.style.setProperty('--active', '0')
|
||||
return
|
||||
}
|
||||
|
||||
const isActive
|
||||
= mouseX > left - props.proximity
|
||||
&& mouseX < left + width + props.proximity
|
||||
&& mouseY > top - props.proximity
|
||||
&& mouseY < top + height + props.proximity
|
||||
|
||||
element.style.setProperty('--active', isActive ? '1' : '0')
|
||||
|
||||
if (!isActive)
|
||||
return
|
||||
|
||||
const currentAngle = Number.parseFloat(element.style.getPropertyValue('--start')) || 0
|
||||
const targetAngle = (180 * Math.atan2(mouseY - center[1], mouseX - center[0])) / Math.PI + 90
|
||||
|
||||
const angleDiff = ((targetAngle - currentAngle + 180) % 360) - 180
|
||||
const newAngle = currentAngle + angleDiff
|
||||
|
||||
animate(currentAngle, newAngle, {
|
||||
duration: props.movementDuration,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
onUpdate: (value) => {
|
||||
element.style.setProperty('--start', String(value))
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-none absolute -inset-px hidden rounded-[inherit] border opacity-0 transition-opacity',
|
||||
glow && 'opacity-100',
|
||||
variant === 'white' && 'border-white',
|
||||
disabled && 'block!',
|
||||
)
|
||||
"
|
||||
/>
|
||||
<div
|
||||
ref="containerRef"
|
||||
:style="containerStyles"
|
||||
:class="
|
||||
cn(
|
||||
'pointer-events-none absolute inset-0 rounded-[inherit] opacity-100 transition-opacity',
|
||||
glow && 'opacity-100',
|
||||
blur > 0 && 'blur-(--blur)',
|
||||
props.class,
|
||||
disabled && 'hidden!',
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'glow',
|
||||
'rounded-[inherit]',
|
||||
`after:content-[''] after:rounded-[inherit] after:absolute after:inset-[calc(-1*var(--glowingeffect-border-width))]`,
|
||||
'after:[border:var(--glowingeffect-border-width)_solid_transparent]',
|
||||
'after:[background:var(--gradient)] after:bg-fixed',
|
||||
'after:opacity-(--active) after:transition-opacity after:duration-300',
|
||||
'after:[mask-clip:padding-box,border-box]',
|
||||
'after:mask-intersect',
|
||||
'after:mask-[linear-gradient(#0000,#0000),conic-gradient(from_calc((var(--start)-var(--spread))*1deg),#00000000_0deg,#fff,#00000000_calc(var(--spread)*2deg))]',
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
76
monisuo-admin/src/components/inspira-ui/marquee/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<script lang="ts" setup>
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
class?: string
|
||||
reverse?: boolean
|
||||
pauseOnHover?: boolean
|
||||
vertical?: boolean
|
||||
repeat?: number
|
||||
}>(),
|
||||
{
|
||||
pauseOnHover: false,
|
||||
vertical: false,
|
||||
repeat: 4,
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'group flex overflow-hidden p-2 [--duration:40s] [--gap:1rem] gap-(--gap)',
|
||||
vertical ? 'flex-col' : 'flex-row',
|
||||
$props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
v-for="index in repeat"
|
||||
:key="index"
|
||||
:class="
|
||||
cn(
|
||||
'flex shrink-0 justify-around gap-(--gap)',
|
||||
vertical ? 'animate-marquee-vertical flex-col' : 'animate-marquee flex-row',
|
||||
pauseOnHover ? 'group-hover:paused' : '',
|
||||
)
|
||||
"
|
||||
:style="{
|
||||
animationDirection: reverse ? 'reverse' : 'normal',
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-marquee {
|
||||
animation: marquee var(--duration) linear infinite;
|
||||
animation-direction: reverse;
|
||||
}
|
||||
|
||||
.animate-marquee-vertical {
|
||||
animation: marquee-vertical var(--duration) linear infinite;
|
||||
}
|
||||
|
||||
@keyframes marquee {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes marquee-vertical {
|
||||
from {
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
transform: translateY(calc(-100% - var(--gap)));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script lang="ts" setup>
|
||||
interface Props {
|
||||
img: string
|
||||
name: string
|
||||
username: string
|
||||
body: string
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<figure
|
||||
class="relative w-64 cursor-pointer overflow-hidden rounded-xl border border-gray-950/10 bg-gray-950/1 p-4 hover:bg-gray-950/5 dark:border-gray-50/10 dark:bg-gray-50/10 dark:hover:bg-gray-50/15"
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
<img :src="img" class="rounded-full" width="32" height="32" alt="">
|
||||
<div class="flex flex-col">
|
||||
<span class="text-sm font-medium dark:text-white">
|
||||
{{ name }}
|
||||
</span>
|
||||
<p class="text-xs font-medium dark:text-white/40">
|
||||
{{ username }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<blockquote class="mt-2 text-sm">
|
||||
{{ body }}
|
||||
</blockquote>
|
||||
</figure>
|
||||
</template>
|
||||
46
monisuo-admin/src/components/inspira-ui/ripple/circle.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
size?: number
|
||||
class?: string
|
||||
opacity?: number
|
||||
animationDelay?: number
|
||||
borderStyle?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 210,
|
||||
opacity: 0.24,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="cn('absolute shadow-xl', 'animate-ripple-circle', props.class)" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-ripple-circle {
|
||||
animation: ripple-effect var(--duration, 2s) ease-in-out calc(var(--i, 0) * 0.2s) infinite;
|
||||
border-width: 1px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: v-bind('`${props.size}px`');
|
||||
height: v-bind('`${props.size}px`');
|
||||
animation-delay: v-bind('`${props.animationDelay}ms`');
|
||||
opacity: v-bind('props.opacity');
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
border-style: v-bind('props.borderStyle');
|
||||
}
|
||||
|
||||
@keyframes ripple-effect {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(0.9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
monisuo-admin/src/components/inspira-ui/ripple/container.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import RippleCircle from './circle.vue'
|
||||
|
||||
interface Props {
|
||||
baseCircleSize?: number
|
||||
baseCircleOpacity?: number
|
||||
spaceBetweenCircle?: number
|
||||
circleOpacityDowngradeRatio?: number
|
||||
circleClass?: string
|
||||
waveSpeed?: number
|
||||
numberOfCircles?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
baseCircleSize: 210,
|
||||
baseCircleOpacity: 0.24,
|
||||
circleOpacityDowngradeRatio: 0.03,
|
||||
waveSpeed: 80,
|
||||
spaceBetweenCircle: 70,
|
||||
numberOfCircles: 7,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0">
|
||||
<RippleCircle
|
||||
v-for="index in numberOfCircles"
|
||||
:key="index"
|
||||
:opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
|
||||
:size="baseCircleSize + index * spaceBetweenCircle"
|
||||
:animation-delay="index * waveSpeed"
|
||||
:border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
|
||||
:class="circleClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
36
monisuo-admin/src/components/inspira-ui/ripple/index.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import RippleCircle from './circle.vue'
|
||||
|
||||
interface Props {
|
||||
baseCircleSize?: number
|
||||
baseCircleOpacity?: number
|
||||
spaceBetweenCircle?: number
|
||||
circleOpacityDowngradeRatio?: number
|
||||
circleClass?: string
|
||||
waveSpeed?: number
|
||||
numberOfCircles?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
baseCircleSize: 210,
|
||||
baseCircleOpacity: 0.24,
|
||||
circleOpacityDowngradeRatio: 0.03,
|
||||
waveSpeed: 80,
|
||||
spaceBetweenCircle: 70,
|
||||
numberOfCircles: 7,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute inset-0">
|
||||
<RippleCircle
|
||||
v-for="index in numberOfCircles"
|
||||
:key="index"
|
||||
:opacity="baseCircleOpacity - index * circleOpacityDowngradeRatio"
|
||||
:size="baseCircleSize + index * spaceBetweenCircle"
|
||||
:animation-delay="index * waveSpeed"
|
||||
:border-style="index === numberOfCircles - 1 ? 'dashed' : 'solid'"
|
||||
:class="circleClass"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
55
monisuo-admin/src/components/language-change.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup lang="ts">
|
||||
import type { AcceptableValue } from 'reka-ui'
|
||||
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import type { Language } from '@/plugins/i18n'
|
||||
|
||||
import { appLocale, DEFAULT_LOCALE, SUPPORTED_LOCALES } from '@/plugins/i18n'
|
||||
|
||||
const { locale } = useI18n()
|
||||
|
||||
function setDefaultLanguage() {
|
||||
locale.value = DEFAULT_LOCALE
|
||||
appLocale.value = DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
function handleLocaleChange(val: AcceptableValue) {
|
||||
if (typeof val !== 'string' || !SUPPORTED_LOCALES.has(val as Language)) {
|
||||
setDefaultLanguage()
|
||||
return
|
||||
}
|
||||
|
||||
locale.value = val as Language
|
||||
appLocale.value = val as Language
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UiDropdownMenu>
|
||||
<UiDropdownMenuTrigger as-child>
|
||||
<UiButton variant="outline">
|
||||
<Icon icon="mdi:translate" class="mr-2" />
|
||||
{{ $t('language') }}
|
||||
</UiButton>
|
||||
</UiDropdownMenuTrigger>
|
||||
<UiDropdownMenuContent>
|
||||
<UiDropdownMenuLabel>{{ $t('changeLanguage') }}</UiDropdownMenuLabel>
|
||||
<UiDropdownMenuSeparator />
|
||||
<UiDropdownMenuRadioGroup
|
||||
v-model="locale"
|
||||
@update:model-value="handleLocaleChange"
|
||||
>
|
||||
<UiDropdownMenuRadioItem value="en">
|
||||
<Icon icon="flag:us-4x3" />
|
||||
<span class="ml-2">English</span>
|
||||
</UiDropdownMenuRadioItem>
|
||||
<UiDropdownMenuRadioItem value="zh">
|
||||
<Icon icon="flag:cn-4x3" />
|
||||
<span class="ml-2">中文</span>
|
||||
</UiDropdownMenuRadioItem>
|
||||
</UiDropdownMenuRadioGroup>
|
||||
</UiDropdownMenuContent>
|
||||
</UiDropdownMenu>
|
||||
</template>
|
||||
3
monisuo-admin/src/components/loading.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<UiSpinner class="w-24 h-24 animate-spin" />
|
||||
</template>
|
||||
48
monisuo-admin/src/components/marketing-layout/the-footer.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
|
||||
const mode = useColorMode()
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: 'bluesky',
|
||||
icon: 'simple-icons:bluesky',
|
||||
url: 'https://bsky.app/profile/bitmc.bsky.social',
|
||||
},
|
||||
{
|
||||
name: 'github',
|
||||
icon: 'simple-icons:github',
|
||||
url: 'https://www.github.com/whbbit1999/shadcn-vue-admin',
|
||||
},
|
||||
{
|
||||
name: 'bilibili',
|
||||
icon: 'simple-icons:bilibili',
|
||||
url: 'https://space.bilibili.com/104376935',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="min-h-18 flex items-center justify-between">
|
||||
<UiAvatar>
|
||||
<UiAvatarImage :src="`${mode === 'dark' ? '/logo.svg' : '/logo-black.svg'}`" alt="Logo" />
|
||||
</UiAvatar>
|
||||
|
||||
<div>© 2025 Whbbit1999</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<UiButton
|
||||
v-for="link in links"
|
||||
:key="link.name"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
as="a"
|
||||
:href="link.url"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon :icon="link.icon" />
|
||||
</UiButton>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
40
monisuo-admin/src/components/marketing-layout/the-header.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<script lang="ts" setup>
|
||||
import { useColorMode } from '@vueuse/core'
|
||||
|
||||
import LanguageChange from '@/components/language-change.vue'
|
||||
import SignInButton from '@/components/sign-in-button.vue'
|
||||
import SignUpButton from '@/components/sign-up-button.vue'
|
||||
import ToggleTheme from '@/components/toggle-theme.vue'
|
||||
|
||||
const mode = useColorMode()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="h-14 flex items-center marketing-header sticky top-0 z-99">
|
||||
<router-link to="/" class="flex items-center gap-2">
|
||||
<UiAvatar>
|
||||
<UiAvatarImage :src="`${mode === 'dark' ? '/logo.svg' : '/logo-black.svg'}`" alt="Logo" />
|
||||
</UiAvatar>
|
||||
<span class="text-base font-bold">Shadcn Vue Admin</span>
|
||||
</router-link>
|
||||
|
||||
<div class="flex-1" />
|
||||
|
||||
<div class="mr-2 hidden lg:flex lg:gap-2">
|
||||
<SignInButton />
|
||||
<SignUpButton />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<LanguageChange />
|
||||
<ToggleTheme />
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.marketing-header {
|
||||
backdrop-filter: saturate(50%) blur(4px);
|
||||
background-size: 4px 4px;
|
||||
}
|
||||
</style>
|
||||
91
monisuo-admin/src/components/marketing/evaluation.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import Marquee from '@/components/inspira-ui/marquee/index.vue'
|
||||
import MarqueeReviewCard from '@/components/inspira-ui/marquee/review-card.vue'
|
||||
|
||||
const reviews = [
|
||||
{
|
||||
name: 'Jack',
|
||||
username: '@jack',
|
||||
body: 'I\'ve never seen anything like this before. It\'s amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/jack',
|
||||
},
|
||||
{
|
||||
name: 'Jill',
|
||||
username: '@jill',
|
||||
body: 'I don\'t know what to say. I\'m speechless. This is amazing.',
|
||||
img: 'https://avatar.vercel.sh/jill',
|
||||
},
|
||||
{
|
||||
name: 'John',
|
||||
username: '@john',
|
||||
body: 'I\'m at a loss for words. This is amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/john',
|
||||
},
|
||||
{
|
||||
name: 'Jane',
|
||||
username: '@jane',
|
||||
body: 'I\'m at a loss for words. This is amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/jane',
|
||||
},
|
||||
{
|
||||
name: 'Jenny',
|
||||
username: '@jenny',
|
||||
body: 'I\'m at a loss for words. This is amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/jenny',
|
||||
},
|
||||
{
|
||||
name: 'James',
|
||||
username: '@james',
|
||||
body: 'I\'m at a loss for words. This is amazing. I love it.',
|
||||
img: 'https://avatar.vercel.sh/james',
|
||||
},
|
||||
]
|
||||
|
||||
// Split reviews into two rows
|
||||
const firstRow = ref(reviews.slice(0, reviews.length / 2))
|
||||
const secondRow = ref(reviews.slice(reviews.length / 2))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h2 class="text-4xl font-black my-4 text-center">
|
||||
{{ $t('marketing.evaluation.title') }}
|
||||
</h2>
|
||||
<h4 class="text-center mb-4">
|
||||
{{ $t('marketing.evaluation.subtitle') }}
|
||||
</h4>
|
||||
<div
|
||||
class="relative flex w-full flex-col items-center justify-center overflow-hidden"
|
||||
>
|
||||
<Marquee pause-on-hover class="[--duration:50s]">
|
||||
<MarqueeReviewCard
|
||||
v-for="review in firstRow"
|
||||
:key="review.username"
|
||||
:img="review.img"
|
||||
:name="review.name"
|
||||
:username="review.username"
|
||||
:body="review.body"
|
||||
/>
|
||||
</Marquee>
|
||||
|
||||
<Marquee reverse pause-on-hover class="[--duration:50s]">
|
||||
<MarqueeReviewCard
|
||||
v-for="review in secondRow"
|
||||
:key="review.username"
|
||||
:img="review.img"
|
||||
:name="review.name"
|
||||
:username="review.username"
|
||||
:body="review.body"
|
||||
/>
|
||||
</Marquee>
|
||||
|
||||
<!-- Left Gradient -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-linear-to-r from-(--ui-bg) dark:from-(--ui-bg)"
|
||||
/>
|
||||
|
||||
<!-- Right Gradient -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-linear-to-l from-(--ui-bg) dark:from-(--ui-bg)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
84
monisuo-admin/src/components/marketing/features.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import GlowingEffect from '@/components/inspira-ui/glowing-effect.vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const gridItems = computed(() => [
|
||||
{
|
||||
icon: 'lucide:box',
|
||||
title: t('marketing.features.feature1.title'),
|
||||
description: t('marketing.features.feature1.description'),
|
||||
},
|
||||
{
|
||||
icon: 'lucide:settings',
|
||||
title: t('marketing.features.feature2.title'),
|
||||
description: t('marketing.features.feature2.description'),
|
||||
},
|
||||
{
|
||||
icon: 'lucide:sparkles',
|
||||
title: t('marketing.features.feature3.title'),
|
||||
description: t('marketing.features.feature3.description'),
|
||||
},
|
||||
{
|
||||
icon: 'lucide:search',
|
||||
title: t('marketing.features.feature4.title'),
|
||||
description: t('marketing.features.feature4.description'),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="text-4xl font-bold text-center mb-8">
|
||||
{{ $t('marketing.features.title') }}
|
||||
</h2>
|
||||
|
||||
<ul
|
||||
class="grid grid-cols-1 grid-rows-none gap-4 overflow-auto xl:max-h-[56rem] xl:grid-rows-2 lg:gap-4 md:grid-cols-2 md:grid-rows-3"
|
||||
>
|
||||
<li
|
||||
v-for="item in gridItems"
|
||||
:key="item.title"
|
||||
:class="cn('min-h-[14rem] list-none')"
|
||||
>
|
||||
<div class="rounded-2.5xl relative h-full border p-2 md:rounded-3xl md:p-3">
|
||||
<GlowingEffect
|
||||
:spread="40"
|
||||
:glow="true"
|
||||
:disabled="false"
|
||||
:proximity="64"
|
||||
:inactive-zone="0.01"
|
||||
/>
|
||||
<div
|
||||
class="border-0.75 relative flex h-full flex-col justify-between gap-6 overflow-hidden rounded-xl p-6 md:p-6 dark:shadow-[0px_0px_27px_0px_#2D2D2D]"
|
||||
>
|
||||
<div class="relative flex flex-1 flex-col justify-between gap-3">
|
||||
<div class="w-fit rounded-lg border border-gray-600 p-2">
|
||||
<Icon
|
||||
class="size-4 text-black dark:text-neutral-500"
|
||||
:icon="item.icon"
|
||||
/>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<h3
|
||||
class="-tracking-4 text-balance pt-0.5 font-sans text-xl/[1.375rem] font-semibold text-black md:text-2xl/[1.875rem] dark:text-white"
|
||||
>
|
||||
{{ item.title }}
|
||||
</h3>
|
||||
<h2
|
||||
class="font-sans text-sm/[1.125rem] text-black md:text-base/[1.375rem] dark:text-neutral-400 [&_b]:md:font-semibold [&_strong]:md:font-semibold"
|
||||
>
|
||||
{{ item.description }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
81
monisuo-admin/src/components/marketing/hero.vue
Normal file
@@ -0,0 +1,81 @@
|
||||
<script lang="ts" setup>
|
||||
import Autoplay from 'embla-carousel-autoplay'
|
||||
|
||||
const images = [
|
||||
'https://picsum.photos/640/640?random=1',
|
||||
'https://picsum.photos/640/640?random=2',
|
||||
'https://picsum.photos/640/640?random=3',
|
||||
'https://picsum.photos/640/640?random=4',
|
||||
'https://picsum.photos/640/640?random=5',
|
||||
'https://picsum.photos/640/640?random=6',
|
||||
]
|
||||
|
||||
const users: { avatar: string, name: string, id: number }[] = [
|
||||
{ avatar: 'https://github.com/benjamincanac.png', name: 'Benjamin Canac', id: 1 },
|
||||
{ avatar: 'https://github.com/romhml.png', name: 'Benjamin Canac', id: 2 },
|
||||
{ avatar: 'https://github.com/noook.png', name: 'Benjamin Canac', id: 3 },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main class="flex gap-8 justify-between flex-col lg:flex-row">
|
||||
<aside class="w-full lg:w-1/3">
|
||||
<p class="text-4xl font-black relative">
|
||||
{{ $t('marketing.hero.title') }}
|
||||
</p>
|
||||
|
||||
<div class="font-bold mt-2 relative">
|
||||
{{ $t('marketing.hero.subtitle') }}
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 my-12 relative">
|
||||
<UiButton>
|
||||
{{ $t('marketing.hero.getMore') }}
|
||||
</UiButton>
|
||||
|
||||
<img
|
||||
src="@/assets/icons/arrow-dark.svg"
|
||||
alt=""
|
||||
class="dark:hidden block w-12 h-12 absolute top-[110%] left-8 -rotate-90"
|
||||
>
|
||||
<img
|
||||
src="@/assets/icons/arrow-light.svg"
|
||||
alt=""
|
||||
class="dark:block hidden w-12 h-12 absolute top-[110%] left-8 -rotate-90"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex gap-2">
|
||||
<UiAvatar v-for="user in users" :key="user.id">
|
||||
<UiAvatarImage :src="user.avatar" />
|
||||
</UiAvatar>
|
||||
</div>
|
||||
|
||||
<span class="font-black">
|
||||
{{ $t('marketing.hero.learnPeople') }}
|
||||
</span>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside class="w-full lg:w-2/3 lg:px-2">
|
||||
<UiCarousel
|
||||
:opts="{
|
||||
align: 'start',
|
||||
loop: true,
|
||||
}"
|
||||
:plugins="[Autoplay({
|
||||
delay: 2000,
|
||||
})]"
|
||||
>
|
||||
<UiCarouselContent>
|
||||
<UiCarouselItem v-for="image in images" :key="image" class="basis-1/3">
|
||||
<img :src="image" width="320" height="320" class="rounded-lg">
|
||||
</UiCarouselItem>
|
||||
</UiCarouselContent>
|
||||
<UiCarouselPrevious class="hidden lg:flex" />
|
||||
<UiCarouselNext class="hidden lg:flex" />
|
||||
</UiCarousel>
|
||||
</aside>
|
||||
</main>
|
||||
</template>
|
||||
42
monisuo-admin/src/components/marketing/logos.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
import Marquee from '@/components/inspira-ui/marquee/index.vue'
|
||||
|
||||
const types = [
|
||||
{ name: 'Nuxt', icon: 'simple-icons:nuxt' },
|
||||
{ name: 'Vue', icon: 'simple-icons:vitess' },
|
||||
{ name: 'Vite', icon: 'simple-icons:vite' },
|
||||
{ name: 'vitest', icon: 'simple-icons:vitest' },
|
||||
{ name: 'vscode', icon: 'simple-icons:visualstudiocode' },
|
||||
{ name: 'mysql', icon: 'simple-icons:mysql' },
|
||||
{ name: 'prisma', icon: 'simple-icons:prisma' },
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex w-full flex-col items-center justify-center overflow-hidden -rotate-3"
|
||||
>
|
||||
<Marquee pause-on-hover reverse class="[--duration:50s]">
|
||||
<div
|
||||
v-for="type in types"
|
||||
:key="type.name"
|
||||
class="flex items-center gap-2 mx-4"
|
||||
>
|
||||
<Icon :icon="type.icon" class="w-12 h-12" />
|
||||
<span class="font-black text-4xl">{{ type.name }}</span>
|
||||
</div>
|
||||
</Marquee>
|
||||
|
||||
<!-- Left Gradient -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 left-0 w-1/3 bg-linear-to-r from-(--ui-bg) dark:from-(--ui-bg)"
|
||||
/>
|
||||
|
||||
<!-- Right Gradient -->
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 w-1/3 bg-linear-to-l from-(--ui-bg) dark:from-(--ui-bg)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
141
monisuo-admin/src/components/marketing/pricing-plans/index.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<script lang="ts" setup>
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
interface Plan {
|
||||
id: string | number
|
||||
title: string
|
||||
description: string
|
||||
badge?: string
|
||||
price: string
|
||||
unit: string
|
||||
discount: string
|
||||
recommendation?: boolean
|
||||
billing?: {
|
||||
cycle: string
|
||||
period: string
|
||||
}
|
||||
features: string[]
|
||||
}
|
||||
|
||||
const plans = computed<Plan[]>(() => [
|
||||
{
|
||||
id: 1,
|
||||
title: t('marketing.pricingPlans.hobby.title'),
|
||||
description: t('marketing.pricingPlans.hobby.description'),
|
||||
price: t('marketing.pricingPlans.hobby.price'),
|
||||
discount: t('marketing.pricingPlans.hobby.discount'),
|
||||
unit: t('marketing.pricingPlans.hobby.unit'),
|
||||
billing: {
|
||||
cycle: t('marketing.pricingPlans.hobby.billing.cycle'),
|
||||
period: t('marketing.pricingPlans.hobby.billing.period'),
|
||||
},
|
||||
features: [
|
||||
t('marketing.pricingPlans.hobby.features.feature1'),
|
||||
t('marketing.pricingPlans.hobby.features.feature2'),
|
||||
t('marketing.pricingPlans.hobby.features.feature3'),
|
||||
t('marketing.pricingPlans.hobby.features.feature4'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
recommendation: true,
|
||||
title: t('marketing.pricingPlans.starter.title'),
|
||||
description: t('marketing.pricingPlans.starter.description'),
|
||||
price: t('marketing.pricingPlans.starter.price'),
|
||||
discount: t('marketing.pricingPlans.starter.discount'),
|
||||
unit: t('marketing.pricingPlans.starter.unit'),
|
||||
billing: {
|
||||
cycle: t('marketing.pricingPlans.starter.billing.cycle'),
|
||||
period: t('marketing.pricingPlans.starter.billing.period'),
|
||||
},
|
||||
features: [
|
||||
t('marketing.pricingPlans.starter.features.feature1'),
|
||||
t('marketing.pricingPlans.starter.features.feature2'),
|
||||
t('marketing.pricingPlans.starter.features.feature3'),
|
||||
t('marketing.pricingPlans.starter.features.feature4'),
|
||||
t('marketing.pricingPlans.starter.features.feature5'),
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: t('marketing.pricingPlans.business.title'),
|
||||
description: t('marketing.pricingPlans.business.description'),
|
||||
price: t('marketing.pricingPlans.business.price'),
|
||||
discount: t('marketing.pricingPlans.business.discount'),
|
||||
unit: t('marketing.pricingPlans.business.unit'),
|
||||
billing: {
|
||||
cycle: t('marketing.pricingPlans.business.billing.cycle'),
|
||||
period: t('marketing.pricingPlans.business.billing.period'),
|
||||
},
|
||||
features: [
|
||||
t('marketing.pricingPlans.business.features.feature1'),
|
||||
t('marketing.pricingPlans.business.features.feature2'),
|
||||
t('marketing.pricingPlans.business.features.feature3'),
|
||||
t('marketing.pricingPlans.business.features.feature4'),
|
||||
t('marketing.pricingPlans.business.features.feature5'),
|
||||
t('marketing.pricingPlans.business.features.feature6'),
|
||||
],
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="pricing-plans">
|
||||
<h2 class="text-center font-black my-4 text-4xl">
|
||||
{{ $t('marketing.pricingPlans.title') }}
|
||||
</h2>
|
||||
<h4 class="text-center text-xl">
|
||||
{{ $t('marketing.pricingPlans.subtitle') }}
|
||||
</h4>
|
||||
<div
|
||||
class="flex flex-col lg:flex-row lg:items-start items-center justify-center gap-4 mt-8"
|
||||
>
|
||||
<UiCard
|
||||
v-for="plan in plans"
|
||||
:key="plan.id"
|
||||
class="w-full lg:w-1/5"
|
||||
:class="{
|
||||
'border-2 border-primary bg-primary/10':
|
||||
plan.recommendation,
|
||||
}"
|
||||
>
|
||||
<h3 class="text-xl font-black text-center">
|
||||
{{ plan.title }}
|
||||
</h3>
|
||||
<div class="text-sm text-center text-neutral-400">
|
||||
{{ plan.description }}
|
||||
</div>
|
||||
|
||||
<div class="flex items-top my-2 justify-center">
|
||||
<div class="text-2xl font-black">
|
||||
{{ plan.unit }}
|
||||
<span class="text-4xl">{{ plan.price }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="plan.discount"
|
||||
class="text-sm font-bold line-through text-neutral-400"
|
||||
>
|
||||
{{ plan.unit }}{{ plan.discount }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-sm mb-4 text-center">
|
||||
<ul>
|
||||
<li v-for="feature in plan.features" :key="feature" class="mb-1">
|
||||
<Icon icon="carbon:checkmark" class="inline-block" />
|
||||
{{ feature }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex justify-center mx-8">
|
||||
<UiButton block>
|
||||
{{ $t('marketing.pricingPlans.buy') }}
|
||||
</UiButton>
|
||||
</div>
|
||||
</UiCard>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
34
monisuo-admin/src/components/marketing/setup.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import Ripple from '@/components/inspira-ui/ripple/index.vue'
|
||||
import SignInButton from '@/components/sign-in-button.vue'
|
||||
import SignUpButton from '@/components/sign-up-button.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex h-[450px] w-full flex-col items-center justify-center overflow-hidden rounded-lg lg:w-full md:w-full"
|
||||
>
|
||||
<p class="z-10 whitespace-pre-wrap text-center text-5xl font-medium tracking-tighter text-black dark:text-white">
|
||||
{{ $t('marketing.setup.title') }}
|
||||
</p>
|
||||
<small class="mt-2">
|
||||
{{ $t('marketing.setup.subtitle') }}
|
||||
</small>
|
||||
|
||||
<div class="flex items-center gap-3 my-2 z-100">
|
||||
<SignInButton />
|
||||
<SignUpButton />
|
||||
</div>
|
||||
|
||||
<Ripple
|
||||
class="bg-white/5 mask-[linear-gradient(to_bottom,white,transparent)]"
|
||||
circle-class="border-[hsl(var(--primary))] bg-primary/25 blobed"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.blobed) {
|
||||
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
|
||||
}
|
||||
</style>
|
||||
19
monisuo-admin/src/components/no-result-found.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang='ts' setup>
|
||||
import { FolderOpenIcon } from 'lucide-vue-next'
|
||||
|
||||
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Empty>
|
||||
<EmptyHeader>
|
||||
<EmptyMedia variant="icon">
|
||||
<FolderOpenIcon />
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No result found.</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
Please try a different search term or check the spelling.
|
||||
</EmptyDescription>
|
||||
</EmptyHeader>
|
||||
</Empty>
|
||||
</template>
|
||||
72
monisuo-admin/src/components/prop-ui/copy/Copy.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { Copy, CopyCheck } from 'lucide-vue-next'
|
||||
|
||||
import type { ButtonVariants } from '@/components/ui/button'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from '@/components/ui/tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { copyVariants } from '.'
|
||||
|
||||
interface Props {
|
||||
content: string
|
||||
size?: 'sm' | 'default'
|
||||
variant?: ButtonVariants['variant']
|
||||
class?: HTMLAttributes['class']
|
||||
copyTooltipText?: string
|
||||
copiedTooltipText?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'default',
|
||||
variant: 'outline',
|
||||
copyTooltipText: 'Copy',
|
||||
copiedTooltipText: 'Copied',
|
||||
})
|
||||
|
||||
const iconSize = computed(() => {
|
||||
return props.size === 'sm' ? 'sm' : 'default'
|
||||
})
|
||||
|
||||
const size = computed(() => {
|
||||
return props.size === 'sm' ? 'sm' : 'icon'
|
||||
})
|
||||
|
||||
const source = computed(() => props.content)
|
||||
|
||||
const { copy, copied, isSupported } = useClipboard({ source })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span v-if="isSupported">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
:variant="props.variant"
|
||||
:size="size"
|
||||
:class="cn(props.class)"
|
||||
@click="copy(source)"
|
||||
>
|
||||
<Copy v-if="!copied" :class="cn(copyVariants({ iconSize }))" />
|
||||
<CopyCheck v-else :class="cn(copyVariants({ iconSize }))" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p v-if="!copied">{{ props.copyTooltipText }}: {{ props.content }}</p>
|
||||
<p v-else>{{ props.copiedTooltipText }}: {{ props.content }}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</span>
|
||||
<span v-else>Your browser does not support Clipboard API</span>
|
||||
</template>
|
||||
22
monisuo-admin/src/components/prop-ui/copy/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as Copy } from './Copy.vue'
|
||||
|
||||
export const copyVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
iconSize: {
|
||||
default: 'size-4',
|
||||
sm: 'size-3',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
iconSize: 'default',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type CopyVariants = VariantProps<typeof copyVariants>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import type { InlineTipVariants } from '.'
|
||||
|
||||
import { inlineTipVariants } from '.'
|
||||
|
||||
interface Props {
|
||||
label: string
|
||||
variant?: InlineTipVariants['variant']
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'info',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn(
|
||||
'bg-secondary text-secondary-foreground text-sm inline-grid grid-cols-[4px_1fr] items-start gap-3 rounded-md border p-3',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<div
|
||||
:class="cn(
|
||||
'h-full w-1 rounded-full',
|
||||
inlineTipVariants({ variant: props.variant }))"
|
||||
/>
|
||||
|
||||
<div class="text-muted-foreground">
|
||||
<strong class="text-sm font-semibold text-foreground mr-2">{{ props.label }}:</strong>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
24
monisuo-admin/src/components/prop-ui/inline-tip/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cva } from 'class-variance-authority'
|
||||
|
||||
export { default as InlineTip } from './InlineTip.vue'
|
||||
|
||||
export const inlineTipVariants = cva(
|
||||
'',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
info: 'bg-stone-400 dark:bg-stone-600',
|
||||
warning: 'bg-yellow-400 dark:bg-yellow-600',
|
||||
success: 'bg-green-400 dark:bg-green-600',
|
||||
error: 'bg-rose-400 dark:bg-rose-600',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'info',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type InlineTipVariants = VariantProps<typeof inlineTipVariants>
|
||||
28
monisuo-admin/src/components/prop-ui/modal/Modal.vue
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogRootProps } from 'reka-ui'
|
||||
import type { DrawerRootProps } from 'vaul-vue'
|
||||
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
type Props = DrawerRootProps | DialogRootProps
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emits = defineEmits<{
|
||||
'update:open': [value: boolean]
|
||||
}>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Root"
|
||||
v-bind="forwarded"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
21
monisuo-admin/src/components/prop-ui/modal/ModalClose.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogCloseProps } from 'reka-ui'
|
||||
import type { DrawerCloseProps } from 'vaul-vue'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
type Props = DrawerCloseProps | DialogCloseProps
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Close"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
29
monisuo-admin/src/components/prop-ui/modal/ModalContent.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useForwardPropsEmits } from 'reka-ui'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes['class'] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const { Modal, contentClass } = useModal()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
|
||||
const mergedClass = computed(() => cn(contentClass.value, props.class))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Content"
|
||||
v-bind="forwarded"
|
||||
:class="mergedClass"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
import type { DialogDescriptionProps } from 'reka-ui'
|
||||
import type { DrawerDescriptionProps } from 'vaul-vue'
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { useForwardProps } from 'reka-ui'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
type Props = (DrawerDescriptionProps | DialogDescriptionProps) & { class?: HTMLAttributes['class'] }
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const delegatedProps = reactiveOmit(props, 'class')
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Description"
|
||||
v-bind="forwardedProps"
|
||||
:class="props.class"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
22
monisuo-admin/src/components/prop-ui/modal/ModalFooter.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
interface Props {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Footer"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
22
monisuo-admin/src/components/prop-ui/modal/ModalHeader.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script lang="ts" setup>
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
|
||||
import { useModal } from './use-modal'
|
||||
|
||||
interface Props {
|
||||
class?: HTMLAttributes['class']
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const { Modal } = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="Modal.Header"
|
||||
v-bind="props"
|
||||
>
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||