FEAT => 本次更新项目为:删除消费记录相关的 API、数据定义、样式和组件,以简化代码结构并移除不再使用的功能。

This commit is contained in:
超级老白兔
2025-08-11 11:11:13 +08:00
parent 8c9a660afd
commit 1d5441b84a
22 changed files with 3064 additions and 407 deletions

View File

@@ -1,17 +0,0 @@
import request from "@/api/request";
import { ConsumptionRecordsResponse, ConsumptionRecordDetail } from "./data";
// 获取消费记录列表
export function getConsumptionRecords(params: {
page: number;
limit: number;
}): Promise<ConsumptionRecordsResponse> {
return request("/v1/consumption-records", params, "GET");
}
// 获取消费记录详情
export function getConsumptionRecordDetail(
id: string,
): Promise<ConsumptionRecordDetail> {
return request(`/v1/consumption-records/${id}`, {}, "GET");
}

View File

@@ -1,26 +0,0 @@
// 消费记录类型定义
export interface ConsumptionRecord {
id: string;
type: "recharge" | "ai_service" | "version_upgrade";
amount: number;
description: string;
createTime: string;
status: "success" | "pending" | "failed";
balance?: number;
}
// API响应类型
export interface ConsumptionRecordsResponse {
list: ConsumptionRecord[];
total: number;
page: number;
limit: number;
}
// 消费记录详情
export interface ConsumptionRecordDetail extends ConsumptionRecord {
orderNo?: string;
paymentMethod?: string;
remark?: string;
operator?: string;
}

View File

@@ -1,141 +0,0 @@
.records-page {
padding: 16px;
background: #f7f8fa;
min-height: 100vh;
}
.records-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.record-card {
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
background: #fff;
}
.record-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 12px;
}
.record-info {
display: flex;
align-items: flex-start;
gap: 12px;
flex: 1;
}
.type-icon-wrapper {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.type-icon {
font-size: 20px;
color: #666;
}
.record-details {
flex: 1;
min-width: 0;
}
.record-description {
font-size: 16px;
font-weight: 500;
color: #222;
margin-bottom: 4px;
line-height: 1.4;
}
.record-time {
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: #999;
}
.time-icon {
font-size: 12px;
}
.record-amount {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
flex-shrink: 0;
}
.amount-text {
font-size: 16px;
font-weight: 600;
}
.status-tag {
font-size: 11px;
padding: 2px 6px;
border-radius: 8px;
}
.balance-info {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #f0f0f0;
font-size: 12px;
color: #666;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 20px;
}
.loading-text {
font-size: 14px;
color: #666;
}
.load-more {
text-align: center;
padding: 16px;
color: var(--primary-color);
font-size: 14px;
font-weight: 500;
cursor: pointer;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
transition: background-color 0.2s ease;
&:hover {
background-color: #f8f9fa;
}
&:active {
background-color: #e9ecef;
}
}
.empty-state {
margin-top: 60px;
}
.empty-icon {
font-size: 48px;
color: #ccc;
}

View File

@@ -1,212 +0,0 @@
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { Card, List, Tag, SpinLoading, Empty } from "antd-mobile";
import { useUserStore } from "@/store/module/user";
import style from "./index.module.scss";
import {
WalletOutlined,
RobotOutlined,
CrownOutlined,
ClockCircleOutlined,
} from "@ant-design/icons";
import NavCommon from "@/components/NavCommon";
import Layout from "@/components/Layout/Layout";
import { getConsumptionRecords } from "./api";
import { ConsumptionRecord } from "./data";
const ConsumptionRecords: React.FC = () => {
const navigate = useNavigate();
const { user } = useUserStore();
const [records, setRecords] = useState<ConsumptionRecord[]>([]);
const [loading, setLoading] = useState(true);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
useEffect(() => {
loadRecords();
}, []);
const loadRecords = async (reset = false) => {
if (loading) return;
setLoading(true);
try {
const currentPage = reset ? 1 : page;
const response = await getConsumptionRecords({
page: currentPage,
limit: 20,
});
const newRecords = response.list || [];
setRecords(prev => (reset ? newRecords : [...prev, ...newRecords]));
setHasMore(newRecords.length === 20);
if (reset) setPage(1);
else setPage(currentPage + 1);
} catch (error) {
console.error("加载消费记录失败:", error);
} finally {
setLoading(false);
}
};
const getTypeIcon = (type: string) => {
switch (type) {
case "recharge":
return <WalletOutlined className={style["type-icon"]} />;
case "ai_service":
return <RobotOutlined className={style["type-icon"]} />;
case "version_upgrade":
return <CrownOutlined className={style["type-icon"]} />;
default:
return <WalletOutlined className={style["type-icon"]} />;
}
};
const getTypeColor = (type: string) => {
switch (type) {
case "recharge":
return "#52c41a";
case "ai_service":
return "#1890ff";
case "version_upgrade":
return "#722ed1";
default:
return "#666";
}
};
const getStatusText = (status: string) => {
switch (status) {
case "success":
return "成功";
case "pending":
return "处理中";
case "failed":
return "失败";
default:
return "未知";
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "success":
return "#52c41a";
case "pending":
return "#faad14";
case "failed":
return "#ff4d4f";
default:
return "#666";
}
};
const formatAmount = (amount: number, type: string) => {
if (type === "recharge") {
return `+¥${amount.toFixed(2)}`;
} else {
return `-¥${amount.toFixed(2)}`;
}
};
const formatTime = (timeStr: string) => {
const date = new Date(timeStr);
const now = new Date();
const diff = now.getTime() - date.getTime();
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (days === 0) {
return date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
});
} else if (days === 1) {
return (
"昨天 " +
date.toLocaleTimeString("zh-CN", {
hour: "2-digit",
minute: "2-digit",
})
);
} else if (days < 7) {
return `${days}天前`;
} else {
return date.toLocaleDateString("zh-CN");
}
};
const renderRecordItem = (record: ConsumptionRecord) => (
<Card key={record.id} className={style["record-card"]}>
<div className={style["record-header"]}>
<div className={style["record-info"]}>
<div
className={style["type-icon-wrapper"]}
style={{ backgroundColor: `${getTypeColor(record.type)}20` }}
>
{getTypeIcon(record.type)}
</div>
<div className={style["record-details"]}>
<div className={style["record-description"]}>
{record.description}
</div>
<div className={style["record-time"]}>
<ClockCircleOutlined className={style["time-icon"]} />
{formatTime(record.createTime)}
</div>
</div>
</div>
<div className={style["record-amount"]}>
<div
className={style["amount-text"]}
style={{
color: record.type === "recharge" ? "#52c41a" : "#ff4d4f",
}}
>
{formatAmount(record.amount, record.type)}
</div>
<Tag
color={getStatusColor(record.status)}
className={style["status-tag"]}
>
{getStatusText(record.status)}
</Tag>
</div>
</div>
{record.balance !== undefined && (
<div className={style["balance-info"]}>
: {record.balance.toFixed(2)}
</div>
)}
</Card>
);
return (
<Layout header={<NavCommon title="消费记录" />}>
<div className={style["records-page"]}>
{records.length === 0 && !loading ? (
<Empty
className={style["empty-state"]}
description="暂无消费记录"
image={<WalletOutlined className={style["empty-icon"]} />}
/>
) : (
<div className={style["records-list"]}>
{records.map(renderRecordItem)}
{loading && (
<div className={style["loading-container"]}>
<SpinLoading color="primary" />
<div className={style["loading-text"]}>...</div>
</div>
)}
{!loading && hasMore && (
<div className={style["load-more"]} onClick={() => loadRecords()}>
</div>
)}
</div>
)}
</div>
</Layout>
);
};
export default ConsumptionRecords;

