完善功能

This commit is contained in:
WenC 2024-03-20 18:30:39 +08:00
parent 9160a37bbe
commit c83637d1bc
10 changed files with 392 additions and 116 deletions

View File

@ -1,18 +1,20 @@
module.exports = { module.exports = {
root: true, root: true,
env: { browser: true, es2020: true }, env: {browser: true, es2020: true},
extends: [ extends: [
'eslint:recommended', 'eslint:recommended',
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended', 'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{ allowConstantExport: true },
], ],
}, ignorePatterns: ['dist', '.eslintrc.cjs'],
parser: '@typescript-eslint/parser',
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': [
'warn',
{allowConstantExport: true},
],
// 去除any报错
'@typescript-eslint/no-explicit-any': ['off']
},
} }

View File

@ -2,10 +2,13 @@ import './card-proj-type.css';
import {ICardProj, ICardProjChargeLine} from "../../interfaces/proj/ICardProj.ts"; import {ICardProj, ICardProjChargeLine} from "../../interfaces/proj/ICardProj.ts";
import {Checkbox} from 'antd'; import {Checkbox} from 'antd';
import {useState} from "react"; import {useState} from "react";
import {ProjAdditionalType} from "../../interfaces/proj/IProj.ts";
export default function CardProjType(props: ICardProj) { export default function CardProjType(props: ICardProj) {
const [chargeAmount, setChargeAmount] = useState(0); const [chargeAmount, setChargeAmount] = useState(0);
const [pkg, setPkg] = useState(false);
const [videoDemo, setVideoDemo] = useState(false);
const renderContents = (lineIndex: number, contents: string[]) => { const renderContents = (lineIndex: number, contents: string[]) => {
return contents.map((item, index) => <li key={`content_${lineIndex}_${index}`}> return contents.map((item, index) => <li key={`content_${lineIndex}_${index}`}>
@ -29,8 +32,18 @@ export default function CardProjType(props: ICardProj) {
const onChargeChange = (checked: boolean, line: ICardProjChargeLine) => { const onChargeChange = (checked: boolean, line: ICardProjChargeLine) => {
if (checked) { if (checked) {
if (line.id == ProjAdditionalType.PKG) {
setPkg(true);
} else if (line.id == ProjAdditionalType.VIDEO_DEMO) {
setVideoDemo(true);
}
setChargeAmount(chargeAmount + line.price); setChargeAmount(chargeAmount + line.price);
} else { } else {
if (line.id == ProjAdditionalType.PKG) {
setPkg(false);
} else if (line.id == ProjAdditionalType.VIDEO_DEMO) {
setVideoDemo(false);
}
setChargeAmount(chargeAmount - line.price); setChargeAmount(chargeAmount - line.price);
} }
} }
@ -60,13 +73,15 @@ export default function CardProjType(props: ICardProj) {
return props.buyArray.map((buy, index) => { return props.buyArray.map((buy, index) => {
return ( return (
<div className="buy-btn" key={`buy_${index}`}> <div className="buy-btn" key={`buy_${index}`}>
<div className="price">{buy.label}{buy.price + chargeAmount} </div> <div className="price">{buy.label}{(buy.price + chargeAmount) / 100} </div>
<hr/> <hr/>
<div className="buy"> <div className="buy">
<button onClick={() => { <button onClick={() => {
buy.handleClick(props.head, buy.price + chargeAmount); buy.handleClick(props.head, {
}}> pkg: pkg,
</button> videoDemo: videoDemo
});
}}></button>
</div> </div>
</div> </div>
) )

View File

@ -1,9 +1,9 @@
import './list-proj.css' import './list-proj.css'
import CardProj from "../card/CardProj.tsx"; import CardProj from "../card/CardProj.tsx";
import {useRef, MutableRefObject, useState, useEffect} from "react"; import {useRef, MutableRefObject, useState, useEffect} from "react";
import {Input, Pagination} from 'antd'; import {Input, Pagination, message} from 'antd';
import type {SearchProps} from 'antd/es/input/Search'; import type {SearchProps} from 'antd/es/input/Search';
import {Axios} from "../../util/AjaxUtils.ts"; import {get} from "../../util/AjaxUtils.ts";
const {Search} = Input; const {Search} = Input;
@ -17,25 +17,28 @@ export default function ListProj() {
const listProjRef: MutableRefObject<HTMLDivElement | null> = useRef(null); const listProjRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const listRef: MutableRefObject<HTMLDivElement | null> = useRef(null); const listRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
const [page, _setPage] = useState(1); const [messageApi, contextHolder] = message.useMessage();
const [total, _setTotal] = useState(0); const [page, setPage] = useState(1);
const [projs, _setProjs] = useState([]); const [total, setTotal] = useState(0);
const [projs, setProjs] = useState([]);
const domHeight = window.innerHeight - 280; const domHeight = window.innerHeight - 280;
const renderData = () => { const reqData = (currentPage: number) => {
Axios.get('/api/proj/listpage/self', { get({
params: { messageApi: messageApi,
page: page, url: '/api/proj/listpage/self',
rows: 20 config: {
params: {
page: currentPage,
rows: 20
}
},
onSuccess({data}) {
setProjs(data.page);
setTotal(data.total);
setProjs(data.rows);
} }
}).then(res => {
const data = res.data;
_setProjs(data.page);
_setTotal(data.total);
_setProjs(data.rows);
}).catch(res => {
console.log(res);
}) })
} }
@ -44,22 +47,28 @@ export default function ListProj() {
} }
useEffect(() => { useEffect(() => {
renderData(); reqData(page);
}, []) }, [])
return ( return (
<div className="list-proj" ref={listProjRef}> <>
<div className="top"> {contextHolder}
<Search placeholder="按项目名搜索" onSearch={onSearch} style={{width: 200}}/> <div className="list-proj" ref={listProjRef}>
</div> <div className="top">
<div className="body"> <Search placeholder="按项目名搜索" onSearch={onSearch} style={{width: 200}}/>
<div className="list" ref={listRef} style={{height: `${domHeight}px`}}>
{renderList()}
</div> </div>
<div className="page"> <div className="body">
<Pagination defaultCurrent={page} total={total}/> <div className="list" ref={listRef} style={{height: `${domHeight}px`}}>
{renderList()}
</div>
<div className="page">
<Pagination defaultCurrent={page} total={total} onChange={(page) => {
setPage(page);
reqData(page);
}}/>
</div>
</div> </div>
</div> </div>
</div> </>
) )
} }

View File

@ -1,18 +1,24 @@
export interface ICardProjBodyLine { export interface ICardProjBodyLine {
title: string; title: string;
contents: string[]; contents: string[];
} }
export interface ICardProjChargeLine { export interface ICardProjChargeLine {
id: string;
title: string; title: string;
price: number; price: number;
} }
export interface ICardProjBuy { export interface ICardProjBuy {
id: string;
label?: string, label?: string,
price: number; price: number;
handleClick(title: string, price: number): void; handleClick(title: string, additional: {
pkg: boolean,
videoDemo: boolean
}): void;
} }
export interface ICardProj { export interface ICardProj {

View File

@ -20,6 +20,34 @@ export enum GenerateStatus {
ERROR = 'ERROR' ERROR = 'ERROR'
} }
export enum ProjChargeType {
ALL = 'ALL',
MATERIAL_AGENT = 'MATERIAL_AGENT',
MATERIAL_AGENT_URGENT = 'MATERIAL_AGENT_URGENT',
MATERIAL = 'MATERIAL',
FREE = 'FREE',
}
export enum ProjAdditionalType {
PKG = 'PKG',
VIDEO_DEMO = 'VIDEO_DEMO'
}
export interface IProjCharge {
proj: {
all: number;
materialAgent: number;
materialAgentUrgent: number;
material: number;
free: number;
},
additional: {
pkg: number;
videoDemo: number;
}
}
export interface IProj { export interface IProj {
projId: string, projId: string,
@ -33,5 +61,4 @@ export interface IProj {
gmtCreate: string, gmtCreate: string,
generateStatus: GenerateStatus generateStatus: GenerateStatus
}
}

View File

@ -16,11 +16,11 @@ const router = createBrowserRouter([
element: <ProjCreate /> element: <ProjCreate />
}, },
{ {
path: '/proj-new/:type/:price', path: '/proj-new/:projChargeType',
element: <ProjNew /> element: <ProjNew />
}, },
{ {
path: '/proj-edit', path: '/proj-edit/:projId',
element: <ProjEdit /> element: <ProjEdit />
}, },
{ {

View File

@ -1,4 +1,3 @@
import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.tsx' import App from './App.tsx'
import './index.css' import './index.css'

View File

@ -1,13 +1,42 @@
import './proj-create.css' import './proj-create.css'
import {Link, useNavigate} from "react-router-dom"; import {Link, useNavigate} from "react-router-dom";
import {Breadcrumb} from "antd"; import {Breadcrumb, message} from "antd";
import CardProjType from "../../components/card/CardProjType.tsx"; import CardProjType from "../../components/card/CardProjType.tsx";
import {IProjCharge, ProjAdditionalType, ProjChargeType} from "../../interfaces/proj/IProj.ts";
import {useEffect, useState} from "react";
import {get} from "../../util/AjaxUtils.ts";
export default function ProjCreate() { export default function ProjCreate() {
const [messageApi, contextHolder] = message.useMessage();
const [charge, setCharge] = useState<IProjCharge>({
proj: {
all: 0,
materialAgent: 0,
materialAgentUrgent: 0,
material: 0,
free: 0,
},
additional: {
pkg: 0,
videoDemo: 0
}
});
const nav = useNavigate(); const nav = useNavigate();
const height = window.innerHeight - 150; const height = window.innerHeight - 150;
useEffect(() => {
get({
messageApi: messageApi,
url: '/api/proj/charge/get',
onSuccess({data}) {
setCharge(data);
}
})
}, [])
return ( return (
<> <>
{contextHolder}
<Breadcrumb <Breadcrumb
items={[ items={[
{title: <Link to={'/'}></Link>}, {title: <Link to={'/'}></Link>},
@ -15,7 +44,7 @@ export default function ProjCreate() {
]} ]}
/> />
<div style={{height: `${height}px`, overflow: 'auto'}}> <div style={{height: `${height}px`, overflow: 'auto'}}>
<div className="proj-create" > <div className="proj-create">
<CardProjType <CardProjType
head={'全托管'} head={'全托管'}
bodyLineArray={[ bodyLineArray={[
@ -42,9 +71,10 @@ export default function ProjCreate() {
]} ]}
buyArray={[ buyArray={[
{ {
price: 500, id: ProjChargeType.ALL,
handleClick: (_title, price) => { price: charge.proj.all,
nav(`/proj-new/q/${price}`) handleClick: () => {
nav(`/proj-new/${ProjChargeType.ALL}`)
} }
} }
]} ]}
@ -72,27 +102,31 @@ export default function ProjCreate() {
]} ]}
chargeLineArray={[ chargeLineArray={[
{ {
price: 100, id: ProjAdditionalType.PKG,
title: '安装包100元' price: charge.additional.pkg,
title: `安装包 ${charge.additional.pkg / 100}`
}, },
{ {
price: 50, id: ProjAdditionalType.VIDEO_DEMO,
title: '系统演示视频文件50元' price: charge.additional.videoDemo,
title: `系统演示视频文件 ${charge.additional.videoDemo / 100}`
} }
]} ]}
buyArray={[ buyArray={[
{ {
id: ProjChargeType.MATERIAL_AGENT,
label: '普件:', label: '普件:',
price: 200, price: charge.proj.materialAgent,
handleClick: (_title, price) => { handleClick: (_title, additional) => {
nav(`/proj-new/xdp/${price}`) nav(`/proj-new/${ProjChargeType.MATERIAL_AGENT}?pkg=${additional.pkg}&videoDemo=${additional.videoDemo}`)
} }
}, },
{ {
id: ProjChargeType.MATERIAL_AGENT_URGENT,
label: '加急:', label: '加急:',
price: 300, price: charge.proj.materialAgentUrgent,
handleClick: (_title, price) => { handleClick: (_title, additional) => {
nav(`/proj-new/xdj/${price}`) nav(`/proj-new/${ProjChargeType.MATERIAL_AGENT_URGENT}?pkg=${additional.pkg}&videoDemo=${additional.videoDemo}`)
} }
} }
]} ]}
@ -123,19 +157,22 @@ export default function ProjCreate() {
]} ]}
chargeLineArray={[ chargeLineArray={[
{ {
price: 100, id: ProjAdditionalType.PKG,
title: '安装包100元' price: charge.additional.pkg,
title: `安装包 ${charge.additional.pkg / 100}`
}, },
{ {
price: 50, id: ProjAdditionalType.VIDEO_DEMO,
title: '系统演示视频文件50元' price: charge.additional.videoDemo,
title: `系统演示视频文件 ${charge.additional.videoDemo / 100}`
} }
]} ]}
buyArray={[ buyArray={[
{ {
price: 200, id: ProjChargeType.MATERIAL,
handleClick: (_title, price) => { price: charge.proj.material,
nav(`/proj-new/x/${price}`) handleClick: () => {
nav(`/proj-new/${ProjChargeType.MATERIAL}`)
} }
} }
]} ]}
@ -159,9 +196,10 @@ export default function ProjCreate() {
]} ]}
buyArray={[ buyArray={[
{ {
price: 0, id: ProjChargeType.FREE,
handleClick: (_title, price) => { price: charge.proj.free,
nav(`/proj-new/m/${price}`) handleClick: () => {
nav(`/proj-new/${ProjChargeType.FREE}`)
} }
} }
]} ]}

View File

@ -1,38 +1,76 @@
import './proj-new.css'; import './proj-new.css';
import {Link, useNavigate, useParams} from "react-router-dom"; import {Link, useNavigate, useParams, useSearchParams} from "react-router-dom";
import {Breadcrumb, Button, Flex, Form, type FormProps, Input, Modal} from "antd"; import {Breadcrumb, Button, Flex, Form, Input, message, Modal, Spin} from "antd";
import {useState} from "react"; import {useEffect, useState} from "react";
import {get, post} from "../../util/AjaxUtils.ts";
import {IProjCharge, ProjAdditionalType, ProjChargeType} from "../../interfaces/proj/IProj.ts";
const {TextArea} = Input; const {TextArea} = Input;
type FieldType = { type ProjInfo = {
projTitle: string; projName: string;
projDesc: string; projIntroduction: string;
}; };
export default function ProjNew() { export default function ProjNew() {
const height = window.innerHeight - 150;
const [isModalOpen, setIsModalOpen] = useState(false);
const nav = useNavigate(); const nav = useNavigate();
const params = useParams(); const pathParams = useParams();
const [queryParams] = useSearchParams();
const onFinish: FormProps<FieldType>["onFinish"] = (values) => { const [messageApi, contextHolder] = message.useMessage();
console.log('Success:', values); const [loading, setLoading] = useState<boolean>(false);
setIsModalOpen(true); const height = window.innerHeight - 150;
}; const [isCreateModalOpen, setIsCreateModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [chargePrice, setChargePrice] = useState(0);
const [projInfo, setProjInfo] = useState<ProjInfo>({
projName: '',
projIntroduction: '',
});
const listProjChargeAdditional: string[] = [];
let createProjId = '';
const onFinishFailed: FormProps<FieldType>["onFinishFailed"] = (errorInfo) => { useEffect(() => {
console.log('Failed:', errorInfo); get({
}; messageApi: messageApi,
url: '/api/proj/charge/get',
const handleConfirmOk = () => { onSuccess({data}) {
// 扣款 const charge = data as IProjCharge;
nav('/proj-edit'); let price = 0;
} switch (pathParams.projChargeType) {
case ProjChargeType.ALL:
const handleConfirmCancel = () => { price = charge.proj.all;
setIsModalOpen(false); break;
} case ProjChargeType.MATERIAL_AGENT:
price = charge.proj.materialAgent;
break;
case ProjChargeType.MATERIAL_AGENT_URGENT:
price = charge.proj.materialAgentUrgent;
break;
case ProjChargeType.MATERIAL:
price = charge.proj.material;
break;
case ProjChargeType.FREE:
price = charge.proj.free;
break;
default:
messageApi.error({
type: 'error',
content: '价格类型错误',
})
}
if (queryParams.get('pkg')) {
price += charge.additional.pkg;
listProjChargeAdditional.push(ProjAdditionalType.PKG);
}
if (queryParams.get('videoDemo')) {
price += charge.additional.videoDemo;
listProjChargeAdditional.push(ProjAdditionalType.VIDEO_DEMO);
}
setChargePrice(price);
}
})
}, []);
const onBack = () => { const onBack = () => {
nav(-1); nav(-1);
@ -40,6 +78,7 @@ export default function ProjNew() {
return ( return (
<> <>
{contextHolder}
<Breadcrumb <Breadcrumb
items={[ items={[
{title: <Link to={'/'}></Link>}, {title: <Link to={'/'}></Link>},
@ -58,21 +97,26 @@ export default function ProjNew() {
wrapperCol={{span: 24}} wrapperCol={{span: 24}}
style={{width: 500}} style={{width: 500}}
initialValues={{remember: true}} initialValues={{remember: true}}
onFinish={onFinish} onFinish={(formData) => {
onFinishFailed={onFinishFailed} setIsCreateModalOpen(true);
setProjInfo({
projName: formData.projName,
projIntroduction: formData.projIntroduction
})
}}
autoComplete="off" autoComplete="off"
> >
<Form.Item<FieldType> <Form.Item<ProjInfo>
label="系统标题" label="系统标题"
name="projTitle" name="projName"
rules={[{required: true, message: '请输入系统标题'}]} rules={[{required: true, message: '请输入系统标题'}]}
> >
<Input placeholder="请输入系统标题"/> <Input placeholder="请输入系统标题"/>
</Form.Item> </Form.Item>
<Form.Item<FieldType> <Form.Item<ProjInfo>
label="简单描述" label="简单描述"
name="projDesc" name="projIntroduction"
rules={[{required: true, message: '请输入简单描述'}]} rules={[{required: true, message: '请输入简单描述'}]}
> >
<TextArea rows={6} placeholder="请用一段话简单描述系统"/> <TextArea rows={6} placeholder="请用一段话简单描述系统"/>
@ -80,7 +124,8 @@ export default function ProjNew() {
<Form.Item> <Form.Item>
<Flex align="center" justify="center" gap="large"> <Flex align="center" justify="center" gap="large">
<Button type="primary" htmlType="submit" style={{backgroundColor: 'var(--color-primary)'}}> <Button type="primary" htmlType="submit"
style={{backgroundColor: 'var(--color-primary)'}}>
</Button> </Button>
<Button type="default" htmlType="button" onClick={onBack}> <Button type="default" htmlType="button" onClick={onBack}>
@ -92,10 +137,51 @@ export default function ProjNew() {
</div> </div>
</div> </div>
</div> </div>
<Modal title="提示" okText="确定" cancelText="取消" open={isModalOpen} onOk={handleConfirmOk} <Modal title="提示"
onCancel={handleConfirmCancel}> okText="确定"
<div>{params.price}</div> cancelText="取消"
open={isCreateModalOpen}
onOk={() => {
setIsCreateModalOpen(false);
post({
messageApi,
url: '/api/proj/create',
body: {
projName: projInfo.projName,
projIntroduction: projInfo.projIntroduction,
projChargeType: pathParams.projChargeType,
listProjChargeAdditional: listProjChargeAdditional
},
onBefore() {
setLoading(true);
},
onSuccess({data}) {
setIsEditModalOpen(true);
createProjId = data.data;
},
onFinally() {
setLoading(false);
}
})
}}
onCancel={() => {
setIsCreateModalOpen(false);
}}>
<div> {chargePrice / 100} </div>
</Modal> </Modal>
<Modal title="提示"
okText="确定"
cancelText="取消"
open={isEditModalOpen}
onOk={() => {
nav(`/proj-edit/${createProjId}`);
}}
onCancel={() => {
setIsEditModalOpen(false);
}}>
<div></div>
</Modal>
<Spin tip="正在提交..." spinning={loading} fullscreen/>
</> </>
) )
} }

View File

@ -1,6 +1,100 @@
import axios from "axios"; import axios, {AxiosRequestConfig, AxiosResponse} from "axios";
import type {MessageInstance} from "antd/es/message/interface";
type Req = {
messageApi: MessageInstance;
url: string;
body?: any;
config?: AxiosRequestConfig;
onBefore?(): void;
onSuccess(data: AxiosResponse): void;
onFinally?(): void;
}
axios.defaults.baseURL = 'http://127.0.0.1:7025/copyright'; axios.defaults.baseURL = 'http://127.0.0.1:7025/copyright';
axios.defaults.headers['X-USER-ID'] = '80d3365e-0597-4988-979e-18ef1c3ec671'; axios.interceptors.request.use(config => {
if (config.method === 'get') {
config.data = {unused: 0} // 这个是关键点解决get 请求添加不上content_type
}
config.headers['X-USER-ID'] = '80d3365e-0597-4988-979e-18ef1c3ec671';
return config
}
);
export const Axios = axios; export const Axios = axios;
export function get(req: Req) {
req.onBefore?.();
Axios.get(req.url, req.config).then(res => {
req.onSuccess(res);
}).catch(error => {
if (error.response) {
const data = error.response.data;
req.messageApi.open({
type: 'error',
content: data.msg ? data.msg : `${data.path}(${data.status})`,
});
} else {
console.error(error)
}
}).finally(() => {
req.onFinally?.();
})
}
export function post(req: Req) {
req.onBefore?.();
Axios.post(req.url, req.body, req.config).then(res => {
req.onSuccess(res);
}).catch(error => {
if (error.response) {
const data = error.response.data;
req.messageApi.open({
type: 'error',
content: data.msg ? data.msg : `${data.path}(${data.status})`,
});
} else {
console.error(error)
}
}).finally(() => {
req.onFinally?.();
})
}
export function put(req: Req) {
req.onBefore?.();
Axios.put(req.url, req.config).then(res => {
req.onSuccess(res);
}).catch(error => {
if (error.response) {
const data = error.response.data;
req.messageApi.open({
type: 'error',
content: data.msg ? data.msg : `${data.path}(${data.status})`,
});
} else {
console.error(error)
}
}).finally(() => {
req.onFinally?.();
})
}
export function del(req: Req) {
req.onBefore?.();
Axios.delete(req.url, req.config).then(res => {
req.onSuccess(res);
}).catch(error => {
if (error.response) {
const data = error.response.data;
req.messageApi.open({
type: 'error',
content: data.msg ? data.msg : `${data.path}(${data.status})`,
});
} else {
console.error(error)
}
}).finally(() => {
req.onFinally?.();
})
}