feat: 本次提交更新内容如下
保存项目构建完成
This commit is contained in:
@@ -357,7 +357,9 @@ export default function NewMomentsSyncTask() {
|
||||
>
|
||||
<ChevronLeft className="h-6 w-6" />
|
||||
</Button>
|
||||
<h1 className="ml-2 text-lg font-medium">新建朋友圈同步</h1>
|
||||
<h1 className="ml-2 text-lg font-medium">
|
||||
{isEditMode ? "编辑朋友圈同步" : "新建朋友圈同步"}
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -436,7 +438,7 @@ export default function NewMomentsSyncTask() {
|
||||
loading={loading}
|
||||
className="flex-1 h-12 bg-blue-500 hover:bg-blue-600 rounded-lg text-white"
|
||||
>
|
||||
{loading ? "创建中..." : "完成"}
|
||||
{loading ? (isEditMode ? "保存中..." : "创建中...") : "完成"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# 基础环境变量示例
|
||||
VITE_API_BASE_URL=http://www.yishi.com
|
||||
# VITE_API_BASE_URL=http://www.yishi.com
|
||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
|
||||
VITE_APP_TITLE=Nkebao Base
|
||||
|
||||
|
||||
@@ -1,257 +1,308 @@
|
||||
import React, { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Input as AntdInput, Switch } from "antd";
|
||||
import { Button, Collapse, Toast, DatePicker, Tabs } from "antd-mobile";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import FriendSelection from "@/components/FriendSelection";
|
||||
import GroupSelection from "@/components/GroupSelection";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./index.module.scss";
|
||||
import request from "@/api/request";
|
||||
|
||||
const { TextArea } = AntdInput;
|
||||
|
||||
function formatDate(date: Date | null) {
|
||||
if (!date) return "";
|
||||
// 格式化为 YYYY-MM-DD
|
||||
const y = date.getFullYear();
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const d = date.getDate().toString().padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
export default function ContentForm() {
|
||||
const navigate = useNavigate();
|
||||
const [sourceType, setSourceType] = useState<"friends" | "groups">("friends");
|
||||
const [name, setName] = useState("");
|
||||
const [selectedFriends, setSelectedFriends] = useState<string[]>([]);
|
||||
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||
const [useAI, setUseAI] = useState(false);
|
||||
const [aiPrompt, setAIPrompt] = useState("");
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||
const [keywordsInclude, setKeywordsInclude] = useState("");
|
||||
const [keywordsExclude, setKeywordsExclude] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!name.trim()) {
|
||||
Toast.show({ content: "请输入内容库名称", position: "top" });
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const payload = {
|
||||
name,
|
||||
sourceType: sourceType === "friends" ? 1 : 2,
|
||||
friends: selectedFriends,
|
||||
groups: selectedGroups,
|
||||
groupMembers: {},
|
||||
keywordInclude: keywordsInclude
|
||||
.split(/,|,|\n|\s+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
keywordExclude: keywordsExclude
|
||||
.split(/,|,|\n|\s+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
aiPrompt,
|
||||
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
|
||||
startTime: dateRange[0] ? formatDate(dateRange[0]) : "",
|
||||
endTime: dateRange[1] ? formatDate(dateRange[1]) : "",
|
||||
status: enabled ? 1 : 0,
|
||||
};
|
||||
await request("/v1/content/library/create", payload, "POST");
|
||||
Toast.show({ content: "创建成功", position: "top" });
|
||||
navigate("/content");
|
||||
} catch (e: any) {
|
||||
Toast.show({ content: e?.message || "创建失败", position: "top" });
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title="新建内容库" />}
|
||||
footer={
|
||||
<div style={{ padding: "16px", backgroundColor: "#fff" }}>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
loading={submitting}
|
||||
disabled={submitting}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
创建内容库
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style["form-page"]}>
|
||||
<form
|
||||
className={style["form-main"]}
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>
|
||||
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
|
||||
内容库名称
|
||||
</label>
|
||||
<AntdInput
|
||||
placeholder="请输入内容库名称"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={style["input"]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style["section-title"]}>数据来源配置</div>
|
||||
<div className={style["form-section"]}>
|
||||
<Tabs
|
||||
activeKey={sourceType}
|
||||
onChange={(key) => setSourceType(key as "friends" | "groups")}
|
||||
className={style["tabs-bar"]}
|
||||
>
|
||||
<Tabs.Tab title="选择微信好友" key="friends">
|
||||
<FriendSelection
|
||||
selectedFriends={selectedFriends}
|
||||
onSelect={setSelectedFriends}
|
||||
placeholder="选择微信好友"
|
||||
/>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="选择聊天群" key="groups">
|
||||
<GroupSelection
|
||||
selectedGroups={selectedGroups}
|
||||
onSelect={setSelectedGroups}
|
||||
placeholder="选择聊天群"
|
||||
/>
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Collapse
|
||||
defaultActiveKey={["keywords"]}
|
||||
className={style["collapse"]}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="keywords"
|
||||
title={<span className={style["form-label"]}>关键词设置</span>}
|
||||
>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>包含关键词</label>
|
||||
<TextArea
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
value={keywordsInclude}
|
||||
onChange={(e) => setKeywordsInclude(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>排除关键词</label>
|
||||
<TextArea
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
value={keywordsExclude}
|
||||
onChange={(e) => setKeywordsExclude(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
|
||||
<div className={style["section-title"]}>是否启用AI</div>
|
||||
<div
|
||||
className={style["form-section"]}
|
||||
style={{ display: "flex", alignItems: "center", gap: 12 }}
|
||||
>
|
||||
<Switch checked={useAI} onChange={setUseAI} />
|
||||
<span className={style["ai-desc"]}>
|
||||
启用AI后,该内容库下的所有内容都会通过AI生成
|
||||
</span>
|
||||
</div>
|
||||
{useAI && (
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>AI提示词</label>
|
||||
<AntdInput
|
||||
placeholder="请输入AI提示词"
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAIPrompt(e.target.value)}
|
||||
className={style["input"]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={style["section-title"]}>时间限制</div>
|
||||
<div
|
||||
className={style["form-section"]}
|
||||
style={{ display: "flex", gap: 12 }}
|
||||
>
|
||||
<label>开始时间</label>
|
||||
<div style={{ flex: 1 }}>
|
||||
<AntdInput
|
||||
readOnly
|
||||
value={dateRange[0] ? dateRange[0].toLocaleDateString() : ""}
|
||||
placeholder="年/月/日"
|
||||
className={style["input"]}
|
||||
onClick={() => setShowStartPicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showStartPicker}
|
||||
title="开始时间"
|
||||
value={dateRange[0]}
|
||||
onClose={() => setShowStartPicker(false)}
|
||||
onConfirm={(val) => {
|
||||
setDateRange([val, dateRange[1]]);
|
||||
setShowStartPicker(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<label>结束时间</label>
|
||||
<div style={{ flex: 1 }}>
|
||||
<AntdInput
|
||||
readOnly
|
||||
value={dateRange[1] ? dateRange[1].toLocaleDateString() : ""}
|
||||
placeholder="年/月/日"
|
||||
className={style["input"]}
|
||||
onClick={() => setShowEndPicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showEndPicker}
|
||||
title="结束时间"
|
||||
value={dateRange[1]}
|
||||
onClose={() => setShowEndPicker(false)}
|
||||
onConfirm={(val) => {
|
||||
setDateRange([dateRange[0], val]);
|
||||
setShowEndPicker(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={style["section-title"]}
|
||||
style={{
|
||||
marginTop: 24,
|
||||
marginBottom: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>是否启用</span>
|
||||
<Switch checked={enabled} onChange={setEnabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { Input as AntdInput, Switch } from "antd";
|
||||
import { Button, Collapse, Toast, DatePicker, Tabs } from "antd-mobile";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import FriendSelection from "@/components/FriendSelection";
|
||||
import GroupSelection from "@/components/GroupSelection";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import style from "./index.module.scss";
|
||||
import request from "@/api/request";
|
||||
import { getContentLibraryDetail, updateContentLibrary } from "./api";
|
||||
|
||||
const { TextArea } = AntdInput;
|
||||
|
||||
function formatDate(date: Date | null) {
|
||||
if (!date) return "";
|
||||
// 格式化为 YYYY-MM-DD
|
||||
const y = date.getFullYear();
|
||||
const m = (date.getMonth() + 1).toString().padStart(2, "0");
|
||||
const d = date.getDate().toString().padStart(2, "0");
|
||||
return `${y}-${m}-${d}`;
|
||||
}
|
||||
|
||||
export default function ContentForm() {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id?: string }>();
|
||||
const isEdit = !!id;
|
||||
const [sourceType, setSourceType] = useState<"friends" | "groups">("friends");
|
||||
const [name, setName] = useState("");
|
||||
const [selectedFriends, setSelectedFriends] = useState<string[]>([]);
|
||||
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
|
||||
const [useAI, setUseAI] = useState(false);
|
||||
const [aiPrompt, setAIPrompt] = useState("");
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([
|
||||
null,
|
||||
null,
|
||||
]);
|
||||
const [showStartPicker, setShowStartPicker] = useState(false);
|
||||
const [showEndPicker, setShowEndPicker] = useState(false);
|
||||
const [keywordsInclude, setKeywordsInclude] = useState("");
|
||||
const [keywordsExclude, setKeywordsExclude] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 编辑模式下拉详情并回填
|
||||
useEffect(() => {
|
||||
if (isEdit && id) {
|
||||
setLoading(true);
|
||||
getContentLibraryDetail(id)
|
||||
.then((data) => {
|
||||
setName(data.name || "");
|
||||
setSourceType(data.sourceType === 1 ? "friends" : "groups");
|
||||
setSelectedFriends(data.sourceFriends || []);
|
||||
setSelectedGroups(data.sourceGroups || []);
|
||||
setKeywordsInclude((data.keywordInclude || []).join(","));
|
||||
setKeywordsExclude((data.keywordExclude || []).join(","));
|
||||
setAIPrompt(data.aiPrompt || "");
|
||||
setUseAI(!!data.aiPrompt);
|
||||
setEnabled(data.status === 1);
|
||||
// 时间范围
|
||||
let start = data.timeStart || data.startTime;
|
||||
let end = data.timeEnd || data.endTime;
|
||||
setDateRange([
|
||||
start ? new Date(start) : null,
|
||||
end ? new Date(end) : null,
|
||||
]);
|
||||
})
|
||||
.catch((e) => {
|
||||
Toast.show({
|
||||
content: e?.message || "获取详情失败",
|
||||
position: "top",
|
||||
});
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [isEdit, id]);
|
||||
|
||||
const handleSubmit = async (e?: React.FormEvent) => {
|
||||
if (e) e.preventDefault();
|
||||
if (!name.trim()) {
|
||||
Toast.show({ content: "请输入内容库名称", position: "top" });
|
||||
return;
|
||||
}
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const payload = {
|
||||
name,
|
||||
sourceType: sourceType === "friends" ? 1 : 2,
|
||||
friends: selectedFriends,
|
||||
groups: selectedGroups,
|
||||
groupMembers: {},
|
||||
keywordInclude: keywordsInclude
|
||||
.split(/,|,|\n|\s+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
keywordExclude: keywordsExclude
|
||||
.split(/,|,|\n|\s+/)
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean),
|
||||
aiPrompt,
|
||||
timeEnabled: dateRange[0] || dateRange[1] ? 1 : 0,
|
||||
startTime: dateRange[0] ? formatDate(dateRange[0]) : "",
|
||||
endTime: dateRange[1] ? formatDate(dateRange[1]) : "",
|
||||
status: enabled ? 1 : 0,
|
||||
};
|
||||
if (isEdit && id) {
|
||||
await updateContentLibrary({ id, ...payload });
|
||||
Toast.show({ content: "保存成功", position: "top" });
|
||||
} else {
|
||||
await request("/v1/content/library/create", payload, "POST");
|
||||
Toast.show({ content: "创建成功", position: "top" });
|
||||
}
|
||||
navigate("/content");
|
||||
} catch (e: any) {
|
||||
Toast.show({
|
||||
content: e?.message || (isEdit ? "保存失败" : "创建失败"),
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title={isEdit ? "编辑内容库" : "新建内容库"} />}
|
||||
footer={
|
||||
<div style={{ padding: "16px", backgroundColor: "#fff" }}>
|
||||
<Button
|
||||
block
|
||||
color="primary"
|
||||
loading={submitting || loading}
|
||||
disabled={submitting || loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{isEdit
|
||||
? submitting
|
||||
? "保存中..."
|
||||
: "保存内容库"
|
||||
: submitting
|
||||
? "创建中..."
|
||||
: "创建内容库"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className={style["form-page"]}>
|
||||
<form
|
||||
className={style["form-main"]}
|
||||
onSubmit={(e) => e.preventDefault()}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>
|
||||
<span style={{ color: "#ff4d4f", marginRight: 4 }}>*</span>
|
||||
内容库名称
|
||||
</label>
|
||||
<AntdInput
|
||||
placeholder="请输入内容库名称"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className={style["input"]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={style["section-title"]}>数据来源配置</div>
|
||||
<div className={style["form-section"]}>
|
||||
<Tabs
|
||||
activeKey={sourceType}
|
||||
onChange={(key) => setSourceType(key as "friends" | "groups")}
|
||||
className={style["tabs-bar"]}
|
||||
>
|
||||
<Tabs.Tab title="选择微信好友" key="friends">
|
||||
<FriendSelection
|
||||
selectedFriends={selectedFriends}
|
||||
onSelect={setSelectedFriends}
|
||||
placeholder="选择微信好友"
|
||||
/>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab title="选择聊天群" key="groups">
|
||||
<GroupSelection
|
||||
selectedGroups={selectedGroups}
|
||||
onSelect={setSelectedGroups}
|
||||
placeholder="选择聊天群"
|
||||
/>
|
||||
</Tabs.Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
<Collapse
|
||||
defaultActiveKey={["keywords"]}
|
||||
className={style["collapse"]}
|
||||
>
|
||||
<Collapse.Panel
|
||||
key="keywords"
|
||||
title={<span className={style["form-label"]}>关键词设置</span>}
|
||||
>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>包含关键词</label>
|
||||
<TextArea
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
value={keywordsInclude}
|
||||
onChange={(e) => setKeywordsInclude(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</div>
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>排除关键词</label>
|
||||
<TextArea
|
||||
placeholder="多个关键词用逗号分隔"
|
||||
value={keywordsExclude}
|
||||
onChange={(e) => setKeywordsExclude(e.target.value)}
|
||||
className={style["input"]}
|
||||
autoSize={{ minRows: 2, maxRows: 4 }}
|
||||
/>
|
||||
</div>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
|
||||
<div className={style["section-title"]}>是否启用AI</div>
|
||||
<div
|
||||
className={style["form-section"]}
|
||||
style={{ display: "flex", alignItems: "center", gap: 12 }}
|
||||
>
|
||||
<Switch checked={useAI} onChange={setUseAI} />
|
||||
<span className={style["ai-desc"]}>
|
||||
启用AI后,该内容库下的所有内容都会通过AI生成
|
||||
</span>
|
||||
</div>
|
||||
{useAI && (
|
||||
<div className={style["form-section"]}>
|
||||
<label className={style["form-label"]}>AI提示词</label>
|
||||
<AntdInput
|
||||
placeholder="请输入AI提示词"
|
||||
value={aiPrompt}
|
||||
onChange={(e) => setAIPrompt(e.target.value)}
|
||||
className={style["input"]}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={style["section-title"]}>时间限制</div>
|
||||
<div
|
||||
className={style["form-section"]}
|
||||
style={{ display: "flex", gap: 12 }}
|
||||
>
|
||||
<label>开始时间</label>
|
||||
<div style={{ flex: 1 }}>
|
||||
<AntdInput
|
||||
readOnly
|
||||
value={dateRange[0] ? dateRange[0].toLocaleDateString() : ""}
|
||||
placeholder="年/月/日"
|
||||
className={style["input"]}
|
||||
onClick={() => setShowStartPicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showStartPicker}
|
||||
title="开始时间"
|
||||
value={dateRange[0]}
|
||||
onClose={() => setShowStartPicker(false)}
|
||||
onConfirm={(val) => {
|
||||
setDateRange([val, dateRange[1]]);
|
||||
setShowStartPicker(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<label>结束时间</label>
|
||||
<div style={{ flex: 1 }}>
|
||||
<AntdInput
|
||||
readOnly
|
||||
value={dateRange[1] ? dateRange[1].toLocaleDateString() : ""}
|
||||
placeholder="年/月/日"
|
||||
className={style["input"]}
|
||||
onClick={() => setShowEndPicker(true)}
|
||||
/>
|
||||
<DatePicker
|
||||
visible={showEndPicker}
|
||||
title="结束时间"
|
||||
value={dateRange[1]}
|
||||
onClose={() => setShowEndPicker(false)}
|
||||
onConfirm={(val) => {
|
||||
setDateRange([dateRange[0], val]);
|
||||
setShowEndPicker(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={style["section-title"]}
|
||||
style={{
|
||||
marginTop: 24,
|
||||
marginBottom: 8,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<span>是否启用</span>
|
||||
<Switch checked={enabled} onChange={setEnabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,309 +1,309 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Card,
|
||||
Badge,
|
||||
Avatar,
|
||||
Skeleton,
|
||||
message,
|
||||
Spin,
|
||||
Divider,
|
||||
Pagination,
|
||||
} from "antd";
|
||||
import {
|
||||
LikeOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import styles from "./record.module.scss";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { fetchLikeRecords } from "./api";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
export default function AutoLikeRecord() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
const [recordsLoading, setRecordsLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setRecordsLoading(true);
|
||||
fetchLikeRecords(id, 1, pageSize)
|
||||
.then((response: any) => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(1);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("获取点赞记录失败,请稍后重试");
|
||||
})
|
||||
.finally(() => setRecordsLoading(false));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
fetchLikeRecords(id!, 1, pageSize, searchTerm)
|
||||
.then((response: any) => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(1);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("获取点赞记录失败,请稍后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchLikeRecords(id!, currentPage, pageSize, searchTerm)
|
||||
.then((response: any) => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("获取点赞记录失败,请稍后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
fetchLikeRecords(id!, newPage, pageSize, searchTerm)
|
||||
.then((response: any) => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(newPage);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("获取点赞记录失败,请稍后重试");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavCommon title="点赞记录" />
|
||||
<div className={styles.headerSearchBar}>
|
||||
<div className={styles.headerSearchInputWrap}>
|
||||
<Input
|
||||
prefix={<SearchOutlined className={styles.headerSearchIcon} />}
|
||||
placeholder="搜索好友昵称或内容"
|
||||
className={styles.headerSearchInput}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
icon={<ReloadOutlined spin={recordsLoading} />}
|
||||
onClick={handleRefresh}
|
||||
loading={recordsLoading}
|
||||
type="default"
|
||||
shape="circle"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<div className={styles.footerPagination}>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
showQuickJumper
|
||||
showTotal={(total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
}
|
||||
size="default"
|
||||
className={styles.pagination}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={styles.bgWrap}>
|
||||
<div className={styles.contentWrap}>
|
||||
{recordsLoading ? (
|
||||
<div className={styles.skeletonWrap}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className={styles.skeletonCard}>
|
||||
<div className={styles.skeletonCardHeader}>
|
||||
<Skeleton.Avatar
|
||||
active
|
||||
size={40}
|
||||
className={styles.skeletonAvatar}
|
||||
/>
|
||||
<div className={styles.skeletonNameWrap}>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeletonName}
|
||||
style={{ width: 96 }}
|
||||
/>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeletonSub}
|
||||
style={{ width: 64 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className={styles.skeletonSep} />
|
||||
<div className={styles.skeletonContentWrap}>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeletonContent1}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeletonContent2}
|
||||
style={{ width: "75%" }}
|
||||
/>
|
||||
<div className={styles.skeletonImgWrap}>
|
||||
<Skeleton.Image
|
||||
active
|
||||
className={styles.skeletonImg}
|
||||
style={{ width: 80, height: 80 }}
|
||||
/>
|
||||
<Skeleton.Image
|
||||
active
|
||||
className={styles.skeletonImg}
|
||||
style={{ width: 80, height: 80 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className={styles.emptyWrap}>
|
||||
<LikeOutlined className={styles.emptyIcon} />
|
||||
<p className={styles.emptyText}>暂无点赞记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{records.map((record) => (
|
||||
<div key={record.id} className={styles.recordCard}>
|
||||
<div className={styles.recordCardHeader}>
|
||||
<div className={styles.recordCardHeaderLeft}>
|
||||
<Avatar
|
||||
src={record.friendAvatar || undefined}
|
||||
icon={<UserOutlined />}
|
||||
size={40}
|
||||
className={styles.avatarImg}
|
||||
/>
|
||||
<div className={styles.friendInfo}>
|
||||
<div
|
||||
className={styles.friendName}
|
||||
title={record.friendName}
|
||||
>
|
||||
{record.friendName}
|
||||
</div>
|
||||
<div className={styles.friendSub}>内容发布者</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className={styles.timeBadge}
|
||||
count={formatDate(record.momentTime || record.likeTime)}
|
||||
style={{
|
||||
background: "#e8f0fe",
|
||||
color: "#333",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Divider className={styles.cardSep} />
|
||||
<div className={styles.cardContent}>
|
||||
{record.content && (
|
||||
<p className={styles.contentText}>{record.content}</p>
|
||||
)}
|
||||
{Array.isArray(record.resUrls) &&
|
||||
record.resUrls.length > 0 && (
|
||||
<div
|
||||
className={
|
||||
`${styles.imgGrid} ` +
|
||||
(record.resUrls.length === 1
|
||||
? styles.grid1
|
||||
: record.resUrls.length === 2
|
||||
? styles.grid2
|
||||
: record.resUrls.length <= 3
|
||||
? styles.grid3
|
||||
: record.resUrls.length <= 6
|
||||
? styles.grid6
|
||||
: styles.grid9)
|
||||
}
|
||||
>
|
||||
{record.resUrls
|
||||
.slice(0, 9)
|
||||
.map((image: string, idx: number) => (
|
||||
<div key={idx} className={styles.imgItem}>
|
||||
<img
|
||||
src={image}
|
||||
alt={`内容图片 ${idx + 1}`}
|
||||
className={styles.img}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.operatorWrap}>
|
||||
<Avatar
|
||||
src={record.operatorAvatar || undefined}
|
||||
icon={<UserOutlined />}
|
||||
size={32}
|
||||
className={styles.operatorAvatar}
|
||||
/>
|
||||
<div className={styles.operatorInfo}>
|
||||
<span
|
||||
className={styles.operatorName}
|
||||
title={record.operatorName}
|
||||
>
|
||||
{record.operatorName}
|
||||
</span>
|
||||
<span className={styles.operatorAction}>
|
||||
<LikeOutlined
|
||||
style={{ color: "red", marginRight: 4 }}
|
||||
/>
|
||||
已赞
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
Input,
|
||||
Card,
|
||||
Badge,
|
||||
Avatar,
|
||||
Skeleton,
|
||||
message,
|
||||
Spin,
|
||||
Divider,
|
||||
Pagination,
|
||||
} from "antd";
|
||||
import {
|
||||
LikeOutlined,
|
||||
ReloadOutlined,
|
||||
SearchOutlined,
|
||||
UserOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import styles from "./record.module.scss";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import { fetchLikeRecords } from "./api";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
try {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString("zh-CN", {
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (error) {
|
||||
return dateString;
|
||||
}
|
||||
};
|
||||
|
||||
export default function AutoLikeRecord() {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
const [recordsLoading, setRecordsLoading] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [total, setTotal] = useState(0);
|
||||
const pageSize = 10;
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setRecordsLoading(true);
|
||||
fetchLikeRecords(id, 1, pageSize)
|
||||
.then((response: any) => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(1);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("获取点赞记录失败,请稍后重试");
|
||||
})
|
||||
.finally(() => setRecordsLoading(false));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id]);
|
||||
|
||||
const handleSearch = () => {
|
||||
setCurrentPage(1);
|
||||
fetchLikeRecords(id!, 1, pageSize, searchTerm)
|
||||
.then((response: any) => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(1);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("获取点赞记录失败,请稍后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchLikeRecords(id!, currentPage, pageSize, searchTerm)
|
||||
.then((response: any) => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("获取点赞记录失败,请稍后重试");
|
||||
});
|
||||
};
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
fetchLikeRecords(id!, newPage, pageSize, searchTerm)
|
||||
.then((response: any) => {
|
||||
setRecords(response.list || []);
|
||||
setTotal(response.total || 0);
|
||||
setCurrentPage(newPage);
|
||||
})
|
||||
.catch(() => {
|
||||
message.error("获取点赞记录失败,请稍后重试");
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavCommon title="点赞记录" />
|
||||
<div className={styles.headerSearchBar}>
|
||||
<div className={styles.headerSearchInputWrap}>
|
||||
<Input
|
||||
prefix={<SearchOutlined className={styles.headerSearchIcon} />}
|
||||
placeholder="搜索好友昵称或内容"
|
||||
className={styles.headerSearchInput}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
allowClear
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
icon={<ReloadOutlined spin={recordsLoading} />}
|
||||
onClick={handleRefresh}
|
||||
loading={recordsLoading}
|
||||
type="default"
|
||||
shape="circle"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
footer={
|
||||
<>
|
||||
<div className={styles.footerPagination}>
|
||||
<Pagination
|
||||
current={currentPage}
|
||||
total={total}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
showSizeChanger={false}
|
||||
showQuickJumper
|
||||
showTotal={(total, range) =>
|
||||
`第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
}
|
||||
size="default"
|
||||
className={styles.pagination}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={styles.bgWrap}>
|
||||
<div className={styles.contentWrap}>
|
||||
{recordsLoading ? (
|
||||
<div className={styles.skeletonWrap}>
|
||||
{Array.from({ length: 3 }).map((_, index) => (
|
||||
<div key={index} className={styles.skeletonCard}>
|
||||
<div className={styles.skeletonCardHeader}>
|
||||
<Skeleton.Avatar
|
||||
active
|
||||
size={40}
|
||||
className={styles.skeletonAvatar}
|
||||
/>
|
||||
<div className={styles.skeletonNameWrap}>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeletonName}
|
||||
style={{ width: 96 }}
|
||||
/>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeletonSub}
|
||||
style={{ width: 64 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Divider className={styles.skeletonSep} />
|
||||
<div className={styles.skeletonContentWrap}>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeletonContent1}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
<Skeleton.Input
|
||||
active
|
||||
size="small"
|
||||
className={styles.skeletonContent2}
|
||||
style={{ width: "75%" }}
|
||||
/>
|
||||
<div className={styles.skeletonImgWrap}>
|
||||
<Skeleton.Image
|
||||
active
|
||||
className={styles.skeletonImg}
|
||||
style={{ width: 80, height: 80 }}
|
||||
/>
|
||||
<Skeleton.Image
|
||||
active
|
||||
className={styles.skeletonImg}
|
||||
style={{ width: 80, height: 80 }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : records.length === 0 ? (
|
||||
<div className={styles.emptyWrap}>
|
||||
<LikeOutlined className={styles.emptyIcon} />
|
||||
<p className={styles.emptyText}>暂无点赞记录</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{records.map((record) => (
|
||||
<div key={record.id} className={styles.recordCard}>
|
||||
<div className={styles.recordCardHeader}>
|
||||
<div className={styles.recordCardHeaderLeft}>
|
||||
<Avatar
|
||||
src={record.friendAvatar || undefined}
|
||||
icon={<UserOutlined />}
|
||||
size={40}
|
||||
className={styles.avatarImg}
|
||||
/>
|
||||
<div className={styles.friendInfo}>
|
||||
<div
|
||||
className={styles.friendName}
|
||||
title={record.friendName}
|
||||
>
|
||||
{record.friendName}
|
||||
</div>
|
||||
<div className={styles.friendSub}>内容发布者</div>
|
||||
</div>
|
||||
</div>
|
||||
<Badge
|
||||
className={styles.timeBadge}
|
||||
count={formatDate(record.momentTime || record.likeTime)}
|
||||
style={{
|
||||
background: "#e8f0fe",
|
||||
color: "#333",
|
||||
fontWeight: 400,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Divider className={styles.cardSep} />
|
||||
<div className={styles.cardContent}>
|
||||
{record.content && (
|
||||
<p className={styles.contentText}>{record.content}</p>
|
||||
)}
|
||||
{Array.isArray(record.resUrls) &&
|
||||
record.resUrls.length > 0 && (
|
||||
<div
|
||||
className={
|
||||
`${styles.imgGrid} ` +
|
||||
(record.resUrls.length === 1
|
||||
? styles.grid1
|
||||
: record.resUrls.length === 2
|
||||
? styles.grid2
|
||||
: record.resUrls.length <= 3
|
||||
? styles.grid3
|
||||
: record.resUrls.length <= 6
|
||||
? styles.grid6
|
||||
: styles.grid9)
|
||||
}
|
||||
>
|
||||
{record.resUrls
|
||||
.slice(0, 9)
|
||||
.map((image: string, idx: number) => (
|
||||
<div key={idx} className={styles.imgItem}>
|
||||
<img
|
||||
src={image}
|
||||
alt={`内容图片 ${idx + 1}`}
|
||||
className={styles.img}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.operatorWrap}>
|
||||
<Avatar
|
||||
src={record.operatorAvatar || undefined}
|
||||
icon={<UserOutlined />}
|
||||
size={32}
|
||||
className={styles.operatorAvatar}
|
||||
/>
|
||||
<div className={styles.operatorInfo}>
|
||||
<span
|
||||
className={styles.operatorName}
|
||||
title={record.operatorName}
|
||||
>
|
||||
{record.operatorName}
|
||||
</span>
|
||||
<span className={styles.operatorAction}>
|
||||
<LikeOutlined
|
||||
style={{ color: "red", marginRight: 4 }}
|
||||
/>
|
||||
已赞
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user