View File

@@ -0,0 +1,26 @@
import request from "@/api/request";
import {
ContentLibrary,
CreateContentLibraryParams,
UpdateContentLibraryParams,
} from "./data";
// 获取内容库详情
export function getContentLibraryDetail(id: string): Promise<any> {
return request("/v1/content/library/detail", { id }, "GET");
}
// 创建内容库
export function createContentLibrary(
params: CreateContentLibraryParams,
): Promise<any> {
return request("/v1/content/library/create", params, "POST");
}
// 更新内容库
export function updateContentLibrary(
params: UpdateContentLibraryParams,
): Promise<any> {
const { id, ...data } = params;
return request(`/v1/content/library/update`, { id, ...data }, "POST");
}

View File

@@ -0,0 +1,61 @@
// 内容库表单数据类型定义
export interface ContentLibrary {
id: string;
name: string;
sourceType: number; // 1=微信好友, 2=聊天群
creatorName?: string;
updateTime: string;
status: number; // 0=未启用, 1=已启用
itemCount?: number;
createTime: string;
sourceFriends?: string[];
sourceGroups?: string[];
keywordInclude?: string[];
keywordExclude?: string[];
aiPrompt?: string;
timeEnabled?: number;
timeStart?: string;
timeEnd?: string;
selectedFriends?: any[];
selectedGroups?: any[];
selectedGroupMembers?: WechatGroupMember[];
}
// 微信群成员
export interface WechatGroupMember {
id: string;
nickname: string;
wechatId: string;
avatar: string;
gender?: "male" | "female";
role?: "owner" | "admin" | "member";
joinTime?: string;
}
// API 响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 创建内容库参数
export interface CreateContentLibraryParams {
name: string;
sourceType: number;
sourceFriends?: string[];
sourceGroups?: string[];
keywordInclude?: string[];
keywordExclude?: string[];
aiPrompt?: string;
timeEnabled?: number;
timeStart?: string;
timeEnd?: string;
}
// 更新内容库参数
export interface UpdateContentLibraryParams
extends Partial<CreateContentLibraryParams> {
id: string;
status?: number;
}

View File

@@ -0,0 +1,140 @@
.form-page {
background: #f7f8fa;
padding: 16px;
}
.form-main {
max-width: 420px;
margin: 0 auto;
padding: 16px 0 0 0;
}
.form-section {
margin-bottom: 18px;
}
.form-card {
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.06);
padding: 24px 18px 18px 18px;
background: #fff;
}
.form-label {
font-weight: 600;
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;
}
.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;
}
}
.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;
align-items: center;
gap: 12px;
}
.ai-desc {
color: #888;
font-size: 13px;
flex: 1;
}
.date-row,
.section-block {
display: flex;
gap: 12px;
align-items: center;
}
.adm-input {
min-height: 44px;
font-size: 15px;
border-radius: 8px;
}
.submit-btn {
margin-top: 32px;
height: 48px !important;
border-radius: 10px !important;
font-size: 17px;
font-weight: 600;
letter-spacing: 1px;
}
@media (max-width: 600px) {
.form-main {
max-width: 100vw;
padding: 0;
}
.form-card {
border-radius: 0;
box-shadow: none;
padding: 16px 6px 12px 6px;
}
.section-title {
font-size: 15px;
margin-top: 22px;
margin-bottom: 8px;
}
.submit-btn {
height: 44px !important;
font-size: 15px;
}
}

View File

@@ -0,0 +1,330 @@
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";
import { GroupSelectionItem } from "@/components/GroupSelection/data";
import { FriendSelectionItem } from "@/components/FriendSelection/data";
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 [friendsGroups, setSelectedFriends] = useState<string[]>([]);
const [friendsGroupsOptions, setSelectedFriendsOptions] = useState<
FriendSelectionItem[]
>([]);
const [selectedGroups, setSelectedGroups] = useState<string[]>([]);
const [selectedGroupsOptions, setSelectedGroupsOptions] = useState<
GroupSelectionItem[]
>([]);
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.selectedGroups || []);
setSelectedGroupsOptions(data.selectedGroupsOptions || []);
setSelectedFriendsOptions(data.sourceFriendsOptions || []);
setKeywordsInclude((data.keywordInclude || []).join(","));
setKeywordsExclude((data.keywordExclude || []).join(","));
setAIPrompt(data.aiPrompt || "");
setUseAI(!!data.aiPrompt);
setEnabled(data.status === 1);
// 时间范围
const start = data.timeStart || data.startTime;
const 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: friendsGroups,
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);
}
};
const handleGroupsChange = (groups: GroupSelectionItem[]) => {
setSelectedGroups(groups.map(g => g.id.toString()));
setSelectedGroupsOptions(groups);
};
const handleFriendsChange = (friends: FriendSelectionItem[]) => {
setSelectedFriends(friends.map(f => f.id.toString()));
setSelectedFriendsOptions(friends);
};
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={friendsGroupsOptions}
onSelect={handleFriendsChange}
placeholder="选择微信好友"
/>
</Tabs.Tab>
<Tabs.Tab title="选择聊天群" key="groups">
<GroupSelection
selectedOptions={selectedGroupsOptions}
onSelect={handleGroupsChange}
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

@@ -0,0 +1,49 @@
import request from "@/api/request";
import {
ContentLibrary,
CreateContentLibraryParams,
UpdateContentLibraryParams,
} from "./data";
// 获取内容库列表
export function getContentLibraryList(params: {
page?: number;
limit?: number;
keyword?: string;
sourceType?: number;
}): Promise<any> {
return request("/v1/content/library/list", params, "GET");
}
// 获取内容库详情
export function getContentLibraryDetail(id: string): Promise<any> {
return request("/v1/content/library/detail", { id }, "GET");
}
// 创建内容库
export function createContentLibrary(
params: CreateContentLibraryParams,
): Promise<any> {
return request("/v1/content/library/create", params, "POST");
}
// 更新内容库
export function updateContentLibrary(
params: UpdateContentLibraryParams,
): Promise<any> {
const { id, ...data } = params;
return request(`/v1/content/library/update`, { id, ...data }, "POST");
}
// 删除内容库
export function deleteContentLibrary(id: string): Promise<any> {
return request("/v1/content/library/delete", { id }, "DELETE");
}
// 切换内容库状态
export function toggleContentLibraryStatus(
id: string,
status: number,
): Promise<any> {
return request("/v1/content/library/update-status", { id, status }, "POST");
}

