From 1d5441b84a2a43347af5ed09bf91ea48c857a60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=B6=85=E7=BA=A7=E8=80=81=E7=99=BD=E5=85=94?= Date: Mon, 11 Aug 2025 11:11:13 +0800 Subject: [PATCH] =?UTF-8?q?FEAT=20=3D>=20=E6=9C=AC=E6=AC=A1=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=A1=B9=E7=9B=AE=E4=B8=BA=EF=BC=9A=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E6=B6=88=E8=B4=B9=E8=AE=B0=E5=BD=95=E7=9B=B8=E5=85=B3=E7=9A=84?= =?UTF-8?q?=20API=E3=80=81=E6=95=B0=E6=8D=AE=E5=AE=9A=E4=B9=89=E3=80=81?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=92=8C=E7=BB=84=E4=BB=B6=EF=BC=8C=E4=BB=A5?= =?UTF-8?q?=E7=AE=80=E5=8C=96=E4=BB=A3=E7=A0=81=E7=BB=93=E6=9E=84=E5=B9=B6?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=B8=8D=E5=86=8D=E4=BD=BF=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mobile/mine/consumption-records/api.ts | 17 - .../mobile/mine/consumption-records/data.ts | 26 - .../consumption-records/index.module.scss | 141 ---- .../mobile/mine/consumption-records/index.tsx | 212 ------ .../src/pages/mobile/mine/content/form/api.ts | 26 + .../pages/mobile/mine/content/form/data.ts | 61 ++ .../mine/content/form/index.module.scss | 140 ++++ .../pages/mobile/mine/content/form/index.tsx | 330 ++++++++++ .../src/pages/mobile/mine/content/list/api.ts | 49 ++ .../pages/mobile/mine/content/list/data.ts | 66 ++ .../mine/content/list/index.module.scss | 217 ++++++ .../pages/mobile/mine/content/list/index.tsx | 317 +++++++++ .../mobile/mine/content/materials/form/api.ts | 20 + .../mine/content/materials/form/data.ts | 93 +++ .../content/materials/form/index.module.scss | 160 +++++ .../mine/content/materials/form/index.tsx | 403 ++++++++++++ .../mobile/mine/content/materials/list/api.ts | 37 ++ .../mine/content/materials/list/data.ts | 106 +++ .../content/materials/list/index.module.scss | 615 ++++++++++++++++++ .../mine/content/materials/list/index.tsx | 413 ++++++++++++ nkebao/src/pages/mobile/mine/main/index.tsx | 2 +- nkebao/src/router/module/content.tsx | 20 +- 22 files changed, 3064 insertions(+), 407 deletions(-) delete mode 100644 nkebao/src/pages/mobile/mine/consumption-records/api.ts delete mode 100644 nkebao/src/pages/mobile/mine/consumption-records/data.ts delete mode 100644 nkebao/src/pages/mobile/mine/consumption-records/index.module.scss delete mode 100644 nkebao/src/pages/mobile/mine/consumption-records/index.tsx create mode 100644 nkebao/src/pages/mobile/mine/content/form/api.ts create mode 100644 nkebao/src/pages/mobile/mine/content/form/data.ts create mode 100644 nkebao/src/pages/mobile/mine/content/form/index.module.scss create mode 100644 nkebao/src/pages/mobile/mine/content/form/index.tsx create mode 100644 nkebao/src/pages/mobile/mine/content/list/api.ts create mode 100644 nkebao/src/pages/mobile/mine/content/list/data.ts create mode 100644 nkebao/src/pages/mobile/mine/content/list/index.module.scss create mode 100644 nkebao/src/pages/mobile/mine/content/list/index.tsx create mode 100644 nkebao/src/pages/mobile/mine/content/materials/form/api.ts create mode 100644 nkebao/src/pages/mobile/mine/content/materials/form/data.ts create mode 100644 nkebao/src/pages/mobile/mine/content/materials/form/index.module.scss create mode 100644 nkebao/src/pages/mobile/mine/content/materials/form/index.tsx create mode 100644 nkebao/src/pages/mobile/mine/content/materials/list/api.ts create mode 100644 nkebao/src/pages/mobile/mine/content/materials/list/data.ts create mode 100644 nkebao/src/pages/mobile/mine/content/materials/list/index.module.scss create mode 100644 nkebao/src/pages/mobile/mine/content/materials/list/index.tsx diff --git a/nkebao/src/pages/mobile/mine/consumption-records/api.ts b/nkebao/src/pages/mobile/mine/consumption-records/api.ts deleted file mode 100644 index 52f95337..00000000 --- a/nkebao/src/pages/mobile/mine/consumption-records/api.ts +++ /dev/null @@ -1,17 +0,0 @@ -import request from "@/api/request"; -import { ConsumptionRecordsResponse, ConsumptionRecordDetail } from "./data"; - -// 获取消费记录列表 -export function getConsumptionRecords(params: { - page: number; - limit: number; -}): Promise { - return request("/v1/consumption-records", params, "GET"); -} - -// 获取消费记录详情 -export function getConsumptionRecordDetail( - id: string, -): Promise { - return request(`/v1/consumption-records/${id}`, {}, "GET"); -} diff --git a/nkebao/src/pages/mobile/mine/consumption-records/data.ts b/nkebao/src/pages/mobile/mine/consumption-records/data.ts deleted file mode 100644 index 391b40dd..00000000 --- a/nkebao/src/pages/mobile/mine/consumption-records/data.ts +++ /dev/null @@ -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; -} diff --git a/nkebao/src/pages/mobile/mine/consumption-records/index.module.scss b/nkebao/src/pages/mobile/mine/consumption-records/index.module.scss deleted file mode 100644 index f244cccc..00000000 --- a/nkebao/src/pages/mobile/mine/consumption-records/index.module.scss +++ /dev/null @@ -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; -} diff --git a/nkebao/src/pages/mobile/mine/consumption-records/index.tsx b/nkebao/src/pages/mobile/mine/consumption-records/index.tsx deleted file mode 100644 index 64e20ac5..00000000 --- a/nkebao/src/pages/mobile/mine/consumption-records/index.tsx +++ /dev/null @@ -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([]); - 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 ; - case "ai_service": - return ; - case "version_upgrade": - return ; - default: - return ; - } - }; - - 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) => ( - -
-
-
- {getTypeIcon(record.type)} -
-
-
- {record.description} -
-
- - {formatTime(record.createTime)} -
-
-
-
-
- {formatAmount(record.amount, record.type)} -
- - {getStatusText(record.status)} - -
-
- {record.balance !== undefined && ( -
- 余额: ¥{record.balance.toFixed(2)} -
- )} -
- ); - - return ( - }> -
- {records.length === 0 && !loading ? ( - } - /> - ) : ( -
- {records.map(renderRecordItem)} - {loading && ( -
- -
加载中...
-
- )} - {!loading && hasMore && ( -
loadRecords()}> - 加载更多 -
- )} -
- )} -
-
- ); -}; - -export default ConsumptionRecords; diff --git a/nkebao/src/pages/mobile/mine/content/form/api.ts b/nkebao/src/pages/mobile/mine/content/form/api.ts new file mode 100644 index 00000000..bc136d88 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/content/form/api.ts @@ -0,0 +1,26 @@ +import request from "@/api/request"; +import { + ContentLibrary, + CreateContentLibraryParams, + UpdateContentLibraryParams, +} from "./data"; + +// 获取内容库详情 +export function getContentLibraryDetail(id: string): Promise { + return request("/v1/content/library/detail", { id }, "GET"); +} + +// 创建内容库 +export function createContentLibrary( + params: CreateContentLibraryParams, +): Promise { + return request("/v1/content/library/create", params, "POST"); +} + +// 更新内容库 +export function updateContentLibrary( + params: UpdateContentLibraryParams, +): Promise { + const { id, ...data } = params; + return request(`/v1/content/library/update`, { id, ...data }, "POST"); +} diff --git a/nkebao/src/pages/mobile/mine/content/form/data.ts b/nkebao/src/pages/mobile/mine/content/form/data.ts new file mode 100644 index 00000000..3f3f3425 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/content/form/data.ts @@ -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 { + 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 { + id: string; + status?: number; +} diff --git a/nkebao/src/pages/mobile/mine/content/form/index.module.scss b/nkebao/src/pages/mobile/mine/content/form/index.module.scss new file mode 100644 index 00000000..b2f49942 --- /dev/null +++ b/nkebao/src/pages/mobile/mine/content/form/index.module.scss @@ -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; + } +} diff --git a/nkebao/src/pages/mobile/mine/content/form/index.tsx b/nkebao/src/pages/mobile/mine/content/form/index.tsx new file mode 100644 index 00000000..d51e646d --- /dev/null +++ b/nkebao/src/pages/mobile/mine/content/form/index.tsx @@ -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([]); + const [friendsGroupsOptions, setSelectedFriendsOptions] = useState< + FriendSelectionItem[] + >([]); + const [selectedGroups, setSelectedGroups] = useState([]); + 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 ( + } + footer={ +
+ +
+ } + > +
+
e.preventDefault()} + autoComplete="off" + > +
+ + setName(e.target.value)} + className={style["input"]} + /> +
+ +
数据来源配置
+
+ setSourceType(key as "friends" | "groups")} + className={style["tabs-bar"]} + > + + + + + + + +
+ + + 关键词设置} + > +
+ +