Template
1
0
mirror of https://github.com/un-pany/v3-admin-vite.git synced 2025-04-21 03:19:19 +08:00

Compare commits

..

29 Commits

Author SHA1 Message Date
pany
4c2e01e320 chore: release 5.0.0-beta.6 2025-04-18 19:31:30 +08:00
pany
ce9918b21a chore: update dependencies 2025-04-18 19:30:33 +08:00
pany
f314c04a59 chore: update dependencies 2025-03-27 16:26:01 +08:00
pany
8d3b688a5f docs: add MobVue introduction 2025-03-21 19:23:08 +08:00
pany
138def830f chore: update dependencies 2025-03-21 19:10:01 +08:00
pany
546d29c39b docs: fix typo 2025-03-08 15:46:30 +08:00
pany
cf24e82e53 chore: update dependencies 2025-03-05 20:22:19 +08:00
pany
5dcd7105c0 docs: fix typo 2025-03-01 11:12:30 +08:00
pany
7cca118cfd docs: add MobVue 2025-03-01 11:08:17 +08:00
pany
97d1e288a9 perf: 优化 useTheme 逻辑 2025-02-22 18:10:18 +08:00
pany
8d0c30b001 chore: release 5.0.0-beta.5 2025-02-21 17:20:36 +08:00
pany
64c1dbb5c4 chore: update dependencies 2025-02-21 17:19:07 +08:00
pany
93e5537332 docs: fix typo 2025-02-19 14:28:40 +08:00
pany
c2f5c8ee91 refactor: 减少路由守卫中的硬编码 2025-02-19 13:35:10 +08:00
pany
73fa762052 types: update vue-router type 2025-02-19 11:31:57 +08:00
pany
8d4588b029 fix: 删除冗余的 async 关键字 2025-02-18 16:28:41 +08:00
pany
a935696af0 docs: fix typo 2025-02-18 11:46:05 +08:00
pany
2b081e4eb4 types: 优化调用 isString 方法后对参数类型的推导 2025-02-17 18:19:01 +08:00
pany
9acc5f156e chore: 优化 UnoCSS 配置 2025-02-12 14:05:27 +08:00
pany
a4d38a4307 docs: 更新简介 2025-02-11 16:33:09 +08:00
pany
2dd0aa8575 docs: fix typo 2025-02-10 11:05:55 +08:00
pany
67abb3b2d9 chore: release 5.0.0-beta.4 2025-02-10 10:56:13 +08:00
pany
70b0889be9 fix: 更新生产和预发环境的接口地址 2025-02-10 10:55:13 +08:00
pany
050fc557a6 chore: release 5.0.0-beta.3 2025-02-08 15:46:50 +08:00
pany
879dd6b318 refactor: 重命名接口使其更加遵循 RESTful 风格 2025-02-08 15:21:39 +08:00
pany
d8d1ad2ab7 docs: fix typo 2025-02-08 14:01:33 +08:00
pany
95a8604a2f feat: 为 401 403 添加合适的 error message 2025-02-07 19:14:16 +08:00
pany
d82f3ec874 feat: 由 Apifox 提供在线 Mock 2025-02-07 17:23:07 +08:00
pany
c84b7bcca1 chore: upgrade pnpm and dependencies 2025-02-06 11:02:19 +08:00
40 changed files with 2114 additions and 1697 deletions

View File

@ -1,7 +1,7 @@
# 生产环境的环境变量(命名必须以 VITE_ 开头) # 生产环境的环境变量(命名必须以 VITE_ 开头)
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径) ## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
VITE_BASE_URL = https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212/api/v1 VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下就需要填写 /v3-admin-vite/ ## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/v3-admin-vite/ 域名下就需要填写 /v3-admin-vite/
VITE_PUBLIC_PATH = /v3-admin-vite/ VITE_PUBLIC_PATH = /v3-admin-vite/

View File

@ -1,7 +1,7 @@
# 预发布环境的环境变量(命名必须以 VITE_ 开头) # 预发布环境的环境变量(命名必须以 VITE_ 开头)
## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径) ## 后端接口地址(如果解决跨域问题采用 CORS 就需要写绝对路径)
VITE_BASE_URL = https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212/api/v1 VITE_BASE_URL = https://apifoxmock.com/m1/2930465-2145633-default/api/v1
## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 / ## 打包构建静态资源公共路径(例如部署到 https://un-pany.github.io/ 域名下就需要填写 /
VITE_PUBLIC_PATH = / VITE_PUBLIC_PATH = /

View File

@ -22,7 +22,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
with: with:
version: 9.15.0 version: 10.2.0
- name: Build - name: Build
run: pnpm install && pnpm build run: pnpm install && pnpm build

View File

