✨
This commit is contained in:
389
src/pages/Product/index.tsx
Normal file
389
src/pages/Product/index.tsx
Normal 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())}>
|
||||
创建时间
|
||||
{states.orderBys.createTime ? (
|
||||
<i
|
||||
className={`ss ssi-triangleupfill ${states.orderBys.createTime == "asc" ? "" : styles.desc}`}
|
||||
></i>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a onClick={() => dispatch(actions.toggleOrderBySoldAmount())}>
|
||||
售出数量
|
||||
{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,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user