View File

@@ -0,0 +1,66 @@
// 内容库接口类型定义
export interface ContentLibrary {
id: string;
name: string;
sourceType: number; // 1=微信好友, 2=聊天群
creatorName?: string;
updateTime: string;
status: number; // 0=未启用, 1=已启用
itemCount?: number;
createTime: string;
sourceFriends?: string[];
sourceGroups?: string[];
keywordInclude?: string[];
keywordExclude?: string[];
aiPrompt?: string;
timeEnabled?: number;
timeStart?: string;
timeEnd?: string;
selectedFriends?: any[];
selectedGroups?: any[];
selectedGroupMembers?: WechatGroupMember[];
}
// 微信群成员
export interface WechatGroupMember {
id: string;
nickname: string;
wechatId: string;
avatar: string;
gender?: "male" | "female";
role?: "owner" | "admin" | "member";
joinTime?: string;
}
// API 响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
export interface LibraryListResponse {
list: ContentLibrary[];
total: number;
}
// 创建内容库参数
export interface CreateContentLibraryParams {
name: string;
sourceType: number;
sourceFriends?: string[];
sourceGroups?: string[];
keywordInclude?: string[];
keywordExclude?: string[];
aiPrompt?: string;
timeEnabled?: number;
timeStart?: string;
timeEnd?: string;
}
// 更新内容库参数
export interface UpdateContentLibraryParams
extends Partial<CreateContentLibraryParams> {
id: string;
status?: number;
}

View File

@@ -0,0 +1,217 @@
.content-library-page {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
}
.search-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
background: white;
padding: 12px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-input-wrapper {
position: relative;
flex: 1;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
z-index: 1;
}
.search-input {
padding-left: 36px;
border-radius: 20px;
border: 1px solid #e0e0e0;
&:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}
}
.create-btn {
border-radius: 20px;
padding: 0 16px;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.tabs {
flex: 1;
}
.library-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
}
.empty-text {
color: #999;
margin-bottom: 20px;
font-size: 14px;
}
.empty-btn {
border-radius: 20px;
padding: 0 20px;
}
.library-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: none;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.library-info {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.library-name {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.status-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
}
.menu-btn {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: #999;
border-radius: 4px;
&:hover {
background: #f5f5f5;
color: #666;
}
}
.menu-dropdown {
position: absolute;
right: 0;
top: 100%;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
min-width: 120px;
padding: 4px;
margin-top: 4px;
}
.menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
color: #333;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
&.danger {
color: #ff4d4f;
&:hover {
background: #fff2f0;
}
}
}
.card-content {
display: flex;
flex-direction: column;
gap: 6px;
}
.info-row {
display: flex;
align-items: center;
font-size: 13px;
}
.label {
color: #999;
min-width: 70px;
margin-right: 8px;
}
.value {
color: #333;
flex: 1;
}

View File

@@ -0,0 +1,317 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import {
Button,
Toast,
SpinLoading,
Dialog,
Card,
Avatar,
Tag,
} from "antd-mobile";
import { Input } from "antd";
import {
PlusOutlined,
SearchOutlined,
ReloadOutlined,
EyeOutlined,
EditOutlined,
DeleteOutlined,
MoreOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
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 {
onView: () => void;
onEdit: () => void;
onDelete: () => void;
onViewMaterials: () => void;
}
const CardMenu: React.FC<CardMenuProps> = ({
onView,
onEdit,
onDelete,
onViewMaterials,
}) => {
const [open, setOpen] = useState(false);
const menuRef = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpen(false);
}
}
if (open) document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [open]);
return (
<div style={{ position: "relative" }}>
<button onClick={() => setOpen(v => !v)} className={style["menu-btn"]}>
<MoreOutlined />
</button>
{open && (
<div ref={menuRef} className={style["menu-dropdown"]}>
<div
onClick={() => {
onEdit();
setOpen(false);
}}
className={style["menu-item"]}
>
<EditOutlined />
</div>
<div
onClick={() => {
onDelete();
setOpen(false);
}}
className={`${style["menu-item"]} ${style["danger"]}`}
>
<DeleteOutlined />
</div>
<div
onClick={() => {
onViewMaterials();
setOpen(false);
}}
className={style["menu-item"]}
>
<EyeOutlined />
</div>
</div>
)}
</div>
);
};
const ContentLibraryList: React.FC = () => {
const navigate = useNavigate();
const [libraries, setLibraries] = useState<ContentLibrary[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [activeTab, setActiveTab] = useState("all");
const [loading, setLoading] = useState(false);
// 获取内容库列表
const fetchLibraries = useCallback(async () => {
setLoading(true);
try {
const response = await getContentLibraryList({
page: 1,
limit: 100,
keyword: searchQuery,
sourceType:
activeTab !== "all" ? (activeTab === "friends" ? 1 : 2) : undefined,
});
setLibraries(response.list || []);
} catch (error: any) {
console.error("获取内容库列表失败:", error);
} finally {
setLoading(false);
}
}, [searchQuery, activeTab]);
useEffect(() => {
fetchLibraries();
}, [fetchLibraries]);
const handleCreateNew = () => {
navigate("/content/new");
};
const handleEdit = (id: string) => {
navigate(`/content/edit/${id}`);
};
const handleDelete = async (id: string) => {
const result = await Dialog.confirm({
content: "确定要删除这个内容库吗?",
confirmText: "删除",
cancelText: "取消",
});
if (result) {
try {
const response = await deleteContentLibrary(id);
if (response.code === 200) {
Toast.show({
content: "删除成功",
position: "top",
});
fetchLibraries();
} else {
Toast.show({
content: response.msg || "删除失败",
position: "top",
});
}
} catch (error: any) {
console.error("删除内容库失败:", error);
Toast.show({
content: error?.message || "请检查网络连接",
position: "top",
});
}
}
};
const handleViewMaterials = (id: string) => {
navigate(`/content/materials/${id}`);
};
const handleSearch = () => {
fetchLibraries();
};
const handleRefresh = () => {
fetchLibraries();
};
const filteredLibraries = libraries.filter(
library =>
library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
library.creatorName?.toLowerCase().includes(searchQuery.toLowerCase()),
);
return (
<Layout
header={
<>
<NavCommon
title="内容库"
right={
<Button size="small" color="primary" onClick={handleCreateNew}>
<PlusOutlined />
</Button>
}
/>
{/* 搜索栏 */}
<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"]}>
<Tabs activeKey={activeTab} onChange={setActiveTab}>
<Tabs.Tab title="全部" key="all" />
<Tabs.Tab title="微信好友" key="friends" />
<Tabs.Tab title="聊天群" key="groups" />
</Tabs>
</div>
</>
}
>
<div className={style["content-library-page"]}>
{/* 内容库列表 */}
<div className={style["library-list"]}>
{loading ? (
<div className={style["loading"]}>
<SpinLoading color="primary" style={{ fontSize: 32 }} />
</div>
) : filteredLibraries.length === 0 ? (
<div className={style["empty-state"]}>
<div className={style["empty-icon"]}>📚</div>
<div className={style["empty-text"]}>
</div>
<Button
color="primary"
size="small"
onClick={handleCreateNew}
className={style["empty-btn"]}
>
</Button>
</div>
) : (
filteredLibraries.map(library => (
<Card key={library.id} className={style["library-card"]}>
<div className={style["card-header"]}>
<div className={style["library-info"]}>
<h3 className={style["library-name"]}>{library.name}</h3>
<Tag
color={library.status === 1 ? "success" : "default"}
className={style["status-tag"]}
>
{library.status === 1 ? "已启用" : "未启用"}
</Tag>
</div>
<CardMenu
onView={() => navigate(`/content/${library.id}`)}
onEdit={() => handleEdit(library.id)}
onDelete={() => handleDelete(library.id)}
onViewMaterials={() => handleViewMaterials(library.id)}
/>
</div>
<div className={style["card-content"]}>
<div className={style["info-row"]}>
<span className={style["label"]}></span>
<span className={style["value"]}>
{library.sourceType === 1 ? "微信好友" : "聊天群"}
</span>
</div>
<div className={style["info-row"]}>
<span className={style["label"]}></span>
<span className={style["value"]}>
{library.creatorName || "系统"}
</span>
</div>
<div className={style["info-row"]}>
<span className={style["label"]}></span>
<span className={style["value"]}>
{library.itemCount || 0}
</span>
</div>
<div className={style["info-row"]}>
<span className={style["label"]}></span>
<span className={style["value"]}>
{new Date(library.updateTime).toLocaleString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
})}
</span>
</div>
</div>
</Card>
))
)}
</div>
</div>
</Layout>
);
};
export default ContentLibraryList;