@ -11,7 +11,7 @@
## Introduction ## Introduction
V3 Admin Vite is a free and open-source foundational solution for backend management systems, based on popular technologies such as Vue3, Vite, TypeScript, Element Plus, and others V3 Admin Vite is a well-crafted backend management system template, built with popular technologies such as Vue3, Vite, TypeScript, and Element Plus
## Notifications ## Notifications
@ -27,6 +27,9 @@ V3 Admin Vite is a free and open-source foundational solution for backend manage
> [!TIP] > [!TIP]
> Paid services are officially launched! If you dont want to do it yourself but want to remove TS or other modules, try the lazy package! [Click to check it out](https://github.com/un-pany/v3-admin-vite/issues/225) > Paid services are officially launched! If you dont want to do it yourself but want to remove TS or other modules, try the lazy package! [Click to check it out](https://github.com/un-pany/v3-admin-vite/issues/225)
> [!TIP]
> If you have mobile web app needs, try the new open-source template. [MobVue](https://github.com/un-pany/mobvue)
## Usage ## Usage
<details> <details>
@ -37,7 +40,7 @@ V3 Admin Vite is a free and open-source foundational solution for backend manage
- Latest version of `Visual Studio Code` - Latest version of `Visual Studio Code`
- Install the recommended plugins in the `.vscode/extensions.json` file - Install the recommended plugins in the `.vscode/extensions.json` file
- `node` 20.x or 22+ - `node` 20.x or 22+
- `pnpm` 9+ - `pnpm` 9.x or 10+
</details> </details>
@ -133,21 +136,23 @@ pnpm test
## Links ## Links
**Online Preview**[github-pages](https://un-pany.github.io/v3-admin-vite) **Online Preview**: [github-pages](https://un-pany.github.io/v3-admin-vite)
**Chinese Documentation**[link](https://juejin.cn/post/7089377403717287972) **Chinese Documentation**: [link](https://juejin.cn/post/7089377403717287972)
**Zero to Hero Tutorial**[link](https://juejin.cn/column/7207659644487139387) **Zero to Hero Tutorial**: [link](https://juejin.cn/column/7207659644487139387)
**Mobile Web App**: [mobvue](https://github.com/un-pany/mobvue)
**Electron Desktop Version**: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite) **Electron Desktop Version**: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
**Chinese Repository**[gitee](https://gitee.com/un-pany/v3-admin-vite) **Chinese Repository**: [gitee](https://gitee.com/un-pany/v3-admin-vite)
**Optional Group**[check how to join](https://github.com/un-pany/v3-admin-vite/issues/191) **Optional Group**: [check how to join](https://github.com/un-pany/v3-admin-vite/issues/191)
**Donations**[buy a coffee for the author](https://github.com/un-pany/v3-admin-vite/issues/69) **Donations**: [buy a coffee for the author](https://github.com/un-pany/v3-admin-vite/issues/69)
**Releases & Changelog**[releases](https://github.com/un-pany/v3-admin-vite/releases) **Releases & Changelog**: [releases](https://github.com/un-pany/v3-admin-vite/releases)
## Features ## Features
@ -163,7 +168,7 @@ pnpm test
**User Management**: Login, logout demonstration **User Management**: Login, logout demonstration
**Permission Management**: Page-level permissions (dynamic routing), button-level permissions (directive permissions, permission functions), route guards **Permission Management**: Page-level permissions (dynamic routing), button-level permissions (permission directives, permission functions), route guards
**Multiple Environments**: Development, staging, and production environments **Multiple Environments**: Development, staging, and production environments
@ -199,7 +204,7 @@ pnpm test
**CSS Variables**: Primarily controls layout and color in the project **CSS Variables**: Primarily controls layout and color in the project
**ESlint**: Code linting and formatting **ESLint**: Code linting and formatting
**Axios**: Sends network requests **Axios**: Sends network requests

View File

@ -11,7 +11,7 @@
## 简介 ## 简介
V3 Admin Vite 是一个免费开源的中后台管理系统基础解决方案,基于 Vue3、Vite、TypeScript、Element Plus 等主流技术 V3 Admin Vite 是一个精心制作的后台管理系统模板,基于 Vue3、Vite、TypeScript、Element Plus 等主流技术
## 通知 ## 通知
@ -27,6 +27,9 @@ V3 Admin Vite 是一个免费开源的中后台管理系统基础解决方案,
> [!TIP] > [!TIP]
> 正式推出付费服务,如果不想自己动手,但想移除 TS 或其他模块?试试懒人套餐![点击看看](https://github.com/un-pany/v3-admin-vite/issues/225) > 正式推出付费服务,如果不想自己动手,但想移除 TS 或其他模块?试试懒人套餐![点击看看](https://github.com/un-pany/v3-admin-vite/issues/225)
> [!TIP]
> 如果你有移动端 H5 需求,试试新的开源模板。[MobVue](https://github.com/un-pany/mobvue)
## 使用 ## 使用
<details> <details>
@ -37,7 +40,7 @@ V3 Admin Vite 是一个免费开源的中后台管理系统基础解决方案,
- 新版 `Visual Studio Code` - 新版 `Visual Studio Code`
- 安装 `.vscode/extensions.json` 文件中推荐的插件 - 安装 `.vscode/extensions.json` 文件中推荐的插件
- `node` 20.x 或 22+ - `node` 20.x 或 22+
- `pnpm` 9+ - `pnpm` 9.x 或 10+
</details> </details>
@ -113,7 +116,7 @@ pnpm test
`fix` 修复错误 `fix` 修复错误
`perf` 优化 `perf` 性能优化
`refactor` 重构代码 `refactor` 重构代码
@ -139,7 +142,9 @@ pnpm test
**零基础教程**[链接](https://juejin.cn/column/7207659644487139387) **零基础教程**[链接](https://juejin.cn/column/7207659644487139387)
**Electron 桌面版**: [v3-electron-vite](https://github.com/un-pany/v3-electron-vite) **移动端 H5**[mobvue](https://github.com/un-pany/mobvue)
**Electron 桌面版**[v3-electron-vite](https://github.com/un-pany/v3-electron-vite)
**国内仓库**[gitee](https://gitee.com/un-pany/v3-admin-vite) **国内仓库**[gitee](https://gitee.com/un-pany/v3-admin-vite)
@ -155,15 +160,15 @@ pnpm test
**详细的注释**:各个配置项都写有尽可能详细的注释 **详细的注释**:各个配置项都写有尽可能详细的注释
**最新的依赖**: 及时更新所有三方依赖至最新版 **最新的依赖**及时更新所有三方依赖至最新版
**有一点规范**: 代码风格统一、命名风格统一、注释风格统一 **有一点规范**代码风格统一、命名风格统一、注释风格统一
## 内置功能 ## 内置功能
**用户管理**:登录、登出演示 **用户管理**:登录、登出演示
**权限管理**:页面级权限(动态路由)、按钮级权限(指令权限、权限函数)、路由守卫 **权限管理**:页面级权限(动态路由)、按钮级权限(权限指令、权限函数)、路由守卫
**多环境**开发环境development、预发布环境staging、生产环境production **多环境**开发环境development、预发布环境staging、生产环境production
@ -173,9 +178,9 @@ pnpm test
**首页**:根据不同用户显示不同的 Dashboard 页面 **首页**:根据不同用户显示不同的 Dashboard 页面
**错误页**: 403、404 **错误页**403、404
**兼容移动端**: 布局兼容移动端页面分辨率 **兼容移动端**布局兼容移动端页面分辨率
**其他**SVG 雪碧图、动态侧边栏、动态面包屑、标签页快捷导航、内容区放大与全屏、组合式函数 **其他**SVG 雪碧图、动态侧边栏、动态面包屑、标签页快捷导航、内容区放大与全屏、组合式函数
@ -185,7 +190,7 @@ pnpm test
**Element Plus**Element UI 的 Vue3 版本 **Element Plus**Element UI 的 Vue3 版本
**Pinia**: 传说中的 Vuex5 **Pinia**传说中的 Vuex5
**Vite**:真的很快 **Vite**:真的很快
@ -199,7 +204,7 @@ pnpm test
**CSS 变量**:主要控制项目的布局和颜色 **CSS 变量**:主要控制项目的布局和颜色
**ESlint**:代码校验与格式化 **ESLint**:代码校验与格式化
**Axios**:发送网络请求(已封装好) **Axios**:发送网络请求(已封装好)

View File

@ -36,7 +36,8 @@ export default antfu(
"no-console": "off", "no-console": "off",
"no-debugger": "off", "no-debugger": "off",
"symbol-description": "off", "symbol-description": "off",
"antfu/if-newline": "off" "antfu/if-newline": "off",
"unicorn/no-instanceof-builtins": "off"
} }
} }
) )

View File

@ -1,7 +1,7 @@
{ {
"name": "v3-admin-vite", "name": "v3-admin-vite",
"type": "module", "type": "module",
"version": "5.0.0-beta.2", "version": "5.0.0-beta.6",
"description": "A crafted admin template, built with Vue3, Vite, TypeScript, Element Plus, and more", "description": "A crafted admin template, built with Vue3, Vite, TypeScript, Element Plus, and more",
"author": "pany <939630029@qq.com> (https://github.com/pany-ang)", "author": "pany <939630029@qq.com> (https://github.com/pany-ang)",
"repository": "https://github.com/un-pany/v3-admin-vite", "repository": "https://github.com/un-pany/v3-admin-vite",
@ -16,9 +16,9 @@
}, },
"dependencies": { "dependencies": {
"@element-plus/icons-vue": "2.3.1", "@element-plus/icons-vue": "2.3.1",
"axios": "1.7.9", "axios": "1.8.4",
"dayjs": "1.11.13", "dayjs": "1.11.13",
"element-plus": "2.9.3", "element-plus": "2.9.7",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"lodash-es": "4.17.21", "lodash-es": "4.17.21",
"mitt": "3.0.1", "mitt": "3.0.1",
@ -26,37 +26,37 @@
"nprogress": "0.2.0", "nprogress": "0.2.0",
"path-browserify": "1.0.1", "path-browserify": "1.0.1",
"path-to-regexp": "8.2.0", "path-to-regexp": "8.2.0",
"pinia": "2.3.1", "pinia": "3.0.2",
"screenfull": "6.0.2", "screenfull": "6.0.2",
"vue": "3.5.13", "vue": "3.5.13",
"vue-router": "4.5.0", "vue-router": "4.5.0",
"vxe-table": "4.6.25" "vxe-table": "4.6.25"
}, },
"devDependencies": { "devDependencies": {
"@antfu/eslint-config": "3.16.0", "@antfu/eslint-config": "4.12.0",
"@types/js-cookie": "3.0.6", "@types/js-cookie": "3.0.6",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "22.10.7", "@types/node": "22.14.1",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/path-browserify": "1.0.3", "@types/path-browserify": "1.0.3",
"@vitejs/plugin-vue": "5.2.1", "@vitejs/plugin-vue": "5.2.3",
"@vitejs/plugin-vue-jsx": "4.1.1", "@vitejs/plugin-vue-jsx": "4.1.2",
"@vue/test-utils": "2.4.6", "@vue/test-utils": "2.4.6",
"eslint": "9.18.0", "eslint": "9.24.0",
"eslint-plugin-format": "1.0.1", "eslint-plugin-format": "1.0.1",
"happy-dom": "16.7.2", "happy-dom": "17.4.4",
"husky": "9.1.7", "husky": "9.1.7",
"lint-staged": "15.4.1", "lint-staged": "15.5.1",
"sass": "1.78.0", "sass": "1.78.0",
"typescript": "5.7.3", "typescript": "5.8.3",
"unocss": "65.4.3", "unocss": "66.1.0-beta.12",
"unplugin-auto-import": "19.0.0", "unplugin-auto-import": "19.1.2",
"unplugin-svg-component": "0.12.1", "unplugin-svg-component": "0.12.1",
"unplugin-vue-components": "28.0.0", "unplugin-vue-components": "28.5.0",
"vite": "6.0.11", "vite": "6.3.2",
"vite-svg-loader": "5.1.0", "vite-svg-loader": "5.1.0",
"vitest": "3.0.3", "vitest": "3.1.1",
"vue-tsc": "2.2.0" "vue-tsc": "2.2.8"
}, },
"lint-staged": { "lint-staged": {
"*": "eslint --fix" "*": "eslint --fix"

3425
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,37 +0,0 @@
import type * as Table from "./type"
import { request } from "@/http/axios"
/** 增 */
export function createTableDataApi(data: Table.CreateOrUpdateTableRequestData) {
return request({
url: "table",
method: "post",
data
})
}
/** 删 */
export function deleteTableDataApi(id: string) {
return request({
url: `table/${id}`,
method: "delete"
})
}
/** 改 */
export function updateTableDataApi(data: Table.CreateOrUpdateTableRequestData) {
return request({
url: "table",
method: "put",
data
})
}
/** 查 */
export function getTableDataApi(params: Table.TableRequestData) {
return request<Table.TableResponseData>({
url: "table",
method: "get",
params
})
}

View File

@ -0,0 +1,37 @@
import type * as Tables from "./type"
import { request } from "@/http/axios"
/** 增 */
export function createTableDataApi(data: Tables.CreateOrUpdateTableRequestData) {
return request({
url: "tables",
method: "post",
data
})
}
/** 删 */
export function deleteTableDataApi(id: number) {
return request({
url: `tables/${id}`,
method: "delete"
})
}
/** 改 */
export function updateTableDataApi(data: Tables.CreateOrUpdateTableRequestData) {
return request({
url: "tables",
method: "put",
data
})
}
/** 查 */
export function getTableDataApi(params: Tables.TableRequestData) {
return request<Tables.TableResponseData>({
url: "tables",
method: "get",
params
})
}

View File

@ -1,5 +1,5 @@
export interface CreateOrUpdateTableRequestData { export interface CreateOrUpdateTableRequestData {
id?: string id?: number
username: string username: string
password?: string password?: string
} }
@ -18,7 +18,7 @@ export interface TableRequestData {
export interface TableData { export interface TableData {
createTime: string createTime: string
email: string email: string
id: string id: number
phone: string phone: string
roles: string roles: string
status: boolean status: boolean

View File

@ -1,10 +0,0 @@
import type * as Login from "./type"
import { request } from "@/http/axios"
/** 获取当前登陆用户详情 */
export function getUserInfoApi() {
return request<Login.UserInfoResponseData>({
url: "users/info",
method: "get"
})
}

View File

@ -1 +0,0 @@
export type UserInfoResponseData = ApiResponseData<{ username: string, roles: string[] }>

View File

@ -0,0 +1,10 @@
import type * as Users from "./type"
import { request } from "@/http/axios"
/** 获取当前登录用户详情 */
export function getCurrentUserApi() {
return request<Users.CurrentUserResponseData>({
url: "users/me",
method: "get"
})
}

View File

@ -0,0 +1 @@
export type CurrentUserResponseData = ApiResponseData<{ username: string, roles: string[] }>

View File

@ -38,8 +38,9 @@ body {
background-color: var(--v3-body-bg-color); background-color: var(--v3-body-bg-color);
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
font-family: Inter, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", font-family:
Arial, sans-serif; Inter, "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial,
sans-serif;
@extend %scrollbar; @extend %scrollbar;
} }

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ElScrollbar } from "element-plus" import type { ElScrollbar } from "element-plus"
import type { RouteRecordName, RouteRecordRaw } from "vue-router" import type { RouteRecordNameGeneric, RouteRecordRaw } from "vue-router"
import { usePermissionStore } from "@/pinia/stores/permission" import { usePermissionStore } from "@/pinia/stores/permission"
import { useDevice } from "@@/composables/useDevice" import { useDevice } from "@@/composables/useDevice"
import { isExternal } from "@@/utils/validate" import { isExternal } from "@@/utils/validate"
@ -20,7 +20,7 @@ const resultRef = ref<InstanceType<typeof Result> | null>(null)
const keyword = ref<string>("") const keyword = ref<string>("")
const result = shallowRef<RouteRecordRaw[]>([]) const result = shallowRef<RouteRecordRaw[]>([])
const activeRouteName = ref<RouteRecordName | undefined>(undefined) const activeRouteName = ref<RouteRecordNameGeneric | undefined>(undefined)
/** 是否按下了上键或下键(用于解决和 mouseenter 事件的冲突) */ /** 是否按下了上键或下键(用于解决和 mouseenter 事件的冲突) */
const isPressUpOrDown = ref<boolean>(false) const isPressUpOrDown = ref<boolean>(false)

View File

@ -1,5 +1,5 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { RouteRecordName, RouteRecordRaw } from "vue-router" import type { RouteRecordNameGeneric, RouteRecordRaw } from "vue-router"
interface Props { interface Props {
data: RouteRecordRaw[] data: RouteRecordRaw[]
@ -9,7 +9,7 @@ interface Props {
const props = defineProps<Props>() const props = defineProps<Props>()
/** 选中的菜单 */ /** 选中的菜单 */
const modelValue = defineModel<RouteRecordName | undefined>({ required: true }) const modelValue = defineModel<RouteRecordNameGeneric | undefined>({ required: true })
const instance = getCurrentInstance() const instance = getCurrentInstance()

View File

@ -1,24 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { ThemeName } from "@@/composables/useTheme"
import { useTheme } from "@@/composables/useTheme" import { useTheme } from "@@/composables/useTheme"
import { MagicStick } from "@element-plus/icons-vue" import { MagicStick } from "@element-plus/icons-vue"
const { themeList, activeThemeName, setTheme } = useTheme() const { themeList, activeThemeName, setTheme } = useTheme()
function handleChangeTheme({ clientX, clientY }: MouseEvent, themeName: ThemeName) {
const maxRadius = Math.hypot(
Math.max(clientX, window.innerWidth - clientX),
Math.max(clientY, window.innerHeight - clientY)
)
const style = document.documentElement.style
style.setProperty("--v3-theme-x", `${clientX}px`)
style.setProperty("--v3-theme-y", `${clientY}px`)
style.setProperty("--v3-theme-r", `${maxRadius}px`)
const handler = () => {
setTheme(themeName)
}
document.startViewTransition ? document.startViewTransition(handler) : handler()
}
</script> </script>
<template> <template>
@ -36,7 +20,7 @@ function handleChangeTheme({ clientX, clientY }: MouseEvent, themeName: ThemeNam
v-for="(theme, index) in themeList" v-for="(theme, index) in themeList"
:key="index" :key="index"
:disabled="activeThemeName === theme.name" :disabled="activeThemeName === theme.name"
@click="(e: MouseEvent) => handleChangeTheme(e, theme.name)" @click="(e: MouseEvent) => setTheme(e, theme.name)"
> >
<span>{{ theme.title }}</span> <span>{{ theme.title }}</span>
</el-dropdown-item> </el-dropdown-item>

View File

@ -1,17 +1,18 @@
import type { RouteLocationNormalized } from "vue-router" import type { Handler } from "mitt"
import mitt, { type Handler } from "mitt" import type { RouteLocationNormalizedGeneric } from "vue-router"
import mitt from "mitt"
/** 回调函数的类型 */ /** 回调函数的类型 */
type Callback = (route: RouteLocationNormalized) => void type Callback = (route: RouteLocationNormalizedGeneric) => void
const emitter = mitt() const emitter = mitt()
const key = Symbol("ROUTE_CHANGE") const key = Symbol("ROUTE_CHANGE")
let latestRoute: RouteLocationNormalized let latestRoute: RouteLocationNormalizedGeneric
/** 设置最新的路由信息,触发路由变化事件 */ /** 设置最新的路由信息,触发路由变化事件 */
export function setRouteChange(to: RouteLocationNormalized) { export function setRouteChange(to: RouteLocationNormalizedGeneric) {
// 触发事件 // 触发事件
emitter.emit(key, to) emitter.emit(key, to)
// 缓存最新的路由信息 // 缓存最新的路由信息

View File

@ -1,4 +1,5 @@
import { getActiveThemeName, setActiveThemeName } from "@@/utils/cache/local-storage" import { getActiveThemeName, setActiveThemeName } from "@@/utils/cache/local-storage"
import { setCssVar } from "@@/utils/css"
const DEFAULT_THEME_NAME = "normal" const DEFAULT_THEME_NAME = "normal"
@ -32,8 +33,18 @@ const themeList: ThemeList[] = [
const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME) const activeThemeName = ref<ThemeName>(getActiveThemeName() || DEFAULT_THEME_NAME)
/** 设置主题 */ /** 设置主题 */
function setTheme(value: ThemeName) { function setTheme({ clientX, clientY }: MouseEvent, value: ThemeName) {
activeThemeName.value = value const maxRadius = Math.hypot(
Math.max(clientX, window.innerWidth - clientX),
Math.max(clientY, window.innerHeight - clientY)
)
setCssVar("--v3-theme-x", `${clientX}px`)
setCssVar("--v3-theme-y", `${clientY}px`)
setCssVar("--v3-theme-r", `${maxRadius}px`)
const handler = () => {
activeThemeName.value = value
}
document.startViewTransition ? document.startViewTransition(handler) : handler()
} }
/** 在 html 根元素上挂载 class */ /** 在 html 根元素上挂载 class */

View File

@ -7,7 +7,7 @@ export function checkPermission(permissionRoles: string[]): boolean {
const { roles } = useUserStore() const { roles } = useUserStore()
return roles.some(role => permissionRoles.includes(role)) return roles.some(role => permissionRoles.includes(role))
} else { } else {
console.error("参数必须是一个数组且长度大于 0参考checkPermission(['admin','editor'])") console.error("参数必须是一个数组且长度大于 0参考checkPermission(['admin', 'editor'])")
return false return false
} }
} }

View File

@ -4,7 +4,7 @@ export function isArray<T>(arg: T) {
} }
/** 判断是否为字符串 */ /** 判断是否为字符串 */
export function isString<T>(str: T) { export function isString(str: unknown) {
return typeof str === "string" || str instanceof String return typeof str === "string" || str instanceof String
} }

View File

@ -52,16 +52,18 @@ function createInstance() {
(error) => { (error) => {
// status 是 HTTP 状态码 // status 是 HTTP 状态码
const status = get(error, "response.status") const status = get(error, "response.status")
const message = get(error, "response.data.message")
switch (status) { switch (status) {
case 400: case 400:
error.message = "请求错误" error.message = "请求错误"
break break
case 401: case 401:
// Token 过期时 // Token 过期时
error.message = message || "未授权"
logout() logout()
break break
case 403: case 403:
error.message = "拒绝访问" error.message = message || "拒绝访问"
break break
case 404: case 404:
error.message = "请求地址出错" error.message = "请求地址出错"

View File

@ -1,6 +1,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TagView } from "@/pinia/stores/tags-view" import type { TagView } from "@/pinia/stores/tags-view"
import type { RouteLocationNormalizedLoaded, RouteRecordRaw, RouterLink } from "vue-router" import type { RouteLocationNormalizedGeneric, RouteRecordRaw, RouterLink } from "vue-router"
import { usePermissionStore } from "@/pinia/stores/permission" import { usePermissionStore } from "@/pinia/stores/permission"
import { useTagsViewStore } from "@/pinia/stores/tags-view" import { useTagsViewStore } from "@/pinia/stores/tags-view"
import { useRouteListener } from "@@/composables/useRouteListener" import { useRouteListener } from "@@/composables/useRouteListener"
@ -77,7 +77,7 @@ function initTags() {
} }
/** 添加标签页 */ /** 添加标签页 */
function addTags(route: RouteLocationNormalizedLoaded) { function addTags(route: RouteLocationNormalizedGeneric) {
if (route.name) { if (route.name) {
tagsViewStore.addVisitedView(route) tagsViewStore.addVisitedView(route)
tagsViewStore.addCachedView(route) tagsViewStore.addCachedView(route)

View File

@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { CreateOrUpdateTableRequestData, TableData } from "@@/apis/table/type" import type { CreateOrUpdateTableRequestData, TableData } from "@@/apis/tables/type"
import type { FormInstance, FormRules } from "element-plus" import type { FormInstance, FormRules } from "element-plus"
import { createTableDataApi, deleteTableDataApi, getTableDataApi, updateTableDataApi } from "@@/apis/table" import { createTableDataApi, deleteTableDataApi, getTableDataApi, updateTableDataApi } from "@@/apis/tables"
import { usePagination } from "@@/composables/usePagination" import { usePagination } from "@@/composables/usePagination"
import { CirclePlus, Delete, Download, Refresh, RefreshRight, Search } from "@element-plus/icons-vue" import { CirclePlus, Delete, Download, Refresh, RefreshRight, Search } from "@element-plus/icons-vue"
import { cloneDeep } from "lodash-es" import { cloneDeep } from "lodash-es"
@ -84,8 +84,8 @@ function getTableData() {
getTableDataApi({ getTableDataApi({
currentPage: paginationData.currentPage, currentPage: paginationData.currentPage,
size: paginationData.pageSize, size: paginationData.pageSize,
username: searchData.username || undefined, username: searchData.username,
phone: searchData.phone || undefined phone: searchData.phone
}).then(({ data }) => { }).then(({ data }) => {
paginationData.total = data.total paginationData.total = data.total
tableData.value = data.list tableData.value = data.list
@ -110,6 +110,12 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
<template> <template>
<div class="app-container"> <div class="app-container">
<el-alert
title="数据来源"
type="success"
description="由 Apifox 提供在线 Mock数据不具备真实性仅供简单的 CRUD 操作演示。"
show-icon
/>
<el-card v-loading="loading" shadow="never" class="search-wrapper"> <el-card v-loading="loading" shadow="never" class="search-wrapper">
<el-form ref="searchFormRef" :inline="true" :model="searchData"> <el-form ref="searchFormRef" :inline="true" :model="searchData">
<el-form-item prop="username" label="用户名"> <el-form-item prop="username" label="用户名">
@ -227,6 +233,10 @@ watch([() => paginationData.currentPage, () => paginationData.pageSize], getTabl
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.el-alert {
margin-bottom: 20px;
}
.search-wrapper { .search-wrapper {
margin-bottom: 20px; margin-bottom: 20px;
:deep(.el-card__body) { :deep(.el-card__body) {

View File

@ -1,5 +1,5 @@
<template> <template>
<div uno-padding-20 h-full text-center flex select-none all:transition-400> <div pa-20px h-full text-center flex select-none all:transition-400>
<div ma> <div ma>
<div text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s> <div text-5xl fw100 animate-bounce-alt animate-count-infinite animate-duration-1s>
UnoCSS UnoCSS
@ -7,7 +7,7 @@
<div op30 text-lg fw300 m1 dark:op60> <div op30 text-lg fw300 m1 dark:op60>
该页面是一个 UnoCSS 的使用案例其他页面依旧采用 Scss 该页面是一个 UnoCSS 的使用案例其他页面依旧采用 Scss
</div> </div>
<div m2 uno-flex-x-center text-lg op30 hover="op80" dark:op60 dark:hover="op80"> <div m2 flex-x-center text-lg op30 hover="op80" dark:op60 dark:hover="op80">
<a href="https://antfu.me/posts/reimagine-atomic-css-zh" target="_blank"> <a href="https://antfu.me/posts/reimagine-atomic-css-zh" target="_blank">
推荐阅读重新构想原子化 CSS 推荐阅读重新构想原子化 CSS
</a> </a>

View File

@ -1,8 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import type { TableResponseData } from "@@/apis/table/type" import type { TableResponseData } from "@@/apis/tables/type"
import type { ElMessageBoxOptions } from "element-plus" import type { ElMessageBoxOptions } from "element-plus"
import type { VxeFormInstance, VxeFormProps, VxeGridInstance, VxeGridProps, VxeModalInstance, VxeModalProps } from "vxe-table" import type { VxeFormInstance, VxeFormProps, VxeGridInstance, VxeGridProps, VxeModalInstance, VxeModalProps } from "vxe-table"
import { deleteTableDataApi, getTableDataApi } from "@@/apis/table" import { deleteTableDataApi, getTableDataApi } from "@@/apis/tables"
import { RoleColumnSlots } from "./tsx/RoleColumnSlots" import { RoleColumnSlots } from "./tsx/RoleColumnSlots"
import { StatusColumnSlots } from "./tsx/StatusColumnSlots" import { StatusColumnSlots } from "./tsx/StatusColumnSlots"
@ -13,7 +13,7 @@ defineOptions({
// #region vxe-grid // #region vxe-grid
interface RowMeta { interface RowMeta {
id: string id: number
username: string username: string
roles: string roles: string
phone: string phone: string
@ -164,8 +164,8 @@ const xGridOpt: VxeGridProps = reactive({
} }
// //
const params = { const params = {
username: form.username || undefined, username: form.username || "",
phone: form.phone || undefined, phone: form.phone || "",
size: page.pageSize, size: page.pageSize,
currentPage: page.currentPage currentPage: page.currentPage
} }
@ -382,6 +382,12 @@ const crudStore = reactive({
<template> <template>
<div class="app-container"> <div class="app-container">
<el-alert
title="数据来源"
type="success"
description="由 Apifox 提供在线 Mock数据不具备真实性仅供简单的 CRUD 操作演示。"
show-icon
/>
<!-- 表格 --> <!-- 表格 -->
<vxe-grid ref="xGridDom" v-bind="xGridOpt"> <vxe-grid ref="xGridDom" v-bind="xGridOpt">
<!-- 左侧按钮列表 --> <!-- 左侧按钮列表 -->
@ -410,3 +416,9 @@ const crudStore = reactive({
</vxe-modal> </vxe-modal>
</div> </div>
</template> </template>
<style lang="scss" scoped>
.el-alert {
margin-bottom: 20px;
}
</style>

View File

@ -1,18 +1,18 @@
import type * as Login from "./type" import type * as Auth from "./type"
import { request } from "@/http/axios" import { request } from "@/http/axios"
/** 获取登录验证码 */ /** 获取登录验证码 */
export function getLoginCodeApi() { export function getCaptchaApi() {
return request<Login.LoginCodeResponseData>({ return request<Auth.CaptchaResponseData>({
url: "login/code", url: "auth/captcha",
method: "get" method: "get"
}) })
} }
/** 登录并返回 Token */ /** 登录并返回 Token */
export function loginApi(data: Login.LoginRequestData) { export function loginApi(data: Auth.LoginRequestData) {
return request<Login.LoginResponseData>({ return request<Auth.LoginResponseData>({
url: "users/login", url: "auth/login",
method: "post", method: "post",
data data
}) })

View File

@ -7,6 +7,6 @@ export interface LoginRequestData {
code: string code: string
} }
export type LoginCodeResponseData = ApiResponseData<string> export type CaptchaResponseData = ApiResponseData<string>
export type LoginResponseData = ApiResponseData<{ token: string }> export type LoginResponseData = ApiResponseData<{ token: string }>

View File

@ -5,7 +5,7 @@ import { useSettingsStore } from "@/pinia/stores/settings"
import { useUserStore } from "@/pinia/stores/user" import { useUserStore } from "@/pinia/stores/user"
import ThemeSwitch from "@@/components/ThemeSwitch/index.vue" import ThemeSwitch from "@@/components/ThemeSwitch/index.vue"
import { Key, Loading, Lock, Picture, User } from "@element-plus/icons-vue" import { Key, Loading, Lock, Picture, User } from "@element-plus/icons-vue"
import { getLoginCodeApi, loginApi } from "./apis" import { getCaptchaApi, loginApi } from "./apis"
import Owl from "./components/Owl.vue" import Owl from "./components/Owl.vue"
import { useFocus } from "./composables/useFocus" import { useFocus } from "./composables/useFocus"
@ -74,7 +74,7 @@ function createCode() {
// //
codeUrl.value = "" codeUrl.value = ""
// //
getLoginCodeApi().then((res) => { getCaptchaApi().then((res) => {
codeUrl.value = res.data codeUrl.value = res.data
}) })
} }

View File

@ -1,9 +1,9 @@
import type { RouteLocationNormalized } from "vue-router" import type { RouteLocationNormalizedGeneric } from "vue-router"
import { pinia } from "@/pinia" import { pinia } from "@/pinia"
import { getCachedViews, getVisitedViews, setCachedViews, setVisitedViews } from "@@/utils/cache/local-storage" import { getCachedViews, getVisitedViews, setCachedViews, setVisitedViews } from "@@/utils/cache/local-storage"
import { useSettingsStore } from "./settings" import { useSettingsStore } from "./settings"
export type TagView = Partial<RouteLocationNormalized> export type TagView = Partial<RouteLocationNormalizedGeneric>
export const useTagsViewStore = defineStore("tags-view", () => { export const useTagsViewStore = defineStore("tags-view", () => {
const { cacheTagsView } = useSettingsStore() const { cacheTagsView } = useSettingsStore()

View File

@ -1,7 +1,7 @@
import { pinia } from "@/pinia" import { pinia } from "@/pinia"
import { resetRouter } from "@/router" import { resetRouter } from "@/router"
import { routerConfig } from "@/router/config" import { routerConfig } from "@/router/config"
import { getUserInfoApi } from "@@/apis/user" import { getCurrentUserApi } from "@@/apis/users"
import { setToken as _setToken, getToken, removeToken } from "@@/utils/cache/cookies" import { setToken as _setToken, getToken, removeToken } from "@@/utils/cache/cookies"
import { useSettingsStore } from "./settings" import { useSettingsStore } from "./settings"
import { useTagsViewStore } from "./tags-view" import { useTagsViewStore } from "./tags-view"
@ -15,21 +15,21 @@ export const useUserStore = defineStore("user", () => {
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
// 设置 Token // 设置 Token
const setToken = async (value: string) => { const setToken = (value: string) => {
_setToken(value) _setToken(value)
token.value = value token.value = value
} }
// 获取用户详情 // 获取用户详情
const getInfo = async () => { const getInfo = async () => {
const { data } = await getUserInfoApi() const { data } = await getCurrentUserApi()
username.value = data.username username.value = data.username
// 验证返回的 roles 是否为一个非空数组,否则塞入一个没有任何作用的默认角色,防止路由守卫逻辑进入无限循环 // 验证返回的 roles 是否为一个非空数组,否则塞入一个没有任何作用的默认角色,防止路由守卫逻辑进入无限循环
roles.value = data.roles?.length > 0 ? data.roles : routerConfig.defaultRoles roles.value = data.roles?.length > 0 ? data.roles : routerConfig.defaultRoles
} }
// 模拟角色变化 // 模拟角色变化
const changeRoles = async (role: string) => { const changeRoles = (role: string) => {
const newToken = `token-${role}` const newToken = `token-${role}`
token.value = newToken token.value = newToken
_setToken(newToken) _setToken(newToken)

View File

@ -14,7 +14,7 @@ const permission: Directive = {
const hasPermission = roles.some(role => permissionRoles.includes(role)) const hasPermission = roles.some(role => permissionRoles.includes(role))
hasPermission || el.parentNode?.removeChild(el) hasPermission || el.parentNode?.removeChild(el)
} else { } else {
throw new Error(`参数必须是一个数组且长度大于 0参考v-permission="['admin','editor']"`) throw new Error(`参数必须是一个数组且长度大于 0参考v-permission="['admin', 'editor']"`)
} }
} }
} }

View File

@ -9,23 +9,26 @@ import { getToken } from "@@/utils/cache/cookies"
import NProgress from "nprogress" import NProgress from "nprogress"
NProgress.configure({ showSpinner: false }) NProgress.configure({ showSpinner: false })
const { setTitle } = useTitle() const { setTitle } = useTitle()
const LOGIN_PATH = "/login"
export function registerNavigationGuard(router: Router) { export function registerNavigationGuard(router: Router) {
// 全局前置守卫 // 全局前置守卫
router.beforeEach(async (to, _from) => { router.beforeEach(async (to, _from) => {
NProgress.start() NProgress.start()
const userStore = useUserStore() const userStore = useUserStore()
const permissionStore = usePermissionStore() const permissionStore = usePermissionStore()
// 如果没有登 // 如果没有登
if (!getToken()) { if (!getToken()) {
// 如果在免登录的白名单中,则直接进入 // 如果在免登录的白名单中,则直接进入
if (isWhiteList(to)) return true if (isWhiteList(to)) return true
// 其他没有访问权限的页面将被重定向到登录页面 // 其他没有访问权限的页面将被重定向到登录页面
return "/login" return LOGIN_PATH
} }
// 如果已经登录,并准备进入 Login 页面,则重定向到主页 // 如果已经登录,并准备进入 Login 页面,则重定向到主页
if (to.path === "/login") return "/" if (to.path === LOGIN_PATH) return "/"
// 如果用户已经获得其权限角色 // 如果用户已经获得其权限角色
if (userStore.roles.length !== 0) return true if (userStore.roles.length !== 0) return true
// 否则要重新获取权限角色 // 否则要重新获取权限角色
@ -43,7 +46,7 @@ export function registerNavigationGuard(router: Router) {
// 过程中发生任何错误,都直接重置 Token并重定向到登录页面 // 过程中发生任何错误,都直接重置 Token并重定向到登录页面
userStore.resetToken() userStore.resetToken()
ElMessage.error((error as Error).message || "路由守卫发生错误") ElMessage.error((error as Error).message || "路由守卫发生错误")
return "/login" return LOGIN_PATH
} }
}) })

View File

@ -1,4 +1,4 @@
import type { RouteLocationNormalized, RouteRecordNameGeneric } from "vue-router" import type { RouteLocationNormalizedGeneric, RouteRecordNameGeneric } from "vue-router"
/** 免登录白名单(匹配路由 path */ /** 免登录白名单(匹配路由 path */
const whiteListByPath: string[] = ["/login"] const whiteListByPath: string[] = ["/login"]
@ -7,7 +7,7 @@ const whiteListByPath: string[] = ["/login"]
const whiteListByName: RouteRecordNameGeneric[] = [] const whiteListByName: RouteRecordNameGeneric[] = []
/** 判断是否在白名单 */ /** 判断是否在白名单 */
export function isWhiteList(to: RouteLocationNormalized) { export function isWhiteList(to: RouteLocationNormalizedGeneric) {
// path 和 name 任意一个匹配上即可 // path 和 name 任意一个匹配上即可
return whiteListByPath.includes(to.path) || whiteListByName.includes(to.name) return whiteListByPath.includes(to.path) || whiteListByName.includes(to.name)
} }

View File

@ -87,6 +87,6 @@ declare global {
// for type re-export // for type re-export
declare global { declare global {
// @ts-ignore // @ts-ignore
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue') import('vue')
} }

View File

@ -2,11 +2,13 @@
// @ts-nocheck // @ts-nocheck
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
// biome-ignore lint: disable
export {} export {}
/* prettier-ignore */ /* prettier-ignore */
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside'] ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar'] ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBacktop: typeof import('element-plus/es')['ElBacktop'] ElBacktop: typeof import('element-plus/es')['ElBacktop']

View File

@ -1,22 +1,25 @@
import { defineConfig, presetAttributify, presetUno } from "unocss" import { defineConfig, presetAttributify, presetWind3 } from "unocss"
export default defineConfig({ export default defineConfig({
// 预设 // 预设
presets: [ presets: [
// 属性化模式 & 无值的属性模式 // 属性化模式 & 无值的属性模式
presetAttributify(), presetAttributify({
prefix: "un-",
prefixedOnly: false
}),
// 默认预设 // 默认预设
presetUno({ presetWind3({
important: "#app" important: "#app"
}) })
], ],
// 自定义规则 // 自定义规则
rules: [["uno-padding-20", { padding: "20px" }]], rules: [],
// 自定义快捷方式 // 自定义快捷方式
shortcuts: { shortcuts: {
"uno-wh-full": "w-full h-full", "wh-full": "w-full h-full",
"uno-flex-center": "flex justify-center items-center", "flex-center": "flex justify-center items-center",
"uno-flex-x-center": "flex justify-center", "flex-x-center": "flex justify-center",
"uno-flex-y-center": "flex items-center" "flex-y-center": "flex items-center"
} }
}) })

View File

@ -38,7 +38,7 @@ export default defineConfig(({ mode }) => {
// 反向代理 // 反向代理
proxy: { proxy: {
"/api/v1": { "/api/v1": {
target: "https://mock.mengxuegu.com/mock/63218b5fb4c53348ed2bc212", target: "https://apifoxmock.com/m1/2930465-2145633-default",
// 是否为 WebSocket // 是否为 WebSocket
ws: false, ws: false,
// 是否允许跨域 // 是否允许跨域