前端构建过程简要说明

目录

项目初始化

运用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

将表头样式独立出来
head源码
icon源码

运行测试

主页

添加菜单页面

lanyout文件夹下增加menu.tsx、menu.less文件

menu.tsx源码
menu.less源码

将图标样式单独提出来,layout创建icon.less文件

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
index

修改布局页面相关组件main.tsx

main

修改layout文件夹下的index.tsx

index

修改layout文件夹下的head.tsx

head

修改layout文件夹下的content.tsx

content

修改layout文件夹下的menu.tsx

menu

运行测试

主页初始化
显示菜单

添加菜单路由

修改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

request源码

币制请求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

user源码

添加一个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

menu实体源码

添加 src/mock/menu.ts 文件

菜单实例数据
menu源码

添加 src/api/golbal/menu_service.ts 文件

菜单api接口
菜单api接口源码

封装菜单组件

添加 src/components/menu/index.tsx

menu组件源码

// 参数说明
// menudatas 主菜单数据,collapsed 菜单是否显示,activeMenudatas 子菜单数据
// onClick 菜单选择以后触发用于控制菜单隐藏
// selectMenu 菜单选择时传入主菜单key
// selectkey 默认主菜单key用于控制主菜单选中时的样式
const Menu: React.FC<AppSiderProps> = ({ menudatas,collapsed,activeMenudatas,onClick,selectMenu,selectkey }) => {

修改src/layout/menu.tsx 内容

menu源码

登录页面

登录接口校验mock 添加 src/mock/user_login.ts

user_log.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导入

添加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

运行测试

excel模板台账
excel自定义模板
导出和导入日志

修改主页布局,增添加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

编写代码生成器,根据单表自动生成

编写模板文件

  1. page_index.cst:台账页面模板
  2. page_detail_modal.cst:新增、编辑页面模板
  3. page_menu_items.cst:台账按钮模板
  4. page_search_fields.cst:台账查询条件模板
  5. page_column.cst:台账Table列模板
  6. type_items_props.cst:台账实体类模板
  7. mock_js.cst:mock生成测试数据模板
  8. api_service.cst:service调用mock模板
  9. local_language.cst:多语言生成模板
  10. local_helper.cst:多语言辅助类模板
  11. batch.cst:批量调用上面模板的总入口

根据mysql数据库表生成

  • 引入连接mysql驱动
  1. 下载MySql.Data.dll:https://dev.mysql.com/downloads/windows/visualstudio/ 下载zip格式的即可,解压后将MySql.Data.dll复制到codesmith的bin文件夹下。
  2. 修改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.请编写创建表语句,所有字段添加注释和说明。

posted on 2025-03-18 09:48  天涯轩  阅读(78)  评论(0)    收藏  举报

导航