View File

@@ -0,0 +1,20 @@
import request from "@/api/request";
// 获取素材详情
export function getContentItemDetail(id: string) {
return request("/v1/content/library/get-item-detail", { id }, "GET");
}
// 创建素材
export function createContentItem(params: any) {
return request("/v1/content/library/create-item", params, "POST");
}
// 更新素材
export function updateContentItem(params: any) {
return request(`/v1/content/library/update-item`, params, "POST");
}
// 获取内容库详情
export function getContentLibraryDetail(id: string) {
return request("/v1/content/library/detail", { id }, "GET");
}

View File

@@ -0,0 +1,93 @@
// 素材数据类型定义
export interface ContentItem {
id: number; // 修改为number类型
libraryId: number; // 修改为number类型
type?: string;
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
title: string;
content: string;
contentAi?: string;
contentData?: any;
snsId?: string | null;
msgId?: string | null;
wechatId?: string | null;
friendId?: string | null;
createMomentTime?: number;
createTime: string;
updateTime: string;
coverImage?: string;
resUrls?: string[];
urls?: any[];
location?: string | null;
lat?: string;
lng?: string;
status?: number;
isDel?: number;
delTime?: number;
wechatChatroomId?: string | null;
senderNickname?: string;
createMessageTime?: string | null;
comment?: string;
sendTime?: string; // 字符串格式的时间
sendTimes?: number;
contentTypeName?: string;
}
// 内容库类型
export interface ContentLibrary {
id: string;
name: string;
sourceType: number; // 1=微信好友, 2=聊天群
creatorName?: string;
updateTime: string;
status: number; // 0=未启用, 1=已启用
itemCount?: number;
createTime: string;
sourceFriends?: string[];
sourceGroups?: string[];
keywordInclude?: string[];
keywordExclude?: string[];
aiPrompt?: string;
timeEnabled?: number;
timeStart?: string;
timeEnd?: string;
selectedFriends?: any[];
selectedGroups?: any[];
selectedGroupMembers?: WechatGroupMember[];
}
// 微信群成员
export interface WechatGroupMember {
id: string;
nickname: string;
wechatId: string;
avatar: string;
gender?: "male" | "female";
role?: "owner" | "admin" | "member";
joinTime?: string;
}
// API 响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
// 创建素材参数
export interface CreateContentItemParams {
libraryId: string;
title: string;
content: string;
contentType: number;
resUrls?: string[];
urls?: string[];
comment?: string;
sendTime?: string;
}
// 更新素材参数
export interface UpdateContentItemParams
extends Partial<CreateContentItemParams> {
id: string;
}

View File

@@ -0,0 +1,160 @@
.form-page {
padding: 16px;
background: #f5f5f5;
min-height: 100vh;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.form {
display: flex;
flex-direction: column;
gap: 16px;
}
.form-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: none;
padding: 16px;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.form-item {
margin-bottom: 16px;
}
.form-label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 8px;
.required {
color: #ff4d4f;
margin-right: 4px;
}
}
.form-input {
width: 100%;
height: 40px;
border-radius: 6px;
border: 1px solid #d9d9d9;
padding: 0 12px;
font-size: 14px;
&:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}
}
.form-select {
width: 100%;
border-radius: 6px;
&:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}
}
.form-textarea {
width: 100%;
border-radius: 6px;
border: 1px solid #d9d9d9;
padding: 8px 12px;
font-size: 14px;
resize: vertical;
&:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}
}
.select-option {
display: flex;
align-items: center;
gap: 8px;
.anticon {
font-size: 16px;
color: #1677ff;
}
}
.form-actions {
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;
}
.back-btn {
flex: 1;
border-radius: 6px;
border: 1px solid #d9d9d9;
&:hover {
border-color: #1677ff;
color: #1677ff;
}
}
.submit-btn {
flex: 1;
border-radius: 6px;
}
// 覆盖 antd-mobile 的默认样式
:global {
.adm-form-item {
margin-bottom: 16px;
}
.adm-form-item-label {
font-size: 14px;
color: #333;
font-weight: 500;
}
.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);
}
}
.adm-select {
border-radius: 6px;
border: 1px solid #d9d9d9;
&:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}
}
}

View File

