fix Bug or add docs

This commit is contained in:
Ah jung
2021-07-30 10:26:19 +08:00
parent 044976b790
commit 619669ec9e
63 changed files with 2039 additions and 470 deletions

View File

@@ -13,14 +13,18 @@ VITE_BASE_URL = /
# 是否删除console # 是否删除console
VITE_DROP_CONSOLE = true VITE_DROP_CONSOLE = true
# 跨域代理,可以配置多个,请注意不要换行
#VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]]
# VITE_PROXY=[["/api","https://naive-ui-admin"]]
# API 接口地址 # API 接口地址
VITE_APP_API_URL = / VITE_GLOB_API_URL =
# 图片上传地址 # 图片上传地址
VITE_GLOB_UPLOAD_URL= / VITE_GLOB_UPLOAD_URL=
# 图片前缀地址 # 图片前缀地址
VITE_GLOB_IMG_URL= / VITE_GLOB_IMG_URL=
# 接口前缀 # 接口前缀
VITE_GLOB_API_URL_PREFIX = /api VITE_GLOB_API_URL_PREFIX = /api

View File

@@ -11,13 +11,21 @@ VITE_BASE_URL = /
VITE_DROP_CONSOLE = true VITE_DROP_CONSOLE = true
# API # API
VITE_APP_API_URL = / VITE_GLOB_API_URL =
# 图片上传地址 # 图片上传地址
VITE_GLOB_UPLOAD_URL= / VITE_GLOB_UPLOAD_URL=
# 图片前缀地址 # 图片前缀地址
VITE_GLOB_IMG_URL= / VITE_GLOB_IMG_URL=
# 接口前缀 # 接口前缀
VITE_GLOB_API_URL_PREFIX = /api VITE_GLOB_API_URL_PREFIX = /api
# 是否启用gzip压缩或brotli压缩
# 可选: gzip | brotli | none
# 如果你需要多种形式,你可以用','来分隔
VITE_BUILD_COMPRESS = 'none'
# 使用压缩时是否删除原始文件默认为false
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false

View File

@@ -1,3 +1,20 @@
# 1.5 (2021-07-30)
### 🐛 Bug Fixes
- 修复表格列配置,拖拽时最后的操作列重复增加
- 多标签页交互优化
- ### ✨ Features
- `项目文档`已上线
- `Application`组件加载机制优化解决路由守卫Axios中可使用DialogMessage 等之类组件
- `BasicTable` 组件新增`高度自适应``单元格编辑``整行编辑` 特性
- `nprogress` 移除,用 `Loading Bar`代替
- 打包支持`gzip``brotli` 压缩
- 新增代理`VITE_PROXY`配置
- 路由菜单,支持多级菜单
- 依赖升级
- 本次更新,有破坏性更新,涉及文件重命名,增删调整
# 1.4 (2021-07-21) # 1.4 (2021-07-21)
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- vite降至2.3.6 - vite降至2.3.6

View File

@@ -1,6 +1,6 @@
## 简介 ## 简介
Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3`,`vite2`,`TypeScript`等主流技术开发,开箱即用的中后台前端解决方案,也可用于学习参考 [Naive Ui Admin](https://github.com/jekip/naive-ui-admin) 是一个基于 [Vue3.0](https://github.com/vuejs/vue-next)、[Vite](https://github.com/vitejs/vite)、 [Naive UI](https://www.naiveui.com/)、[TypeScript](https://www.typescriptlang.org/) 的中后台解决方案,它使用了最新的前端技术栈,并提炼了典型的业务模型,页面,包括二次封装组件、动态菜单、权限校验、粒子化权限控制等功能,它可以帮助你快速搭建企业级中后台项目,该项目使用最新的前端技术栈,相信不管是从新技术使用还是其他方面,都能帮助到你
## 特性 ## 特性
- **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发 - **最新技术栈**:使用 Vue3/vite2 等前端前沿技术开发
@@ -10,35 +10,15 @@ Naive Ui Admin 是一个免费开源的中后台模版,使用了最新的`vue3
- **权限** 内置完善的动态路由权限生成方案 - **权限** 内置完善的动态路由权限生成方案
- **组件** 二次封装了多个常用的组件 - **组件** 二次封装了多个常用的组件
### 页面功能
#### 系统看板
- [x] 主控台
- [ ] 监控页
- [x] 工作台
- [x] 表单页面
- [x] 列表页面
- [x] 异常页面
- [x] 结果页面
- [x] 设置页面
- [x] 系统设置
- [x] 菜单权限
- [x] 角色权限
### 页面组件
#### ProTable
- [x] 基础表格
- [x] 上传图片
- [x] 滑块验证码
- 持续开发中...
## 在线预览 ## 在线预览
- [naive-ui-admin](https://jekip.github.io) - [naive-ui-admin](https://jekip.github.io)
账号admin密码123456 账号admin密码123456(随意)
## 文档 ## 文档
[文档地址](https://github.com/jekip/naive-ui-admin) - 待完善 [文档地址](https://jekip.github.io/docs/)
## 准备 ## 准备
@@ -85,7 +65,7 @@ yarn build
[CHANGELOG](./CHANGELOG.md) [CHANGELOG](./CHANGELOG.md)
## 感谢 ## 感谢
[@Vben](https://github.com/anncwb/vue-vben-admin) 借鉴 vue-vben-admin 实现的骨架,同时也使用作者开发的 vite 插件,非常感谢作者。 [@Vben](https://github.com/anncwb/vue-vben-admin) 借鉴 vue-vben-admin 实现的骨架,同时也使用作者开发的 vite 插件,再次感谢作者。
## 如何贡献 ## 如何贡献

View File

@@ -31,8 +31,7 @@ export function wrapperEnv(envConf: Recordable): ViteEnv {
if (envName === 'VITE_PROXY') { if (envName === 'VITE_PROXY') {
try { try {
realName = JSON.parse(realName); realName = JSON.parse(realName);
} catch (error) { } catch (error) {}
}
} }
ret[envName] = realName; ret[envName] = realName;
process.env[envName] = realName; process.env[envName] = realName;
@@ -51,12 +50,11 @@ export function getEnvConfig(match = 'VITE_GLOB_', confFiles = ['.env', '.env.pr
try { try {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item))); const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
envConfig = { ...envConfig, ...env }; envConfig = { ...envConfig, ...env };
} catch (error) { } catch (error) {}
}
}); });
Object.keys(envConfig).forEach((key) => { Object.keys(envConfig).forEach((key) => {
const reg = new RegExp(`^(${ match })`); const reg = new RegExp(`^(${match})`);
if (!reg.test(key)) { if (!reg.test(key)) {
Reflect.deleteProperty(envConfig, key); Reflect.deleteProperty(envConfig, key);
} }

View File

@@ -0,0 +1,35 @@
/**
* Used to package and output gzip. Note that this does not work properly in Vite, the specific reason is still being investigated
* https://github.com/anncwb/vite-plugin-compression
*/
import type { Plugin } from 'vite';
import compressPlugin from 'vite-plugin-compression';
export function configCompressPlugin(
compress: 'gzip' | 'brotli' | 'none',
deleteOriginFile = false
): Plugin | Plugin[] {
const compressList = compress.split(',');
const plugins: Plugin[] = [];
if (compressList.includes('gzip')) {
plugins.push(
compressPlugin({
ext: '.gz',
deleteOriginFile,
})
);
}
if (compressList.includes('brotli')) {
plugins.push(
compressPlugin({
ext: '.br',
algorithm: 'brotliCompress',
deleteOriginFile,
})
);
}
return plugins;
}

View File

@@ -5,9 +5,10 @@ import vueJsx from '@vitejs/plugin-vue-jsx';
import { configHtmlPlugin } from './html'; import { configHtmlPlugin } from './html';
import { configMockPlugin } from './mock'; import { configMockPlugin } from './mock';
import { configCompressPlugin } from './compress';
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock) { export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock) {
const { VITE_USE_MOCK } = viteEnv; const { VITE_USE_MOCK, VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv;
const vitePlugins: (Plugin | Plugin[])[] = [ const vitePlugins: (Plugin | Plugin[])[] = [
// have to // have to
@@ -22,5 +23,12 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock)
// vite-plugin-mock // vite-plugin-mock
VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild, prodMock)); VITE_USE_MOCK && vitePlugins.push(configMockPlugin(isBuild, prodMock));
if (isBuild) {
// rollup-plugin-gzip
vitePlugins.push(
configCompressPlugin(VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE)
);
}
return vitePlugins; return vitePlugins;
} }

34
build/vite/proxy.ts Normal file
View File

@@ -0,0 +1,34 @@
/**
* Used to parse the .env.development proxy configuration
*/
import type { ProxyOptions } from 'vite';
type ProxyItem = [string, string];
type ProxyList = ProxyItem[];
type ProxyTargetList = Record<string, ProxyOptions & { rewrite: (path: string) => string }>;
const httpsRE = /^https:\/\//;
/**
* Generate proxy
* @param list
*/
export function createProxy(list: ProxyList = []) {
const ret: ProxyTargetList = {};
for (const [prefix, target] of list) {
const isHttps = httpsRE.test(target);
// https://github.com/http-party/node-http-proxy#options
ret[prefix] = {
target: target,
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ''),
// https is require secure=false
...(isHttps ? { secure: false } : {}),
};
}
return ret;
}

View File

@@ -13,6 +13,7 @@
<style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style> <style>.first-loading-wrp{display:flex;justify-content:center;align-items:center;flex-direction:column;min-height:420px;height:100%}.first-loading-wrp>h1{font-size:128px}.first-loading-wrp .loading-wrp{padding:98px;display:flex;justify-content:center;align-items:center}.dot{animation:antRotate 1.2s infinite linear;transform:rotate(45deg);position:relative;display:inline-block;font-size:32px;width:32px;height:32px;box-sizing:border-box}.dot i{width:14px;height:14px;position:absolute;display:block;background-color:#1890ff;border-radius:100%;transform:scale(.75);transform-origin:50% 50%;opacity:.3;animation:antSpinMove 1s infinite linear alternate}.dot i:nth-child(1){top:0;left:0}.dot i:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.dot i:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.dot i:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antSpinMove{to{opacity:1}}</style>
</head> </head>
<body> <body>
<div id="appProvider" style="display: none"></div>
<div id="app"> <div id="app">
<div class="first-loading-wrp"> <div class="first-loading-wrp">
<div class="loading-wrp"> <div class="loading-wrp">

View File

@@ -14,7 +14,7 @@ const tableList = (pageSize) => {
date: `@date('yyyy-MM-dd')`, date: `@date('yyyy-MM-dd')`,
time: `@time('HH:mm')`, time: `@time('HH:mm')`,
'no|100000-10000000': 100000, 'no|100000-10000000': 100000,
'status|1': ['normal', 'enable', 'disable'], 'status|1': [true, false],
}); });
}); });
return result; return result;

View File

