feat: 新增开发与生产环境配置、重构API请求模块、优化组件自动导入体系及新增表格组件

This commit is contained in:
FalingCliff 2025-06-15 20:38:03 +08:00
parent ecb78e5982
commit 8c4f5fcbac
13 changed files with 356 additions and 46 deletions

4
.env.development Normal file
View File

@ -0,0 +1,4 @@
# 开发环境配置
VITE_ENV=development
VITE_API_URL=/api
VITE_APP_TITLE=Template Admin (Dev)

4
.env.production Normal file
View File

@ -0,0 +1,4 @@
# 生产环境配置
VITE_ENV=production
VITE_API_URL=/prod-api
VITE_APP_TITLE=Template Admin

0
src/api/auth/index.ts Normal file
View File

View File

@ -1,6 +0,0 @@
// API 请求封装
export const fetchData = async (url: string) => {
const response = await fetch(url);
return response.json();
};

24
src/components.d.ts vendored
View File

@ -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']
}
}

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,4 @@
export enum ComponentsEnum {
Input = 'Input',
Select = 'Select'
}

130
src/request/index.ts Normal file
View File

@ -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

View File

@ -66,7 +66,7 @@ declare global {
const triggerRef: typeof import('vue')['triggerRef'] const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref'] const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs'] 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 useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars'] const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId'] const useId: typeof import('vue')['useId']
@ -153,7 +153,7 @@ declare module 'vue' {
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']> readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly unref: UnwrapRef<typeof import('vue')['unref']> readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']> 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 useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']> readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
readonly useId: UnwrapRef<typeof import('vue')['useId']> readonly useId: UnwrapRef<typeof import('vue')['useId']>

47
src/types/components.d.ts vendored Normal file
View File

@ -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']
}
}

View File

@ -4,7 +4,7 @@
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"module": "es2020",
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }

View File

@ -6,6 +6,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools' import vueDevTools from 'vite-plugin-vue-devtools'
import AutoImport from 'unplugin-auto-import/vite' import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite' import Components from 'unplugin-vue-components/vite'
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
@ -14,34 +15,34 @@ export default defineConfig({
vueJsx(), vueJsx(),
vueDevTools(), vueDevTools(),
AutoImport({ AutoImport({
// 自动导入Vue相关API
imports: [ imports: [
'vue', 'vue',
'vue-router', 'vue-router',
'pinia', 'pinia',
// 可以添加更多需要自动导入的库
], ],
// 生成自动导入的TS声明文件 resolvers: [
dts: 'src/auto-imports.d.ts', AntDesignVueResolver({
// 自动导入目录下的模块 importStyle: 'less', // 可选: 'css' | 'less',根据你是否启用了 less 主题定制
}),
],
dts: 'src/types/auto-imports.d.ts',
dirs: [ dirs: [
'src/composables', 'src/components',
'src/stores', 'src/stores',
], ],
// 自动导入的API前缀
vueTemplate: true, vueTemplate: true,
}), }),
Components({ Components({
// 指定组件所在目录,默认为 src/components
dirs: ['src/components'], dirs: ['src/components'],
// 组件的有效文件扩展名
extensions: ['vue', 'tsx'], extensions: ['vue', 'tsx'],
// 配置文件生成位置 dts: 'src/types/components.d.ts',
dts: 'src/components.d.ts',
// 搜索子目录
deep: true, deep: true,
// 允许子目录作为组件的命名空间前缀
directoryAsNamespace: false, directoryAsNamespace: false,
resolvers: [
AntDesignVueResolver({
importStyle: 'less', // 保持与 AutoImport 中一致
}),
],
}), }),
], ],
resolve: { resolve: {