@@ -0,0 +1,403 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Button, Toast, SpinLoading, Card } from "antd-mobile";
import { Input, Select } from "antd";
import {
ArrowLeftOutlined,
SaveOutlined,
PictureOutlined,
LinkOutlined,
VideoCameraOutlined,
FileTextOutlined,
AppstoreOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import UploadComponent from "@/components/Upload/ImageUpload/ImageUpload";
import VideoUpload from "@/components/Upload/VideoUpload";
import {
getContentItemDetail,
createContentItem,
updateContentItem,
} from "./api";
import style from "./index.module.scss";
const { Option } = Select;
const { TextArea } = Input;
// 内容类型选项
const contentTypeOptions = [
{ value: 1, label: "图片", icon: <PictureOutlined /> },
{ value: 2, label: "链接", icon: <LinkOutlined /> },
{ value: 3, label: "视频", icon: <VideoCameraOutlined /> },
{ value: 4, label: "文本", icon: <FileTextOutlined /> },
{ value: 5, label: "小程序", icon: <AppstoreOutlined /> },
];
const MaterialForm: React.FC = () => {
const navigate = useNavigate();
const { id: libraryId, materialId } = useParams<{
id: string;
materialId: string;
}>();
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState(false);
// 表单状态
const [contentType, setContentType] = useState<number>(4);
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const [comment, setComment] = useState("");
const [sendTime, setSendTime] = useState("");
const [resUrls, setResUrls] = useState<string[]>([]);
// 链接相关状态
const [linkDesc, setLinkDesc] = useState("");
const [linkImage, setLinkImage] = useState("");
const [linkUrl, setLinkUrl] = useState("");
// 小程序相关状态
const [appTitle, setAppTitle] = useState("");
const [appId, setAppId] = useState("");
const isEdit = !!materialId;
// 获取素材详情
const fetchMaterialDetail = useCallback(async () => {
if (!materialId) return;
setLoading(true);
try {
const response = await getContentItemDetail(materialId);
// 填充表单数据
setTitle(response.title || "");
setContent(response.content || "");
setContentType(response.contentType || 4);
setComment(response.comment || "");
// 处理时间格式 - sendTime是字符串格式需要转换为datetime-local格式
if (response.sendTime) {
// 将 "2025-07-28 16:11:00" 转换为 "2025-07-28T16:11"
const dateTime = new Date(response.sendTime);
setSendTime(dateTime.toISOString().slice(0, 16));
} else {
setSendTime("");
}
setResUrls(response.resUrls || []);
// 设置链接相关数据
if (response.urls && response.urls.length > 0) {
const firstUrl = response.urls[0];
if (typeof firstUrl === "object" && firstUrl !== null) {
setLinkDesc(firstUrl.desc || "");
setLinkImage(firstUrl.image || "");
setLinkUrl(firstUrl.url || "");
}
}
} catch (error: unknown) {
console.error("获取素材详情失败:", error);
} finally {
setLoading(false);
}
}, [materialId]);
useEffect(() => {
if (isEdit && materialId) {
fetchMaterialDetail();
}
}, [isEdit, materialId, fetchMaterialDetail]);
const handleSubmit = async () => {
if (!libraryId) return;
if (!content.trim()) {
Toast.show({
content: "请输入素材内容",
position: "top",
});
return;
}
setSaving(true);
try {
// 构建urls数据
let finalUrls: { desc: string; image: string; url: string }[] = [];
if (contentType === 2 && linkUrl) {
finalUrls = [
{
desc: linkDesc,
image: linkImage,
url: linkUrl,
},
];
}
const params = {
libraryId,
title,
content,
contentType,
comment,
sendTime: sendTime || "",
resUrls,
urls: finalUrls,
type: contentType,
};
if (isEdit) {
await updateContentItem({
id: materialId!,
...params,
});
} else {
await createContentItem(params);
}
// 直接使用返回数据无需判断code
Toast.show({
content: isEdit ? "更新成功" : "创建成功",
position: "top",
});
navigate(`/content/materials/${libraryId}`);
} catch (error: unknown) {
console.error("保存素材失败:", error);
Toast.show({
content: error instanceof Error ? error.message : "请检查网络连接",
position: "top",
});
} finally {
setSaving(false);
}
};
const handleBack = () => {
navigate(`/content/materials/${libraryId}`);
};
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 ? "编辑素材" : "新建素材"} />}
footer={
<div className={style["form-actions"]}>
<Button
fill="outline"
onClick={handleBack}
className={style["back-btn"]}
>
<ArrowLeftOutlined />
</Button>
<Button
color="primary"
onClick={handleSubmit}
loading={saving}
className={style["submit-btn"]}
>
<SaveOutlined />
{isEdit ? " 保存修改" : " 保存素材"}
</Button>
</div>
}
>
<div className={style["form-page"]}>
<div className={style["form"]}>
{/* 基础信息 */}
<Card className={style["form-card"]}>
<div className={style["card-title"]}></div>
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<Input
type="datetime-local"
value={sendTime}
onChange={e => setSendTime(e.target.value)}
placeholder="请选择发布时间"
className={style["form-input"]}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}>
<span className={style["required"]}>*</span>
</label>
<Select
value={contentType}
onChange={value => setContentType(value)}
placeholder="请选择类型"
className={style["form-select"]}
>
{contentTypeOptions.map(option => (
<Option key={option.value} value={option.value}>
<div className={style["select-option"]}>
{option.icon}
<span>{option.label}</span>
</div>
</Option>
))}
</Select>
</div>
</Card>
{/* 内容信息 */}
<Card className={style["form-card"]}>
<div className={style["card-title"]}></div>
<div className={style["form-item"]}>
<label className={style["form-label"]}>
<span className={style["required"]}>*</span>
</label>
<TextArea
value={content}
onChange={e => setContent(e.target.value)}
placeholder="请输入内容"
rows={6}
className={style["form-textarea"]}
/>
</div>
{/* 链接类型特有字段 */}
{contentType === 2 && (
<>
<div className={style["form-item"]}>
<label className={style["form-label"]}>
<span className={style["required"]}>*</span>
</label>
<Input
value={linkDesc}
onChange={e => setLinkDesc(e.target.value)}
placeholder="请输入描述"
className={style["form-input"]}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<UploadComponent
value={linkImage ? [linkImage] : []}
onChange={urls => setLinkImage(urls[0] || "")}
count={1}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}>
<span className={style["required"]}>*</span>
</label>
<Input
value={linkUrl}
onChange={e => setLinkUrl(e.target.value)}
placeholder="请输入链接地址"
className={style["form-input"]}
/>
</div>
</>
)}
{/* 视频类型特有字段 */}
{contentType === 3 && (
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<VideoUpload
value={resUrls[0] || ""}
onChange={url => setResUrls([url])}
/>
</div>
)}
</Card>
{/* 素材上传(仅图片类型和小程序类型) */}
{[1, 5].includes(contentType) && (
<Card className={style["form-card"]}>
<div className={style["card-title"]}>
(: {contentType})
</div>
{contentType === 1 && (
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<div>
<UploadComponent
value={resUrls}
onChange={setResUrls}
count={9}
/>
</div>
<div
style={{
fontSize: "12px",
color: "#666",
marginTop: "4px",
}}
>
: {contentType}, : {resUrls.length}
</div>
</div>
)}
{contentType === 5 && (
<>
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<Input
value={appTitle}
onChange={e => setAppTitle(e.target.value)}
placeholder="请输入小程序名称"
className={style["form-input"]}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}>AppID</label>
<Input
value={appId}
onChange={e => setAppId(e.target.value)}
placeholder="请输入AppID"
className={style["form-input"]}
/>
</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<UploadComponent
value={resUrls}
onChange={setResUrls}
count={9}
/>
</div>
</>
)}
</Card>
)}
{/* 评论/备注 */}
<Card className={style["form-card"]}>
<div className={style["card-title"]}>/</div>
<div className={style["form-item"]}>
<label className={style["form-label"]}></label>
<TextArea
value={comment}
onChange={e => setComment(e.target.value)}
placeholder="请输入评论或备注"
rows={4}
className={style["form-textarea"]}
/>
</div>
</Card>
</div>
</div>
</Layout>
);
};
export default MaterialForm;