@@ -1,6 +1,6 @@
{ {
"name": "naive-ui-admin", "name": "naive-ui-admin",
"version": "1.4", "version": "1.5",
"author": { "author": {
"name": "Ahjung", "name": "Ahjung",
"email": "735878602@qq.com", "email": "735878602@qq.com",
@@ -8,8 +8,12 @@
}, },
"private": true, "private": true,
"scripts": { "scripts": {
"bootstrap": "yarn install",
"serve": "npm run dev",
"dev": "vite", "dev": "vite",
"build": "vite build && esno ./build/script/postBuild.ts", "build": "vite build && esno ./build/script/postBuild.ts",
"build:no-cache": "yarn clean:cache && npm run build",
"report": "cross-env REPORT=true npm run build",
"preview": "vite preview", "preview": "vite preview",
"build typecheck": "vuedx-typecheck . && vite build", "build typecheck": "vuedx-typecheck . && vite build",
"deploy": "gh-pages -d dist", "deploy": "gh-pages -d dist",
@@ -34,8 +38,7 @@
"makeit-captcha": "^1.2.5", "makeit-captcha": "^1.2.5",
"mitt": "^2.1.0", "mitt": "^2.1.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"naive-ui": "^2.15.5", "naive-ui": "^2.15.11",
"nprogress": "^1.0.0-1",
"pinia": "^2.0.0-beta.3", "pinia": "^2.0.0-beta.3",
"qs": "^6.10.1", "qs": "^6.10.1",
"vfonts": "^0.1.0", "vfonts": "^0.1.0",
@@ -76,14 +79,16 @@
"postcss": "^8.3.5", "postcss": "^8.3.5",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"pretty-quick": "^3.1.0", "pretty-quick": "^3.1.0",
"rimraf": "^3.0.2",
"stylelint": "^13.13.1", "stylelint": "^13.13.1",
"stylelint-config-prettier": "^8.0.2", "stylelint-config-prettier": "^8.0.2",
"stylelint-config-standard": "^22.0.0", "stylelint-config-standard": "^22.0.0",
"stylelint-order": "^4.1.0", "stylelint-order": "^4.1.0",
"stylelint-scss": "^3.19.0", "stylelint-scss": "^3.19.0",
"tailwindcss": "^2.2.4", "tailwindcss": "^2.2.7",
"typescript": "^4.3.2", "typescript": "^4.3.5",
"vite": "2.3.6", "vite": "2.3.6",
"vite-plugin-compression": "^0.3.1",
"vite-plugin-html": "^2.0.7", "vite-plugin-html": "^2.0.7",
"vite-plugin-mock": "^2.9.3", "vite-plugin-mock": "^2.9.3",
"vite-plugin-style-import": "^1.0.1", "vite-plugin-style-import": "^1.0.1",

View File

@@ -11,7 +11,7 @@
</AppProvider> </AppProvider>
</NConfigProvider> </NConfigProvider>
<transition v-if="isLock && $route.name != 'login'" name="slide-up"> <transition v-if="isLock && $route.name !== 'login'" name="slide-up">
<LockScreen /> <LockScreen />
</transition> </transition>
</template> </template>
@@ -34,9 +34,7 @@
const designStore = useDesignSettingStore(); const designStore = useDesignSettingStore();
const isLock = computed(() => useLockscreen.isLock); const isLock = computed(() => useLockscreen.isLock);
const lockTime = computed(() => useLockscreen.lockTime); const lockTime = computed(() => useLockscreen.lockTime);
/**
* @type import('naive-ui').GlobalThemeOverrides
*/
const getThemeOverrides = computed(() => { const getThemeOverrides = computed(() => {
return { return {
common: { common: {
@@ -89,9 +87,7 @@
</script> </script>
<style lang="less"> <style lang="less">
@import 'styles/global.less';
@import 'styles/common.less'; @import 'styles/common.less';
@import 'styles/override.less';
.slide-up-enter-active, .slide-up-enter-active,
.slide-up-leave-active { .slide-up-leave-active {

View File

@@ -14,7 +14,7 @@ export function adminMenus() {
* 获取tree菜单列表 * 获取tree菜单列表
* @param params * @param params
*/ */
export function getMenuList(params) { export function getMenuList(params?) {
return http.request({ return http.request({
url: '/menu/list', url: '/menu/list',
method: 'GET', method: 'GET',

View File

@@ -33,7 +33,7 @@ export function login(params) {
params, params,
}, },
{ {
isTransformRequestResult: false, isTransformResponse: false,
} }
); );
} }
@@ -49,7 +49,7 @@ export function changePassword(params, uid) {
params, params,
}, },
{ {
isTransformRequestResult: false, isTransformResponse: false,
} }
); );
} }

View File

@@ -1,23 +1,41 @@
<template> <template>
<n-dialog-provider> <n-loading-bar-provider>
<DialogContent /> <LoadingContent />
<n-notification-provider> <n-dialog-provider>
<n-message-provider> <DialogContent />
<MessageContent /> <n-notification-provider>
<slot slot="default"></slot> <n-message-provider>
</n-message-provider> <MessageContent />
</n-notification-provider> <slot slot="default"></slot>
</n-dialog-provider> </n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
} from 'naive-ui';
import { LoadingContent } from '@/components/LoadingContent';
import { MessageContent } from '@/components/MessageContent'; import { MessageContent } from '@/components/MessageContent';
import { DialogContent } from '@/components/DialogContent'; import { DialogContent } from '@/components/DialogContent';
export default defineComponent({ export default defineComponent({
name: 'Application', name: 'Application',
components: { MessageContent, DialogContent }, components: {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
LoadingContent,
MessageContent,
DialogContent,
},
setup() { setup() {
return {}; return {};
}, },

View File

@@ -0,0 +1,3 @@
import LoadingContent from './index.vue';
export { LoadingContent };

View File

@@ -0,0 +1,12 @@
<template></template>
<script lang="ts">
import { useLoadingBar } from 'naive-ui';
export default {
name: 'LoadingContent',
setup() {
//挂载在 window 方便与在js中使用
window['$loading'] = useLoadingBar();
},
};
</script>

View File

@@ -1,95 +0,0 @@
BasicTable 重封装组件说明
====
封装说明
----
> 基础的使用方式与 API 与 [官方版(data-table)](https://www.naiveui.com/zh-CN/os-theme/components/data-table#tree) 本一致,在其基础上,封装了加载数据的方法。
>
> 你无需在你是用表格的页面进行分页逻辑处理,仅需向 BasicTable 组件传递绑定 `:api="Promise"` 对象即可
>
> 例子1
----
(基础使用)
```vue
<template>
<BasicTable
title="表格列表"
:columns="columns"
:api="loadDataTable"
:row-key="row => row.id"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary">添加会员</n-button>
</template>
</BasicTable>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import { BasicTable } from '@/components/Table'
import { getTableList } from '@/api/table/list'
const columns = [
{
title: 'id',
key: 'id'
},
{
title: '名称',
key: 'name'
},
{
title: '地址',
key: 'address',
auth: ['amdin'], // 同时根据权限控制是否显示
ifShow: (row) => {
return true; // 根据业务控制是否显示
},
},
{
title: '日期',
key: 'date'
},
]
export default defineComponent({
components: { BasicTable },
setup() {
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data
}
return {
columns,
loadDataTable
}
}
})
</script>
```
API
----
BasicTable 在 NaiveUi 的 data-table 上进行了一层封装,支持了一些预设,并且封装了一些行为。这里只列出与 data-table 不同的 api。
> requestPromise 参考上面例子写法
> ref可绑定ref 调用组件内部方法data-table本身的方法和参数
Methods
----
> reloadactionRef.value.reload()
> 其余方法,请打印查看
Slots
----
> 名称tableTitle | 表格顶部左侧区域
> 名称toolbar | 表格顶部右侧区域
更新时间
----
该文档最后更新于: 2021-07-12 PM 10:13

View File

@@ -59,6 +59,7 @@
</div> </div>
<div class="s-table"> <div class="s-table">
<n-data-table <n-data-table
ref="tableElRef"
v-bind="getBindValues" v-bind="getBindValues"
:pagination="pagination" :pagination="pagination"
@update:page="updatePage" @update:page="updatePage"
@@ -73,7 +74,17 @@
<script lang="ts"> <script lang="ts">
import { NDataTable } from 'naive-ui'; import { NDataTable } from 'naive-ui';
import { ref, defineComponent, reactive, unref, toRaw, computed, toRefs } from 'vue'; import {
ref,
defineComponent,
reactive,
unref,
toRaw,
computed,
toRefs,
onMounted,
nextTick,
} from 'vue';
import { ReloadOutlined, ColumnHeightOutlined, QuestionCircleOutlined } from '@vicons/antd'; import { ReloadOutlined, ColumnHeightOutlined, QuestionCircleOutlined } from '@vicons/antd';
import { createTableContext } from './hooks/useTableContext'; import { createTableContext } from './hooks/useTableContext';
@@ -88,6 +99,10 @@
import { BasicTableProps } from './types/table'; import { BasicTableProps } from './types/table';
import { getViewportOffset } from '@/utils/domUtils';
import { useWindowSizeFn } from '@/hooks/event/useWindowSizeFn';
import { isBoolean } from '@/utils/is';
const densityOptions = [ const densityOptions = [
{ {
type: 'menu', type: 'menu',
@@ -117,9 +132,20 @@
...NDataTable.props, // 这里继承原 UI 组件的 props ...NDataTable.props, // 这里继承原 UI 组件的 props
...basicProps, ...basicProps,
}, },
emits: ['fetch-success', 'fetch-error', 'update:checked-row-keys'], emits: [
'fetch-success',
'fetch-error',
'update:checked-row-keys',
'edit-end',
'edit-cancel',
'edit-row-end',
'edit-change',
],
setup(props, { emit }) { setup(props, { emit }) {
const deviceHeight = ref(150);
const tableElRef = ref<ComponentRef>(null);
const wrapRef = ref<Nullable<HTMLDivElement>>(null); const wrapRef = ref<Nullable<HTMLDivElement>>(null);
let paginationEl: HTMLElement | null;
const tableData = ref<Recordable[]>([]); const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>(); const innerPropsRef = ref<Partial<BasicTableProps>>();
@@ -173,20 +199,14 @@
emit('update:checked-row-keys', rowKeys); emit('update:checked-row-keys', rowKeys);
} }
//重置 Columns
const resetColumns = () => {
columns.map((item) => {
item.isShow = true;
});
};
//获取表格大小 //获取表格大小
const getTableSize = computed(() => state.tableSize); const getTableSize = computed(() => state.tableSize);
//组装表格信息 //组装表格信息
const getBindValues = computed(() => { const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef); const tableData = unref(getDataSourceRef);
let propsData = { const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : 'auto';
return {
...unref(getProps), ...unref(getProps),
loading: unref(getLoading), loading: unref(getLoading),
columns: toRaw(unref(getPageColumns)), columns: toRaw(unref(getPageColumns)),
@@ -194,8 +214,8 @@
data: tableData, data: tableData,
size: unref(getTableSize), size: unref(getTableSize),
remote: true, remote: true,
'max-height': maxHeight,
}; };
return propsData;
}); });
//获取分页信息 //获取分页信息
@@ -205,7 +225,7 @@
innerPropsRef.value = { ...unref(innerPropsRef), ...props }; innerPropsRef.value = { ...unref(innerPropsRef), ...props };
} }
const tableAction: TableActionType = { const tableAction = {
reload, reload,
setColumns, setColumns,
setLoading, setLoading,
@@ -216,14 +236,54 @@
setCacheColumnsField, setCacheColumnsField,
emit, emit,
getSize: () => { getSize: () => {
return unref(getBindValues).size as SizeType; return unref(getBindValues).size;
}, },
}; };
const getCanResize = computed(() => {
const { canResize } = unref(getProps);
return canResize;
});
async function computeTableHeight() {
const table = unref(tableElRef);
if (!table) return;
if (!unref(getCanResize)) return;
const tableEl: any = table?.$el;
const headEl = tableEl.querySelector('.n-data-table-thead ');
const { bottomIncludeBody } = getViewportOffset(headEl);
const headerH = 64;
let paginationH = 2;
let marginH = 24;
if (!isBoolean(pagination)) {
paginationEl = tableEl.querySelector('.n-data-table__pagination') as HTMLElement;
if (paginationEl) {
const offsetHeight = paginationEl.offsetHeight;
paginationH += offsetHeight || 0;
} else {
paginationH += 28;
}
}
let height =
bottomIncludeBody - (headerH + paginationH + marginH + (props.resizeHeightOffset || 0));
const maxHeight = props.maxHeight;
height = maxHeight && maxHeight < height ? maxHeight : height;
deviceHeight.value = height;
}
useWindowSizeFn(computeTableHeight, 280);
onMounted(() => {
nextTick(() => {
computeTableHeight();
});
});
createTableContext({ ...tableAction, wrapRef, getBindValues }); createTableContext({ ...tableAction, wrapRef, getBindValues });
return { return {
...toRefs(state), ...toRefs(state),
tableElRef,
getBindValues, getBindValues,
densityOptions, densityOptions,
reload, reload,
@@ -232,7 +292,6 @@
updatePageSize, updatePageSize,
updateCheckedRowKeys, updateCheckedRowKeys,
pagination, pagination,
resetColumns,
tableAction, tableAction,
}; };
}, },

View File

@@ -0,0 +1,41 @@
import type { Component } from 'vue';
import {
NInput,
NSelect,
NCheckbox,
NInputNumber,
NSwitch,
NDatePicker,
NTimePicker,
} from 'naive-ui';
import type { ComponentType } from './types/componentType';
export enum EventEnum {
NInput = 'on-input',
NInputNumber = 'on-input',
NSelect = 'on-update:value',
NSwitch = 'on-update:value',
NCheckbox = 'on-update:value',
NDatePicker = 'on-update:value',
NTimePicker = 'on-update:value',
}
const componentMap = new Map<ComponentType, Component>();
componentMap.set('NInput', NInput);
componentMap.set('NInputNumber', NInputNumber);
componentMap.set('NSelect', NSelect);
componentMap.set('NSwitch', NSwitch);
componentMap.set('NCheckbox', NCheckbox);
componentMap.set('NDatePicker', NDatePicker);
componentMap.set('NTimePicker', NTimePicker);
export function add(compName: ComponentType, component: Component) {
componentMap.set(compName, component);
}
export function del(compName: ComponentType) {
componentMap.delete(compName);
}
export { componentMap };

View File

@@ -41,6 +41,7 @@
actions: { actions: {
type: Array as PropType<ActionItem[]>, type: Array as PropType<ActionItem[]>,
default: null, default: null,
required: true,
}, },
dropDownActions: { dropDownActions: {
type: Array as PropType<ActionItem[]>, type: Array as PropType<ActionItem[]>,

View File

@@ -0,0 +1,47 @@
import type { FunctionalComponent, defineComponent } from 'vue';
import type { ComponentType } from '../../types/componentType';
import { componentMap } from '@/components/Table/src/componentMap';
import { h } from 'vue';
import { NPopover } from 'naive-ui';
export interface ComponentProps {
component: ComponentType;
rule: boolean;
popoverVisible: boolean;
ruleMessage: string;
}
export const CellComponent: FunctionalComponent = (
{ component = 'NInput', rule = true, ruleMessage, popoverVisible }: ComponentProps,
{ attrs }
) => {
const Comp = componentMap.get(component) as typeof defineComponent;
const DefaultComp = h(Comp, attrs);
if (!rule) {
return DefaultComp;
}
return h(
NPopover,
{ 'display-directive': 'show', show: !!popoverVisible, manual: 'manual' },
{
trigger: () => DefaultComp,
default: () =>
h(
'span',
{
style: {
color: 'red',
width: '90px',
display: 'inline-block',
},
},
{
default: () => ruleMessage,
}
),
}
);
};

View File

@@ -0,0 +1,402 @@
<template>
<div class="editable-cell">
<div v-show="!isEdit" class="editable-cell-content" @click="handleEdit">
{{ getValues }}
<n-icon class="edit-icon" v-if="!column.editRow">
<FormOutlined />
</n-icon>
</div>
<div class="flex editable-cell-content" v-show="isEdit" v-click-outside="onClickOutside">
<CellComponent
v-bind="getComponentProps"
:component="getComponent"
:style="getWrapperStyle"
:popoverVisible="getRuleVisible"
:ruleMessage="ruleMessage"
:rule="getRule"
:class="getWrapperClass"
ref="elRef"
@options-change="handleOptionsChange"
@pressEnter="handleEnter"
/>
<div class="editable-cell-action" v-if="!getRowEditable">
<n-icon class="cursor-pointer mx-2">
<CheckOutlined @click="handleSubmit" />
</n-icon>
<n-icon class="cursor-pointer mx-2">
<CloseOutlined @click="handleCancel" />
</n-icon>
</div>
</div>
</div>
</template>
<script lang="ts">
import type { CSSProperties, PropType } from 'vue';
import type { BasicColumn } from '../../types/table';
import type { EditRecordRow } from './index';
import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
import { FormOutlined, CloseOutlined, CheckOutlined } from '@vicons/antd';
import { CellComponent } from './CellComponent';
import { useTableContext } from '../../hooks/useTableContext';
import clickOutside from '@/directives/clickOutside';
import { propTypes } from '@/utils/propTypes';
import { isString, isBoolean, isFunction, isNumber, isArray } from '@/utils/is';
import { createPlaceholderMessage } from './helper';
import { set, omit } from 'lodash-es';
import { EventEnum } from '@/components/Table/src/componentMap';
import dayjs from 'dayjs';
export default defineComponent({
name: 'EditableCell',
components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
directives: {
clickOutside,
},
props: {
value: {
type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
default: '',
},
record: {
type: Object as PropType<EditRecordRow>,
},
column: {
type: Object as PropType<BasicColumn>,
default: () => ({}),
},
index: propTypes.number,
},
setup(props) {
const table = useTableContext();
const isEdit = ref(false);
const elRef = ref();
const ruleVisible = ref(false);
const ruleMessage = ref('');
const optionsRef = ref<LabelValueOptions>([]);
const currentValueRef = ref<any>(props.value);
const defaultValueRef = ref<any>(props.value);
// const { prefixCls } = useDesign('editable-cell');
const getComponent = computed(() => props.column?.editComponent || 'NInput');
const getRule = computed(() => props.column?.editRule);
const getRuleVisible = computed(() => {
return unref(ruleMessage) && unref(ruleVisible);
});
const getIsCheckComp = computed(() => {
const component = unref(getComponent);
return ['NCheckbox', 'NSwitch'].includes(component);
});
const getComponentProps = computed(() => {
const compProps = props.column?.editComponentProps ?? {};
const editComponent = props.column?.editComponent ?? null;
const component = unref(getComponent);
const apiSelectProps: Recordable = {};
const isCheckValue = unref(getIsCheckComp);
const valueField = isCheckValue ? 'checked' : 'value';
const val = unref(currentValueRef);
let value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
if (component === 'NDatePicker') {
value = dayjs(value).valueOf();
}
const onEvent: any = editComponent ? EventEnum[editComponent] : undefined;
return {
placeholder: createPlaceholderMessage(unref(getComponent)),
...apiSelectProps,
...omit(compProps, 'onChange'),
[onEvent]: handleChange,
[valueField]: value,
};
});
const getValues = computed(() => {
const { editComponentProps, editValueMap } = props.column;
const value = unref(currentValueRef);
if (editValueMap && isFunction(editValueMap)) {
return editValueMap(value);
}
const component = unref(getComponent);
if (!component.includes('NSelect')) {
return value;
}
const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
const option = options.find((item) => `${item.value}` === `${value}`);
return option?.label ?? value;
});
const getWrapperStyle = computed((): CSSProperties => {
// if (unref(getIsCheckComp) || unref(getRowEditable)) {
// return {};
// }
return {
width: 'calc(100% - 48px)',
};
});
const getWrapperClass = computed(() => {
const { align = 'center' } = props.column;
return `edit-cell-align-${align}`;
});
const getRowEditable = computed(() => {
const { editable } = props.record || {};
return !!editable;
});
watchEffect(() => {
defaultValueRef.value = props.value;
});
watchEffect(() => {
const { editable } = props.column;
if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
isEdit.value = !!editable || unref(getRowEditable);
}
});
function handleEdit() {
if (unref(getRowEditable) || unref(props.column?.editRow)) return;
ruleMessage.value = '';
isEdit.value = true;
nextTick(() => {
const el = unref(elRef);
el?.focus?.();
});
}
async function handleChange(e: any) {
const component = unref(getComponent);
if (!e) {
currentValueRef.value = e;
} else if (e?.target && Reflect.has(e.target, 'value')) {
currentValueRef.value = (e as ChangeEvent).target.value;
} else if (component === 'NCheckbox') {
currentValueRef.value = (e as ChangeEvent).target.checked;
} else if (isString(e) || isBoolean(e) || isNumber(e)) {
currentValueRef.value = e;
}
//TODO 这里组件参数格式和dayjs格式不一致
if (component === 'NDatePicker') {
let format = (props.column.editComponentProps?.format)
.replace(/yyyy/g, 'YYYY')
.replace(/dd/g, 'DD');
currentValueRef.value = dayjs(currentValueRef.value).format(format);
}
const onChange = props.column?.editComponentProps?.onChange;
if (onChange && isFunction(onChange)) onChange(...arguments);
table.emit?.('edit-change', {
column: props.column,
value: unref(currentValueRef),
record: toRaw(props.record),
});
await handleSubmiRule();
}
async function handleSubmiRule() {
const { column, record } = props;
const { editRule } = column;
const currentValue = unref(currentValueRef);
if (editRule) {
if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
ruleVisible.value = true;
const component = unref(getComponent);
ruleMessage.value = createPlaceholderMessage(component);
return false;
}
if (isFunction(editRule)) {
const res = await editRule(currentValue, record as Recordable);
if (!!res) {
ruleMessage.value = res;
ruleVisible.value = true;
return false;
} else {
ruleMessage.value = '';
return true;
}
}
}
ruleMessage.value = '';
return true;
}
async function handleSubmit(needEmit = true, valid = true) {
if (valid) {
const isPass = await handleSubmiRule();
if (!isPass) return false;
}
const { column, index, record } = props;
if (!record) return false;
const { key } = column;
const value = unref(currentValueRef);
if (!key) return;
const dataKey = key as string;
set(record, dataKey, value);
//const record = await table.updateTableData(index, dataKey, value);
needEmit && table.emit?.('edit-end', { record, index, key, value });
isEdit.value = false;
}
async function handleEnter() {
if (props.column?.editRow) {
return;
}
await handleSubmit();
}
function handleCancel() {
isEdit.value = false;
currentValueRef.value = defaultValueRef.value;
const { column, index, record } = props;
const { key } = column;
ruleVisible.value = true;
ruleMessage.value = '';
table.emit?.('edit-cancel', {
record,
index,
key: key,
value: unref(currentValueRef),
});
}
function onClickOutside() {
if (props.column?.editable || unref(getRowEditable)) {
return;
}
const component = unref(getComponent);
if (component.includes('NInput')) {
handleCancel();
}
}
// only ApiSelect
function handleOptionsChange(options: LabelValueOptions) {
optionsRef.value = options;
}
function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
if (props.record) {
/* eslint-disable */
isArray(props.record[cbs])
? props.record[cbs]?.push(handle)
: (props.record[cbs] = [handle]);
}
}
if (props.record) {
initCbs('submitCbs', handleSubmit);
initCbs('validCbs', handleSubmiRule);
initCbs('cancelCbs', handleCancel);
if (props.column.key) {
if (!props.record.editValueRefs) props.record.editValueRefs = {};
props.record.editValueRefs[props.column.key] = currentValueRef;
}
/* eslint-disable */
props.record.onCancelEdit = () => {
isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
};
/* eslint-disable */
props.record.onSubmitEdit = async() => {
if (isArray(props.record?.submitCbs)) {
const validFns = (props.record?.validCbs || []).map((fn) => fn());
const res = await Promise.all(validFns);
const pass = res.every((item) => !!item);
if (!pass) return;
const submitFns = props.record?.submitCbs || [];
submitFns.forEach((fn) => fn(false, false));
table.emit?.('edit-row-end');
return true;
}
};
}
return {
isEdit,
handleEdit,
currentValueRef,
handleSubmit,
handleChange,
handleCancel,
elRef,
getComponent,
getRule,
onClickOutside,
ruleMessage,
getRuleVisible,
getComponentProps,
handleOptionsChange,
getWrapperStyle,
getWrapperClass,
getRowEditable,
getValues,
handleEnter,
// getSize,
};
},
});
</script>
<style lang="less">
.editable-cell {
&-content {
position: relative;
overflow-wrap: break-word;
word-break: break-word;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
.edit-icon {
font-size: 14px;
//position: absolute;
//top: 4px;
//right: 0;
display: none;
width: 20px;
cursor: pointer;
}
&:hover {
.edit-icon {
display: inline-block;
}
}
}
&-action {
display: flex;
align-items: center;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,15 @@
import { ComponentType } from '../../types/componentType';
/**
* @description: 生成placeholder
*/
export function createPlaceholderMessage(component: ComponentType) {
if (component === 'NInput') return '请输入';
if (
['NPicker', 'NSelect', 'NCheckbox', 'NRadio', 'NSwitch', 'NDatePicker', 'NTimePicker'].includes(
component
)
)
return '请选择';
return '';
}

View File

@@ -0,0 +1,49 @@
import type { BasicColumn } from '@/components/Table/src/types/table';
import { h, Ref } from 'vue';
import EditableCell from './EditableCell.vue';
export function renderEditCell(column: BasicColumn) {
return (record, index) => {
const _key = column.key;
const value = record[_key];
record.onEdit = async (edit: boolean, submit = false) => {
if (!submit) {
record.editable = edit;
}
if (!edit && submit) {
const res = await record.onSubmitEdit?.();
if (res) {
record.editable = false;
return true;
}
return false;
}
// cancel
if (!edit && !submit) {
record.onCancelEdit?.();
}
return true;
};
return h(EditableCell, {
value,
record,
column,
index,
});
};
}
export type EditRecordRow<T = Recordable> = Partial<
{
onEdit: (editable: boolean, submit?: boolean) => Promise<boolean>;
editable: boolean;
onCancel: Fn;
onSubmit: Fn;
submitCbs: Fn[];
cancelCbs: Fn[];
validCbs: Fn[];
editValueRefs: Recordable<Ref>;
} & T
>;

View File

@@ -158,7 +158,7 @@
table.getColumns().forEach((item) => { table.getColumns().forEach((item) => {
newRet.push({ ...item }); newRet.push({ ...item });
}); });
return newRet; return newRet.filter((item) => item.key != 'action' && item.title != '操作');
} }
//重置 //重置

View File

@@ -1,9 +1,12 @@
import { ref, Ref, ComputedRef, unref, computed, watch, toRaw } from 'vue'; import { ref, Ref, ComputedRef, unref, computed, watch, toRaw, h } from 'vue';
import type { BasicColumn, BasicTableProps } from '../types/table'; import type { BasicColumn, BasicTableProps } from '../types/table';
import { isEqual, cloneDeep } from 'lodash-es'; import { isEqual, cloneDeep } from 'lodash-es';
import { isArray, isString, isBoolean, isFunction } from '@/utils/is'; import { isArray, isString, isBoolean, isFunction } from '@/utils/is';
import { usePermission } from '@/hooks/web/usePermission'; import { usePermission } from '@/hooks/web/usePermission';
import { ActionItem } from '@/components/Table'; import { ActionItem } from '@/components/Table';
import { renderEditCell } from '../components/editable';
import { NTooltip, NIcon } from 'naive-ui';
import { FormOutlined } from '@vicons/antd';
export function useColumns(propsRef: ComputedRef<BasicTableProps>) { export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>; const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
@@ -33,13 +36,48 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
return isIfShow; return isIfShow;
} }
const renderTooltip = (trigger, content) => {
return h(NTooltip, null, {
trigger: () => trigger,
default: () => content,
});
};
const getPageColumns = computed(() => { const getPageColumns = computed(() => {
const pageColumns = unref(getColumnsRef); const pageColumns = unref(getColumnsRef);
const columns = cloneDeep(pageColumns); const columns = cloneDeep(pageColumns);
return columns.filter((column) => { return columns
// @ts-ignore .filter((column) => {
return hasPermission(column.auth) && isIfShow(column); // @ts-ignore
}); return hasPermission(column.auth) && isIfShow(column);
})
.map((column) => {
const { edit, editRow } = column;
if (edit) {
column.render = renderEditCell(column);
if (edit) {
const title: any = column.title;
column.title = () => {
return renderTooltip(
h('span', {}, [
h('span', { style: { 'margin-right': '5px' } }, title),
h(
NIcon,
{
size: 14,
},
{
default: () => h(FormOutlined),
}
),
]),
'该列可编辑'
);
};
}
}
return column;
});
}); });
watch( watch(

View File

@@ -1,4 +1,5 @@
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { propTypes } from '@/utils/propTypes';
import { BasicColumn } from './types/table'; import { BasicColumn } from './types/table';
export const basicProps = { export const basicProps = {
@@ -16,7 +17,7 @@ export const basicProps = {
}, },
tableData: { tableData: {
type: [Object], type: [Object],
default: () => {}, default: () => [],
}, },
columns: { columns: {
type: [Array] as PropType<BasicColumn[]>, type: [Array] as PropType<BasicColumn[]>,
@@ -36,6 +37,7 @@ export const basicProps = {
type: [Object, Boolean], type: [Object, Boolean],
default: () => {}, default: () => {},
}, },
//废弃
showPagination: { showPagination: {
type: [String, Boolean], type: [String, Boolean],
default: 'auto', default: 'auto',
@@ -44,4 +46,6 @@ export const basicProps = {
type: Object as PropType<BasicColumn>, type: Object as PropType<BasicColumn>,
default: null, default: null,
}, },
canResize: propTypes.bool.def(true),
resizeHeightOffset: propTypes.number.def(0),
}; };

View File

@@ -0,0 +1,8 @@
export type ComponentType =
| 'NInput'
| 'NInputNumber'
| 'NSelect'
| 'NCheckbox'
| 'NSwitch'
| 'NDatePicker'
| 'NTimePicker';

View File

@@ -1,6 +1,20 @@
import type { TableBaseColumn } from 'naive-ui/lib/data-table/src/interface'; import type { TableBaseColumn } from 'naive-ui/lib/data-table/src/interface';
import { ComponentType } from './componentType';
export type BasicColumn = TableBaseColumn; export interface BasicColumn extends TableBaseColumn {
//编辑表格
edit?: boolean;
editRow?: boolean;
editable?: boolean;
editComponent?: ComponentType;
editComponentProps?: Recordable;
editRule?: boolean | ((text: string, record: Recordable) => Promise<string>);
editValueMap?: (value: any) => string;
onEditRow?: () => void;
// 权限编码控制是否显示
auth?: RoleEnum | RoleEnum[] | string | string[];
// 业务控制是否显示
ifShow?: boolean | ((column: BasicColumn) => boolean);
}
export interface TableActionType { export interface TableActionType {
reload: (opt) => Promise<void>; reload: (opt) => Promise<void>;
@@ -16,4 +30,6 @@ export interface BasicTableProps {
pagination: object; pagination: object;
showPagination: boolean; showPagination: boolean;
actionColumn: any[]; actionColumn: any[];
canResize: boolean;
resizeHeightOffset: number;
} }

View File

@@ -0,0 +1,86 @@
import { on } from '@/utils/domUtils';
import { isServer } from '@/utils/is';
import type { ComponentPublicInstance, DirectiveBinding, ObjectDirective } from 'vue';
type DocumentHandler = <T extends MouseEvent>(mouseup: T, mousedown: T) => void;
type FlushList = Map<
HTMLElement,
{
documentHandler: DocumentHandler;
bindingFn: (...args: unknown[]) => unknown;
}
>;
const nodeList: FlushList = new Map();
let startClick: MouseEvent;
if (!isServer) {
on(document, 'mousedown', (e: MouseEvent) => (startClick = e));
on(document, 'mouseup', (e: MouseEvent) => {
for (const { documentHandler } of nodeList.values()) {
documentHandler(e, startClick);
}
});
}
function createDocumentHandler(el: HTMLElement, binding: DirectiveBinding): DocumentHandler {
let excludes: HTMLElement[] = [];
if (Array.isArray(binding.arg)) {
excludes = binding.arg;
} else {
// due to current implementation on binding type is wrong the type casting is necessary here
excludes.push(binding.arg as unknown as HTMLElement);
}
return function (mouseup, mousedown) {
const popperRef = (
binding.instance as ComponentPublicInstance<{
popperRef: Nullable<HTMLElement>;
}>
).popperRef;
const mouseUpTarget = mouseup.target as Node;
const mouseDownTarget = mousedown.target as Node;
const isBound = !binding || !binding.instance;
const isTargetExists = !mouseUpTarget || !mouseDownTarget;
const isContainedByEl = el.contains(mouseUpTarget) || el.contains(mouseDownTarget);
const isSelf = el === mouseUpTarget;
const isTargetExcluded =
(excludes.length && excludes.some((item) => item?.contains(mouseUpTarget))) ||
(excludes.length && excludes.includes(mouseDownTarget as HTMLElement));
const isContainedByPopper =
popperRef && (popperRef.contains(mouseUpTarget) || popperRef.contains(mouseDownTarget));
if (
isBound ||
isTargetExists ||
isContainedByEl ||
isSelf ||
isTargetExcluded ||
isContainedByPopper
) {
return;
}
binding.value();
};
}
const ClickOutside: ObjectDirective = {
beforeMount(el, binding) {
nodeList.set(el, {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
});
},
updated(el, binding) {
nodeList.set(el, {
documentHandler: createDocumentHandler(el, binding),
bindingFn: binding.value,
});
},
unmounted(el) {
nodeList.delete(el);
},
};
export default ClickOutside;

View File

@@ -0,0 +1,36 @@
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core';
import { useDebounceFn } from '@vueuse/core';
interface WindowSizeOptions {
once?: boolean;
immediate?: boolean;
listenerOptions?: AddEventListenerOptions | boolean;
}
export function useWindowSizeFn<T>(fn: Fn<T>, wait = 150, options?: WindowSizeOptions) {
let handler = () => {
fn();
};
const handleSize = useDebounceFn(handler, wait);
handler = handleSize;
const start = () => {
if (options && options.immediate) {
handler();
}
window.addEventListener('resize', handler);
};
const stop = () => {
window.removeEventListener('resize', handler);
};
tryOnMounted(() => {
start();
});
tryOnUnmounted(() => {
stop();
});
return [start, stop];
}

View File

@@ -23,7 +23,7 @@
<style lang="less" scoped> <style lang="less" scoped>
.page-footer { .page-footer {
margin: 48px 0 24px 0; //margin: 28px 0 24px 0;
padding: 0 16px; padding: 0 16px;
text-align: center; text-align: center;

View File

@@ -7,7 +7,18 @@
<div class="drawer-setting-item justify-center dark-switch"> <div class="drawer-setting-item justify-center dark-switch">
<n-tooltip placement="bottom"> <n-tooltip placement="bottom">
<template #trigger> <template #trigger>
<n-switch v-model:value="designStore.darkTheme" /> <n-switch v-model:value="designStore.darkTheme" class="dark-theme-switch">
<template #checked>
<n-icon size="14" color="#ffd93b">
<SunnySharp />
</n-icon>
</template>
<template #unchecked>
<n-icon size="14" color="#ffd93b">
<Moon />
</n-icon>
</template>
</n-switch>
</template> </template>
<span>深色主题</span> <span>深色主题</span>
</n-tooltip> </n-tooltip>
@@ -146,13 +157,13 @@
<n-switch v-model:value="settingStore.multiTabsSetting.show" /> <n-switch v-model:value="settingStore.multiTabsSetting.show" />
</div> </div>
</div> </div>
<!--1.15废弃没啥用占用操作空间-->
<div class="drawer-setting-item"> <!-- <div class="drawer-setting-item">-->
<div class="drawer-setting-item-title"> 显示页脚 </div> <!-- <div class="drawer-setting-item-title"> 显示页脚 </div>-->
<div class="drawer-setting-item-action"> <!-- <div class="drawer-setting-item-action">-->
<n-switch v-model:value="settingStore.showFooter" /> <!-- <n-switch v-model:value="settingStore.showFooter" />-->
</div> <!-- </div>-->
</div> <!-- </div>-->
<div class="drawer-setting-item"> <div class="drawer-setting-item">
<n-alert type="warning" :showIcon="false"> <n-alert type="warning" :showIcon="false">
@@ -169,11 +180,12 @@
import { useProjectSettingStore } from '@/store/modules/projectSetting'; import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useDesignSettingStore } from '@/store/modules/designSetting'; import { useDesignSettingStore } from '@/store/modules/designSetting';
import { CheckOutlined } from '@vicons/antd'; import { CheckOutlined } from '@vicons/antd';
import { Moon, SunnySharp } from '@vicons/ionicons5';
import { darkTheme } from 'naive-ui'; import { darkTheme } from 'naive-ui';
export default defineComponent({ export default defineComponent({
name: 'ProjectSetting', name: 'ProjectSetting',
components: { CheckOutlined }, components: { CheckOutlined, Moon, SunnySharp },
props: { props: {
title: { title: {
type: String, type: String,
@@ -300,8 +312,7 @@
.justify-center { .justify-center {
justify-content: center; justify-content: center;
} }
.dark-switch .n-switch {
.dark-switch .n-switch--active {
::v-deep(.n-switch__rail) { ::v-deep(.n-switch__rail) {
background-color: #000e1c; background-color: #000e1c;
} }

View File

@@ -86,7 +86,7 @@
<div class="layout-header-trigger layout-header-trigger-min"> <div class="layout-header-trigger layout-header-trigger-min">
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions"> <n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
<div class="avatar"> <div class="avatar">
<n-avatar> <n-avatar round>
{{ username }} {{ username }}
<template #icon> <template #icon>
<UserOutlined /> <UserOutlined />

View File

@@ -6,6 +6,7 @@
:collapsed="collapsed" :collapsed="collapsed"
:collapsed-width="64" :collapsed-width="64"
:collapsed-icon-size="20" :collapsed-icon-size="20"
:indent="28"
:expanded-keys="openKeys" :expanded-keys="openKeys"
v-model:value="selectedKeys" v-model:value="selectedKeys"
@update:value="clickMenuItem" @update:value="clickMenuItem"
@@ -14,7 +15,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, reactive, computed, watch, toRefs } from 'vue'; import { defineComponent, reactive, computed, watch, toRefs, unref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useAsyncRouteStore } from '@/store/modules/asyncRoute'; import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
import { generatorMenu } from '@/utils/index'; import { generatorMenu } from '@/utils/index';
@@ -44,7 +45,7 @@
// 获取当前打开的子菜单 // 获取当前打开的子菜单
const matched = currentRoute.matched; const matched = currentRoute.matched;
const getOpenKeys = matched && matched.length ? [matched[0]?.name] : []; const getOpenKeys = matched && matched.length ? matched.map((item) => item.name) : [];
const state = reactive({ const state = reactive({
openKeys: getOpenKeys, openKeys: getOpenKeys,
@@ -73,8 +74,7 @@
() => currentRoute.fullPath, () => currentRoute.fullPath,
() => { () => {
const matched = currentRoute.matched; const matched = currentRoute.matched;
const getOpenKeys = matched && matched.length ? [matched[0]?.name] : []; state.openKeys = matched.map((item) => item.name);
state.openKeys = getOpenKeys;
state.selectedKeys = currentRoute.name; state.selectedKeys = currentRoute.name;
} }
); );
@@ -90,10 +90,22 @@
//展开菜单 //展开菜单
function menuExpanded(openKeys: string[]) { function menuExpanded(openKeys: string[]) {
console.log(openKeys);
if (!openKeys) return; if (!openKeys) return;
const latestOpenKey = openKeys.pop(); const latestOpenKey = openKeys.find((key) => state.openKeys.indexOf(key) === -1);
state.openKeys = latestOpenKey ? [latestOpenKey] : []; const isExistChildren = findChildrenLen(latestOpenKey as string);
state.openKeys = isExistChildren ? (latestOpenKey ? [latestOpenKey] : []) : openKeys;
}
//查找是否存在子路由
function findChildrenLen(key: string) {
if (!key) return false;
const subRouteChildren: string[] = [];
for (const { children, key } of unref(menus)) {
if (children && children.length > 0) {
subRouteChildren.push(key as string);
}
}
return subRouteChildren.includes(key);
} }
return { return {

View File

@@ -46,10 +46,10 @@
<MainView /> <MainView />
</div> </div>
</div> </div>
<!--1.15废弃没啥用占用操作空间-->
<NLayoutFooter v-if="getShowFooter"> <!-- <NLayoutFooter v-if="getShowFooter">-->
<PageFooter /> <!-- <PageFooter />-->
</NLayoutFooter> <!-- </NLayoutFooter>-->
</NLayoutContent> </NLayoutContent>
</NLayout> </NLayout>
</NLayout> </NLayout>

View File

@@ -0,0 +1,3 @@
<template>
<router-view />
</template>

View File

@@ -5,9 +5,12 @@ import router, { setupRouter } from './router';
import { setupStore } from '@/store'; import { setupStore } from '@/store';
import MakeitCaptcha from 'makeit-captcha'; import MakeitCaptcha from 'makeit-captcha';
import 'makeit-captcha/dist/captcha.min.css'; import 'makeit-captcha/dist/captcha.min.css';
import { setupNaive, setupDirectives, setupGlobalMethods, setupCustomComponents } from '@/plugins'; import { setupNaive, setupDirectives } from '@/plugins';
import { AppProvider } from '@/components/Application';
async function bootstrap() { async function bootstrap() {
const appProvider = createApp(AppProvider);
const app = createApp(App); const app = createApp(App);
app.use(MakeitCaptcha); app.use(MakeitCaptcha);
@@ -15,18 +18,21 @@ async function bootstrap() {
// 注册全局常用的 naive-ui 组件 // 注册全局常用的 naive-ui 组件
setupNaive(app); setupNaive(app);
// 注册全局自定义组件,如:<svg-icon /> // 注册全局自定义组件
setupCustomComponents(app); //setupCustomComponents();
// 注册全局自定义指令v-permission权限指令 // 注册全局自定义指令v-permission权限指令
setupDirectives(app); setupDirectives(app);
// 注册全局方法app.config.globalProperties.$message = message // 注册全局方法app.config.globalProperties.$message = message
setupGlobalMethods(app); //setupGlobalMethods(app);
// 挂载状态管理 // 挂载状态管理
setupStore(app); setupStore(app);
//优先挂载一下 Provider 解决路由守卫Axios中可使用DialogMessage 等之类组件
appProvider.mount('#appProvider', true);
// 挂载路由 // 挂载路由
await setupRouter(app); await setupRouter(app);

View File

@@ -3,3 +3,5 @@ export const RedirectName = 'Redirect';
export const ErrorPage = () => import('@/views/exception/404.vue'); export const ErrorPage = () => import('@/views/exception/404.vue');
export const Layout = () => import('@/layout/index.vue'); export const Layout = () => import('@/layout/index.vue');
export const ParentLayout = () => import('@/layout/parentLayout.vue');

View File

@@ -1,5 +1,5 @@
import { RouteRecordRaw } from 'vue-router'; import { RouteRecordRaw } from 'vue-router';
import { Layout } from '@/router/constant'; import { Layout, ParentLayout } from '@/router/constant';
import { WalletOutlined } from '@vicons/antd'; import { WalletOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index'; import { renderIcon } from '@/utils/index';
@@ -20,8 +20,8 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: '/comp', path: '/comp',
name: routeName, name: routeName,
redirect: '/comp/console',
component: Layout, component: Layout,
redirect: '/comp/table',
meta: { meta: {
title: '组件示例', title: '组件示例',
icon: renderIcon(WalletOutlined), icon: renderIcon(WalletOutlined),
@@ -31,10 +31,37 @@ const routes: Array<RouteRecordRaw> = [
{ {
path: 'table', path: 'table',
name: `${routeName}_table`, name: `${routeName}_table`,
redirect: '/comp/table/basic',
component: ParentLayout,
meta: { meta: {
title: '表格', title: '表格',
}, },
component: () => import('@/views/comp/table/list.vue'), children: [
{
path: 'basic',
name: `${routeName}_table_basic`,
meta: {
title: '基础表格',
},
component: () => import('@/views/comp/table/basic.vue'),
},
{
path: 'editCell',
name: `${routeName}_table_editCell`,
meta: {
title: '单元格编辑',
},
component: () => import('@/views/comp/table/editCell.vue'),
},
{
path: 'editRow',
name: `${routeName}_table_editRow`,
meta: {
title: '整行编辑',
},
component: () => import('@/views/comp/table/editRow.vue'),
},
],
}, },
{ {
path: 'upload', path: 'upload',

View File

@@ -1,13 +1,10 @@
import { isNavigationFailure, Router } from 'vue-router'; import { isNavigationFailure, Router } from 'vue-router';
import { useUserStoreWidthOut } from '@/store/modules/user'; import { useUserStoreWidthOut } from '@/store/modules/user';
import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute'; import { useAsyncRouteStoreWidthOut } from '@/store/modules/asyncRoute';
import NProgress from 'nprogress'; // progress bar
import { ACCESS_TOKEN } from '@/store/mutation-types'; import { ACCESS_TOKEN } from '@/store/mutation-types';
import { storage } from '@/utils/Storage'; import { storage } from '@/utils/Storage';
import { PageEnum } from '@/enums/pageEnum'; import { PageEnum } from '@/enums/pageEnum';
NProgress.configure({ showSpinner: false }); // NProgress Configuration
const LOGIN_PATH = PageEnum.BASE_LOGIN; const LOGIN_PATH = PageEnum.BASE_LOGIN;
const whitePathList = [LOGIN_PATH]; // no redirect whitelist const whitePathList = [LOGIN_PATH]; // no redirect whitelist
@@ -15,8 +12,9 @@ const whitePathList = [LOGIN_PATH]; // no redirect whitelist
export function createRouterGuards(router: Router) { export function createRouterGuards(router: Router) {
const userStore = useUserStoreWidthOut(); const userStore = useUserStoreWidthOut();
const asyncRouteStore = useAsyncRouteStoreWidthOut(); const asyncRouteStore = useAsyncRouteStoreWidthOut();
const Loading = window['$loading'] || null;
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start(); Loading && Loading.start();
if (from.path === LOGIN_PATH && to.name === 'errorPage') { if (from.path === LOGIN_PATH && to.name === 'errorPage') {
next(PageEnum.BASE_HOME); next(PageEnum.BASE_HOME);
return; return;
@@ -70,7 +68,7 @@ export function createRouterGuards(router: Router) {
const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect }; const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect };
asyncRouteStore.setDynamicAddedRoute(true); asyncRouteStore.setDynamicAddedRoute(true);
next(nextData); next(nextData);
NProgress.done(); Loading && Loading.finish();
}); });
router.afterEach((to, _, failure) => { router.afterEach((to, _, failure) => {
@@ -93,7 +91,7 @@ export function createRouterGuards(router: Router) {
} }
} }
asyncRouteStore.setKeepAliveComponents(keepAliveComponents); asyncRouteStore.setKeepAliveComponents(keepAliveComponents);
NProgress.done(); // finish progress bar Loading && Loading.finish();
}); });
router.onError((error) => { router.onError((error) => {

View File

@@ -10,6 +10,14 @@ export const appThemeList: string[] = [
'#0096c7', '#0096c7',
'#9c27b0', '#9c27b0',
'#ff9800', '#ff9800',
'#FF3D68',
'#00C1D4',
'#71EFA3',
'#171010',
'#78DEC7',
'#1768AC',
'#FB9300',
'#FC5404',
]; ];
const setting = { const setting = {

View File

@@ -1,4 +0,0 @@
@primaryColor: #2d8cf0;
@primaryColorHover: 57 a3f3;
@header-height: 64px;
@footer-height: 70px;

View File

@@ -1,5 +0,0 @@
body {
.ant-message {
z-index: 999999;
}
}

165
src/utils/domUtils.ts Normal file
View File

@@ -0,0 +1,165 @@
import { upperFirst } from 'lodash-es';
export interface ViewportOffsetResult {
left: number;
top: number;
right: number;
bottom: number;
rightIncludeBody: number;
bottomIncludeBody: number;
}
export function getBoundingClientRect(element: Element): DOMRect | number {
if (!element || !element.getBoundingClientRect) {
return 0;
}
return element.getBoundingClientRect();
}
function trim(string: string) {
return (string || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '');
}
/* istanbul ignore next */
export function hasClass(el: Element, cls: string) {
if (!el || !cls) return false;
if (cls.indexOf(' ') !== -1) throw new Error('className should not contain space.');
if (el.classList) {
return el.classList.contains(cls);
} else {
return (' ' + el.className + ' ').indexOf(' ' + cls + ' ') > -1;
}
}
/* istanbul ignore next */
export function addClass(el: Element, cls: string) {
if (!el) return;
let curClass = el.className;
const classes = (cls || '').split(' ');
for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i];
if (!clsName) continue;
if (el.classList) {
el.classList.add(clsName);
} else if (!hasClass(el, clsName)) {
curClass += ' ' + clsName;
}
}
if (!el.classList) {
el.className = curClass;
}
}
/* istanbul ignore next */
export function removeClass(el: Element, cls: string) {
if (!el || !cls) return;
const classes = cls.split(' ');
let curClass = ' ' + el.className + ' ';
for (let i = 0, j = classes.length; i < j; i++) {
const clsName = classes[i];
if (!clsName) continue;
if (el.classList) {
el.classList.remove(clsName);
} else if (hasClass(el, clsName)) {
curClass = curClass.replace(' ' + clsName + ' ', ' ');
}
}
if (!el.classList) {
el.className = trim(curClass);
}
}
/**
* Get the left and top offset of the current element
* left: the distance between the leftmost element and the left side of the document
* top: the distance from the top of the element to the top of the document
* right: the distance from the far right of the element to the right of the document
* bottom: the distance from the bottom of the element to the bottom of the document
* rightIncludeBody: the distance between the leftmost element and the right side of the document
* bottomIncludeBody: the distance from the bottom of the element to the bottom of the document
*
* @description:
*/
export function getViewportOffset(element: Element): ViewportOffsetResult {
const doc = document.documentElement;
const docScrollLeft = doc.scrollLeft;
const docScrollTop = doc.scrollTop;
const docClientLeft = doc.clientLeft;
const docClientTop = doc.clientTop;
const pageXOffset = window.pageXOffset;
const pageYOffset = window.pageYOffset;
const box = getBoundingClientRect(element);
const { left: retLeft, top: rectTop, width: rectWidth, height: rectHeight } = box as DOMRect;
const scrollLeft = (pageXOffset || docScrollLeft) - (docClientLeft || 0);
const scrollTop = (pageYOffset || docScrollTop) - (docClientTop || 0);
const offsetLeft = retLeft + pageXOffset;
const offsetTop = rectTop + pageYOffset;
const left = offsetLeft - scrollLeft;
const top = offsetTop - scrollTop;
const clientWidth = window.document.documentElement.clientWidth;
const clientHeight = window.document.documentElement.clientHeight;
return {
left: left,
top: top,
right: clientWidth - rectWidth - left,
bottom: clientHeight - rectHeight - top,
rightIncludeBody: clientWidth - left,
bottomIncludeBody: clientHeight - top,
};
}
export function hackCss(attr: string, value: string) {
const prefix: string[] = ['webkit', 'Moz', 'ms', 'OT'];
const styleObj: any = {};
prefix.forEach((item) => {
styleObj[`${item}${upperFirst(attr)}`] = value;
});
return {
...styleObj,
[attr]: value,
};
}
/* istanbul ignore next */
export function on(
element: Element | HTMLElement | Document | Window,
event: string,
handler: EventListenerOrEventListenerObject
): void {
if (element && event && handler) {
element.addEventListener(event, handler, false);
}
}
/* istanbul ignore next */
export function off(
element: Element | HTMLElement | Document | Window,
event: string,
handler: Fn
): void {
if (element && event && handler) {
element.removeEventListener(event, handler, false);
}
}
/* istanbul ignore next */
export function once(el: HTMLElement, event: string, fn: EventListener): void {
const listener = function (this: any, ...args: unknown[]) {
if (fn) {
fn.apply(this, args);
}
off(el, event, listener);
};
on(el, event, listener);
}

View File

@@ -1,5 +1,4 @@
export function checkStatus(status: number, msg: string): void { export function checkStatus(status: number, msg: string, message: any): void {
const message = window['$message'];
switch (status) { switch (status) {
case 400: case 400:
message.error(`${msg}`); message.error(`${msg}`);

View File

@@ -1,5 +1,4 @@
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动 // axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
import { VAxios } from './Axios'; import { VAxios } from './Axios';
import { AxiosTransform } from './axiosTransform'; import { AxiosTransform } from './axiosTransform';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
@@ -18,8 +17,6 @@ import { useUserStoreWidthOut } from '@/store/modules/user';
const globSetting = useGlobSetting(); const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix || ''; const urlPrefix = globSetting.urlPrefix || '';
// @ts-ignore
const { $message: Message, $dialog: Modal } = window;
import router from '@/router'; import router from '@/router';
import { storage } from '@/utils/Storage'; import { storage } from '@/utils/Storage';
@@ -32,18 +29,35 @@ const transform: AxiosTransform = {
* @description: 处理请求数据 * @description: 处理请求数据
*/ */
transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => { transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
const { $message: Message, $dialog: Modal } = window;
const { const {
isTransformRequestResult,
isShowMessage = true, isShowMessage = true,
isShowErrorMessage, isShowErrorMessage,
isShowSuccessMessage, isShowSuccessMessage,
successMessageText, successMessageText,
errorMessageText, errorMessageText,
isTransformResponse,
isReturnNativeResponse,
} = options; } = options;
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
if (!isTransformResponse) {
return res.data;
}
const reject = Promise.reject; const reject = Promise.reject;
const { data } = res; const { data } = res;
if (!data) {
// return '[HTTP] Request has no return value';
return reject(data);
}
// 这里 coderesultmessage为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式 // 这里 coderesultmessage为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { code, result, message } = data; const { code, result, message } = data;
// 请求成功 // 请求成功
@@ -58,19 +72,14 @@ const transform: AxiosTransform = {
Message.error(message || errorMessageText || '操作失败!'); Message.error(message || errorMessageText || '操作失败!');
} else if (!hasSuccess && options.errorMessageMode === 'modal') { } else if (!hasSuccess && options.errorMessageMode === 'modal') {
// errorMessageMode=custom-modal的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误 // errorMessageMode=custom-modal的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
Modal.confirm({ title: '错误提示', content: message }); Modal.info({
title: '提示',
content: message,
positiveText: '确定',
onPositiveClick: () => {},
});
} }
} }
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
if (!isTransformRequestResult) {
return res.data;
}
if (!data) {
// return '[HTTP] Request has no return value';
return reject(data);
}
// 接口请求成功,直接返回结果 // 接口请求成功,直接返回结果
if (code === ResultEnum.SUCCESS) { if (code === ResultEnum.SUCCESS) {
@@ -94,19 +103,21 @@ const transform: AxiosTransform = {
if (router.currentRoute.value.name == 'login') return; if (router.currentRoute.value.name == 'login') return;
// 到登录页 // 到登录页
const timeoutMsg = '登录超时,请重新登录!'; const timeoutMsg = '登录超时,请重新登录!';
Modal.destroyAll();
Modal.warning({ Modal.warning({
title: '提示', title: '提示',
content: '登录身份已失效,请重新登录!', content: '登录身份已失效请重新登录!',
onOk: () => { positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
storage.clear();
router.replace({ router.replace({
name: 'login', name: 'login',
query: { query: {
redirect: router.currentRoute.value.fullPath, redirect: router.currentRoute.value.fullPath,
}, },
}); });
storage.clear();
}, },
onNegativeClick: () => {},
}); });
return reject(new Error(timeoutMsg)); return reject(new Error(timeoutMsg));
} }
@@ -175,9 +186,11 @@ const transform: AxiosTransform = {
* @description: 响应错误处理 * @description: 响应错误处理
*/ */
responseInterceptorsCatch: (error: any) => { responseInterceptorsCatch: (error: any) => {
const { $message: Message, $dialog: Modal } = window;
const { response, code, message } = error || {}; const { response, code, message } = error || {};
// TODO 此处要根据后端接口返回格式修改
const msg: string = const msg: string =
response && response.data && response.data.error ? response.data.error.message : ''; response && response.data && response.data.message ? response.data.message : '';
const err: string = error.toString(); const err: string = error.toString();
try { try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) { if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
@@ -185,9 +198,11 @@ const transform: AxiosTransform = {
return; return;
} }
if (err && err.includes('Network Error')) { if (err && err.includes('Network Error')) {
Modal.confirm({ Modal.info({
title: '网络异常', title: '网络异常',
content: '请检查您的网络连接是否正常!', content: '请检查您的网络连接是否正常!',
positiveText: '确定',
onPositiveClick: () => {},
}); });
return; return;
} }
@@ -197,7 +212,7 @@ const transform: AxiosTransform = {
// 请求是否被取消 // 请求是否被取消
const isCancel = axios.isCancel(error); const isCancel = axios.isCancel(error);
if (!isCancel) { if (!isCancel) {
checkStatus(error.response && error.response.status, msg); checkStatus(error.response && error.response.status, msg, Message);
} else { } else {
console.warn(error, '请求被取消!'); console.warn(error, '请求被取消!');
} }
@@ -206,20 +221,20 @@ const transform: AxiosTransform = {
}; };
const Axios = new VAxios({ const Axios = new VAxios({
timeout: 15 * 1000, timeout: 10 * 1000,
// 基础接口地址 // 接口前缀
// baseURL: globSetting.apiUrl, prefixUrl: urlPrefix,
// 接口可能会有通用的地址部分,可以统一抽取出来 headers: { 'Content-Type': ContentTypeEnum.JSON },
// prefixUrl: prefix,
headers: { 'Content-Type': ContentTypeEnum.FORM_URLENCODED },
// 数据处理方式 // 数据处理方式
transform, transform,
// 配置项,下面的选项都可以在独立的接口请求中覆盖 // 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: { requestOptions: {
// 默认将prefix 添加到url // 默认将prefix 添加到url
joinPrefix: true, joinPrefix: true,
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理 // 需要对返回数据进行处理
isTransformRequestResult: true, isTransformResponse: true,
// post请求的时候添加参数到url // post请求的时候添加参数到url
joinParamsToUrl: false, joinParamsToUrl: false,
// 格式化提交参数时间 // 格式化提交参数时间

View File

@@ -13,7 +13,7 @@ export interface RequestOptions {
// 格式化请求参数时间 // 格式化请求参数时间
formatDate?: boolean; formatDate?: boolean;
// 是否处理请求结果 // 是否处理请求结果
isTransformRequestResult?: boolean; isTransformResponse?: boolean;
// 是否显示提示信息 // 是否显示提示信息
isShowMessage?: boolean; isShowMessage?: boolean;
// 是否解析成JSON // 是否解析成JSON
@@ -34,6 +34,10 @@ export interface RequestOptions {
errorMessageMode?: 'none' | 'modal'; errorMessageMode?: 'none' | 'modal';
// 是否添加时间戳 // 是否添加时间戳
joinTime?: boolean; joinTime?: boolean;
// 不进行任何处理,直接返回
isTransformResponse?: boolean;
// 是否返回原生响应头
isReturnNativeResponse?: boolean;
} }
export interface Result<T = any> { export interface Result<T = any> {

View File

@@ -0,0 +1,75 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
},
{
title: '编码',
key: 'no',
},
{
title: '名称',
key: 'name',
editComponent: 'NInput',
// 默认必填校验
editRule: true,
edit: true,
width: 200,
},
{
title: '头像',
key: 'avatar',
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
editComponent: 'NSelect',
editComponentProps: {
options: [
{
label: '广东省',
value: 1,
},
{
label: '浙江省',
value: 2,
},
],
},
edit: true,
width: 200,
},
{
title: '开始日期',
key: 'beginTime',
edit: true,
width: 250,
editComponent: 'NDatePicker',
editComponentProps: {
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
},
},
{
title: '结束日期',
key: 'endTime',
width: 200,
},
{
title: '创建时间',
key: 'date',
},
{
title: '停留时间',
key: 'time',
},
];

View File

@@ -0,0 +1,121 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './basicColumns';
import { useDialog, useMessage } from 'naive-ui';
export default defineComponent({
components: { BasicTable },
setup() {
const message = useMessage();
const dialog = useDialog();
const actionRef = ref();
const state = reactive({
params: {
pageSize: 5,
name: 'xiaoMa',
},
actionColumn: {
width: 150,
title: '操作',
dataIndex: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction, {
style: 'button',
actions: createActions(record),
});
},
},
});
function createActions(record) {
return [
{
label: '删除',
icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
];
}
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data;
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function handleDelete(record) {
console.log(record);
dialog.info({
title: '提示',
content: `您想删除${record.name}`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('删除成功');
},
onNegativeClick: () => {},
});
}
function handleEdit(record) {
console.log(record);
message.success('您点击了编辑按钮');
}
return {
...toRefs(state),
columns,
actionRef,
loadDataTable,
onCheckedRow,
reloadTable,
};
},
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,66 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
},
{
title: '编码',
key: 'no',
},
{
title: '名称',
key: 'name',
width: 200,
},
{
title: '头像',
key: 'avatar',
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
width: 200,
},
{
title: '开始日期',
key: 'beginTime',
width: 200,
},
{
title: '结束日期',
key: 'endTime',
width: 200,
},
{
title: '状态',
key: 'status',
render(row) {
return h(
NTag,
{
type: row.status ? 'success' : 'error',
},
{
default: () => (row.status ? '启用' : '禁用'),
}
);
},
},
{
title: '创建时间',
key: 'date',
},
{
title: '停留时间',
key: 'time',
},
];

View File

@@ -1,67 +0,0 @@
import { h } from 'vue';
import { NAvatar, NButton } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
},
{
title: '名称',
key: 'name',
},
{
title: '头像',
key: 'avatar',
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
},
{
title: '开始日期',
key: 'beginTime',
},
{
title: '结束日期',
key: 'endTime',
},
{
title: '创建时间',
key: 'date',
},
{
title: '操作',
key: 'actions',
width: 150,
//简单写一下例子,不建议这么写,过段时间,这里封二次封装
render() {
return [
h(
NButton,
{
size: 'small',
type: 'error',
style: 'margin-right:10px',
onClick: () => {},
},
{ default: () => '删除' }
),
h(
NButton,
{
size: 'small',
onClick: () => {},
},
{ default: () => '编辑' }
),
];
},
},
];

View File

@@ -0,0 +1,131 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@edit-end="editEnd"
@edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './CellColumns';
export default defineComponent({
components: { BasicTable },
setup() {
const actionRef = ref();
const currentEditKeyRef = ref('');
const state = reactive({
params: {
pageSize: 5,
name: 'xiaoMa',
},
actionColumn: {
width: 150,
title: '操作',
dataIndex: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction, {
style: 'button',
actions: createActions(record),
});
},
},
});
function handleEdit(record) {
currentEditKeyRef.value = record.key;
record.onEdit?.(true);
}
function handleCancel(record: EditRecordRow) {
currentEditKeyRef.value = '';
record.onEdit?.(false, false);
}
function onEditChange({ column, value, record }) {
if (column.dataIndex === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
async function handleSave(record: EditRecordRow) {
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
}
}
function createActions(record) {
if (!record.editable) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
} else {
return [
{
label: '保存',
onClick: handleSave.bind(null, record),
},
{
label: '取消',
onClick: handleCancel.bind(null, record),
},
];
}
}
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data;
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
return {
...toRefs(state),
columns,
actionRef,
loadDataTable,
onCheckedRow,
reloadTable,
editEnd,
onEditChange,
};
},
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,131 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@edit-end="editEnd"
@edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './rowColumns';
export default defineComponent({
components: { BasicTable },
setup() {
const actionRef = ref();
const currentEditKeyRef = ref('');
const state = reactive({
params: {
pageSize: 5,
name: 'xiaoMa',
},
actionColumn: {
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction, {
style: 'button',
actions: createActions(record),
});
},
},
});
function handleEdit(record) {
currentEditKeyRef.value = record.key;
record.onEdit?.(true);
}
function handleCancel(record: EditRecordRow) {
currentEditKeyRef.value = '';
record.onEdit?.(false, false);
}
function onEditChange({ column, value, record }) {
if (column.dataIndex === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
async function handleSave(record: EditRecordRow) {
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
}
}
function createActions(record) {
if (!record.editable) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
} else {
return [
{
label: '保存',
onClick: handleSave.bind(null, record),
},
{
label: '取消',
onClick: handleCancel.bind(null, record),
},
];
}
}
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data;
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
return {
...toRefs(state),
columns,
actionRef,
loadDataTable,
onCheckedRow,
reloadTable,
editEnd,
onEditChange,
};
},
});
</script>
<style lang="less" scoped></style>

View File

@@ -1,61 +0,0 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref } from 'vue';
import { BasicTable } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './columns';
export default defineComponent({
components: { BasicTable },
setup() {
const actionRef = ref();
const state = reactive({
params: {
pageSize: 5,
name: 'xiaoMa',
},
});
const loadDataTable = async (params) => {
const data = await getTableList(params);
return data;
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
return {
...toRefs(state),
columns,
actionRef,
loadDataTable,
onCheckedRow,
reloadTable,
};
},
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,89 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
},
{
title: '编码',
key: 'no',
},
{
title: '名称',
key: 'name',
editComponent: 'NInput',
editRow: true,
// 默认必填校验
editRule: true,
edit: true,
width: 200,
},
{
title: '头像',
key: 'avatar',
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
editRow: true,
editComponent: 'NSelect',
editComponentProps: {
options: [
{
label: '广东省',
value: 1,
},
{
label: '浙江省',
value: 2,
},
],
},
edit: true,
width: 200,
},
{
title: '开始日期',
key: 'beginTime',
editRow: true,
edit: true,
width: 250,
editComponent: 'NDatePicker',
editComponentProps: {
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
},
},
{
title: '结束日期',
key: 'endTime',
width: 200,
},
{
title: '状态',
key: 'status',
editRow: true,
edit: true,
width: 100,
editComponent: 'NSwitch',
editValueMap: (value) => {
return value ? '启用' : '禁用';
},
},
{
title: '创建时间',
key: 'date',
},
{
title: '停留时间',
key: 'time',
},
];

View File

@@ -219,7 +219,7 @@
@media (min-width: 768px) { @media (min-width: 768px) {
.view-account { .view-account {
background-image: url('@/assets/images/login.svg'); background-image: url('../../assets/images/login.svg');
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 50%; background-position: 50%;
background-size: 100%; background-size: 100%;

View File

@@ -5,7 +5,6 @@
页面数据为 Mock 示例数据非真实数据 页面数据为 Mock 示例数据非真实数据
</n-card> </n-card>
</div> </div>
<n-grid class="mt-4" cols="1 s:1 m:1 l:3 xl:3 2xl:3" responsive="screen" :x-gap="12"> <n-grid class="mt-4" cols="1 s:1 m:1 l:3 xl:3 2xl:3" responsive="screen" :x-gap="12">
<n-gi span="1"> <n-gi span="1">
<n-card :segmented="{ content: 'hard' }" :bordered="false" size="small"> <n-card :segmented="{ content: 'hard' }" :bordered="false" size="small">
@@ -35,7 +34,6 @@
</n-button> </n-button>
</n-space> </n-space>
</template> </template>
<div class="w-full menu"> <div class="w-full menu">
<n-input type="input" v-model:value="pattern" placeholder="输入菜单名称搜索"> <n-input type="input" v-model:value="pattern" placeholder="输入菜单名称搜索">
<template #suffix> <template #suffix>
@@ -61,6 +59,7 @@
:expandedKeys="expandedKeys" :expandedKeys="expandedKeys"
style="max-height: 650px; overflow: hidden" style="max-height: 650px; overflow: hidden"
@update:selected-keys="selectedTree" @update:selected-keys="selectedTree"
@update:expanded-keys="onExpandedKeys"
/> />
</template> </template>
</div> </div>
@@ -77,7 +76,7 @@
<span>编辑菜单{{ treeItemTitle ? `${treeItemTitle}` : '' }}</span> <span>编辑菜单{{ treeItemTitle ? `${treeItemTitle}` : '' }}</span>
</n-space> </n-space>
</template> </template>
<n-alert type="info" closable> 从菜单列表选择一项后进行编辑 </n-alert> <n-alert type="info" closable> 从菜单列表选择一项后进行编辑</n-alert>
<n-form <n-form
:model="formParams" :model="formParams"
:rules="rules" :rules="rules"
@@ -122,17 +121,15 @@
</n-card> </n-card>
</n-gi> </n-gi>
</n-grid> </n-grid>
<CreateDrawer ref="createDrawerRef" :title="drawerTitle" /> <CreateDrawer ref="createDrawerRef" :title="drawerTitle" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, ref, unref, reactive, toRefs, onMounted, computed } from 'vue'; import { defineComponent, ref, unref, reactive, toRefs, onMounted, computed } from 'vue';
import { useMessage } from 'naive-ui'; import { useMessage } from 'naive-ui';
import { DownOutlined, AlignLeftOutlined, SearchOutlined, FormOutlined } from '@vicons/antd'; import { DownOutlined, AlignLeftOutlined, SearchOutlined, FormOutlined } from '@vicons/antd';
import { getMenuList } from '@/api/system/menu'; import { getMenuList } from '@/api/system/menu';
import { getTreeItem } from '@/utils/index'; import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue'; import CreateDrawer from './CreateDrawer.vue';
const rules = { const rules = {
@@ -156,35 +153,49 @@
const message = useMessage(); const message = useMessage();
const isAddSon = computed(() => { const isAddSon = computed(() => {
return state.treeItemKey.length ? false : true; return !state.treeItemKey.length;
}); });
const addMenuOptions = ref([
{
label: '添加顶级菜单',
key: 'home',
disabled: false,
},
{
label: '添加子菜单',
key: 'son',
disabled: isAddSon,
},
]);
const treeItemKey: string[] = reactive([]);
const expandedKeys: string[] = reactive([]);
const treeData: string[] = reactive([]);
const state = reactive({ const state = reactive({
addMenuOptions: [
{
label: '添加顶级菜单',
key: 'home',
disabled: false,
},
{
label: '添加子菜单',
key: 'son',
disabled: isAddSon,
},
],
loading: true, loading: true,
subLoading: false, subLoading: false,
isEditMenu: false, isEditMenu: false,
treeData: [],
treeItemKey: [],
treeItemTitle: '', treeItemTitle: '',
expandedKeys: [], formParams: {
formParams: {}, type: 1,
label: '',
subtitle: '',
path: '',
auth: '',
openType: 1,
},
pattern: '', pattern: '',
drawerTitle: '', drawerTitle: '',
treeItemKey,
expandedKeys,
treeData,
}); });
function selectAddMenu(key) { function selectAddMenu(key: string) {
state.drawerTitle = key === 'home' ? '添加顶栏菜单' : `添加子菜单:${state.treeItemTitle}`; state.drawerTitle = key === 'home' ? '添加顶栏菜单' : `添加子菜单:${state.treeItemTitle}`;
openCreateDrawer(); openCreateDrawer();
} }
@@ -194,7 +205,7 @@
openDrawer(); openDrawer();
} }
function selectedTree(keys) { function selectedTree(keys: string[]) {
if (keys.length) { if (keys.length) {
const treeItem = getTreeItem(unref(state.treeData), keys[0]); const treeItem = getTreeItem(unref(state.treeData), keys[0]);
state.treeItemKey = keys; state.treeItemKey = keys;
@@ -214,7 +225,7 @@
} }
function formSubmit() { function formSubmit() {
formRef.value.validate((errors) => { formRef.value.validate((errors: boolean) => {
if (!errors) { if (!errors) {
message.error('抱歉,您没有该权限'); message.error('抱歉,您没有该权限');
} else { } else {
@@ -227,21 +238,10 @@
if (state.expandedKeys.length) { if (state.expandedKeys.length) {
state.expandedKeys = []; state.expandedKeys = [];
} else { } else {
state.expandedKeys = state.treeData.map((item) => item.key); state.expandedKeys = state.treeData.map((item: any) => item.key);
} }
} }
function onExpandedKeys() {
// let key = keys[0]
// console.log(state.expandedKeys)
// if(state.expandedKeys.includes(key)){
// state.expandedKeys.splice(state.expandedKeys.findIndex(item => item === key), 1)
// console.log(state.expandedKeys)
// }else{
// state.expandedKeys.push(key)
// }
}
onMounted(async () => { onMounted(async () => {
const treeMenuList = await getMenuList(); const treeMenuList = await getMenuList();
state.expandedKeys = treeMenuList.list.map((item) => item.key); state.expandedKeys = treeMenuList.list.map((item) => item.key);
@@ -249,8 +249,13 @@
state.loading = false; state.loading = false;
}); });
function onExpandedKeys(keys: string[]) {
state.expandedKeys = keys;
}
return { return {
...toRefs(state), ...toRefs(state),
addMenuOptions,
createDrawerRef, createDrawerRef,
formRef, formRef,
rules, rules,

5
types/global.d.ts vendored
View File

@@ -66,6 +66,9 @@ declare global {
VITE_DROP_CONSOLE: boolean; VITE_DROP_CONSOLE: boolean;
VITE_GLOB_PROD_MOCK: boolean; VITE_GLOB_PROD_MOCK: boolean;
VITE_GLOB_IMG_URL: string; VITE_GLOB_IMG_URL: string;
VITE_PROXY: [string, string][];
VITE_BUILD_COMPRESS: 'gzip' | 'brotli' | 'none';
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE: boolean;
} }
declare function parseInt(s: string | number, radix?: number): number; declare function parseInt(s: string | number, radix?: number): number;
@@ -94,6 +97,6 @@ declare global {
declare module 'vue' { declare module 'vue' {
export type JSXComponent<Props = any> = export type JSXComponent<Props = any> =
| { new(): ComponentPublicInstance<Props> } | { new (): ComponentPublicInstance<Props> }
| FunctionalComponent<Props>; | FunctionalComponent<Props>;
} }

6
types/index.d.ts vendored
View File

@@ -8,6 +8,12 @@ declare interface PromiseFn<T = any, R = T> {
declare type RefType<T> = T | null; declare type RefType<T> = T | null;
declare type LabelValueOptions = {
label: string;
value: any;
disabled: boolean;
[key: string]: string | number | boolean;
}[];
declare type EmitType = (event: string, ...args: any[]) => void; declare type EmitType = (event: string, ...args: any[]) => void;

View File

@@ -4,6 +4,7 @@ import { resolve } from 'path';
import { wrapperEnv } from './build/utils'; import { wrapperEnv } from './build/utils';
import { createVitePlugins } from './build/vite/plugin'; import { createVitePlugins } from './build/vite/plugin';
import { OUTPUT_DIR } from './build/constant'; import { OUTPUT_DIR } from './build/constant';
import { createProxy } from './build/vite/proxy';
function pathResolve(dir: string) { function pathResolve(dir: string) {
return resolve(process.cwd(), '.', dir); return resolve(process.cwd(), '.', dir);
@@ -13,7 +14,8 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
const root = process.cwd(); const root = process.cwd();
const env = loadEnv(mode, root); const env = loadEnv(mode, root);
const viteEnv = wrapperEnv(env); const viteEnv = wrapperEnv(env);
const { VITE_PUBLIC_PATH, VITE_DROP_CONSOLE, VITE_PORT, VITE_GLOB_PROD_MOCK } = viteEnv; const { VITE_PUBLIC_PATH, VITE_DROP_CONSOLE, VITE_PORT, VITE_GLOB_PROD_MOCK, VITE_PROXY } =
viteEnv;
const prodMock = VITE_GLOB_PROD_MOCK; const prodMock = VITE_GLOB_PROD_MOCK;
const isBuild = command === 'build'; const isBuild = command === 'build';
return { return {
@@ -38,15 +40,14 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
less: { less: {
modifyVars: {}, modifyVars: {},
javascriptEnabled: true, javascriptEnabled: true,
additionalData: `@import "src/styles/global.less";`, additionalData: `@import "src/styles/var.less";`,
},
scss: {
additionalData: `@import "src/styles/global.less";`,
}, },
}, },
}, },
server: { server: {
host: true,
port: VITE_PORT, port: VITE_PORT,
proxy: createProxy(VITE_PROXY),
// proxy: { // proxy: {
// '/api': { // '/api': {
// target: '', // target: '',

View File

@@ -641,13 +641,6 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@fullhuman/postcss-purgecss@^4.0.3":
version "4.0.3"
resolved "https://registry.npm.taobao.org/@fullhuman/postcss-purgecss/download/@fullhuman/postcss-purgecss-4.0.3.tgz#55d71712ec1c7a88e0d1ba5f10ce7fb6aa05beb4"
integrity sha1-VdcXEuwceojg0bpfEM5/tqoFvrQ=
dependencies:
purgecss "^4.0.3"
"@istanbuljs/load-nyc-config@^1.0.0": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.nlark.com/@istanbuljs/load-nyc-config/download/@istanbuljs/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" resolved "https://registry.nlark.com/@istanbuljs/load-nyc-config/download/@istanbuljs/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -2093,7 +2086,7 @@ collect-v8-coverage@^1.0.0:
resolved "https://registry.npm.taobao.org/collect-v8-coverage/download/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" resolved "https://registry.npm.taobao.org/collect-v8-coverage/download/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
integrity sha1-zCyOlPwYu9/+ZNZTRXDIpnOyf1k= integrity sha1-zCyOlPwYu9/+ZNZTRXDIpnOyf1k=
color-convert@^1.9.0, color-convert@^1.9.1: color-convert@^1.9.0, color-convert@^1.9.3:
version "1.9.3" version "1.9.3"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
@@ -2117,21 +2110,21 @@ color-name@^1.0.0, color-name@~1.1.4:
resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.5.4: color-string@^1.6.0:
version "1.5.5" version "1.6.0"
resolved "https://registry.npm.taobao.org/color-string/download/color-string-1.5.5.tgz?cache=0&sync_timestamp=1614967162868&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcolor-string%2Fdownload%2Fcolor-string-1.5.5.tgz#65474a8f0e7439625f3d27a6a19d89fc45223014" resolved "https://registry.nlark.com/color-string/download/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
integrity sha1-ZUdKjw50OWJfPSemoZ2J/EUiMBQ= integrity sha1-w5FfYf4mdnLLfh4GTJ1pIhn2wxI=
dependencies: dependencies:
color-name "^1.0.0" color-name "^1.0.0"
simple-swizzle "^0.2.2" simple-swizzle "^0.2.2"
color@^3.1.3: color@^3.2.0:
version "3.1.3" version "3.2.1"
resolved "https://registry.npm.taobao.org/color/download/color-3.1.3.tgz?cache=0&sync_timestamp=1602228883047&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fcolor%2Fdownload%2Fcolor-3.1.3.tgz#ca67fb4e7b97d611dcde39eceed422067d91596e" resolved "https://registry.nlark.com/color/download/color-3.2.1.tgz?cache=0&sync_timestamp=1626601281817&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fcolor%2Fdownload%2Fcolor-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
integrity sha1-ymf7TnuX1hHc3jns7tQiBn2RWW4= integrity sha1-NUTcGYyvRJDD7MmnkLVP6f9F4WQ=
dependencies: dependencies:
color-convert "^1.9.1" color-convert "^1.9.3"
color-string "^1.5.4" color-string "^1.6.0"
colorette@^1.2.1, colorette@^1.2.2: colorette@^1.2.1, colorette@^1.2.2:
version "1.2.2" version "1.2.2"
@@ -2489,10 +2482,10 @@ detective@^5.2.0:
defined "^1.0.0" defined "^1.0.0"
minimist "^1.1.1" minimist "^1.1.1"
didyoumean@^1.2.1: didyoumean@^1.2.2:
version "1.2.1" version "1.2.2"
resolved "https://registry.npm.taobao.org/didyoumean/download/didyoumean-1.2.1.tgz#e92edfdada6537d484d73c0172fd1eba0c4976ff" resolved "https://registry.nlark.com/didyoumean/download/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
integrity sha1-6S7f2tplN9SE1zwBcv0eugxJdv8= integrity sha1-mJNG/+noObRVXs9WZu3qDT6K0Dc=
diff-sequences@^26.6.2: diff-sequences@^26.6.2:
version "26.6.2" version "26.6.2"
@@ -5006,10 +4999,10 @@ mute-stream@0.0.7:
resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
naive-ui@^2.15.5: naive-ui@^2.15.11:
version "2.15.5" version "2.15.11"
resolved "https://registry.nlark.com/naive-ui/download/naive-ui-2.15.5.tgz#50ffc1834fd64765621a31f0b09e64d72770cd6c" resolved "https://registry.nlark.com/naive-ui/download/naive-ui-2.15.11.tgz#1a7fa5ca6d42ed2c4d50cde827b994f42edc5743"
integrity sha1-UP/Bg0/WR2ViGjHwsJ5k1ydwzWw= integrity sha1-Gn+lym1C7SxNUM3oJ7mU9C7cV0M=
dependencies: dependencies:
"@css-render/plugin-bem" "^0.15.4" "@css-render/plugin-bem" "^0.15.4"
"@css-render/vue3-ssr" "^0.15.4" "@css-render/vue3-ssr" "^0.15.4"
@@ -5912,8 +5905,8 @@ reusify@^1.0.4:
rimraf@^3.0.0, rimraf@^3.0.2: rimraf@^3.0.0, rimraf@^3.0.2:
version "3.0.2" version "3.0.2"
resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" resolved "https://registry.npm.taobao.org/rimraf/download/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== integrity sha1-8aVAK6YiCtUswSgrrBrjqkn9Bho=
dependencies: dependencies:
glob "^7.1.3" glob "^7.1.3"
@@ -6454,22 +6447,21 @@ table@^6.0.9, table@^6.6.0:
string-width "^4.2.0" string-width "^4.2.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
tailwindcss@^2.2.4: tailwindcss@^2.2.7:
version "2.2.4" version "2.2.7"
resolved "https://registry.nlark.com/tailwindcss/download/tailwindcss-2.2.4.tgz#6a2e259b1e26125aeaa7cdc479963fd217c308b0" resolved "https://registry.nlark.com/tailwindcss/download/tailwindcss-2.2.7.tgz#795d07a14ef46c2dc4a1610f7f906f697daaf731"
integrity sha1-ai4lmx4mElrqp83EeZY/0hfDCLA= integrity sha1-eV0HoU70bC3EoWEPf5BvaX2q9zE=
dependencies: dependencies:
"@fullhuman/postcss-purgecss" "^4.0.3"
arg "^5.0.0" arg "^5.0.0"
bytes "^3.0.0" bytes "^3.0.0"
chalk "^4.1.1" chalk "^4.1.1"
chokidar "^3.5.2" chokidar "^3.5.2"
color "^3.1.3" color "^3.2.0"
cosmiconfig "^7.0.0" cosmiconfig "^7.0.0"
detective "^5.2.0" detective "^5.2.0"
didyoumean "^1.2.1" didyoumean "^1.2.2"
dlv "^1.1.3" dlv "^1.1.3"
fast-glob "^3.2.5" fast-glob "^3.2.7"
fs-extra "^10.0.0" fs-extra "^10.0.0"
glob-parent "^6.0.0" glob-parent "^6.0.0"
html-tags "^3.1.0" html-tags "^3.1.0"
@@ -6486,6 +6478,7 @@ tailwindcss@^2.2.4:
postcss-selector-parser "^6.0.6" postcss-selector-parser "^6.0.6"
postcss-value-parser "^4.1.0" postcss-value-parser "^4.1.0"
pretty-hrtime "^1.0.3" pretty-hrtime "^1.0.3"
purgecss "^4.0.3"
quick-lru "^5.1.1" quick-lru "^5.1.1"
reduce-css-calc "^2.1.8" reduce-css-calc "^2.1.8"
resolve "^1.20.0" resolve "^1.20.0"
@@ -6691,10 +6684,10 @@ typedarray-to-buffer@^3.1.5:
dependencies: dependencies:
is-typedarray "^1.0.0" is-typedarray "^1.0.0"
typescript@^4.3.2: typescript@^4.3.5:
version "4.3.2" version "4.3.5"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805" resolved "https://registry.nlark.com/typescript/download/typescript-4.3.5.tgz?cache=0&sync_timestamp=1627197601863&other_urls=https%3A%2F%2Fregistry.nlark.com%2Ftypescript%2Fdownload%2Ftypescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw== integrity sha1-TRw3zBbok5c8RaBohrcRMjTxGfQ=
unified@^9.1.0: unified@^9.1.0:
version "9.2.1" version "9.2.1"
@@ -6826,6 +6819,15 @@ vfonts@^0.1.0:
resolved "https://registry.nlark.com/vfonts/download/vfonts-0.1.0.tgz#c16af37ca044b2725ae55553049280efbe6222a9" resolved "https://registry.nlark.com/vfonts/download/vfonts-0.1.0.tgz#c16af37ca044b2725ae55553049280efbe6222a9"
integrity sha1-wWrzfKBEsnJa5VVTBJKA775iIqk= integrity sha1-wWrzfKBEsnJa5VVTBJKA775iIqk=
vite-plugin-compression@^0.3.1:
version "0.3.1"
resolved "https://registry.nlark.com/vite-plugin-compression/download/vite-plugin-compression-0.3.1.tgz#b8947d0fd7bba60f6c3385cfcb8ce3205028c541"
integrity sha1-uJR9D9e7pg9sM4XPy4zjIFAoxUE=
dependencies:
chalk "^4.1.1"
debug "^4.3.2"
fs-extra "^9.1.0"
vite-plugin-html@^2.0.7: vite-plugin-html@^2.0.7:
version "2.0.7" version "2.0.7"
resolved "https://registry.npm.taobao.org/vite-plugin-html/download/vite-plugin-html-2.0.7.tgz#55139ff8a843ba3b3d5cd48610ca6f6afdf7c23d" resolved "https://registry.npm.taobao.org/vite-plugin-html/download/vite-plugin-html-2.0.7.tgz#55139ff8a843ba3b3d5cd48610ca6f6afdf7c23d"