Merge branch 'yongpxu-dev' into yongpxu-dev4

This commit is contained in:
笔记本里的永平
2025-07-23 19:12:42 +08:00
9 changed files with 323 additions and 247 deletions

View File

@@ -1,28 +1,27 @@
import request from "./request"; import request from "./request";
/** /**
* 通用文件上传方法(支持图片、文件) * 通用文件上传方法(支持图片、文件)
* @param {File} file - 要上传的文件对象 * @param {File} file - 要上传的文件对象
* @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址 * @param {string} [uploadUrl='/v1/attachment/upload'] - 上传接口地址
* @returns {Promise<string>} - 上传成功后返回文件url * @returns {Promise<string>} - 上传成功后返回文件url
*/ */
export async function uploadFile( export async function uploadFile(
file: File, file: File,
uploadUrl: string = "/v1/attachment/upload" uploadUrl: string = "/v1/attachment/upload"
): Promise<string> { ): Promise<string> {
try { try {
// 创建 FormData 对象用于文件上传 // 创建 FormData 对象用于文件上传
const formData = new FormData(); const formData = new FormData();
formData.append("file", file); formData.append("file", file);
// 使用 request 方法上传文件,设置正确的 Content-Type // 使用 request 方法上传文件,设置正确的 Content-Type
const res = await request(uploadUrl, formData, "POST", { const res = await request(uploadUrl, formData, "POST", {
headers: { headers: {
"Content-Type": "multipart/form-data", "Content-Type": "multipart/form-data",
}, },
}); });
return res.url;
return res.url; } catch (e: any) {
} catch (e: any) { throw new Error(e?.message || "文件上传失败");
throw new Error(e?.message || "文件上传失败"); }
} }
}

View File

