Files
stop-shoping-web/src/pages/Product/index.tsx
2026-03-30 11:08:25 +08:00

390 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
productList,
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 productList({
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,
)}
</>
);
}