Implement contact filtering functionality in CreatePushTask component. Add modal for filtering contacts by tags, cities, and nickname/remark. Update styles for filter modal and enhance state management for filter values.
This commit is contained in:
@@ -10,6 +10,8 @@ import {
|
||||
Pagination,
|
||||
Spin,
|
||||
message,
|
||||
Modal,
|
||||
Select,
|
||||
} from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
@@ -23,6 +25,37 @@ import { getContactList, getGroupList } from "@/pages/pc/ckbox/weChat/api";
|
||||
import styles from "../../index.module.scss";
|
||||
import { ContactItem, PushType } from "../../types";
|
||||
|
||||
interface ContactFilterValues {
|
||||
includeTags: string[];
|
||||
excludeTags: string[];
|
||||
includeCities: string[];
|
||||
excludeCities: string[];
|
||||
nicknameRemark: string;
|
||||
groupIds: string[];
|
||||
}
|
||||
|
||||
const createDefaultFilterValues = (): ContactFilterValues => ({
|
||||
includeTags: [],
|
||||
excludeTags: [],
|
||||
includeCities: [],
|
||||
excludeCities: [],
|
||||
nicknameRemark: "",
|
||||
groupIds: [],
|
||||
});
|
||||
|
||||
const cloneFilterValues = (
|
||||
values: ContactFilterValues,
|
||||
): ContactFilterValues => ({
|
||||
includeTags: [...values.includeTags],
|
||||
excludeTags: [...values.excludeTags],
|
||||
includeCities: [...values.includeCities],
|
||||
excludeCities: [...values.excludeCities],
|
||||
nicknameRemark: values.nicknameRemark,
|
||||
groupIds: [...values.groupIds],
|
||||
});
|
||||
|
||||
const DISABLED_TAG_LABELS = new Set(["请选择标签"]);
|
||||
|
||||
interface StepSelectContactsProps {
|
||||
pushType: PushType;
|
||||
selectedAccounts: any[];
|
||||
@@ -41,6 +74,12 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
||||
const [page, setPage] = useState(1);
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const [total, setTotal] = useState(0);
|
||||
const [filterModalVisible, setFilterModalVisible] = useState(false);
|
||||
const [filterValues, setFilterValues] = useState<ContactFilterValues>(
|
||||
createDefaultFilterValues,
|
||||
);
|
||||
const [draftFilterValues, setDraftFilterValues] =
|
||||
useState<ContactFilterValues>(createDefaultFilterValues);
|
||||
|
||||
const pageSize = 20;
|
||||
|
||||
@@ -140,12 +179,159 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const filteredContacts = useMemo(() => {
|
||||
if (searchValue.trim()) {
|
||||
return contactsData;
|
||||
const tagOptions = useMemo(() => {
|
||||
const tagSet = new Set<string>();
|
||||
contactsData.forEach(contact => {
|
||||
(contact.labels || []).forEach(tag => {
|
||||
const normalizedTag = (tag || "").trim();
|
||||
if (normalizedTag && !DISABLED_TAG_LABELS.has(normalizedTag)) {
|
||||
tagSet.add(normalizedTag);
|
||||
}
|
||||
});
|
||||
});
|
||||
return Array.from(tagSet).map(tag => ({ label: tag, value: tag }));
|
||||
}, [contactsData]);
|
||||
|
||||
const cityOptions = useMemo(() => {
|
||||
const citySet = new Set<string>();
|
||||
contactsData.forEach(contact => {
|
||||
const city = (contact.city || contact.region || "").trim();
|
||||
if (city) {
|
||||
citySet.add(city);
|
||||
}
|
||||
});
|
||||
return Array.from(citySet).map(city => ({ label: city, value: city }));
|
||||
}, [contactsData]);
|
||||
|
||||
const groupOptions = useMemo(() => {
|
||||
const groupMap = new Map<string, string>();
|
||||
contactsData.forEach(contact => {
|
||||
const key =
|
||||
contact.groupName ||
|
||||
contact.groupLabel ||
|
||||
(contact.groupId !== undefined ? contact.groupId.toString() : "");
|
||||
if (key) {
|
||||
const display =
|
||||
contact.groupName || contact.groupLabel || `分组 ${key}`;
|
||||
groupMap.set(key, display);
|
||||
}
|
||||
});
|
||||
return Array.from(groupMap.entries()).map(([value, label]) => ({
|
||||
value,
|
||||
label,
|
||||
}));
|
||||
}, [contactsData]);
|
||||
|
||||
const hasActiveFilter = useMemo(() => {
|
||||
const {
|
||||
includeTags,
|
||||
excludeTags,
|
||||
includeCities,
|
||||
excludeCities,
|
||||
nicknameRemark,
|
||||
groupIds,
|
||||
} = filterValues;
|
||||
|
||||
if (
|
||||
includeTags.length ||
|
||||
excludeTags.length ||
|
||||
includeCities.length ||
|
||||
excludeCities.length ||
|
||||
groupIds.length ||
|
||||
nicknameRemark.trim()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return contactsData;
|
||||
}, [contactsData, searchValue]);
|
||||
return false;
|
||||
}, [filterValues]);
|
||||
|
||||
const filteredContacts = useMemo(() => {
|
||||
const keyword = searchValue.trim().toLowerCase();
|
||||
const nicknameKeyword = filterValues.nicknameRemark.trim().toLowerCase();
|
||||
|
||||
return contactsData.filter(contact => {
|
||||
const labels = contact.labels || [];
|
||||
const city = (contact.city || contact.region || "").toLowerCase();
|
||||
const groupValue =
|
||||
contact.groupName ||
|
||||
contact.groupLabel ||
|
||||
(contact.groupId !== undefined ? contact.groupId.toString() : "");
|
||||
|
||||
if (keyword) {
|
||||
const combined = `${contact.nickname || ""} ${
|
||||
contact.conRemark || ""
|
||||
}`.toLowerCase();
|
||||
if (!combined.includes(keyword)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterValues.includeTags.length > 0) {
|
||||
const hasAllIncludes = filterValues.includeTags.every(tag =>
|
||||
labels.includes(tag),
|
||||
);
|
||||
if (!hasAllIncludes) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterValues.excludeTags.length > 0) {
|
||||
const hasExcluded = filterValues.excludeTags.some(tag =>
|
||||
labels.includes(tag),
|
||||
);
|
||||
if (hasExcluded) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterValues.includeCities.length > 0) {
|
||||
const matchCity = filterValues.includeCities.some(value =>
|
||||
city.includes(value.toLowerCase()),
|
||||
);
|
||||
if (!matchCity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterValues.excludeCities.length > 0) {
|
||||
const matchExcludedCity = filterValues.excludeCities.some(value =>
|
||||
city.includes(value.toLowerCase()),
|
||||
);
|
||||
if (matchExcludedCity) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (nicknameKeyword) {
|
||||
const combined = `${contact.nickname || ""} ${
|
||||
contact.conRemark || ""
|
||||
}`.toLowerCase();
|
||||
if (!combined.includes(nicknameKeyword)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filterValues.groupIds.length > 0) {
|
||||
if (!groupValue) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
!filterValues.groupIds.some(value => value === groupValue.toString())
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, [contactsData, filterValues, searchValue]);
|
||||
|
||||
const displayTotal = useMemo(() => {
|
||||
if (hasActiveFilter) {
|
||||
return filteredContacts.length;
|
||||
}
|
||||
return total;
|
||||
}, [filteredContacts, hasActiveFilter, total]);
|
||||
|
||||
const handleContactToggle = (contact: ContactItem) => {
|
||||
const isSelected = selectedContacts.some(c => c.id === contact.id);
|
||||
@@ -176,6 +362,39 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
||||
onChange([...selectedContacts, ...toAdd]);
|
||||
};
|
||||
|
||||
const openFilterModal = () => {
|
||||
setDraftFilterValues(cloneFilterValues(filterValues));
|
||||
setFilterModalVisible(true);
|
||||
};
|
||||
|
||||
const closeFilterModal = () => {
|
||||
setFilterModalVisible(false);
|
||||
};
|
||||
|
||||
const handleFilterConfirm = () => {
|
||||
setFilterValues(cloneFilterValues(draftFilterValues));
|
||||
setPage(1);
|
||||
setFilterModalVisible(false);
|
||||
};
|
||||
|
||||
const handleFilterReset = () => {
|
||||
const nextValues = createDefaultFilterValues();
|
||||
setDraftFilterValues(nextValues);
|
||||
setFilterValues(nextValues);
|
||||
setPage(1);
|
||||
setFilterModalVisible(false);
|
||||
};
|
||||
|
||||
const updateDraftFilter = <K extends keyof ContactFilterValues>(
|
||||
key: K,
|
||||
value: ContactFilterValues[K],
|
||||
) => {
|
||||
setDraftFilterValues(prev => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handlePageChange = (p: number) => {
|
||||
setPage(p);
|
||||
};
|
||||
@@ -195,14 +414,22 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
||||
onChange={e => handleSearchChange(e.target.value)}
|
||||
allowClear
|
||||
/>
|
||||
<Button onClick={handleSelectAllContacts}>全选</Button>
|
||||
</div>
|
||||
<div className={styles.contentBody}>
|
||||
<div className={styles.contactList}>
|
||||
<div className={styles.listHeader}>
|
||||
<span>
|
||||
{stepTitle}列表(共{total}个)
|
||||
{stepTitle}列表(共{displayTotal}个)
|
||||
</span>
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
<Button onClick={handleSelectAllContacts}>全选</Button>
|
||||
<Button
|
||||
type={hasActiveFilter ? "primary" : "default"}
|
||||
onClick={openFilterModal}
|
||||
>
|
||||
筛选
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.listContent}>
|
||||
{loadingContacts ? (
|
||||
@@ -260,13 +487,13 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{total > 0 && (
|
||||
{displayTotal > 0 && (
|
||||
<div className={styles.paginationContainer}>
|
||||
<Pagination
|
||||
size="small"
|
||||
current={page}
|
||||
pageSize={pageSize}
|
||||
total={total}
|
||||
total={displayTotal}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
/>
|
||||
@@ -332,6 +559,118 @@ const StepSelectContacts: React.FC<StepSelectContactsProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
title={`筛选${stepTitle}`}
|
||||
open={filterModalVisible}
|
||||
onCancel={closeFilterModal}
|
||||
width={720}
|
||||
className={styles.filterModal}
|
||||
footer={[
|
||||
<Button key="reset" onClick={handleFilterReset}>
|
||||
重置
|
||||
</Button>,
|
||||
<Button key="cancel" onClick={closeFilterModal}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button key="ok" type="primary" onClick={handleFilterConfirm}>
|
||||
确定
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterLabel}>标签</div>
|
||||
<div className={styles.filterControls}>
|
||||
<div className={styles.filterControl}>
|
||||
<Button type="primary">包含</Button>
|
||||
<Select
|
||||
allowClear
|
||||
mode="multiple"
|
||||
placeholder="请选择"
|
||||
options={tagOptions}
|
||||
value={draftFilterValues.includeTags}
|
||||
onChange={(value: string[]) =>
|
||||
updateDraftFilter("includeTags", value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterControl}>
|
||||
<Button className={styles.excludeButton}>不包含</Button>
|
||||
<Select
|
||||
allowClear
|
||||
mode="multiple"
|
||||
placeholder="请选择"
|
||||
options={tagOptions}
|
||||
value={draftFilterValues.excludeTags}
|
||||
onChange={(value: string[]) =>
|
||||
updateDraftFilter("excludeTags", value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterLabel}>城市</div>
|
||||
<div className={styles.filterControls}>
|
||||
<div className={styles.filterControl}>
|
||||
<Button type="primary">包含</Button>
|
||||
<Select
|
||||
allowClear
|
||||
mode="multiple"
|
||||
placeholder="请选择"
|
||||
options={cityOptions}
|
||||
value={draftFilterValues.includeCities}
|
||||
onChange={(value: string[]) =>
|
||||
updateDraftFilter("includeCities", value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterControl}>
|
||||
<Button className={styles.excludeButton}>不包含</Button>
|
||||
<Select
|
||||
allowClear
|
||||
mode="multiple"
|
||||
placeholder="请选择"
|
||||
options={cityOptions}
|
||||
value={draftFilterValues.excludeCities}
|
||||
onChange={(value: string[]) =>
|
||||
updateDraftFilter("excludeCities", value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterLabel}>昵称/备注</div>
|
||||
<div className={styles.filterSingleControl}>
|
||||
<Input
|
||||
placeholder="请输入内容"
|
||||
value={draftFilterValues.nicknameRemark}
|
||||
onChange={e =>
|
||||
updateDraftFilter("nicknameRemark", e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.filterRow}>
|
||||
<div className={styles.filterLabel}>分组</div>
|
||||
<div className={styles.filterSingleControl}>
|
||||
<Select
|
||||
allowClear
|
||||
mode="multiple"
|
||||
placeholder="请选择"
|
||||
options={groupOptions}
|
||||
value={draftFilterValues.groupIds}
|
||||
onChange={(value: string[]) =>
|
||||
updateDraftFilter("groupIds", value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -576,6 +576,89 @@
|
||||
}
|
||||
}
|
||||
|
||||
.filterModal {
|
||||
:global(.ant-modal-body) {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.filterRow {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.filterLabel {
|
||||
width: 64px;
|
||||
text-align: right;
|
||||
line-height: 32px;
|
||||
font-weight: 500;
|
||||
color: #1f1f1f;
|
||||
}
|
||||
|
||||
.filterControls {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.filterControl {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
:global(.ant-select) {
|
||||
min-width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.filterSingleControl {
|
||||
flex: 1;
|
||||
|
||||
:global(.ant-input),
|
||||
:global(.ant-select) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.extendFields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.extendFieldItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.extendFieldLabel {
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.extendFieldItem :global(.ant-input) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.excludeButton {
|
||||
background-color: #faad14;
|
||||
border-color: #faad14;
|
||||
color: #fff;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #d48806;
|
||||
border-color: #d48806;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
@@ -12,4 +12,10 @@ export interface ContactItem {
|
||||
gender?: number;
|
||||
region?: string;
|
||||
type?: "friend" | "group";
|
||||
labels?: string[];
|
||||
groupId?: number | string;
|
||||
groupName?: string;
|
||||
groupLabel?: string;
|
||||
city?: string;
|
||||
extendFields?: Record<string, any>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user