@@ -150,9 +150,18 @@ export default function FriendSelection({
}; };
// 获取已选好友详细信息 // 获取已选好友详细信息
const selectedFriendObjs = friends.filter((friend) => const selectedFriendObjs = [
selectedFriends.includes(friend.id) ...friends.filter((friend) => selectedFriends.includes(friend.id)),
); ...selectedFriends
.filter((id) => !friends.some((friend) => friend.id === id))
.map((id) => ({
id,
nickname: id,
wechatId: id,
avatar: "",
customer: "",
})),
];
// 删除已选好友 // 删除已选好友
const handleRemoveFriend = (id: string) => { const handleRemoveFriend = (id: string) => {

View File

@@ -598,6 +598,7 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
onChange={async (e) => { onChange={async (e) => {
const file = e.target.files?.[0]; const file = e.target.files?.[0];
if (file) { if (file) {
// 直接上传
try { try {
const url = await uploadFile(file); const url = await uploadFile(file);
const newPoster = { const newPoster = {
@@ -606,14 +607,9 @@ const BasicSettings: React.FC<BasicSettingsProps> = ({
type: "poster", type: "poster",
preview: url, preview: url,
}; };
console.log(newPoster);
setCustomPosters((prev) => [...prev, newPoster]); setCustomPosters((prev) => [...prev, newPoster]);
setSelectedMaterials([newPoster]);
onChange({ ...formData, materials: [newPoster] });
} catch (err) { } catch (err) {
// 可加toast提示 // 可加toast提示
log;
} }
e.target.value = ""; e.target.value = "";
} }

View File

@@ -1,19 +1,10 @@
"use client"; "use client";
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { import { Input, Button, Modal, Alert, Select } from "antd";
Form, import { MessageOutlined } from "@ant-design/icons";
Input,
Button,
Checkbox,
Modal,
Alert,
Select,
message,
} from "antd";
import { QuestionCircleOutlined, MessageOutlined } from "@ant-design/icons";
import DeviceSelection from "@/components/DeviceSelection"; import DeviceSelection from "@/components/DeviceSelection";
import Layout from "@/components/Layout/Layout"; import styles from "./friend.module.scss";
interface FriendRequestSettingsProps { interface FriendRequestSettingsProps {
formData: any; formData: any;
@@ -46,8 +37,8 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
}) => { }) => {
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false); const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false);
const [hasWarnings, setHasWarnings] = useState(false); const [hasWarnings, setHasWarnings] = useState(false);
const [selectedDevices, setSelectedDevices] = useState<any[]>( const [selectedDevices, setSelectedDevices] = useState<string[]>(
formData.selectedDevices || [] formData.device || []
); );
const [showRemarkTip, setShowRemarkTip] = useState(false); const [showRemarkTip, setShowRemarkTip] = useState(false);
@@ -97,171 +88,156 @@ const FriendRequestSettings: React.FC<FriendRequestSettingsProps> = ({
}; };
return ( return (
<> <div className={styles["friend-container"]}>
<Layout {/* 选择设备区块 */}
footer={ <div className={styles["friend-label"]}></div>
<div className="p-4 border-t bg-white"> <div className={styles["friend-block"]}>
<div className="flex justify-between"> <DeviceSelection
<Button onClick={onPrev}></Button> selectedDevices={selectedDevices}
<Button type="primary" onClick={handleNext}> onSelect={(deviceIds) => {
setSelectedDevices(deviceIds);
</Button> onChange({ ...formData, device: deviceIds });
</div> }}
</div> placeholder="选择设备"
} />
> </div>
<div className="p-4 space-y-6">
<div>
<span className="font-medium text-base"></span>
<div className="mt-2">
<DeviceSelection
selectedDevices={selectedDevices.map((d) => d.id)}
onSelect={(deviceIds) => {
const newSelectedDevices = deviceIds.map((id) => ({
id,
name: `设备 ${id}`,
status: "online",
}));
setSelectedDevices(newSelectedDevices);
onChange({ ...formData, device: deviceIds });
}}
placeholder="选择设备"
/>
</div>
</div>
<div className="mb-4"> {/* 好友备注区块 */}
<div className="flex items-center space-x-2 mb-1 relative"> <div className={styles["friend-label"]}></div>
<span className="font-medium text-base"></span> <div className={styles["friend-block"]} style={{ position: "relative" }}>
<span <Select
className="inline-flex items-center justify-center w-5 h-5 rounded-full bg-gray-200 text-gray-500 text-xs cursor-pointer hover:bg-gray-300 transition-colors" value={formData.remarkType || "phone"}
onMouseEnter={() => setShowRemarkTip(true)} onChange={(value) => onChange({ ...formData, remarkType: value })}
onMouseLeave={() => setShowRemarkTip(false)} style={{ width: "100%" }}
onClick={() => setShowRemarkTip((v) => !v)} >
> {remarkTypes.map((type) => (
? <Select.Option key={type.value} value={type.value}>
</span> {type.label}
{showRemarkTip && ( </Select.Option>
<div className="absolute left-24 top-0 z-20 w-64 p-3 bg-white border border-gray-200 rounded shadow-lg text-sm text-gray-700"> ))}
<div></div> </Select>
<div className="mt-2 text-xs text-gray-500"> <span
className={styles["friend-remark-q"]}
</div> onMouseEnter={() => setShowRemarkTip(true)}
<div className="mt-1 text-blue-600"> onMouseLeave={() => setShowRemarkTip(false)}
{formData.remarkType === "phone" && >
`138****1234+${getScenarioTitle()}`} ?
{formData.remarkType === "nickname" && </span>
`小红书用户2851+${getScenarioTitle()}`} {showRemarkTip && (
{formData.remarkType === "source" && <div className={styles["friend-remark-tip"]}>
`抖音直播+${getScenarioTitle()}`} <div></div>
</div> <div style={{ marginTop: 8, color: "#888", fontSize: 12 }}>
</div>
)}
</div> </div>
<Select <div style={{ marginTop: 4, color: "#1677ff" }}>
value={formData.remarkType || "phone"} {formData.remarkType === "phone" &&
onChange={(value) => onChange({ ...formData, remarkType: value })} `138****1234+${getScenarioTitle()}`}
className="w-full mt-2" {formData.remarkType === "nickname" &&
`小红书用户2851+${getScenarioTitle()}`}
{formData.remarkType === "source" &&
`抖音直播+${getScenarioTitle()}`}
</div>
</div>
)}
</div>
{/* 招呼语区块 */}
<div className={styles["friend-label"]}></div>
<div className={styles["friend-block"]}>
<Input
value={formData.greeting}
onChange={(e) => onChange({ ...formData, greeting: e.target.value })}
placeholder="请输入招呼语"
suffix={
<Button
type="link"
onClick={() => setIsTemplateDialogOpen(true)}
style={{ padding: 0 }}
> >
{remarkTypes.map((type) => ( <MessageOutlined />
<Select.Option key={type.value} value={type.value}> </Button>
{type.label} }
</Select.Option> />
))} </div>
</Select>
</div>
<div> {/* 添加间隔区块 */}
<div className="flex items-center justify-between"> <div className={styles["friend-label"]}></div>
<span className="font-medium text-base"></span> <div
<Button className={styles["friend-interval-row"] + " " + styles["friend-block"]}
onClick={() => setIsTemplateDialogOpen(true)} >
className="text-blue-500" <Input
> type="number"
<MessageOutlined className="h-4 w-4 mr-2" /> value={formData.addFriendInterval || 1}
onChange={(e) =>
</Button> onChange({
</div> ...formData,
<Input addFriendInterval: Number(e.target.value),
value={formData.greeting} })
onChange={(e) => }
onChange({ ...formData, greeting: e.target.value }) style={{ width: 100 }}
} />
placeholder="请输入招呼语" <span></span>
className="mt-2" </div>
/>
</div>
<div> {/* 允许加人时间段区块 */}
<span className="font-medium text-base"></span> <div className={styles["friend-label"]}></div>
<div className="flex items-center space-x-2 mt-2"> <div className={styles["friend-time-row"] + " " + styles["friend-block"]}>
<Input <Input
type="number" type="time"
value={formData.addFriendInterval || 1} value={formData.addFriendTimeStart || "09:00"}
onChange={(e) => onChange={(e) =>
onChange({ onChange({ ...formData, addFriendTimeStart: e.target.value })
...formData, }
addFriendInterval: Number(e.target.value), style={{ width: 120 }}
}) />
} <span></span>
/> <Input
<div className="w-10"></div> type="time"
</div> value={formData.addFriendTimeEnd || "18:00"}
</div> onChange={(e) =>
onChange({ ...formData, addFriendTimeEnd: e.target.value })
}
style={{ width: 120 }}
/>
</div>
<div> {hasWarnings && (
<span className="font-medium text-base"></span> <Alert
<div className="flex items-center space-x-2 mt-2"> message="警告"
<Input description="您有未完成的设置项,建议完善后再进入下一步。"
type="time" type="warning"
value={formData.addFriendTimeStart || "09:00"} showIcon
onChange={(e) => style={{ marginBottom: 16 }}
onChange({ ...formData, addFriendTimeStart: e.target.value }) />
} )}
className="w-32"
/>
<span></span>
<Input
type="time"
value={formData.addFriendTimeEnd || "18:00"}
onChange={(e) =>
onChange({ ...formData, addFriendTimeEnd: e.target.value })
}
className="w-32"
/>
</div>
</div>
{hasWarnings && ( {/* 底部按钮 */}
<Alert <div className={styles["friend-footer"]}>
message="警告" <Button onClick={onPrev}></Button>
description="您有未完成的设置项,建议完善后再进入下一步。" <Button type="primary" onClick={handleNext}>
type="warning"
showIcon </Button>
className="bg-amber-50 border-amber-200" </div>
/>
)}
</div>
</Layout>
{/* 招呼语模板弹窗 */}
<Modal <Modal
open={isTemplateDialogOpen} open={isTemplateDialogOpen}
onCancel={() => setIsTemplateDialogOpen(false)} onCancel={() => setIsTemplateDialogOpen(false)}
footer={null} footer={null}
> >
<div className="space-y-2"> <div>
{greetingTemplates.map((template, index) => ( {greetingTemplates.map((template, index) => (
<Button <Button
key={index} key={index}
onClick={() => handleTemplateSelect(template)} onClick={() => handleTemplateSelect(template)}
style={{ width: "100%", marginBottom: 8 }} className={styles["friend-modal-btn"]}
> >
{template} {template}
</Button> </Button>
))} ))}
</div> </div>
</Modal> </Modal>
</> </div>
); );
}; };

View File

@@ -1,5 +1,5 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Form, Input, Button, Tabs, Modal, Alert, Upload, message } from "antd"; import { Input, Button, Tabs, Modal, Alert, message } from "antd";
import { import {
PlusOutlined, PlusOutlined,
CloseOutlined, CloseOutlined,
@@ -13,7 +13,7 @@ import {
LinkOutlined, LinkOutlined,
TeamOutlined, TeamOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import Layout from "@/components/Layout/Layout"; import styles from "./messages.module.scss";
interface MessageContent { interface MessageContent {
id: string; id: string;
@@ -194,9 +194,9 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
key: plan.day.toString(), key: plan.day.toString(),
label: plan.day === 0 ? "即时消息" : `${plan.day}`, label: plan.day === 0 ? "即时消息" : `${plan.day}`,
children: ( children: (
<div className="space-y-4"> <div className={styles["messages-day-panel"]}>
{plan.messages.map((message, messageIndex) => ( {plan.messages.map((message, messageIndex) => (
<div key={message.id} className="space-y-4 p-4 bg-gray-50 rounded-lg"> <div key={message.id} className={styles["messages-message-card"]}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
{plan.day === 0 ? ( {plan.day === 0 ? (
@@ -524,7 +524,10 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
</div> </div>
))} ))}
<Button onClick={() => handleAddMessage(dayIndex)} className="w-full"> <Button
onClick={() => handleAddMessage(dayIndex)}
className={styles["messages-add-message-btn"]}
>
<PlusOutlined className="w-4 h-4 mr-2" /> <PlusOutlined className="w-4 h-4 mr-2" />
</Button> </Button>
@@ -533,31 +536,24 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
})); }));
return ( return (
<> <div className={styles["messages-container"]}>
<Layout <div className={styles["messages-header"]}>
footer={ <h2 className={styles["messages-title"]}></h2>
<div className="p-4 border-t bg-white"> <Button onClick={() => setIsAddDayPlanOpen(true)}>
<div className="flex justify-between"> <PlusOutlined />
<Button onClick={onPrev}></Button> </Button>
<Button type="primary" onClick={onNext}> </div>
<Tabs
</Button> defaultActiveKey="0"
</div> items={items}
</div> className={styles["messages-tab"]}
} />
> <div className={styles["messages-footer"]}>
<div className="p-4 space-y-6"> <Button onClick={onPrev}></Button>
<div className="flex items-center justify-between"> <Button type="primary" onClick={onNext}>
<h2 className="text-lg font-semibold"></h2>
<Button onClick={() => setIsAddDayPlanOpen(true)}> </Button>
<PlusOutlined className="h-4 w-4" /> </div>
</Button>
</div>
<Tabs defaultActiveKey="0" items={items} />
</div>
</Layout>
{/* 添加天数计划弹窗 */} {/* 添加天数计划弹窗 */}
<Modal <Modal
title="添加消息计划" title="添加消息计划"
@@ -569,11 +565,13 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
}} }}
> >
<p className="text-sm text-gray-500 mb-4"></p> <p className="text-sm text-gray-500 mb-4"></p>
<Button onClick={handleAddDayPlan} className="w-full"> <Button
onClick={handleAddDayPlan}
className={styles["messages-modal-btn"]}
>
{dayPlans.length} {dayPlans.length}
</Button> </Button>
</Modal> </Modal>
{/* 选择群聊弹窗 */} {/* 选择群聊弹窗 */}
<Modal <Modal
title="选择群聊" title="选择群聊"
@@ -584,15 +582,14 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
setIsGroupSelectOpen(false); setIsGroupSelectOpen(false);
}} }}
> >
<div className="space-y-2"> <div>
{mockGroups.map((group) => ( {mockGroups.map((group) => (
<div <div
key={group.id} key={group.id}
className={`p-4 rounded-lg cursor-pointer hover:bg-gray-100 ${ className={
selectedGroupId === group.id styles["messages-group-select-item"] +
? "bg-blue-50 border border-blue-200" (selectedGroupId === group.id ? " " + styles.selected : "")
: "" }
}`}
onClick={() => handleSelectGroup(group.id)} onClick={() => handleSelectGroup(group.id)}
> >
<div className="font-medium">{group.name}</div> <div className="font-medium">{group.name}</div>
@@ -603,7 +600,7 @@ const MessageSettings: React.FC<MessageSettingsProps> = ({
))} ))}
</div> </div>
</Modal> </Modal>
</> </div>
); );
}; };

