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

@@ -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,23 +1,41 @@
<template>
<n-dialog-provider>
<DialogContent />
<n-notification-provider>
<n-message-provider>
<MessageContent />
<slot slot="default"></slot>
</n-message-provider>
</n-notification-provider>
</n-dialog-provider>
<n-loading-bar-provider>
<LoadingContent />
<n-dialog-provider>
<DialogContent />
<n-notification-provider>
<n-message-provider>
<MessageContent />
<slot slot="default"></slot>
</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,13 +36,48 @@ 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) => {
// @ts-ignore
return hasPermission(column.auth) && isIfShow(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;
});
});
watch(

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>
@@ -77,7 +76,7 @@
<span>编辑菜单{{ treeItemTitle ? `${treeItemTitle}` : '' }}</span>
</n-space>
</template>
<n-alert type="info" closable> 从菜单列表选择一项后进行编辑 </n-alert>
<n-alert type="info" closable> 从菜单列表选择一项后进行编辑</n-alert>
<n-form
:model="formParams"
:rules="rules"
@@ -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,35 +153,49 @@
const message = useMessage();
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({
addMenuOptions: [
{
label: '添加顶级菜单',
key: 'home',
disabled: false,
},
{
label: '添加子菜单',
key: 'son',
disabled: isAddSon,
},
],
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,