This commit is contained in:
ahjung
2024-09-14 22:39:11 +08:00
parent 1ae5372396
commit 7a62de39c2
62 changed files with 1996 additions and 2938 deletions

View File

@@ -1,9 +1,35 @@
import { http } from '@/utils/http/axios';
import { Alova } from '@/utils/http/alova/index';
export interface TypeVisits {
dayVisits: number;
rise: number;
decline: number;
amount: number;
}
export interface TypeSaleroom {
weekSaleroom: number;
amount: number;
degree: number;
}
export interface TypeOrderLarge {
weekLarge: number;
rise: number;
decline: number;
amount: number;
}
export interface TypeConsole {
visits: TypeVisits;
//销售额
saleroom: TypeSaleroom;
//订单量
orderLarge: TypeOrderLarge;
//成交额度
volume: TypeOrderLarge;
}
//获取主控台信息
export function getConsoleInfo() {
return http.request({
url: '/dashboard/console',
method: 'get',
});
return Alova.Get<TypeConsole>('/dashboard/console');
}

View File

@@ -1,13 +1,11 @@
import { http } from '@/utils/http/axios';
import { Alova } from '@/utils/http/alova/index';
import { ListDate } from 'mock/system/menu';
/**
* @description: 根据用户id获取用户菜单
*/
export function adminMenus() {
return http.request({
url: '/menus',
method: 'GET',
});
return Alova.Get('/menus');
}
/**
@@ -15,9 +13,7 @@ export function adminMenus() {
* @param params
*/
export function getMenuList(params?) {
return http.request({
url: '/menu/list',
method: 'GET',
return Alova.Get<{ list: ListDate[] }>('/menu/list', {
params,
});
}

View File

@@ -1,11 +1,8 @@
import { http } from '@/utils/http/axios';
import { Alova } from '@/utils/http/alova/index';
/**
* @description: 角色列表
*/
export function getRoleList() {
return http.request({
url: '/role/list',
method: 'GET',
});
export function getRoleList(params) {
return Alova.Get('/role/list', { params });
}

View File

@@ -1,24 +1,13 @@
import { http } from '@/utils/http/axios';
export interface BasicResponseModel<T = any> {
code: number;
message: string;
result: T;
}
export interface BasicPageParams {
pageNumber: number;
pageSize: number;
total: number;
}
import { Alova } from '@/utils/http/alova/index';
/**
* @description: 获取用户信息
*/
export function getUserInfo() {
return http.request({
url: '/admin_info',
method: 'get',
return Alova.Get<InResult>('/admin_info', {
meta: {
isReturnNativeResponse: true,
},
});
}
@@ -26,14 +15,15 @@ export function getUserInfo() {
* @description: 用户登录
*/
export function login(params) {
return http.request<BasicResponseModel>(
return Alova.Post<InResult>(
'/login',
{
url: '/login',
method: 'POST',
params,
},
{
isTransformResponse: false,
meta: {
isReturnNativeResponse: true,
},
}
);
}
@@ -42,25 +32,14 @@ export function login(params) {
* @description: 用户修改密码
*/
export function changePassword(params, uid) {
return http.request(
{
url: `/user/u${uid}/changepw`,
method: 'POST',
params,
},
{
isTransformResponse: false,
}
);
return Alova.Post(`/user/u${uid}/changepw`, { params });
}
/**
* @description: 用户登出
*/
export function logout(params) {
return http.request({
url: '/login/logout',
method: 'POST',
return Alova.Post('/login/logout', {
params,
});
}

View File

@@ -1,10 +1,6 @@
import { http } from '@/utils/http/axios';
import { Alova } from '@/utils/http/alova/index';
//获取table
export function getTableList(params) {
return http.request({
url: '/table/list',
method: 'get',
params,
});
return Alova.Get('/table/list', { params });
}

View File

@@ -3,12 +3,14 @@
{{ value }}
</span>
</template>
<script lang="ts">
import { defineComponent, ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
<script lang="ts" setup>
import { ref, computed, watchEffect, unref, onMounted, watch } from 'vue';
import { useTransition, TransitionPresets } from '@vueuse/core';
import { isNumber } from '@/utils/is';
const props = {
defineOptions({ name: 'CountTo' });
const props = defineProps({
startVal: { type: Number, default: 0 },
endVal: { type: Number, default: 2021 },
duration: { type: Number, default: 1500 },
@@ -36,75 +38,72 @@
* Digital animation
*/
transition: { type: String, default: 'linear' },
};
});
export default defineComponent({
name: 'CountTo',
props,
emits: ['onStarted', 'onFinished'],
setup(props, { emit }) {
const source = ref(props.startVal);
const disabled = ref(false);
let outputValue = useTransition(source);
const emit = defineEmits(['onStarted', 'onFinished']);
const value = computed(() => formatNumber(unref(outputValue)));
const source = ref(props.startVal);
const disabled = ref(false);
let outputValue = useTransition(source);
watchEffect(() => {
source.value = props.startVal;
});
const value = computed(() => formatNumber(unref(outputValue)));
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
watchEffect(() => {
source.value = props.startVal;
});
onMounted(() => {
props.autoplay && start();
});
watch([() => props.startVal, () => props.endVal], () => {
if (props.autoplay) {
start();
}
});
function start() {
run();
source.value = props.endVal;
onMounted(() => {
props.autoplay && start();
});
function start() {
run();
source.value = props.endVal;
}
function reset() {
source.value = props.startVal;
run();
}
function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onFinished: () => emit('onFinished'),
onStarted: () => emit('onStarted'),
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
});
}
function formatNumber(num: number | string) {
if (!num && num !== 0) {
return '';
}
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += '';
const x = num.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2');
}
}
return prefix + x1 + x2 + suffix;
}
function reset() {
source.value = props.startVal;
run();
}
function run() {
outputValue = useTransition(source, {
disabled,
duration: props.duration,
onFinished: () => emit('onFinished'),
onStarted: () => emit('onStarted'),
...(props.useEasing ? { transition: TransitionPresets[props.transition] } : {}),
});
}
function formatNumber(num: number | string) {
if (!num) {
return '';
}
const { decimals, decimal, separator, suffix, prefix } = props;
num = Number(num).toFixed(decimals);
num += '';
const x = num.split('.');
let x1 = x[0];
const x2 = x.length > 1 ? decimal + x[1] : '';
const rgx = /(\d+)(\d{3})/;
if (separator && !isNumber(separator)) {
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + separator + '$2');
}
}
return prefix + x1 + x2 + suffix;
}
return { value, start, reset };
},
defineExpose({
reset,
});
</script>

View File

@@ -2,23 +2,23 @@
<div class="table-toolbar">
<!--顶部左侧区域-->
<div class="flex items-center table-toolbar-left">
<template v-if="title">
<template v-if="props.title">
<div class="table-toolbar-left-title">
{{ title }}
<n-tooltip trigger="hover" v-if="titleTooltip">
{{ props.title }}
<n-tooltip trigger="hover" v-if="props.titleTooltip">
<template #trigger>
<n-icon size="18" class="ml-1 text-gray-400 cursor-pointer">
<QuestionCircleOutlined />
</n-icon>
</template>
{{ titleTooltip }}
{{ props.titleTooltip }}
</n-tooltip>
</div>
</template>
<slot name="tableTitle"></slot>
</div>
<div class="flex items-center table-toolbar-right">
<div class="flex items-center leading-none table-toolbar-right">
<!--顶部右侧区域-->
<slot name="toolbar"></slot>
@@ -84,18 +84,8 @@
</div>
</template>
<script lang="ts">
import {
ref,
defineComponent,
reactive,
unref,
toRaw,
computed,
toRefs,
onMounted,
nextTick,
} from 'vue';
<script lang="ts" setup>
import { ref, unref, toRaw, computed, onMounted, nextTick } from 'vue';
import { ReloadOutlined, ColumnHeightOutlined, QuestionCircleOutlined } from '@vicons/antd';
import { createTableContext } from './hooks/useTableContext';
@@ -132,181 +122,150 @@
},
];
export default defineComponent({
components: {
ReloadOutlined,
ColumnHeightOutlined,
ColumnSetting,
QuestionCircleOutlined,
},
props: {
...basicProps,
},
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 isStriped = ref(props.striped || false);
const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>();
const emit = defineEmits([
'fetch-success',
'fetch-error',
'update:checked-row-keys',
'edit-end',
'edit-cancel',
'edit-row-end',
'edit-change',
]);
const getProps = computed(() => {
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
});
const props = defineProps({ ...basicProps });
const deviceHeight = ref(150);
const tableElRef = ref<ComponentRef>(null);
const wrapRef = ref<Nullable<HTMLDivElement>>(null);
let paginationEl: HTMLElement | null;
const isStriped = ref(props.striped || false);
const tableData = ref<Recordable[]>([]);
const innerPropsRef = ref<Partial<BasicTableProps>>();
const { getLoading, setLoading } = useLoading(getProps);
const { getPaginationInfo, setPagination } = usePagination(getProps);
const { getDataSourceRef, getDataSource, getRowKey, reload } = useDataSource(
getProps,
{
getPaginationInfo,
setPagination,
tableData,
setLoading,
},
emit
);
const { getPageColumns, setColumns, getColumns, getCacheColumns, setCacheColumnsField } =
useColumns(getProps);
const state = reactive({
tableSize: unref(getProps as any).size || 'medium',
isColumnSetting: false,
});
//页码切换
function updatePage(page) {
setPagination({ page: page });
reload();
}
//分页数量切换
function updatePageSize(size) {
setPagination({ page: 1, pageSize: size });
reload();
}
//密度切换
function densitySelect(e) {
state.tableSize = e;
}
//选中行
function updateCheckedRowKeys(rowKeys) {
emit('update:checked-row-keys', rowKeys);
}
//获取表格大小
const getTableSize = computed(() => state.tableSize);
//组装表格信息
const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef);
const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : 'auto';
return {
...unref(getProps),
loading: unref(getLoading),
columns: toRaw(unref(getPageColumns)),
rowKey: unref(getRowKey),
data: tableData,
size: unref(getTableSize),
remote: true,
'max-height': maxHeight,
};
});
//获取分页信息
const pagination = computed(() => toRaw(unref(getPaginationInfo)));
function setProps(props: Partial<BasicTableProps>) {
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const setStriped = (value: boolean) => (isStriped.value = value);
const tableAction = {
reload,
setColumns,
setLoading,
setProps,
getColumns,
getPageColumns,
getCacheColumns,
setCacheColumnsField,
emit,
};
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(unref(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,
getDataSource,
densityOptions,
reload,
densitySelect,
updatePage,
updatePageSize,
pagination,
tableAction,
setStriped,
isStriped,
};
},
const getProps = computed(() => {
return { ...props, ...unref(innerPropsRef) } as BasicTableProps;
});
const tableSize = ref(unref(getProps as any).size || 'medium');
const { getLoading, setLoading } = useLoading(getProps);
const { getPaginationInfo, setPagination } = usePagination(getProps);
const { getDataSourceRef, getDataSource, getRowKey, reload } = useDataSource(
getProps,
{
getPaginationInfo,
setPagination,
tableData,
setLoading,
},
emit
);
const { getPageColumns, setColumns, getColumns, getCacheColumns, setCacheColumnsField } =
useColumns(getProps);
//页码切换
function updatePage(page) {
setPagination({ page: page });
reload();
}
//分页数量切换
function updatePageSize(size) {
setPagination({ page: 1, pageSize: size });
reload();
}
//密度切换
function densitySelect(e) {
tableSize.value = e;
}
//获取表格大小
const getTableSize = computed(() => tableSize.value);
//组装表格信息
const getBindValues = computed(() => {
const tableData = unref(getDataSourceRef);
const maxHeight = tableData.length ? `${unref(deviceHeight)}px` : 'auto';
return {
...unref(getProps),
loading: unref(getLoading),
columns: toRaw(unref(getPageColumns)),
rowKey: unref(getRowKey),
data: tableData,
size: unref(getTableSize),
remote: true,
'max-height': maxHeight,
title: '', // 重置为空 避免绑定到 table 上面
};
});
//获取分页信息
const pagination = computed(() => toRaw(unref(getPaginationInfo)));
function setProps(props: Partial<BasicTableProps>) {
innerPropsRef.value = { ...unref(innerPropsRef), ...props };
}
const setStriped = (value: boolean) => (isStriped.value = value);
const tableAction = {
reload,
setColumns,
setLoading,
setProps,
getColumns,
getDataSource,
getPageColumns,
getCacheColumns,
setCacheColumnsField,
emit,
};
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(unref(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 });
defineExpose(tableAction);
</script>
<style lang="less" scoped>
.table-toolbar {

View File

@@ -15,10 +15,10 @@
/>
</div>
<div class="editable-cell-action" v-if="!getRowEditable">
<n-icon class="mx-2 cursor-pointer">
<n-icon class="mx-2 cursor-pointer" title="保存">
<CheckOutlined @click="handleSubmit" />
</n-icon>
<n-icon class="mx-2 cursor-pointer">
<n-icon class="mx-2 cursor-pointer" title="取消">
<CloseOutlined @click="handleCancel" />
</n-icon>
</div>

View File

@@ -60,7 +60,7 @@ export function useColumns(propsRef: ComputedRef<BasicTableProps>) {
const title: any = column.title;
column.title = () => {
return renderTooltip(
h('span', {}, [
h('div', { class: 'flex items-center' }, [
h('span', { style: { 'margin-right': '5px' } }, title),
h(
NIcon,

View File

@@ -10,8 +10,9 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PROD_MOCK,
VITE_GLOB_IMG_URL,
VITE_GLOB_FILE_URL,
VITE_USE_MOCK,
VITE_LOGGER_MOCK,
} = getAppEnvConfig();
if (!/[a-zA-Z\_]*/.test(VITE_GLOB_APP_SHORT_NAME)) {
@@ -27,8 +28,9 @@ export const useGlobSetting = (): Readonly<GlobConfig> => {
shortName: VITE_GLOB_APP_SHORT_NAME,
urlPrefix: VITE_GLOB_API_URL_PREFIX,
uploadUrl: VITE_GLOB_UPLOAD_URL,
prodMock: VITE_GLOB_PROD_MOCK,
imgUrl: VITE_GLOB_IMG_URL,
fileUrl: VITE_GLOB_FILE_URL,
useMock: VITE_USE_MOCK,
loggerMock: VITE_LOGGER_MOCK,
};
return glob as Readonly<GlobConfig>;
};

View File

@@ -101,8 +101,7 @@
<div class="layout-header-trigger layout-header-trigger-min">
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
<div class="avatar">
<n-avatar round :src="websiteConfig.logo">
<n-avatar :src="websiteConfig.logo">
<template #icon>
<UserOutlined />
</template>

View File

@@ -5,17 +5,6 @@ import { renderIcon, renderNew } from '@/utils';
const routeName = 'comp';
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
*
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/comp',

View File

@@ -5,16 +5,6 @@ import { renderIcon } from '@/utils/index';
const routeName = 'dashboard';
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/dashboard',

View File

@@ -3,17 +3,6 @@ import { Layout } from '@/router/constant';
import { ExclamationCircleOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index';
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
*
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/exception',

View File

@@ -3,17 +3,6 @@ import { Layout } from '@/router/constant';
import { ProfileOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index';
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
*
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/form',

View File

@@ -3,17 +3,6 @@ import { Layout } from '@/router/constant';
import { TableOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index';
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
*
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/list',

View File

@@ -3,17 +3,6 @@ import { Layout } from '@/router/constant';
import { CheckCircleOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index';
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
*
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/result',

View File

@@ -3,17 +3,6 @@ import { Layout } from '@/router/constant';
import { SettingOutlined } from '@vicons/antd';
import { renderIcon } from '@/utils/index';
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
*
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/setting',

View File

@@ -3,17 +3,6 @@ import { Layout } from '@/router/constant';
import { OptionsSharp } from '@vicons/ionicons5';
import { renderIcon } from '@/utils/index';
/**
* @param name 路由名称, 必须设置,且不能重名
* @param meta 路由元信息(路由附带扩展信息)
* @param redirect 重定向地址, 访问这个路由时,自定进行重定向
* @param meta.disabled 禁用整个菜单
* @param meta.title 菜单名称
* @param meta.icon 菜单图标
* @param meta.keepAlive 缓存该路由
* @param meta.sort 排序越小越排前
*
* */
const routes: Array<RouteRecordRaw> = [
{
path: '/system',
@@ -30,7 +19,7 @@ const routes: Array<RouteRecordRaw> = [
path: 'menu',
name: 'system_menu',
meta: {
title: '菜单权限管理',
title: '菜单权限',
},
component: () => import('@/views/system/menu/menu.vue'),
},
@@ -38,7 +27,7 @@ const routes: Array<RouteRecordRaw> = [
path: 'role',
name: 'system_role',
meta: {
title: '角色权限管理',
title: '角色权限',
},
component: () => import('@/views/system/role/role.vue'),
},

View File

@@ -78,7 +78,8 @@ export const useUserStore = defineStore({
// 获取用户信息
async getInfo() {
const result = await getUserInfoApi();
const data = await getUserInfoApi();
const { result } = data;
if (result.permissions && result.permissions.length) {
const permissionsList = result.permissions;
this.setPermissions(permissionsList);

View File

@@ -28,8 +28,9 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PROD_MOCK,
VITE_GLOB_IMG_URL,
VITE_GLOB_FILE_URL,
VITE_USE_MOCK,
VITE_LOGGER_MOCK,
} = ENV;
if (!/^[a-zA-Z\_]*$/.test(VITE_GLOB_APP_SHORT_NAME)) {
@@ -44,8 +45,9 @@ export function getAppEnvConfig() {
VITE_GLOB_APP_SHORT_NAME,
VITE_GLOB_API_URL_PREFIX,
VITE_GLOB_UPLOAD_URL,
VITE_GLOB_PROD_MOCK,
VITE_GLOB_IMG_URL,
VITE_GLOB_FILE_URL,
VITE_USE_MOCK,
VITE_LOGGER_MOCK,
};
}

View File

@@ -0,0 +1,124 @@
import { createAlova } from 'alova';
import VueHook from 'alova/vue';
import adapterFetch from 'alova/fetch';
import { createAlovaMockAdapter } from '@alova/mock';
import { isString } from 'lodash-es';
import mocks from './mocks';
import { useUser } from '@/store/modules/user';
import { storage } from '@/utils/Storage';
import { useGlobSetting } from '@/hooks/setting';
import { PageEnum } from '@/enums/pageEnum';
import { ResultEnum } from '@/enums/httpEnum';
import { isUrl } from '@/utils';
const { useMock, apiUrl, urlPrefix, loggerMock } = useGlobSetting();
const mockAdapter = createAlovaMockAdapter([...mocks], {
// 全局控制是否启用mock接口默认为true
enable: useMock,
// 非模拟请求适配器用于未匹配mock接口时发送请求
httpAdapter: adapterFetch(),
// mock接口响应延迟单位毫秒
delay: 1000,
// 自定义打印mock接口请求信息
// mockRequestLogger: (res) => {
// loggerMock && console.log(`Mock Request ${res.url}`, res);
// },
mockRequestLogger: loggerMock,
onMockError(error, currentMethod) {
console.error('🚀 ~ onMockError ~ currentMethod:', currentMethod);
console.error('🚀 ~ onMockError ~ error:', error);
},
});
export const Alova = createAlova({
baseURL: apiUrl,
statesHook: VueHook,
// 关闭全局请求缓存
// cacheFor: null,
// 全局缓存配置
// cacheFor: {
// POST: {
// mode: 'memory',
// expire: 60 * 10 * 1000
// },
// GET: {
// mode: 'memory',
// expire: 60 * 10 * 1000
// },
// HEAD: 60 * 10 * 1000 // 统一设置HEAD请求的缓存模式
// },
// 在开发环境开启缓存命中日志
cacheLogger: process.env.NODE_ENV === 'development',
requestAdapter: mockAdapter,
beforeRequest(method) {
const userStore = useUser();
const token = userStore.getToken;
// 添加 token 到请求头
if (!method.meta?.ignoreToken && token) {
method.config.headers['token'] = token;
}
// 处理 api 请求前缀
const isUrlStr = isUrl(method.url as string);
if (!isUrlStr && urlPrefix) {
method.url = `${urlPrefix}${method.url}`;
}
if (!isUrlStr && apiUrl && isString(apiUrl)) {
method.url = `${apiUrl}${method.url}`;
}
},
responded: {
onSuccess: async (response, method) => {
const res = (response.json && (await response.json())) || response.body;
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (method.meta?.isReturnNativeResponse) {
return res;
}
// 请根据自身情况修改数据结构
const { message, code, result } = res;
// 不进行任何处理,直接返回
// 用于需要直接获取 code、result、 message 这些信息时开启
if (method.meta?.isTransformResponse === false) {
return res.data;
}
// @ts-ignore
const Message = window.$message;
// @ts-ignore
const Modal = window.$dialog;
const LoginPath = PageEnum.BASE_LOGIN;
if (ResultEnum.SUCCESS === code) {
return result;
}
// 需要登录
if (code === 912) {
Modal?.warning({
title: '提示',
content: '登录身份已失效,请重新登录!',
okText: '确定',
closable: false,
maskClosable: false,
onOk: async () => {
storage.clear();
window.location.href = LoginPath;
},
});
} else {
// 可按需处理错误 一般情况下不是 912 错误,不一定需要弹出 message
Message?.error(message);
throw new Error(message);
}
},
},
});
// 项目,多个不同 api 地址,可导出多个实例
// export const AlovaTwo = createAlova({
// baseURL: 'http://localhost:9001',
// });

View File

@@ -0,0 +1,10 @@
// 这里按需导入 mock 文件,只有在这里导入并导出,才会执行 mock 拦截
// 跟根据实际开发情况配置
import UserMock from '../../../../mock/user';
import MenusMock from '../../../../mock/user/menus';
import ConsoleMock from '../../../../mock/dashboard/console';
import TableMock from '../../../../mock/table/list';
import SystemMenuMock from '../../../../mock/system/menu';
import SystemRoleMock from '../../../../mock/system/role';
export default [UserMock, MenusMock, TableMock, ConsoleMock, SystemMenuMock, SystemRoleMock];

View File

@@ -1,200 +0,0 @@
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios';
import axios from 'axios';
import { AxiosCanceler } from './axiosCancel';
import { isFunction } from '@/utils/is';
import { cloneDeep } from 'lodash-es';
import type { RequestOptions, CreateAxiosOptions, Result, UploadFileParams } from './types';
import { ContentTypeEnum } from '@/enums/httpEnum';
export * from './axiosTransform';
/**
* @description: axios模块
*/
export class VAxios {
private axiosInstance: AxiosInstance;
private options: CreateAxiosOptions;
constructor(options: CreateAxiosOptions) {
this.options = options;
this.axiosInstance = axios.create(options);
this.setupInterceptors();
}
getAxios(): AxiosInstance {
return this.axiosInstance;
}
/**
* @description: 重新配置axios
*/
configAxios(config: CreateAxiosOptions) {
if (!this.axiosInstance) {
return;
}
this.createAxios(config);
}
/**
* @description: 设置通用header
*/
setHeader(headers: any): void {
if (!this.axiosInstance) {
return;
}
Object.assign(this.axiosInstance.defaults.headers, headers);
}
/**
* @description: 请求方法
*/
request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
let conf: AxiosRequestConfig = cloneDeep(config);
const transform = this.getTransform();
const { requestOptions } = this.options;
const opt: RequestOptions = Object.assign({}, requestOptions, options);
const { beforeRequestHook, requestCatch, transformRequestData } = transform || {};
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt);
}
//这里重新 赋值成最新的配置
// @ts-ignore
conf.requestOptions = opt;
return new Promise((resolve, reject) => {
this.axiosInstance
.request<any, AxiosResponse<Result>>(conf)
.then((res: AxiosResponse<Result>) => {
// 请求是否被取消
const isCancel = axios.isCancel(res);
if (transformRequestData && isFunction(transformRequestData) && !isCancel) {
try {
const ret = transformRequestData(res, opt);
resolve(ret);
} catch (err) {
reject(err || new Error('request error!'));
}
return;
}
resolve(res as unknown as Promise<T>);
})
.catch((e: Error) => {
if (requestCatch && isFunction(requestCatch)) {
reject(requestCatch(e));
return;
}
reject(e);
});
});
}
/**
* @description: 创建axios实例
*/
private createAxios(config: CreateAxiosOptions): void {
this.axiosInstance = axios.create(config);
}
private getTransform() {
const { transform } = this.options;
return transform;
}
/**
* @description: 文件上传
*/
uploadFile<T = any>(config: AxiosRequestConfig, params: UploadFileParams) {
const formData = new window.FormData();
const customFilename = params.name || 'file';
if (params.filename) {
formData.append(customFilename, params.file, params.filename);
} else {
formData.append(customFilename, params.file);
}
if (params.data) {
Object.keys(params.data).forEach((key) => {
const value = params.data![key];
if (Array.isArray(value)) {
value.forEach((item) => {
formData.append(`${key}[]`, item);
});
return;
}
formData.append(key, params.data![key]);
});
}
return this.axiosInstance.request<T>({
method: 'POST',
data: formData,
headers: {
'Content-type': ContentTypeEnum.FORM_DATA,
ignoreCancelToken: true,
},
...config,
});
}
/**
* @description: 拦截器配置
*/
private setupInterceptors() {
const transform = this.getTransform();
if (!transform) {
return;
}
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch,
} = transform;
const axiosCanceler = new AxiosCanceler();
// 请求拦截器配置处理
this.axiosInstance.interceptors.request.use((config: AxiosRequestConfig) => {
const {
headers: { ignoreCancelToken },
} = config;
const ignoreCancel =
ignoreCancelToken !== undefined
? ignoreCancelToken
: this.options.requestOptions?.ignoreCancelToken;
!ignoreCancel && axiosCanceler.addPending(config);
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options);
}
return config;
}, undefined);
// 请求拦截器错误捕获
requestInterceptorsCatch &&
isFunction(requestInterceptorsCatch) &&
this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);
// 响应结果拦截器处理
this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
res && axiosCanceler.removePending(res.config);
if (responseInterceptors && isFunction(responseInterceptors)) {
res = responseInterceptors(res);
}
return res;
}, undefined);
// 响应结果拦截器错误捕获
responseInterceptorsCatch &&
isFunction(responseInterceptorsCatch) &&
this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
}
}

View File

@@ -1,61 +0,0 @@
import axios, { AxiosRequestConfig, Canceler } from 'axios';
import qs from 'qs';
import { isFunction } from '@/utils/is/index';
// 声明一个 Map 用于存储每个请求的标识 和 取消函数
let pendingMap = new Map<string, Canceler>();
export const getPendingUrl = (config: AxiosRequestConfig) =>
[config.method, config.url, qs.stringify(config.data), qs.stringify(config.params)].join('&');
export class AxiosCanceler {
/**
* 添加请求
* @param {Object} config
*/
addPending(config: AxiosRequestConfig) {
this.removePending(config);
const url = getPendingUrl(config);
config.cancelToken =
config.cancelToken ||
new axios.CancelToken((cancel) => {
if (!pendingMap.has(url)) {
// 如果 pending 中不存在当前请求,则添加进去
pendingMap.set(url, cancel);
}
});
}
/**
* @description: 清空所有pending
*/
removeAllPending() {
pendingMap.forEach((cancel) => {
cancel && isFunction(cancel) && cancel();
});
pendingMap.clear();
}
/**
* 移除请求
* @param {Object} config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
// 如果在 pending 中存在当前请求标识,需要取消当前请求,并且移除
const cancel = pendingMap.get(url);
cancel && cancel(url);
pendingMap.delete(url);
}
}
/**
* @description: 重置
*/
reset(): void {
pendingMap = new Map<string, Canceler>();
}
}

View File

@@ -1,52 +0,0 @@
/**
* 数据处理类,可以根据项目自行配置
*/
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import type { RequestOptions, Result } from './types';
export interface CreateAxiosOptions extends AxiosRequestConfig {
authenticationScheme?: string;
transform?: AxiosTransform;
requestOptions?: RequestOptions;
}
export abstract class AxiosTransform {
/**
* @description: 请求之前处理配置
* @description: Process configuration before request
*/
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
/**
* @description: 请求成功处理
*/
transformRequestData?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
/**
* @description: 请求失败处理
*/
requestCatch?: (e: Error) => Promise<any>;
/**
* @description: 请求之前的拦截器
*/
requestInterceptors?: (
config: AxiosRequestConfig,
options: CreateAxiosOptions
) => AxiosRequestConfig;
/**
* @description: 请求之后的拦截器
*/
responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
/**
* @description: 请求之前的拦截器错误处理
*/
requestInterceptorsCatch?: (error: Error) => void;
/**
* @description: 请求之后的拦截器错误处理
*/
responseInterceptorsCatch?: (error: Error) => void;
}

View File

@@ -1,47 +0,0 @@
export function checkStatus(status: number, msg: string): void {
const $message = window['$message'];
switch (status) {
case 400:
$message.error(msg);
break;
// 401: 未登录
// 未登录则跳转登录页面,并携带当前页面的路径
// 在登录成功后返回当前页面,这一步需要在登录页操作。
case 401:
$message.error('用户没有权限(令牌、用户名、密码错误)!');
break;
case 403:
$message.error('用户得到授权,但是访问是被禁止的。!');
break;
// 404请求不存在
case 404:
$message.error('网络请求错误,未找到该资源!');
break;
case 405:
$message.error('网络请求错误,请求方法未允许!');
break;
case 408:
$message.error('网络请求超时');
break;
case 500:
$message.error('服务器错误,请联系管理员!');
break;
case 501:
$message.error('网络未实现');
break;
case 502:
$message.error('网络错误');
break;
case 503:
$message.error('服务不可用,服务器暂时过载或维护!');
break;
case 504:
$message.error('网络超时');
break;
case 505:
$message.error('http版本不支持该请求!');
break;
default:
$message.error(msg);
}
}

View File

@@ -1,47 +0,0 @@
import { isObject, isString } from '@/utils/is';
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
export function joinTimestamp<T extends boolean>(
join: boolean,
restful: T
): T extends true ? string : object;
export function joinTimestamp(join: boolean, restful = false): string | object {
if (!join) {
return restful ? '' : {};
}
const now = new Date().getTime();
if (restful) {
return `?_t=${now}`;
}
return { _t: now };
}
/**
* @description: Format request parameter time
*/
export function formatRequestDate(params: Recordable) {
if (Object.prototype.toString.call(params) !== '[object Object]') {
return;
}
for (const key in params) {
if (params[key] && params[key]._isAMomentObject) {
params[key] = params[key].format(DATE_TIME_FORMAT);
}
if (isString(key)) {
const value = params[key];
if (value) {
try {
params[key] = isString(value) ? value.trim() : value;
} catch (error) {
throw new Error(error as any);
}
}
}
if (isObject(params[key])) {
formatRequestDate(params[key]);
}
}
}

View File

@@ -1,287 +0,0 @@
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
import { VAxios } from './Axios';
import { AxiosTransform } from './axiosTransform';
import axios, { AxiosResponse } from 'axios';
import { checkStatus } from './checkStatus';
import { joinTimestamp, formatRequestDate } from './helper';
import { RequestEnum, ResultEnum, ContentTypeEnum } from '@/enums/httpEnum';
import { PageEnum } from '@/enums/pageEnum';
import { useGlobSetting } from '@/hooks/setting';
import { isString } from '@/utils/is/';
import { deepMerge, isUrl } from '@/utils';
import { setObjToUrlParams } from '@/utils/urlUtils';
import { RequestOptions, Result, CreateAxiosOptions } from './types';
import { useUser } from '@/store/modules/user';
const globSetting = useGlobSetting();
const urlPrefix = globSetting.urlPrefix || '';
import router from '@/router';
import { storage } from '@/utils/Storage';
/**
* @description: 数据处理,方便区分多种处理方式
*/
const transform: AxiosTransform = {
/**
* @description: 处理请求数据
*/
transformRequestData: (res: AxiosResponse<Result>, options: RequestOptions) => {
const {
isShowMessage = true,
isShowErrorMessage,
isShowSuccessMessage,
successMessageText,
errorMessageText,
isTransformResponse,
isReturnNativeResponse,
} = options;
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res;
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
if (!isTransformResponse) {
return res.data;
}
const { data } = res;
const $dialog = window['$dialog'];
const $message = window['$message'];
if (!data) {
// return '[HTTP] Request has no return value';
throw new Error('请求出错,请稍候重试');
}
// 这里 coderesultmessage为 后台统一的字段,需要修改为项目自己的接口返回格式
const { code, result, message } = data;
// 请求成功
const hasSuccess = data && Reflect.has(data, 'code') && code === ResultEnum.SUCCESS;
// 是否显示提示信息
if (isShowMessage) {
if (hasSuccess && (successMessageText || isShowSuccessMessage)) {
// 是否显示自定义信息提示
$dialog.success({
type: 'success',
content: successMessageText || message || '操作成功!',
});
} else if (!hasSuccess && (errorMessageText || isShowErrorMessage)) {
// 是否显示自定义信息提示
$message.error(message || errorMessageText || '操作失败!');
} else if (!hasSuccess && options.errorMessageMode === 'modal') {
// errorMessageMode=custom-modal的时候会显示modal错误弹窗而不是消息提示用于一些比较重要的错误
$dialog.info({
title: '提示',
content: message,
positiveText: '确定',
onPositiveClick: () => {},
});
}
}
// 接口请求成功,直接返回结果
if (code === ResultEnum.SUCCESS) {
return result;
}
// 接口请求错误,统一提示错误信息 这里逻辑可以根据项目进行修改
let errorMsg = message;
switch (code) {
// 请求失败
case ResultEnum.ERROR:
$message.error(errorMsg);
break;
// 登录超时
case ResultEnum.TIMEOUT:
const LoginName = PageEnum.BASE_LOGIN_NAME;
const LoginPath = PageEnum.BASE_LOGIN;
if (router.currentRoute.value?.name === LoginName) return;
// 到登录页
errorMsg = '登录超时,请重新登录!';
$dialog.warning({
title: '提示',
content: '登录身份已失效,请重新登录!',
positiveText: '确定',
//negativeText: '取消',
closable: false,
maskClosable: false,
onPositiveClick: () => {
storage.clear();
window.location.href = LoginPath;
},
onNegativeClick: () => {},
});
break;
}
throw new Error(errorMsg);
},
// 请求之前处理config
beforeRequestHook: (config, options) => {
const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
const isUrlStr = isUrl(config.url as string);
if (!isUrlStr && joinPrefix) {
config.url = `${urlPrefix}${config.url}`;
}
if (!isUrlStr && apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`;
}
const params = config.params || {};
const data = config.data || false;
if (config.method?.toUpperCase() === RequestEnum.GET) {
if (!isString(params)) {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
} else {
// 兼容restful风格
config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
config.params = undefined;
}
} else {
if (!isString(params)) {
formatDate && formatRequestDate(params);
if (Reflect.has(config, 'data') && config.data && Object.keys(config.data).length > 0) {
config.data = data;
config.params = params;
} else {
config.data = params;
config.params = undefined;
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(
config.url as string,
Object.assign({}, config.params, config.data)
);
}
} else {
// 兼容restful风格
config.url = config.url + params;
config.params = undefined;
}
}
return config;
},
/**
* @description: 请求拦截器处理
*/
requestInterceptors: (config, options) => {
// 请求之前处理config
const userStore = useUser();
const token = userStore.getToken;
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
// jwt token
(config as Recordable).headers.Authorization = options.authenticationScheme
? `${options.authenticationScheme} ${token}`
: token;
}
return config;
},
/**
* @description: 响应错误处理
*/
responseInterceptorsCatch: (error: any) => {
const $dialog = window['$dialog'];
const $message = window['$message'];
const { response, code, message } = error || {};
// TODO 此处要根据后端接口返回格式修改
const msg: string =
response && response.data && response.data.message ? response.data.message : '';
const err: string = error.toString();
try {
if (code === 'ECONNABORTED' && message.indexOf('timeout') !== -1) {
$message.error('接口请求超时,请刷新页面重试!');
return;
}
if (err && err.includes('Network Error')) {
$dialog.info({
title: '网络异常',
content: '请检查您的网络连接是否正常',
positiveText: '确定',
//negativeText: '取消',
closable: false,
maskClosable: false,
onPositiveClick: () => {},
onNegativeClick: () => {},
});
return Promise.reject(error);
}
} catch (error) {
throw new Error(error as any);
}
// 请求是否被取消
const isCancel = axios.isCancel(error);
if (!isCancel) {
checkStatus(error.response && error.response.status, msg);
} else {
console.warn(error, '请求被取消!');
}
//return Promise.reject(error);
return Promise.reject(response?.data);
},
};
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(
deepMerge(
{
timeout: 10 * 1000,
authenticationScheme: '',
// 接口前缀
prefixUrl: urlPrefix,
headers: { 'Content-Type': ContentTypeEnum.JSON },
// 数据处理方式
transform,
// 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: {
// 默认将prefix 添加到url
joinPrefix: true,
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理
isTransformResponse: true,
// post请求的时候添加参数到url
joinParamsToUrl: false,
// 格式化提交参数时间
formatDate: true,
// 消息提示类型
errorMessageMode: 'none',
// 接口地址
apiUrl: globSetting.apiUrl,
// 接口拼接地址
urlPrefix: urlPrefix,
// 是否加入时间戳
joinTime: true,
// 忽略重复请求
ignoreCancelToken: true,
// 是否携带token
withToken: true,
},
withCredentials: false,
},
opt || {}
)
);
}
export const http = createAxios();
// 项目,多个不同 api 地址,直接在这里导出多个
// src/api ts 里面接口,就可以单独使用这个请求,
// import { httpTwo } from '@/utils/http/axios'
// export const httpTwo = createAxios({
// requestOptions: {
// apiUrl: 'http://localhost:9001',
// urlPrefix: 'api',
// },
// });

View File

@@ -1,65 +0,0 @@
import { AxiosRequestConfig } from 'axios';
import { AxiosTransform } from './axiosTransform';
export interface CreateAxiosOptions extends AxiosRequestConfig {
transform?: AxiosTransform;
requestOptions?: RequestOptions;
authenticationScheme?: string;
}
// 上传文件
export interface UploadFileParams {
// 其他参数
data?: Recordable;
// 文件参数接口字段名
name?: string;
// 文件
file: File | Blob;
// 文件名称
filename?: string;
[key: string]: any;
}
export interface RequestOptions {
// 请求参数拼接到url
joinParamsToUrl?: boolean;
// 格式化请求参数时间
formatDate?: boolean;
// 是否显示提示信息
isShowMessage?: boolean;
// 是否解析成JSON
isParseToJson?: boolean;
// 成功的文本信息
successMessageText?: string;
// 是否显示成功信息
isShowSuccessMessage?: boolean;
// 是否显示失败信息
isShowErrorMessage?: boolean;
// 错误的文本信息
errorMessageText?: string;
// 是否加入url
joinPrefix?: boolean;
// 接口地址, 不填则使用默认apiUrl
apiUrl?: string;
// 请求拼接路径
urlPrefix?: string;
// 错误消息提示类型
errorMessageMode?: 'none' | 'modal';
// 是否添加时间戳
joinTime?: boolean;
// 不进行任何处理,直接返回
isTransformResponse?: boolean;
// 是否返回原生响应头
isReturnNativeResponse?: boolean;
//忽略重复请求
ignoreCancelToken?: boolean;
// 是否携带token
withToken?: boolean;
}
export interface Result<T = any> {
code: number;
type?: 'success' | 'error' | 'warning';
message: string;
result?: T;
}

View File

@@ -55,7 +55,7 @@
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { ref } from 'vue';
import { useMessage } from 'naive-ui';
import { basicModal, useModal } from '@/components/Modal';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';

View File

@@ -1,16 +1,34 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
import { NAvatar, NTag } from 'naive-ui';
import { BasicColumn } from '@/components/Table';
export interface ListData {
id: number;
name: string;
sex: string;
avatar: string;
email: string;
city: string;
status: string;
type: string;
createDate: string;
}
export const columns = [
const sexMap = {
male: '男',
female: '女',
unknown: '未知',
};
const statusMap = {
close: '已取消',
refuse: '已拒绝',
pass: '已通过',
};
export const columns: BasicColumn<ListData>[] = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
@@ -19,65 +37,78 @@ export const columns = [
// 默认必填校验
editRule: true,
edit: true,
width: 200,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
render(record) {
return h(NAvatar, {
size: 48,
src: row.avatar,
size: 50,
src: record.avatar,
});
},
},
{
title: '地址',
key: 'address',
title: '性别',
key: 'sex',
render(record) {
return h(
NTag,
{
type: record.sex === 'male' ? 'info' : 'error',
},
{
default: () => sexMap[record.sex],
}
);
},
},
{
title: '邮箱',
key: 'email',
width: 220,
},
{
title: '城市',
key: 'city',
editComponent: 'NSelect',
editComponentProps: {
options: [
{
label: '广东省',
label: '深圳市',
value: 1,
},
{
label: '浙江省',
label: '广州市',
value: 2,
},
],
},
edit: true,
width: 200,
ellipsis: false,
width: 220,
},
{
title: '开始日期',
key: 'beginTime',
edit: true,
width: 160,
editComponent: 'NDatePicker',
editComponentProps: {
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
title: '状态',
key: 'status',
render(record) {
return h(
NTag,
{
type:
record.status === 'close'
? 'default'
: record.status === 'refuse'
? 'error'
: 'success',
},
{
default: () => statusMap[record.status],
}
);
},
ellipsis: false,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
key: 'createDate',
},
];

View File

@@ -10,11 +10,7 @@
:actionColumn="actionColumn"
:scroll-x="1360"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
/>
</n-card>
</template>
@@ -32,18 +28,18 @@
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
name: 'NaiveAdmin',
});
const actionColumn = reactive({
width: 150,
width: 180,
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction as any, {
style: 'text',
style: 'button',
actions: createActions(record),
});
},
@@ -53,26 +49,16 @@
return [
{
label: '删除',
type: 'error',
// 配置 color 会覆盖 type
color: 'red',
icon: DeleteOutlined,
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
type: 'primary',
icon: EditOutlined,
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
];
@@ -86,10 +72,6 @@
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function handleDelete(record) {
console.log(record);
dialog.info({

View File

@@ -1,72 +1,95 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
import { BasicColumn } from '@/components/Table';
export interface ListData {
id: number;
name: string;
sex: string;
avatar: string;
email: string;
city: string;
status: string;
type: string;
createDate: string;
}
export const columns = [
const sexMap = {
male: '男',
female: '女',
unknown: '未知',
};
const statusMap = {
close: '已取消',
refuse: '已拒绝',
pass: '已通过',
};
export const columns: BasicColumn<ListData>[] = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
key: 'name',
width: 100,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
render(record) {
return h(NAvatar, {
size: 48,
src: row.avatar,
size: 50,
src: record.avatar,
});
},
},
{
title: '地址',
key: 'address',
width: 150,
title: '性别',
key: 'sex',
render(record) {
return h(
NTag,
{
type: record.sex === 'male' ? 'info' : 'error',
},
{
default: () => sexMap[record.sex],
}
);
},
},
{
title: '开始日期',
key: 'beginTime',
width: 160,
title: '邮箱',
key: 'email',
width: 220,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
title: '城市',
key: 'city',
},
{
title: '状态',
key: 'status',
width: 100,
render(row) {
render(record) {
return h(
NTag,
{
type: row.status ? 'success' : 'error',
type:
record.status === 'close'
? 'default'
: record.status === 'refuse'
? 'error'
: 'success',
},
{
default: () => (row.status ? '启用' : '禁用'),
default: () => statusMap[record.status],
}
);
},
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
key: 'createDate',
},
];

View File

@@ -11,11 +11,7 @@
@edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1360"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
/>
</n-card>
</template>
@@ -28,7 +24,7 @@
const actionRef = ref();
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
name: 'NaiveAdmin',
});
function onEditChange({ column, value, record }) {
@@ -46,11 +42,6 @@
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ value }) {
console.log(value);
}

View File

@@ -12,11 +12,7 @@
@edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1590"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
/>
</n-card>
</template>
@@ -30,7 +26,7 @@
const currentEditKeyRef = ref('');
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
name: 'NaiveAdmin',
});
const actionColumn = reactive({
@@ -101,11 +97,6 @@
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ value }) {
console.log(value);
}

View File

@@ -1,97 +1,115 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
import { NAvatar, NTag } from 'naive-ui';
import { BasicColumn } from '@/components/Table';
export interface ListData {
id: number;
name: string;
sex: string;
avatar: string;
email: string;
city: string;
status: string;
type: string;
createDate: string;
}
export const columns = [
const sexMap = {
male: '男',
female: '女',
unknown: '未知',
};
const statusMap = {
close: '已取消',
refuse: '已拒绝',
pass: '已通过',
};
export const columns: BasicColumn<ListData>[] = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
key: 'name',
editComponent: 'NInput',
editRow: true,
// 默认必填校验
editRule: true,
edit: true,
width: 200,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
render(record) {
return h(NAvatar, {
size: 48,
src: row.avatar,
size: 50,
src: record.avatar,
});
},
},
{
title: '地址',
key: 'address',
editRow: true,
title: '性别',
key: 'sex',
render(record) {
return h(
NTag,
{
type: record.sex === 'male' ? 'info' : 'error',
},
{
default: () => sexMap[record.sex],
}
);
},
},
{
title: '邮箱',
key: 'email',
width: 220,
},
{
title: '城市',
key: 'city',
editComponent: 'NSelect',
editComponentProps: {
options: [
{
label: '广东省',
label: '深圳市',
value: 1,
},
{
label: '浙江省',
label: '广州市',
value: 2,
},
],
},
edit: true,
width: 200,
ellipsis: false,
},
{
title: '开始日期',
key: 'beginTime',
editRow: true,
edit: true,
width: 240,
editComponent: 'NDatePicker',
editComponentProps: {
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
},
ellipsis: false,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
width: 220,
},
{
title: '状态',
key: 'status',
editRow: true,
edit: true,
width: 100,
editComponent: 'NSwitch',
editValueMap: (value) => {
return value ? '启用' : '禁用';
render(record) {
return h(
NTag,
{
type:
record.status === 'close'
? 'default'
: record.status === 'refuse'
? 'error'
: 'success',
},
{
default: () => statusMap[record.status],
}
);
},
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
key: 'createDate',
},
];

View File

@@ -1,59 +1,95 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
import { NAvatar, NTag } from 'naive-ui';
import { BasicColumn } from '@/components/Table';
export interface ListData {
id: string;
id: number;
name: string;
sex: string;
avatar: string;
address: string;
beginTime: string;
endTime: string;
date: string;
email: string;
city: string;
status: string;
type: string;
createDate: string;
}
const sexMap = {
male: '男',
female: '女',
unknown: '未知',
};
const statusMap = {
close: '已取消',
refuse: '已拒绝',
pass: '已通过',
};
export const columns: BasicColumn<ListData>[] = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '名称',
key: 'name',
width: 100,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
render(record) {
return h(NAvatar, {
size: 48,
src: row.avatar,
size: 50,
src: record.avatar,
});
},
},
{
title: '地址',
key: 'address',
auth: ['basic_list'], // 同时根据权限控制是否显示
ifShow: (_column) => {
return true; // 根据业务控制是否显示
title: '性别',
key: 'sex',
render(record) {
return h(
NTag,
{
type: record.sex === 'male' ? 'info' : 'error',
},
{
default: () => sexMap[record.sex],
}
);
},
width: 150,
},
{
title: '开始日期',
key: 'beginTime',
width: 160,
title: '邮箱',
key: 'email',
width: 220,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
title: '城市',
key: 'city',
},
{
title: '状态',
key: 'status',
render(record) {
return h(
NTag,
{
type:
record.status === 'close'
? 'default'
: record.status === 'refuse'
? 'error'
: 'success',
},
{
default: () => statusMap[record.status],
}
);
},
},
{
title: '创建时间',
key: 'date',
width: 100,
key: 'createDate',
},
];

View File

@@ -1,11 +1,12 @@
<template>
<n-card :bordered="false" class="proCard">
<n-card :bordered="false">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</n-card>
<n-card :bordered="false" class="mt-3">
<BasicTable
:columns="columns"
:request="loadDataTable"
@@ -27,9 +28,7 @@
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
<template #toolbar> </template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">

View File

@@ -132,6 +132,7 @@
import { getMenuList } from '@/api/system/menu';
import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue';
import { ListDate } from 'mock/system/menu';
const rules = {
label: {
@@ -155,7 +156,7 @@
let expandedKeys = ref([]);
const treeData = ref([]);
const treeData = ref<ListDate[]>([]);
const loading = ref(true);
const subLoading = ref(false);

View File

@@ -0,0 +1,68 @@
<template>
<basicModal @register="modalRegister" ref="modalRef" @on-ok="okModal">
<div class="pt-8">
<BasicForm @register="registerForm" />
</div>
</basicModal>
</template>
<script lang="ts" setup>
import { FormSchema, useForm } from '@/components/Form';
import { basicModal, useModal } from '@/components/Modal';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '角色名称',
componentProps: {
placeholder: '请输入角色名称',
},
rules: [{ required: true, message: '请输入角色名称', trigger: ['blur'] }],
},
{
field: 'explain',
component: 'NInput',
label: '角色说明',
componentProps: {
type: 'textarea',
placeholder: '请输入角色角色说明',
},
},
{
field: 'isDefault',
component: 'NSwitch',
label: '默认角色',
componentProps: {},
},
];
const [registerForm, { submit }] = useForm({
gridProps: { cols: 1 },
collapsedRows: 3,
labelWidth: 80,
layout: 'horizontal',
submitButtonText: '保存',
showActionButtonGroup: false,
schemas,
});
const [modalRegister, { openModal, closeModal, setSubLoading }] = useModal({
title: '新增角色',
subBtuText: '保存',
});
async function okModal() {
const formRes = await submit();
if (formRes) {
closeModal();
console.log('formRes', formRes);
} else {
setSubLoading(false);
}
}
defineExpose({
openModal,
});
</script>

View File

@@ -0,0 +1,76 @@
<template>
<basicModal @register="modalRegister" ref="modalRef" @on-ok="okModal">
<div class="pt-8">
<BasicForm @register="registerForm" />
</div>
</basicModal>
</template>
<script lang="ts" setup>
import { nextTick } from 'vue';
import { FormSchema, useForm } from '@/components/Form';
import { basicModal, useModal } from '@/components/Modal';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '角色名称',
componentProps: {
placeholder: '请输入角色名称',
},
rules: [{ required: true, message: '请输入角色名称', trigger: ['blur'] }],
},
{
field: 'explain',
component: 'NInput',
label: '角色说明',
componentProps: {
type: 'textarea',
placeholder: '请输入角色角色说明',
},
},
{
field: 'isDefault',
component: 'NSwitch',
label: '默认角色',
componentProps: {},
},
];
const [registerForm, { submit, setFieldsValue }] = useForm({
gridProps: { cols: 1 },
collapsedRows: 3,
labelWidth: 80,
layout: 'horizontal',
submitButtonText: '保存',
showActionButtonGroup: false,
schemas,
});
const [modalRegister, { openModal, closeModal, setSubLoading }] = useModal({
title: '编辑角色',
subBtuText: '保存',
});
function showModal(record: any) {
openModal();
nextTick(() => {
record && setFieldsValue({ ...record });
});
}
async function okModal() {
const formRes = await submit();
if (formRes) {
closeModal();
console.log('formRes', formRes);
} else {
setSubLoading(false);
}
}
defineExpose({
showModal,
});
</script>

View File

@@ -15,13 +15,13 @@
@update:checked-row-keys="onCheckedRow"
>
<template #tableTitle>
<n-button type="primary">
<n-button type="primary" @click="addRole">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加角色
新增角色
</n-button>
</template>
@@ -59,6 +59,8 @@
</n-space>
</template>
</n-modal>
<CreateModal ref="createModalRef" />
<EditModal ref="editModalRef" />
</div>
</template>
@@ -71,23 +73,26 @@
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { getTreeAll } from '@/utils';
import CreateModal from './CreateModal.vue';
import EditModal from './EditModal.vue';
import { useRouter } from 'vue-router';
import { ListDate } from 'mock/system/menu';
const router = useRouter();
const message = useMessage();
const actionRef = ref();
const createModalRef = ref();
const editModalRef = ref();
const showModal = ref(false);
const formBtnLoading = ref(false);
const checkedAll = ref(false);
const editRoleTitle = ref('');
const treeData = ref([]);
const expandedKeys = ref([]);
const checkedKeys = ref(['console', 'step-form']);
const treeData = ref<ListDate[]>([]);
const expandedKeys = ref<string[]>([]);
const checkedKeys = ref<string[]>(['console', 'step-form']);
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
name: 'NaiveAdmin',
});
const actionColumn = reactive({
@@ -140,6 +145,10 @@
return await getRoleList(_params);
};
function addRole() {
createModalRef.value.openModal();
}
function onCheckedRow(rowKeys: any[]) {
console.log(rowKeys);
}
@@ -161,7 +170,8 @@
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'basic-info', params: { id: record.id } });
// router.push({ name: 'basic-info', params: { id: record.id } });
editModalRef.value.showModal(record);
}
function handleDelete(record: Recordable) {
@@ -203,8 +213,8 @@
onMounted(async () => {
const treeMenuList = await getMenuList();
expandedKeys.value = treeMenuList.list.map((item) => item.key);
treeData.value = treeMenuList.list;
expandedKeys.value = treeMenuList?.list.map((item) => item.key);
treeData.value = treeMenuList?.list;
});
</script>