View File

@@ -0,0 +1,48 @@
.friend-container {
padding: 12px;
}
.friend-label {
margin-bottom: 12px;
font-weight: 500;
}
.friend-block {
margin-bottom: 16px;
}
.friend-remark-tip {
position: absolute;
right: 0;
top: 36px;
z-index: 10;
background: #fff;
border: 1px solid #eee;
border-radius: 6px;
padding: 12px;
width: 220px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.friend-remark-q {
position: absolute;
right: 8px;
top: 8px;
cursor: pointer;
color: #888;
}
.friend-interval-row {
display: flex;
align-items: center;
gap: 8px;
}
.friend-time-row {
display: flex;
align-items: center;
gap: 8px;
}
.friend-footer {
display: flex;
justify-content: space-between;
margin-top: 32px;
}
.friend-modal-btn {
width: 100%;
margin-bottom: 8px;
}

View File

@@ -0,0 +1,60 @@
.messages-container {
padding: 16px;
}
.messages-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.messages-title {
font-size: 18px;
font-weight: 600;
}
.messages-tab {
margin-bottom: 16px;
}
.messages-day-panel {
background: #fafbfc;
border-radius: 10px;
padding: 16px;
margin-bottom: 16px;
}
.messages-message-card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.03);
padding: 16px;
margin-bottom: 16px;
}
.messages-message-type-btns {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.messages-add-message-btn {
width: 100%;
margin-top: 8px;
}
.messages-footer {
display: flex;
justify-content: space-between;
margin-top: 32px;
}
.messages-modal-btn {
width: 100%;
margin-bottom: 8px;
}
.messages-group-select-item {
padding: 16px;
border-radius: 8px;
cursor: pointer;
background: #fff;
margin-bottom: 8px;
border: 1px solid #eee;
transition: border 0.2s, background 0.2s;
}
.messages-group-select-item.selected {
background: #e6f7ff;
border: 1.5px solid #1677ff;
}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect, useRef } from "react"; import React, { useState, useEffect, useRef } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { NavBar, Button, Toast, SpinLoading, Dialog, Card } from "antd-mobile"; import { Button, Toast, SpinLoading, Dialog, Card } from "antd-mobile";
import NavCommon from "@/components/NavCommon";
import { Input } from "antd"; import { Input } from "antd";
import { import {
PlusOutlined, PlusOutlined,
@@ -230,25 +231,15 @@ const AutoLike: React.FC = () => {
<Layout <Layout
header={ header={
<> <>
<NavBar <NavCommon
back={null} title="自动点赞"
style={{ background: "#fff" }} backFn={() => navigate("/workspace")}
left={
<div className="nav-title">
<ArrowLeftOutlined
twoToneColor="#1677ff"
onClick={() => navigate(-1)}
/>
</div>
}
right={ right={
<Button size="small" color="primary" onClick={handleCreateNew}> <Button size="small" color="primary" onClick={handleCreateNew}>
<PlusOutlined /> <PlusOutlined />
</Button> </Button>
} }
> />
<span className="nav-title"></span>
</NavBar>
{/* 搜索栏 */} {/* 搜索栏 */}
<div className="search-bar"> <div className="search-bar">

View File

@@ -40,7 +40,7 @@ const workspaceRoutes = [
auth: true, auth: true,
}, },
{ {
path: "/workspace/auto-like/:id/edit", path: "/workspace/auto-like/edit/:id",
element: <NewAutoLike />, element: <NewAutoLike />,
auth: true, auth: true,
}, },