Merge branch 'yongpxu-dev' of https://e.coding.net/g-xtcy5189/cunkebao/cunkebao_v3 into yongpxu-dev

This commit is contained in:
笔记本里的永平
2025-07-24 22:11:08 +08:00
7 changed files with 771 additions and 825 deletions

View File

@@ -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>

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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 ? (

View File

@@ -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>
);
}