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