390 lines
12 KiB
TypeScript
390 lines
12 KiB
TypeScript
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())}>
|
||
创建时间
|
||
{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,
|
||
)}
|
||
</>
|
||
);
|
||
}
|