This commit is contained in:
2026-03-25 14:59:06 +08:00
commit ae315100b4
92 changed files with 9285 additions and 0 deletions

389
src/pages/Product/index.tsx Normal file
View File

@@ -0,0 +1,389 @@
import React, { useEffect, useReducer, useCallback, useRef } from "react";
import { createPortal } from "react-dom";
import { useOutletContext } from "react-router";
import {
TreeSelector,
type TreeItemWithUnknown,
} from "../../components/TreeSelector";
import { LoadMore } from "../../components/LoadMore";
import { orderByTypes } from "../../store/Types";
import { productReducer, initialState } from "../../store/product";
import * as actions from "../../store/product/action";
import {
getCategoryTree,
productSearch,
productDetail,
productDelete,
productEdit,
upload,
} from "../../api";
import {
type Category,
ProductOrderBys,
type ProductOrderBysType,
UploadScences,
} from "../../api/models";
import styles from "./index.module.css";
export function Component() {
const [states, dispatch] = useReducer(productReducer, initialState);
const asideContainer = useOutletContext<HTMLDivElement>();
const keywordRef = useRef<HTMLInputElement>(null);
useEffect(() => {
async function getTree() {
try {
const resp = await getCategoryTree();
if (resp.isSucced) {
dispatch(actions.setCategories(resp.data || []));
} else {
throw Error(resp.message || "");
}
} finally {
dispatch(actions.setLoading(false));
}
}
getTree();
}, []);
function categoryChecked(item: TreeItemWithUnknown) {
const category = item as Category;
dispatch(actions.setCategory(category));
}
const search = useCallback(async () => {
try {
dispatch(actions.setIsSearch(true));
const orderBys: ProductOrderBysType[] = [];
if (
!states.orderBys.createTime ||
states.orderBys.createTime == orderByTypes.ASC
)
orderBys.push(ProductOrderBys.CreateTimeDesc);
if (
!states.orderBys.categoryId ||
states.orderBys.categoryId == orderByTypes.ASC
)
orderBys.push(ProductOrderBys.Category);
else orderBys.push(ProductOrderBys.CategoryDesc);
if (
!states.orderBys.createTime ||
states.orderBys.createTime == orderByTypes.ASC
)
orderBys.push(ProductOrderBys.CreateTime);
else orderBys.push(ProductOrderBys.CreateTimeDesc);
const result = await productSearch({
categoryId: states.categoryId,
keyword: states.keyword,
orderBys: orderBys,
pageIndex: states.pageIndex,
pageSize: states.pageSize,
});
if (result.isSucced) {
dispatch(
actions.setSearchResult({
pageCount: result.data?.pageCount || 0,
products: result.data?.data || [],
}),
);
} else {
alert(result.message || "服务器错误");
}
} finally {
dispatch(actions.setIsSearch(false));
}
}, [
states.categoryId,
states.keyword,
states.orderBys,
states.pageIndex,
states.pageSize,
]);
useEffect(() => {
search();
}, [search]);
async function onLoadMore() {
dispatch(actions.setPageIndex(states.pageIndex + 1));
}
async function viewDetailClick(id: number) {
try {
const detailResult = await productDetail(id);
if (detailResult.isSucced) {
dispatch(actions.setEditProduct({ ...detailResult.data }));
} else {
throw Error(detailResult.message || "服务器错误");
}
} catch (err) {
throw Error(err as string);
}
}
async function pictureSelected(e: React.ChangeEvent) {
const el = e.target as HTMLInputElement;
if (el.files && el.files.length) {
try {
const uploadResult = await upload({
file: el.files[0],
scences: UploadScences.Product,
});
if (uploadResult.isSucced) {
dispatch(
actions.setEditProduct({
logoName: uploadResult.data?.newName,
logoUrl: uploadResult.data?.url,
}),
);
} else {
throw Error(uploadResult.message || "服务器错误");
}
} catch (err) {
throw Error(err as string);
}
}
}
const saveDetail = useCallback(async () => {
if (!states.editProduct.categoryId) {
alert("请选择分类");
return;
}
if (!states.editProduct.name) {
alert("请输入名称");
return;
}
if (!states.editProduct.minimumUnit) {
alert("请输入单位");
return;
}
if (!states.editProduct.unitPrice) {
alert("请输入单价");
return;
}
try {
const editResult = await productEdit({
categoryId: states.editProduct.categoryId || 0,
description: states.editProduct.description || "",
detail: states.editProduct.detail || "",
id: states.editProduct.id || 0,
logoName: states.editProduct.logoName || "",
minimumUnit: states.editProduct.minimumUnit || "",
name: states.editProduct.name || "",
unitPrice: states.editProduct.unitPrice || 0,
});
if (editResult.isSucced) {
alert("保存成功");
} else {
throw Error(editResult.message || "服务器错误");
}
} catch (err) {
throw Error(err as string);
}
}, [states.editProduct]);
async function deleteProduct(productId: number) {
try {
const deleteResult = await productDelete(productId);
if (deleteResult.isSucced) {
dispatch(actions.removeProduct(productId));
} else {
throw Error(deleteResult.message || "服务器错误");
}
} catch (err) {
throw Error(err as string);
}
}
function searchClick() {
if (keywordRef.current) {
dispatch(actions.setKeyword(keywordRef.current.value));
}
}
return (
<>
<div className={styles.container}>
<div className={styles.filter}>
<div className={styles.search}>
{!states.isLoading && (
<TreeSelector
items={states.categories}
onChecked={categoryChecked}
/>
)}
<div className={styles.keyword}>
<input
ref={keywordRef}
type="text"
placeholder="请输入关键词"
// value={states.keyword}
// onChange={(e) => dispatch(actions.setKeyword(e.target.value))}
onKeyDown={(e) => {
if (e.key == "Enter") searchClick();
}}
/>
<a onClick={searchClick}>
<i className="ss ssi-sousuo"></i>
</a>
</div>
</div>
<ul className={styles.sort}>
<li>
<a onClick={() => dispatch(actions.toggleOrderByCreateTime())}>
&nbsp;
{states.orderBys.createTime ? (
<i
className={`ss ssi-triangleupfill ${states.orderBys.createTime == "asc" ? "" : styles.desc}`}
></i>
) : (
<></>
)}
</a>
</li>
<li>
<a onClick={() => dispatch(actions.toggleOrderBySoldAmount())}>
&nbsp;
{states.orderBys.soldAmount ? (
<i
className={`ss ssi-triangleupfill ${states.orderBys.soldAmount == "asc" ? "" : styles.desc}`}
></i>
) : (
<></>
)}
<i></i>
</a>
</li>
</ul>
</div>
<div className={styles.content}>
{states.products.map((prod) => {
return (
<div
key={prod.id}
className={styles.card}
onClick={() => viewDetailClick(prod.id)}
>
<div className={styles.body}>
<div className={styles.logo}>
<img src={prod.logoUrl || ""} />
</div>
<div className={styles.text}>
<p className={styles.name}>{prod.name}</p>
<p>{prod.categoryName}</p>
<p>
<span>{prod.unitPrice}</span>/
<span>{prod.minimumUnit}</span>
</p>
<p>{prod.soldAmount}</p>
<p>{prod.createTime}</p>
<p className={styles.desc}>{prod.description}</p>
</div>
</div>
<div className={styles.footer}>
<button onClick={() => deleteProduct(prod.id)}></button>
</div>
</div>
);
})}
<LoadMore
pageIndex={states.pageIndex}
pageCount={states.pageCount}
loadMore={onLoadMore}
/>
</div>
</div>
{asideContainer &&
createPortal(
<div className={styles.aside}>
<div className={styles.form}>
<div className={styles.formGroup}>
<label></label>
<input
type="text"
value={states.editProduct.categoryName || ""}
disabled
/>
</div>
<div className={styles.formGroup}>
<label></label>
<input
type="text"
placeholder="请输入名称"
value={states.editProduct.name || ""}
onChange={(e) =>
dispatch(actions.setEditProduct({ name: e.target.value }))
}
/>
</div>
<div className={styles.formGroup}>
<label></label>
<textarea
placeholder="请输入描述"
value={states.editProduct.description || ""}
onChange={(e) =>
dispatch(
actions.setEditProduct({ description: e.target.value }),
)
}
rows={4}
/>
</div>
<div className={styles.formGroup}>
<label></label>
<div>
{states.editProduct.logoUrl && (
<img src={states.editProduct.logoUrl || ""} />
)}
<input
type="file"
accept="image/*"
onChange={pictureSelected}
/>
</div>
</div>
<div className={styles.formGroup}>
<label></label>
<input
type="text"
placeholder="请输入最小销售单位"
value={states.editProduct.minimumUnit || ""}
onChange={(e) =>
dispatch(
actions.setEditProduct({ minimumUnit: e.target.value }),
)
}
/>
</div>
<div className={styles.formGroup}>
<label></label>
<input
type="number"
value={states.editProduct.unitPrice || ""}
onChange={(e) =>
dispatch(
actions.setEditProduct({
unitPrice: Number.parseFloat(e.target.value),
}),
)
}
/>
</div>
<div className={styles.formGroup}>
<textarea rows={10}>TODO:</textarea>
</div>
<div className={styles.btnGroup}>
<button onClick={saveDetail}></button>
</div>
</div>
</div>,
asideContainer,
)}
</>
);
}