18 Commits

Author SHA1 Message Date
ahjung
02930e5208 Merge remote-tracking branch 'origin/main' 2025-08-12 17:28:38 +08:00
ahjung
ffca3ed61f 2.1.0-publish 2025-08-12 17:28:18 +08:00
Ah jung
0f92c953cb Update README.md 2025-08-12 17:27:04 +08:00
ahjung
00247ee7b9 调整构建target 2025-08-12 16:40:20 +08:00
ahjung
449761796c 2.1.0 2025-08-12 16:10:10 +08:00
Ah jung
c96789f1ff Merge pull request #313 from tu6ge/main
feat: vscode 推荐 naive-UI 智能提示插件
2025-04-21 15:37:39 +08:00
tu6ge
301ca1a0df feat: vscode 推荐 naive-UI 智能提示插件 2025-04-21 15:33:15 +08:00
Ah jung
c5c28e958d Merge pull request #310 from emeiziying/bugfix/env-type
🐞 fix(env): fix env type
2025-03-24 15:53:48 +08:00
Ah jung
2f97cbee06 Merge pull request #311 from emeiziying/bugfix/redirect
🐞 fix(tabs): fix redirect tag while refresh in header
2025-03-24 15:52:43 +08:00
BlushGo
764cb71f39 🐞 fix(tabs): fix redirect tag while refresh in header 2025-03-19 23:02:46 +08:00
BlushGo
6d0aa46f20 🐞 fix(env): fix env type 2025-03-19 22:58:44 +08:00
Ah jung
f68ec16563 Update base.ts 修正 RedirectName 重复 2025-02-11 13:22:10 +08:00
ahjung
79c3cb5d4d README update 2024-11-12 03:28:15 +08:00
ahjung
7eb081ae87 README.md-新增交流qq群2 2024-11-01 15:43:38 +08:00
Ah jung
cc2a911f2a Merge pull request #298 from Mr-BeanSir/main
修复封装state时忘记修改html导致的报错
2024-10-21 12:10:32 +08:00
J.Bean
b88c047643 统一account页面与system中state的封装 2024-10-18 18:10:35 +08:00
J.Bean
01f9ba1046 修复封装state时忘记修改html导致的报错 2024-10-18 18:05:21 +08:00
ahjung
f729a5b8ba Fix Type Error 2024-10-17 21:02:08 +08:00
24 changed files with 2421 additions and 1716 deletions

1
.gitignore vendored
View File

@@ -18,6 +18,7 @@ pnpm-debug.log*
# Editor directories and files
.idea
.vscode
!.vscode/extensions.json
*.suo
*.ntvs*
*.njsproj

5
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"recommendations": [
"tu6ge.naive-ui-intelligence",
]
}

View File

@@ -1,5 +1,12 @@
# CHANGELOG
## 2.1.0
- 优化 `登录页面` 排版
- 新增 `构建分包策略`
- 新增 `useLocalSetting` hook
- 依赖升级
## 2.0.0
- 新增 `alova` 请求库

View File