View File

@@ -0,0 +1,37 @@
import request from "@/api/request";
import {
GetContentItemListParams,
CreateContentItemParams,
UpdateContentItemParams,
} from "./data";
// 获取素材列表
export function getContentItemList(params: GetContentItemListParams) {
return request("/v1/content/library/item-list", params, "GET");
}
// 获取素材详情
export function getContentItemDetail(id: string) {
return request("/v1/content/item/detail", { id }, "GET");
}
// 创建素材
export function createContentItem(params: CreateContentItemParams) {
return request("/v1/content/item/create", params, "POST");
}
// 更新素材
export function updateContentItem(params: UpdateContentItemParams) {
const { id, ...data } = params;
return request(`/v1/content/item/update`, { id, ...data }, "POST");
}
// 删除素材
export function deleteContentItem(id: string) {
return request("/v1/content/library/delete-item", { id }, "DELETE");
}
// 获取内容库详情
export function getContentLibraryDetail(id: string) {
return request("/v1/content/library/detail", { id }, "GET");
}

View File

@@ -0,0 +1,106 @@
// 素材数据类型定义
export interface ContentItem {
id: number;
libraryId: number;
type: string;
contentType: number; // 0=未知, 1=图片, 2=链接, 3=视频, 4=文本, 5=小程序, 6=图文
title: string;
content: string;
contentAi?: string | null;
contentData?: string | null;
snsId?: string | null;
msgId?: string | null;
wechatId?: string | null;
friendId?: string | null;
createMomentTime: number;
createTime: string;
updateTime: string;
coverImage: string;
resUrls: string[];
urls: { desc: string; image: string; url: string }[];
location?: string | null;
lat: string;
lng: string;
status: number;
isDel: number;
delTime: number;
wechatChatroomId?: string | null;
senderNickname: string;
createMessageTime?: string | null;
comment: string;
sendTime: number;
sendTimes: number;
contentTypeName: string;
}
// 内容库类型
export interface ContentLibrary {
id: string;
name: string;
sourceType: number; // 1=微信好友, 2=聊天群
creatorName?: string;
updateTime: string;
status: number; // 0=未启用, 1=已启用
itemCount?: number;
createTime: string;
sourceFriends?: string[];
sourceGroups?: string[];
keywordInclude?: string[];
keywordExclude?: string[];
aiPrompt?: string;
timeEnabled?: number;
timeStart?: string;
timeEnd?: string;
selectedFriends?: any[];
selectedGroups?: any[];
selectedGroupMembers?: WechatGroupMember[];
}
// 微信群成员
export interface WechatGroupMember {
id: string;
nickname: string;
wechatId: string;
avatar: string;
gender?: "male" | "female";
role?: "owner" | "admin" | "member";
joinTime?: string;
}
// API 响应类型
export interface ApiResponse<T = any> {
code: number;
msg: string;
data: T;
}
export interface ItemListResponse {
list: ContentItem[];
total: number;
}
// 获取素材列表参数
export interface GetContentItemListParams {
libraryId: string;
page?: number;
limit?: number;
keyword?: string;
}
// 创建素材参数
export interface CreateContentItemParams {
libraryId: string;
title: string;
content: string;
contentType: number;
resUrls?: string[];
urls?: (string | { desc?: string; image?: string; url: string })[];
comment?: string;
sendTime?: string;
}
// 更新素材参数
export interface UpdateContentItemParams
extends Partial<CreateContentItemParams> {
id: string;
}

View File

