私域操盘手 - 调整场景获客列表业务逻辑

This commit is contained in:
柳清爽
2025-05-19 16:30:19 +08:00
parent 1d0eb15b84
commit 1a9afdd58e
13 changed files with 474 additions and 508 deletions

View File

@@ -21,12 +21,7 @@ export interface SceneItem {
export interface ScenesResponse {
code: number;
msg: string;
data: {
list: SceneItem[];
total: number;
page: number;
limit: number;
};
data: SceneItem[];
}
// 前端使用的场景数据类型
@@ -80,12 +75,7 @@ export const fetchScenes = async (params: {
return {
code: 500,
msg: "获取场景列表失败",
data: {
list: [],
total: 0,
page: 1,
limit: 10
}
data: []
};
}
};

View File

@@ -168,8 +168,19 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
const response = await fetchScenes({ limit: 30 })
if (response.code === 200 && response.data?.list) {
setScenes(response.data.list)
if (response.code === 200 && Array.isArray(response.data)) {
const formattedScenes = response.data.map(scene => ({
id: scene.id.toString(),
name: scene.name,
image: scene.image,
status: scene.status,
sort: scene.sort,
createTime: scene.createTime,
updateTime: scene.updateTime,
deleteTime: scene.deleteTime
}))
setScenes(formattedScenes)
setSceneError(null) // 成功时清空错误
} else {
setSceneError(response.msg || "获取场景列表失败")
console.error("获取场景列表失败:", response.msg)
@@ -191,15 +202,27 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
onChange({ ...formData, scenario: "haibao" })
}
if (!formData.planName) {
// 只在初始化时设置默认计划名称
if (!formData.planName && !formData._initialized) {
if (formData.materials?.length > 0) {
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
onChange({ ...formData, planName: `海报${today}` })
onChange({ ...formData, planName: `海报${today}`, _initialized: true })
} else {
onChange({ ...formData, planName: "场景" })
onChange({ ...formData, planName: "场景", _initialized: true })
}
}
}, [formData, onChange])
}, []) // 移除 formData 依赖,只在组件挂载时执行一次
// 处理本地场景选择
const handleScenarioSelect = (scenarioId: string) => {
onChange({ ...formData, scenario: scenarioId })
// 如果选择了电话获客,自动更新计划名称
if (scenarioId === "phone") {
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
onChange({ ...formData, planName: `电话${today}` })
}
}
// 处理从API获取的场景选择
const handleSceneSelect = (scene: SceneItem) => {
@@ -220,17 +243,6 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
}
}
// 处理本地场景选择
const handleScenarioSelect = (scenarioId: string) => {
onChange({ ...formData, scenario: scenarioId })
// 如果选择了电话获客,自动更新计划名称
if (scenarioId === "phone") {
const today = new Date().toLocaleDateString("zh-CN").replace(/\//g, "")
onChange({ ...formData, planName: `电话${today}` })
}
}
// 根据场景名称推断本地场景类型
const getLocalScenarioType = (name: string): string => {
if (name.includes("海报")) return "haibao";
@@ -366,311 +378,318 @@ export function BasicSettings({ formData, onChange, onNext }: BasicSettingsProps
};
return (
<div className="w-full p-4">
<div className="space-y-6">
<div>
<Label className="text-base mb-4 block"></Label>
{/* 场景按钮阵列 */}
<div className="grid grid-cols-3 gap-2">
{loadingScenes ? (
// 加载中状态
Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="h-10 w-full rounded-lg bg-gray-200 animate-pulse"></div>
))
) : sceneError || scenes.length === 0 ? (
// 加载失败或无数据时显示本地场景
displayedScenarios.map((scenario) => (
<button
key={scenario.id}
className={`p-2 rounded-lg text-center transition-all ${
formData.scenario === scenario.id
? "bg-blue-100 text-blue-600 font-medium"
: "bg-gray-50 text-gray-600 hover:bg-gray-100"
}`}
onClick={() => handleScenarioSelect(scenario)}
>
{formatSceneName(scenario.name)}
</button>
))
) : (
// 从API获取的场景列表
scenes.map((scene) => (
<button
key={scene.id}
className={`p-2 rounded-lg text-center transition-all ${
formData.sceneId === scene.id
? "bg-blue-100 text-blue-600 font-medium"
: "bg-gray-50 text-gray-600 hover:bg-gray-100"
}`}
onClick={() => handleSceneSelect(scene)}
>
{formatSceneName(scene.name)}
</button>
))
)}
</div>
{/* 展开更多按钮 - 仅当显示本地场景且未展开全部时显示 */}
{(!loadingScenes && (sceneError || scenes.length === 0) && !showAllScenarios) && (
<Button variant="ghost" className="mt-2 w-full text-blue-600" onClick={() => setShowAllScenarios(true)}>
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
<div className="w-full p-4 bg-gray-50">
{/* 错误提示,只在 sceneError 存在且不为 'success' 时显示 */}
{sceneError && sceneError !== "success" && (
<div className="bg-red-100 border border-red-300 text-red-700 p-4 rounded mb-4">
<div>{sceneError}</div>
<Button onClick={() => window.location.reload()}></Button>
</div>
)}
<div className="space-y-6">
<div>
<Label className="text-base mb-4 block"></Label>
{/* 场景按钮阵列 */}
<div className="grid grid-cols-3 gap-2">
{loadingScenes ? (
// 加载中状态
Array.from({ length: 6 }).map((_, index) => (
<div key={index} className="h-10 w-full rounded-lg bg-gray-200 animate-pulse"></div>
))
) : sceneError || scenes.length === 0 ? (
// 加载失败或无数据时显示本地场景
displayedScenarios.map((scenario) => (
<button
key={scenario.id}
className={`p-2 rounded-lg text-center transition-all ${
formData.scenario === scenario.id
? "bg-blue-100 text-blue-600 font-medium"
: "bg-gray-50 text-gray-600 hover:bg-gray-100"
}`}
onClick={() => handleScenarioSelect(scenario.id)}
>
{formatSceneName(scenario.name)}
</button>
))
) : (
// 从API获取的场景列表
scenes.map((scene) => (
<button
key={scene.id}
className={`p-2 rounded-lg text-center transition-all ${
formData.sceneId === scene.id
? "bg-blue-100 text-blue-600 font-medium"
: "bg-gray-50 text-gray-600 hover:bg-gray-100"
}`}
onClick={() => handleSceneSelect(scene)}
>
{formatSceneName(scene.name)}
</button>
))
)}
</div>
{/* 展开更多按钮 - 仅当显示本地场景且未展开全部时显示 */}
{(!loadingScenes && (sceneError || scenes.length === 0) && !showAllScenarios) && (
<Button variant="ghost" className="mt-2 w-full text-blue-600" onClick={() => setShowAllScenarios(true)}>
<ChevronDown className="ml-2 h-4 w-4" />
</Button>
)}
</div>
<div>
<Label htmlFor="planName"></Label>
<Input
id="planName"
value={formData.planName}
onChange={(e) => onChange({ ...formData, planName: e.target.value })}
placeholder="请输入计划名称"
className="mt-2"
/>
</div>
<div>
<Label htmlFor="planName"></Label>
<Input
id="planName"
value={formData.planName}
onChange={(e) => onChange({ ...formData, planName: e.target.value })}
placeholder="请输入计划名称"
className="mt-2"
/>
</div>
{formData.scenario && (
<>
{scenarios.find((s) => s.id === formData.scenario)?.type === "social" && (
<div>
<Label></Label>
<div className="flex gap-2 mt-2">
<Button
variant="outline"
className="flex-1 justify-start"
onClick={() => setIsAccountDialogOpen(true)}
>
{selectedAccounts.length > 0 ? `已选择 ${selectedAccounts.length} 个账号` : "选择账号"}
</Button>
<Button variant="outline" size="icon" onClick={() => setIsQRCodeOpen(true)}>
<QrCode className="h-4 w-4" />
</Button>
</div>
{selectedAccounts.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{selectedAccounts.map((account) => (
<div key={account.id} className="flex items-center bg-gray-100 rounded-full px-3 py-1">
<img
src={account.avatar || "/placeholder.svg"}
alt={account.nickname}
className="w-4 h-4 rounded-full mr-2"
/>
<span className="text-sm">{account.nickname}</span>
<Button
variant="ghost"
size="sm"
className="ml-2 p-0"
onClick={() => handleRemoveAccount(account.id)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
{formData.scenario && (
<>
{scenarios.find((s) => s.id === formData.scenario)?.type === "social" && (
<div>
<Label></Label>
<div className="flex gap-2 mt-2">
<Button
variant="outline"
className="flex-1 justify-start"
onClick={() => setIsAccountDialogOpen(true)}
>
{selectedAccounts.length > 0 ? `已选择 ${selectedAccounts.length} 个账号` : "选择账号"}
</Button>
<Button variant="outline" size="icon" onClick={() => setIsQRCodeOpen(true)}>
<QrCode className="h-4 w-4" />
</Button>
</div>
)}
{/* 电话获客特殊设置 */}
{formData.scenario === "phone" && (
<Card className="p-4 border-blue-100 bg-blue-50/50 mt-4">
<div className="flex items-center justify-between mb-3">
<Label className="text-base font-medium text-blue-700"></Label>
<Button
variant="outline"
size="sm"
onClick={() => setIsPhoneSettingsOpen(true)}
className="flex items-center gap-1 bg-white border-blue-200 text-blue-700 hover:bg-blue-100"
>
<Settings className="h-3.5 w-3.5" />
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="flex items-center justify-between bg-white p-3 rounded-lg border border-blue-100 shadow-sm">
<div className="flex items-center">
<div
className={`w-3 h-3 rounded-full mr-2 ${phoneSettings.autoAdd ? "bg-green-500" : "bg-gray-300"}`}
></div>
<span></span>
</div>
<div
className={`px-2 py-0.5 rounded-full text-xs ${phoneSettings.autoAdd ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"}`}
>
{phoneSettings.autoAdd ? "已开启" : "已关闭"}
</div>
</div>
<div className="flex items-center justify-between bg-white p-3 rounded-lg border border-blue-100 shadow-sm">
<div className="flex items-center">
<div
className={`w-3 h-3 rounded-full mr-2 ${phoneSettings.speechToText ? "bg-green-500" : "bg-gray-300"}`}
></div>
<span></span>
</div>
<div
className={`px-2 py-0.5 rounded-full text-xs ${phoneSettings.speechToText ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"}`}
>
{phoneSettings.speechToText ? "已开启" : "已关闭"}
</div>
</div>
<div className="flex items-center justify-between bg-white p-3 rounded-lg border border-blue-100 shadow-sm">
<div className="flex items-center">
<div
className={`w-3 h-3 rounded-full mr-2 ${phoneSettings.questionExtraction ? "bg-green-500" : "bg-gray-300"}`}
></div>
<span></span>
</div>
<div
className={`px-2 py-0.5 rounded-full text-xs ${phoneSettings.questionExtraction ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"}`}
>
{phoneSettings.questionExtraction ? "已开启" : "已关闭"}
</div>
</div>
</div>
<p className="text-xs text-blue-600 mt-2">
</p>
</Card>
)}
{scenarios.find((s) => s.id === formData.scenario)?.type === "material" && (
<div>
<div className="flex items-center justify-between mb-4">
<Label></Label>
<Button variant="outline" onClick={() => setIsMaterialDialogOpen(true)}>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 海报展示区域 */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{materials.map((material) => (
<div
key={material.id}
className={`relative cursor-pointer rounded-lg overflow-hidden group ${
selectedMaterials.find((m) => m.id === material.id)
? "ring-2 ring-blue-600"
: "hover:ring-2 hover:ring-blue-600"
}`}
onClick={() => handleMaterialSelect(material)}
>
{selectedAccounts.length > 0 && (
<div className="mt-2 flex flex-wrap gap-2">
{selectedAccounts.map((account) => (
<div key={account.id} className="flex items-center bg-gray-100 rounded-full px-3 py-1">
<img
src={material.preview || "/placeholder.svg"}
alt={material.name}
className="w-full aspect-[9/16] object-cover"
src={account.avatar || "/placeholder.svg"}
alt={account.nickname}
className="w-4 h-4 rounded-full mr-2"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all">
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation()
handlePreviewImage(material.preview)
}}
>
<Maximize2 className="h-4 w-4 text-white" />
</Button>
</div>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-black/50 text-white">
<div className="text-sm truncate">{material.name}</div>
</div>
<span className="text-sm">{account.nickname}</span>
<Button
variant="ghost"
size="sm"
className="ml-2 p-0"
onClick={() => handleRemoveAccount(account.id)}
>
<X className="h-4 w-4" />
</Button>
</div>
))}
</div>
)}
</div>
)}
{selectedMaterials.length > 0 && (
<div className="mt-4">
<Label></Label>
<div className="mt-2">
<div className="relative w-full max-w-[200px]">
<img
src={selectedMaterials[0].preview || "/placeholder.svg"}
alt={selectedMaterials[0].name}
className="w-full aspect-[9/16] object-cover rounded-lg cursor-pointer"
onClick={() => handlePreviewImage(selectedMaterials[0].preview)}
/>
<Button
variant="secondary"
size="sm"
className="absolute top-2 right-2"
onClick={() => handleRemoveMaterial(selectedMaterials[0].id)}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
{/* 电话获客特殊设置 */}
{formData.scenario === "phone" && (
<Card className="p-4 border-blue-100 bg-blue-50/50 mt-4">
<div className="flex items-center justify-between mb-3">
<Label className="text-base font-medium text-blue-700"></Label>
<Button
variant="outline"
size="sm"
onClick={() => setIsPhoneSettingsOpen(true)}
className="flex items-center gap-1 bg-white border-blue-200 text-blue-700 hover:bg-blue-100"
>
<Settings className="h-3.5 w-3.5" />
</Button>
</div>
)}
{scenarios.find((s) => s.id === formData.scenario)?.id === "order" && (
<div>
<div className="flex items-center justify-between mb-4">
<Label></Label>
<div className="flex gap-2">
<Button variant="outline" onClick={handleDownloadTemplate}>
<Download className="h-4 w-4 mr-2" />
</Button>
<Button onClick={() => setIsImportDialogOpen(true)}>
<Upload className="h-4 w-4 mr-2" />
</Button>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<div className="flex items-center justify-between bg-white p-3 rounded-lg border border-blue-100 shadow-sm">
<div className="flex items-center">
<div
className={`w-3 h-3 rounded-full mr-2 ${phoneSettings.autoAdd ? "bg-green-500" : "bg-gray-300"}`}
></div>
<span></span>
</div>
<div
className={`px-2 py-0.5 rounded-full text-xs ${phoneSettings.autoAdd ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"}`}
>
{phoneSettings.autoAdd ? "已开启" : "已关闭"}
</div>
</div>
<div className="flex items-center justify-between bg-white p-3 rounded-lg border border-blue-100 shadow-sm">
<div className="flex items-center">
<div
className={`w-3 h-3 rounded-full mr-2 ${phoneSettings.speechToText ? "bg-green-500" : "bg-gray-300"}`}
></div>
<span></span>
</div>
<div
className={`px-2 py-0.5 rounded-full text-xs ${phoneSettings.speechToText ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"}`}
>
{phoneSettings.speechToText ? "已开启" : "已关闭"}
</div>
</div>
<div className="flex items-center justify-between bg-white p-3 rounded-lg border border-blue-100 shadow-sm">
<div className="flex items-center">
<div
className={`w-3 h-3 rounded-full mr-2 ${phoneSettings.questionExtraction ? "bg-green-500" : "bg-gray-300"}`}
></div>
<span></span>
</div>
<div
className={`px-2 py-0.5 rounded-full text-xs ${phoneSettings.questionExtraction ? "bg-green-100 text-green-700" : "bg-gray-100 text-gray-700"}`}
>
{phoneSettings.questionExtraction ? "已开启" : "已关闭"}
</div>
</div>
</div>
<p className="text-xs text-blue-600 mt-2">
</p>
</Card>
)}
{importedTags.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-medium mb-2"> {importedTags.length} </h4>
<div className="max-h-[300px] overflow-auto border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{importedTags.slice(0, 5).map((tag, index) => (
<TableRow key={index}>
<TableCell>{tag.phone}</TableCell>
<TableCell>{tag.source}</TableCell>
</TableRow>
))}
{importedTags.length > 5 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-gray-500">
{importedTags.length - 5}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{scenarios.find((s) => s.id === formData.scenario)?.type === "material" && (
<div>
<div className="flex items-center justify-between mb-4">
<Label></Label>
<Button variant="outline" onClick={() => setIsMaterialDialogOpen(true)}>
<Plus className="h-4 w-4" />
</Button>
</div>
{/* 海报展示区域 */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{materials.map((material) => (
<div
key={material.id}
className={`relative cursor-pointer rounded-lg overflow-hidden group ${
selectedMaterials.find((m) => m.id === material.id)
? "ring-2 ring-blue-600"
: "hover:ring-2 hover:ring-blue-600"
}`}
onClick={() => handleMaterialSelect(material)}
>
<img
src={material.preview || "/placeholder.svg"}
alt={material.name}
className="w-full aspect-[9/16] object-cover"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-all">
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={(e) => {
e.stopPropagation()
handlePreviewImage(material.preview)
}}
>
<Maximize2 className="h-4 w-4 text-white" />
</Button>
</div>
<div className="absolute bottom-0 left-0 right-0 p-2 bg-black/50 text-white">
<div className="text-sm truncate">{material.name}</div>
</div>
</div>
)}
))}
</div>
)}
</>
)}
<div className="flex items-center justify-between">
<Label htmlFor="enabled"></Label>
<Switch
id="enabled"
checked={formData.enabled}
onCheckedChange={(checked) => onChange({ ...formData, enabled: checked })}
/>
</div>
{selectedMaterials.length > 0 && (
<div className="mt-4">
<Label></Label>
<div className="mt-2">
<div className="relative w-full max-w-[200px]">
<img
src={selectedMaterials[0].preview || "/placeholder.svg"}
alt={selectedMaterials[0].name}
className="w-full aspect-[9/16] object-cover rounded-lg cursor-pointer"
onClick={() => handlePreviewImage(selectedMaterials[0].preview)}
/>
<Button
variant="secondary"
size="sm"
className="absolute top-2 right-2"
onClick={() => handleRemoveMaterial(selectedMaterials[0].id)}
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
</div>
)}
</div>
)}
<Button className="w-full h-12 text-base" onClick={onNext}>
</Button>
</div>
</div>
{scenarios.find((s) => s.id === formData.scenario)?.id === "order" && (
<div>
<div className="flex items-center justify-between mb-4">
<Label></Label>
<div className="flex gap-2">
<Button variant="outline" onClick={handleDownloadTemplate}>
<Download className="h-4 w-4 mr-2" />
</Button>
<Button onClick={() => setIsImportDialogOpen(true)}>
<Upload className="h-4 w-4 mr-2" />
</Button>
</div>
</div>
{importedTags.length > 0 && (
<div className="mt-4">
<h4 className="text-sm font-medium mb-2"> {importedTags.length} </h4>
<div className="max-h-[300px] overflow-auto border rounded-md">
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{importedTags.slice(0, 5).map((tag, index) => (
<TableRow key={index}>
<TableCell>{tag.phone}</TableCell>
<TableCell>{tag.source}</TableCell>
</TableRow>
))}
{importedTags.length > 5 && (
<TableRow>
<TableCell colSpan={4} className="text-center text-gray-500">
{importedTags.length - 5}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</div>
)}
</div>
)}
</>
)}
<div className="flex items-center justify-between">
<Label htmlFor="enabled"></Label>
<Switch
id="enabled"
checked={formData.enabled}
onCheckedChange={(checked) => onChange({ ...formData, enabled: checked })}
/>
</div>
<Button className="w-full h-12 text-base" onClick={onNext}>
</Button>
</div>
</div>
)
}

View File

@@ -140,7 +140,7 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
}
return (
<div className="w-full p-4">
<div className="w-full p-4 bg-gray-50">
<div className="space-y-6">
<div>
<Label className="text-base"></Label>

View File

@@ -192,7 +192,7 @@ export function MessageSettings({ formData, onChange, onNext, onPrev }: MessageS
}
return (
<div className="w-full p-4">
<div className="w-full p-4 bg-gray-50">
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold"></h2>

View File

@@ -107,7 +107,7 @@ export function TagSettings({ formData, onChange, onNext, onPrev }: TagSettingsP
}
return (
<div className="w-full p-4">
<div className="w-full p-4 bg-gray-50">
<div className="space-y-6">
<div>
<div className="flex items-center justify-between mb-4">

View File

@@ -121,9 +121,9 @@ export default function ScenariosPage() {
setLoading(true)
const response = await fetchScenes({ limit: 50 })
if (response.code === 200 && response.data?.list) {
if (response.code === 200 && response.data) {
// 转换场景数据为前端展示格式
const transformedScenes = response.data.list.map((scene) => {
const transformedScenes = response.data.map((scene) => {
const transformedScene = transformSceneItem(scene)
// 添加link属性用于导航
@@ -216,9 +216,6 @@ export default function ScenariosPage() {
<header className="sticky top-0 z-10 bg-white border-b">
<div className="flex justify-between items-center p-4">
<div className="flex items-center space-x-3">
<Button variant="ghost" size="icon" onClick={() => window.history.back()}>
<ChevronLeft className="h-5 w-5" />
</Button>
<h1 className="text-xl font-semibold text-blue-600"></h1>
</div>

View File

@@ -0,0 +1,26 @@
<?php
namespace app\common\model;
use think\Model;
use think\model\concern\SoftDelete;
/**
* 获客场景模型类
*/
class PlanScene extends Model
{
use SoftDelete;
const STATUS_ACTIVE = 1; // 活动状态
// 设置表名
protected $name = 'plan_scene';
// 自动写入时间戳
protected $autoWriteTimestamp = true;
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
protected $deleteTime = 'deleteTime';
protected $defaultSoftDelete = 0;
}

View File

@@ -35,9 +35,16 @@ Route::group('v1/', function () {
});
// 获客场景相关
Route::group('plan/scenes', function () {
Route::get('', 'app\cunkebao\controller\Scene@index'); // 获取场景列表
Route::post('create', 'app\cunkebao\controller\Plan@index'); // 获取场景列表
Route::group('plan', function () {
Route::get('scenes', 'app\cunkebao\controller\plan\GetPlanSceneListV1Controller@index');
// 添加计划任务
Route::post('add', 'app\cunkebao\controller\Plan@index');
// 获取计划任务列表
Route::get('list', 'app\cunkebao\controller\Plan@getList');
});
// 流量池相关
@@ -86,15 +93,6 @@ Route::group('v1/', function () {
Route::get('getMemberList', 'app\cunkebao\controller\chatroom\GetChatroomListV1Controller@getMemberList'); // 获取群详情
});
// 计划任务相关路由
Route::group('plan', function () {
// 添加计划任务
Route::post('add', 'app\cunkebao\controller\Plan@index');
// 获取计划任务列表
Route::get('list', 'app\cunkebao\controller\Plan@getList');
});
})->middleware(['jwt']);
return [];

View File

@@ -1,67 +0,0 @@
<?php
namespace app\cunkebao\controller;
use app\cunkebao\model\PlanScene;
use think\Controller;
use think\facade\Request;
/**
* 获客场景控制器
*/
class Scene extends Controller
{
/**
* 获取场景列表
*
* @return \think\response\Json
*/
public function index()
{
$page = Request::param('page', 1, 'intval');
$limit = Request::param('limit', 10, 'intval');
$keyword = Request::param('keyword', '');
// 构建查询条件
$where = [];
if (!empty($keyword)) {
$where[] = ['name', 'like', "%{$keyword}%"];
}
// 默认只显示有效场景
$where[] = ['status', '=', 1];
// 查询列表
$result = PlanScene::getSceneList($where, 'sort desc', $page, $limit);
return json([
'code' => 200,
'msg' => '获取成功',
'data' => $result
]);
}
/**
* 获取单个场景详情
*
* @param int $id 场景ID
* @return \think\response\Json
*/
public function read($id)
{
// 查询场景信息
$scene = PlanScene::getSceneInfo($id);
if (!$scene) {
return json([
'code' => 404,
'msg' => '场景不存在'
]);
}
return json([
'code' => 200,
'msg' => '获取成功',
'data' => $scene
]);
}
}

View File

@@ -1,64 +0,0 @@
<?php
namespace app\cunkebao\controller;
use app\cunkebao\model\TrafficTag as TrafficTagModel;
use think\Controller;
use think\facade\Request;
/**
* 流量标签控制器
*/
class TrafficTag extends Controller
{
/**
* 获取标签列表
*
* @return \think\response\Json
*/
public function index()
{
try {
// 获取登录用户信息
$userInfo = request()->userInfo;
// 获取查询条件
$where = [];
// 关键词搜索
$keyword = Request::param('keyword', '');
if (!empty($keyword)) {
$where[] = ['tagName', 'like', "%{$keyword}%"];
}
// 添加公司ID过滤条件
$where[] = ['companyId', '=', $userInfo['companyId']];
// 获取分页参数
$page = (int)Request::param('page', 1);
$limit = (int)Request::param('limit', 200); // 默认每页显示200条
// 获取排序参数
$sort = Request::param('sort', 'id');
$order = Request::param('order', 'desc');
// 查询列表
$list = TrafficTagModel::getTagsByCompany($where, "{$sort} {$order}", $page, $limit);
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'total' => $list->total(),
'list' => $list->items(),
'page' => $page,
'limit' => $limit
]
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '获取失败:' . $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace app\cunkebao\controller\plan;
use app\common\model\PlanScene as PlansSceneModel;
use app\cunkebao\controller\BaseController;
use library\ResponseHelper;
/**
* 获客场景控制器
*/
class GetPlanSceneListV1Controller extends BaseController
{
/**
* 获取开启的场景列表
*
* @return array
*/
protected function getSceneList(): array
{
return PlansSceneModel::where(
[
'status' => PlansSceneModel::STATUS_ACTIVE
]
)
->order('sort desc')
->select()->toArray();
}
/**
* 获取场景列表
*
* @return \think\response\Json
*/
public function index()
{
return ResponseHelper::success(
$this->getSceneList()
);
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace app\cunkebao\controller\plan;
use library\ResponseHelper;
use think\Controller;
use think\Db;
use think\facade\Request;
/**
* 获客场景控制器
*/
class PostCreateAddFriendPlanV1Controller extends Controller
{
/**
* 添加计划任务
*
* @return \think\response\Json
*/
public function index()
{
try {
// 获取表单数据
$data = [
'name' => Request::post('name', ''),
'sceneId' => Request::post('sceneId', 0),
'status' => Request::post('status', 0),
'reqConf' => Request::post('reqConf', ''),
'msgConf' => Request::post('msgConf', ''),
'tagConf' => Request::post('tagConf', ''),
'createTime' => time(),
'updateTime' => time()
];
// 验证必填字段
if (empty($data['name'])) {
return ResponseHelper::error('计划名称不能为空', 400);
}
if (empty($data['sceneId'])) {
return ResponseHelper::error('场景ID不能为空', 400);
}
// 验证数据格式
if (!$this->validateJson($data['reqConf'])) {
return ResponseHelper::error('好友申请设置格式不正确', 400);
}
if (!$this->validateJson($data['msgConf'])) {
return ResponseHelper::error('消息设置格式不正确', 400);
}
if (!$this->validateJson($data['tagConf'])) {
return ResponseHelper::error('标签设置格式不正确', 400);
}
// 插入数据库
$result = Db::name('friend_plan')->insert($data);
if ($result) {
return ResponseHelper::success([], '添加计划任务成功');
} else {
return ResponseHelper::error('添加计划任务失败', 500);
}
} catch (\Exception $e) {
return ResponseHelper::error('系统错误: ' . $e->getMessage(), 500);
}
}
}

View File

@@ -1,43 +0,0 @@
<?php
namespace app\cunkebao\model;
use think\Model;
/**
* 获客场景模型类
*/
class PlanScene extends Model
{
// 设置表名
protected $name = 'plan_scene';
/**
* 获取场景列表
*
* @param array $where 查询条件
* @param string $order 排序
* @param int $page 页码
* @param int $limit 每页数量
* @return array 场景列表和总数
*/
public static function getSceneList($where = [], $order = 'id desc', $page = 1, $limit = 10)
{
// 构建查询
$query = self::where($where);
// 计算总数
$total = $query->count();
// 分页查询数据
$list = $query->page($page, $limit)
->order($order)
->select();
return [
'list' => $list,
'total' => $total,
'page' => $page,
'limit' => $limit
];
}
}