新建获客计划 - 设备选择
This commit is contained in:
@@ -5,13 +5,15 @@ import { Card } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { HelpCircle, MessageSquare, AlertCircle } from "lucide-react"
|
||||
import { HelpCircle, MessageSquare, AlertCircle, RefreshCw } from "lucide-react"
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert"
|
||||
import { ChevronsUpDown } from "lucide-react"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { fetchDeviceList } from "@/api/devices"
|
||||
import type { ServerDevice } from "@/types/device"
|
||||
|
||||
interface FriendRequestSettingsProps {
|
||||
formData: any
|
||||
@@ -36,20 +38,15 @@ const remarkTypes = [
|
||||
{ value: "source", label: "来源" },
|
||||
]
|
||||
|
||||
// 模拟设备数据
|
||||
const mockDevices = [
|
||||
{ id: "1", name: "iPhone 13 Pro", status: "online" },
|
||||
{ id: "2", name: "Xiaomi 12", status: "online" },
|
||||
{ id: "3", name: "Huawei P40", status: "offline" },
|
||||
{ id: "4", name: "OPPO Find X3", status: "online" },
|
||||
{ id: "5", name: "Samsung S21", status: "online" },
|
||||
]
|
||||
|
||||
export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: FriendRequestSettingsProps) {
|
||||
const [isTemplateDialogOpen, setIsTemplateDialogOpen] = useState(false)
|
||||
const [hasWarnings, setHasWarnings] = useState(false)
|
||||
const [isDeviceSelectorOpen, setIsDeviceSelectorOpen] = useState(false)
|
||||
const [selectedDevices, setSelectedDevices] = useState<any[]>(formData.selectedDevices || [])
|
||||
const [selectedDevices, setSelectedDevices] = useState<ServerDevice[]>(formData.selectedDevices || [])
|
||||
const [devices, setDevices] = useState<ServerDevice[]>([])
|
||||
const [loadingDevices, setLoadingDevices] = useState(false)
|
||||
const [deviceError, setDeviceError] = useState<string | null>(null)
|
||||
const [searchKeyword, setSearchKeyword] = useState("")
|
||||
|
||||
// 获取场景标题
|
||||
const getScenarioTitle = () => {
|
||||
@@ -67,6 +64,33 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
|
||||
}
|
||||
}
|
||||
|
||||
// 加载设备列表
|
||||
const loadDevices = async () => {
|
||||
try {
|
||||
setLoadingDevices(true)
|
||||
setDeviceError(null)
|
||||
|
||||
const response = await fetchDeviceList(1, 100, searchKeyword)
|
||||
|
||||
if (response.code === 200 && response.data?.list) {
|
||||
setDevices(response.data.list)
|
||||
} else {
|
||||
setDeviceError(response.msg || "获取设备列表失败")
|
||||
console.error("获取设备列表失败:", response.msg)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("获取设备列表失败:", err)
|
||||
setDeviceError("获取设备列表失败,请稍后重试")
|
||||
} finally {
|
||||
setLoadingDevices(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时加载设备列表
|
||||
useEffect(() => {
|
||||
loadDevices()
|
||||
}, [])
|
||||
|
||||
// 使用useEffect设置默认值
|
||||
useEffect(() => {
|
||||
if (!formData.greeting) {
|
||||
@@ -96,7 +120,7 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
|
||||
onNext()
|
||||
}
|
||||
|
||||
const toggleDeviceSelection = (device: any) => {
|
||||
const toggleDeviceSelection = (device: ServerDevice) => {
|
||||
const isSelected = selectedDevices.some((d) => d.id === device.id)
|
||||
let newSelectedDevices
|
||||
|
||||
@@ -110,6 +134,11 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
|
||||
onChange({ ...formData, selectedDevices: newSelectedDevices })
|
||||
}
|
||||
|
||||
// 根据关键词搜索设备
|
||||
const handleSearch = () => {
|
||||
loadDevices()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
@@ -128,31 +157,77 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
|
||||
{isDeviceSelectorOpen && (
|
||||
<div className="absolute z-10 w-full mt-1 bg-white border rounded-md shadow-lg">
|
||||
<div className="p-2">
|
||||
<Input placeholder="搜索设备..." className="mb-2" />
|
||||
<div className="max-h-60 overflow-auto">
|
||||
{mockDevices.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
className="flex items-center justify-between p-2 hover:bg-gray-100 cursor-pointer"
|
||||
onClick={() => toggleDeviceSelection(device)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={selectedDevices.some((d) => d.id === device.id)}
|
||||
onCheckedChange={() => toggleDeviceSelection(device)}
|
||||
/>
|
||||
<span>{device.name}</span>
|
||||
</div>
|
||||
<span className={`text-xs ${device.status === "online" ? "text-green-500" : "text-gray-400"}`}>
|
||||
{device.status === "online" ? "在线" : "离线"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-2 mb-2">
|
||||
<Input
|
||||
placeholder="搜索设备..."
|
||||
value={searchKeyword}
|
||||
onChange={(e) => setSearchKeyword(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
/>
|
||||
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loadingDevices ? (
|
||||
<div className="flex justify-center items-center py-4">
|
||||
<div className="animate-spin h-5 w-5 border-2 border-blue-500 rounded-full border-t-transparent"></div>
|
||||
</div>
|
||||
) : deviceError ? (
|
||||
<div className="text-center text-red-500 py-4">
|
||||
{deviceError}
|
||||
<Button variant="outline" size="sm" onClick={loadDevices} className="ml-2">
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
) : devices.length === 0 ? (
|
||||
<div className="text-center text-gray-500 py-4">
|
||||
没有找到设备
|
||||
</div>
|
||||
) : (
|
||||
<div className="max-h-60 overflow-auto">
|
||||
{devices.map((device) => (
|
||||
<div
|
||||
key={device.id}
|
||||
className="flex items-center justify-between p-2 hover:bg-gray-100 cursor-pointer"
|
||||
onClick={() => toggleDeviceSelection(device)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
checked={selectedDevices.some((d) => d.id === device.id)}
|
||||
onCheckedChange={() => toggleDeviceSelection(device)}
|
||||
/>
|
||||
<span>{device.memo}</span>
|
||||
</div>
|
||||
<span className={`text-xs ${device.alive === 1 ? "text-green-500" : "text-gray-400"}`}>
|
||||
{device.alive === 1 ? "在线" : "离线"}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedDevices.length > 0 && (
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{selectedDevices.map((device) => (
|
||||
<div key={device.id} className="flex items-center bg-gray-100 rounded-full px-3 py-1">
|
||||
<span className="text-sm">{device.memo}</span>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="ml-2 p-0"
|
||||
onClick={() => toggleDeviceSelection(device)}
|
||||
>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -239,7 +314,7 @@ export function FriendRequestSettings({ formData, onChange, onNext, onPrev }: Fr
|
||||
</div>
|
||||
|
||||
{hasWarnings && (
|
||||
<Alert variant="warning" className="bg-amber-50 border-amber-200">
|
||||
<Alert variant="destructive" className="bg-amber-50 border-amber-200">
|
||||
<AlertCircle className="h-4 w-4 text-amber-500" />
|
||||
<AlertDescription>您有未完成的设置项,建议完善后再进入下一步。</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
<?php
|
||||
namespace app\plan\controller;
|
||||
|
||||
use think\Controller;
|
||||
use think\Request;
|
||||
use app\plan\model\Tag as TagModel;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 标签控制器
|
||||
*/
|
||||
class Tag extends Controller
|
||||
{
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
protected function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签列表
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$type = Request::param('type', '');
|
||||
$status = Request::param('status', 1, 'intval');
|
||||
|
||||
// 查询标签列表
|
||||
$tags = TagModel::getTagsByType($type, $status);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $tags
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建标签
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
$data = Request::post();
|
||||
|
||||
// 数据验证
|
||||
if (empty($data['name']) || empty($data['type'])) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '缺少必要参数'
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建或获取标签
|
||||
$tagId = TagModel::getOrCreate($data['name'], $data['type']);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '创建成功',
|
||||
'data' => $tagId
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('创建标签异常', [
|
||||
'data' => $data,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '创建失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建标签
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function batchCreate()
|
||||
{
|
||||
$data = Request::post();
|
||||
|
||||
// 数据验证
|
||||
if (empty($data['names']) || empty($data['type'])) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '缺少必要参数'
|
||||
]);
|
||||
}
|
||||
|
||||
// 检查名称数组
|
||||
if (!is_array($data['names'])) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '标签名称必须是数组'
|
||||
]);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = [];
|
||||
|
||||
// 批量处理标签
|
||||
foreach ($data['names'] as $name) {
|
||||
$name = trim($name);
|
||||
if (empty($name)) continue;
|
||||
|
||||
$tagId = TagModel::getOrCreate($name, $data['type']);
|
||||
$result[] = [
|
||||
'id' => $tagId,
|
||||
'name' => $name,
|
||||
'type' => $data['type']
|
||||
];
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '创建成功',
|
||||
'data' => $result
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('批量创建标签异常', [
|
||||
'data' => $data,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return json([
|
||||
'code' => 500,
|
||||
'msg' => '创建失败:' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新标签
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function update($id)
|
||||
{
|
||||
$data = Request::put();
|
||||
|
||||
// 检查标签是否存在
|
||||
$tag = TagModel::get($id);
|
||||
if (!$tag) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '标签不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 准备更新数据
|
||||
$updateData = [];
|
||||
|
||||
// 只允许更新特定字段
|
||||
$allowedFields = ['name', 'status'];
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$updateData[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
// 更新标签
|
||||
$tag->save($updateData);
|
||||
|
||||
// 如果更新了标签名称,且该标签有使用次数,则增加计数
|
||||
if (isset($updateData['name']) && $updateData['name'] != $tag->name && $tag->count > 0) {
|
||||
$tag->updateCount(1);
|
||||
}
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '更新成功'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除标签
|
||||
*
|
||||
* @param int $id
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
{
|
||||
// 检查标签是否存在
|
||||
$tag = TagModel::get($id);
|
||||
if (!$tag) {
|
||||
return json([
|
||||
'code' => 404,
|
||||
'msg' => '标签不存在'
|
||||
]);
|
||||
}
|
||||
|
||||
// 更新状态为删除
|
||||
$tag->save([
|
||||
'status' => 0
|
||||
]);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '删除成功'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签名称
|
||||
*
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function getNames()
|
||||
{
|
||||
$ids = Request::param('ids');
|
||||
|
||||
// 验证参数
|
||||
if (empty($ids)) {
|
||||
return json([
|
||||
'code' => 400,
|
||||
'msg' => '缺少标签ID参数'
|
||||
]);
|
||||
}
|
||||
|
||||
// 处理参数
|
||||
if (is_string($ids)) {
|
||||
$ids = explode(',', $ids);
|
||||
}
|
||||
|
||||
// 获取标签名称
|
||||
$names = TagModel::getTagNames($ids);
|
||||
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $names
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
<?php
|
||||
namespace app\plan\model;
|
||||
|
||||
use think\Model;
|
||||
|
||||
/**
|
||||
* 标签模型
|
||||
*/
|
||||
class Tag extends Model
|
||||
{
|
||||
// 设置表名
|
||||
protected $name = 'tag';
|
||||
protected $prefix = 'tk_';
|
||||
|
||||
// 设置主键
|
||||
protected $pk = 'id';
|
||||
|
||||
// 自动写入时间戳
|
||||
protected $autoWriteTimestamp = 'int';
|
||||
|
||||
// 定义时间戳字段名
|
||||
protected $createTime = 'createTime';
|
||||
protected $updateTime = 'updateTime';
|
||||
|
||||
// 定义字段类型
|
||||
protected $type = [
|
||||
'id' => 'integer',
|
||||
'count' => 'integer',
|
||||
'status' => 'integer',
|
||||
'createTime' => 'integer',
|
||||
'updateTime' => 'integer'
|
||||
];
|
||||
|
||||
/**
|
||||
* 获取或创建标签
|
||||
* @param string $name 标签名
|
||||
* @param string $type 标签类型
|
||||
* @param string $color 标签颜色
|
||||
* @return int 标签ID
|
||||
*/
|
||||
public static function getOrCreate($name, $type = 'traffic', $color = '')
|
||||
{
|
||||
$tag = self::where([
|
||||
['name', '=', $name],
|
||||
['type', '=', $type]
|
||||
])->find();
|
||||
|
||||
if ($tag) {
|
||||
return $tag['id'];
|
||||
} else {
|
||||
$model = new self();
|
||||
$model->save([
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'color' => $color ?: self::getRandomColor(),
|
||||
'count' => 0,
|
||||
'status' => 1
|
||||
]);
|
||||
return $model->id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新标签使用次数
|
||||
* @param int $id 标签ID
|
||||
* @param int $increment 增量
|
||||
* @return bool 更新结果
|
||||
*/
|
||||
public static function updateCount($id, $increment = 1)
|
||||
{
|
||||
return self::where('id', $id)->inc('count', $increment)->update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取标签列表
|
||||
* @param string $type 标签类型
|
||||
* @param array $where 额外条件
|
||||
* @return array 标签列表
|
||||
*/
|
||||
public static function getTagsByType($type = 'traffic', $where = [])
|
||||
{
|
||||
$conditions = array_merge([
|
||||
['type', '=', $type],
|
||||
['status', '=', 1]
|
||||
], $where);
|
||||
|
||||
return self::where($conditions)
|
||||
->order('count DESC, id DESC')
|
||||
->select();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取标签名称
|
||||
* @param array $ids 标签ID数组
|
||||
* @return array 标签名称数组
|
||||
*/
|
||||
public static function getTagNames($ids)
|
||||
{
|
||||
if (empty($ids)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tagIds = is_array($ids) ? $ids : explode(',', $ids);
|
||||
|
||||
$tags = self::where('id', 'in', $tagIds)->column('name');
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取随机颜色
|
||||
* @return string 颜色代码
|
||||
*/
|
||||
private static function getRandomColor()
|
||||
{
|
||||
$colors = [
|
||||
'#f44336', '#e91e63', '#9c27b0', '#673ab7', '#3f51b5',
|
||||
'#2196f3', '#03a9f4', '#00bcd4', '#009688', '#4caf50',
|
||||
'#8bc34a', '#cddc39', '#ffeb3b', '#ffc107', '#ff9800',
|
||||
'#ff5722', '#795548', '#9e9e9e', '#607d8b'
|
||||
];
|
||||
|
||||
return $colors[array_rand($colors)];
|
||||
}
|
||||
}
|
||||
@@ -19,11 +19,6 @@ Route::group('api/plan', function () {
|
||||
Route::get('traffic/stats', 'plan/Traffic/sourceStats');
|
||||
Route::post('traffic/import', 'plan/Traffic/importTraffic');
|
||||
Route::post('traffic/external', 'plan/Traffic/handleExternalTraffic');
|
||||
|
||||
// 标签相关路由
|
||||
Route::resource('tags', 'plan/Tag');
|
||||
Route::post('tags/batch', 'plan/Tag/batchCreate');
|
||||
Route::get('tags/names', 'plan/Tag/getNames');
|
||||
});
|
||||
|
||||
// 返回空数组,避免路由注册冲突
|
||||
|
||||
Reference in New Issue
Block a user