@@ -0,0 +1,615 @@
.materials-page {
padding: 16px;
}
.search-bar {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
background: white;
padding: 12px;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.search-input-wrapper {
position: relative;
flex: 1;
}
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
z-index: 1;
}
.search-input {
padding-left: 36px;
border-radius: 20px;
border: 1px solid #e0e0e0;
&:focus {
border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.1);
}
}
.create-btn {
border-radius: 20px;
padding: 0 16px;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.materials-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: 40px 0;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.6;
}
.empty-text {
color: #999;
margin-bottom: 20px;
font-size: 14px;
}
.empty-btn {
border-radius: 20px;
padding: 0 20px;
}
.material-card {
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
border: none;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.avatar-section {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: #e6f7ff;
display: flex;
align-items: center;
justify-content: center;
}
.avatar-icon {
font-size: 24px;
color: #1677ff;
}
.header-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.creator-name {
font-size: 16px;
font-weight: 600;
color: #333;
line-height: 1.2;
}
.material-id {
background: #e6f7ff;
color: #1677ff;
font-size: 12px;
font-weight: 600;
border-radius: 12px;
padding: 2px 8px;
display: inline-block;
}
.material-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 12px;
line-height: 1.4;
}
.content-icon {
font-size: 16px;
color: #1677ff;
}
.type-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 10px;
}
.menu-btn {
background: none;
border: none;
padding: 4px;
cursor: pointer;
color: #999;
border-radius: 4px;
&:hover {
background: #f5f5f5;
color: #666;
}
}
.menu-dropdown {
position: absolute;
right: 0;
top: 100%;
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 1000;
min-width: 120px;
padding: 4px;
margin-top: 4px;
}
.menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
color: #333;
transition: background 0.2s;
&:hover {
background: #f5f5f5;
}
&.danger {
color: #ff4d4f;
&:hover {
background: #fff2f0;
}
}
}
.link-preview {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 16px;
border: 1px solid #e9ecef;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #e9ecef;
border-color: #1677ff;
}
}
.link-icon {
width: 40px;
height: 40px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.link-content {
flex: 1;
min-width: 0;
}
.link-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
line-height: 1.3;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.link-url {
font-size: 12px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.action-buttons {
display: flex;
margin-top: 16px;
justify-content: space-between;
}
.action-btn-group {
display: flex;
gap: 8px;
}
.action-btn {
border-radius: 6px;
font-size: 16px;
padding: 6px 12px;
border: 1px solid #d9d9d9;
background: white;
color: #333;
&:hover {
border-color: #1677ff;
color: #1677ff;
}
}
.delete-btn {
border-radius: 6px;
font-size: 16px;
padding: 6px 12px;
background: #ff4d4f;
border-color: #ff4d4f;
color: white;
&:hover {
background: #ff7875;
border-color: #ff7875;
}
}
.pagination-wrapper {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
background: white;
border-top: 1px solid #f0f0f0;
}
// 内容类型标签样式
.content-type-tag {
display: inline-flex;
align-items: center;
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: 500;
border: 1px solid currentColor;
}
// 图片类型预览样式
.material-image-preview {
margin: 12px 0;
.image-grid {
display: grid;
gap: 8px;
width: 100%;
// 1张图片宽度拉伸高度自适应
&.single {
grid-template-columns: 1fr;
img {
width: 100%;
height: auto;
object-fit: cover;
border-radius: 8px;
}
}
// 2张图片左右并列
&.double {
grid-template-columns: 1fr 1fr;
img {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 8px;
}
}
// 3张图片三张并列
&.triple {
grid-template-columns: 1fr 1fr 1fr;
img {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: 8px;
}
}
// 4张图片2x2网格布局
&.quad {
grid-template-columns: repeat(2, 1fr);
img {
width: 100%;
height: 140px;
object-fit: cover;
border-radius: 8px;
}
}
// 5张及以上网格布局
&.grid {
grid-template-columns: repeat(3, 1fr);
img {
width: 100%;
height: 100px;
object-fit: cover;
border-radius: 8px;
}
.image-more {
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 8px;
color: #666;
font-size: 12px;
font-weight: 500;
height: 100px;
}
}
}
.no-image {
display: flex;
align-items: center;
justify-content: center;
height: 80px;
background: #f5f5f5;
border-radius: 8px;
color: #999;
font-size: 14px;
}
}
// 链接类型预览样式
.material-link-preview {
margin: 12px 0;
.link-card {
display: flex;
background: #e9f8ff;
border-radius: 8px;
padding: 12px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid #cde6ff;
&:hover {
background: #cde6ff;
}
.link-image {
width: 60px;
height: 60px;
margin-right: 12px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 6px;
}
}
.link-content {
flex: 1;
min-width: 0;
.link-title {
font-weight: 500;
margin-bottom: 4px;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.link-url {
font-size: 12px;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
// 视频类型预览样式
.material-video-preview {
margin: 12px 0;
.video-thumbnail {
video {
width: 100%;
max-height: 200px;
border-radius: 8px;
}
}
.no-video {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
background: #f5f5f5;
border-radius: 8px;
color: #999;
font-size: 14px;
}
}
// 文本类型预览样式
.material-text-preview {
margin: 12px 0;
.text-content {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
line-height: 1.6;
color: #333;
font-size: 14px;
}
}
// 小程序类型预览样式
.material-miniprogram-preview {
margin: 12px 0;
.miniprogram-card {
display: flex;
background: #f8f9fa;
border-radius: 8px;
padding: 12px;
width: 100%;
img {
width: 60px;
height: 60px;
border-radius: 8px;
margin-right: 12px;
flex-shrink: 0;
object-fit: cover;
}
.miniprogram-info {
flex: 1;
min-width: 0;
.miniprogram-title {
font-weight: 500;
color: #333;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
// 图文类型预览样式
.material-article-preview {
margin: 12px 0;
.article-image {
margin-bottom: 12px;
img {
width: 100%;
height: 120px;
object-fit: cover;
border-radius: 8px;
}
}
.article-content {
.article-title {
font-weight: 500;
color: #333;
margin-bottom: 8px;
font-size: 16px;
}
.article-text {
color: #666;
line-height: 1.6;
font-size: 14px;
}
}
}
// 默认预览样式
.material-default-preview {
margin: 12px 0;
.default-content {
background: #f8f9fa;
padding: 12px;
border-radius: 8px;
color: #333;
line-height: 1.6;
}
}

View File

@@ -0,0 +1,413 @@
import React, { useState, useEffect, useCallback } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { Toast, SpinLoading, Dialog, Card } from "antd-mobile";
import { Input, Pagination, Button } from "antd";
import {
PlusOutlined,
SearchOutlined,
ReloadOutlined,
EditOutlined,
DeleteOutlined,
UserOutlined,
BarChartOutlined,
PictureOutlined,
LinkOutlined,
VideoCameraOutlined,
FileTextOutlined,
AppstoreOutlined,
} from "@ant-design/icons";
import Layout from "@/components/Layout/Layout";
import NavCommon from "@/components/NavCommon";
import { getContentItemList, deleteContentItem } from "./api";
import { ContentItem } from "./data";
import style from "./index.module.scss";
// 内容类型配置
const contentTypeConfig = {
1: { label: "图片", icon: PictureOutlined, color: "#52c41a" },
2: { label: "链接", icon: LinkOutlined, color: "#1890ff" },
3: { label: "视频", icon: VideoCameraOutlined, color: "#722ed1" },
4: { label: "文本", icon: FileTextOutlined, color: "#fa8c16" },
5: { label: "小程序", icon: AppstoreOutlined, color: "#eb2f96" },
6: { label: "图文", icon: PictureOutlined, color: "#13c2c2" },
};
const MaterialsList: React.FC = () => {
const navigate = useNavigate();
const { id } = useParams<{ id: string }>();
const [materials, setMaterials] = useState<ContentItem[]>([]);
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [total, setTotal] = useState(0);
const pageSize = 20;
// 获取素材列表
const fetchMaterials = useCallback(async () => {
if (!id) return;
setLoading(true);
try {
const response = await getContentItemList({
libraryId: id,
page: currentPage,
limit: pageSize,
keyword: searchQuery,
});
setMaterials(response.list || []);
setTotal(response.total || 0);
} catch (error: unknown) {
console.error("获取素材列表失败:", error);
Toast.show({
content: error instanceof Error ? error.message : "请检查网络连接",
position: "top",
});
} finally {
setLoading(false);
}
}, [id, currentPage, searchQuery]);
useEffect(() => {
fetchMaterials();
}, [fetchMaterials]);
const handleCreateNew = () => {
navigate(`/content/materials/new/${id}`);
};
const handleEdit = (materialId: number) => {
navigate(`/content/materials/edit/${id}/${materialId}`);
};
const handleDelete = async (materialId: number) => {
const result = await Dialog.confirm({
content: "确定要删除这个素材吗?",
confirmText: "删除",
cancelText: "取消",
});
if (result) {
try {
await deleteContentItem(materialId.toString());
Toast.show({
content: "删除成功",
position: "top",
});
fetchMaterials();
} catch (error: unknown) {
console.error("删除素材失败:", error);
Toast.show({
content: error instanceof Error ? error.message : "请检查网络连接",
position: "top",
});
}
}
};
const handleView = (materialId: number) => {
// 可以跳转到素材详情页面或显示弹窗
console.log("查看素材:", materialId);
};
const handleSearch = () => {
setCurrentPage(1);
fetchMaterials();
};
const handleRefresh = () => {
fetchMaterials();
};
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
// 渲染内容类型标签
const renderContentTypeTag = (contentType: number) => {
const config =
contentTypeConfig[contentType as keyof typeof contentTypeConfig];
if (!config) return null;
const IconComponent = config.icon;
return (
<div
className={style["content-type-tag"]}
style={{ backgroundColor: config.color + "20", color: config.color }}
>
<IconComponent style={{ fontSize: 12, marginRight: 4 }} />
{config.label}
</div>
);
};
// 渲染素材内容预览
const renderContentPreview = (material: ContentItem) => {
const { contentType, content, resUrls, urls, coverImage } = material;
switch (contentType) {
case 1: // 图片
return (
<div className={style["material-image-preview"]}>
{resUrls && resUrls.length > 0 ? (
<div
className={`${style["image-grid"]} ${
resUrls.length === 1
? style.single
: resUrls.length === 2
? style.double
: resUrls.length === 3
? style.triple
: resUrls.length === 4
? style.quad
: style.grid
}`}
>
{resUrls.slice(0, 9).map((url, index) => (
<img key={index} src={url} alt={`图片${index + 1}`} />
))}
{resUrls.length > 9 && (
<div className={style["image-more"]}>
+{resUrls.length - 9}
</div>
)}
</div>
) : coverImage ? (
<div className={`${style["image-grid"]} ${style.single}`}>
<img src={coverImage} alt="封面图" />
</div>
) : (
<div className={style["no-image"]}></div>
)}
</div>
);
case 2: // 链接
return (
<div className={style["material-link-preview"]}>
{urls && urls.length > 0 && (
<div
className={style["link-card"]}
onClick={() => {
window.open(urls[0].url, "_blank");
}}
>
{urls[0].image && (
<div className={style["link-image"]}>
<img src={urls[0].image} alt="链接预览" />
</div>
)}
<div className={style["link-content"]}>
<div className={style["link-title"]}>
{urls[0].desc || "链接"}
</div>
<div className={style["link-url"]}>{urls[0].url}</div>
</div>
</div>
)}
</div>
);
case 3: // 视频
return (
<div className={style["material-video-preview"]}>
{resUrls && resUrls.length > 0 ? (
<div className={style["video-thumbnail"]}>
<video src={resUrls[0]} controls />
</div>
) : (
<div className={style["no-video"]}></div>
)}
</div>
);
case 4: // 文本
return (
<div className={style["material-text-preview"]}>
<div className={style["text-content"]}>
{content.length > 100
? `${content.substring(0, 100)}...`
: content}
</div>
</div>
);
case 5: // 小程序
return (
<div className={style["material-miniprogram-preview"]}>
{resUrls && resUrls.length > 0 && (
<div className={style["miniprogram-card"]}>
<img src={resUrls[0]} alt="小程序封面" />
<div className={style["miniprogram-info"]}>
<div className={style["miniprogram-title"]}>
{material.title || "小程序"}
</div>
</div>
</div>
)}
</div>
);
case 6: // 图文
return (
<div className={style["material-article-preview"]}>
{coverImage && (
<div className={style["article-image"]}>
<img src={coverImage} alt="文章封面" />
</div>
)}
<div className={style["article-content"]}>
<div className={style["article-title"]}>
{material.title || "图文内容"}
</div>
<div className={style["article-text"]}>
{content.length > 80
? `${content.substring(0, 80)}...`
: content}
</div>
</div>
</div>
);
default:
return (
<div className={style["material-default-preview"]}>
<div className={style["default-content"]}>{content}</div>
</div>
);
}
};
return (
<Layout
header={
<>
<NavCommon
title="素材管理"
right={
<Button type="primary" onClick={handleCreateNew}>
<PlusOutlined />
</Button>
}
/>
{/* 搜索栏 */}
<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
onClick={handleRefresh}
loading={loading}
icon={<ReloadOutlined />}
size="large"
></Button>
</div>
</>
}
footer={
<div className={style["pagination-wrapper"]}>
<Pagination
current={currentPage}
pageSize={pageSize}
total={total}
onChange={handlePageChange}
showSizeChanger={false}
/>
</div>
}
loading={loading}
>
<div className={style["materials-page"]}>
{/* 素材列表 */}
<div className={style["materials-list"]}>
{loading ? (
<div className={style["loading"]}>
<SpinLoading color="primary" style={{ fontSize: 32 }} />
</div>
) : materials.length === 0 ? (
<div className={style["empty-state"]}>
<div className={style["empty-icon"]}>📄</div>
<div className={style["empty-text"]}>
</div>
<Button
color="primary"
onClick={handleCreateNew}
className={style["empty-btn"]}
>
</Button>
</div>
) : (
<>
{materials.map(material => (
<Card key={material.id} className={style["material-card"]}>
{/* 顶部信息 */}
<div className={style["card-header"]}>
<div className={style["avatar-section"]}>
<div className={style["avatar"]}>
<UserOutlined className={style["avatar-icon"]} />
</div>
<div className={style["header-info"]}>
<span className={style["creator-name"]}>
{material.senderNickname || "系统创建"}
</span>
<span className={style["material-id"]}>
ID: {material.id}
</span>
</div>
</div>
{renderContentTypeTag(material.contentType)}
</div>
{/* 标题 */}
{material.contentType != 4 && (
<div className={style["card-title"]}>
{material.content}
</div>
)}
{/* 内容预览 */}
{renderContentPreview(material)}
{/* 操作按钮区 */}
<div className={style["action-buttons"]}>
<div className={style["action-btn-group"]}>
<Button
onClick={() => handleEdit(material.id)}
className={style["action-btn"]}
>
<EditOutlined />
</Button>
<Button
onClick={() => handleView(material.id)}
className={style["action-btn"]}
>
<BarChartOutlined />
AI改写
</Button>
</div>
<Button
color="danger"
onClick={() => handleDelete(material.id)}
className={style["delete-btn"]}
>
<DeleteOutlined />
</Button>
</div>
</Card>
))}
</>
)}
</div>
</div>
</Layout>
);
};
export default MaterialsList;

View File

@@ -74,7 +74,7 @@ const Mine: React.FC = () => {
description: "管理营销内容和素材",
icon: <FolderOpenOutlined />,
count: stats.content,
path: "/content",
path: "/mine/content",
bgColor: "#fff7e6",
iconColor: "#fa8c16",
},

View File

@@ -1,36 +1,36 @@
import ContentLibraryList from "@/pages/mobile/content/list/index";
import ContentLibraryForm from "@/pages/mobile/content/form/index";
import MaterialsList from "@/pages/mobile/content/materials/list/index";
import MaterialForm from "@/pages/mobile/content/materials/form/index";
import ContentLibraryList from "@/pages/mobile/mine/content/list/index";
import ContentLibraryForm from "@/pages/mobile/mine/content/form/index";
import MaterialsList from "@/pages/mobile/mine/content/materials/list/index";
import MaterialForm from "@/pages/mobile/mine/content/materials/form/index";
const contentRoutes = [
{
path: "/content",
path: "/mine/content",
element: <ContentLibraryList />,
auth: true,
},
{
path: "/content/new",
path: "/mine/content/new",
element: <ContentLibraryForm />,
auth: true,
},
{
path: "/content/edit/:id",
path: "/mine/content/edit/:id",
element: <ContentLibraryForm />,
auth: true,
},
{
path: "/content/materials/:id",
path: "/mine/content/materials/:id",
element: <MaterialsList />,
auth: true,
},
{
path: "/content/materials/new/:id",
path: "/mine/content/materials/new/:id",
element: <MaterialForm />,
auth: true,
},
{
path: "/content/materials/edit/:id/:materialId",
path: "/mine/content/materials/edit/:id/:materialId",
element: <MaterialForm />,
auth: true,
},