feat: 新增开发与生产环境配置、重构API请求模块、优化组件自动导入体系及新增表格组件
This commit is contained in:
parent
ecb78e5982
commit
8c4f5fcbac
|
|
@ -0,0 +1,4 @@
|
|||
# 开发环境配置
|
||||
VITE_ENV=development
|
||||
VITE_API_URL=/api
|
||||
VITE_APP_TITLE=Template Admin (Dev)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# 生产环境配置
|
||||
VITE_ENV=production
|
||||
VITE_API_URL=/prod-api
|
||||
VITE_APP_TITLE=Template Admin
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
// API 请求封装
|
||||
export const fetchData = async (url: string) => {
|
||||
const response = await fetch(url);
|
||||
return response.json();
|
||||
};
|
||||
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
AdminLayout: typeof import('./components/layout/AdminLayout.vue')['default']
|
||||
AdvancedForm: typeof import('./components/form/AdvancedForm.vue')['default']
|
||||
AdvancedTable: typeof import('./components/table/AdvancedTable.vue')['default']
|
||||
Breadcrumb: typeof import('./components/common/Breadcrumb.vue')['default']
|
||||
FooterBar: typeof import('./components/layout/FooterBar.vue')['default']
|
||||
HeaderNav: typeof import('./components/layout/HeaderNav.vue')['default']
|
||||
MainContent: typeof import('./components/layout/MainContent.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SideMenu: typeof import('./components/layout/SideMenu.vue')['default']
|
||||
TabsBar: typeof import('./components/layout/TabsBar.vue')['default']
|
||||
YourComponent: typeof import('./components/YourComponent.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<script setup lang="ts">
|
||||
import { ComponentsEnum } from '@/enum/componentsEnum';
|
||||
|
||||
interface SearchBoxProps {
|
||||
searchColumns: Record<string, any>[];
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<SearchBoxProps>(), {});
|
||||
|
||||
const { searchColumns } = props;
|
||||
|
||||
const searchModel = ref<Record<string, any>>({});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'setParams', data: Record<string, any>): void;
|
||||
}>();
|
||||
|
||||
const reset = () => {
|
||||
searchModel.value = {};
|
||||
emit('setParams', searchModel.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ACard>
|
||||
<ARow :gutter="[16, 16]" wrap>
|
||||
<ACol v-for="(item, index) in searchColumns" :key="index" :span="24" :md="12" :lg="6">
|
||||
<AFormItem :label="item?.label ?? item?.title" class="m-0">
|
||||
<AInput
|
||||
v-if="item.components === ComponentsEnum.Input"
|
||||
v-model:value="searchModel[item?.alias ?? item?.dataIndex]"
|
||||
:placeholder="`请输入${item?.title}`"
|
||||
/>
|
||||
<ASelect
|
||||
v-else-if="item.components === ComponentsEnum.Select"
|
||||
v-model:value="searchModel[item?.alias ?? item?.dataIndex]"
|
||||
:placeholder="`请输入${item?.title}`"
|
||||
clearable
|
||||
v-bind="item.componentProps"
|
||||
/>
|
||||
</AFormItem>
|
||||
</ACol>
|
||||
<div class="flex-1">
|
||||
<AFormItem class="m-0">
|
||||
<div class="w-full flex-y-center justify-end gap-12px">
|
||||
<AButton @click="reset">
|
||||
<template #icon>
|
||||
<icon-ic-round-refresh class="align-sub text-icon" />
|
||||
</template>
|
||||
<span class="ml-8px">重置</span>
|
||||
</AButton>
|
||||
<AButton type="primary" ghost @click="emit('setParams', searchModel)">
|
||||
<template #icon>
|
||||
<icon-ic-round-search class="align-sub text-icon" />
|
||||
</template>
|
||||
<span class="ml-8px">搜索</span>
|
||||
</AButton>
|
||||
</div>
|
||||
</AFormItem>
|
||||
</div>
|
||||
</ARow>
|
||||
</ACard>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
<script lang="tsx" setup>
|
||||
import { Button, Popconfirm } from 'ant-design-vue';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface IProps {
|
||||
title: string;
|
||||
loading: boolean;
|
||||
columns: Record<string, any>[];
|
||||
list?: Record<string, any>[];
|
||||
tableSize?: 'small' | 'middle' | 'large';
|
||||
pagination?: Record<string, number>;
|
||||
componentProps?: Record<string, number>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<IProps>(), {
|
||||
list: () => [],
|
||||
pagination: () => ({}),
|
||||
componentProps: () => ({}),
|
||||
tableSize: 'small'
|
||||
});
|
||||
|
||||
const { columns, title, list, pagination, componentProps } = toRefs(props);
|
||||
|
||||
const searchColumns = computed(() => {
|
||||
return columns.value.filter(item => item?.search);
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'setPage', data: Record<string, any>): void;
|
||||
(e: 'setParams', data: Record<string, any>): void;
|
||||
}>();
|
||||
|
||||
const aTableRef = ref<any>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="falling-table">
|
||||
<ACard v-if="searchColumns.length" :bordered="false" class="card-wrapper" title="搜索">
|
||||
<SearchBox :search-columns="searchColumns" @set-params="emit('setParams', $event)" />
|
||||
</ACard>
|
||||
<ACard :bordered="false" :title="title">
|
||||
<template #extra>
|
||||
<slot name="table-extra"></slot>
|
||||
</template>
|
||||
<ATable
|
||||
ref="aTableRef"
|
||||
:columns="columns"
|
||||
:data-source="list"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:size="tableSize"
|
||||
v-bind="componentProps"
|
||||
@change="emit('setPage', $event)"
|
||||
>
|
||||
<template #bodyCell="{ text, record, index, column }">
|
||||
<template v-if="column.dataIndex === 'operation'">
|
||||
<div class="flex-center gap-8px">
|
||||
<div v-for="(item, key) in (column as any).actions" :key="key">
|
||||
<Popconfirm
|
||||
v-if="item.delete"
|
||||
title="确定要删除吗?"
|
||||
@confirm="item?.onClick({ record, index, column })"
|
||||
>
|
||||
<Button danger size="small">{{ item?.title }}</Button>
|
||||
</Popconfirm>
|
||||
<Button v-else size="small" type="primary" @click="item?.onClick({ record, index, column })">
|
||||
{{ item?.title }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<slot :column="column" :index="index" :record="record" :text="text" name="bodyCell"></slot>
|
||||
</template>
|
||||
</ATable>
|
||||
</ACard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.falling-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
export enum ComponentsEnum {
|
||||
Input = 'Input',
|
||||
Select = 'Select'
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
import type { AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'
|
||||
import axios from 'axios'
|
||||
|
||||
// 对应后端的Result<T>结构
|
||||
export interface Result<T = any> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
isSuccess?: boolean
|
||||
}
|
||||
|
||||
// 对应后端的ResultCode枚举
|
||||
export enum ResultCode {
|
||||
SUCCESS = 200,
|
||||
BAD_REQUEST = 400,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
NOT_FOUND = 404,
|
||||
INTERNAL_SERVER_ERROR = 500,
|
||||
SERVICE_UNAVAILABLE = 503,
|
||||
}
|
||||
|
||||
// 创建axios实例
|
||||
const service: AxiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL, // 从环境变量获取API基础URL
|
||||
timeout: 10000, // 请求超时时间
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
// 在发送请求之前做些什么,例如添加token
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers = config.headers || {}
|
||||
config.headers['Authorization'] = token
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
// 对请求错误做些什么
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
(response) => {
|
||||
const res = response.data
|
||||
|
||||
// 如果自定义code不是200,则判断为错误
|
||||
if (res.code !== ResultCode.SUCCESS) {
|
||||
// 处理特定的错误码
|
||||
switch (res.code) {
|
||||
case ResultCode.UNAUTHORIZED:
|
||||
// 未授权,跳转到登录页
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case ResultCode.FORBIDDEN:
|
||||
// 无权限,显示提示
|
||||
console.error('无权限访问')
|
||||
break
|
||||
default:
|
||||
console.error(res.msg || '请求失败')
|
||||
}
|
||||
return Promise.reject(new Error(res.msg || 'Error'))
|
||||
} else {
|
||||
// 成功请求,标记isSuccess为true
|
||||
res.isSuccess = true
|
||||
return res
|
||||
}
|
||||
},
|
||||
(error: AxiosError) => {
|
||||
// 对响应错误做点什么
|
||||
let message = error.message
|
||||
if (error.response) {
|
||||
// 处理HTTP状态码
|
||||
switch (error.response.status) {
|
||||
case 400:
|
||||
message = '请求参数错误'
|
||||
break
|
||||
case 401:
|
||||
message = '未授权,请登录'
|
||||
window.location.href = '/login'
|
||||
break
|
||||
case 403:
|
||||
message = '拒绝访问'
|
||||
break
|
||||
case 404:
|
||||
message = '请求资源不存在'
|
||||
break
|
||||
case 500:
|
||||
message = '服务器内部错误'
|
||||
break
|
||||
case 503:
|
||||
message = '服务不可用'
|
||||
break
|
||||
}
|
||||
}
|
||||
console.error(message)
|
||||
return Promise.reject(error)
|
||||
},
|
||||
)
|
||||
|
||||
// 封装通用请求方法
|
||||
export function request<T = any>(config: AxiosRequestConfig): Promise<Result<T>> {
|
||||
return service(config)
|
||||
}
|
||||
|
||||
// 封装GET请求
|
||||
export function get<T = any>(url: string, params?: any): Promise<Result<T>> {
|
||||
return request({ method: 'GET', url, params })
|
||||
}
|
||||
|
||||
// 封装POST请求
|
||||
export function post<T = any>(url: string, data?: any): Promise<Result<T>> {
|
||||
return request({ method: 'POST', url, data })
|
||||
}
|
||||
|
||||
// 封装PUT请求
|
||||
export function put<T = any>(url: string, data?: any): Promise<Result<T>> {
|
||||
return request({ method: 'PUT', url, data })
|
||||
}
|
||||
|
||||
// 封装DELETE请求
|
||||
export function del<T = any>(url: string, params?: any): Promise<Result<T>> {
|
||||
return request({ method: 'DELETE', url, params })
|
||||
}
|
||||
|
||||
export default service
|
||||
|
|
@ -66,7 +66,7 @@ declare global {
|
|||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCounterStore: typeof import('./stores/counter')['useCounterStore']
|
||||
const useCounterStore: typeof import('../stores/counter')['useCounterStore']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useId: typeof import('vue')['useId']
|
||||
|
|
@ -153,7 +153,7 @@ declare module 'vue' {
|
|||
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
|
||||
readonly unref: UnwrapRef<typeof import('vue')['unref']>
|
||||
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
|
||||
readonly useCounterStore: UnwrapRef<typeof import('./stores/counter')['useCounterStore']>
|
||||
readonly useCounterStore: UnwrapRef<typeof import('../stores/counter')['useCounterStore']>
|
||||
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
|
||||
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
|
||||
readonly useId: UnwrapRef<typeof import('vue')['useId']>
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ABadge: typeof import('ant-design-vue/es')['Badge']
|
||||
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
|
||||
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
|
||||
AButton: typeof import('ant-design-vue/es')['Button']
|
||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||
ACol: typeof import('ant-design-vue/es')['Col']
|
||||
AdminLayout: typeof import('./../components/layout/AdminLayout.vue')['default']
|
||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||
AForm: typeof import('ant-design-vue/es')['Form']
|
||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||
AInput: typeof import('ant-design-vue/es')['Input']
|
||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||
ALayout: typeof import('ant-design-vue/es')['Layout']
|
||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||
ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
|
||||
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
|
||||
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
|
||||
AMenu: typeof import('ant-design-vue/es')['Menu']
|
||||
AMenuDivider: typeof import('ant-design-vue/es')['MenuDivider']
|
||||
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
|
||||
ARow: typeof import('ant-design-vue/es')['Row']
|
||||
ASpace: typeof import('ant-design-vue/es')['Space']
|
||||
ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
|
||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||
Breadcrumb: typeof import('./../components/common/Breadcrumb.vue')['default']
|
||||
FallingTable: typeof import('./../components/fallingTable/index.vue')['default']
|
||||
FooterBar: typeof import('./../components/layout/FooterBar.vue')['default']
|
||||
HeaderNav: typeof import('./../components/layout/HeaderNav.vue')['default']
|
||||
MainContent: typeof import('./../components/layout/MainContent.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchBox: typeof import('./../components/fallingTable/components/searchBox.vue')['default']
|
||||
SideMenu: typeof import('./../components/layout/SideMenu.vue')['default']
|
||||
TabsBar: typeof import('./../components/layout/TabsBar.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@
|
|||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"module": "es2020",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
|
|||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import AutoImport from 'unplugin-auto-import/vite'
|
||||
import Components from 'unplugin-vue-components/vite'
|
||||
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
|
|
@ -14,34 +15,34 @@ export default defineConfig({
|
|||
vueJsx(),
|
||||
vueDevTools(),
|
||||
AutoImport({
|
||||
// 自动导入Vue相关API
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'pinia',
|
||||
// 可以添加更多需要自动导入的库
|
||||
],
|
||||
// 生成自动导入的TS声明文件
|
||||
dts: 'src/auto-imports.d.ts',
|
||||
// 自动导入目录下的模块
|
||||
resolvers: [
|
||||
AntDesignVueResolver({
|
||||
importStyle: 'less', // 可选: 'css' | 'less',根据你是否启用了 less 主题定制
|
||||
}),
|
||||
],
|
||||
dts: 'src/types/auto-imports.d.ts',
|
||||
dirs: [
|
||||
'src/composables',
|
||||
'src/components',
|
||||
'src/stores',
|
||||
],
|
||||
// 自动导入的API前缀
|
||||
vueTemplate: true,
|
||||
}),
|
||||
Components({
|
||||
// 指定组件所在目录,默认为 src/components
|
||||
dirs: ['src/components'],
|
||||
// 组件的有效文件扩展名
|
||||
extensions: ['vue', 'tsx'],
|
||||
// 配置文件生成位置
|
||||
dts: 'src/components.d.ts',
|
||||
// 搜索子目录
|
||||
dts: 'src/types/components.d.ts',
|
||||
deep: true,
|
||||
// 允许子目录作为组件的命名空间前缀
|
||||
directoryAsNamespace: false,
|
||||
resolvers: [
|
||||
AntDesignVueResolver({
|
||||
importStyle: 'less', // 保持与 AutoImport 中一致
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue