前端构建过程简要说明
项目初始化
运用vite、react、react-router、react-redux、Ant Design、less、tailwindcss、axios等前沿技术栈,构建一个高效、响应式的后台管理系统
更新Node.js
到官网下载最新的安装包更新
修改npm数据源
# 国内淘宝数据源
npm config set registry https://registry.npmmirror.com
# 国内腾讯云数据源
npm config set registry http://mirrors.cloud.tencent.com/npm/
# 国内华为云数据源
npm config set registry https://mirrors.huaweicloud.com/repository/npm/
执行命令
npm create vite
安装依赖
npm install
配置别名
如果开发环境是ts,会提示如找不到path或找不到__dirname等,需要先安装一下node的类型声明文件
npm i -D @types/node
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path' //手动添加引用
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, './src') // 路径别名
}
}
})
路径别名
修改tsconfig.json文件:添加baseUrl和paths
# tsconfig.app.json
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "Bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "./",
/* path alias */
"paths": {
"@/*": ["src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
样式重置
安装normlize.css重置游览器样式
npm install normalize.css -S
配置Less
npm install less -S
配置Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
安装完成后执行
npx tailwindcss init -p
配置tailwindcss相关文件
//tailwind.config.js
export default {
content: ["./index.html","./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
}
//postcss.config.js
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
引入antd
npm install antd -S
引入路由
npm install react-router-dom -S
创建布局组件
先在src文件夹下创建一个layout文件夹 添加index.tsx文件
// 文件全路径 src/layout/index.tsx
import React from 'react';
import { Breadcrumb, Layout, Menu, theme } from 'antd';
const { Header, Content, Footer } = Layout;
const items = new Array(3).fill(null).map((_, index) => ({
key: String(index + 1),
label: `nav ${index + 1}`,
}));
const App: React.FC = () => {
return (
<Layout className="app-layout ">
<Header
style={{
position: 'sticky',
top: 0,
zIndex: 1,
width: '100%',
display: 'flex',
alignItems: 'center',
}}
>
<div className="demo-logo" />
<Menu
theme="dark"
mode="horizontal"
defaultSelectedKeys={['2']}
items={items}
style={{ flex: 1, minWidth: 0 }}
/>
</Header>
<Content style={{ padding: '0 48px' }}>
<div className="bg-blue-500 text-white p-4 rounded-md">Content</div>
</Content>
<Footer style={{ textAlign: 'center' }}>
Ant Design ©{new Date().getFullYear()} Created by Ant UED
</Footer>
</Layout>
);
};
export default App;
路由文件
在src下新增router文件夹添加index.tsx文件
当浏览器地址改变时,通过这里匹配调转到指定的页面
// 文件全路径 src/router/index.tsx
import { createBrowserRouter} from "react-router-dom";
import AppLayout from "@/layout/index";
const routers = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
children: [], // 如果需要子路由,可以在这里添加
},
]);
export default routers;
修改样式,将App.css改成App.less
// 文件全路径src/App.less
#root {
margin: 0 auto;
padding: 2rem;
text-align: center;
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}
App.ts文件引入
这个是使用react-router-dom第三方包的固定写法
// 文件全路径src/App.ts
import { RouterProvider } from "react-router-dom";
import routers from '@/router';
import './App.less'
function App() {
return (
<>
<RouterProvider router={routers} />
</>
)
}
export default App
修改样式,将index.css改成index.less
// 文件全路径 index.less
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,body,#root,.app-layout{
height:100%;
}
main.ts 文件引入
// 文件全路径src/main.ts
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import 'normalize.css'
import './index.less'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
运行测试
npm run dev
拆分布局组件
因为目前的布局组件是都在一个文件中,正常项目里面都是拆分为多个组件中写逻辑,现在需要把layout文件夹下的index.tsx文件进行拆分,在layout文件夹下新增三个文件header.tsx,sider.tsx和main.tsx,本文先大概拆分layout这个布局组件到各个子组件中,下面会详细丰富各个子组件的内容。
在layout文件就夹下新建content.tsx、head.tsx文件
// 文件全路径 src/layout/header.tsx
import {Layout } from "antd";
import './head.less'
const { Header} = Layout;
const AppHeader = () => {
return (
<Header className="">
<div className="nc-workbench-top-container height-72">
<nav className="nc-workbench-nav">
<div className="nav-left n-left n-v-middle flex-fixed">
<div className="n-v-middle">
<div className="nc-workbench-allAppsBtn nc-workbench-icon-close">
<div className="nc-workbench-icon-new">
<i className="iconfont icon-logo1"></i>
</div>
</div>
<div className="nc-workbench-allAppsBtn nc-workbenchRecent">
<div className="nc-workbench-icon">
<div className="iconContainer">
<i className="iconfont icon-zuijinfangwen1"></i>
</div>
</div>
</div>
<div className="nc-workbench-group-switch">
<div className="fieldid_group ant-select" style={{display: "block"}}>
<div className="ant-select-selection ant-select-selection-single" tabIndex={0}>
<div className="ant-select-selection-rendered">
<div className="ant-select-selection-selected-value" title="集团公司" style={{display: "block", opacity: 1}}>
集团公司
</div>
</div>
<span className="ant-select-arrow" unselectable="on" style={{userSelect: "none"}}>
<i className="ant-select-arrow-icon">
<svg viewBox="64 64 896 896" className="" data-icon="down" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false">
<path
d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z">
</path>
</svg>
</i>
</span>
</div>
</div>
</div>
</div>
</div>
<div className="nav-middle"></div>
<div className="nav-right n-right n-v-middle">
<div className="n-v-middle n-right right-block">
<div className="container">
<div className="margin-right-10 search-item">
<div className="ant-select-selection ant-select-selection-single">
<div className="ant-select-selection-rendered">
<div>
<input className="ant-select-search-field" placeholder="请输入应用名称" />
</div>
</div>
</div>
<div>
<div className="iconContainer">
<i className="iconfont icon-sousuo1"></i>
</div>
</div>
</div>
<div style={{marginRight: "10px" }}>
<div className="iconContainer">
<i className="iconfont icon-xiaoxi2"></i>
</div>
</div>
<div>
<div className="iconContainer">
<i className="iconfont icon-luzhi1"></i>
</div>
</div>
</div>
<span className="block-span"></span>
<span className="block-span2">2023-12-12</span>
<div className="avatar_container">
<div className="nc-workbench-hp margin-right-10 margin-left-10">
<img src="/红衣男子.png" alt="logo" />
</div>
</div>
</div>
</div>
</nav>
</div>
</Header>
);
};
export default AppHeader;
// 文件全路径 src/layout/main.tsx
import React, { useState } from 'react';
import { Layout } from "antd";
import { Button, Drawer } from 'antd';
const { Content } = Layout;
const AppContent = () => {
const width = window.innerWidth;
const [open, setOpen] = useState(false);
const showDrawer = () => {
setOpen(true);
};
const onClose = () => {
setOpen(false);
};
const containerStyle: React.CSSProperties = {
position: 'relative',
height: 200,
padding: 48,
overflow: 'hidden',
};
return (
<Content style={containerStyle}>
<div className="bg-blue-500 text-white p-4 rounded-md">
<Button type="primary" onClick={showDrawer}>
Open
</Button>
</div>
<Drawer
title="菜单"
placement="left"
width={width}
closable={true}
onClose={onClose}
open={open}
getContainer={false}
>
<p>系统具体的菜单</p>
</Drawer>
</Content>
);
};
export default AppContent;
在layount文件夹下新建表头样式文件src/layout/layout_less/head.less、icon.less
运行测试
添加菜单页面
lanyout文件夹下增加menu.tsx、menu.less文件
将图标样式单独提出来,layout创建icon.less文件
运行测试
使用redux
引入redux和@reduxjs/toolkit
# 如果你使用 npm:
npm install react-redux @reduxjs/toolkit -S
为什么使用 Redux Toolkit
Redux Toolkit 是官方推荐的编写 Redux 逻辑的方法。它围绕 Redux 核心,并包含我们认为对于构建 Redux 应用必不可少的软件包和功能。Redux Toolkit 简化了大多数 Redux 任务,防止了常见错误,并使编写 Redux 应用程序更加容易。
创建store文件
首先在src文件夹下创建store文件夹,新建一个index.ts文件
index.ts源码
创建reducer
在store文件夹下新建reducers文件夹,新增一个文件global.ts
golbal.ts源码
使用 Hooks 类型
尽管你可以将 RootState 和 AppDispatch 类型导入每个组件, 更好的方式是创建 useDispatch 和 useSelector 钩子的类型定义,以便在你的应用程序中使用
- 对于 useSelector ,它不需要你每次输入(state: RootState)
- 对于 useDispatch ,默认的 Dispatch 类型不知道 thunk 。为了正确调度 thunk ,你需要使用 store 中包含 thunk 中间件类型的特定自定义 AppDispatch 类型,并将其与 useDispatch 一起使用。添加一个预先输入的 useDispatch 钩子可以防止你忘记在需要的地方导入 AppDispatch。
由于这些是实际变量,而不是类型,因此将它们定义在单独的文件中很重要,而不是 store 设置文件。这允许你将它们导入到需要使用挂钩的任何组件文件中,并避免潜在的循环导入依赖问题。
定义 Hooks 类型
在src文件夹下新增hooks文件夹,新增文件use_global.hooks.ts
// 文件全路径src/hooks/use_global.hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { RootState, AppDispatch } from '@/store/index';
// 在整个应用程序中使用,而不是简单的 `useDispatch` 和 `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
引入globalReducer
修改store文件夹下的index.ts
修改布局页面相关组件main.tsx
修改layout文件夹下的index.tsx
修改layout文件夹下的head.tsx
修改layout文件夹下的content.tsx
修改layout文件夹下的menu.tsx
运行测试
添加菜单路由
修改route/index.tsx
createBrowserRouter:浏览器地址根据路由变化
createMemoryRouter:浏览器地址不根据路由变化
// 原文件
import { createBrowserRouter} from "react-router-dom";
import AppLayout from "@/layout/index";
import Currency from "@/pages/currency";
const routers = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
children: [
], // 如果需要子路由,可以在这里添加
},
]);
export default routers;
//修改后
import { createBrowserRouter} from "react-router-dom";
import AppLayout from "@/layout/index";
import Currency from "@/pages/currency";
const routers = createBrowserRouter([
{
path: "/",
element: <AppLayout />,
children: [
{
path: "/currency",
element: <Currency />,
},
], // 如果需要子路由,可以在这里添加
},
]);
export default routers;
添加pages/currency文件夹,在currency文件夹下添加index.tsx文件
// 文件全路径src/pages/currency/index.tsx
const Currency = ()=>{
return (
<div>
<h1>currency</h1>
</div>
)
}
export default Currency;
修改菜单文件增加路由跳转,修改menu.tsx文件
<div className="item-app" grp-index="0" item-index="0" open-type="tab">
<Link to="/currency">币制</Link>
<i className="iconfont icon-open app-open" grp-index="0" item-index="0"open-type="newtab"></i>
</div>
修改台账页面增加按钮和样式
添加样式文件src/pages/page_list.less
添加币制台账页面src/pages/currency/index.tsx
测试页面
币制页面添加mock测试数据
引入mock
npm i mockjs @types/mockjs vite-plugin-mock -D
配置mock
vite-plugin-mock提供本地和生产模拟服务。
vite 的数据模拟插件,是基于 vite.js 开发的。 并同时支持本地环境和生产环境。 Connect 服务中间件在本地使用,mockjs 在生产环境中使用。
修改vite.config.ts文件
//修改前
plugins: [react(),
],
//修改后
viteMockServe({
mockPath: './src/mock', // mock文件夹路径默认是 src/mock
enable: true, // 默认是 false,可以根据环境变量开启
}),
添加types实体src/tpyes/currency/currency.d.ts
该文件相当于后端的实体类型定义
currency源码
添加币制mock数据,src/mock/currency.ts
该文件相当于第三方接口提供的api
currency源码
引入axios
npm install axios
封装axios,添加文件src/api/request.ts
币制请求api,添加文件src/api/financial_basic_data/currency_service.ts
该文件相当于后端的第三方接口的封装
currency_service源码
接口请求
// 在src/pages/currency/index.tsx页面中增加数据请求
// 币制数据
const [currentyList, setCurrencyList] = useState([] as CurrencyItemProps[]);
// 获取币制数据
useEffect(() => {
// 获取币制数据
const getData = async () => {
const res = await getCurrencyList();
const currencyData = res?.data as CurrencyItemProps[];
// 设置币制台账数据
setCurrencyList([...currencyData]);
};
getData();
}, []);
测试页面
币制详细页面
台账Table中的编辑按钮
// 获取相关数据,根据选择的行关键字获取单条数据
const handleEdit = (key: React.Key) => {
const newData = currentyList.filter((item) => item.Code === key);
setFormData(newData[0]);
showModal();
};
{
title: '操作',
key: 'operation',
fixed: 'right',
width: 100,
render: (_, record) => (
<>
<a href='#'>启用</a>
<a href='#' onClick={()=>handleEdit(record.Code)}>编辑</a>
<Popconfirm title="确定要删除吗?" cancelText="取消" okText="确定" onConfirm={() => handleDelete(record.Code)}>
<a href='#'>删除</a>
</Popconfirm>
</>
),
},
// 详细页面加载选择的数据,initialValues={formData}通过这个属性绑定数据集
<Form {...formItemLayout} style={{ maxWidth: 600 }} initialValues={formData}>
</Form>
详细页面表单数据提交
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
// 绑定控件数据变化时触发相关函数
<Form.Item label="币种" name="CurrencyFullName" rules={[{ required: true,message: '' }]}>
<Input onChange={handleChange} />
</Form.Item>
// 保存按钮绑定提交方法
const handleOk = () => {
// formData就是最新的提交对象数据
// 将formData提交到Api接口
setOpen(false);
};
<div style={{textAlign:'right'}}>
<Space>
<Button onClick={handleCancel}>取消</Button>
<Button>保存并新增</Button>
<Button type="primary" onClick={handleOk} danger>保存</Button>
</Space>
</div>
台账Table中删除按钮
const handleDelete = (key: React.Key) => {
alert(key);
//根据主键调用api删除相关数据
};
// 和上面的编辑按钮在一个地方配置
<Popconfirm title="确定要删除吗?" cancelText="取消" okText="确定" onConfirm={() => handleDelete(record.Code)}>
<a href='#'>删除</a>
</Popconfirm>
测试运行
台账页面CheckBox按钮设置
Table中行配置
// 定义行类型,固定写法不要修改
type TableRowSelection<T extends object = object> = TableProps<T>['rowSelection'];
//表格选中和取消时触发的函数
const rowSelection: TableRowSelection<CurrencyItemProps> = {
// 选中项发生变化时的回调
onChange: (selectedRowKeys, selectedRows) => {
console.log('onchange');
console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
},
// 用户手动选择/取消选择某行的回调
onSelect: (record, selected, selectedRows) => {
console.log('onselect');
console.log(record, selected, selectedRows);
},
// 用户手动选择/取消选择所有行的回调
onSelectAll: (selected, selectedRows, changeRows) => {
console.log('onallselect');
console.log(selected, selectedRows, changeRows);
},
type: 'checkbox',
};
<Table<CurrencyItemProps>
columns={columnsType}
rowSelection={{ ...rowSelection}}
/>
根据checkbox的选择,处理修改、复制、删除、导出相关按钮的逻辑
编写相关判断逻辑,例如:
- 修改时只能选择一行进行修改
- 删除时要加提醒
添加一个用户的redux
添加文件src/store/reducers/user.ts
添加一个src/store/reducers/global_state.d.ts
全局共享信息实体定义(用户登录信息、页面状态信息)
global_stae源码
修改src/strore/index.ts
将用户redux加入到index文件中
index源码
// 修改位置,添加userStatus
const store = configureStore({
reducer: {
globalStatus: globalReducer,
userStatus: userReducer,
},
});
菜单改成动态获取mock
添加菜单类型定义 src/types/menu/menu.d.ts
添加 src/mock/menu.ts 文件
菜单实例数据
menu源码
添加 src/api/golbal/menu_service.ts 文件
菜单api接口
菜单api接口源码
封装菜单组件
添加 src/components/menu/index.tsx
// 参数说明
// menudatas 主菜单数据,collapsed 菜单是否显示,activeMenudatas 子菜单数据
// onClick 菜单选择以后触发用于控制菜单隐藏
// selectMenu 菜单选择时传入主菜单key
// selectkey 默认主菜单key用于控制主菜单选中时的样式
const Menu: React.FC<AppSiderProps> = ({ menudatas,collapsed,activeMenudatas,onClick,selectMenu,selectkey }) => {
修改src/layout/menu.tsx 内容
登录页面
登录接口校验mock 添加 src/mock/user_login.ts
登录接口校验api 添加 src/api/golbal/user_service.ts
import request from '../request'
export const getUserList = () => {
return request({
method: 'GET',
url: '/getUserList'
})
}
添加 src/pages/login/index.tsx
- 校验主要逻辑
const onFinish = async (values: any) => {
const res = await getUserList();
const userList = res?.data as UserLogin[];
const userInfo = userList.find(item => item.UserEmail === values.useremail && item.UserPassword === values.password);
if(userInfo) {
sessionStorage.setItem('userlogin', JSON.stringify(userInfo));
dispatch(setUserState(userInfo));
window.location.href = '/';
return;
}else {
alert('账号或密码错误');
}
};
添加 src/page/login/login.less
修改路由文件 src/router/index.tsx
//添加
{
path: "/login",
element: <Login />,
},
运行测试
添加路由权限过滤
增加公共路由守卫 src/components/router_guard.tsx
import { UserLogin } from "@/types/user";
import { useLocation,Navigate } from "react-router-dom";
const RouterGuard = ({ children }: { children: JSX.Element }) => {
const locationPath = useLocation();
const userLoginString = sessionStorage.getItem('userlogin')
|| JSON.stringify({Token: '',
UserCode: '',
UserName: '',
UserEmail: ''});
const userLogin:UserLogin = JSON.parse(userLoginString);
if (!userLogin.Token && locationPath.pathname !== '/login') {
return <Navigate to="/login" replace/>;
}
return children;
}
export default RouterGuard;
修改路由文件 src/router/index.tsx
// 将原来的组件包含在路由守卫中
<RouterGuard>
<AppLayout />
</RouterGuard>),
修改主页头部 src/layout/head.tsx
增加头像下拉菜单
<div className="avatar_container">
<div className="nc-workbench-hp margin-right-10 margin-left-10">
<Dropdown menu={{ items }}>
<a onClick={(e) => e.preventDefault()}>
<img src="/man.png" alt="logo" />
</a>
</Dropdown>
</div>
</div>
增加图标角标
<div>
<div className="iconContainer">
<Badge count={9} size="small" offset={[-10, 15]}>
<Avatar size={44} icon={<BarsOutlined />} />
</Badge>
</div>
</div>
测试页面
封装一个图标组件,将antd里的图标组件封装
增加src/components/custom-icon/index.tsx
const icon = createFromIconfontCN({
//自定义图标可以在这里无限扩展,在这个网站https://www.iconfont.cn/生成js文件引用
scriptUrl: "//at.alicdn.com/t/c/font_4108710_4qayguv5weh.js",
});
/** 装饰器包装一下icon组件,内置一个32px字体大小的样式 */
const CustomIcon = (Comp: React.FC<any>) => {
return (props: any) => (
<Comp
onClick={(e: any) => navigate(e, props.url)}
style={{ fontSize: "32px" }}
{...props}
className="custom-icon"
/>
);
};
export default CustomIcon(icon);
添加多语言支持
添加依赖库
npm install i18next react-i18next i18next-browser-languagedetector
添加资源文件 src/locales/zh-cn.js、src/locales/en-us.js
定义的键值对使用时,父级.子级这种格式,例如common.title就是key,t(common.title)就是value
中文
英文
添加多语言工具 src/i18n.ts
i18next第三方包的初始化封装
源码
添加多语言key字典工具 src/utils/localHelper.ts
这个可用也可以不用,就是将key提供一个公共的调用接口,方便后面修改时不用全部文件修改
源码
修改表头增加中英文切换逻辑 src/layout/head.tsx
// 中英文切换
const { t, i18n } = useTranslation();
const handleChangeLanguage = (value: string) => {
i18n.changeLanguage(value);
window.location.reload();
};
//
const itemsLanguage: MenuProps['items'] = [
{
key: '1',
danger: true,
className:'ant-drowndown-custom',
label:(
<CustomIcon type="icon-ICON-297" style={{fontSize:'24px'}} onClick={()=>{handleChangeLanguage('zh-CN')}} />
)
},
{
key: '2',
danger: true,
className:'ant-drowndown-custom',
label:(
<CustomIcon type="icon-English" style={{fontSize:'24px'}} onClick={()=>{handleChangeLanguage('en-US')}} />
)
},
];
//修改页面样式
<div className="iconContainer">
<Dropdown menu={{ items:itemsLanguage }}>
<TranslationOutlined style={{fontSize: "22px"}} />
</Dropdown>
</div>
运行测试
看table第一列
封装查询组件
添加文件 src/components/search-form/index.tsx
展开时显示全部动态传入的查询控件
收起时显示一行查询控件
查询时将查询控件选择的参数回传到父页面,用于api调用参数
清空时将查询控件选择的参数重置
源码
// 组件主要参数
export type AdvancedSearchFormProps = {
fields: Field[];// 查询控件集合
span?: number;// 每行显示控件数量
onSearch: (values: any) => void; // 查询事件
};
// 查询控件数据整理
const [formData, setFormData] = useState({});
// input控件onchange
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData({ ...formData, [name]: value });
};
// select控件onchange
const handleSelectChange = (name:string,value:{ value: string; label: React.ReactNode }) => {
setFormData({ ...formData, [name]: value.label });
};
// 日期和日期区间控件onchange
const handleDateChange = (name:string,value:string | Array<string>) => {
setFormData({ ...formData, [name]: value });
};
// 查询
const handOnFinish = () => {
console.log(formData);
onSearch(formData);
};
// 重置
const handleCancel = () => {
setFormData({});
form.resetFields();
};
父组件使用查询表单
// 参数定义
const fields:AdvancedSearchFormProps["fields"] =
[
{
type: 'input',
label: '币种',
key: 'CurrencyFullName',
},
{
type: 'select',
key: 'PriceRoundingRule',
label: '舍入规则',
selectOptions: [{ value: '1', label: '四舍五入' }, { value: '2', label: '向上舍入' }, { value: '3', label: '向下舍入' }]
},
]
const handleSearch = (values:any) => {
console.log('handleSearch',values);
};
// 调用查询表单
<AdvancedSearchForm fields={fields} onSearch={handleSearch} />
运行测试
设置自定义查询条件设定
自定义查询条件设置,例如一个表有10个字段,我可以自己设置5个字段用于查询条件,实现单价左边字段,右边自动生成字段条件设置,并且左边相对应字段变成不可用,右边字段可以通过鼠标上下移动设置排序,右边控件点击可以删除,并且左边字段自动变成可用状态
添加src/components/search-form/darggable.tsx、src/components/search-form/tranfer_right.tsx
tranfer_right:穿梭框的右半部分
darggable:右半部分可以鼠标移动控件排序
修改src/components/search-form/index.tsx
// 添加事件
const showModal = () => {
setOpen(true);
};
// 添加事件调用
<span className="search-super" onClick={showModal}>高级</span>
// 自定义查询条件弹出页面
<Modal open={open} title="编辑方案"
onCancel={handleCancel}
width={800}
className='transferModal'
destroyOnClose={true}
maskClosable={false}
footer={(_) => (
<div style={{ textAlign: 'right' }}>
<Space>
<Button onClick={handleCancel}>取消</Button>
<Button type="primary">保存</Button>
</Space>
</div>
)}
>
<Row className='ant-tranfer-row' wrap={false}>
<Col span={6} className='ant-tranfer-col-left'>
<ul>
{mockData.map((item) => {
return targetKeys?.includes(item.key) ?
(
<li key={item.key}>
<div className='tranfer-col-left-item-disabled'>
{item.text}
</div>
</li>
)
:
(
<li key={item.key}>
<div className='tranfer-col-left-item'>
{item.text}
<i className="iconfont icon-xinzengfenzu" onClick={()=>leftItemOnClick(item.key)}></i>
</div>
</li>
)
})
}
</ul>
</Col>
<Col span={18} className='ant-tranfer-col-right'>
<TranferRight drapItems={mockData} selectKeyItmes={targetKeys?.map((key)=>key.toString())} onRemoveItem={rightRemoveItemKey}/>
</Col>
</Row>
</Modal>
运行测试
添加Excel导入
添加src/components/excel/import.tsx、src/components/excel/modal_import.tsx
import.tsx里包含主要功能,modal_import是使用模式窗口弹出的方式(引用import)
主要功能,根据下载的excel模板填写数据,上传excel文件校验excle文件的合法性完成新增数据导入。
源码
添加src/types/excel/import_template.d.ts实体
export interface ImportTemplateProps {
SerialNo: number;
Version: string;
TemplateCode: string;
TemplateName:string;
TemplateSource:string;
CreatDate:string;
LastDate:string;
Operator:string;
//0 未发布,1已发布
Status:number;
Country:string;
//0 否,1 是
IsDefault:number;
}
修改币制台账页面内容src/pages/currency/index.tsx
const excelImportOnClick: MenuProps['onClick'] = ({ key }) => {
console.log(`Click on item ${key}`);
if(key==='1'){
setExcelOpen(true);
}else if(key==='2'){
setExcelTemplateOpen(true);
console.log(openExcelTemplate)
}else{
setExcelOpen(true);
}
};
const [openExcel, setExcelOpen] = useState(false);
const handleExcelCancel = () => {
setExcelOpen(false);
};
<ModelExcelImport open={openExcel} onCancel={handleExcelCancel} businessType='currency' />
运行测试
添加Excel模板台账
添加src/components/excel/import_template.tsx、src/components/excel/modal_import_template.tsx
主要功能自定义excel导入模板格式导入字段顺序和是否必填
修改币制台账页面内容src/pages/currency/index.tsx
const [openExcelTemplate, setExcelTemplateOpen] = useState(false);
const handleExcelTemplateCancel = () => {
setExcelTemplateOpen(false);
};
<ModelExcelImportTemplate open={openExcelTemplate} onCancel={handleExcelTemplateCancel} businessType='currency' />
添加src/components/excel/custom_template.tsx
主要功能设置excel模板导入的格式,并生成模板记录,支持字段上移、下移、移动到指定位置
添加导出日志src/log/export_log.tsx
添加导入日志src/log/import_log.tsx
运行测试
修改主页布局,增添加tabs导航标签
修改 src\layout\content.tsx 文件
return (
<Content style={containerStyle}>
<AppMenu collapsed={collapsed}></AppMenu>
<Tabs
activeKey={activeKey}
onChange={handleTabChange}
items={tabs}
type="editable-card"
hideAdd
onEdit={(targetKey: any, action) => {
if (action === 'remove') {
removeTab(targetKey as string);
}
}}
/>
{/* <Outlet></Outlet> */}
</Content>
);
运行测试
添加保存实时进度条效果
上传、下载、保存、任务批处理这种情况一般有实时处理进度用户体验会比较好,处理结果实时显示
mock模拟流式数据,修改src\mock\currency.ts文件
{
url: "/api/currency/save",
method: "POST",
response: ({ body }: { body: CurrencyItemProps }) => {
return {
code: 200,
success: true,
message: "开始处理",
data: body
};
}
},
{
url: "/api/currency/save/progress",
method: "GET",
rawResponse: async (req: IncomingMessage, res: ServerResponse) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no');
let progress = 0;
const sendProgress = () => {
const data = {
code: 200,
success: true,
message: progress >= 100 ? "保存成功" : "处理中...",
data: {
progress: progress,
status: progress >= 100 ? 'completed' : 'processing',
result: progress >= 100 ? null : null
}
};
res.write(`data: ${JSON.stringify(data)}\n\n`);
console.log('Sending progress:', progress);
if (progress >= 100) {
res.end();
return;
}
progress += 10;
setTimeout(sendProgress, 1000);
};
sendProgress();
}
}
修改请求文件,修改src\api\request.ts文件
// 带进度的请求函数
export const requestWithProgress = async <T>(config: ProgressConfig) => {
const { onUploadProgress, ...axiosConfig } = config;
// 首先发送 POST 请求
const postResponse = await fetch(`${instance.defaults.baseURL}${config.url}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(config.data)
});
if (!postResponse.ok) {
throw new Error(`HTTP error! status: ${postResponse.status}`);
}
// 使用 GET 请求来获取进度更新
const progressUrl = `${instance.defaults.baseURL}${config.url}/progress`;
const eventSource = new EventSource(progressUrl);
return new Promise<ApiRes<T>>((resolve, reject) => {
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as ApiRes<ProgressData>;
console.log('Received stream data:', data);
if (data.data?.progress !== undefined) {
onUploadProgress?.(data.data.progress);
}
if (data.data?.status === 'completed') {
eventSource.close();
resolve(data as ApiRes<T>);
}
} catch (e) {
console.error('Failed to parse event data:', e);
}
};
eventSource.onerror = (error) => {
eventSource.close();
reject(new Error('EventSource failed: ' + error));
};
});
};
修改币制前端页面,保存功能添加进度条
const handleOk = async () => {
setSaving(true);
// 打开通知
const key = `progress_${Date.now()}`;
notification.open({
key,
message: '提示',
placement:'bottomRight',
description: <Progress type='circle' percent={0} size={60} />,
duration: 0,
icon: <HourglassOutlined />,
style: {
width: 200,
},
});
try {
const response = await requestWithProgress({
method: 'POST',
url: '/currency/save',
data: formData,
onUploadProgress: (progress) => {
// 更新通知中的进度条
notification.open({
key,
message: '提示',
description: <Progress type='circle' percent={progress} strokeColor={progress === 100 ? "" : "#ff1648"} size={60} />,
duration: 0,
placement:'bottomRight',
icon: <HourglassOutlined />,
style: {
width: 200,
},
});
}
});
if (response?.success) {
setFormData(initFormData);
// 等待1秒
await new Promise(resolve => setTimeout(resolve, 500));
notification.destroy(key);
setOpen(false);
}
} catch (error) {
console.error('Save failed:', error);
notification.destroy(key);
} finally {
setSaving(false);
}
};
运行测试
修改币制台账代码,拆分代码
台账列定义单独出来 src\pages\currency\columns.tsx
台账按钮定义单独出来 src\pages\currency\menu-items.tsx
自定义查询单独出来 src\pages\currency\search-fields.ts
编辑和新增页面单独出来 src\pages\currency\currency-modal.tsx
编写代码生成器,根据单表自动生成
编写模板文件
- page_index.cst:台账页面模板
- page_detail_modal.cst:新增、编辑页面模板
- page_menu_items.cst:台账按钮模板
- page_search_fields.cst:台账查询条件模板
- page_column.cst:台账Table列模板
- type_items_props.cst:台账实体类模板
- mock_js.cst:mock生成测试数据模板
- api_service.cst:service调用mock模板
- local_language.cst:多语言生成模板
- local_helper.cst:多语言辅助类模板
- batch.cst:批量调用上面模板的总入口
根据mysql数据库表生成
- 引入连接mysql驱动
- 下载MySql.Data.dll:https://dev.mysql.com/downloads/windows/visualstudio/ 下载zip格式的即可,解压后将MySql.Data.dll复制到codesmith的bin文件夹下。
- 修改DbProviderFactories:找到C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config 注意64位的,因为权限问题可能无法修改,将其复制到桌面做如下修改后替换回来。
在DbProviderFactories节点下添加,此处的版本号要与下载的dll版本一致,可以右键查看dll的详细信息获取
<system.data>
<DbProviderFactories>
<add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.9.9.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
</DbProviderFactories>
</system.data>
- codesmith引入mysql驱动dll
<%@ Assembly Name="mysql.data" %>
<%@ Import Namespace="MySql.Data.MySqlClient" %>
- 引入Mysql数据库操作类库
源码
根据sqlserver数据库表生成
- ai提问根据字段名称创建表
1.使用的数据库是sql server。
2.表名是invoice_issuance_receipt。
3.使用英文名称定义字段名,中文名称作为字段的备注和注释说明,表名的注释和说明是开票收票。
4.sql server数据库的注释和说明是使用sp_addextendedproperty方式添加。
4.请编写创建表语句,所有字段添加注释和说明。