├── .env
├── .env.development
├── .env.production
├── .env.test
├── .eslintrc.cjs
├── .gitignore
├── LICENSE
├── README.md
├── components.json
├── index.html
├── package.json
├── plate-components.json
├── pnpm-lock.yaml
├── postcss.config.js
├── public
├── _redirects
└── vite.svg
├── screenshot
├── dashboard.png
├── list.png
├── list1.png
├── list2.png
├── login.png
├── product-01.png
└── sider.png
├── src
├── apis
│ ├── auth.ts
│ ├── common.ts
│ ├── models
│ │ ├── base-model.ts
│ │ ├── permission-model.ts
│ │ ├── product-model.ts
│ │ └── user-model.ts
│ ├── permission.ts
│ └── product.ts
├── assets
│ └── react.svg
├── components
│ ├── custom
│ │ ├── auto-form
│ │ │ ├── common
│ │ │ │ ├── label.tsx
│ │ │ │ └── tooltip.tsx
│ │ │ ├── config.ts
│ │ │ ├── dependencies.ts
│ │ │ ├── fields
│ │ │ │ ├── array.tsx
│ │ │ │ ├── checkbox.tsx
│ │ │ │ ├── date.tsx
│ │ │ │ ├── enum.tsx
│ │ │ │ ├── file.tsx
│ │ │ │ ├── input.tsx
│ │ │ │ ├── number.tsx
│ │ │ │ ├── object.tsx
│ │ │ │ ├── radio-group.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ ├── switch.tsx
│ │ │ │ └── textarea.tsx
│ │ │ ├── index.tsx
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── avatar-uploader.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── confirm-dialog.tsx
│ │ ├── data-table
│ │ │ ├── data-simple-table.tsx
│ │ │ ├── data-table-bak.tsx
│ │ │ ├── data-table-column-header.tsx
│ │ │ ├── data-table-faceted-filter.tsx
│ │ │ ├── data-table-pagination-page.tsx
│ │ │ ├── data-table-pagination-single-page.tsx
│ │ │ ├── data-table-pagination.tsx
│ │ │ ├── data-table-row-actions.tsx
│ │ │ ├── data-table-searchbar.tsx
│ │ │ ├── data-table-toolbar.tsx
│ │ │ ├── data-table-view-options.tsx
│ │ │ ├── data-table.tsx
│ │ │ └── makeData.ts
│ │ ├── date-picker.tsx
│ │ ├── drawer-form.tsx
│ │ ├── fancy-multi-select.tsx
│ │ ├── file-uploader.tsx
│ │ ├── form-dialog.tsx
│ │ ├── icon-list.tsx
│ │ ├── icon.tsx
│ │ ├── icons.tsx
│ │ ├── image-uploader.tsx
│ │ ├── multi-select.tsx
│ │ ├── novel-editor
│ │ │ ├── extensions.ts
│ │ │ ├── generative
│ │ │ │ ├── ai-completion-command.tsx
│ │ │ │ ├── ai-selector-commands.tsx
│ │ │ │ ├── ai-selector.tsx
│ │ │ │ └── generative-menu-switch.tsx
│ │ │ ├── icons
│ │ │ │ ├── crazy-spinner.tsx
│ │ │ │ ├── font-default.tsx
│ │ │ │ ├── font-mono.tsx
│ │ │ │ ├── font-serif.tsx
│ │ │ │ ├── index.tsx
│ │ │ │ ├── loading-circle.tsx
│ │ │ │ └── magic.tsx
│ │ │ ├── image-upload.ts
│ │ │ ├── novel-editor.tsx
│ │ │ ├── selectors
│ │ │ │ ├── color-selector.tsx
│ │ │ │ ├── link-selector.tsx
│ │ │ │ ├── node-selector.tsx
│ │ │ │ └── text-buttons.tsx
│ │ │ └── slash-command.tsx
│ │ ├── password-input.tsx
│ │ ├── resource-upload.tsx
│ │ ├── search-input.tsx
│ │ ├── single-breadcrumb.tsx
│ │ ├── skeleton-list.tsx
│ │ ├── theme-provider.tsx
│ │ ├── theme-switch.tsx
│ │ ├── tiptap-editor
│ │ │ ├── components
│ │ │ │ ├── menu-bar.tsx
│ │ │ │ ├── menu-bubble.tsx
│ │ │ │ ├── menu-floating.tsx
│ │ │ │ └── menu-item.tsx
│ │ │ ├── index.scss
│ │ │ └── tiptap.tsx
│ │ └── top-nav.tsx
│ ├── layout
│ │ ├── index.tsx
│ │ ├── language.tsx
│ │ ├── notice.tsx
│ │ ├── sidebar.tsx
│ │ └── user-nav.tsx
│ ├── menu
│ │ └── nav.tsx
│ └── ui
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── breadcrumb.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── collapsible.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── pagination.tsx
│ │ ├── popover.tsx
│ │ ├── progress.tsx
│ │ ├── radio-group.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── skeleton.tsx
│ │ ├── sonner.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── tabs.tsx
│ │ ├── textarea.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ ├── toggle-group.tsx
│ │ ├── toggle.tsx
│ │ ├── tooltip.tsx
│ │ └── use-toast.ts
├── context.tsx
├── hooks
│ ├── use-check-active-nav.tsx
│ ├── use-data-table.tsx
│ ├── use-is-collapsed.tsx
│ ├── use-local-storage.tsx
│ └── use-pagination.tsx
├── layout.tsx
├── lib
│ ├── request.ts
│ └── utils.ts
├── locale
│ ├── en-US.ts
│ ├── en-US
│ │ ├── menu.ts
│ │ ├── permission.ts
│ │ ├── product.ts
│ │ └── settings.ts
│ ├── index.ts
│ ├── zh-CN.ts
│ └── zh-CN
│ │ ├── menu.ts
│ │ ├── permission.ts
│ │ ├── product.ts
│ │ └── settings.ts
├── main.tsx
├── pages
│ ├── auth
│ │ ├── components
│ │ │ ├── forgot-form.tsx
│ │ │ ├── sign-up-form.tsx
│ │ │ └── user-auth-form.tsx
│ │ └── login
│ │ │ └── index.tsx
│ ├── dashboard
│ │ ├── components
│ │ │ ├── overview.tsx
│ │ │ └── recent-sales.tsx
│ │ └── index.tsx
│ ├── exception
│ │ ├── 401
│ │ │ └── index.tsx
│ │ ├── 404
│ │ │ └── index.tsx
│ │ ├── 500
│ │ │ └── index.tsx
│ │ └── 503
│ │ │ └── index.tsx
│ ├── permission
│ │ ├── member
│ │ │ ├── assign-role.tsx
│ │ │ ├── ban-confirm.tsx
│ │ │ ├── columns
│ │ │ │ └── index.tsx
│ │ │ ├── components
│ │ │ │ ├── data-table-column-header.tsx
│ │ │ │ ├── data-table-row-actions.tsx
│ │ │ │ └── data-table-searchbar.tsx
│ │ │ ├── data-form.tsx
│ │ │ ├── data
│ │ │ │ ├── data.tsx
│ │ │ │ └── schema.ts
│ │ │ ├── delete-confirm.tsx
│ │ │ ├── index.tsx
│ │ │ └── reset-pass.tsx
│ │ ├── resource
│ │ │ ├── ban-confirm.tsx
│ │ │ ├── columns
│ │ │ │ ├── data-form-fields.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── components
│ │ │ │ ├── data-table-column-header.tsx
│ │ │ │ ├── data-table-row-actions.tsx
│ │ │ │ └── data-table-searchbar.tsx
│ │ │ ├── data-form.tsx
│ │ │ ├── data
│ │ │ │ ├── data.tsx
│ │ │ │ └── schema.ts
│ │ │ ├── delete-confirm.tsx
│ │ │ └── index.tsx
│ │ └── role
│ │ │ ├── ban-confirm.tsx
│ │ │ ├── columns
│ │ │ └── index.tsx
│ │ │ ├── components
│ │ │ ├── data-table-column-header.tsx
│ │ │ ├── data-table-row-actions.tsx
│ │ │ └── data-table-searchbar.tsx
│ │ │ ├── data-form.tsx
│ │ │ ├── data
│ │ │ ├── data.tsx
│ │ │ └── schema.ts
│ │ │ ├── delete-confirm.tsx
│ │ │ └── index.tsx
│ ├── products
│ │ ├── attrs
│ │ │ ├── attr-data-form.tsx
│ │ │ ├── attr-delete-confirm.tsx
│ │ │ ├── attr-list.tsx
│ │ │ ├── ban-confirm.tsx
│ │ │ ├── columns
│ │ │ │ ├── attr-data-form-fields.tsx
│ │ │ │ ├── attr.tsx
│ │ │ │ ├── data-form-fields.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── components
│ │ │ │ ├── data-table-attr-row-actions.tsx
│ │ │ │ ├── data-table-column-header.tsx
│ │ │ │ ├── data-table-row-actions.tsx
│ │ │ │ └── data-table-searchbar.tsx
│ │ │ ├── data-form.tsx
│ │ │ ├── data
│ │ │ │ ├── data.tsx
│ │ │ │ └── schema.ts
│ │ │ ├── delete-confirm.tsx
│ │ │ └── index.tsx
│ │ ├── brand
│ │ │ ├── ban-confirm.tsx
│ │ │ ├── columns
│ │ │ │ ├── data-form-fields.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── components
│ │ │ │ ├── data-table-column-header.tsx
│ │ │ │ ├── data-table-row-actions.tsx
│ │ │ │ └── data-table-searchbar.tsx
│ │ │ ├── data-form.tsx
│ │ │ ├── data
│ │ │ │ ├── data.tsx
│ │ │ │ └── schema.ts
│ │ │ ├── delete-confirm.tsx
│ │ │ └── index.tsx
│ │ ├── cate
│ │ │ ├── ban-confirm.tsx
│ │ │ ├── columns
│ │ │ │ ├── data-form-fields.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── components
│ │ │ │ ├── data-table-column-header.tsx
│ │ │ │ ├── data-table-row-actions.tsx
│ │ │ │ └── data-table-searchbar.tsx
│ │ │ ├── data-form.tsx
│ │ │ ├── data
│ │ │ │ ├── data.tsx
│ │ │ │ └── schema.ts
│ │ │ ├── delete-confirm.tsx
│ │ │ └── index.tsx
│ │ └── list
│ │ │ ├── ban-confirm.tsx
│ │ │ ├── columns
│ │ │ ├── data-form-fields.tsx
│ │ │ └── index.tsx
│ │ │ ├── components
│ │ │ ├── data-table-column-header.tsx
│ │ │ ├── data-table-row-actions.tsx
│ │ │ └── data-table-searchbar.tsx
│ │ │ ├── data
│ │ │ ├── data.tsx
│ │ │ └── schema.ts
│ │ │ ├── delete-confirm.tsx
│ │ │ ├── detail.tsx
│ │ │ ├── index.tsx
│ │ │ └── sku-form.tsx
│ └── users
│ │ └── list
│ │ ├── components
│ │ ├── columns.tsx
│ │ ├── data-table-column-header.tsx
│ │ ├── data-table-faceted-filter.tsx
│ │ ├── data-table-pagination.tsx
│ │ ├── data-table-row-actions.tsx
│ │ ├── data-table-toolbar.tsx
│ │ ├── data-table-view-options.tsx
│ │ └── data-table.tsx
│ │ ├── data
│ │ ├── data.tsx
│ │ ├── schema.ts
│ │ └── tasks.ts
│ │ └── index.tsx
├── plugin.tsx
├── plugins
│ ├── demo
│ │ ├── index.tsx
│ │ ├── list
│ │ │ ├── components
│ │ │ │ ├── data-table-pagination.tsx
│ │ │ │ ├── data-table.tsx
│ │ │ │ ├── search-bar.tsx
│ │ │ │ └── tool-bar.tsx
│ │ │ └── index.tsx
│ │ └── router.tsx
│ ├── demo1
│ │ ├── index.tsx
│ │ └── router.tsx
│ ├── demo3
│ │ ├── index.tsx
│ │ └── router.tsx
│ └── manifest.json
├── routes.tsx
├── routes
│ ├── exception.tsx
│ ├── order.tsx
│ ├── permission.tsx
│ ├── product.tsx
│ └── user.tsx
├── sidelinks.tsx
├── stores
│ └── user-info.ts
├── style
│ ├── globals.css
│ ├── index.css
│ └── prosemirror.css
└── vite-env.d.ts
├── tailwind.config.cjs
├── tsconfig.json
├── tsconfig.node.json
├── vercel.json
└── vite.config.ts
/.env:
--------------------------------------------------------------------------------
1 | VITE_BASE_URL = 'http://127.0.0.1:9526/api'
2 | VITE_RESOURCE_URL = 'http://127.0.0.1:9527/'
3 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | VITE_BASE_URL = 'http://127.0.0.1:9526/api'
2 | VITE_RESOURCE_URL = 'http://127.0.0.1:9527/'
3 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | VITE_BASE_URL = 'https://mqshop-backend.yippai.com'
2 | VITE_RESOURCE_URL = '//127.0.0.1:9527/'
3 |
--------------------------------------------------------------------------------
/.env.test:
--------------------------------------------------------------------------------
1 | VITE_BASE_URL = '//127.0.0.1:9526/api'
2 | VITE_RESOURCE_URL = '//127.0.0.1:9527/'
3 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:@typescript-eslint/recommended',
7 | 'plugin:react-hooks/recommended',
8 | ],
9 | ignorePatterns: ['dist', '.eslintrc.cjs'],
10 | parser: '@typescript-eslint/parser',
11 | plugins: ['react-refresh'],
12 | rules: {
13 | 'react-refresh/only-export-components': [
14 | 'warn',
15 | { allowConstantExport: true },
16 | ],
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 MQEnergy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 麻雀后台管理系统
2 | 基于React18 + Zustand + Vite + TS + RouterV6 + shadcn-UI的mqshop电商后台管理系统
3 |
4 | # 努力开发中,敬请期待...
5 | https://mqshop-admin.netlify.app/
6 |
7 | admin / admin888
8 |
9 | # 后端项目
10 | [https://github.com/MQEnergy/mqshop](https://github.com/MQEnergy/mqshop)
11 |
12 | # demo地址(准备中...)
13 |
14 | # 部分截图
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | # 运行
27 | ```shell
28 | pnpm install
29 | pnpm run dev
30 | ```
31 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.cjs",
8 | "css": "src/style/globals.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Vite + React + TS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/plate-components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "aliases": {
4 | "components": "@/components"
5 | },
6 | "rsc": false,
7 | "style": "default",
8 | "tailwind": {
9 | "baseColor": "slate",
10 | "config": "tailwind.config.cjs",
11 | "css": "src/style/globals.css",
12 | "cssVariables": true,
13 | "prefix": ""
14 | }
15 | }
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/_redirects:
--------------------------------------------------------------------------------
1 | /* /index.html 200
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/screenshot/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MQEnergy/mqshop-admin/b814ece660c9eb6cf2156e58aa6de55de9134a8b/screenshot/dashboard.png
--------------------------------------------------------------------------------
/screenshot/list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MQEnergy/mqshop-admin/b814ece660c9eb6cf2156e58aa6de55de9134a8b/screenshot/list.png
--------------------------------------------------------------------------------
/screenshot/list1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MQEnergy/mqshop-admin/b814ece660c9eb6cf2156e58aa6de55de9134a8b/screenshot/list1.png
--------------------------------------------------------------------------------
/screenshot/list2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MQEnergy/mqshop-admin/b814ece660c9eb6cf2156e58aa6de55de9134a8b/screenshot/list2.png
--------------------------------------------------------------------------------
/screenshot/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MQEnergy/mqshop-admin/b814ece660c9eb6cf2156e58aa6de55de9134a8b/screenshot/login.png
--------------------------------------------------------------------------------
/screenshot/product-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MQEnergy/mqshop-admin/b814ece660c9eb6cf2156e58aa6de55de9134a8b/screenshot/product-01.png
--------------------------------------------------------------------------------
/screenshot/sider.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MQEnergy/mqshop-admin/b814ece660c9eb6cf2156e58aa6de55de9134a8b/screenshot/sider.png
--------------------------------------------------------------------------------
/src/apis/auth.ts:
--------------------------------------------------------------------------------
1 | import {HttpClient} from '@/lib/request';
2 | import {LoginReq, LogoutReq} from './models/user-model';
3 |
4 | export const Login = (data: LoginReq) => {
5 | return HttpClient.post(`/backend/auth/login?noCache=${data.noCache}`, data)
6 | }
7 | export const Logout = (data?: LogoutReq) => {
8 | return HttpClient.post(`/backend/auth/logout?noCache=${data?.noCache}`, {})
9 | }
--------------------------------------------------------------------------------
/src/apis/common.ts:
--------------------------------------------------------------------------------
1 | import {HttpClient} from "@/lib/request";
2 | import {AttachmentIndexReq, AttachmentUploadReq} from "@/apis/models/base-model";
3 |
4 | export const AttachmentIndex = (params: AttachmentIndexReq) => {
5 | return HttpClient.get(`/backend/attachment/index`, {params});
6 | }
7 |
8 | export const AttachmentUpload = (data: AttachmentUploadReq) => {
9 | return HttpClient.post(`/backend/attachment/upload?noCache=${data.noCache}`, data, {
10 | headers: {
11 | 'Content-Type': 'multipart/form-data'
12 | }
13 | });
14 | }
15 |
--------------------------------------------------------------------------------
/src/apis/models/base-model.ts:
--------------------------------------------------------------------------------
1 | export interface CacheReq {
2 | noCache?: boolean
3 | }
4 |
5 | export interface PageReq {
6 | page: number; // 页码
7 | limit: number; // 每页数量
8 | search?: any // 搜索
9 | }
10 |
11 | export interface ViewReq {
12 | id: number;
13 | }
14 |
15 | // =============================== attachment ==================================
16 | export interface AttachmentModel {
17 | attach_name: string;
18 | attach_origin_name: string;
19 | attach_url: string;
20 | attach_type: number;
21 | attach_mine_type: string;
22 | attach_extension: string;
23 | attach_size: string;
24 | status: number;
25 | }
26 |
27 | export interface AttachmentIndexReq extends PageReq, CacheReq {
28 | }
29 |
30 | export interface AttachmentUploadReq extends FormData, CacheReq {
31 | }
32 |
33 | export interface AttachmentViewReq extends ViewReq, CacheReq {
34 | }
35 |
36 | export interface AttachmentDeleteReq extends ViewReq, CacheReq {
37 | }
38 |
39 | export interface AttachmentUpdateReq extends AttachmentModel, ViewReq, CacheReq {
40 | }
41 |
42 | // =============================== region ==================================
43 | export interface CityModel {
44 | code: string;
45 | name: string;
46 | province_code: string;
47 | leter: string;
48 | status: string;
49 | spelling: string;
50 | acronym: string;
51 | }
52 | export interface AreaModel {
53 | code: string;
54 | name: string;
55 | city_code: string;
56 | province_code: string;
57 | }
58 |
59 | export interface CityIndexReq extends PageReq, CacheReq {
60 | }
61 |
62 | export interface CityViewReq extends ViewReq, CacheReq {
63 | }
64 |
65 | export interface CityCreateReq extends CityModel, CacheReq {
66 | }
67 |
68 | export interface CityUpdateReq extends CityModel, ViewReq, CacheReq {
69 | }
70 |
71 | export interface CityDeleteReq extends ViewReq, CacheReq {
72 | }
73 |
74 | export interface CityListReq extends CacheReq {
75 | }
76 |
--------------------------------------------------------------------------------
/src/apis/models/permission-model.ts:
--------------------------------------------------------------------------------
1 | import {CacheReq, PageReq, ViewReq} from "@/apis/models/base-model";
2 |
3 | // ============================ member =====================================
4 | export interface MemberReq {
5 | account: string;
6 | real_name: string;
7 | password: string;
8 | phone: string;
9 | avatar: string;
10 | status: number;
11 | role_ids: string;
12 | }
13 | export interface MemberIndexReq extends PageReq, CacheReq {}
14 | export interface MemberViewReq extends CacheReq {}
15 | export interface MemberInfoReq extends ViewReq, CacheReq {}
16 | export interface MemberUpdateReq extends ViewReq, MemberReq, CacheReq {}
17 | export interface MemberCreateReq extends MemberReq, CacheReq {}
18 | export interface MemberDeleteReq extends CacheReq {
19 | ids: string
20 | }
21 | export interface MemberChangePassReq extends CacheReq {
22 | uuid: string;
23 | new_pass: string;
24 | repeat_pass: string;
25 | }
26 | export interface MemberRoleDistributionReq extends ViewReq, CacheReq {
27 | role_ids: string
28 | }
29 |
30 | // ============================ role =====================================
31 | interface RoleReq {
32 | name: string;
33 | desc: string;
34 | status: number;
35 | }
36 | export interface RoleIndexReq extends PageReq, CacheReq {}
37 | export interface RoleListReq extends CacheReq {}
38 | export interface RoleUpdateReq extends ViewReq, RoleReq, CacheReq {}
39 | export interface RoleDeleteReq extends CacheReq {
40 | ids: string;
41 | }
42 | export interface RoleCreateReq extends RoleReq, CacheReq {}
43 |
44 | // ============================ resource =====================================
45 | interface ResourceReq {
46 | name: string;
47 | alias: string;
48 | desc: string;
49 | f_url: string;
50 | b_url: string;
51 | icon: string;
52 | parent_id: number;
53 | path: string;
54 | menu_type: number;
55 | status: number;
56 | sort_order: number;
57 | }
58 | export interface ResourceIndexReq extends PageReq, CacheReq {}
59 | export interface ResourceUpdateReq extends ViewReq, ResourceReq, CacheReq {}
60 | export interface ResourceViewReq extends ViewReq, CacheReq {}
61 | export interface ResourceDeleteReq extends CacheReq {
62 | ids: string;
63 | }
64 | export interface ResourceCreateReq extends ResourceReq, CacheReq {}
--------------------------------------------------------------------------------
/src/apis/models/user-model.ts:
--------------------------------------------------------------------------------
1 | import {CacheReq} from "@/apis/models/base-model";
2 |
3 | export interface LoginReq extends CacheReq {
4 | account: string; //
5 | password: string; //
6 | }
7 | export interface LogoutReq extends CacheReq {}
--------------------------------------------------------------------------------
/src/components/custom/auto-form/common/label.tsx:
--------------------------------------------------------------------------------
1 | import { FormLabel } from "@/components/ui/form";
2 | import { cn } from "@/lib/utils";
3 | import {ReactNode} from "react";
4 |
5 | function AutoFormLabel({
6 | icon,
7 | label,
8 | isRequired,
9 | className,
10 | }: {
11 | icon?: ReactNode;
12 | label: string;
13 | isRequired: boolean;
14 | className?: string;
15 | }) {
16 | return (
17 | <>
18 |
19 | {icon} {label}
20 | {isRequired && *}
21 |
22 | >
23 | );
24 | }
25 |
26 | export default AutoFormLabel;
27 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/common/tooltip.tsx:
--------------------------------------------------------------------------------
1 | function AutoFormTooltip({ fieldConfigItem }: { fieldConfigItem: any }) {
2 | return (
3 | <>
4 | {fieldConfigItem?.description && (
5 |
6 | {fieldConfigItem.description}
7 |
8 | )}
9 | >
10 | );
11 | }
12 |
13 | export default AutoFormTooltip;
14 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/config.ts:
--------------------------------------------------------------------------------
1 | import AutoFormCheckbox from "./fields/checkbox";
2 | import AutoFormDate from "./fields/date";
3 | import AutoFormEnum from "./fields/enum";
4 | import AutoFormFile from "./fields/file";
5 | import AutoFormInput from "./fields/input";
6 | import AutoFormNumber from "./fields/number";
7 | import AutoFormRadioGroup from "./fields/radio-group";
8 | import AutoFormSwitch from "./fields/switch";
9 | import AutoFormTextarea from "./fields/textarea";
10 |
11 | export const INPUT_COMPONENTS = {
12 | checkbox: AutoFormCheckbox,
13 | date: AutoFormDate,
14 | select: AutoFormEnum,
15 | radio: AutoFormRadioGroup,
16 | switch: AutoFormSwitch,
17 | textarea: AutoFormTextarea,
18 | number: AutoFormNumber,
19 | file: AutoFormFile,
20 | fallback: AutoFormInput,
21 | };
22 |
23 | /**
24 | * Define handlers for specific Zod types.
25 | * You can expand this object to support more types.
26 | */
27 | export const DEFAULT_ZOD_HANDLERS: {
28 | [key: string]: keyof typeof INPUT_COMPONENTS;
29 | } = {
30 | ZodBoolean: "checkbox",
31 | ZodDate: "date",
32 | ZodEnum: "select",
33 | ZodNativeEnum: "select",
34 | ZodNumber: "number",
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/dependencies.ts:
--------------------------------------------------------------------------------
1 | import { FieldValues, UseFormWatch } from "react-hook-form";
2 | import { Dependency, DependencyType, EnumValues } from "./types";
3 | import * as z from "zod";
4 |
5 | export default function resolveDependencies<
6 | SchemaType extends z.infer>,
7 | >(
8 | dependencies: Dependency[],
9 | currentFieldName: keyof SchemaType,
10 | watch: UseFormWatch,
11 | ) {
12 | let isDisabled = false;
13 | let isHidden = false;
14 | let isRequired = false;
15 | let overrideOptions: EnumValues | undefined;
16 |
17 | const currentFieldValue = watch(currentFieldName as string);
18 |
19 | const currentFieldDependencies = dependencies.filter(
20 | (dependency) => dependency.targetField === currentFieldName,
21 | );
22 | for (const dependency of currentFieldDependencies) {
23 | const watchedValue = watch(dependency.sourceField as string);
24 |
25 | const conditionMet = dependency.when(watchedValue, currentFieldValue);
26 |
27 | switch (dependency.type) {
28 | case DependencyType.DISABLES:
29 | if (conditionMet) {
30 | isDisabled = true;
31 | }
32 | break;
33 | case DependencyType.REQUIRES:
34 | if (conditionMet) {
35 | isRequired = true;
36 | }
37 | break;
38 | case DependencyType.HIDES:
39 | if (conditionMet) {
40 | isHidden = true;
41 | }
42 | break;
43 | case DependencyType.SETS_OPTIONS:
44 | if (conditionMet) {
45 | overrideOptions = dependency.options;
46 | }
47 | break;
48 | }
49 | }
50 |
51 | return {
52 | isDisabled,
53 | isHidden,
54 | isRequired,
55 | overrideOptions,
56 | };
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import { Checkbox } from "@/components/ui/checkbox";
2 | import { FormControl, FormItem } from "@/components/ui/form";
3 | import AutoFormTooltip from "../common/tooltip";
4 | import { AutoFormInputComponentProps } from "../types";
5 | import AutoFormLabel from "../common/label";
6 |
7 | export default function AutoFormCheckbox({
8 | label,
9 | isRequired,
10 | field,
11 | fieldConfigItem,
12 | fieldProps,
13 | }: AutoFormInputComponentProps) {
14 | return (
15 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/date.tsx:
--------------------------------------------------------------------------------
1 | import { DatePicker } from "@/components/custom/date-picker";
2 | import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
3 | import AutoFormLabel from "../common/label";
4 | import AutoFormTooltip from "../common/tooltip";
5 | import { AutoFormInputComponentProps } from "../types";
6 |
7 | export default function AutoFormDate({
8 | label,
9 | isRequired,
10 | field,
11 | fieldConfigItem,
12 | fieldProps,
13 | }: AutoFormInputComponentProps) {
14 | return (
15 |
16 |
20 |
21 |
26 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/enum.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
2 | import {
3 | Select,
4 | SelectContent,
5 | SelectItem,
6 | SelectTrigger,
7 | SelectValue,
8 | } from "@/components/ui/select";
9 | import * as z from "zod";
10 | import AutoFormLabel from "../common/label";
11 | import AutoFormTooltip from "../common/tooltip";
12 | import { AutoFormInputComponentProps } from "../types";
13 | import { getBaseSchema } from "../utils";
14 |
15 | export default function AutoFormEnum({
16 | label,
17 | isRequired,
18 | field,
19 | fieldConfigItem,
20 | zodItem,
21 | fieldProps,
22 | }: AutoFormInputComponentProps) {
23 | const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum)._def
24 | .values;
25 |
26 | let values: [string, string][] = [];
27 | if (!Array.isArray(baseValues)) {
28 | values = Object.entries(baseValues);
29 | } else {
30 | values = baseValues.map((value) => [value, value]);
31 | }
32 |
33 | function findItem(value: any) {
34 | return values.find((item) => item[0] === value);
35 | }
36 |
37 | return (
38 |
39 |
43 |
44 |
62 |
63 |
64 |
65 |
66 | );
67 | }
68 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/input.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
2 | import { Input } from "@/components/ui/input";
3 | import AutoFormLabel from "../common/label";
4 | import AutoFormTooltip from "../common/tooltip";
5 | import { AutoFormInputComponentProps } from "../types";
6 |
7 | export default function AutoFormInput({
8 | label,
9 | isRequired,
10 | fieldConfigItem,
11 | fieldProps,
12 | }: AutoFormInputComponentProps) {
13 | const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
14 | const showLabel = _showLabel === undefined ? true : _showLabel;
15 | const type = fieldProps.type || "text";
16 |
17 | return (
18 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/number.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
2 | import { Input } from "@/components/ui/input";
3 | import AutoFormLabel from "../common/label";
4 | import AutoFormTooltip from "../common/tooltip";
5 | import { AutoFormInputComponentProps } from "../types";
6 |
7 | export default function AutoFormNumber({
8 | label,
9 | isRequired,
10 | fieldConfigItem,
11 | fieldProps,
12 | }: AutoFormInputComponentProps) {
13 | const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
14 | const showLabel = _showLabel === undefined ? true : _showLabel;
15 |
16 | return (
17 |
18 | {showLabel && (
19 |
23 | )}
24 |
25 |
26 |
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FormControl,
3 | FormItem,
4 | FormLabel,
5 | FormMessage,
6 | } from "@/components/ui/form";
7 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
8 | import * as z from "zod";
9 | import AutoFormLabel from "../common/label";
10 | import AutoFormTooltip from "../common/tooltip";
11 | import { AutoFormInputComponentProps } from "../types";
12 | import { getBaseSchema } from "../utils";
13 |
14 | export default function AutoFormRadioGroup({
15 | label,
16 | isRequired,
17 | field,
18 | zodItem,
19 | fieldProps,
20 | fieldConfigItem,
21 | }: AutoFormInputComponentProps) {
22 | const baseValues = (getBaseSchema(zodItem) as unknown as z.ZodEnum)._def
23 | .values;
24 |
25 | let values: string[] = [];
26 | if (!Array.isArray(baseValues)) {
27 | values = Object.entries(baseValues).map((item) => item[0]);
28 | } else {
29 | values = baseValues;
30 | }
31 |
32 | return (
33 |
34 |
35 |
39 |
40 |
45 | {values?.map((value: any) => (
46 |
50 |
51 |
52 |
53 | {value}
54 |
55 | ))}
56 |
57 |
58 |
59 |
60 |
61 |
62 | );
63 | }
64 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/select.tsx:
--------------------------------------------------------------------------------
1 | import {FormControl, FormItem, FormMessage} from "@/components/ui/form";
2 | import {
3 | Select,
4 | SelectContent,
5 | SelectItem,
6 | SelectTrigger,
7 | SelectValue,
8 | } from "@/components/ui/select";
9 | import AutoFormLabel from "../common/label";
10 | import AutoFormTooltip from "../common/tooltip";
11 | import {AutoFormInputComponentProps} from "../types";
12 | import {getDefaultValueInZodStack} from "../utils";
13 |
14 | export default function AutoFormSelect({
15 | label,
16 | isRequired,
17 | field,
18 | fieldConfigItem,
19 | zodItem,
20 | fieldProps,
21 | }: AutoFormInputComponentProps) {
22 | const baseValues = getDefaultValueInZodStack(zodItem)
23 | let values: { label: string, value: number }[] = [];
24 | if (typeof baseValues === 'object') {
25 | values = baseValues?.map((item: any) => {
26 | return {
27 | label: item?.label || '',
28 | value: parseInt(item.value) || 0
29 | };
30 | });
31 | }
32 |
33 | function findItem(value: any) {
34 | return values.find((item) => item.value === value);
35 | }
36 |
37 | return (
38 |
39 |
43 |
44 |
63 |
64 |
65 |
66 |
67 | );
68 | }
69 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/switch.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormItem } from "@/components/ui/form";
2 | import { Switch } from "@/components/ui/switch";
3 | import AutoFormLabel from "../common/label";
4 | import AutoFormTooltip from "../common/tooltip";
5 | import { AutoFormInputComponentProps } from "../types";
6 |
7 | export default function AutoFormSwitch({
8 | label,
9 | isRequired,
10 | field,
11 | fieldConfigItem,
12 | fieldProps,
13 | }: AutoFormInputComponentProps) {
14 | return (
15 |
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/src/components/custom/auto-form/fields/textarea.tsx:
--------------------------------------------------------------------------------
1 | import { FormControl, FormItem, FormMessage } from "@/components/ui/form";
2 | import { Textarea } from "@/components/ui/textarea";
3 | import AutoFormLabel from "../common/label";
4 | import AutoFormTooltip from "../common/tooltip";
5 | import { AutoFormInputComponentProps } from "../types";
6 |
7 | export default function AutoFormTextarea({
8 | label,
9 | isRequired,
10 | fieldConfigItem,
11 | fieldProps,
12 | }: AutoFormInputComponentProps) {
13 | const { showLabel: _showLabel, ...fieldPropsWithoutShowLabel } = fieldProps;
14 | const showLabel = _showLabel === undefined ? true : _showLabel;
15 | return (
16 |
17 | {showLabel && (
18 |
22 | )}
23 |
24 |
25 |
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/custom/badge.tsx:
--------------------------------------------------------------------------------
1 | import {cn} from "@/lib/utils.ts";
2 | import {cva, type VariantProps} from "class-variance-authority";
3 | import * as React from "react";
4 |
5 | const badgeVariants = cva(
6 | "inline-flex items-center rounded-md border px-2.5 py-1 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
7 | {
8 | variants: {
9 | variant: {
10 | default:
11 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12 | secondary:
13 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14 | destructive:
15 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16 | outline: "text-foreground",
17 | gray: "border-transparent bg-gray-50 text-gray-600",
18 | red: "border-transparent bg-red-50 text-red-700",
19 | yellow: "border-transparent bg-yellow-50 text-yellow-800",
20 | green: "border-transparent bg-green-50 text-green-700",
21 | blue: "border-transparent bg-blue-50 text-blue-700",
22 | purple: "border-transparent bg-purple-50 text-purple-700",
23 | indigo: "border-transparent bg-indigo-50 text-indigo-700",
24 | pink: "border-transparent bg-pink-50 text-pink-700",
25 | },
26 | },
27 | defaultVariants: {
28 | variant: "default",
29 | },
30 | }
31 | )
32 |
33 | interface CustomBadgeProps extends React.HTMLAttributes,
34 | VariantProps {
35 | }
36 |
37 | function Badge({className, variant, ...props}: CustomBadgeProps) {
38 | return (
39 |
40 | )
41 | }
42 |
43 | export {Badge, badgeVariants}
--------------------------------------------------------------------------------
/src/components/custom/confirm-dialog.tsx:
--------------------------------------------------------------------------------
1 | import {AlertDialogProps} from "@radix-ui/react-alert-dialog";
2 | import {
3 | AlertDialog,
4 | AlertDialogContent, AlertDialogDescription,
5 | AlertDialogFooter,
6 | AlertDialogHeader,
7 | AlertDialogTitle
8 | } from "@/components/ui/alert-dialog.tsx";
9 | import {Button} from "@/components/custom/button.tsx";
10 | import {useTranslation} from "react-i18next";
11 |
12 | export interface DialogAlertProps extends AlertDialogProps {
13 | width?: string;
14 | height?: string;
15 | loading: boolean
16 | title?: string
17 | description?: string
18 | closeTitle?: string
19 | submitTitle?: string
20 | isDelete?: boolean
21 | onCancel: () => void
22 | onSubmit: (values: any) => void
23 | }
24 |
25 | function ConfirmDialog({...props}: DialogAlertProps) {
26 | const {t} = useTranslation()
27 | return (
28 |
29 |
34 |
35 | {props.title || t('settings.dialog.title')}
36 | {props.description && {props.description}}
37 |
38 |
39 |
41 | {props.isDelete ?
42 |
44 | :
45 | }
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | ConfirmDialog.displayName = 'ConfirmDialog'
54 |
55 | export default ConfirmDialog
56 |
--------------------------------------------------------------------------------
/src/components/custom/data-table/data-table-searchbar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {Card, CardContent} from "@/components/ui/card.tsx";
3 |
4 | import {Button} from "@/components/custom/button.tsx";
5 | import {Trash2, Search, Loader2} from "lucide-react";
6 | import {useContext} from "react";
7 | import {TableContext} from "@/context.tsx";
8 |
9 | export interface SearchBarProps extends React.HTMLAttributes {
10 | children: React.ReactNode
11 | loading?: boolean
12 | onReset: () => void
13 | onSubmit: () => void
14 | }
15 |
16 | // DataTableSearchBar 搜索
17 | const DataTableSearchBar = React.forwardRef(
18 | ({className, children, ...props}, ref) => {
19 | const {trans} = useContext(TableContext)
20 | return (
21 |
22 |
23 |
24 | {children}
25 |
26 |
27 |
28 |
34 |
40 |
41 |
42 |
43 |
44 |
45 | )
46 | }
47 | );
48 |
49 | DataTableSearchBar.displayName = 'DataTableSearchBar'
50 |
51 | export default DataTableSearchBar;
--------------------------------------------------------------------------------
/src/components/custom/data-table/makeData.ts:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker'
2 |
3 | export type Person = {
4 | firstName: string
5 | lastName: string
6 | age: number
7 | visits: number
8 | progress: number
9 | status: 'relationship' | 'complicated' | 'single'
10 | subRows?: Person[]
11 | }
12 |
13 | const range = (len: number) => {
14 | const arr: number[] = []
15 | for (let i = 0; i < len; i++) {
16 | arr.push(i)
17 | }
18 | return arr
19 | }
20 |
21 | const newPerson = (): Person => {
22 | return {
23 | firstName: faker.person.firstName(),
24 | lastName: faker.person.lastName(),
25 | age: faker.number.int(40),
26 | visits: faker.number.int(1000),
27 | progress: faker.number.int(100),
28 | status: faker.helpers.shuffle([
29 | 'relationship',
30 | 'complicated',
31 | 'single',
32 | ])[0]!,
33 | }
34 | }
35 |
36 | export function makeData(...lens: number[]) {
37 | const makeDataLevel = (depth = 0): Person[] => {
38 | const len = lens[depth]!
39 | return range(len).map((): Person => {
40 | return {
41 | ...newPerson(),
42 | subRows: lens[depth + 1] ? makeDataLevel(depth + 1) : undefined,
43 | }
44 | })
45 | }
46 |
47 | return makeDataLevel()
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/custom/date-picker.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 | import { format } from "date-fns";
3 | import { Calendar as CalendarIcon } from "lucide-react";
4 |
5 | import { cn } from "@/lib/utils";
6 | import { Button } from "@/components/ui/button";
7 | import { Calendar } from "@/components/ui/calendar";
8 | import {
9 | Popover,
10 | PopoverContent,
11 | PopoverTrigger,
12 | } from "@/components/ui/popover";
13 | import { forwardRef } from "react";
14 |
15 | export const DatePicker = forwardRef<
16 | HTMLDivElement,
17 | {
18 | date?: Date;
19 | setDate: (date?: Date) => void;
20 | }
21 | >(function DatePickerCmp({ date, setDate }, ref) {
22 | return (
23 |
24 |
25 |
35 |
36 |
37 |
43 |
44 |
45 | );
46 | });
--------------------------------------------------------------------------------
/src/components/custom/form-dialog.tsx:
--------------------------------------------------------------------------------
1 | import {DialogProps} from "@radix-ui/react-dialog";
2 | import {
3 | Dialog,
4 | DialogContent,
5 | DialogDescription,
6 | DialogFooter,
7 | DialogHeader,
8 | DialogTitle
9 | } from "@/components/ui/dialog.tsx";
10 | import {Button} from "@/components/custom/button.tsx";
11 | import {useTranslation} from "react-i18next";
12 | import {cn} from "@/lib/utils";
13 |
14 | export interface FormDialogProps extends DialogProps {
15 | className?: string;
16 | width?: string;
17 | height?: string;
18 | loading: boolean
19 | title?: string
20 | description?: string
21 | closeTitle?: string
22 | submitTitle?: string
23 | onCancel: () => void
24 | onSubmit?: (values: any) => void
25 | }
26 |
27 | function FormDialog({...props}: FormDialogProps) {
28 | const {t} = useTranslation()
29 | return (
30 |
55 | )
56 | }
57 |
58 | FormDialog.displayName = 'FormDialog'
59 |
60 | export default FormDialog
61 |
--------------------------------------------------------------------------------
/src/components/custom/icon.tsx:
--------------------------------------------------------------------------------
1 | import {icons, LucideProps} from 'lucide-react';
2 |
3 | interface IconProps extends Omit {
4 | name: keyof typeof icons
5 | }
6 |
7 | const Icon = ({name, ...props}: IconProps) => {
8 | const LucideIcon = icons[name] !== undefined ? icons[name] : ''
9 | if (!LucideIcon) {
10 | return ''
11 | }
12 | return
13 | }
14 |
15 | export default Icon
--------------------------------------------------------------------------------
/src/components/custom/image-uploader.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | FileUploader,
3 | FileInput,
4 | FileUploaderContent,
5 | FileUploaderItem, FileSvgDraw,
6 | } from "@/components/custom/file-uploader";
7 | import {Accept, DropzoneOptions} from "react-dropzone";
8 | import 'react-photo-view/dist/react-photo-view.css';
9 | import {PhotoProvider, PhotoView} from "react-photo-view";
10 |
11 |
12 | interface FileUploadDropzoneProps {
13 | files: File[] | null;
14 | onValueChange: (files: File[] | null) => void;
15 | accept?: Accept;
16 | multiple?: boolean;
17 | maxFiles?: number;
18 | maxSize?: number;
19 | noPreview: boolean;
20 | }
21 |
22 | const FileUploadDropzone = ({...props}: FileUploadDropzoneProps) => {
23 |
24 | const dropzone = {
25 | accept: props.accept || {
26 | "image/*": [".jpg", ".jpeg", ".png"],
27 | },
28 | multiple: props.multiple || true,
29 | maxFiles: props.maxFiles || 4,
30 | maxSize: props.maxSize || 1024 * 1024,
31 | } satisfies DropzoneOptions;
32 |
33 | return (
34 |
40 |
41 |
42 |
43 |
44 |
45 | {!props.noPreview &&
46 |
47 | {props.files?.map((file, i) => (
48 |
54 |
55 |
60 |
61 |
62 | ))}
63 |
64 | }
65 |
66 | );
67 | };
68 |
69 | export default FileUploadDropzone;
70 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/generative/ai-completion-command.tsx:
--------------------------------------------------------------------------------
1 | import { CommandGroup, CommandItem, CommandSeparator } from "@/components/ui/command";
2 | import { useEditor } from "novel";
3 | import { Check, TextQuote, TrashIcon } from "lucide-react";
4 |
5 | const AICompletionCommands = ({
6 | completion,
7 | onDiscard,
8 | }: {
9 | completion: string;
10 | onDiscard: () => void;
11 | }) => {
12 | const { editor } = useEditor();
13 | return (
14 | <>
15 |
16 | {
20 | const selection = editor?.view.state.selection;
21 |
22 | editor?.chain()
23 | .focus()
24 | .insertContentAt(
25 | {
26 | from: selection?.from as number,
27 | to: selection?.to as number,
28 | },
29 | completion,
30 | )
31 | .run();
32 | }}
33 | >
34 |
35 | Replace selection
36 |
37 | {
41 | const selection = editor?.view.state.selection;
42 | editor?.chain()
43 | .focus()
44 | .insertContentAt(selection?.to as number + 1, completion)
45 | .run();
46 | }}
47 | >
48 |
49 | Insert below
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | Discard
58 |
59 |
60 | >
61 | );
62 | };
63 |
64 | export default AICompletionCommands;
65 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/generative/ai-selector-commands.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowDownWideNarrow, CheckCheck, RefreshCcwDot, StepForward, WrapText } from "lucide-react";
2 | import { useEditor } from "novel";
3 | import { getPrevText } from "novel/utils";
4 | import { CommandGroup, CommandItem, CommandSeparator } from "@/components/ui/command";
5 |
6 | const options = [
7 | {
8 | value: "improve",
9 | label: "Improve writing",
10 | icon: RefreshCcwDot,
11 | },
12 |
13 | {
14 | value: "fix",
15 | label: "Fix grammar",
16 | icon: CheckCheck,
17 | },
18 | {
19 | value: "shorter",
20 | label: "Make shorter",
21 | icon: ArrowDownWideNarrow,
22 | },
23 | {
24 | value: "longer",
25 | label: "Make longer",
26 | icon: WrapText,
27 | },
28 | ];
29 |
30 | interface AISelectorCommandsProps {
31 | onSelect: (value: string, option: string) => void;
32 | }
33 |
34 | const AISelectorCommands = ({ onSelect }: AISelectorCommandsProps) => {
35 | const { editor } = useEditor();
36 |
37 | return (
38 | <>
39 |
40 | {options.map((option) => (
41 | {
43 | const slice = editor?.state.selection.content();
44 | const text = editor?.storage.markdown.serializer.serialize(slice?.content);
45 | onSelect(text, value);
46 | }}
47 | className="flex gap-2 px-4"
48 | key={option.value}
49 | value={option.value}
50 | >
51 |
52 | {option.label}
53 |
54 | ))}
55 |
56 |
57 |
58 | {
60 | const pos = editor?.state.selection.from;
61 |
62 | if (editor && pos) {
63 | const text = getPrevText(editor, pos);
64 | onSelect(text, "continue");
65 | }
66 | }}
67 | value="continue"
68 | className="gap-2 px-4"
69 | >
70 |
71 | Continue writing
72 |
73 |
74 | >
75 | );
76 | };
77 |
78 | export default AISelectorCommands;
79 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/generative/generative-menu-switch.tsx:
--------------------------------------------------------------------------------
1 | import { EditorBubble, useEditor } from "novel";
2 | import { removeAIHighlight } from "novel/extensions";
3 | import {} from "novel/plugins";
4 | import { Fragment, type ReactNode, useEffect } from "react";
5 | import { Button } from "@/components/ui/button";
6 | import Magic from "../icons/magic";
7 | import { AISelector } from "./ai-selector";
8 |
9 | interface GenerativeMenuSwitchProps {
10 | children: ReactNode;
11 | open: boolean;
12 | onOpenChange: (open: boolean) => void;
13 | }
14 | const GenerativeMenuSwitch = ({ children, open, onOpenChange }: GenerativeMenuSwitchProps) => {
15 | const { editor } = useEditor();
16 |
17 | useEffect(() => {
18 | if (!open && editor) removeAIHighlight(editor);
19 | }, [open]);
20 | return (
21 | {
25 | onOpenChange(false);
26 | editor?.chain().unsetHighlight().run();
27 | },
28 | }}
29 | className="flex w-fit max-w-[90vw] overflow-hidden rounded-md border border-muted bg-background shadow-xl"
30 | >
31 | {open && }
32 | {!open && (
33 |
34 |
43 | {children}
44 |
45 | )}
46 |
47 | );
48 | };
49 |
50 | export default GenerativeMenuSwitch;
51 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/icons/crazy-spinner.tsx:
--------------------------------------------------------------------------------
1 | const CrazySpinner = () => {
2 | return (
3 |
8 | );
9 | };
10 |
11 | export default CrazySpinner;
12 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/icons/font-default.tsx:
--------------------------------------------------------------------------------
1 | export default function FontDefault({ className }: { className?: string }) {
2 | return (
3 |
14 | );
15 | }
16 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/icons/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as FontDefault } from "./font-default";
2 | export { default as FontSerif } from "./font-serif";
3 | export { default as FontMono } from "./font-mono";
4 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/icons/loading-circle.tsx:
--------------------------------------------------------------------------------
1 | export default function LoadingCircle({ dimensions }: { dimensions?: string }) {
2 | return (
3 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/image-upload.ts:
--------------------------------------------------------------------------------
1 | import {createImageUpload} from "novel/plugins";
2 | import {AttachmentUpload} from "@/apis/common";
3 | import {toast} from "sonner";
4 |
5 |
6 | const onUpload = (file: File) => {
7 | const formData = new FormData();
8 | formData.append('file', file)
9 | formData.append('file_path', 'product')
10 |
11 | const promise = AttachmentUpload(formData)
12 |
13 | return new Promise((resolve, reject) => {
14 | toast.promise(
15 | promise.then(async (res) => {
16 | // Successfully uploaded image
17 | if (res.errcode === 0) {
18 | const url = res.data.file_path ? import.meta.env.VITE_RESOURCE_URL + res.data.file_path : res.data.file_path
19 | // preload the image
20 | const image = new Image();
21 | image.src = url;
22 | image.onload = () => {
23 | resolve(url);
24 | };
25 | // No blob store configured
26 | } else {
27 | resolve('');
28 | throw new Error("Error uploading image. Please try again.");
29 | }
30 | }),
31 | {
32 | loading: "Uploading image...",
33 | success: "Image uploaded successfully.",
34 | error: (e) => {
35 | reject(e);
36 | return e.message;
37 | },
38 | },
39 | );
40 | });
41 | };
42 |
43 | export const uploadFn = createImageUpload({
44 | onUpload,
45 | validateFn: (file) => {
46 | if (!file.type.includes("image/")) {
47 | toast.error("File type not supported.");
48 | return false;
49 | }
50 | if (file.size / 1024 / 1024 > 20) {
51 | toast.error("File size too big (max 20MB).");
52 | return false;
53 | }
54 | return true;
55 | },
56 | });
57 |
--------------------------------------------------------------------------------
/src/components/custom/novel-editor/selectors/text-buttons.tsx:
--------------------------------------------------------------------------------
1 | import {Button} from "@/components/ui/button";
2 | import {cn} from "@/lib/utils";
3 | import {BoldIcon, CodeIcon, ItalicIcon, StrikethroughIcon, UnderlineIcon} from "lucide-react";
4 | import {EditorBubbleItem, useEditor} from "novel";
5 | import type {SelectorItem} from "./node-selector";
6 |
7 | export const TextButtons = () => {
8 | const {editor} = useEditor();
9 | if (!editor) return null;
10 | const items: SelectorItem[] = [
11 | {
12 | name: "bold",
13 | isActive: (editor) => editor?.isActive("bold") || false,
14 | command: (editor) => editor?.chain().focus().toggleBold().run(),
15 | icon: BoldIcon,
16 | },
17 | {
18 | name: "italic",
19 | isActive: (editor) => editor?.isActive("italic") || false,
20 | command: (editor) => editor?.chain().focus().toggleItalic().run(),
21 | icon: ItalicIcon,
22 | },
23 | {
24 | name: "underline",
25 | isActive: (editor) => editor?.isActive("underline") || false,
26 | command: (editor) => editor?.chain().focus().toggleUnderline().run(),
27 | icon: UnderlineIcon,
28 | },
29 | {
30 | name: "strike",
31 | isActive: (editor) => editor?.isActive("strike") || false,
32 | command: (editor) => editor?.chain().focus().toggleStrike().run(),
33 | icon: StrikethroughIcon,
34 | },
35 | {
36 | name: "code",
37 | isActive: (editor) => editor?.isActive("code") || false,
38 | command: (editor) => editor?.chain().focus().toggleCode().run(),
39 | icon: CodeIcon,
40 | },
41 | ];
42 | return (
43 |
44 | {items.map((item) => (
45 |
46 |
56 |
57 | ))}
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/components/custom/password-input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import {IconEye, IconEyeOff} from '@tabler/icons-react'
3 | import {Button} from '@/components/ui/button'
4 | import {cn} from '@/lib/utils'
5 | import {Input} from "@/components/ui/input";
6 |
7 | export interface PasswordInputProps
8 | extends Omit, 'type'> {
9 | }
10 |
11 | const PasswordInput = React.forwardRef(
12 | ({className, ...props}, ref) => {
13 | const [showPassword, setShowPassword] = React.useState(false)
14 | return (
15 |
16 |
25 |
34 |
35 | )
36 | }
37 | )
38 | PasswordInput.displayName = 'PasswordInput'
39 |
40 | export {PasswordInput}
41 |
--------------------------------------------------------------------------------
/src/components/custom/search-input.tsx:
--------------------------------------------------------------------------------
1 | import {Input, InputProps} from '@/components/ui/input'
2 | import {cn} from "@/lib/utils";
3 | import {useTranslation} from "react-i18next";
4 |
5 | interface SearchProps extends InputProps {
6 | type?: string;
7 | placeholder?: string;
8 | className?: string;
9 | onKeyword: (keyword: any) => void;
10 | }
11 |
12 | function SearchInput(props: SearchProps) {
13 | const {type, placeholder, className} = props
14 | const {t} = useTranslation()
15 |
16 | return (
17 |
18 | props.onKeyword(e.target.value)}
22 | placeholder={placeholder || t('settings.search.placeholder')}
23 | className={cn('h-9 md:w-[200px] lg:w-[300px]', className)}
24 | />
25 |
26 | )
27 | }
28 |
29 | export {SearchInput}
--------------------------------------------------------------------------------
/src/components/custom/single-breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {
3 | Breadcrumb,
4 | BreadcrumbItem,
5 | BreadcrumbLink,
6 | BreadcrumbList,
7 | BreadcrumbSeparator
8 | } from "@/components/ui/breadcrumb";
9 | import {Fragment} from "react";
10 | import {Link} from "react-router-dom";
11 |
12 | interface BreadListItem {
13 | name?: string
14 | link?: string
15 | }
16 |
17 | interface BreadcrumbProps extends React.HTMLAttributes {
18 | breadList: BreadListItem[]
19 | }
20 |
21 | const SingleBreadcrumb = React.forwardRef((
22 | {className, breadList = [], ...props}, ref) => (
23 |
24 |
25 | {breadList.map((val, idx) => {
26 | return (
27 |
28 |
29 |
30 | {val.link ? {val.name} : {val.name}}
31 |
32 |
33 | {idx < breadList.length - 1 && }
34 |
35 | )
36 | }
37 | )}
38 |
39 |
40 | ))
41 |
42 | SingleBreadcrumb.displayName = 'SingleBreadcrumb';
43 |
44 | export {SingleBreadcrumb};
45 | export type {BreadListItem};
46 |
--------------------------------------------------------------------------------
/src/components/custom/skeleton-list.tsx:
--------------------------------------------------------------------------------
1 | import {Skeleton} from "@/components/ui/skeleton"
2 | import React from "react";
3 | import {cn} from "@/lib/utils.ts";
4 |
5 | interface SkeletonListProps extends React.HTMLAttributes {
6 | count: number;
7 | }
8 |
9 | export function SkeletonList({className, count, ...props}: SkeletonListProps) {
10 | const skeletonList: number[] = []
11 | for (let i = 0; i < count; i++) {
12 | skeletonList.push(i)
13 | }
14 | return (
15 |
16 | {skeletonList.map((i) =>
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | )}
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/custom/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | import {createContext, ReactNode, useContext, useEffect, useState} from "react"
2 |
3 | type Theme = "dark" | "light" | "system"
4 |
5 | type ThemeProviderProps = {
6 | children: ReactNode
7 | defaultTheme?: Theme
8 | storageKey?: string
9 | }
10 |
11 | type ThemeProviderState = {
12 | theme: Theme
13 | setTheme: (theme: Theme) => void
14 | }
15 |
16 | const initialState: ThemeProviderState = {
17 | theme: "system",
18 | setTheme: () => null,
19 | }
20 |
21 | const ThemeProviderContext = createContext(initialState)
22 |
23 | export function ThemeProvider({
24 | children,
25 | defaultTheme = "system",
26 | storageKey = "mqshop-ui-theme",
27 | ...props
28 | }: ThemeProviderProps) {
29 | const [theme, setTheme] = useState(
30 | () => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31 | )
32 |
33 | useEffect(() => {
34 | const root = window.document.documentElement
35 |
36 | root.classList.remove("light", "dark")
37 |
38 | if (theme === "system") {
39 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40 | .matches
41 | ? "dark"
42 | : "light"
43 |
44 | root.classList.add(systemTheme)
45 | return
46 | }
47 |
48 | root.classList.add(theme)
49 | }, [theme])
50 |
51 | const value = {
52 | theme,
53 | setTheme: (theme: Theme) => {
54 | localStorage.setItem(storageKey, theme)
55 | setTheme(theme)
56 | },
57 | }
58 |
59 | return (
60 |
61 | {children}
62 |
63 | )
64 | }
65 |
66 | export const useTheme = () => {
67 | const context = useContext(ThemeProviderContext)
68 |
69 | if (context === undefined)
70 | throw new Error("useTheme must be used within a ThemeProvider")
71 |
72 | return context
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/custom/tiptap-editor/components/menu-bubble.tsx:
--------------------------------------------------------------------------------
1 | import {Editor, BubbleMenu} from "@tiptap/react";
2 | import {Fragment} from "react";
3 | import MenuItem from "./menu-item.tsx";
4 |
5 | export default function MenuBubble({editor}: { editor: Editor }) {
6 | const items = [
7 | {
8 | icon: 'bold',
9 | title: 'Bold',
10 | action: () => editor.chain().focus().toggleBold().run(),
11 | isActive: () => editor.isActive('bold'),
12 | },
13 | {
14 | icon: 'italic',
15 | title: 'Italic',
16 | action: () => editor.chain().focus().toggleItalic().run(),
17 | isActive: () => editor.isActive('italic'),
18 | },
19 | {
20 | icon: 'h-1',
21 | title: 'Heading 1',
22 | action: () => editor.chain().focus().toggleHeading({level: 1}).run(),
23 | isActive: () => editor.isActive('heading', {level: 1}),
24 | },
25 | {
26 | icon: 'h-2',
27 | title: 'Heading 2',
28 | action: () => editor.chain().focus().toggleHeading({level: 2}).run(),
29 | isActive: () => editor.isActive('heading', {level: 2}),
30 | },
31 | {
32 | icon: 'h-3',
33 | title: 'Heading 3',
34 | action: () => editor.chain().focus().toggleHeading({level: 3}).run(),
35 | isActive: () => editor.isActive('heading', {level: 3}),
36 | },
37 | {
38 | icon: 'h-4',
39 | title: 'Heading 4',
40 | action: () => editor.chain().focus().toggleHeading({level: 4}).run(),
41 | isActive: () => editor.isActive('heading', {level: 4}),
42 | },
43 | {
44 | icon: 'paragraph',
45 | title: 'Paragraph',
46 | action: () => editor.chain().focus().setParagraph().run(),
47 | isActive: () => editor.isActive('paragraph'),
48 | },
49 | ]
50 |
51 | return (
52 |
54 | {items.map((item, index) => (
55 |
56 |
57 |
58 | ))}
59 |
60 | )
61 | }
--------------------------------------------------------------------------------
/src/components/custom/tiptap-editor/components/menu-floating.tsx:
--------------------------------------------------------------------------------
1 | import {Editor, FloatingMenu} from "@tiptap/react";
2 | import {Fragment} from "react";
3 | import MenuItem from "./menu-item.tsx";
4 |
5 | export default function MenuFloating({editor}: { editor: Editor }) {
6 | const items = [
7 | {
8 | icon: 'h-1',
9 | title: 'Heading 1',
10 | action: () => editor.chain().focus().toggleHeading({level: 1}).run(),
11 | isActive: () => editor.isActive('heading', {level: 1}),
12 | },
13 | {
14 | icon: 'h-2',
15 | title: 'Heading 2',
16 | action: () => editor.chain().focus().toggleHeading({level: 2}).run(),
17 | isActive: () => editor.isActive('heading', {level: 2}),
18 | },
19 | {
20 | icon: 'paragraph',
21 | title: 'Paragraph',
22 | action: () => editor.chain().focus().setParagraph().run(),
23 | isActive: () => editor.isActive('paragraph'),
24 | },
25 | ]
26 |
27 | return (
28 |
29 | {items.map((item, index) => (
30 |
31 |
32 |
33 | ))}
34 |
35 | )
36 | }
--------------------------------------------------------------------------------
/src/components/custom/tiptap-editor/components/menu-item.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '../../button';
2 |
3 | import remixiconUrl from 'remixicon/fonts/remixicon.symbol.svg'
4 |
5 | export default function MenuItem({
6 | icon, title, action, isActive = null,
7 | }: {
8 | icon?: string,
9 | title?: string,
10 | action?: () => void,
11 | isActive?: (() => boolean) | null
12 | }) {
13 | return
26 | }
--------------------------------------------------------------------------------
/src/components/custom/top-nav.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import { Link } from 'react-router-dom'
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuTrigger,
8 | } from '@/components/ui/dropdown-menu'
9 | import { Button } from '@/components/ui/button'
10 | import { IconMenu } from '@tabler/icons-react'
11 |
12 | interface TopNavProps extends React.HTMLAttributes {
13 | links: {
14 | title: string
15 | href: string
16 | isActive: boolean
17 | }[]
18 | }
19 |
20 | export function TopNav({ className, links, ...props }: TopNavProps) {
21 | return (
22 | <>
23 |
24 |
25 |
26 |
29 |
30 |
31 | {links.map(({ title, href, isActive }) => (
32 |
33 |
37 | {title}
38 |
39 |
40 | ))}
41 |
42 |
43 |
44 |
45 |
62 | >
63 | )
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/layout/index.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import { cn } from '@/lib/utils'
3 |
4 | interface LayoutProps extends React.HTMLAttributes {
5 | fadedBelow?: boolean
6 | fixedHeight?: boolean
7 | }
8 |
9 | const Layout = React.forwardRef(
10 | ({
11 | className,
12 | fadedBelow = false,
13 | fixedHeight = false,
14 | ...props
15 | }, ref) => (
16 |
26 | )
27 | )
28 | Layout.displayName = 'Layout'
29 |
30 | const LayoutHeader = React.forwardRef<
31 | HTMLDivElement,
32 | React.HTMLAttributes
33 | >(({ className, ...props }, ref) => (
34 |
42 | ))
43 |
44 | LayoutHeader.displayName = 'LayoutHeader'
45 |
46 | interface LayoutBodyProps extends React.HTMLAttributes {
47 | fixedHeight?: boolean
48 | }
49 |
50 | const LayoutBody = React.forwardRef(
51 | ({ className, fixedHeight, ...props }, ref) => (
52 |
61 | )
62 | )
63 | LayoutBody.displayName = 'LayoutBody'
64 |
65 | export { Layout, LayoutHeader, LayoutBody }
66 |
--------------------------------------------------------------------------------
/src/components/layout/language.tsx:
--------------------------------------------------------------------------------
1 | import {Button} from "@/components/ui/button"
2 | import {
3 | DropdownMenu,
4 | DropdownMenuContent,
5 | DropdownMenuRadioGroup,
6 | DropdownMenuRadioItem,
7 | DropdownMenuTrigger,
8 | } from "@/components/ui/dropdown-menu"
9 | import {Languages} from "lucide-react";
10 | import {useTranslation} from "react-i18next";
11 | import {useStorage} from "@/hooks/use-local-storage";
12 |
13 | export default function Language() {
14 | const [lang, setLang] = useStorage({
15 | key: "mqshop-lng",
16 | defaultValue: 'en-US'
17 | });
18 | const {i18n} = useTranslation();
19 |
20 | const changeLanguage = (lng: string) => {
21 | setLang(lng);
22 | i18n.changeLanguage(lng).then((t) => {
23 | t('menu.welcome');
24 | });
25 | };
26 |
27 | return (
28 |
29 |
30 |
37 |
38 |
39 |
40 | 中文
41 | English
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/layout/notice.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | Popover,
4 | PopoverContent,
5 | PopoverTrigger,
6 | } from "@/components/ui/popover"
7 | import {BellRing} from "lucide-react";
8 | import {Tabs, TabsList, TabsTrigger, TabsContent} from "@/components/ui/tabs";
9 | import {useState} from "react";
10 |
11 | export default function Notice() {
12 | const [activeTab, setActiveTab] = useState('all');
13 | return (
14 |
15 |
16 |
23 |
24 |
25 |
26 |
27 | setActiveTab('all')}>所有
28 | setActiveTab('message')}>消息
29 | setActiveTab('notice')}>通知
30 | setActiveTab('backlog')}>待办
31 |
32 | Make changes to your account here.
33 | Change your password here.
34 |
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDown } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
18 | ))
19 | AccordionItem.displayName = "AccordionItem"
20 |
21 | const AccordionTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
26 | svg]:rotate-180",
30 | className
31 | )}
32 | {...props}
33 | >
34 | {children}
35 |
36 |
37 |
38 | ))
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ))
53 |
54 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
55 |
56 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
57 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Avatar = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Avatar.displayName = AvatarPrimitive.Root.displayName
20 |
21 | const AvatarImage = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
32 |
33 | const AvatarFallback = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 | ))
46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47 |
48 | export { Avatar, AvatarImage, AvatarFallback }
49 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
3 | import { Check } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const Checkbox = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => (
11 |
19 |
22 |
23 |
24 |
25 | ))
26 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
27 |
28 | export { Checkbox }
29 |
--------------------------------------------------------------------------------
/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2 |
3 | const Collapsible = CollapsiblePrimitive.Root
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8 |
9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
10 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as LabelPrimitive from "@radix-ui/react-label"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const labelVariants = cva(
8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9 | )
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef &
14 | VariantProps
15 | >(({ className, ...props }, ref) => (
16 |
21 | ))
22 | Label.displayName = LabelPrimitive.Root.displayName
23 |
24 | export { Label }
25 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverContent = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
14 |
15 |
25 |
26 | ))
27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
28 |
29 | export { Popover, PopoverTrigger, PopoverContent }
30 |
--------------------------------------------------------------------------------
/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ProgressPrimitive from "@radix-ui/react-progress"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Progress = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, value, ...props }, ref) => (
10 |
18 |
22 |
23 | ))
24 | Progress.displayName = ProgressPrimitive.Root.displayName
25 |
26 | export { Progress }
27 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
3 | import { Circle } from "lucide-react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const RadioGroup = React.forwardRef<
8 | React.ElementRef,
9 | React.ComponentPropsWithoutRef
10 | >(({ className, ...props }, ref) => {
11 | return (
12 |
17 | )
18 | })
19 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
20 |
21 | const RadioGroupItem = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => {
25 | return (
26 |
34 |
35 |
36 |
37 |
38 | )
39 | })
40 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
41 |
42 | export { RadioGroup, RadioGroupItem }
43 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 | ))
22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23 |
24 | const ScrollBar = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, orientation = "vertical", ...props }, ref) => (
28 |
41 |
42 |
43 | ))
44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45 |
46 | export { ScrollArea, ScrollBar }
47 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/src/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | import { useTheme } from "next-themes"
2 | import { Toaster as Sonner } from "sonner"
3 |
4 | type ToasterProps = React.ComponentProps
5 |
6 | const Toaster = ({ ...props }: ToasterProps) => {
7 | const { theme = "system" } = useTheme()
8 |
9 | return (
10 |
26 | )
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SwitchPrimitives from "@radix-ui/react-switch"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Switch = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
23 |
24 | ))
25 | Switch.displayName = SwitchPrimitives.Root.displayName
26 |
27 | export { Switch }
28 |
--------------------------------------------------------------------------------
/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Toast,
3 | ToastClose,
4 | ToastDescription,
5 | ToastProvider,
6 | ToastTitle,
7 | ToastViewport,
8 | } from "@/components/ui/toast"
9 | import { useToast } from "@/components/ui/use-toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
19 |
20 | {title && {title}}
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
3 | import { type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 | import { toggleVariants } from "@/components/ui/toggle"
7 |
8 | const ToggleGroupContext = React.createContext<
9 | VariantProps
10 | >({
11 | size: "default",
12 | variant: "default",
13 | })
14 |
15 | const ToggleGroup = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef &
18 | VariantProps
19 | >(({ className, variant, size, children, ...props }, ref) => (
20 |
25 |
26 | {children}
27 |
28 |
29 | ))
30 |
31 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
32 |
33 | const ToggleGroupItem = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef &
36 | VariantProps
37 | >(({ className, children, variant, size, ...props }, ref) => {
38 | const context = React.useContext(ToggleGroupContext)
39 |
40 | return (
41 |
52 | {children}
53 |
54 | )
55 | })
56 |
57 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
58 |
59 | export { ToggleGroup, ToggleGroupItem }
60 |
--------------------------------------------------------------------------------
/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TogglePrimitive from "@radix-ui/react-toggle"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const toggleVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:ring data-[state=on]:ring-indigo-500 data-[state=on]:text-accent-foreground",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border border-input hover:ring hover:ring-indigo-500 bg-transparent",
15 | },
16 | size: {
17 | default: "h-10 px-3",
18 | sm: "h-9 px-2.5",
19 | lg: "h-11 px-5",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | }
27 | )
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ))
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName
42 |
43 | export { Toggle, toggleVariants }
44 |
--------------------------------------------------------------------------------
/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const TooltipProvider = TooltipPrimitive.Provider
7 |
8 | const Tooltip = TooltipPrimitive.Root
9 |
10 | const TooltipTrigger = TooltipPrimitive.Trigger
11 |
12 | const TooltipContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, sideOffset = 4, ...props }, ref) => (
16 |
25 | ))
26 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
27 |
28 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
29 |
--------------------------------------------------------------------------------
/src/context.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import {UseTranslationResponse} from "react-i18next";
3 |
4 | // table context
5 | export const TableContext = createContext<{
6 | setInfo?: (values: any) => void; // set detail info
7 | onRefresh?: () => void;
8 | trans?: UseTranslationResponse<'translation', undefined>;
9 | }>({})
10 |
11 | export const MemberContext = createContext<{
12 | onResetPassword?: () => void;
13 | }>({});
14 |
15 |
--------------------------------------------------------------------------------
/src/hooks/use-check-active-nav.tsx:
--------------------------------------------------------------------------------
1 | import { useLocation } from 'react-router-dom'
2 |
3 | export default function useCheckActiveNav() {
4 | const { pathname } = useLocation()
5 |
6 | const checkActiveNav = (nav: string) => {
7 | const pathArray = pathname.split('/').filter((item) => item !== '')
8 |
9 | if (nav === '/' && pathArray.length < 1) return true
10 | return pathArray.includes(nav.replace(/^\//, ''))
11 | }
12 |
13 | const checkIsActive = (nav: string) => {
14 | const pathArray = pathname.split('/').filter((item) => item !== '')
15 |
16 | if (nav === '/' && pathArray.length < 1) return true
17 | const navLink = nav.replace(/^\/|\/$/g, '')
18 | const pathLink = pathname.replace(/^\/|\/$/g, '')
19 | return navLink === pathLink
20 | }
21 |
22 | return { checkActiveNav, checkIsActive }
23 | }
24 |
--------------------------------------------------------------------------------
/src/hooks/use-data-table.tsx:
--------------------------------------------------------------------------------
1 | import {useState} from "react";
2 | import {
3 | ColumnDef,
4 | ColumnFiltersState,
5 | ExpandedState,
6 | getCoreRowModel,
7 | getExpandedRowModel,
8 | getFacetedRowModel, getFacetedUniqueValues,
9 | getFilteredRowModel,
10 | getPaginationRowModel,
11 | getSortedRowModel,
12 | SortingState, Table,
13 | useReactTable,
14 | VisibilityState
15 | } from "@tanstack/react-table";
16 | import * as React from "react";
17 |
18 |
19 | export interface UseDataTableProps {
20 | columns: ColumnDef[]
21 | data: TData[]
22 | pageCount: number
23 | rowCount: number
24 | pagination: {
25 | pageIndex: number,
26 | pageSize: number
27 | }
28 | onPaginationChange: any
29 | getSubRows?: (originalRow: TData, index: number) => undefined | TData[]
30 | }
31 |
32 | export function useDataTable(props: UseDataTableProps) {
33 | const [rowSelection, setRowSelection] = useState({})
34 | const [columnVisibility, setColumnVisibility] = useState({})
35 | const [columnFilters, setColumnFilters] = useState([])
36 | const [sorting, setSorting] = useState([])
37 | const [expanded, setExpanded] = React.useState({})
38 |
39 | const table: Table = useReactTable({
40 | data: props.data,
41 | columns: props.columns,
42 | pageCount: props.pageCount,
43 | rowCount: props.rowCount,
44 | state: {
45 | pagination: props.pagination,
46 | sorting,
47 | columnVisibility,
48 | rowSelection,
49 | columnFilters,
50 | expanded,
51 | },
52 | onExpandedChange: setExpanded,
53 | getSubRows: props.getSubRows,
54 | enableRowSelection: true,
55 | manualPagination: true,
56 | onRowSelectionChange: setRowSelection,
57 | onSortingChange: setSorting,
58 | onColumnFiltersChange: setColumnFilters,
59 | onColumnVisibilityChange: setColumnVisibility,
60 | getCoreRowModel: getCoreRowModel(),
61 | getFilteredRowModel: getFilteredRowModel(),
62 | getExpandedRowModel: getExpandedRowModel(),
63 | getPaginationRowModel: getPaginationRowModel(),
64 | getSortedRowModel: getSortedRowModel(),
65 | getFacetedRowModel: getFacetedRowModel(),
66 | getFacetedUniqueValues: getFacetedUniqueValues(),
67 | onPaginationChange: props.onPaginationChange,
68 | // debugTable: true,
69 | })
70 | return table
71 | }
72 |
--------------------------------------------------------------------------------
/src/hooks/use-is-collapsed.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import useLocalStorage from './use-local-storage'
3 |
4 | export default function useIsCollapsed() {
5 | const [isCollapsed, setIsCollapsed] = useLocalStorage({
6 | key: 'collapsed-sidebar',
7 | defaultValue: false,
8 | })
9 |
10 | useEffect(() => {
11 | const handleResize = () => {
12 | // Update isCollapsed based on window.innerWidth
13 | setIsCollapsed(window.innerWidth < 768 ? false : isCollapsed)
14 | }
15 |
16 | // Initial setup
17 | handleResize()
18 |
19 | // Add event listener for window resize
20 | window.addEventListener('resize', handleResize)
21 |
22 | // Cleanup event listener on component unmount
23 | return () => {
24 | window.removeEventListener('resize', handleResize)
25 | }
26 | }, [isCollapsed, setIsCollapsed])
27 |
28 | return [isCollapsed, setIsCollapsed] as const
29 | }
30 |
--------------------------------------------------------------------------------
/src/hooks/use-local-storage.tsx:
--------------------------------------------------------------------------------
1 | import {useEffect, useState} from 'react'
2 |
3 | interface LocalStorageProps {
4 | key: string
5 | defaultValue: T
6 | }
7 |
8 | export default function useLocalStorage({
9 | key,
10 | defaultValue,
11 | }: LocalStorageProps) {
12 | const [value, setValue] = useState(() => {
13 | const storedValue = localStorage.getItem(key)
14 | return storedValue !== null ? (JSON.parse(storedValue) as T) : defaultValue
15 | })
16 |
17 | useEffect(() => {
18 | localStorage.setItem(key, JSON.stringify(value))
19 | }, [value, key])
20 |
21 | return [value, setValue] as const
22 | }
23 |
24 | export function useStorage({
25 | key,
26 | defaultValue,
27 | }: LocalStorageProps) {
28 | const [value, setValue] = useState(() => {
29 | const storedValue = localStorage.getItem(key)
30 | return storedValue !== null ? storedValue as T : defaultValue
31 | })
32 |
33 | useEffect(() => {
34 | // @ts-ignore
35 | return localStorage.setItem(key, value)
36 | }, [value, key])
37 |
38 | return [value, setValue] as const
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/src/hooks/use-pagination.tsx:
--------------------------------------------------------------------------------
1 | import {useState} from "react";
2 |
3 | export function usePagination(initialSize = 10) {
4 | const [pagination, setPagination] = useState({
5 | pageSize: initialSize,
6 | pageIndex: 0,
7 | });
8 | const {pageSize, pageIndex} = pagination;
9 |
10 | return {
11 | // table state
12 | onPaginationChange: setPagination,
13 | pagination,
14 | // API
15 | limit: pageSize,
16 | page: pageIndex + 1
17 | };
18 | }
--------------------------------------------------------------------------------
/src/layout.tsx:
--------------------------------------------------------------------------------
1 | import {Outlet} from 'react-router-dom'
2 | import Sidebar from '@/components/layout/sidebar'
3 | import useIsCollapsed from '@/hooks/use-is-collapsed'
4 | import {LayoutBody, LayoutHeader} from '@/components/layout'
5 | import {SearchInput} from '@/components/custom/search-input.tsx'
6 | import {ThemeToggle} from '@/components/custom/theme-switch'
7 | import {UserNav} from '@/components/layout/user-nav'
8 | import Notice from "@/components/layout/notice";
9 | import Language from "@/components/layout/language";
10 | import {useTranslation} from "react-i18next";
11 | import useUserInfoStore from "@/stores/user-info";
12 |
13 | export default function Layout() {
14 | const token = useUserInfoStore.getState().userInfo?.token || '';
15 | if (token === '') {
16 | window.location.href = `/login?redirect=${window.location.pathname}`
17 | return
18 | }
19 | const [isCollapsed, setIsCollapsed] = useIsCollapsed()
20 | const {t} = useTranslation()
21 |
22 | return (
23 |
24 |
25 |
29 |
30 |
32 |
33 | {
34 | }}/>
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | )
52 | }
53 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/src/locale/en-US.ts:
--------------------------------------------------------------------------------
1 | import menu from "@/locale/en-US/menu";
2 | import settings from "@/locale/en-US/settings";
3 | import permission from "@/locale/en-US/permission";
4 | import product from "@/locale/en-US/product.ts";
5 |
6 | export default {
7 | ...menu,
8 | ...settings,
9 | ...permission,
10 | ...product,
11 | }
--------------------------------------------------------------------------------
/src/locale/en-US/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.welcome': 'Welcome',
3 | 'menu.dashboard': 'Dashboard',
4 | /* products */
5 | 'menu.product': 'Products',
6 | 'menu.product.list': 'Product List',
7 | 'menu.cate.list': 'Cate List',
8 | 'menu.brand.list': 'Brand List',
9 | 'menu.attrs.list': 'Attrs List',
10 | /* orders */
11 | 'menu.order': 'Orders',
12 | 'menu.order.list': 'List',
13 | /* invoice */
14 | 'menu.invoice': 'Invoices',
15 | 'menu.invoice.list': 'List',
16 | /* users */
17 | 'menu.user': 'Users',
18 | 'menu.user.list': 'List',
19 | /* system */
20 | 'menu.system': 'System',
21 | 'menu.system.permission': 'Permissions',
22 | 'menu.system.permission.resource': 'Resources',
23 | 'menu.system.permission.role': 'Roles',
24 | 'menu.system.permission.member': 'Members',
25 | 'menu.system.settings': 'Settings',
26 | 'menu.system.settings.attach': 'Attachments',
27 | 'menu.system.settings.banner': 'Banners',
28 | 'menu.system.plugin': 'Plugins',
29 | 'menu.system.plugin.list': 'List',
30 | /* exception */
31 | 'menu.exception': 'Error Pages',
32 | 'menu.exception.401': 'Not Authentication',
33 | 'menu.exception.404': 'Not Found',
34 | 'menu.exception.500': 'Internal Server Error',
35 | 'menu.exception.503': 'Mantenance Error',
36 | }
--------------------------------------------------------------------------------
/src/locale/en-US/permission.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'permission.member.title': 'Member Management',
3 | 'permission.member.add': 'Add User',
4 | 'permission.member.edit': 'Edit User',
5 | 'permission.member.delete': 'Delete User',
6 | 'permission.member.reset': 'Reset Password',
7 | 'permission.member.role': 'Assign Role',
8 | 'permission.member.role.confirm': 'Confirm Assign',
9 | 'permission.member.reset.confirm': 'Confirm Reset',
10 | 'permission.member.ban': 'Forbidden',
11 | 'permission.member.open': 'Open',
12 | 'permission.member.ban.confirm': 'Are you sure you want to ban user?',
13 | 'permission.member.open.confirm': 'Are you sure you want to open user?',
14 | 'permission.member.delete.desc': 'Are you sure you want to delete the selected user?',
15 | 'permission.member.delete.loading': 'Deleting',
16 | 'permission.member.delete.confirm': 'Confirm Delete',
17 | }
--------------------------------------------------------------------------------
/src/locale/en-US/product.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'product.attr.dialog.title': 'Property Configuration',
3 | 'product.attr.dialog.save': 'Save',
4 | 'product.attr.dialog.ok': 'Confirm',
5 | 'product.attr.dialog.cancel': 'Cancel',
6 | }
--------------------------------------------------------------------------------
/src/locale/en-US/settings.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'settings.title': 'Settings',
3 | 'settings.select.placeholder': 'Please select...',
4 | 'settings.search.placeholder': 'Please enter a search keyword',
5 | 'settings.search.submit': 'Submit',
6 | 'settings.search.reset': 'Reset',
7 | 'settings.search.cancel': 'Cancel',
8 | 'settings.search.ok': 'Confirm',
9 | 'settings.search.save': 'Save',
10 | 'settings.dialog.title': 'Notice',
11 | 'settings.dialog.delete.desc': 'Are you sure you want to delete?',
12 | 'settings.auth.login': 'Login',
13 | 'settings.auth.login.tips': 'Enter your email and password below',
14 | 'settings.auth.login.tips1': 'to log into your account',
15 | 'settings.auth.login.account': 'Account',
16 | 'settings.auth.login.password': 'Password',
17 | 'settings.auth.login.forget': 'Forgot password?',
18 | // table settings
19 | 'settings.table.add': 'Add',
20 | 'settings.table.refresh': 'Refresh',
21 | 'settings.table.view': 'View Columns',
22 | 'settings.table.multi.import': 'Multi Import',
23 | 'settings.table.multi.export': 'Export',
24 | 'settings.table.multi.delete': 'Multi Delete',
25 | 'settings.table.delete.desc': 'Do you want to delete these records?',
26 | 'settings.table.ban.desc': 'Are you sure you want to ban this record?',
27 | 'settings.table.open.desc': 'Are you sure you want to open this record?',
28 | 'settings.table.delete.loading': 'Deleting',
29 | 'settings.table.delete.confirm': 'Confirm Delete',
30 | 'settings.table.submit.title': 'Submit',
31 | 'settings.table.view.option.title': 'View Options',
32 | 'settings.table.action.edit.title': 'Edit',
33 | 'settings.table.action.pass.title': 'Reset Password',
34 | 'settings.table.action.role.title': 'Assign Role',
35 | 'settings.table.action.ban.title': 'Forbidden',
36 | 'settings.table.action.open.title': 'Open',
37 | 'settings.table.action.delete.title': 'Delete',
38 | 'settings.table.action.processing.title': 'Handling...',
39 | }
--------------------------------------------------------------------------------
/src/locale/index.ts:
--------------------------------------------------------------------------------
1 | import i18next from "i18next";
2 | import {initReactI18next} from "react-i18next";
3 | import LanguageDetector from 'i18next-browser-languagedetector';
4 | // @ts-ignore
5 | import Cache from 'i18next-localstorage-cache';
6 |
7 | import enUS from "@/locale/en-US";
8 | import zhCN from "@/locale/zh-CN";
9 |
10 | const resources = {
11 | 'zh-CN': {
12 | translation: zhCN
13 | },
14 | 'en-US': {
15 | translation: enUS
16 | },
17 | };
18 |
19 | const defaultLng = localStorage.getItem('mqshop-lng') || 'zh-CN'
20 |
21 | i18next
22 | .use(Cache)
23 | .use(new LanguageDetector(null, {lookupLocalStorage: "mqshop-lng"}))
24 | .use(initReactI18next)
25 | .init({
26 | // debug: true,
27 | lng: defaultLng,
28 | resources: resources,
29 | fallbackLng: 'en-US',
30 | interpolation: {
31 | escapeValue: false
32 | },
33 | })
34 | .then((t) => {
35 | t('menu.welcome');
36 | });
37 |
38 | export default i18next;
39 |
--------------------------------------------------------------------------------
/src/locale/zh-CN.ts:
--------------------------------------------------------------------------------
1 | import menu from "@/locale/zh-CN/menu";
2 | import settings from "@/locale/zh-CN/settings";
3 | import permission from "@/locale/zh-CN/permission";
4 | import product from "@/locale/zh-CN/product.ts";
5 |
6 | export default {
7 | ...menu,
8 | ...settings,
9 | ...permission,
10 | ...product,
11 | }
--------------------------------------------------------------------------------
/src/locale/zh-CN/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.welcome': '欢迎',
3 | 'menu.dashboard': '仪表盘',
4 | /* products */
5 | 'menu.product': '商品',
6 | 'menu.product.list': '商品管理',
7 | 'menu.cate.list': '分类管理',
8 | 'menu.brand.list': '品牌管理',
9 | 'menu.attrs.list': '商品属性',
10 | /* orders */
11 | 'menu.order': '订单',
12 | 'menu.order.list': '订单管理',
13 | /* invoice */
14 | 'menu.invoice': '发票',
15 | 'menu.invoice.list': '发票管理',
16 | /* users */
17 | 'menu.user': '客户',
18 | 'menu.user.list': '客户管理',
19 | /* system */
20 | 'menu.system': '系统',
21 | 'menu.system.permission': '权限',
22 | 'menu.system.permission.resource': '资源管理',
23 | 'menu.system.permission.role': '角色管理',
24 | 'menu.system.permission.member': '人员管理',
25 | 'menu.system.settings': '设置',
26 | 'menu.system.settings.attach': '附件管理',
27 | 'menu.system.settings.banner': '轮播图管理',
28 | 'menu.system.plugin': '插件',
29 | 'menu.system.plugin.list': '插件管理',
30 | /* exception */
31 | 'menu.exception': '错误页面',
32 | 'menu.exception.401': '无页面访问权限',
33 | 'menu.exception.404': '页面不存在',
34 | 'menu.exception.500': '服务器内部错误',
35 | 'menu.exception.503': '服务不可用',
36 | }
--------------------------------------------------------------------------------
/src/locale/zh-CN/permission.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'permission.member.title': '用户管理',
3 | 'permission.member.add': '添加用户',
4 | 'permission.member.edit': '编辑用户',
5 | 'permission.member.delete': '删除用户',
6 | 'permission.member.reset': '重置密码',
7 | 'permission.member.role': '分配角色',
8 | 'permission.member.role.confirm': '确定分配',
9 | 'permission.member.reset.confirm': '确定重置',
10 | 'permission.member.ban': '禁用',
11 | 'permission.member.open': '启用',
12 | 'permission.member.ban.confirm': '您确定要禁用吗?',
13 | 'permission.member.open.confirm': '您确定要开启吗?',
14 | 'permission.member.delete.desc': '确定要删除选中的用户吗?',
15 | 'permission.member.delete.loading': '删除中',
16 | 'permission.member.delete.confirm': '确定删除',
17 | }
--------------------------------------------------------------------------------
/src/locale/zh-CN/product.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'product.attr.dialog.title': '属性配置',
3 | 'product.attr.dialog.save': '保存',
4 | 'product.attr.dialog.ok': '确定',
5 | 'product.attr.dialog.cancel': '取消',
6 | }
--------------------------------------------------------------------------------
/src/locale/zh-CN/settings.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'settings.title': '页面配置',
3 | 'settings.select.placeholder': '请选择...',
4 | 'settings.search.placeholder': '请输入搜索关键词',
5 | 'settings.search.submit': '搜索',
6 | 'settings.search.reset': '重置',
7 | 'settings.search.cancel': '取消',
8 | 'settings.search.ok': '确定',
9 | 'settings.search.save': '保存',
10 | 'settings.dialog.title': '提示',
11 | 'settings.dialog.delete.desc': '您确定要删除吗?',
12 | 'settings.auth.login': '登录',
13 | 'settings.auth.login.tips': '在下面输入您的电子邮件和密码',
14 | 'settings.auth.login.tips1': '登录您的帐户',
15 | 'settings.auth.login.account': '账号',
16 | 'settings.auth.login.password': '密码',
17 | 'settings.auth.login.forget': '忘记密码?',
18 | // table settings
19 | 'settings.table.add': '新增',
20 | 'settings.table.refresh': '刷新',
21 | 'settings.table.view': '切换列',
22 | 'settings.table.multi.import': '批量导入',
23 | 'settings.table.multi.export': '导出',
24 | 'settings.table.multi.delete': '批量删除',
25 | 'settings.table.delete.desc': '你确定要删除这些记录吗?',
26 | 'settings.table.ban.desc': '你确定要禁用这条记录吗?',
27 | 'settings.table.open.desc': '你确定要开启这条记录吗?',
28 | 'settings.table.delete.loading': '删除中',
29 | 'settings.table.delete.confirm': '确定删除',
30 | 'settings.table.submit.title': '提交',
31 | 'settings.table.view.option.title': '切换列',
32 | 'settings.table.action.edit.title': '编辑信息',
33 | 'settings.table.action.pass.title': '重置密码',
34 | 'settings.table.action.role.title': '分配角色',
35 | 'settings.table.action.ban.title': '禁用',
36 | 'settings.table.action.open.title': '开启',
37 | 'settings.table.action.delete.title': '删除',
38 | 'settings.table.action.processing.title': '处理中...',
39 |
40 | }
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as ReactDOM from 'react-dom/client'
3 | import './style/globals.css'
4 | import './style/index.css'
5 | import {RouterProvider} from "react-router-dom";
6 | import router from "@/routes";
7 | import {ThemeProvider} from "@/components/custom/theme-provider";
8 | import i18next from "@/locale";
9 | import {I18nextProvider} from "react-i18next";
10 | import {Toaster} from 'react-hot-toast';
11 | import {IconFidgetSpinner} from "@tabler/icons-react";
12 | import {TooltipProvider} from "@/components/ui/tooltip.tsx";
13 |
14 | const Loading = () => {
15 | return (
16 |
17 |
18 |
Loading...
19 |
20 | );
21 | };
22 |
23 | ReactDOM.createRoot(document.getElementById('root')!).render(
24 |
25 |
26 |
27 |
28 | }/>
29 |
30 |
31 |
32 |
33 |
34 | )
35 |
--------------------------------------------------------------------------------
/src/pages/auth/components/forgot-form.tsx:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes, useState } from 'react'
2 | import { cn } from '@/lib/utils'
3 | import { zodResolver } from '@hookform/resolvers/zod'
4 | import { useForm } from 'react-hook-form'
5 | import { z } from 'zod'
6 | import { Button } from '@/components/custom/button'
7 | import {
8 | Form,
9 | FormControl,
10 | FormField,
11 | FormItem,
12 | FormLabel,
13 | FormMessage,
14 | } from '@/components/ui/form'
15 | import { Input } from '@/components/ui/input'
16 |
17 | interface ForgotFormProps extends HTMLAttributes {}
18 |
19 | const formSchema = z.object({
20 | email: z
21 | .string()
22 | .min(1, { message: 'Please enter your email' })
23 | .email({ message: 'Invalid email address' }),
24 | })
25 |
26 | export function ForgotForm({ className, ...props }: ForgotFormProps) {
27 | const [isLoading, setIsLoading] = useState(false)
28 |
29 | const form = useForm>({
30 | resolver: zodResolver(formSchema),
31 | defaultValues: { email: '' },
32 | })
33 |
34 | function onSubmit(data: z.infer) {
35 | setIsLoading(true)
36 | console.log(data)
37 |
38 | setTimeout(() => {
39 | setIsLoading(false)
40 | }, 3000)
41 | }
42 |
43 | return (
44 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/src/pages/dashboard/components/overview.tsx:
--------------------------------------------------------------------------------
1 | import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from 'recharts'
2 |
3 | const data = [
4 | {
5 | name: 'Jan',
6 | total: Math.floor(Math.random() * 5000) + 1000,
7 | },
8 | {
9 | name: 'Feb',
10 | total: Math.floor(Math.random() * 5000) + 1000,
11 | },
12 | {
13 | name: 'Mar',
14 | total: Math.floor(Math.random() * 5000) + 1000,
15 | },
16 | {
17 | name: 'Apr',
18 | total: Math.floor(Math.random() * 5000) + 1000,
19 | },
20 | {
21 | name: 'May',
22 | total: Math.floor(Math.random() * 5000) + 1000,
23 | },
24 | {
25 | name: 'Jun',
26 | total: Math.floor(Math.random() * 5000) + 1000,
27 | },
28 | {
29 | name: 'Jul',
30 | total: Math.floor(Math.random() * 5000) + 1000,
31 | },
32 | {
33 | name: 'Aug',
34 | total: Math.floor(Math.random() * 5000) + 1000,
35 | },
36 | {
37 | name: 'Sep',
38 | total: Math.floor(Math.random() * 5000) + 1000,
39 | },
40 | {
41 | name: 'Oct',
42 | total: Math.floor(Math.random() * 5000) + 1000,
43 | },
44 | {
45 | name: 'Nov',
46 | total: Math.floor(Math.random() * 5000) + 1000,
47 | },
48 | {
49 | name: 'Dec',
50 | total: Math.floor(Math.random() * 5000) + 1000,
51 | },
52 | ]
53 |
54 | export function Overview() {
55 | return (
56 |
57 |
58 |
65 | `$${value}`}
71 | />
72 |
78 |
79 |
80 | )
81 | }
82 |
--------------------------------------------------------------------------------
/src/pages/exception/401/index.tsx:
--------------------------------------------------------------------------------
1 | import { useNavigate } from 'react-router-dom'
2 | import { Button } from '@/components/ui/button'
3 | import { cn } from '@/lib/utils'
4 |
5 | interface GeneralErrorProps extends React.HTMLAttributes {
6 | minimal?: boolean
7 | }
8 |
9 | export default function Exception401({
10 | className,
11 | minimal = false,
12 | }: GeneralErrorProps) {
13 | const navigate = useNavigate()
14 | return (
15 |
16 |
17 | {!minimal && (
18 |
401
19 | )}
20 |
Oops! Something went wrong {`:')`}
21 |
22 | We apologize for the inconvenience.
Please try again later.
23 |
24 | {!minimal && (
25 |
26 |
29 |
30 |
31 | )}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/src/pages/exception/404/index.tsx:
--------------------------------------------------------------------------------
1 | import {useNavigate, useRouteError} from 'react-router-dom'
2 | import {Button} from '@/components/ui/button'
3 | import {cn} from "@/lib/utils";
4 | import React from "react";
5 |
6 |
7 | interface Exception404Props extends React.HTMLAttributes {
8 | }
9 |
10 | export default function Exception404({className, ...props}: Exception404Props) {
11 | const navigate = useNavigate()
12 | const error = useRouteError()
13 | if (error !== null) {
14 | console.log(error)
15 | }
16 | return (
17 |
18 |
19 |
404
20 |
Oops! Page Not Found!
21 |
22 | It seems like the page you're looking for
23 | does not exist or might have been removed.
24 |
25 |
26 |
29 |
30 |
31 |
32 |
33 | )
34 | }
35 |
--------------------------------------------------------------------------------
/src/pages/exception/500/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button'
2 |
3 | export default function Exception500() {
4 | return (
5 |
6 |
7 |
500
8 |
Internal Server Error!
9 |
10 | The site is not available at the moment.
11 | We'll be back online shortly.
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/exception/503/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button'
2 |
3 | export default function Exception503() {
4 | return (
5 |
6 |
7 |
503
8 |
Website is under maintenance!
9 |
10 | The site is not available at the moment.
11 | We'll be back online shortly.
12 |
13 |
14 |
15 |
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/src/pages/permission/member/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CircleIcon,
3 | } from '@radix-ui/react-icons'
4 | import {SunIcon} from "lucide-react";
5 |
6 | export const statuses = [
7 | {
8 | value: 1,
9 | label: '正常',
10 | icon: SunIcon,
11 | },
12 | {
13 | value: 0,
14 | label: '禁用',
15 | icon: CircleIcon,
16 | },
17 | ]
--------------------------------------------------------------------------------
/src/pages/permission/member/data/schema.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod'
2 |
3 | // We're keeping a simple non-relational schema here.
4 | // IRL, you will have a schema for your data models.
5 | export const columnSchema: z.ZodObject = z.object({
6 | id: z.number(),
7 | uuid: z.string(),
8 | avatar: z.string(),
9 | account: z.string(),
10 | real_name: z.string(),
11 | phone: z.string(),
12 | status: z.number(),
13 | password: z.string(),
14 | created_at: z.number(),
15 | role_ids: z.string(),
16 | role_list: z.array(z.object({
17 | id: z.number(),
18 | name: z.string()
19 | })),
20 | is_super: z.number(),
21 | })
22 |
23 | export const formSchema = z.object({
24 | id: z.number().default(0),
25 | uuid: z.string().default(''),
26 | account: z.string().min(1, '账号不能为空').min(5, '账号不能小于5个字符'),
27 | real_name: z.string().default(''),
28 | password: z.string().min(1, '密码不能为空').min(6, '密码不能少于6位数'),
29 | phone: z.string().min(1, '手机号不能为空'),
30 | avatar: z.string().min(1, '头像不能为空'),
31 | _status: z.boolean().default(true),
32 | status: z.number().default(1),
33 | role_ids: z.string().min(1, '角色不能为空'),
34 | role_list: z.array(z.object({
35 | id: z.number(),
36 | name: z.string()
37 | })).default([]),
38 | })
39 |
40 | export type ColumnSchemaType = z.infer
41 | export type FormSchemaType = z.infer
42 |
--------------------------------------------------------------------------------
/src/pages/permission/member/delete-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {MemberDelete} from "@/apis/permission.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface DeleteConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function DeleteConfirm({...props}: DeleteConfirmProps) {
15 | // =========================== Params ==========================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 | // =========================== Params ==========================================
18 |
19 | // =========================== API request =====================================
20 | const deleteRes = useRequest(MemberDelete, {
21 | manual: true,
22 | })
23 | // =========================== API request =====================================
24 |
25 | // =========================== Method ==========================================
26 | const handleDelete = () => {
27 | const runAsync = deleteRes.runAsync({
28 | ids: props.row.id.toString(),
29 | noCache: true,
30 | });
31 | toast.promise(
32 | runAsync,
33 | {
34 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
35 | success: (data) => data.message,
36 | error: (err) => err.response?.data.message || err.message || 'Server Error'
37 | }
38 | ).then(() => {
39 | props.onOpen(false)
40 | onRefresh?.()
41 | })
42 | }
43 |
44 | return (
45 | props.onOpen(false)}
50 | onSubmit={handleDelete}/>)
51 | }
52 |
53 | DeleteConfirm.displayName = 'DeleteConfirm'
54 |
55 | export default DeleteConfirm
--------------------------------------------------------------------------------
/src/pages/permission/resource/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CircleIcon,
3 | } from '@radix-ui/react-icons'
4 | import {SunIcon} from "lucide-react";
5 |
6 | export const statuses = [
7 | {
8 | value: 1,
9 | label: '正常',
10 | icon: SunIcon,
11 | },
12 | {
13 | value: 0,
14 | label: '禁用',
15 | icon: CircleIcon,
16 | },
17 | ]
--------------------------------------------------------------------------------
/src/pages/permission/resource/data/schema.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod'
2 |
3 | export const columnSchema: z.ZodObject = z.object({
4 | id: z.number(),
5 | parent_id: z.number(),
6 | name: z.string(),
7 | desc: z.string(),
8 | alias: z.string(),
9 | b_url: z.string(),
10 | f_url: z.string(),
11 | icon: z.string(),
12 | menu_type: z.number(),
13 | sort_order: z.number(),
14 | children: z.array(z.lazy(() => columnSchema)),
15 | path: z.string(),
16 | status: z.number(),
17 | created_at: z.number(),
18 | })
19 |
20 | export const formSchema = z.object({
21 | name: z.string().min(1, {message: '名称不能为空'}).min(2, '名称不能小于2个字符').describe("How many marshmallows fit in your mouth?"),
22 | parent_id: z.number().default(0),
23 | alias: z.string().min(1, '别名不能为空').min(2, '别名不能小于2个字符'),
24 | icon: z.string().default('').optional(),
25 | desc: z.string().default('').optional(),
26 | b_url: z.string().min(1, '后端路由不能为空'),
27 | f_url: z.string().min(1, '前端路由不能为空'),
28 | menu_type: z.number().default(1),
29 | sort_order: z.number().default(50),
30 | status: z.number().default(1),
31 | })
32 | export const selectValuesSchema = z.array(z.object({
33 | label: z.string(),
34 | value: z.number()
35 | }))
36 |
37 | export type ColumnSchemaType = z.infer
38 | export type FormSchemaType = z.infer
39 | export type SelectSchemaType = z.infer
40 |
--------------------------------------------------------------------------------
/src/pages/permission/resource/delete-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ResourceDelete} from "@/apis/permission.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface DeleteConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 | export function DeleteConfirm({...props}: DeleteConfirmProps) {
14 | // =========================== Params ==========================================
15 | const {trans, onRefresh} = React.useContext(TableContext)
16 | // =========================== Params ==========================================
17 |
18 | // =========================== API request =====================================
19 | const deleteRes = useRequest(ResourceDelete, {
20 | manual: true,
21 | })
22 | // =========================== API request =====================================
23 |
24 | // =========================== Method ==========================================
25 | const handleDelete = () => {
26 | const runAsync = deleteRes.runAsync({
27 | ids: props.row.id.toString(),
28 | noCache: true,
29 | });
30 | toast.promise(
31 | runAsync,
32 | {
33 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
34 | success: (data) => data.message,
35 | error: (err) => err.response?.data.message || err.message || 'Server Error'
36 | }
37 | ).then(() => {
38 | props.onOpen(false)
39 | onRefresh?.()
40 | })
41 | }
42 |
43 | return (
44 | props.onOpen(false)}
49 | onSubmit={handleDelete}/>)
50 | }
51 |
52 | DeleteConfirm.displayName = 'DeleteConfirm'
53 |
54 | export default DeleteConfirm
--------------------------------------------------------------------------------
/src/pages/permission/role/ban-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {RoleUpdate} from "@/apis/permission.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface BanConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function BanConfirm({...props}: BanConfirmProps) {
15 | // =========================== Params ======================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 | const description = props.row.status === 1 ? trans?.t('settings.table.ban.desc') : trans?.t('settings.table.open.desc')
18 | const submitTitle = props.row.status === 1 ? trans?.t('permission.member.ban') : trans?.t('permission.member.open')
19 | // =========================== Params ======================================
20 |
21 | // =========================== API request ======================================
22 | const banRes = useRequest(RoleUpdate, {
23 | manual: true,
24 | })
25 | // =========================== API request ======================================
26 |
27 | // =========================== Method ===========================================
28 | const handleBan = () => {
29 | const runAsync = banRes.runAsync({
30 | id: props.row.id,
31 | status: props.row.status == 1 ? 0 : 1,
32 | name: props.row.name,
33 | desc: props.row.desc,
34 | noCache: true,
35 | });
36 | toast.promise(
37 | runAsync,
38 | {
39 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
40 | success: (data) => data.message,
41 | error: (err) => err.response?.data.message || err.message || 'Server Error'
42 | }
43 | ).then(() => {
44 | props.onOpen(false)
45 | onRefresh?.()
46 | })
47 | }
48 |
49 | return (
50 | props.onOpen(false)}
55 | onSubmit={handleBan}/>)
56 | }
57 |
58 | BanConfirm.displayName = 'BanConfirm'
59 |
60 | export default BanConfirm
--------------------------------------------------------------------------------
/src/pages/permission/role/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CircleIcon,
3 | } from '@radix-ui/react-icons'
4 | import {SunIcon} from "lucide-react";
5 |
6 | export const statuses = [
7 | {
8 | value: 1,
9 | label: '正常',
10 | icon: SunIcon,
11 | },
12 | {
13 | value: 0,
14 | label: '禁用',
15 | icon: CircleIcon,
16 | },
17 | ]
--------------------------------------------------------------------------------
/src/pages/permission/role/data/schema.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod'
2 |
3 | // We're keeping a simple non-relational schema here.
4 | // IRL, you will have a schema for your data models.
5 | export const columnSchema: z.ZodObject = z.object({
6 | id: z.number(),
7 | name: z.string(),
8 | desc: z.string(),
9 | status: z.number(),
10 | created_at: z.number(),
11 | })
12 |
13 | export const formSchema = z.object({
14 | id: z.number().default(0),
15 | name: z.string().min(1, '名称不能为空').min(2, '名称不能小于2个字符'),
16 | desc: z.string().default(''),
17 | _status: z.boolean().default(true),
18 | status: z.number().default(1),
19 | })
20 |
21 | export type ColumnSchemaType = z.infer
22 | export type FormSchemaType = z.infer
23 |
--------------------------------------------------------------------------------
/src/pages/permission/role/delete-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {RoleDelete} from "@/apis/permission.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface DeleteConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function DeleteConfirm({...props}: DeleteConfirmProps) {
15 | // =========================== Params ==========================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 | // =========================== Params ==========================================
18 |
19 | // =========================== API request =====================================
20 | const deleteRes = useRequest(RoleDelete, {
21 | manual: true,
22 | })
23 | // =========================== API request =====================================
24 |
25 | // =========================== Method ==========================================
26 | const handleDelete = () => {
27 | const runAsync = deleteRes.runAsync({
28 | ids: props.row.id.toString(),
29 | noCache: true,
30 | });
31 | toast.promise(
32 | runAsync,
33 | {
34 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
35 | success: (data) => data.message,
36 | error: (err) => err.response?.data.message || err.message || 'Server Error'
37 | }
38 | ).then(() => {
39 | props.onOpen(false)
40 | onRefresh?.()
41 | })
42 | }
43 |
44 | return (
45 | props.onOpen(false)}
50 | onSubmit={handleDelete}/>)
51 | }
52 |
53 | DeleteConfirm.displayName = 'DeleteConfirm'
54 |
55 | export default DeleteConfirm
--------------------------------------------------------------------------------
/src/pages/products/attrs/attr-delete-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductAttrCateAttrDelete} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface DeleteConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function AttrDeleteConfirm({...props}: DeleteConfirmProps) {
15 | // =========================== Params ==========================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 |
18 | // =========================== API request =====================================
19 | const deleteRes = useRequest(ProductAttrCateAttrDelete, {manual: true,})
20 |
21 | // =========================== Method ==========================================
22 | const handleDelete = () => {
23 | const runAsync = deleteRes.runAsync({
24 | ids: props.row.id,
25 | noCache: true,
26 | });
27 | toast.promise(
28 | runAsync,
29 | {
30 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
31 | success: (data) => data.message,
32 | error: (err) => err.response?.data.message || err.message || 'Server Error'
33 | }
34 | ).then(() => {
35 | props.onOpen(false)
36 | onRefresh?.()
37 | })
38 | }
39 |
40 | return (
41 | props.onOpen(false)}
46 | onSubmit={handleDelete}/>)
47 | }
48 |
49 | AttrDeleteConfirm.displayName = 'DeleteConfirm'
50 |
51 | export default AttrDeleteConfirm
--------------------------------------------------------------------------------
/src/pages/products/attrs/ban-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductAttrCateUpdate} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface BanConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function BanConfirm({...props}: BanConfirmProps) {
15 | // =========================== Params ======================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 | const description = props.row.status === 1 ? trans?.t('settings.table.ban.desc') : trans?.t('settings.table.open.desc')
18 | const submitTitle = props.row.status === 1 ? trans?.t('permission.member.ban') : trans?.t('permission.member.open')
19 | // =========================== Params ======================================
20 |
21 | // =========================== API request ======================================
22 | const banRes = useRequest(ProductAttrCateUpdate, {
23 | manual: true,
24 | })
25 | // =========================== API request ======================================
26 |
27 | // =========================== Method ===========================================
28 | const handleBan = () => {
29 | const runAsync = banRes.runAsync({
30 | id: props.row.id,
31 | cate_name: props.row.cate_name,
32 | status: props.row.status === 1 ? 0 : 1,
33 | noCache: true,
34 | });
35 | toast.promise(
36 | runAsync,
37 | {
38 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
39 | success: (data) => data.message,
40 | error: (err) => err.response?.data.message || err.message || 'Server Error'
41 | }
42 | ).then(() => {
43 | props.onOpen(false)
44 | onRefresh?.()
45 | })
46 | }
47 |
48 | return (
49 | props.onOpen(false)}
54 | onSubmit={handleBan}/>)
55 | }
56 |
57 | BanConfirm.displayName = 'BanConfirm'
58 |
59 | export default BanConfirm
--------------------------------------------------------------------------------
/src/pages/products/attrs/columns/data-form-fields.tsx:
--------------------------------------------------------------------------------
1 | import {AutoFormInputComponentProps, FieldConfig} from "@/components/custom/auto-form/types.ts";
2 | import {FormControl, FormItem} from "@/components/ui/form.tsx";
3 | import AutoFormLabel from "@/components/custom/auto-form/common/label.tsx";
4 | import AutoFormTooltip from "@/components/custom/auto-form/common/tooltip.tsx";
5 | import {Switch} from "@/components/ui/switch.tsx";
6 |
7 | interface FieldConfigProps {
8 | }
9 |
10 | export default function FieldConfigForm({}: FieldConfigProps): FieldConfig {
11 | return {
12 | cate_name: {
13 | label: '名称',
14 | description: '如:大小',
15 | inputProps: {
16 | required: true,
17 | placeholder: "请输入分类名称",
18 | },
19 | },
20 | status: {
21 | label: '状态',
22 | fieldType: ({...props}: AutoFormInputComponentProps) => {
23 | return (
24 |
25 |
26 |
30 |
31 |
36 |
37 |
38 |
39 |
40 | )
41 | },
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/src/pages/products/attrs/components/data-table-searchbar.tsx:
--------------------------------------------------------------------------------
1 | import DataTableSearchBar from "@/components/custom/data-table/data-table-searchbar.tsx";
2 | import {SearchInput} from "@/components/custom/search-input.tsx";
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectGroup,
7 | SelectItem,
8 | SelectLabel,
9 | SelectTrigger,
10 | SelectValue
11 | } from "@/components/ui/select.tsx";
12 | import {useImmer} from "use-immer";
13 | import {useContext} from "react";
14 | import {TableContext} from "@/context.tsx";
15 |
16 | export interface SearchInfo {
17 | keyword: string;
18 | status: string;
19 | }
20 |
21 | interface DataTableSearchbarProps {
22 | info: TData
23 | loading?: boolean
24 | onSearch: (values: SearchInfo) => void
25 | }
26 |
27 | export function DataTableSearchbar({...props}: DataTableSearchbarProps) {
28 | const {trans} = useContext(TableContext)
29 | const [searchInfo, setSearchInfo] = useImmer({
30 | keyword: '',
31 | status: ''
32 | })
33 | const handleSearch = () => {
34 | props.onSearch(searchInfo)
35 | }
36 | const handleReset = () => {
37 | setSearchInfo(draft => {
38 | draft.keyword = ''
39 | draft.status = ''
40 | props.onSearch(draft)
41 | })
42 | }
43 | return (
44 |
46 | setSearchInfo(draft => {
51 | draft.keyword = val
52 | })}/>
53 |
68 |
69 | )
70 | }
--------------------------------------------------------------------------------
/src/pages/products/attrs/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CircleIcon,
3 | } from '@radix-ui/react-icons'
4 | import {SunIcon} from "lucide-react";
5 |
6 | export const statuses = [
7 | {
8 | value: 1,
9 | label: '正常',
10 | icon: SunIcon,
11 | },
12 | {
13 | value: 0,
14 | label: '禁用',
15 | icon: CircleIcon,
16 | },
17 | ]
18 |
19 | export const inputTypes = [
20 | {
21 | value: 1,
22 | label: '手工录入',
23 | },
24 | {
25 | value: 2,
26 | label: '从列表中选择',
27 | }
28 | ]
--------------------------------------------------------------------------------
/src/pages/products/attrs/data/schema.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod'
2 |
3 | // We're keeping a simple non-relational schema here.
4 | // IRL, you will have a schema for your data models.
5 | export const columnSchema: z.ZodObject = z.object({
6 | id: z.number(),
7 | cate_name: z.string(),
8 | status: z.number(),
9 | created_at: z.number(),
10 | })
11 | export const formSchema = z.object({
12 | cate_name: z.string().min(1, '名称不能为空').min(2, '名称不能小于2个字符'),
13 | status: z.number().default(1),
14 | })
15 | export const attrColumnSchema: z.ZodObject = z.object({
16 | id: z.number(),
17 | cate_id: z.number(),
18 | attr_type: z.number(),
19 | attr_name: z.string(),
20 | attr_value: z.string(),
21 | input_type: z.number(),
22 | sort_order: z.number(),
23 | created_at: z.number(),
24 | })
25 | export const attrFormSchema = z.object({
26 | attr_type: z.number().default(1),
27 | cate_id: z.number().min(1, '属性分类不能为空'),
28 | attr_name: z.string().min(1, '名称不能为空').min(2, '名称不能小于2个字符'),
29 | input_type: z.number().default(1),
30 | attr_value: z.string().optional(),
31 | sort_order: z.number().default(1),
32 | })
33 | export const selectValuesSchema = z.array(z.object({
34 | label: z.string(),
35 | value: z.number()
36 | }))
37 |
38 | export type ColumnSchemaType = z.infer
39 | export type FormSchemaType = z.infer
40 | export type AttrColumnSchemaType = z.infer
41 | export type AttrFormSchemaType = z.infer
42 | export type SelectSchemaType = z.infer
43 |
--------------------------------------------------------------------------------
/src/pages/products/attrs/delete-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductAttrCateDelete} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface DeleteConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function DeleteConfirm({...props}: DeleteConfirmProps) {
15 | // =========================== Params ==========================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 |
18 | // =========================== API request =====================================
19 | const deleteRes = useRequest(ProductAttrCateDelete, {
20 | manual: true,
21 | })
22 |
23 | // =========================== Method ==========================================
24 | const handleDelete = () => {
25 | const runAsync = deleteRes.runAsync({
26 | ids: props.row.id,
27 | noCache: true,
28 | });
29 | toast.promise(
30 | runAsync,
31 | {
32 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
33 | success: (data) => data.message,
34 | error: (err) => err.response?.data.message || err.message || 'Server Error'
35 | }
36 | ).then(() => {
37 | props.onOpen(false)
38 | onRefresh?.()
39 | })
40 | }
41 |
42 | return (
43 | props.onOpen(false)}
48 | onSubmit={handleDelete}/>)
49 | }
50 |
51 | DeleteConfirm.displayName = 'DeleteConfirm'
52 |
53 | export default DeleteConfirm
--------------------------------------------------------------------------------
/src/pages/products/brand/ban-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductBrandStatus} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface BanConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function BanConfirm({...props}: BanConfirmProps) {
15 | // =========================== Params ======================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 | const description = props.row.status === 1 ? trans?.t('settings.table.ban.desc') : trans?.t('settings.table.open.desc')
18 | const submitTitle = props.row.status === 1 ? trans?.t('permission.member.ban') : trans?.t('permission.member.open')
19 |
20 | // =========================== API request ======================================
21 | const banRes = useRequest(ProductBrandStatus, {manual: true})
22 |
23 | // =========================== Method ===========================================
24 | const handleBan = () => {
25 | const runAsync = banRes.runAsync({
26 | id: props.row.id,
27 | status: props.row.status === 1 ? 0 : 1,
28 | noCache: true,
29 | });
30 | toast.promise(
31 | runAsync,
32 | {
33 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
34 | success: (data) => data.message,
35 | error: (err) => err.response?.data.message || err.message || 'Server Error'
36 | }
37 | ).then(() => {
38 | props.onOpen(false)
39 | onRefresh?.()
40 | })
41 | }
42 |
43 | return (
44 | props.onOpen(false)}
49 | onSubmit={handleBan}/>)
50 | }
51 |
52 | BanConfirm.displayName = 'BanConfirm'
53 |
54 | export default BanConfirm
--------------------------------------------------------------------------------
/src/pages/products/brand/components/data-table-searchbar.tsx:
--------------------------------------------------------------------------------
1 | import DataTableSearchBar from "@/components/custom/data-table/data-table-searchbar.tsx";
2 | import {SearchInput} from "@/components/custom/search-input.tsx";
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectGroup,
7 | SelectItem,
8 | SelectLabel,
9 | SelectTrigger,
10 | SelectValue
11 | } from "@/components/ui/select.tsx";
12 | import {useImmer} from "use-immer";
13 | import {useContext} from "react";
14 | import {TableContext} from "@/context.tsx";
15 |
16 | export interface SearchInfo {
17 | keyword: string;
18 | status: string;
19 | }
20 |
21 | interface DataTableSearchbarProps {
22 | info: TData
23 | loading?: boolean
24 | onSearch: (values: SearchInfo) => void
25 | }
26 |
27 | export function DataTableSearchbar({...props}: DataTableSearchbarProps) {
28 | const {trans} = useContext(TableContext)
29 | const [searchInfo, setSearchInfo] = useImmer({
30 | keyword: '',
31 | status: ''
32 | })
33 | const handleSearch = () => {
34 | props.onSearch(searchInfo)
35 | }
36 | const handleReset = () => {
37 | setSearchInfo(draft => {
38 | draft.keyword = ''
39 | draft.status = ''
40 | props.onSearch(draft)
41 | })
42 | }
43 | return (
44 |
46 | setSearchInfo(draft => {
51 | draft.keyword = val
52 | })}/>
53 |
68 |
69 | )
70 | }
--------------------------------------------------------------------------------
/src/pages/products/brand/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CircleIcon,
3 | } from '@radix-ui/react-icons'
4 | import {SunIcon} from "lucide-react";
5 |
6 | export const statuses = [
7 | {
8 | value: 1,
9 | label: '正常',
10 | icon: SunIcon,
11 | },
12 | {
13 | value: 0,
14 | label: '禁用',
15 | icon: CircleIcon,
16 | },
17 | ]
--------------------------------------------------------------------------------
/src/pages/products/brand/data/schema.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod'
2 |
3 | // We're keeping a simple non-relational schema here.
4 | // IRL, you will have a schema for your data models.
5 | export const columnSchema = z.object({
6 | id: z.number(),
7 | brand_name: z.string(),
8 | logo_url: z.string(),
9 | desc: z.string(),
10 | sort_order: z.number(),
11 | is_hot: z.number(),
12 | status: z.number(),
13 | created_at: z.number(),
14 | })
15 |
16 | export const formSchema = z.object({
17 | logo_url: z.string().default(''),
18 | brand_name: z.string().min(1, '名称不能为空').min(2, '名称不能小于2个字符'),
19 | desc: z.string().default('').optional(),
20 | sort_order: z.number().default(50).optional(),
21 | is_hot: z.number().default(0),
22 | status: z.number().default(1),
23 | })
24 |
25 | export type ColumnSchemaType = z.infer
26 | export type FormSchemaType = z.infer
27 |
--------------------------------------------------------------------------------
/src/pages/products/brand/delete-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductBrandDelete} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface DeleteConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function DeleteConfirm({...props}: DeleteConfirmProps) {
15 | // =========================== Params ==========================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 |
18 | // =========================== API request =====================================
19 | const deleteRes = useRequest(ProductBrandDelete, {manual: true})
20 |
21 | // =========================== Method ==========================================
22 | const handleDelete = () => {
23 | const runAsync = deleteRes.runAsync({
24 | ids: props.row.id.toString(),
25 | noCache: true,
26 | });
27 | toast.promise(
28 | runAsync,
29 | {
30 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
31 | success: (data) => data.message,
32 | error: (err) => err.response?.data.message || err.message || 'Server Error'
33 | }
34 | ).then(() => {
35 | props.onOpen(false)
36 | onRefresh?.()
37 | })
38 | }
39 |
40 | return (
41 | props.onOpen(false)}
46 | onSubmit={handleDelete}/>)
47 | }
48 |
49 | DeleteConfirm.displayName = 'DeleteConfirm'
50 |
51 | export default DeleteConfirm
--------------------------------------------------------------------------------
/src/pages/products/cate/ban-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductCateStatus} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface BanConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function BanConfirm({...props}: BanConfirmProps) {
15 | // =========================== Params ======================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 | const description = props.row.status === 1 ? trans?.t('settings.table.ban.desc') : trans?.t('settings.table.open.desc')
18 | const submitTitle = props.row.status === 1 ? trans?.t('permission.member.ban') : trans?.t('permission.member.open')
19 | // =========================== Params ======================================
20 |
21 | // =========================== API request ======================================
22 | const banRes = useRequest(ProductCateStatus, {
23 | manual: true,
24 | })
25 | // =========================== API request ======================================
26 |
27 | // =========================== Method ===========================================
28 | const handleBan = () => {
29 | const runAsync = banRes.runAsync({
30 | id: props.row.id,
31 | status: props.row.status === 1 ? 0 : 1,
32 | noCache: true,
33 | });
34 | toast.promise(
35 | runAsync,
36 | {
37 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
38 | success: (data) => data.message,
39 | error: (err) => err.response?.data.message || err.message || 'Server Error'
40 | }
41 | ).then(() => {
42 | props.onOpen(false)
43 | onRefresh?.()
44 | })
45 | }
46 |
47 | return (
48 | props.onOpen(false)}
53 | onSubmit={handleBan}/>)
54 | }
55 |
56 | BanConfirm.displayName = 'BanConfirm'
57 |
58 | export default BanConfirm
--------------------------------------------------------------------------------
/src/pages/products/cate/components/data-table-searchbar.tsx:
--------------------------------------------------------------------------------
1 | import DataTableSearchBar from "@/components/custom/data-table/data-table-searchbar.tsx";
2 | import {SearchInput} from "@/components/custom/search-input.tsx";
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectGroup,
7 | SelectItem,
8 | SelectLabel,
9 | SelectTrigger,
10 | SelectValue
11 | } from "@/components/ui/select.tsx";
12 | import {useImmer} from "use-immer";
13 | import {useContext} from "react";
14 | import {TableContext} from "@/context.tsx";
15 |
16 | export interface SearchInfo {
17 | keyword: string;
18 | status: string;
19 | }
20 |
21 | interface DataTableSearchbarProps {
22 | info: TData
23 | loading?: boolean
24 | onSearch: (values: SearchInfo) => void
25 | }
26 |
27 | export function DataTableSearchbar({...props}: DataTableSearchbarProps) {
28 | const {trans} = useContext(TableContext)
29 | const [searchInfo, setSearchInfo] = useImmer({
30 | keyword: '',
31 | status: ''
32 | })
33 | const handleSearch = () => {
34 | props.onSearch(searchInfo)
35 | }
36 | const handleReset = () => {
37 | setSearchInfo(draft => {
38 | draft.keyword = ''
39 | draft.status = ''
40 | props.onSearch(draft)
41 | })
42 | }
43 | return (
44 |
46 | setSearchInfo(draft => {
51 | draft.keyword = val
52 | })}/>
53 |
68 |
69 | )
70 | }
--------------------------------------------------------------------------------
/src/pages/products/cate/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CircleIcon,
3 | } from '@radix-ui/react-icons'
4 | import {SunIcon} from "lucide-react";
5 |
6 | export const statuses = [
7 | {
8 | value: 1,
9 | label: '正常',
10 | icon: SunIcon,
11 | },
12 | {
13 | value: 0,
14 | label: '禁用',
15 | icon: CircleIcon,
16 | },
17 | ]
--------------------------------------------------------------------------------
/src/pages/products/cate/data/schema.ts:
--------------------------------------------------------------------------------
1 | import {z} from 'zod'
2 |
3 | // We're keeping a simple non-relational schema here.
4 | // IRL, you will have a schema for your data models.
5 | export const columnSchema: z.ZodObject = z.object({
6 | id: z.number(),
7 | cate_name: z.string(),
8 | cate_desc: z.string(),
9 | cate_url: z.string(),
10 | sort_order: z.number(),
11 | parent_id: z.number(),
12 | path: z.string(),
13 | is_hot: z.number(),
14 | is_index: z.number(),
15 | status: z.number(),
16 | children: z.array(z.lazy(() => columnSchema)),
17 | created_at: z.number(),
18 | })
19 |
20 | export const formSchema = z.object({
21 | cate_url: z.string().default(''),
22 | cate_name: z.string().min(1, '名称不能为空').min(2, '名称不能小于2个字符'),
23 | cate_desc: z.string().default('').optional(),
24 | parent_id: z.number().default(0),
25 | sort_order: z.number().default(50).optional(),
26 | is_hot: z.number().default(0),
27 | is_index: z.number().default(0),
28 | status: z.number().default(1),
29 | })
30 |
31 | export const selectValuesSchema = z.array(z.object({
32 | label: z.string(),
33 | value: z.number()
34 | }))
35 |
36 | export type ColumnSchemaType = z.infer
37 | export type FormSchemaType = z.infer
38 | export type SelectSchemaType = z.infer
39 |
--------------------------------------------------------------------------------
/src/pages/products/cate/delete-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductCateDelete} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface DeleteConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function DeleteConfirm({...props}: DeleteConfirmProps) {
15 | // =========================== Params ==========================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 |
18 | // =========================== API request =====================================
19 | const deleteRes = useRequest(ProductCateDelete, {
20 | manual: true,
21 | })
22 |
23 | // =========================== Method ==========================================
24 | const handleDelete = () => {
25 | const runAsync = deleteRes.runAsync({
26 | ids: props.row.id.toString(),
27 | noCache: true,
28 | });
29 | toast.promise(
30 | runAsync,
31 | {
32 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
33 | success: (data) => data.message,
34 | error: (err) => err.response?.data.message || err.message || 'Server Error'
35 | }
36 | ).then(() => {
37 | props.onOpen(false)
38 | onRefresh?.()
39 | })
40 | }
41 |
42 | return (
43 | props.onOpen(false)}
48 | onSubmit={handleDelete}/>)
49 | }
50 |
51 | DeleteConfirm.displayName = 'DeleteConfirm'
52 |
53 | export default DeleteConfirm
--------------------------------------------------------------------------------
/src/pages/products/list/ban-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductStatus} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface BanConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function BanConfirm({...props}: BanConfirmProps) {
15 | // =========================== Params ======================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 | const description = props.row.status === 1 ? trans?.t('settings.table.ban.desc') : trans?.t('settings.table.open.desc')
18 | const submitTitle = props.row.status === 1 ? trans?.t('permission.member.ban') : trans?.t('permission.member.open')
19 |
20 | // =========================== API request ======================================
21 | const banRes = useRequest(ProductStatus, {manual: true})
22 |
23 | // =========================== Method ===========================================
24 | const handleBan = () => {
25 | const runAsync = banRes.runAsync({
26 | id: props.row.id,
27 | status: props.row.status === 1 ? 0 : 1,
28 | noCache: true,
29 | });
30 | toast.promise(
31 | runAsync,
32 | {
33 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
34 | success: (data) => data.message,
35 | error: (err) => err.response?.data.message || err.message || 'Server Error'
36 | }
37 | ).then(() => {
38 | props.onOpen(false)
39 | onRefresh?.()
40 | })
41 | }
42 |
43 | return (
44 | props.onOpen(false)}
49 | onSubmit={handleBan}/>)
50 | }
51 |
52 | BanConfirm.displayName = 'BanConfirm'
53 |
54 | export default BanConfirm
--------------------------------------------------------------------------------
/src/pages/products/list/components/data-table-searchbar.tsx:
--------------------------------------------------------------------------------
1 | import DataTableSearchBar from "@/components/custom/data-table/data-table-searchbar.tsx";
2 | import {SearchInput} from "@/components/custom/search-input.tsx";
3 | import {
4 | Select,
5 | SelectContent,
6 | SelectGroup,
7 | SelectItem,
8 | SelectLabel,
9 | SelectTrigger,
10 | SelectValue
11 | } from "@/components/ui/select.tsx";
12 | import {useImmer} from "use-immer";
13 | import {useContext} from "react";
14 | import {TableContext} from "@/context.tsx";
15 |
16 | export interface SearchInfo {
17 | keyword: string;
18 | status: string;
19 | }
20 |
21 | interface DataTableSearchbarProps {
22 | info: TData
23 | loading?: boolean
24 | onSearch: (values: SearchInfo) => void
25 | }
26 |
27 | export function DataTableSearchbar({...props}: DataTableSearchbarProps) {
28 | const {trans} = useContext(TableContext)
29 | const [searchInfo, setSearchInfo] = useImmer({
30 | keyword: '',
31 | status: ''
32 | })
33 | const handleSearch = () => {
34 | props.onSearch(searchInfo)
35 | }
36 | const handleReset = () => {
37 | setSearchInfo(draft => {
38 | draft.keyword = ''
39 | draft.status = ''
40 | props.onSearch(draft)
41 | })
42 | }
43 | return (
44 |
46 | setSearchInfo(draft => {
51 | draft.keyword = val
52 | })}/>
53 |
68 |
69 | )
70 | }
--------------------------------------------------------------------------------
/src/pages/products/list/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | CircleIcon,
3 | } from '@radix-ui/react-icons'
4 | import {SunIcon} from "lucide-react";
5 |
6 | export const statuses = [
7 | {
8 | value: 1,
9 | label: '已上架',
10 | icon: SunIcon,
11 | },{
12 | value: 2,
13 | label: '定时上架',
14 | icon: SunIcon,
15 | },
16 | {
17 | value: 0,
18 | label: '下架',
19 | icon: CircleIcon,
20 | },
21 | ]
--------------------------------------------------------------------------------
/src/pages/products/list/delete-confirm.tsx:
--------------------------------------------------------------------------------
1 | import ConfirmDialog from "@/components/custom/confirm-dialog.tsx";
2 | import {useRequest} from "ahooks";
3 | import {ProductDelete} from "@/apis/product.ts";
4 | import {toast} from "react-hot-toast";
5 | import * as React from "react";
6 | import {TableContext} from "@/context.tsx";
7 |
8 | interface DeleteConfirmProps {
9 | row: any;
10 | open: boolean;
11 | onOpen: (open: boolean) => void;
12 | }
13 |
14 | export function DeleteConfirm({...props}: DeleteConfirmProps) {
15 | // =========================== Params ==========================================
16 | const {trans, onRefresh} = React.useContext(TableContext)
17 |
18 | // =========================== API request =====================================
19 | const deleteRes = useRequest(ProductDelete, {manual: true})
20 |
21 | // =========================== Method ==========================================
22 | const handleDelete = () => {
23 | const runAsync = deleteRes.runAsync({
24 | ids: props.row.id.toString(),
25 | noCache: true,
26 | });
27 | toast.promise(
28 | runAsync,
29 | {
30 | loading: trans?.t('settings.table.action.processing.title') || 'loading...',
31 | success: (data) => data.message,
32 | error: (err) => err.response?.data.message || err.message || 'Server Error'
33 | }
34 | ).then(() => {
35 | props.onOpen(false)
36 | onRefresh?.()
37 | })
38 | }
39 |
40 | return (
41 | props.onOpen(false)}
46 | onSubmit={handleDelete}/>)
47 | }
48 |
49 | DeleteConfirm.displayName = 'DeleteConfirm'
50 |
51 | export default DeleteConfirm
--------------------------------------------------------------------------------
/src/pages/users/list/components/data-table-row-actions.tsx:
--------------------------------------------------------------------------------
1 | import { DotsHorizontalIcon } from '@radix-ui/react-icons'
2 | import { Row } from '@tanstack/react-table'
3 |
4 | import { Button } from '@/components/ui/button'
5 | import {
6 | DropdownMenu,
7 | DropdownMenuContent,
8 | DropdownMenuItem,
9 | DropdownMenuRadioGroup,
10 | DropdownMenuRadioItem,
11 | DropdownMenuSeparator,
12 | DropdownMenuShortcut,
13 | DropdownMenuSub,
14 | DropdownMenuSubContent,
15 | DropdownMenuSubTrigger,
16 | DropdownMenuTrigger,
17 | } from '@/components/ui/dropdown-menu'
18 |
19 | import { labels } from '../data/data'
20 | import { taskSchema } from '../data/schema'
21 |
22 | interface DataTableRowActionsProps {
23 | row: Row
24 | }
25 |
26 | export function DataTableRowActions({
27 | row,
28 | }: DataTableRowActionsProps) {
29 | const task = taskSchema.parse(row.original)
30 |
31 | return (
32 |
33 |
34 |
41 |
42 |
43 | Edit
44 | Make a copy
45 | Favorite
46 |
47 |
48 | Labels
49 |
50 |
51 | {labels.map((label) => (
52 |
53 | {label.label}
54 |
55 | ))}
56 |
57 |
58 |
59 |
60 |
61 | Delete
62 | ⌘⌫
63 |
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/src/pages/users/list/components/data-table-view-options.tsx:
--------------------------------------------------------------------------------
1 | import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu'
2 | import { MixerHorizontalIcon } from '@radix-ui/react-icons'
3 | import { Table } from '@tanstack/react-table'
4 |
5 | import { Button } from '@/components/ui/button'
6 | import {
7 | DropdownMenu,
8 | DropdownMenuCheckboxItem,
9 | DropdownMenuContent,
10 | DropdownMenuLabel,
11 | DropdownMenuSeparator,
12 | } from '@/components/ui/dropdown-menu'
13 |
14 | interface DataTableViewOptionsProps {
15 | table: Table
16 | }
17 |
18 | export function DataTableViewOptions({
19 | table,
20 | }: DataTableViewOptionsProps) {
21 | return (
22 |
23 |
24 |
31 |
32 |
33 | Toggle columns
34 |
35 | {table
36 | .getAllColumns()
37 | .filter(
38 | (column) =>
39 | typeof column.accessorFn !== 'undefined' && column.getCanHide()
40 | )
41 | .map((column) => {
42 | return (
43 | column.toggleVisibility(!!value)}
48 | >
49 | {column.id}
50 |
51 | )
52 | })}
53 |
54 |
55 | )
56 | }
57 |
--------------------------------------------------------------------------------
/src/pages/users/list/data/data.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | ArrowDownIcon,
3 | ArrowRightIcon,
4 | ArrowUpIcon,
5 | CheckCircledIcon,
6 | CircleIcon,
7 | CrossCircledIcon,
8 | QuestionMarkCircledIcon,
9 | StopwatchIcon,
10 | } from '@radix-ui/react-icons'
11 |
12 | export const labels = [
13 | {
14 | value: 'bug',
15 | label: 'Bug',
16 | },
17 | {
18 | value: 'feature',
19 | label: 'Feature',
20 | },
21 | {
22 | value: 'documentation',
23 | label: 'Documentation',
24 | },
25 | ]
26 |
27 | export const statuses = [
28 | {
29 | value: 'backlog',
30 | label: 'Backlog',
31 | icon: QuestionMarkCircledIcon,
32 | },
33 | {
34 | value: 'todo',
35 | label: 'Todo',
36 | icon: CircleIcon,
37 | },
38 | {
39 | value: 'in progress',
40 | label: 'In Progress',
41 | icon: StopwatchIcon,
42 | },
43 | {
44 | value: 'done',
45 | label: 'Done',
46 | icon: CheckCircledIcon,
47 | },
48 | {
49 | value: 'canceled',
50 | label: 'Canceled',
51 | icon: CrossCircledIcon,
52 | },
53 | ]
54 |
55 | export const priorities = [
56 | {
57 | label: 'Low',
58 | value: 'low',
59 | icon: ArrowDownIcon,
60 | },
61 | {
62 | label: 'Medium',
63 | value: 'medium',
64 | icon: ArrowRightIcon,
65 | },
66 | {
67 | label: 'High',
68 | value: 'high',
69 | icon: ArrowUpIcon,
70 | },
71 | ]
72 |
--------------------------------------------------------------------------------
/src/pages/users/list/data/schema.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod'
2 |
3 | // We're keeping a simple non-relational schema here.
4 | // IRL, you will have a schema for your data models.
5 | export const taskSchema = z.object({
6 | id: z.string(),
7 | url: z.string(),
8 | title: z.string(),
9 | status: z.string(),
10 | label: z.string(),
11 | priority: z.string(),
12 | })
13 |
14 | export type Task = z.infer
15 |
--------------------------------------------------------------------------------
/src/pages/users/list/index.tsx:
--------------------------------------------------------------------------------
1 | import {DataTable} from './components/data-table'
2 | import {columns} from './components/columns'
3 | import {tasks} from './data/tasks'
4 | import {BreadListItem, SingleBreadcrumb} from "@/components/custom/single-breadcrumb";
5 | import DataTableSearchbar from "@/components/custom/data-table/data-table-searchbar.tsx";
6 | import {SearchInput} from "@/components/custom/search-input.tsx";
7 | import {
8 | Select,
9 | SelectContent,
10 | SelectGroup,
11 | SelectItem,
12 | SelectLabel,
13 | SelectTrigger,
14 | SelectValue
15 | } from "@/components/ui/select";
16 | import {useTranslation} from "react-i18next";
17 |
18 | export default function Tasks() {
19 | const breadList: BreadListItem[] = [{
20 | name: "首页",
21 | link: '/'
22 | }, {
23 | name: "客户管理",
24 | link: '/users/index'
25 | }];
26 | const {t} = useTranslation();
27 | const tasksList = [...tasks, ...tasks]
28 |
29 | return (
30 | <>
31 | {/* 面包屑 */}
32 |
33 | {/* 搜索 */}
34 | {
35 | }} onClick={() => {
36 | }} onSubmit={() => {
37 | }}>
38 | {}}/>
40 | {[1, 2, 3].map((index) => (
41 |
56 | ))}
57 |
58 |
59 | >
60 | )
61 | }
62 |
--------------------------------------------------------------------------------
/src/plugin.tsx:
--------------------------------------------------------------------------------
1 | // import manifestJson from "@/plugins/manifest.json";
2 |
3 | const pluginRouters = () => {
4 | const routers: any[] = [];
5 | // for (const [key, value] of Object.entries(manifestJson)) {
6 | // routers.push((await import(`@/plugins/${key}/${value}.tsx`)).default)
7 | // }
8 | // @ts-ignore
9 | const modules = import.meta.glob('@/plugins/*/router.tsx');
10 | Object.keys(modules).forEach(async (key) => {
11 | console.log(modules[key])
12 | // @ts-ignore
13 | const modulePath = (await modules[key]()).default;
14 | routers.push(modulePath)
15 | });
16 | console.log(routers.keys())
17 | return routers
18 | }
19 |
20 | export {pluginRouters}
--------------------------------------------------------------------------------
/src/plugins/demo/index.tsx:
--------------------------------------------------------------------------------
1 | import {Layout, LayoutBody} from "@/components/layout";
2 | import {Outlet} from "react-router-dom";
3 |
4 | export default function PluginDemo() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | )
14 | }
--------------------------------------------------------------------------------
/src/plugins/demo/list/index.tsx:
--------------------------------------------------------------------------------
1 | import DataTables from "./components/data-table";
2 | import {SingleBreadcrumb, BreadListItem} from "@/components/custom/single-breadcrumb";
3 | import DataTableSearchbar from "@/components/custom/data-table/data-table-searchbar.tsx";
4 | import {
5 | Select,
6 | SelectContent,
7 | SelectGroup,
8 | SelectItem,
9 | SelectLabel,
10 | SelectTrigger,
11 | SelectValue
12 | } from "@/components/ui/select";
13 | import {SearchInput} from "@/components/custom/search-input.tsx";
14 | import {useTranslation} from "react-i18next";
15 |
16 | export default function Products() {
17 | const breadList: BreadListItem[] = [{
18 | name: "首页",
19 | link: '/'
20 | }, {
21 | name: "商品管理",
22 | link: '/products/index'
23 | }];
24 | const {t} = useTranslation();
25 |
26 | return (
27 | <>
28 | {/* 面包屑 */}
29 |
30 | {/* 搜索 */}
31 | {
32 | }} onClick={() => {
33 | }} onSubmit={() => {
34 | }}>
35 | {}}/>
37 | {[1, 2, 3].map((index) => (
38 |
53 | ))}
54 |
55 | {/* 表格 */}
56 |
57 | >
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/src/plugins/demo/router.tsx:
--------------------------------------------------------------------------------
1 | import Exception404 from "@/pages/exception/404";
2 |
3 | export default {
4 | path: 'demo',
5 | lazy: async () => ({
6 | Component: (await import('./index')).default,
7 | }),
8 | errorElement: ,
9 | children: [
10 | {
11 | index: true,
12 | lazy: async () => ({
13 | Component: (await import('./list')).default,
14 | })
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/src/plugins/demo1/index.tsx:
--------------------------------------------------------------------------------
1 | import {Button} from "@/components/ui/button.tsx";
2 |
3 | export default function PluginDemo1() {
4 | return (
5 | <>
6 |
7 | >
8 | )
9 | }
--------------------------------------------------------------------------------
/src/plugins/demo1/router.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | path: 'demo1',
3 | lazy: async () => ({
4 | Component: (await import('./index')).default,
5 | }),
6 | children: [
7 | {
8 | index: true,
9 | path: 'index',
10 | lazy: async () => ({
11 | Component: (await import('./index')).default,
12 | }),
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/src/plugins/demo3/index.tsx:
--------------------------------------------------------------------------------
1 | import {Button} from "@/components/ui/button.tsx";
2 |
3 | export default function PluginDemo() {
4 | return (
5 |
6 | )
7 | }
--------------------------------------------------------------------------------
/src/plugins/demo3/router.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | path: 'demo3',
3 | lazy: async () => ({
4 | Component: (await import('./index')).default,
5 | }),
6 | }
7 |
--------------------------------------------------------------------------------
/src/plugins/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "demo": "router"
3 | }
4 |
--------------------------------------------------------------------------------
/src/routes.tsx:
--------------------------------------------------------------------------------
1 | import {createBrowserRouter, RouteObject} from 'react-router-dom'
2 | import Exception404 from "@/pages/exception/404";
3 | import Product from "@/routes/product";
4 | import Order from "@/routes/order";
5 | import Exception from "@/routes/exception";
6 | import User from "@/routes/user";
7 | import Permission from "@/routes/permission";
8 | import Layout from "@/layout.tsx";
9 |
10 | const staticRoutes: RouteObject[] = [
11 | // 静态的路由配置
12 | {
13 | path: '/login',
14 | lazy: async () => ({
15 | Component: (await import('./pages/auth/login')).default
16 | })
17 | },
18 | // Main routes
19 | {
20 | path: '/',
21 | element: ,
22 | errorElement: ,
23 | children: [
24 | {
25 | index: true,
26 | path: '',
27 | lazy: async () => ({
28 | Component: (await import('./pages/dashboard')).default,
29 | }),
30 | },
31 | ...Exception,
32 | ],
33 | },
34 | Product,
35 | Permission,
36 | Order,
37 | User,
38 | ];
39 |
40 | const router = createBrowserRouter(staticRoutes)
41 |
42 | export default router
43 |
--------------------------------------------------------------------------------
/src/routes/exception.tsx:
--------------------------------------------------------------------------------
1 | import Exception401 from "@/pages/exception/401";
2 | import Exception404 from "@/pages/exception/404";
3 | import Exception500 from "@/pages/exception/500";
4 | import Exception503 from "@/pages/exception/503";
5 |
6 | export default [
7 | // Error routes
8 | {path: '401', Component: Exception401},
9 | {path: '404', Component: Exception404},
10 | {path: '500', Component: Exception500},
11 | {path: '503', Component: Exception503},
12 |
13 | // Fallback 404 route
14 | {path: '*', Component: Exception404},
15 | ];
16 |
--------------------------------------------------------------------------------
/src/routes/order.tsx:
--------------------------------------------------------------------------------
1 | import Layout from "@/layout.tsx";
2 | import Exception404 from "@/pages/exception/404";
3 |
4 | export default {
5 | path: '/orders',
6 | element: ,
7 | errorElement: ,
8 | children: [
9 | {
10 | index: true,
11 | path: 'index',
12 | lazy: async () => ({
13 | Component: (await import('@/pages/permission/member')).default,
14 | })
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/src/routes/permission.tsx:
--------------------------------------------------------------------------------
1 | import Exception404 from "@/pages/exception/404";
2 | import Layout from "@/layout.tsx";
3 |
4 | export default {
5 | path: '/permissions',
6 | element: ,
7 | errorElement: ,
8 | children: [
9 | {
10 | index: true,
11 | path: 'member',
12 | lazy: async () => ({
13 | Component: (await import('@/pages/permission/member')).default,
14 | }),
15 | },
16 | {
17 | path: 'role',
18 | lazy: async () => ({
19 | Component: (await import('@/pages/permission/role')).default,
20 | }),
21 | },
22 | {
23 | path: 'resource',
24 | lazy: async () => ({
25 | Component: (await import('@/pages/permission/resource')).default,
26 | }),
27 | },
28 | ]
29 | }
--------------------------------------------------------------------------------
/src/routes/product.tsx:
--------------------------------------------------------------------------------
1 | import Exception404 from "@/pages/exception/404";
2 | import Layout from "@/layout.tsx";
3 |
4 | export default {
5 | path: '/products',
6 | element: ,
7 | errorElement: ,
8 | children: [
9 | {
10 | index: true,
11 | path: 'index',
12 | lazy: async () => ({
13 | Component: (await import('@/pages/products/list')).default,
14 | }),
15 | },
16 | {
17 | path: 'create',
18 | lazy: async () => ({
19 | Component: (await import('@/pages/products/list/detail')).default,
20 | }),
21 | },
22 | {
23 | path: 'detail/:id',
24 | lazy: async () => ({
25 | Component: (await import('@/pages/products/list/detail')).default,
26 | }),
27 | },
28 | {
29 | path: 'cates',
30 | lazy: async () => ({
31 | Component: (await import('@/pages/products/cate')).default,
32 | }),
33 | },
34 | {
35 | path: 'brands',
36 | lazy: async () => ({
37 | Component: (await import('@/pages/products/brand')).default,
38 | }),
39 | },
40 | {
41 | path: 'attrs',
42 | children: [
43 | {
44 | index: true,
45 | path: '',
46 | lazy: async () => ({
47 | Component: (await import('@/pages/products/attrs')).default,
48 | }),
49 | },
50 | {
51 | path: 'attr-list/:id',
52 | lazy: async () => ({
53 | Component: (await import('@/pages/products/attrs/attr-list')).default,
54 | }),
55 | },
56 | ]
57 | },
58 | ]
59 | };
60 |
--------------------------------------------------------------------------------
/src/routes/user.tsx:
--------------------------------------------------------------------------------
1 | import Exception404 from "@/pages/exception/404";
2 | import Layout from "@/layout.tsx";
3 |
4 | export default {
5 | path: '/users',
6 | element: ,
7 | errorElement: ,
8 | children: [
9 | {
10 | index: true,
11 | path: 'index',
12 | lazy: async () => ({
13 | Component: (await import('@/pages/users/list')).default,
14 | })
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/src/stores/user-info.ts:
--------------------------------------------------------------------------------
1 | import {create} from 'zustand'
2 | import {persist} from 'zustand/middleware'
3 |
4 | interface userInfoState {
5 | userInfo: Record | null;
6 | setUserInfo: (info: userInfoState["userInfo"]) => void;
7 | }
8 |
9 | const useUserInfoStore = create()(
10 | persist(
11 | (set) => ({
12 | userInfo: null,
13 | setUserInfo: (userInfo) => set(() => ({userInfo})),
14 | }),
15 | {name: 'mqshop-userinfo'}
16 | )
17 | )
18 |
19 | export default useUserInfoStore
20 |
--------------------------------------------------------------------------------
/src/style/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 |
10 | --card: 0 0% 100%;
11 | --card-foreground: 240 10% 3.9%;
12 |
13 | --popover: 0 0% 100%;
14 | --popover-foreground: 240 10% 3.9%;
15 |
16 | --primary: 240 5.9% 10%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 240 4.8% 95.9%;
20 | --secondary-foreground: 240 5.9% 10%;
21 |
22 | --muted: 240 4.8% 95.9%;
23 | --muted-foreground: 240 3.8% 46.1%;
24 |
25 | --accent: 240 4.8% 95.9%;
26 | --accent-foreground: 240 5.9% 10%;
27 |
28 | --destructive: 0 84.2% 60.2%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 240 5.9% 90%;
32 | --input: 240 5.9% 90%;
33 | --ring: 240 10% 3.9%;
34 |
35 | --radius: 0.5rem;
36 | }
37 |
38 | .dark {
39 | --background: 240 10% 3.9%;
40 | --foreground: 0 0% 98%;
41 |
42 | --card: 240 10% 3.9%;
43 | --card-foreground: 0 0% 98%;
44 |
45 | --popover: 240 10% 3.9%;
46 | --popover-foreground: 0 0% 98%;
47 |
48 | --primary: 0 0% 98%;
49 | --primary-foreground: 240 5.9% 10%;
50 |
51 | --secondary: 240 3.7% 15.9%;
52 | --secondary-foreground: 0 0% 98%;
53 |
54 | --muted: 240 3.7% 15.9%;
55 | --muted-foreground: 240 5% 64.9%;
56 |
57 | --accent: 240 3.7% 15.9%;
58 | --accent-foreground: 0 0% 98%;
59 |
60 | --destructive: 0 62.8% 30.6%;
61 | --destructive-foreground: 0 0% 98%;
62 |
63 | --border: 240 3.7% 15.9%;
64 | --input: 240 3.7% 15.9%;
65 | --ring: 240 4.9% 83.9%;
66 | }
67 | }
68 |
69 | @layer base {
70 | * {
71 | @apply border-border;
72 | }
73 | body {
74 | @apply bg-background text-foreground;
75 | }
76 | }
--------------------------------------------------------------------------------
/src/style/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 |
9 | font-synthesis: none;
10 | text-rendering: optimizeLegibility;
11 | -webkit-font-smoothing: antialiased;
12 | -moz-osx-font-smoothing: grayscale;
13 | }
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | interface ImportMetaEnv {
3 | readonly VITE_BASE_URL: string;
4 | readonly VITE_RESOURCE_URL: string;
5 | }
6 |
7 | interface ImportMeta {
8 | readonly env: ImportMetaEnv;
9 | }
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["ESNext", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "allowJs": true,
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 | "esModuleInterop": true, // 允许使用 import 引入使用 export 导出
18 |
19 | /* Alias */
20 | "baseUrl": ".",
21 | "paths": {
22 | "@/*": [
23 | "./src/*"
24 | ]
25 | },
26 |
27 | /* Linting */
28 | "strict": true,
29 | "noUnusedLocals": true,
30 | "noUnusedParameters": true,
31 | "noFallthroughCasesInSwitch": true
32 | },
33 | "include": [
34 | "src"
35 | ],
36 | "references": [{ "path": "./tsconfig.node.json" }],
37 | "exclude": ["cypress", "cypress.config.ts"]
38 | }
39 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": [
11 | "vite.config.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "rewrites": [
3 | {"source": "/(.*)", "destination": "/"}
4 | ]
5 | }
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import {defineConfig, loadEnv} from 'vite'
2 | import react from '@vitejs/plugin-react-swc'
3 | import * as path from "path";
4 |
5 | // https://vitejs.dev/config/
6 | // @ts-ignore
7 | export default ({mode}) => {
8 | console.log('mode', loadEnv(mode, process.cwd()).VITE_BASE_URL); //127.0.0.1:9000/api
9 | // const env = loadEnv(mode, process.cwd()) as ImportMetaEnv
10 |
11 | return defineConfig({
12 | plugins: [react()],
13 | resolve: {
14 | alias: {
15 | "@": path.resolve(__dirname, "./src"),
16 | },
17 | },
18 | build: {
19 | target: "ESNext",
20 | chunkSizeWarningLimit: 2000, // 消除打包大小超过500kb警告
21 | outDir: 'dist', // 指定打包路径,默认为项目根目录下的dist目录
22 | // minify: 'terser', // Vite 2.6.x 以上需要配置 minify:"terser",terserOptions才能生效
23 | // terserOptions: {
24 | // compress: {
25 | // keep_infinity: true, // 防止 Infinity 被压缩成 1/0,这可能会导致 Chrome 上的性能问题
26 | // drop_console: true, // 生产环境去除 console
27 | // drop_debugger: true // 生产环境去除 debugger
28 | // },
29 | // format: {
30 | // comments: false // 删除注释
31 | // }
32 | // },
33 | // 静态资源打包到dist下的不同目录
34 | rollupOptions: {
35 | output: {
36 | chunkFileNames: 'static/js/[name]-[hash].js',
37 | entryFileNames: 'static/js/[name]-[hash].js',
38 | assetFileNames: 'static/[ext]/[name]-[hash].[ext]'
39 | }
40 | }
41 | },
42 | server: {
43 | host: '0.0.0.0',
44 | port: 9526,
45 | open: true,
46 | proxy: {
47 | '/api': {
48 | target: 'http://127.0.0.1:9527',
49 | changeOrigin: true,
50 | ws: true,
51 | rewrite: (path: string) => path.replace(/^\/api/, ''),
52 | },
53 | },
54 | },
55 | })
56 | }
57 |
--------------------------------------------------------------------------------