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
VITE_DROP_CONSOLE = true
# 跨域代理,可以配置多个,请注意不要换行
#VITE_PROXY = [["/appApi","http://localhost:8001"],["/upload","http://localhost:8001/upload"]]
# VITE_PROXY=[["/api","https://naive-ui-admin"]]
# 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

View File

@@ -11,13 +11,21 @@ VITE_BASE_URL = /
VITE_DROP_CONSOLE = true
# 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
# 是否启用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)
### 🐛 Bug Fixes
- 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 等前端前沿技术开发
@@ -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)
账号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)
## 感谢
[@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') {
try {
realName = JSON.parse(realName);
} catch (error) {
}
} catch (error) {}
}
ret[envName] = realName;
process.env[envName] = realName;
@@ -51,8 +50,7 @@ export function getEnvConfig(match = 'VITE_GLOB_', confFiles = ['.env', '.env.pr
try {
const env = dotenv.parse(fs.readFileSync(path.resolve(process.cwd(), item)));
envConfig = { ...envConfig, ...env };
} catch (error) {
}
} catch (error) {}
});
Object.keys(envConfig).forEach((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 { configMockPlugin } from './mock';
import { configCompressPlugin } from './compress';
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[])[] = [
// have to
@@ -22,5 +23,12 @@ export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean, prodMock)
// vite-plugin-mock
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;
}

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>
</head>
<body>
<div id="appProvider" style="display: none"></div>
<div id="app">
<div class="first-loading-wrp">
<div class="loading-wrp">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,6 @@
<template>
<n-loading-bar-provider>
<LoadingContent />
<n-dialog-provider>
<DialogContent />
<n-notification-provider>
@@ -8,16 +10,32 @@
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
</n-loading-bar-provider>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
} from 'naive-ui';
import { LoadingContent } from '@/components/LoadingContent';
import { MessageContent } from '@/components/MessageContent';
import { DialogContent } from '@/components/DialogContent';
export default defineComponent({
name: 'Application',
components: { MessageContent, DialogContent },
components: {
NDialogProvider,
NNotificationProvider,
NMessageProvider,
NLoadingBarProvider,
LoadingContent,
MessageContent,
DialogContent,
},
setup() {
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 class="s-table">
<n-data-table
ref="tableElRef"
v-bind="getBindValues"
:pagination="pagination"
@update:page="updatePage"
@@ -73,7 +74,17 @@
<script lang="ts">
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 { createTableContext } from './hooks/useTableContext';
@@ -88,6 +99,10 @@
import { BasicTableProps } from './types/table';
import { getViewportOffset } from '@/utils/domUtils';
import { useWindowSizeFn } from '@/hooks/event/useWindowSizeFn';
import { isBoolean } from '@/utils/is';
const densityOptions = [
{
type: 'menu',
@@ -117,9 +132,20 @@
...NDataTable.props, // 这里继承原 UI 组件的 props
...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 }) {
const deviceHeight = ref(150);
const tableElRef = ref<ComponentRef>(null);
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
let paginationEl: HTMLElement | null;
const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>();
@@ -173,20 +199,14 @@
emit('update:checked-row-keys', rowKeys);
}
//重置 Columns
const resetColumns = () => {
columns.map((item) => {
item.isShow = true;
});
};
//获取表格大小
const getTableSize = computed(() => state.tableSize);
//组装表格信息
const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef);
let propsData = {
const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : 'auto';
return {
...unref(getProps),
loading: unref(getLoading),
columns: toRaw(unref(getPageColumns)),
@@ -194,8 +214,8 @@
data: tableData,
size: unref(getTableSize),
remote: true,
'max-height': maxHeight,
};
return propsData;
});
//获取分页信息
@@ -205,7 +225,7 @@
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const tableAction: TableActionType = {
const tableAction = {
reload,
setColumns,
setLoading,
@@ -216,14 +236,54 @@
setCacheColumnsField,
emit,
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 });
return {
...toRefs(state),
tableElRef,
getBindValues,
densityOptions,
reload,
@@ -232,7 +292,6 @@
updatePageSize,
updateCheckedRowKeys,
pagination,
resetColumns,
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: {
type: Array as PropType<ActionItem[]>,
default: null,
required: true,
},
dropDownActions: {
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) => {
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 { isEqual, cloneDeep } from 'lodash-es';
import { isArray, isString, isBoolean, isFunction } from '@/utils/is';
import { usePermission } from '@/hooks/web/usePermission';
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>) {
const columnsRef = ref(unref(propsRef).columns) as unknown as Ref<BasicColumn[]>;
@@ -33,12 +36,47 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
return isIfShow;
}
const renderTooltip = (trigger, content) => {
return h(NTooltip, null, {
trigger: () => trigger,
default: () => content,
});
};
const getPageColumns = computed(() => {
const pageColumns = unref(getColumnsRef);
const columns = cloneDeep(pageColumns);
return columns.filter((column) => {
return columns
.filter((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;
});
});

View File

@@ -1,4 +1,5 @@
import type { PropType } from 'vue';
import { propTypes } from '@/utils/propTypes';
import { BasicColumn } from './types/table';
export const basicProps = {
@@ -16,7 +17,7 @@ export const basicProps = {
},
tableData: {
type: [Object],
default: () => {},
default: () => [],
},
columns: {
type: [Array] as PropType<BasicColumn[]>,
@@ -36,6 +37,7 @@ export const basicProps = {
type: [Object, Boolean],
default: () => {},
},
//废弃
showPagination: {
type: [String, Boolean],
default: 'auto',
@@ -44,4 +46,6 @@ export const basicProps = {
type: Object as PropType<BasicColumn>,
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';
export type BasicColumn = TableBaseColumn;
import { ComponentType } from './componentType';
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 {
reload: (opt) => Promise<void>;
@@ -16,4 +30,6 @@ export interface BasicTableProps {
pagination: object;
showPagination: boolean;
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>
.page-footer {
margin: 48px 0 24px 0;
//margin: 28px 0 24px 0;
padding: 0 16px;
text-align: center;

View File

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

View File

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

View File

@@ -6,6 +6,7 @@
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="20"
:indent="28"
:expanded-keys="openKeys"
v-model:value="selectedKeys"
@update:value="clickMenuItem"
@@ -14,7 +15,7 @@
</template>
<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 { useAsyncRouteStore } from '@/store/modules/asyncRoute';
import { generatorMenu } from '@/utils/index';
@@ -44,7 +45,7 @@
// 获取当前打开的子菜单
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({
openKeys: getOpenKeys,
@@ -73,8 +74,7 @@
() => currentRoute.fullPath,
() => {
const matched = currentRoute.matched;
const getOpenKeys = matched && matched.length ? [matched[0]?.name] : [];
state.openKeys = getOpenKeys;
state.openKeys = matched.map((item) => item.name);
state.selectedKeys = currentRoute.name;
}
);
@@ -90,10 +90,22 @@
//展开菜单
function menuExpanded(openKeys: string[]) {
console.log(openKeys);
if (!openKeys) return;
const latestOpenKey = openKeys.pop();
state.openKeys = latestOpenKey ? [latestOpenKey] : [];
const latestOpenKey = openKeys.find((key) => state.openKeys.indexOf(key) === -1);
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 {

View File

@@ -46,10 +46,10 @@
<MainView />
</div>
</div>
<NLayoutFooter v-if="getShowFooter">
<PageFooter />
</NLayoutFooter>
<!--1.15废弃没啥用占用操作空间-->
<!-- <NLayoutFooter v-if="getShowFooter">-->
<!-- <PageFooter />-->
<!-- </NLayoutFooter>-->
</NLayoutContent>
</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 MakeitCaptcha from 'makeit-captcha';
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() {
const appProvider = createApp(AppProvider);
const app = createApp(App);
app.use(MakeitCaptcha);
@@ -15,18 +18,21 @@ async function bootstrap() {
// 注册全局常用的 naive-ui 组件
setupNaive(app);
// 注册全局自定义组件,如:<svg-icon />
setupCustomComponents(app);
// 注册全局自定义组件
//setupCustomComponents();
// 注册全局自定义指令v-permission权限指令
setupDirectives(app);
// 注册全局方法app.config.globalProperties.$message = message
setupGlobalMethods(app);
//setupGlobalMethods(app);
// 挂载状态管理
setupStore(app);
//优先挂载一下 Provider 解决路由守卫Axios中可使用DialogMessage 等之类组件
appProvider.mount('#appProvider', true);
// 挂载路由
await setupRouter(app);

View File

@@ -3,3 +3,5 @@ export const RedirectName = 'Redirect';
export const ErrorPage = () => import('@/views/exception/404.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 { Layout } from '@/router/constant';
import { Layout, ParentLayout } from '@/router/constant';
import { WalletOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index';
@@ -20,8 +20,8 @@ const routes: Array<RouteRecordRaw> = [
{
path: '/comp',
name: routeName,
redirect: '/comp/console',
component: Layout,
redirect: '/comp/table',
meta: {
title: '组件示例',
icon: renderIcon(WalletOutlined),
@@ -31,10 +31,37 @@ const routes: Array<RouteRecordRaw> = [
{
path: 'table',
name: `${routeName}_table`,
redirect: '/comp/table/basic',
component: ParentLayout,
meta: {
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',

View File

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

View File

@@ -10,6 +10,14 @@ export const appThemeList: string[] = [
'#0096c7',
'#9c27b0',
'#ff9800',
'#FF3D68',
'#00C1D4',
'#71EFA3',
'#171010',
'#78DEC7',
'#1768AC',
'#FB9300',
'#FC5404',
];
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 {
const message = window['$message'];
export function checkStatus(status: number, msg: string, message: any): void {
switch (status) {
case 400:
message.error(`${msg}`);

View File

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

View File

@@ -13,7 +13,7 @@ export interface RequestOptions {
// 格式化请求参数时间
formatDate?: boolean;
// 是否处理请求结果
isTransformRequestResult?: boolean;
isTransformResponse?: boolean;
// 是否显示提示信息
isShowMessage?: boolean;
// 是否解析成JSON
@@ -34,6 +34,10 @@ export interface RequestOptions {
errorMessageMode?: 'none' | 'modal';
// 是否添加时间戳
joinTime?: boolean;
// 不进行任何处理,直接返回
isTransformResponse?: boolean;
// 是否返回原生响应头
isReturnNativeResponse?: boolean;
}
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) {
.view-account {
background-image: url('@/assets/images/login.svg');
background-image: url('../../assets/images/login.svg');
background-repeat: no-repeat;
background-position: 50%;
background-size: 100%;

View File

@@ -5,7 +5,6 @@
页面数据为 Mock 示例数据非真实数据
</n-card>
</div>
<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-card :segmented="{ content: 'hard' }" :bordered="false" size="small">
@@ -35,7 +34,6 @@
</n-button>
</n-space>
</template>
<div class="w-full menu">
<n-input type="input" v-model:value="pattern" placeholder="输入菜单名称搜索">
<template #suffix>
@@ -61,6 +59,7 @@
:expandedKeys="expandedKeys"
style="max-height: 650px; overflow: hidden"
@update:selected-keys="selectedTree"
@update:expanded-keys="onExpandedKeys"
/>
</template>
</div>
@@ -122,17 +121,15 @@
</n-card>
</n-gi>
</n-grid>
<CreateDrawer ref="createDrawerRef" :title="drawerTitle" />
</div>
</template>
<script lang="ts">
import { defineComponent, ref, unref, reactive, toRefs, onMounted, computed } from 'vue';
import { useMessage } from 'naive-ui';
import { DownOutlined, AlignLeftOutlined, SearchOutlined, FormOutlined } from '@vicons/antd';
import { getMenuList } from '@/api/system/menu';
import { getTreeItem } from '@/utils/index';
import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue';
const rules = {
@@ -156,11 +153,10 @@
const message = useMessage();
const isAddSon = computed(() => {
return state.treeItemKey.length ? false : true;
return !state.treeItemKey.length;
});
const state = reactive({
addMenuOptions: [
const addMenuOptions = ref([
{
label: '添加顶级菜单',
key: 'home',
@@ -171,20 +167,35 @@
key: 'son',
disabled: isAddSon,
},
],
]);
const treeItemKey: string[] = reactive([]);
const expandedKeys: string[] = reactive([]);
const treeData: string[] = reactive([]);
const state = reactive({
loading: true,
subLoading: false,
isEditMenu: false,
treeData: [],
treeItemKey: [],
treeItemTitle: '',
expandedKeys: [],
formParams: {},
formParams: {
type: 1,
label: '',
subtitle: '',
path: '',
auth: '',
openType: 1,
},
pattern: '',
drawerTitle: '',
treeItemKey,
expandedKeys,
treeData,
});
function selectAddMenu(key) {
function selectAddMenu(key: string) {
state.drawerTitle = key === 'home' ? '添加顶栏菜单' : `添加子菜单:${state.treeItemTitle}`;
openCreateDrawer();
}
@@ -194,7 +205,7 @@
openDrawer();
}
function selectedTree(keys) {
function selectedTree(keys: string[]) {
if (keys.length) {
const treeItem = getTreeItem(unref(state.treeData), keys[0]);
state.treeItemKey = keys;
@@ -214,7 +225,7 @@
}
function formSubmit() {
formRef.value.validate((errors) => {
formRef.value.validate((errors: boolean) => {
if (!errors) {
message.error('抱歉,您没有该权限');
} else {
@@ -227,21 +238,10 @@
if (state.expandedKeys.length) {
state.expandedKeys = [];
} 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 () => {
const treeMenuList = await getMenuList();
state.expandedKeys = treeMenuList.list.map((item) => item.key);
@@ -249,8 +249,13 @@
state.loading = false;
});
function onExpandedKeys(keys: string[]) {
state.expandedKeys = keys;
}
return {
...toRefs(state),
addMenuOptions,
createDrawerRef,
formRef,
rules,

3
types/global.d.ts vendored
View File

@@ -66,6 +66,9 @@ declare global {
VITE_DROP_CONSOLE: boolean;
VITE_GLOB_PROD_MOCK: boolean;
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;

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 LabelValueOptions = {
label: string;
value: any;
disabled: boolean;
[key: string]: string | number | boolean;
}[];
declare type EmitType = (event: string, ...args: any[]) => void;

View File

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

View File

@@ -641,13 +641,6 @@
minimatch "^3.0.4"
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":
version "1.1.0"
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"
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"
resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
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"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
color-string@^1.5.4:
version "1.5.5"
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"
integrity sha1-ZUdKjw50OWJfPSemoZ2J/EUiMBQ=
color-string@^1.6.0:
version "1.6.0"
resolved "https://registry.nlark.com/color-string/download/color-string-1.6.0.tgz#c3915f61fe267672cb7e1e064c9d692219f6c312"
integrity sha1-w5FfYf4mdnLLfh4GTJ1pIhn2wxI=
dependencies:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color@^3.1.3:
version "3.1.3"
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"
integrity sha1-ymf7TnuX1hHc3jns7tQiBn2RWW4=
color@^3.2.0:
version "3.2.1"
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-NUTcGYyvRJDD7MmnkLVP6f9F4WQ=
dependencies:
color-convert "^1.9.1"
color-string "^1.5.4"
color-convert "^1.9.3"
color-string "^1.6.0"
colorette@^1.2.1, colorette@^1.2.2:
version "1.2.2"
@@ -2489,10 +2482,10 @@ detective@^5.2.0:
defined "^1.0.0"
minimist "^1.1.1"
didyoumean@^1.2.1:
version "1.2.1"
resolved "https://registry.npm.taobao.org/didyoumean/download/didyoumean-1.2.1.tgz#e92edfdada6537d484d73c0172fd1eba0c4976ff"
integrity sha1-6S7f2tplN9SE1zwBcv0eugxJdv8=
didyoumean@^1.2.2:
version "1.2.2"
resolved "https://registry.nlark.com/didyoumean/download/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037"
integrity sha1-mJNG/+noObRVXs9WZu3qDT6K0Dc=
diff-sequences@^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"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
naive-ui@^2.15.5:
version "2.15.5"
resolved "https://registry.nlark.com/naive-ui/download/naive-ui-2.15.5.tgz#50ffc1834fd64765621a31f0b09e64d72770cd6c"
integrity sha1-UP/Bg0/WR2ViGjHwsJ5k1ydwzWw=
naive-ui@^2.15.11:
version "2.15.11"
resolved "https://registry.nlark.com/naive-ui/download/naive-ui-2.15.11.tgz#1a7fa5ca6d42ed2c4d50cde827b994f42edc5743"
integrity sha1-Gn+lym1C7SxNUM3oJ7mU9C7cV0M=
dependencies:
"@css-render/plugin-bem" "^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:
version "3.0.2"
resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
resolved "https://registry.npm.taobao.org/rimraf/download/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
integrity sha1-8aVAK6YiCtUswSgrrBrjqkn9Bho=
dependencies:
glob "^7.1.3"
@@ -6454,22 +6447,21 @@ table@^6.0.9, table@^6.6.0:
string-width "^4.2.0"
strip-ansi "^6.0.0"
tailwindcss@^2.2.4:
version "2.2.4"
resolved "https://registry.nlark.com/tailwindcss/download/tailwindcss-2.2.4.tgz#6a2e259b1e26125aeaa7cdc479963fd217c308b0"
integrity sha1-ai4lmx4mElrqp83EeZY/0hfDCLA=
tailwindcss@^2.2.7:
version "2.2.7"
resolved "https://registry.nlark.com/tailwindcss/download/tailwindcss-2.2.7.tgz#795d07a14ef46c2dc4a1610f7f906f697daaf731"
integrity sha1-eV0HoU70bC3EoWEPf5BvaX2q9zE=
dependencies:
"@fullhuman/postcss-purgecss" "^4.0.3"
arg "^5.0.0"
bytes "^3.0.0"
chalk "^4.1.1"
chokidar "^3.5.2"
color "^3.1.3"
color "^3.2.0"
cosmiconfig "^7.0.0"
detective "^5.2.0"
didyoumean "^1.2.1"
didyoumean "^1.2.2"
dlv "^1.1.3"
fast-glob "^3.2.5"
fast-glob "^3.2.7"
fs-extra "^10.0.0"
glob-parent "^6.0.0"
html-tags "^3.1.0"
@@ -6486,6 +6478,7 @@ tailwindcss@^2.2.4:
postcss-selector-parser "^6.0.6"
postcss-value-parser "^4.1.0"
pretty-hrtime "^1.0.3"
purgecss "^4.0.3"
quick-lru "^5.1.1"
reduce-css-calc "^2.1.8"
resolve "^1.20.0"
@@ -6691,10 +6684,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@^4.3.2:
version "4.3.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.2.tgz#399ab18aac45802d6f2498de5054fcbbe716a805"
integrity sha512-zZ4hShnmnoVnAHpVHWpTcxdv7dWP60S2FsydQLV8V5PbS3FifjWFFRiHSWpDJahly88PRyV5teTSLoq4eG7mKw==
typescript@^4.3.5:
version "4.3.5"
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 sha1-TRw3zBbok5c8RaBohrcRMjTxGfQ=
unified@^9.1.0:
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"
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:
version "2.0.7"
resolved "https://registry.npm.taobao.org/vite-plugin-html/download/vite-plugin-html-2.0.7.tgz#55139ff8a843ba3b3d5cd48610ca6f6afdf7c23d"