Merge branch 'yongpxu-dev' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongpxu-dev
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,5 +1,6 @@
|
||||
# 基础环境变量示例
|
||||
# VITE_API_BASE_URL=http://www.yishi.com
|
||||
VITE_API_BASE_URL=https://ckbapi.quwanzhi.com
|
||||
|
||||
VITE_APP_TITLE=Nkebao Base
|
||||
|
||||
|
||||
@@ -1,116 +1,138 @@
|
||||
.form-page {
|
||||
background: #f7f8fa;
|
||||
padding: 16px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 40px 0;
|
||||
.form-main {
|
||||
max-width: 420px;
|
||||
margin: 0 auto;
|
||||
padding: 16px 0 0 0;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
.form-section {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.form-card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border: none;
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.06);
|
||||
padding: 24px 18px 18px 18px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 16px;
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 16px;
|
||||
color: #222;
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
margin-top: 28px;
|
||||
margin-bottom: 12px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.section-block {
|
||||
padding: 12px 0 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
&:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||
.tabs-bar {
|
||||
.adm-tabs-header {
|
||||
background: #f7f8fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.adm-tabs-tab {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.time-settings {
|
||||
.collapse {
|
||||
margin-top: 12px;
|
||||
.adm-collapse-panel-content {
|
||||
padding-bottom: 8px;
|
||||
background: #f8fafc;
|
||||
border-radius: 10px;
|
||||
padding: 18px 14px 10px 14px;
|
||||
margin-top: 2px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
|
||||
}
|
||||
.form-section {
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
.form-label {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
color: #333;
|
||||
}
|
||||
.adm-input {
|
||||
min-height: 42px;
|
||||
font-size: 15px;
|
||||
border-radius: 7px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.ai-row, .section-block {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.time-picker {
|
||||
width: 100%;
|
||||
border-radius: 6px;
|
||||
|
||||
&:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
.ai-desc {
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
.date-row, .section-block {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
margin-top: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
flex: 1;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
&:hover {
|
||||
border-color: #1677ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
.adm-input {
|
||||
min-height: 44px;
|
||||
font-size: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
flex: 1;
|
||||
border-radius: 6px;
|
||||
margin-top: 32px;
|
||||
height: 48px !important;
|
||||
border-radius: 10px !important;
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
// 覆盖 antd-mobile 的默认样式
|
||||
:global {
|
||||
.adm-form-item {
|
||||
margin-bottom: 16px;
|
||||
@media (max-width: 600px) {
|
||||
.form-main {
|
||||
max-width: 100vw;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.adm-form-item-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
.form-card {
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
padding: 16px 6px 12px 6px;
|
||||
}
|
||||
|
||||
.adm-input {
|
||||
border-radius: 6px;
|
||||
border: 1px solid #d9d9d9;
|
||||
|
||||
&:focus {
|
||||
border-color: #1677ff;
|
||||
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
margin-top: 22px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.adm-switch {
|
||||
--checked-color: #1677ff;
|
||||
.submit-btn {
|
||||
height: 44px !important;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,331 +1,308 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import {
|
||||
Button,
|
||||
Toast,
|
||||
SpinLoading,
|
||||
Form,
|
||||
Input,
|
||||
Switch,
|
||||
Card,
|
||||
Space,
|
||||
} from "antd-mobile";
|
||||
import { Input as AntdInput, Select, TimePicker } from "antd";
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
SaveOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import Layout from "@/components/Layout/Layout";
|
||||
import NavCommon from "@/components/NavCommon";
|
||||
import {
|
||||
getContentLibraryDetail,
|
||||
createContentLibrary,
|
||||
updateContentLibrary,
|
||||
} from "./api";
|
||||
import { ContentLibrary, CreateContentLibraryParams } from "./data";
|
||||
import style from "./index.module.scss";
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = AntdInput;
|
||||
|
||||
const ContentLibraryForm: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [library, setLibrary] = useState<ContentLibrary | null>(null);
|
||||
const [sourceType, setSourceType] = useState<number>(1);
|
||||
|
||||
const isEdit = !!id;
|
||||
|
||||
// 获取内容库详情
|
||||
useEffect(() => {
|
||||
if (isEdit && id) {
|
||||
fetchLibraryDetail();
|
||||
}
|
||||
}, [isEdit, id]);
|
||||
|
||||
const fetchLibraryDetail = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getContentLibraryDetail(id!);
|
||||
if (response.code === 200 && response.data) {
|
||||
setLibrary(response.data);
|
||||
setSourceType(response.data.sourceType);
|
||||
|
||||
// 填充表单数据
|
||||
form.setFieldsValue({
|
||||
name: response.data.name,
|
||||
sourceType: response.data.sourceType,
|
||||
keywordInclude: response.data.keywordInclude?.join(",") || "",
|
||||
keywordExclude: response.data.keywordExclude?.join(",") || "",
|
||||
aiPrompt: response.data.aiPrompt || "",
|
||||
timeEnabled: response.data.timeEnabled === 1,
|
||||
timeStart: response.data.timeStart || "09:00",
|
||||
timeEnd: response.data.timeEnd || "18:00",
|
||||
});
|
||||
} else {
|
||||
Toast.show({
|
||||
content: response.msg || "获取内容库详情失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("获取内容库详情失败:", error);
|
||||
Toast.show({
|
||||
content: error?.message || "请检查网络连接",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
setSaving(true);
|
||||
try {
|
||||
const params: CreateContentLibraryParams = {
|
||||
name: values.name,
|
||||
sourceType: values.sourceType,
|
||||
keywordInclude: values.keywordInclude
|
||||
? values.keywordInclude
|
||||
.split(",")
|
||||
.map((k: string) => k.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
keywordExclude: values.keywordExclude
|
||||
? values.keywordExclude
|
||||
.split(",")
|
||||
.map((k: string) => k.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
aiPrompt: values.aiPrompt || "",
|
||||
timeEnabled: values.timeEnabled ? 1 : 0,
|
||||
timeStart: values.timeStart || "09:00",
|
||||
timeEnd: values.timeEnd || "18:00",
|
||||
};
|
||||
|
||||
let response;
|
||||
if (isEdit) {
|
||||
response = await updateContentLibrary({
|
||||
id: id!,
|
||||
...params,
|
||||
});
|
||||
} else {
|
||||
response = await createContentLibrary(params);
|
||||
}
|
||||
|
||||
if (response.code === 200) {
|
||||
Toast.show({
|
||||
content: isEdit ? "更新成功" : "创建成功",
|
||||
position: "top",
|
||||
});
|
||||
navigate("/content");
|
||||
} else {
|
||||
Toast.show({
|
||||
content: response.msg || (isEdit ? "更新失败" : "创建失败"),
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("保存内容库失败:", error);
|
||||
Toast.show({
|
||||
content: error?.message || "请检查网络连接",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
navigate("/content");
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Layout
|
||||
header={<NavCommon title={isEdit ? "编辑内容库" : "新建内容库"} />}
|
||||
>
|
||||
<div className={style["loading"]}>
|
||||
<SpinLoading color="primary" style={{ fontSize: 32 }} />
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title={isEdit ? "编辑内容库" : "新建内容库"} />}>
|
||||
<div className={style["form-page"]}>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
className={style["form"]}
|
||||
initialValues={{
|
||||
sourceType: 1,
|
||||
timeEnabled: false,
|
||||
timeStart: "09:00",
|
||||
timeEnd: "18:00",
|
||||
}}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<Card className={style["form-card"]}>
|
||||
<div className={style["card-title"]}>基本信息</div>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="内容库名称"
|
||||
rules={[{ required: true, message: "请输入内容库名称" }]}
|
||||
>
|
||||
<Input placeholder="请输入内容库名称" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="sourceType"
|
||||
label="数据来源"
|
||||
rules={[{ required: true, message: "请选择数据来源" }]}
|
||||
>
|
||||
<Select
|
||||
placeholder="请选择数据来源"
|
||||
onChange={(value) => setSourceType(value)}
|
||||
>
|
||||
<Option value={1}>
|
||||
<Space>
|
||||
<UserOutlined />
|
||||
微信好友
|
||||
</Space>
|
||||
</Option>
|
||||
<Option value={2}>
|
||||
<Space>
|
||||
<TeamOutlined />
|
||||
聊天群
|
||||
</Space>
|
||||
</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* 关键词设置 */}
|
||||
<Card className={style["form-card"]}>
|
||||
<div className={style["card-title"]}>关键词设置</div>
|
||||
|
||||
<Form.Item
|
||||
name="keywordInclude"
|
||||
label="包含关键词"
|
||||
extra="多个关键词用逗号分隔"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="请输入包含的关键词,多个用逗号分隔"
|
||||
rows={3}
|
||||
className={style["textarea"]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="keywordExclude"
|
||||
label="排除关键词"
|
||||
extra="多个关键词用逗号分隔"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="请输入排除的关键词,多个用逗号分隔"
|
||||
rows={3}
|
||||
className={style["textarea"]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* AI 设置 */}
|
||||
<Card className={style["form-card"]}>
|
||||
<div className={style["card-title"]}>AI 设置</div>
|
||||
|
||||
<Form.Item
|
||||
name="aiPrompt"
|
||||
label="AI 提示词"
|
||||
extra="用于AI处理内容的提示词"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="请输入AI提示词"
|
||||
rows={4}
|
||||
className={style["textarea"]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* 时间设置 */}
|
||||
<Card className={style["form-card"]}>
|
||||
<div className={style["card-title"]}>时间设置</div>
|
||||
|
||||
<Form.Item name="timeEnabled" label="启用时间限制">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
noStyle
|
||||
shouldUpdate={(prevValues, currentValues) =>
|
||||
prevValues.timeEnabled !== currentValues.timeEnabled
|
||||
}
|
||||
>
|
||||
{({ getFieldValue }) => {
|
||||
const timeEnabled = getFieldValue("timeEnabled");
|
||||
return timeEnabled ? (
|
||||
<div className={style["time-settings"]}>
|
||||
<Form.Item
|
||||
name="timeStart"
|
||||
label="开始时间"
|
||||
rules={[{ required: true, message: "请选择开始时间" }]}
|
||||
>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
placeholder="选择开始时间"
|
||||
className={style["time-picker"]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="timeEnd"
|
||||
label="结束时间"
|
||||
rules={[{ required: true, message: "请选择结束时间" }]}
|
||||
>
|
||||
<TimePicker
|
||||
format="HH:mm"
|
||||
placeholder="选择结束时间"
|
||||
className={style["time-picker"]}
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
) : null;
|
||||
}}
|
||||
</Form.Item>
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className={style["form-actions"]}>
|
||||
<Button
|
||||
fill="outline"
|
||||
onClick={handleBack}
|
||||
className={style["back-btn"]}
|
||||
>
|
||||
<ArrowLeftOutlined />
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
type="submit"
|
||||
loading={saving}
|
||||
className={style["submit-btn"]}
|
||||
>
|
||||
<SaveOutlined />
|
||||
{isEdit ? "更新" : "创建"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default ContentLibraryForm;
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,42 +60,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tabs-wrapper {
|
||||
margin-bottom: 16px;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
padding: 2px;
|
||||
flex:1;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(22, 119, 255, 0.1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: white;
|
||||
color: #1677ff;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.library-list {
|
||||
display: flex;
|
||||
|
||||
@@ -24,6 +24,7 @@ import NavCommon from "@/components/NavCommon";
|
||||
import { getContentLibraryList, deleteContentLibrary } from "./api";
|
||||
import { ContentLibrary } from "./data";
|
||||
import style from "./index.module.scss";
|
||||
import { Tabs } from "antd-mobile";
|
||||
|
||||
// 卡片菜单组件
|
||||
interface CardMenuProps {
|
||||
@@ -114,20 +115,9 @@ const ContentLibraryList: React.FC = () => {
|
||||
activeTab !== "all" ? (activeTab === "friends" ? 1 : 2) : undefined,
|
||||
});
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
setLibraries(response.data.list || []);
|
||||
} else {
|
||||
Toast.show({
|
||||
content: response.msg || "获取内容库列表失败",
|
||||
position: "top",
|
||||
});
|
||||
}
|
||||
setLibraries(response.list || []);
|
||||
} catch (error: any) {
|
||||
console.error("获取内容库列表失败:", error);
|
||||
Toast.show({
|
||||
content: error?.message || "请检查网络连接",
|
||||
position: "top",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -196,69 +186,52 @@ const ContentLibraryList: React.FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Layout header={<NavCommon title="内容库" />}>
|
||||
<div className={style["content-library-page"]}>
|
||||
{/* 搜索和操作栏 */}
|
||||
<div className={style["search-bar"]}>
|
||||
<div className={style["search-input-wrapper"]}>
|
||||
<SearchOutlined className={style["search-icon"]} />
|
||||
<Input
|
||||
placeholder="搜索内容库..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
className={style["search-input"]}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
className={style["refresh-btn"]}
|
||||
>
|
||||
<ReloadOutlined className={loading ? style["spinning"] : ""} />
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={handleCreateNew}
|
||||
className={style["create-btn"]}
|
||||
>
|
||||
<PlusOutlined />
|
||||
新建
|
||||
</Button>
|
||||
</div>
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<NavCommon
|
||||
title="内容库"
|
||||
right={
|
||||
<Button size="small" color="primary" onClick={handleCreateNew}>
|
||||
<PlusOutlined /> 新建内容库
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* 标签页 */}
|
||||
<div className={style["tabs-wrapper"]}>
|
||||
{/* 搜索栏 */}
|
||||
<div className="search-bar">
|
||||
<div className="search-input-wrapper">
|
||||
<Input
|
||||
placeholder="搜索内容库"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
prefix={<SearchOutlined />}
|
||||
allowClear
|
||||
size="large"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={handleRefresh}
|
||||
loading={loading}
|
||||
className="refresh-btn"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 标签页 */}
|
||||
<div className={style["tabs"]}>
|
||||
<div
|
||||
className={`${style["tab"]} ${
|
||||
activeTab === "all" ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("all")}
|
||||
>
|
||||
全部
|
||||
</div>
|
||||
<div
|
||||
className={`${style["tab"]} ${
|
||||
activeTab === "friends" ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("friends")}
|
||||
>
|
||||
微信好友
|
||||
</div>
|
||||
<div
|
||||
className={`${style["tab"]} ${
|
||||
activeTab === "groups" ? style["active"] : ""
|
||||
}`}
|
||||
onClick={() => setActiveTab("groups")}
|
||||
>
|
||||
聊天群
|
||||
</div>
|
||||
<Tabs activeKey={activeTab} onChange={setActiveTab}>
|
||||
<Tabs.Tab title="全部" key="all" />
|
||||
<Tabs.Tab title="微信好友" key="friends" />
|
||||
<Tabs.Tab title="聊天群" key="groups" />
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
}
|
||||
>
|
||||
<div className={style["content-library-page"]}>
|
||||
{/* 内容库列表 */}
|
||||
<div className={style["library-list"]}>
|
||||
{loading ? (
|
||||
|
||||
@@ -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