@@ -1,10 +1,7 @@
## 🚀 简介
`Naive Ui Admin` 是一款 完全免费 且可商用的中后台解决方案,基于 🌟 `Vue3.0` 🌟、🚀 `Vite` 🚀、✨ [Naive UI](https://www.naiveui.com/) ✨ 和 🎉 `TypeScript` 🎉。
它融合了最新的前端技术栈,提炼了典型的业务模型和页面,包括二次封装组件、动态菜单、权限校验等功能,助力快速搭建企业级中后台项目
它融合了最新的前端技术栈,提炼了典型的业务模型和页面,包括二次封装组件、动态菜单、权限校验等功能,助力快速搭建企业级中后台项目
## 🌈 特性
📦 二次封装的实用高扩展性组件
@@ -14,49 +11,56 @@
## 🎥 预览
- [naive-ui-admin](https://jekip.github.io)
- [naive-ui-admin](https://gratis.naiveadmin.com)
账号admin密码123456随意
## 💡 提示
# 🚀 Naive Admin - 开箱即用的企业级前后端框架
如果您需要更多功能和组件,不妨尝试全新的 `NaiveAdmin`,它可能正是您寻找的解决方案
> **✨ 多生态支持 · 多租户就绪 · 四年持续迭代**
> 前端自由切换 Vue3 UI 库 | 后端支持 Java/PHP 单体与多租户架构
> [官网直达](https://www.naiveadmin.com) | [更新日志](https://www.yuque.com/u5825/zaqu0e)
[NaiveAdmin 官网](https://www.naiveadmin.com)
---
[NaiveAdmin 变更日志](https://www.yuque.com/u5825/zaqu0e)
## 🔥 为什么选择 NaiveAdmin 商业版?
[为什么选我们?](https://www.naiveadmin.com/choose/we)
`⏱️ 节省200+人日` · `🏆 千人+开发者信任` · `🚀 四年持续迭代`
### Plus
> **"告别重复造轮子!"**
> 全系列版本提供 **30+开箱即用组件** 与 **企业级业务模块**,让您专注核心业务创新!
基于 `NaiveUi` 全新设计版本,增加了众多特性,值得一试
---
[NaiveAdmin Plus 预览](https://plus.naiveadmin.com)
## 🖥️ 纯前端版本
### Arco vue
| 版本 | 技术栈 | 设计特点 | 配套后端 | 预览 |
|----------------|--------|------------------|----------------------------|----------------------------------------|
| **Naive UI Plus** | Vu3、Ts | 全新设计语言 · 50+新增特性 | 支持Java/PHP | [立即体验](https://plus.naiveadmin.com) |
| **Naive UI** | Vu3、Ts | 经典设计语言 · 30+新增特性 | 支持Java/PHP | [立即体验](https://pro.naiveadmin.com) |
| **Arco Design** | Vu3、Ts | 智能设计体系 · 轻盈交互 | 支持Java | [立即体验](https://arco.naiveadmin.com) |
| **Element Plus** | Vu3、Ts | 设计师友好 · 直观易用 | 支持Java | [立即体验](https://element.naiveadmin.com) |
| **Antd Vue** | Vu3、Ts | 企业级设计规范 · 完备组件 | 否 | [立即体验](https://antd.naiveadmin.com) |
智能设计体系,提供轻盈体验
## 🔌 前后端版本
[NaiveAdmin Arco 预览](https://arco.naiveadmin.com)
| 版本 | 技术栈 | 设计特点 | 预览 |
|------|--------|----------|--------------------------------------------------------------|
| **Naive UI Plus** | Vu3、Ts | 全新设计语言 · 50+新增特性 | [立即体验](https://plus-full.naiveadmin.com) |
| **Arco Design** | Vu3、Ts | 智能设计体系 · 轻盈交互 |[立即体验](https://arco-full.naiveadmin.com) |
| **Element Plus** | Vu3、Ts | 设计师友好 · 直观易用 | [立即体验](https://element-full.naiveadmin.com) |
### Element Plus
## 🏢 多租户版本
面向设计师和开发者的组件库
| 版本 | 技术栈 | 设计特点 | 适用场景 | 预览 |
|--------------|---------------|-----------------------------|----------------|-------------------------------------------|
| **Vue3** | Vu3、Ts、Java | 百家落地验证架构,免去试错成本,专为 Saas 化系统设计 | 构建企业级 Saas 化系统 | [立即体验](https://tenant.naiveadmin.com) |
| **React** | React、Ts、Java | 百家落地验证架构,免去试错成本,专为 Saas 化系统设计 | 构建企业级 Saas 化系统 | [立即体验](https://compose.warden.vip) |
[Element Plus Admin 预览](https://element.naiveadmin.com)
以上版本同时具备 `NaiveAdmin` 功能/组件/页面,一如既往、开箱即用,欢迎前往查看。
### Antd vue
新产品,如果您选的技术栈是 `Antd` 的话,不妨看看
[NaiveAdmin Antd 预览](https://antd.naiveadmin.com)
## 📚 文档
[文档地址](https://docs.naiveadmin.com)
[开源版本文档](https://docs.naiveadmin.com)
## 🛠 准备
@@ -151,7 +155,8 @@ pnpm build
有关 `Naive Ui Admin` 的使用或其他问题,欢迎加入我们的讨论群组或提出问题。
![160335146-c28dd205-4600-4d62-b2c6-6456034ab7b1](https://user-images.githubusercontent.com/19426584/217689718-407e6cb9-dd3b-4a11-a025-3c58834b52ff.jpg)
QQ1群328347666 (已满)
QQ2群741353560
## 💖 赞助
#### 如果您觉得这个项目对您有帮助,可以通过下面的链接为作者买一杯果汁,表示感谢!。

View File

@@ -1,16 +1,6 @@
import { defineMock } from '@alova/mock';
import { resultSuccess } from '../_util';
export interface ListDate {
label: string;
key: string;
type: number;
subtitle: string;
openType: number;
auth: string;
path: string;
children?: ListDate[];
}
import type { ListDate } from '@/api/system/menu';
const menuList = () => {
const result: ListDate[] = [

View File

@@ -1,6 +1,6 @@
{
"name": "naive-ui-admin",
"version": "2.0.0",
"version": "2.1.0",
"author": {
"name": "Ahjung",
"email": "735878602@qq.com",
@@ -25,56 +25,57 @@
"lint:pretty": "pretty-quick --staged"
},
"dependencies": {
"@alova/mock": "^2.0.6",
"@alova/mock": "^2.0.17",
"@vicons/antd": "^0.12.0",
"@vicons/ionicons5": "^0.12.0",
"@vueup/vue-quill": "^1.2.0",
"@vueuse/core": "^9.13.0",
"alova": "^3.0.16",
"alova": "^3.3.4",
"date-fns": "^2.30.0",
"dayjs": "^1.11.13",
"echarts": "^5.5.1",
"echarts": "^5.6.0",
"element-resize-detector": "^1.2.4",
"lodash-es": "^4.17.21",
"mockjs": "^1.1.0",
"naive-ui": "^2.39.0",
"pinia": "^2.2.2",
"qs": "^6.13.0",
"vue": "^3.5.5",
"vue-router": "^4.4.5",
"naive-ui": "^2.42.0",
"pinia": "^2.3.1",
"qs": "^6.14.0",
"vue": "^3.5.18",
"vue-router": "^4.5.1",
"vue-types": "^4.2.1"
},
"devDependencies": {
"@commitlint/cli": "^17.8.1",
"@commitlint/config-conventional": "^17.8.1",
"@faker-js/faker": "^9.0.0",
"@types/lodash": "^4.17.7",
"@types/node": "^18.19.50",
"@faker-js/faker": "^9.9.0",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"@types/node": "^18.19.122",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vitejs/plugin-vue": "^3.2.0",
"@vitejs/plugin-vue-jsx": "^2.1.1",
"@vue/compiler-sfc": "^3.5.5",
"@vue/compiler-sfc": "^3.5.18",
"@vue/eslint-config-typescript": "^11.0.3",
"autoprefixer": "^10.4.20",
"commitizen": "^4.3.0",
"core-js": "^3.38.1",
"autoprefixer": "^10.4.21",
"commitizen": "^4.3.1",
"core-js": "^3.45.0",
"cross-env": "^7.0.3",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^8.10.0",
"dotenv": "^16.6.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^8.10.2",
"eslint-define-config": "1.12.0",
"eslint-plugin-jest": "^27.9.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.28.0",
"esno": "^0.16.3",
"eslint-plugin-prettier": "^4.2.5",
"eslint-plugin-vue": "^9.33.0",
"esno": "^4.8.0",
"gh-pages": "^4.0.0",
"husky": "^8.0.3",
"jest": "^29.7.0",
"less": "^4.2.0",
"less": "^4.4.0",
"less-loader": "^11.1.4",
"lint-staged": "^13.3.0",
"postcss": "^8.4.45",
"postcss": "^8.5.6",
"prettier": "^2.8.8",
"pretty-quick": "^3.3.1",
"rimraf": "^3.0.2",
@@ -83,10 +84,10 @@
"stylelint-config-standard": "^29.0.0",
"stylelint-order": "^5.0.0",
"stylelint-scss": "^4.7.0",
"tailwindcss": "^3.4.11",
"tailwindcss": "^3.4.17",
"typescript": "^4.9.5",
"unplugin-vue-components": "^0.22.12",
"vite": "^5.4.5",
"vite": "^5.4.19",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-html": "^3.2.2",
"vite-plugin-style-import": "^2.0.0",

3223
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,14 @@
import { Alova } from '@/utils/http/alova/index';
import { ListDate } from 'mock/system/menu';
export interface ListDate {
label: string;
key: string;
type: number;
subtitle: string;
openType: number;
auth: string;
path: string;
children?: ListDate[];
}
/**
* @description: 根据用户id获取用户菜单

View File

@@ -5,5 +5,5 @@ export const websiteConfig = Object.freeze({
title: 'NaiveUiAdmin',
logo: logoImage,
loginImage: loginImage,
loginDesc: 'Naive Ui Admin中后台前端/设计解决方案',
loginDesc: 'Naive Ui Admin 中后台前端/设计解决方案',
});

View File

@@ -1,8 +1,9 @@
import type { GlobConfig } from '/#/config';
import type { GlobConfig, LocalConfig } from '/#/config';
import { warn } from '@/utils/log';
import { getAppEnvConfig } from '@/utils/env';
import { warn } from '@/utils/log';
// 这里的 useGlobSetting 用于获取全局配置,以下环境变量 带 VITE_GLOB_开头 会打包到 app.config 中去
export const useGlobSetting = (): Readonly<GlobConfig> => {
const {
VITE_GLOB_APP_TITLE,
@@ -11,8 +12,6 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_FILE_URL,
VITE_USE_MOCK,
VITE_LOGGER_MOCK,
} = getAppEnvConfig();
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
@@ -22,15 +21,25 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
}
// Take global configuration
const glob: Readonly<GlobConfig> = {
return {
title: VITE_GLOB_APP_TITLE,
apiUrl: VITE_GLOB_API_URL,
shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL,
fileUrl: VITE_GLOB_FILE_URL,
useMock: VITE_USE_MOCK,
loggerMock: VITE_LOGGER_MOCK,
};
return glob as Readonly<GlobConfig>;
};
// 这里的 useLocalSetting 用于获取本地配置,以下环境变量不会打包到 app.config 中去
export const useLocalSetting = (): Readonly<LocalConfig> => {
const { VITE_USE_MOCK, VITE_LOGGER_MOCK } = import.meta.env;
function strToBoolean(val): boolean {
return val === 'true';
}
return {
useMock: strToBoolean(VITE_USE_MOCK),
loggerMock: strToBoolean(VITE_LOGGER_MOCK),
};
};

View File

@@ -44,7 +44,7 @@
<n-breadcrumb v-if="crumbsSetting.show">
<template
v-for="routeItem in breadcrumbList"
:key="routeItem.name === 'Redirect' ? void 0 : routeItem.name"
:key="routeItem.name === RedirectName ? void 0 : routeItem.name"
>
<n-breadcrumb-item v-if="routeItem.meta.title">
<n-dropdown
@@ -129,17 +129,18 @@
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref, computed, unref } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import components from './components';
import { NDialogProvider, useDialog, useMessage } from 'naive-ui';
import { TABS_ROUTES } from '@/store/mutation-types';
import { useUserStore } from '@/store/modules/user';
import { useScreenLockStore } from '@/store/modules/screenLock';
import ProjectSetting from './ProjectSetting.vue';
import { AsideMenu } from '@/layout/components/Menu';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { websiteConfig } from '@/config/website.config';
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
import { AsideMenu } from '@/layout/components/Menu';
import { RedirectName } from '@/router/constant';
import { useScreenLockStore } from '@/store/modules/screenLock';
import { useUserStore } from '@/store/modules/user';
import { TABS_ROUTES } from '@/store/mutation-types';
import { NDialogProvider, useDialog, useMessage } from 'naive-ui';
import { computed, defineComponent, reactive, ref, toRefs, unref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import components from './components';
import ProjectSetting from './ProjectSetting.vue';
export default defineComponent({
name: 'PageHeader',
@@ -346,6 +347,7 @@
mixMenu,
websiteConfig,
handleMenuCollapsed,
RedirectName,
};
},
});

View File

@@ -34,7 +34,7 @@ export const RedirectRoute: RouteRecordRaw = {
children: [
{
path: '/redirect/:path(.*)',
name: RedirectName,
name: `${RedirectName}Son`,
component: () => import('@/views/redirect/index.vue'),
meta: {
title: RedirectName,

View File

@@ -1,11 +1,12 @@
import type { RouteRecordRaw } from 'vue-router';
import { isNavigationFailure, Router } from 'vue-router';
import { useUser } from '@/store/modules/user';
import { useAsyncRoute } from '@/store/modules/asyncRoute';
import { ACCESS_TOKEN } from '@/store/mutation-types';
import { storage } from '@/utils/Storage';
import { PageEnum } from '@/enums/pageEnum';
import { ErrorPageRoute } from '@/router/base';
import { useAsyncRoute } from '@/store/modules/asyncRoute';
import { useUser } from '@/store/modules/user';
import { ACCESS_TOKEN } from '@/store/mutation-types';
import { storage } from '@/utils/Storage';
import type { RouteRecordRaw } from 'vue-router';
import { isNavigationFailure, Router } from 'vue-router';
import { RedirectName } from './constant';
const LOGIN_PATH = PageEnum.BASE_LOGIN;
@@ -91,7 +92,7 @@ export function createRouterGuards(router: Router) {
if (currentComName && !keepAliveComponents.includes(currentComName) && to.meta?.keepAlive) {
// 需要缓存的组件
keepAliveComponents.push(currentComName);
} else if (!to.meta?.keepAlive || to.name == 'Redirect') {
} else if (!to.meta?.keepAlive || to.name == RedirectName) {
// 不需要缓存的组件
const index = asyncRouteStore.keepAliveComponents.findIndex((name) => name == currentComName);
if (index != -1) {

View File

@@ -9,7 +9,7 @@ const routes: Array<RouteRecordRaw> = [
name: 'https://www.naiveadmin.com',
component: Layout,
meta: {
title: 'Pro 版本',
title: 'Plus 版本',
extra: renderNew(),
icon: renderIcon(SketchOutlined),
sort: 12,

View File

@@ -1,8 +1,9 @@
import { RedirectName } from '@/router/constant';
import { defineStore } from 'pinia';
import { RouteLocationNormalized } from 'vue-router';
// 不需要出现在标签页中的路由
const whiteList = ['Redirect', 'login'];
const whiteList = [RedirectName, `${RedirectName}Son`, 'login'];
export type RouteItem = Partial<RouteLocationNormalized> & {
fullPath: string;

View File

@@ -6,12 +6,14 @@ import { isString } from 'lodash-es';
import mocks from './mocks';
import { useUser } from '@/store/modules/user';
import { storage } from '@/utils/Storage';
import { useGlobSetting } from '@/hooks/setting';
import { useGlobSetting, useLocalSetting } from '@/hooks/setting';
import { PageEnum } from '@/enums/pageEnum';
import { ResultEnum } from '@/enums/httpEnum';
import { isUrl } from '@/utils';
const { useMock, apiUrl, urlPrefix, loggerMock } = useGlobSetting();
const { apiUrl, urlPrefix } = useGlobSetting();
const { useMock, loggerMock } = useLocalSetting();
const mockAdapter = createAlovaMockAdapter([...mocks], {
// 全局控制是否启用mock接口默认为true

View File

@@ -1,7 +1,18 @@
<template>
<div class="view-account">
<div class="view-account-header"></div>
<div class="view-account-container">
<div class="view-account-background">
<div class="line line-1"></div>
<div class="line line-2"></div>
<div class="line line-3"></div>
<div class="square square-1"></div>
<div class="square square-2"></div>
<div class="triangle"></div>
<div class="wave wave-1"></div>
<div class="wave wave-2"></div>
<div class="wave wave-3"></div>
</div>
<div class="view-account-container animate__animated animate__fadeInDown">
<div class="view-account-top">
<div class="view-account-top-logo">
<img :src="websiteConfig.loginImage" alt="" />
@@ -9,15 +20,22 @@
<div class="view-account-top-desc">{{ websiteConfig.loginDesc }}</div>
</div>
<div class="view-account-form">
<h2 class="view-account-title">账号登录</h2>
<div class="login-welcome">欢迎回来请登录您的账号</div>
<n-form
ref="formRef"
label-placement="left"
size="large"
:model="formInline"
:rules="rules"
class="login-form"
>
<n-form-item path="username">
<n-input v-model:value="formInline.username" placeholder="请输入用户名">
<n-form-item path="username" class="username-item">
<n-input
v-model:value="formInline.username"
placeholder="请输入用户名"
class="login-input"
>
<template #prefix>
<n-icon size="18" color="#808695">
<PersonOutline />
@@ -25,12 +43,13 @@
</template>
</n-input>
</n-form-item>
<n-form-item path="password">
<n-form-item path="password" class="password-item">
<n-input
v-model:value="formInline.password"
type="password"
showPasswordOn="click"
placeholder="请输入密码"
class="login-input"
>
<template #prefix>
<n-icon size="18" color="#808695">
@@ -39,42 +58,52 @@
</template>
</n-input>
</n-form-item>
<n-form-item class="default-color">
<div class="flex justify-between">
<div class="flex-initial">
<n-form-item class="default-color remember-forgot">
<div class="flex-between-wrapper">
<div class="left">
<n-checkbox v-model:checked="autoLogin">自动登录</n-checkbox>
</div>
<div class="flex-initial order-last">
<a href="javascript:">忘记密码</a>
<div class="right">
<a href="javascript:" class="forgot-link">忘记密码</a>
</div>
</div>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleSubmit" size="large" :loading="loading" block>
<n-button
type="primary"
@click="handleSubmit"
size="large"
:loading="loading"
block
class="login-button"
>
登录
</n-button>
</n-form-item>
<n-form-item class="default-color">
<n-form-item class="default-color other-item">
<div class="flex view-account-other">
<div class="flex-initial">
<div class="flex-initial other-text">
<span>其它登录方式</span>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<div class="social-login">
<a href="javascript:" class="social-icon">
<n-icon size="24" color="#909399">
<LogoGithub />
</n-icon>
</a>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<a href="javascript:" class="social-icon">
<n-icon size="24" color="#909399">
<LogoFacebook />
</n-icon>
</a>
<a href="javascript:" class="social-icon">
<n-icon size="24" color="#909399">
<LogoWechat />
</n-icon>
</a>
</div>
<div class="flex-initial" style="margin-left: auto">
<a href="javascript:">注册账号</a>
<a href="javascript:" class="register-link">注册账号</a>
</div>
</div>
</n-form-item>
@@ -85,14 +114,25 @@
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { reactive, ref, onMounted } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { useMessage } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline, LogoGithub, LogoFacebook } from '@vicons/ionicons5';
import { PersonOutline, LockClosedOutline, LogoGithub, LogoFacebook, LogoWechat } from '@vicons/ionicons5';
import { PageEnum } from '@/enums/pageEnum';
import { websiteConfig } from '@/config/website.config';
// 添加页面加载动画效果
onMounted(() => {
// 聚焦用户名输入框
setTimeout(() => {
const usernameInput = document.querySelector('input[placeholder="请输入用户名"]');
if (usernameInput) {
(usernameInput as HTMLElement).focus();
}
}, 500);
});
interface FormState {
username: string;
password: string;
@@ -161,27 +201,122 @@
flex-direction: column;
height: 100vh;
overflow: auto;
background-color: #f0f2f5;
background: linear-gradient(140deg, #e8f1fa, #c2d9ec, #a1c3e0, #80aed3);
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCI+CiAgPGNpcmNsZSBjeD0iMTAiIGN5PSIxMCIgcj0iMiIgZmlsbD0icmdiYSg0NSwgMTQwLCAyNDAsIDAuMSkiIC8+Cjwvc3ZnPg==');
opacity: 0.6;
z-index: 0;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIj4KICA8cmVjdCB4PSI1MCIgeT0iNTAiIHdpZHRoPSIxMCIgaGVpZ2h0PSIxMCIgdHJhbnNmb3JtPSJyb3RhdGUoNDUgNTUgNTUpIiBmaWxsPSJyZ2JhKDQ1LCAxNDAsIDI0MCwgMC4wNSkiIC8+Cjwvc3ZnPg==');
opacity: 0.8;
z-index: 0;
}
&-container {
flex: 1;
padding: 32px 12px;
max-width: 384px;
min-width: 320px;
padding: 32px 40px 20px;
max-width: 580px;
min-width: 460px;
margin: 0 auto;
background-color: #fff;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
margin-top: 10vh;
position: relative;
backdrop-filter: blur(10px);
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(255, 255, 255, 0.18);
transition: all 0.3s ease;
&:hover {
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
transform: translateY(-5px);
}
// 移除圆形装饰元素
@keyframes float {
0% {
transform: translateY(0px);
}
50% {
transform: translateY(-10px);
}
100% {
transform: translateY(0px);
}
}
}
&-title {
text-align: center;
font-size: 22px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
position: relative;
&::after {
content: '';
position: absolute;
bottom: -10px;
left: 50%;
transform: translateX(-50%);
width: 40px;
height: 2px;
background: linear-gradient(to right, #2d8cf0, #0081ff);
border-radius: 2px;
}
}
.login-welcome {
text-align: center;
font-size: 14px;
color: #606266;
margin-bottom: 30px;
margin-top: 20px;
}
&-top {
padding: 32px 0;
padding: 10px 0;
text-align: center;
&-logo {
margin-bottom: 8px;
display: flex;
justify-content: center;
img {
height: 60px;
}
}
&-desc {
font-size: 14px;
color: #808695;
color: #606266;
}
}
&-other {
width: 100%;
display: flex;
align-items: center;
}
.default-color {
@@ -191,18 +326,368 @@
color: #515a6e;
}
}
.login-button {
margin-top: 10px;
height: 42px;
font-size: 16px;
border-radius: 4px;
transition: all 0.3s;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
}
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 5px;
height: 5px;
background: rgba(255, 255, 255, 0.5);
opacity: 0;
border-radius: 100%;
transform: scale(1, 1) translate(-50%);
transform-origin: 50% 50%;
}
&:focus:not(:active)::after {
animation: ripple 1s ease-out;
}
@keyframes ripple {
0% {
transform: scale(0, 0);
opacity: 0.5;
}
20% {
transform: scale(25, 25);
opacity: 0.3;
}
100% {
opacity: 0;
transform: scale(40, 40);
}
}
}
.remember-forgot {
margin-bottom: 5px;
.flex-between-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.right {
text-align: right;
}
}
.forgot-link {
color: #606266;
transition: all 0.2s;
&:hover {
color: #2d8cf0;
}
}
.social-login {
display: flex;
margin-left: 16px;
}
.social-icon {
display: flex;
justify-content: center;
align-items: center;
width: 36px;
height: 36px;
border-radius: 50%;
margin-right: 12px;
transition: all 0.3s;
background-color: rgba(144, 147, 153, 0.1);
&:hover {
background-color: rgba(45, 140, 240, 0.2);
transform: scale(1.1);
:deep(svg) {
color: #2d8cf0 !important;
}
}
}
.register-link {
color: #2d8cf0;
transition: all 0.3s;
&:hover {
color: #57a3f3;
text-decoration: underline;
}
}
.login-form {
:deep(.n-form-item-feedback-wrapper) {
min-height: 18px;
}
:deep(.n-input) {
border-radius: 4px;
}
padding: 0;
}
.login-input {
:deep(.n-input__input-el) {
padding-left: 5px;
}
:deep(.n-input-wrapper) {
transition: all 0.3s ease;
}
&:hover {
:deep(.n-input-wrapper) {
box-shadow: 0 0 0 1px rgba(45, 140, 240, 0.2);
}
}
}
.username-item, .password-item {
margin-bottom: 24px;
}
.other-text {
padding-left: 5px;
}
.other-item {
margin-bottom: 0;
}
}
@media (min-width: 768px) {
.view-account {
background-image: url('../../assets/images/login.svg');
background-image: url('../../assets/images/login.svg'),
radial-gradient(circle at 10% 20%, rgba(100, 149, 237, 0.25) 0%, rgba(65, 105, 225, 0.2) 40%, rgba(30, 144, 255, 0.1) 90%);
background-repeat: no-repeat;
background-position: 50%;
background-size: 100%;
background-size: cover;
position: relative;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(45, 140, 240, 0.1), rgba(45, 140, 240, 0.05));
backdrop-filter: blur(10px);
z-index: 0;
}
&::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
background-image: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgo8ZGVmcz4KICA8cGF0dGVybiBpZD0icGF0dGVybiIgeD0iMCIgeT0iMCIgd2lkdGg9IjYwIiBoZWlnaHQ9IjYwIiBwYXR0ZXJuVW5pdHM9InVzZXJTcGFjZU9uVXNlIiBwYXR0ZXJuVHJhbnNmb3JtPSJyb3RhdGUoNDUpIj4KICAgIDxjaXJjbGUgY3g9IjMwIiBjeT0iMzAiIHI9IjEuNSIgZmlsbD0icmdiYSg0NSwgMTQwLCAyNDAsIDAuMikiIC8+CiAgPC9wYXR0ZXJuPgo8L2RlZnM+CjxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjcGF0dGVybikiIC8+Cjwvc3ZnPg==');
opacity: 0.3;
z-index: 0;
pointer-events: none;
}
&-container {
margin-top: 15vh;
z-index: 1;
position: relative;
}
}
}
.page-account-container {
padding: 32px 0 24px 0;
@media (max-height: 650px) {
.view-account-container {
margin-top: 5vh;
}
}
.view-account-background {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
.line {
position: absolute;
background: linear-gradient(90deg, rgba(45, 140, 240, 0.2), rgba(0, 129, 255, 0.1));
&-1 {
width: 300px;
height: 2px;
top: 15%;
right: 5%;
transform: rotate(-30deg);
animation: pulse 8s ease-in-out infinite;
}
&-2 {
width: 200px;
height: 2px;
bottom: 20%;
left: 10%;
transform: rotate(45deg);
animation: pulse 6s ease-in-out infinite 1s;
}
&-3 {
width: 150px;
height: 2px;
top: 40%;
left: 5%;
transform: rotate(-15deg);
animation: pulse 7s ease-in-out infinite 2s;
}
}
.square {
position: absolute;
&-1 {
width: 80px;
height: 80px;
top: 10%;
left: 15%;
background: linear-gradient(45deg, rgba(45, 140, 240, 0.15), rgba(0, 129, 255, 0.05));
transform: rotate(30deg);
animation: rotate 15s linear infinite;
}
&-2 {
width: 60px;
height: 60px;
bottom: 15%;
right: 10%;
border: 2px solid rgba(45, 140, 240, 0.1);
background: transparent;
animation: rotate 12s linear infinite reverse;
}
}
.triangle {
position: absolute;
bottom: 30%;
right: 20%;
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 80px solid rgba(45, 140, 240, 0.08);
animation: float 10s ease-in-out infinite;
}
@keyframes pulse {
0% {
opacity: 0.3;
}
50% {
opacity: 0.6;
}
100% {
opacity: 0.3;
}
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.wave {
position: absolute;
opacity: 0.3;
transform-origin: bottom left;
&-1 {
bottom: 0;
left: 0;
width: 100%;
height: 120px;
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNDQwIDMyMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PHBhdGggZmlsbD0icmdiYSg0NSwgMTQwLCAyNDAsIDAuMikiIGQ9Ik0wLDMyMEMwLDI0MCA0MCwxNjAgODAsMTYwQzEyMCwxNjAgMTYwLDI0MCAyMDAsMjQwQzI0MCwyNDAgMjgwLDE2MCAzMjAsMTYwQzM2MCwxNjAgNDAwLDI0MCA0NDAsMjQwQzQ4MCwyNDAgNTIwLDE2MCA1NjAsMTYwQzYwMCwxNjAgNjQwLDI0MCA2ODAsMjQwQzcyMCwyNDAgNzYwLDE2MCA4MDAsMTYwQzg0MCwxNjAgODgwLDI0MCA5MjAsMjQwQzk2MCwyNDAgMTAwMCwxNjAgMTA0MCwxNjBDMTA4MCwxNjAgMTEyMCwyNDAgMTE2MCwyNDBDMTIwMCwyNDAgMTI0MCwxNjAgMTI4MCwxNjBDMTMyMCwxNjAgMTM2MCwyNDAgMTQwMCwyNDBDMTQ0MCwyNDAgMTQ0MCwxNjAgMTQ0MCwxNjBMMTQ0MCwzMjBMMCwzMjBaIj48L3BhdGg+PC9zdmc+');
background-size: 100% 120px;
animation: wave-left-to-right 15s ease-in-out infinite;
transform: rotate(-2deg);
}
&-2 {
bottom: 0;
left: 0;
width: 100%;
height: 100px;
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNDQwIDMyMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PHBhdGggZmlsbD0icmdiYSg0NSwgMTQwLCAyNDAsIDAuMTUpIiBkPSJNMCwzMjBDMCwyNDAgNjAsMTgwIDEyMCwxODBDMTgwLDE4MCAyNDAsMjQwIDMwMCwyNDBDMzYwLDI0MCA0MjAsMTgwIDQ4MCwxODBDNTQwLDE4MCA2MDAsMjQwIDY2MCwyNDBDNzIwLDI0MCA3ODAsMTgwIDg0MCwxODBDOTAwLDE4MCA5NjAsMjQwIDEwMjAsMjQwQzEwODAsMjQwIDExNDAsMTgwIDEyMDAsMTgwQzEyNjAsMTgwIDEzMjAsMjQwIDEzODAsMjQwQzE0NDAsMjQwIDE0NDAsMTgwIDE0NDAsMTgwTDE0NDAsMzIwTDAsMzIwWiI+PC9wYXRoPjwvc3ZnPg==');
background-size: 100% 100px;
animation: wave-left-to-right 18s ease-in-out infinite;
animation-delay: -5s;
transform: rotate(-1deg);
}
&-3 {
bottom: 0;
left: 0;
width: 100%;
height: 80px;
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNDQwIDMyMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ibm9uZSI+PHBhdGggZmlsbD0icmdiYSg0NSwgMTQwLCAyNDAsIDAuMSkiIGQ9Ik0wLDMyMEMwLDI2MCAzMCwyMDAgNjAsMjAwQzkwLDIwMCAxMjAsMjYwIDE1MCwyNjBDMTgwLDI2MCAyMTAsMjAwIDI0MCwyMDBDMjcwLDIwMCAzMDAsMjYwIDMzMCwyNjBDMzYwLDI2MCAzOTAsMjAwIDQyMCwyMDBDNDUwLDIwMCA0ODAsMjYwIDUxMCwyNjBDNTQwLDI2MCA1NzAsMjAwIDYwMCwyMDBDNjMwLDIwMCA2NjAsMjYwIDY5MCwyNjBDNzIwLDI2MCA3NTAsMjAwIDc4MCwyMDBDODEwLDIwMCA4NDAsMjYwIDg3MCwyNjBDOTAwLDI2MCA5MzAsMjAwIDk2MCwyMDBDOTkwLDIwMCAxMDIwLDI2MCAxMDUwLDI2MEMxMDgwLDI2MCAxMTEwLDIwMCAxMTQwLDIwMEMxMTcwLDIwMCAxMjAwLDI2MCAxMjMwLDI2MEMxMjYwLDI2MCAxMjkwLDIwMCAxMzIwLDIwMEMxMzUwLDIwMCAxMzgwLDI2MCAxNDEwLDI2MEMxNDQwLDI2MCAxNDQwLDIwMCAxNDQwLDIwMEwxNDQwLDMyMEwwLDMyMFoiPjwvcGF0aD48L3N2Zz4=');
background-size: 100% 80px;
animation: wave-left-to-right 20s ease-in-out infinite;
animation-delay: -2s;
}
}
@keyframes wave-left-to-right {
0% {
background-position-x: 0;
background-position-y: 100%;
}
50% {
background-position-x: 720px;
background-position-y: 50%;
}
100% {
background-position-x: 1440px;
background-position-y: 0%;
}
}
@keyframes float {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-15px);
}
100% {
transform: translateY(0);
}
}
}
</style>

View File

@@ -7,7 +7,7 @@
class="thing-cell"
v-for="item in typeTabList"
:key="item.key"
:class="{ 'thing-cell-on': type === item.key }"
:class="{ 'thing-cell-on': state.type === item.key }"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
@@ -16,61 +16,63 @@
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1" />
<SafetySetting v-if="type === 2" />
<n-card :bordered="false" size="small" :title="state.typeTitle" class="proCard">
<BasicSetting v-if="state.type === 1" />
<SafetySetting v-if="state.type === 2" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import BasicSetting from './BasicSetting.vue';
import SafetySetting from './SafetySetting.vue';
import { reactive, ref } from 'vue';
import BasicSetting from './BasicSetting.vue';
import SafetySetting from './SafetySetting.vue';
const typeTabList = [
{
name: '基本设置',
desc: '个人账户信息设置',
key: 1,
},
{
name: '安全设置',
desc: '密码,邮箱等设置',
key: 2,
},
];
const typeTabList = [
{
name: '基本设置',
desc: '个人账户信息设置',
key: 1,
},
{
name: '安全设置',
desc: '密码,邮箱等设置',
key: 2,
},
];
const type = ref(1);
const typeTitle = ref('基本设置');
const state = reactive({
type: 1,
typeTitle: '基本设置',
});
function switchType(e) {
type.value = e.key;
typeTitle.value = e.name;
}
function switchType(e) {
state.type = e.key;
state.typeTitle = e.name;
}
</script>
<style lang="less" scoped>
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
&:hover {
background: #f3f3f3;
cursor: pointer;
}
&:hover {
background: #f3f3f3;
cursor: pointer;
}
}
.thing-cell-on {
background: #f0faff;
.thing-cell-on {
background: #f0faff;
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0;
}
&:hover {
background: #f0faff;
}
}
&:hover {
background: #f0faff;
}
}
</style>

View File

@@ -7,7 +7,7 @@
class="thing-cell"
v-for="item in typeTabList"
:key="item.key"
:class="{ 'thing-cell-on': type === item.key }"
:class="{ 'thing-cell-on': state.type === item.key }"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
@@ -16,10 +16,10 @@
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1" />
<RevealSetting v-if="type === 2" />
<EmailSetting v-if="type === 3" />
<n-card :bordered="false" size="small" :title="state.typeTitle" class="proCard">
<BasicSetting v-if="state.type === 1" />
<RevealSetting v-if="state.type === 2" />
<EmailSetting v-if="state.type === 3" />
</n-card>
</n-grid-item>
</n-grid>

View File

@@ -132,7 +132,7 @@
import { getMenuList } from '@/api/system/menu';
import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue';
import { ListDate } from 'mock/system/menu';
import type { ListDate } from '@/api/system/menu';
const rules = {
label: {

View File

@@ -75,10 +75,8 @@
import { getTreeAll } from '@/utils';
import CreateModal from './CreateModal.vue';
import EditModal from './EditModal.vue';
import { useRouter } from 'vue-router';
import { ListDate } from 'mock/system/menu';
import type { ListDate } from '@/api/system/menu';
const router = useRouter();
const message = useMessage();
const actionRef = ref();
const createModalRef = ref();

11
types/config.d.ts vendored
View File

@@ -52,8 +52,13 @@ export interface GlobConfig {
shortName: string;
urlPrefix?: string;
uploadUrl?: string;
useMock: boolean;
fileUrl?: string;
}
export interface LocalConfig {
// 生产环境开启 mock
useMock: boolean;
// 打印 mock 请求信息
loggerMock: boolean;
}
@@ -71,7 +76,7 @@ export interface GlobEnvConfig {
// 文件前缀地址
VITE_GLOB_FILE_URL?: string;
// 开启 mock
VITE_USE_MOCK: boolean;
VITE_USE_MOCK: string;
// 是否开启控制台打印 mock 请求信息
VITE_LOGGER_MOCK: boolean;
VITE_LOGGER_MOCK: string;
}

1
types/global.d.ts vendored
View File

@@ -66,6 +66,7 @@ declare global {
declare interface ViteEnv {
VITE_PORT: number;
VITE_USE_MOCK: boolean;
VITE_LOGGER_MOCK: boolean;
VITE_PUBLIC_PATH: string;
VITE_GLOB_APP_TITLE: string;
VITE_GLOB_APP_SHORT_NAME: string;

View File

@@ -56,11 +56,29 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
exclude: ['vue-demi'],
},
build: {
target: 'es2015',
target: 'es2020',
cssTarget: 'chrome80',
outDir: OUTPUT_DIR,
reportCompressedSize: false,
chunkSizeWarningLimit: 2000,
// 构建分包策略
rollupOptions: {
output: {
manualChunks: {
'naive-ui': ['naive-ui'],
'lodash-es': ['lodash-es'],
'vue-router': ['vue-router'],
'vue-quill': ['@vueup/vue-quill'],
'vicons-antd': ['@vicons/antd'],
'vicons-ionicons5': ['@vicons/ionicons5'],
vuedraggable: ['vuedraggable'],
echarts: ['echarts'],
vueuse: ['@vueuse/core'],
vue: ['vue'],
pinia: ['pinia'],
},
},
},
},
};
};