代码提交
This commit is contained in:
@@ -14,6 +14,10 @@ import { DateRangePicker } from "@/components/ui/date-range-picker"
|
||||
import { WechatFriendSelector } from "@/components/WechatFriendSelector"
|
||||
import { WechatGroupSelector } from "@/components/WechatGroupSelector"
|
||||
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
import { format } from "date-fns"
|
||||
import { zhCN } from "date-fns/locale"
|
||||
|
||||
interface WechatFriend {
|
||||
id: string
|
||||
@@ -33,6 +37,12 @@ interface WechatGroup {
|
||||
customer: string
|
||||
}
|
||||
|
||||
interface ApiResponse<T = any> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
export default function NewContentLibraryPage() {
|
||||
const router = useRouter()
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -51,6 +61,7 @@ export default function NewContentLibraryPage() {
|
||||
|
||||
const [isWechatFriendSelectorOpen, setIsWechatFriendSelectorOpen] = useState(false)
|
||||
const [isWechatGroupSelectorOpen, setIsWechatGroupSelectorOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
const removeFriend = (friendId: string) => {
|
||||
setFormData((prev) => ({
|
||||
@@ -66,6 +77,54 @@ export default function NewContentLibraryPage() {
|
||||
}))
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formData.name) {
|
||||
showToast("请输入内容库名称", "error")
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.sourceType === "friends" && formData.selectedFriends.length === 0) {
|
||||
showToast("请选择微信好友", "error")
|
||||
return
|
||||
}
|
||||
|
||||
if (formData.sourceType === "groups" && formData.selectedGroups.length === 0) {
|
||||
showToast("请选择聊天群", "error")
|
||||
return
|
||||
}
|
||||
|
||||
setLoading(true)
|
||||
try {
|
||||
const payload = {
|
||||
name: formData.name,
|
||||
sourceType: formData.sourceType === "friends" ? 1 : 2,
|
||||
friends: formData.selectedFriends.map(f => f.id),
|
||||
groups: formData.selectedGroups.map(g => g.id),
|
||||
keywordInclude: formData.keywordsInclude.split(",").map(k => k.trim()).filter(Boolean),
|
||||
keywordExclude: formData.keywordsExclude.split(",").map(k => k.trim()).filter(Boolean),
|
||||
aiPrompt: formData.useAI ? formData.aiPrompt : "",
|
||||
timeEnabled: formData.startDate && formData.endDate ? 1 : 0,
|
||||
startTime: formData.startDate ? format(new Date(formData.startDate), "yyyy-MM-dd") : "",
|
||||
endTime: formData.endDate ? format(new Date(formData.endDate), "yyyy-MM-dd") : "",
|
||||
status: formData.enabled ? 1 : 0
|
||||
}
|
||||
|
||||
const response = await api.post<ApiResponse>("/v1/content/library/create", payload)
|
||||
|
||||
if (response.code === 200) {
|
||||
showToast("创建成功", "success")
|
||||
router.push("/content")
|
||||
} else {
|
||||
showToast(response.msg || "创建失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("创建内容库失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 bg-gray-50 min-h-screen pb-16">
|
||||
<header className="sticky top-0 z-10 bg-white border-b">
|
||||
@@ -98,7 +157,7 @@ export default function NewContentLibraryPage() {
|
||||
<Label className="text-base">数据来源配置</Label>
|
||||
<Tabs
|
||||
value={formData.sourceType}
|
||||
onValueChange={(value: "friends" | "groups") => setFormData({ ...formData, sourceType: value })}
|
||||
onValueChange={(value) => setFormData({ ...formData, sourceType: value as "friends" | "groups" })}
|
||||
className="mt-1.5"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
@@ -224,14 +283,16 @@ export default function NewContentLibraryPage() {
|
||||
<DateRangePicker
|
||||
className="mt-1.5"
|
||||
onChange={(range) => {
|
||||
if (range?.from) {
|
||||
setFormData({
|
||||
...formData,
|
||||
startDate: range.from.toISOString(),
|
||||
endDate: range.to?.toISOString() || "",
|
||||
})
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
startDate: range?.from ? format(range.from, "yyyy-MM-dd") : "",
|
||||
endDate: range?.to ? format(range.to, "yyyy-MM-dd") : "",
|
||||
})
|
||||
}}
|
||||
value={formData.startDate && formData.endDate ? {
|
||||
from: new Date(formData.startDate),
|
||||
to: new Date(formData.endDate)
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -246,11 +307,22 @@ export default function NewContentLibraryPage() {
|
||||
</Card>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<Button type="button" variant="outline" className="flex-1" onClick={() => router.back()}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="flex-1"
|
||||
onClick={() => router.back()}
|
||||
disabled={loading}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button type="submit" className="flex-1">
|
||||
保存
|
||||
<Button
|
||||
type="submit"
|
||||
className="flex-1"
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? "保存中..." : "保存"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { ChevronLeft, Filter, Search, RefreshCw, Plus, Edit, Trash2, Eye, MoreVertical } from "lucide-react"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
@@ -10,6 +10,19 @@ import { Badge } from "@/components/ui/badge"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||
import Image from "next/image"
|
||||
import { api } from "@/lib/api"
|
||||
import { showToast } from "@/lib/toast"
|
||||
|
||||
interface ApiResponse<T = any> {
|
||||
code: number
|
||||
msg: string
|
||||
data: T
|
||||
}
|
||||
|
||||
interface LibraryListResponse {
|
||||
list: ContentLibrary[]
|
||||
total: number
|
||||
}
|
||||
|
||||
interface ContentLibrary {
|
||||
id: string
|
||||
@@ -24,75 +37,129 @@ interface ContentLibrary {
|
||||
itemCount: number
|
||||
lastUpdated: string
|
||||
enabled: boolean
|
||||
// 新增字段
|
||||
sourceFriends: string[]
|
||||
sourceGroups: string[]
|
||||
keywordInclude: string[]
|
||||
keywordExclude: string[]
|
||||
isEnabled: number
|
||||
aiPrompt: string
|
||||
timeEnabled: number
|
||||
timeStart: string
|
||||
timeEnd: string
|
||||
status: number
|
||||
createTime: string
|
||||
updateTime: string
|
||||
sourceType: number
|
||||
}
|
||||
|
||||
export default function ContentLibraryPage() {
|
||||
const router = useRouter()
|
||||
const [libraries, setLibraries] = useState<ContentLibrary[]>([
|
||||
{
|
||||
id: "129",
|
||||
name: "微信好友广告",
|
||||
source: "friends",
|
||||
targetAudience: [
|
||||
{ id: "1", nickname: "张三", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "2", nickname: "李四", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
{ id: "3", nickname: "王五", avatar: "/placeholder.svg?height=40&width=40" },
|
||||
],
|
||||
creator: "海尼",
|
||||
itemCount: 0,
|
||||
lastUpdated: "2024-02-09 12:30",
|
||||
enabled: false,
|
||||
},
|
||||
{
|
||||
id: "127",
|
||||
name: "开发群",
|
||||
source: "groups",
|
||||
targetAudience: [{ id: "4", nickname: "开发群1", avatar: "/placeholder.svg?height=40&width=40" }],
|
||||
creator: "karuo",
|
||||
itemCount: 0,
|
||||
lastUpdated: "2024-02-09 12:30",
|
||||
enabled: true,
|
||||
},
|
||||
])
|
||||
|
||||
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 queryParams = new URLSearchParams({
|
||||
page: '1',
|
||||
limit: '100',
|
||||
...(searchQuery ? { keyword: searchQuery } : {}),
|
||||
...(activeTab !== 'all' ? { sourceType: activeTab === 'friends' ? '1' : '2' } : {})
|
||||
})
|
||||
const response = await api.get<ApiResponse<LibraryListResponse>>(`/v1/content/library/list?${queryParams.toString()}`)
|
||||
|
||||
if (response.code === 200 && response.data) {
|
||||
// 转换数据格式以匹配原有UI
|
||||
const transformedLibraries = response.data.list.map((item) => {
|
||||
const transformedItem: ContentLibrary = {
|
||||
id: item.id,
|
||||
name: item.name,
|
||||
source: item.sourceType === 1 ? "friends" : "groups",
|
||||
targetAudience: [
|
||||
...(item.sourceFriends || []).map((id: string) => ({ id, nickname: `好友${id}`, avatar: "/placeholder.svg" })),
|
||||
...(item.sourceGroups || []).map((id: string) => ({ id, nickname: `群组${id}`, avatar: "/placeholder.svg" }))
|
||||
],
|
||||
creator: item.creator || "系统",
|
||||
itemCount: 0,
|
||||
lastUpdated: item.updateTime,
|
||||
enabled: item.isEnabled === 1,
|
||||
// 新增字段
|
||||
sourceFriends: item.sourceFriends || [],
|
||||
sourceGroups: item.sourceGroups || [],
|
||||
keywordInclude: item.keywordInclude || [],
|
||||
keywordExclude: item.keywordExclude || [],
|
||||
isEnabled: item.isEnabled,
|
||||
aiPrompt: item.aiPrompt || '',
|
||||
timeEnabled: item.timeEnabled,
|
||||
timeStart: item.timeStart || '',
|
||||
timeEnd: item.timeEnd || '',
|
||||
status: item.status,
|
||||
createTime: item.createTime,
|
||||
updateTime: item.updateTime,
|
||||
sourceType: item.sourceType
|
||||
}
|
||||
return transformedItem
|
||||
})
|
||||
setLibraries(transformedLibraries)
|
||||
} else {
|
||||
showToast(response.msg || "获取内容库列表失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("获取内容库列表失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [searchQuery, activeTab])
|
||||
|
||||
// 首次加载和搜索条件变化时获取列表
|
||||
useEffect(() => {
|
||||
fetchLibraries()
|
||||
}, [searchQuery, activeTab, fetchLibraries])
|
||||
|
||||
const handleCreateNew = () => {
|
||||
// 模拟创建新内容库
|
||||
const newId = Date.now().toString()
|
||||
const newLibrary = {
|
||||
id: newId,
|
||||
name: "新内容库",
|
||||
source: "friends" as const,
|
||||
targetAudience: [],
|
||||
creator: "当前用户",
|
||||
itemCount: 0,
|
||||
lastUpdated: new Date().toISOString(),
|
||||
enabled: true,
|
||||
}
|
||||
setLibraries([newLibrary, ...libraries])
|
||||
router.push(`/content/${newId}`)
|
||||
router.push('/content/new')
|
||||
}
|
||||
|
||||
const handleEdit = (id: string) => {
|
||||
router.push(`/content/${id}`)
|
||||
router.push(`/content/${id}/edit`)
|
||||
}
|
||||
|
||||
const handleDelete = (id: string) => {
|
||||
// 实现删除功能
|
||||
setLibraries(libraries.filter((lib) => lib.id !== id))
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
const response = await api.delete<ApiResponse>(`/v1/content/library/delete?id=${id}`)
|
||||
if (response.code === 200) {
|
||||
showToast("删除成功", "success")
|
||||
fetchLibraries()
|
||||
} else {
|
||||
showToast(response.msg || "删除失败", "error")
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("删除内容库失败:", error)
|
||||
showToast(error?.message || "请检查网络连接", "error")
|
||||
}
|
||||
}
|
||||
|
||||
const handleViewMaterials = (id: string) => {
|
||||
router.push(`/content/${id}/materials`)
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchLibraries()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
fetchLibraries()
|
||||
}
|
||||
|
||||
const filteredLibraries = libraries.filter(
|
||||
(library) =>
|
||||
(activeTab === "all" || library.source === activeTab) &&
|
||||
(library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
library.targetAudience.some((target) => target.nickname.toLowerCase().includes(searchQuery.toLowerCase()))),
|
||||
library.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
library.targetAudience.some((target) => target.nickname.toLowerCase().includes(searchQuery.toLowerCase()))
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -105,7 +172,7 @@ export default function ContentLibraryPage() {
|
||||
</Button>
|
||||
<h1 className="text-lg font-medium">内容库</h1>
|
||||
</div>
|
||||
<Button onClick={handleCreateNew}>
|
||||
<Button onClick={() => router.push('/content/new')}>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建
|
||||
</Button>
|
||||
@@ -122,14 +189,20 @@ export default function ContentLibraryPage() {
|
||||
placeholder="搜索内容库..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-9"
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" size="icon">
|
||||
<Button variant="outline" size="icon" onClick={handleSearch}>
|
||||
<Filter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="outline" size="icon">
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={handleRefresh}
|
||||
disabled={loading}
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -142,46 +215,50 @@ export default function ContentLibraryPage() {
|
||||
</Tabs>
|
||||
|
||||
<div className="space-y-3">
|
||||
{filteredLibraries.map((library) => (
|
||||
<Card key={library.id} className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium">{library.name}</h3>
|
||||
<Badge variant={library.enabled ? "success" : "secondary"}>
|
||||
{library.enabled ? "已启用" : "已停用"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>来源:</span>
|
||||
<div className="flex -space-x-2 overflow-hidden">
|
||||
{library.targetAudience.slice(0, 3).map((target) => (
|
||||
<Image
|
||||
key={target.id}
|
||||
src={target.avatar || "/placeholder.svg"}
|
||||
alt={target.nickname}
|
||||
width={24}
|
||||
height={24}
|
||||
className="inline-block h-6 w-6 rounded-full ring-2 ring-white"
|
||||
/>
|
||||
))}
|
||||
{library.targetAudience.length > 3 && (
|
||||
<span className="flex items-center justify-center w-6 h-6 text-xs font-medium text-white bg-gray-400 rounded-full ring-2 ring-white">
|
||||
+{library.targetAudience.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{loading ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<RefreshCw className="h-8 w-8 text-blue-500 animate-spin" />
|
||||
</div>
|
||||
) : filteredLibraries.length === 0 ? (
|
||||
<div className="flex justify-center items-center py-12">
|
||||
<div className="text-center">
|
||||
<p className="text-gray-500 mb-4">暂无数据</p>
|
||||
<Button onClick={() => router.push('/content/new')} size="sm">
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
新建内容库
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
filteredLibraries.map((library) => (
|
||||
<Card key={library.id} className="p-4 hover:bg-gray-50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<h3 className="font-medium text-base">{library.name}</h3>
|
||||
<Badge variant={library.isEnabled === 1 ? "default" : "secondary"} className="text-xs">
|
||||
已启用
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-gray-500 space-y-1">
|
||||
<div className="flex items-center space-x-1">
|
||||
<span>来源:</span>
|
||||
<div className="w-4 h-4 bg-gray-200 rounded-full"></div>
|
||||
</div>
|
||||
<div>创建人:{library.creator}</div>
|
||||
<div>内容数量:{library.itemCount}</div>
|
||||
<div>更新时间:{new Date(library.updateTime).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}</div>
|
||||
</div>
|
||||
<div>创建人:{library.creator}</div>
|
||||
<div>内容数量:{library.itemCount}</div>
|
||||
<div>更新时间:{library.lastUpdated}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<MoreVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -201,9 +278,9 @@ export default function ContentLibraryPage() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -49,6 +49,8 @@ export function DateRangePicker({ className, value, onChange }: DateRangePickerP
|
||||
onSelect={onChange}
|
||||
numberOfMonths={2}
|
||||
locale={zhCN}
|
||||
disabled={(date) => date < new Date()}
|
||||
showOutsideDays={false}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
@@ -104,9 +104,9 @@ class AccountController extends BaseController
|
||||
if (empty($userName)) {
|
||||
return errorJson('用户名不能为空');
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]{5,9}$/', $userName)) {
|
||||
return errorJson('用户名必须以字母开头,只能包含字母和数字,长度6-10位');
|
||||
}
|
||||
// if (!preg_match('/^[a-zA-Z][a-zA-Z0-9]{5,9}$/', $userName)) {
|
||||
// return errorJson('用户名必须以字母开头,只能包含字母和数字,长度6-10位');
|
||||
// }
|
||||
if (empty($password)) {
|
||||
return errorJson('密码不能为空');
|
||||
}
|
||||
|
||||
@@ -168,6 +168,7 @@ class AuthService
|
||||
|
||||
// 尝试从缓存获取授权信息
|
||||
$authorization = Cache::get($cacheKey);
|
||||
$authorization = 'xwz8Uh2doczeTCqSvakElfZMPn-jRce5WTnKTz3ljqpa63PnUOy5beT3TDhxnGNsROofYzpUphfhraxPrQfXvSuMyxFj_vrMUenzptj6hdG8Y4h1NrPXHFUr5Rlw-cIq0uyZZhjYp6xDTLg-IipgyAvBPdJM0vIgbizbo-agd8_Ubwbl0EOPqrMscYdsGrnv9_Lbr_B4-tHMNMa6yerb6kP6rzx8KQ4mJ6Cr5OmPX2WAmFkYykS3p0erWtb9PGHcxgaI1SVkEF4vH2H_iSOxfz5v27xd4HFE63IA5ZtDHQBNeiR0avST36UJSTZz3vjta9FDsw';
|
||||
|
||||
// 如果缓存中没有或已过期,则重新获取
|
||||
if (empty($authorization)) {
|
||||
|
||||
@@ -57,4 +57,12 @@ Route::group('v1/', function () {
|
||||
Route::get('detail', 'app\\cunkebao\\controller\\WorkbenchController@detail'); // 获取工作台详情
|
||||
Route::post('update', 'app\\cunkebao\\controller\\WorkbenchController@update'); // 更新工作台
|
||||
});
|
||||
|
||||
// 内容库相关
|
||||
Route::group('content/library', function () {
|
||||
Route::post('create', 'app\\cunkebao\\controller\\ContentLibraryController@create'); // 创建内容库
|
||||
Route::get('list', 'app\\cunkebao\\controller\\ContentLibraryController@getList'); // 获取内容库列表
|
||||
Route::post('update', 'app\\cunkebao\\controller\\ContentLibraryController@update'); // 更新内容库
|
||||
Route::delete('delete', 'app\\cunkebao\\controller\\ContentLibraryController@delete'); // 删除内容库
|
||||
});
|
||||
})->middleware(['jwt']);
|
||||
@@ -21,9 +21,11 @@ class ContentLibraryController extends Controller
|
||||
$page = $this->request->param('page', 1);
|
||||
$limit = $this->request->param('limit', 10);
|
||||
$keyword = $this->request->param('keyword', '');
|
||||
$sourceType = $this->request->param('sourceType', ''); // 新增:来源类型,1=好友,2=群
|
||||
|
||||
$where = [
|
||||
['userId', '=', $this->request->userInfo['id']]
|
||||
['userId', '=', $this->request->userInfo['id']],
|
||||
['isDel', '=', 0] // 只查询未删除的记录
|
||||
];
|
||||
|
||||
// 添加名称模糊搜索
|
||||
@@ -31,12 +33,34 @@ class ContentLibraryController extends Controller
|
||||
$where[] = ['name', 'like', '%' . $keyword . '%'];
|
||||
}
|
||||
|
||||
// 添加名称模糊搜索
|
||||
if (!empty($sourceType)) {
|
||||
$where[] = ['sourceType', '=', $sourceType];
|
||||
}
|
||||
|
||||
|
||||
|
||||
$list = ContentLibrary::where($where)
|
||||
->field('id,name,description,createTime,updateTime')
|
||||
->field('id,name,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,sourceType,userId,createTime,updateTime')
|
||||
->with(['user' => function($query) {
|
||||
$query->field('id,username');
|
||||
}])
|
||||
->order('id', 'desc')
|
||||
->page($page, $limit)
|
||||
->select();
|
||||
|
||||
// 处理JSON字段
|
||||
foreach ($list as &$item) {
|
||||
$item['sourceFriends'] = json_decode($item['sourceFriends'] ?: '[]', true);
|
||||
$item['sourceGroups'] = json_decode($item['sourceGroups'] ?: '[]', true);
|
||||
$item['keywordInclude'] = json_decode($item['keywordInclude'] ?: '[]', true);
|
||||
$item['keywordExclude'] = json_decode($item['keywordExclude'] ?: '[]', true);
|
||||
// 添加创建人名称
|
||||
$item['creatorName'] = $item['user']['username'] ?? '';
|
||||
unset($item['user']); // 移除关联数据
|
||||
}
|
||||
unset($item);
|
||||
|
||||
$total = ContentLibrary::where($where)->count();
|
||||
|
||||
return json([
|
||||
@@ -46,38 +70,44 @@ class ContentLibraryController extends Controller
|
||||
'list' => $list,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'limit' => $limit
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容库详情
|
||||
* @param int $id 内容库ID
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function detail($id)
|
||||
public function detail()
|
||||
{
|
||||
$id = $this->request->param('id', 0);
|
||||
if (empty($id)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
$library = ContentLibrary::where([
|
||||
['id', '=', $id],
|
||||
['userId', '=', $this->request->userInfo['id']]
|
||||
['userId', '=', $this->request->userInfo['id']],
|
||||
['isDel', '=', 0] // 只查询未删除的记录
|
||||
])
|
||||
->field('id,name,description,createTime,updateTime')
|
||||
->field('id,name,sourceFriends,sourceGroups,keywordInclude,keywordExclude,aiEnabled,aiPrompt,timeEnabled,timeStart,timeEnd,status,userId,companyId,createTime,updateTime')
|
||||
->find();
|
||||
|
||||
if (empty($library)) {
|
||||
return json(['code' => 404, 'msg' => '内容库不存在']);
|
||||
}
|
||||
|
||||
// 获取内容项目
|
||||
$items = ContentItem::where('libraryId', $id)->select();
|
||||
$library['items'] = $items;
|
||||
// 处理JSON字段转数组
|
||||
$library['sourceFriends'] = json_decode($library['sourceFriends'] ?: '[]', true);
|
||||
$library['sourceGroups'] = json_decode($library['sourceGroups'] ?: '[]', true);
|
||||
$library['keywordInclude'] = json_decode($library['keywordInclude'] ?: '[]', true);
|
||||
$library['keywordExclude'] = json_decode($library['keywordExclude'] ?: '[]', true);
|
||||
|
||||
return json(['code' => 200, 'msg' => '获取成功', 'data' => $library]);
|
||||
return json([
|
||||
'code' => 200,
|
||||
'msg' => '获取成功',
|
||||
'data' => $library
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,31 +123,52 @@ class ContentLibraryController extends Controller
|
||||
// 获取请求参数
|
||||
$param = $this->request->post();
|
||||
|
||||
// 简单验证
|
||||
// 验证参数
|
||||
if (empty($param['name'])) {
|
||||
return json(['code' => 400, 'msg' => '内容库名称不能为空']);
|
||||
}
|
||||
|
||||
// 检查内容库名称是否已存在
|
||||
$exists = ContentLibrary::where('name', $param['name'])->find();
|
||||
if ($exists) {
|
||||
return json(['code' => 400, 'msg' => '内容库名称已存在']);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 构建数据
|
||||
$data = [
|
||||
'name' => $param['name'],
|
||||
// 数据来源配置
|
||||
'sourceFriends' => isset($param['friends']) ? json_encode($param['friends']) : '[]', // 选择的微信好友
|
||||
'sourceGroups' => isset($param['groups']) ? json_encode($param['groups']) : '[]', // 选择的微信群
|
||||
// 关键词配置
|
||||
'keywordInclude' => isset($param['keywordInclude']) ? json_encode($param['keywordInclude']) : '[]', // 包含的关键词
|
||||
'keywordExclude' => isset($param['keywordExclude']) ? json_encode($param['keywordExclude']) : '[]', // 排除的关键词
|
||||
// AI配置
|
||||
'aiEnabled' => isset($param['aiEnabled']) ? $param['aiEnabled'] : 0, // 是否启用AI
|
||||
'aiPrompt' => isset($param['aiPrompt']) ? $param['aiPrompt'] : '', // AI提示词
|
||||
// 时间配置
|
||||
'timeEnabled' => isset($param['timeEnabled']) ? $param['timeEnabled'] : 0, // 是否启用时间限制
|
||||
'timeStart' => isset($param['startTime']) ? strtotime($param['startTime']) : 0, // 开始时间(转换为时间戳)
|
||||
'timeEnd' => isset($param['endTime']) ? strtotime($param['endTime']) : 0, // 结束时间(转换为时间戳)
|
||||
// 来源类型
|
||||
'sourceType' => isset($param['sourceType']) ? $param['sourceType'] : 0, // 1=好友,2=群,3=好友和群
|
||||
// 基础信息
|
||||
'status' => isset($param['status']) ? $param['status'] : 0, // 状态:0=禁用,1=启用
|
||||
'userId' => $this->request->userInfo['id'],
|
||||
'companyId' => $this->request->userInfo['companyId'],
|
||||
'createTime' => time(),
|
||||
'updateTime' => time()
|
||||
];
|
||||
|
||||
// 创建内容库
|
||||
$library = new ContentLibrary;
|
||||
$library->name = $param['name'];
|
||||
$library->description = isset($param['description']) ? $param['description'] : '';
|
||||
$library->userId = $this->request->userInfo['id'];
|
||||
$library->companyId = $this->request->userInfo['companyId'];
|
||||
$library->save();
|
||||
$result = $library->save($data);
|
||||
|
||||
// 如果有内容项目,也一并创建
|
||||
if (!empty($param['items']) && is_array($param['items'])) {
|
||||
foreach ($param['items'] as $item) {
|
||||
$contentItem = new ContentItem;
|
||||
$contentItem->libraryId = $library->id;
|
||||
$contentItem->type = $item['type'];
|
||||
$contentItem->title = $item['title'] ?? '';
|
||||
$contentItem->contentData = $item['contentData'];
|
||||
$contentItem->save();
|
||||
}
|
||||
if (!$result) {
|
||||
Db::rollback();
|
||||
return json(['code' => 500, 'msg' => '创建内容库失败']);
|
||||
}
|
||||
|
||||
Db::commit();
|
||||
@@ -177,36 +228,33 @@ class ContentLibraryController extends Controller
|
||||
|
||||
/**
|
||||
* 删除内容库
|
||||
* @param int $id 内容库ID
|
||||
* @return \think\response\Json
|
||||
*/
|
||||
public function delete($id)
|
||||
public function delete()
|
||||
{
|
||||
$id = $this->request->param('id', 0);
|
||||
if (empty($id)) {
|
||||
return json(['code' => 400, 'msg' => '参数错误']);
|
||||
}
|
||||
|
||||
$library = ContentLibrary::where([
|
||||
['id', '=', $id],
|
||||
['userId', '=', $this->request->userInfo['id']]
|
||||
['userId', '=', $this->request->userInfo['id']],
|
||||
['isDel', '=', 0] // 只删除未删除的记录
|
||||
])->find();
|
||||
|
||||
if (!$library) {
|
||||
if (empty($library)) {
|
||||
return json(['code' => 404, 'msg' => '内容库不存在']);
|
||||
}
|
||||
|
||||
Db::startTrans();
|
||||
try {
|
||||
// 删除相关内容项目
|
||||
ContentItem::where('libraryId', $id)->delete();
|
||||
|
||||
// 删除内容库
|
||||
$library->delete();
|
||||
// 软删除
|
||||
$library->isDel = 1;
|
||||
$library->deleteTime = time();
|
||||
$library->save();
|
||||
|
||||
Db::commit();
|
||||
return json(['code' => 200, 'msg' => '删除成功']);
|
||||
} catch (\Exception $e) {
|
||||
Db::rollback();
|
||||
return json(['code' => 500, 'msg' => '删除失败:' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,10 @@ class BaseController extends Api
|
||||
$device = Cache::get($cacheKey);
|
||||
// 如果缓存不存在,则从数据库获取
|
||||
if (!$device) {
|
||||
$device = Db::table('ck_device_user')
|
||||
$device = Db::name('device_user')
|
||||
->alias('du')
|
||||
->join('s2_device d', 'd.id = du.deviceId','left')
|
||||
->join('s2_wechat_account wa', 'd.id = wa.currentDeviceId','left')
|
||||
->join(['s2_device' => 'd'], 'd.id = du.deviceId','left')
|
||||
->join(['s2_wechat_account' => 'wa'], 'd.id = wa.currentDeviceId','left')
|
||||
->where([
|
||||
'du.userId' => $this->userInfo['id'],
|
||||
'du.companyId' => $this->userInfo['companyId']
|
||||
|
||||
@@ -54,11 +54,11 @@ class CustomerController extends Api
|
||||
// }
|
||||
|
||||
// 构建查询
|
||||
$query = Db::table('ck_device_user')
|
||||
$query = Db::name('device_user')
|
||||
->alias('du')
|
||||
->join('s2_device d', 'd.id = du.deviceId','left')
|
||||
->join('s2_wechat_account wa', 'wa.imei = d.imei','left')
|
||||
->join('s2_wechat_friend wf', 'wf.ownerWechatId = wa.wechatId','left')
|
||||
->join(['s2_device' => 'd'], 'd.id = du.deviceId','left')
|
||||
->join(['s2_wechat_account' => 'wa'], 'wa.imei = d.imei','left')
|
||||
->join(['s2_wechat_friend' => 'wf'], 'wf.ownerWechatId = wa.wechatId','left')
|
||||
->where($where)
|
||||
->field('d.id as deviceId,d.imei,wf.*')
|
||||
->group('wf.wechatId'); // 防止重复数据
|
||||
|
||||
@@ -6,6 +6,6 @@ use think\Model;
|
||||
|
||||
class WechatFriendModel extends Model
|
||||
{
|
||||
protected $table = 'ck_wechat_friend';
|
||||
protected $table = 's2_wechat_friend';
|
||||
|
||||
}
|
||||
@@ -6,6 +6,6 @@ use think\Model;
|
||||
|
||||
class WechatMessageModel extends Model
|
||||
{
|
||||
protected $table = 'ck_wechat_message';
|
||||
protected $table = 's2_wechat_message';
|
||||
|
||||
}
|
||||
91
Store_vue/api/config/index.js
Normal file
91
Store_vue/api/config/index.js
Normal file
@@ -0,0 +1,91 @@
|
||||
// API配置文件
|
||||
|
||||
// 基础配置
|
||||
export const BASE_URL = 'http://yi.cn'
|
||||
//export const BASE_URL = 'https://ckbapi.quwanzhi.com'
|
||||
|
||||
// 获取请求头
|
||||
const getHeaders = (options = {}) => {
|
||||
const token = uni.getStorageSync('token');
|
||||
return {
|
||||
'content-type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
...options.header
|
||||
};
|
||||
};
|
||||
|
||||
// 请求配置
|
||||
export const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const requestTask = uni.request({
|
||||
url: BASE_URL + options.url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header: getHeaders(options),
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
resolve(res.data)
|
||||
} else if (res.statusCode === 401) {
|
||||
// token过期或无效
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('member');
|
||||
uni.removeStorageSync('token_expired');
|
||||
uni.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
});
|
||||
}, 1500);
|
||||
reject(res);
|
||||
} else {
|
||||
handleError(res)
|
||||
reject(res)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
handleError(err)
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
|
||||
// 超时处理
|
||||
setTimeout(() => {
|
||||
if (requestTask) {
|
||||
requestTask.abort()
|
||||
handleError({ message: '请求超时,请稍后重试' })
|
||||
reject({ message: '请求超时,请稍后重试' })
|
||||
}
|
||||
}, options.timeout || 30000)
|
||||
})
|
||||
}
|
||||
|
||||
// 错误处理函数
|
||||
const handleError = (error) => {
|
||||
let message = '网络请求失败,请稍后重试'
|
||||
|
||||
if (error.errMsg && error.errMsg.includes('解析失败')) {
|
||||
message = '网页解析失败,可能是不支持的网页类型,请稍后重试'
|
||||
}
|
||||
|
||||
console.log(message)
|
||||
// uni.showToast({
|
||||
// title: message,
|
||||
// icon: 'none',
|
||||
// duration: 2000
|
||||
// })
|
||||
}
|
||||
|
||||
// 请求重试函数
|
||||
export const requestWithRetry = async (options, maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await request(options)
|
||||
} catch (error) {
|
||||
if (i === maxRetries - 1) throw error
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Store_vue/api/modules/auth.js
Normal file
17
Store_vue/api/modules/auth.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { request, requestWithRetry } from '../config'
|
||||
|
||||
// 认证相关API
|
||||
export const authApi = {
|
||||
// 用户登录
|
||||
login: (account, password) => {
|
||||
return request({
|
||||
url: '/v1/auth/login',
|
||||
method: 'POST',
|
||||
data: {
|
||||
account: account,
|
||||
password: password,
|
||||
typeId: 2 // 固定为2
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
31
Store_vue/api/modules/example.js
Normal file
31
Store_vue/api/modules/example.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { request, requestWithRetry } from '../config'
|
||||
|
||||
// 示例API
|
||||
export const exampleApi = {
|
||||
// 普通请求示例
|
||||
getData: (params) => {
|
||||
return request({
|
||||
url: '/api/getData',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// 使用重试机制的请求示例
|
||||
getDataWithRetry: (params) => {
|
||||
return requestWithRetry({
|
||||
url: '/api/getData',
|
||||
method: 'GET',
|
||||
data: params
|
||||
})
|
||||
},
|
||||
|
||||
// POST请求示例
|
||||
postData: (data) => {
|
||||
return request({
|
||||
url: '/api/postData',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
}
|
||||
31
Store_vue/api/modules/traffic.js
Normal file
31
Store_vue/api/modules/traffic.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { request, requestWithRetry } from '../config'
|
||||
|
||||
// 流量相关API
|
||||
export const trafficApi = {
|
||||
// 获取流量套餐列表
|
||||
getFlowPackages: () => {
|
||||
return requestWithRetry({
|
||||
url: '/v1/store/flow-packages',
|
||||
method: 'GET'
|
||||
})
|
||||
},
|
||||
|
||||
// 获取本月流量使用情况
|
||||
getRemainingFlow: () => {
|
||||
return requestWithRetry({
|
||||
url: '/v1/store/flow-packages/remaining-flow',
|
||||
method: 'GET'
|
||||
})
|
||||
},
|
||||
|
||||
// 创建流量套餐订单
|
||||
createOrder: (packageId) => {
|
||||
return request({
|
||||
url: '/v1/store/flow-packages/order',
|
||||
method: 'POST',
|
||||
data: {
|
||||
packageId
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
48
Store_vue/api/utils/auth.js
Normal file
48
Store_vue/api/utils/auth.js
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* token验证与跳转工具函数
|
||||
*/
|
||||
|
||||
// 检查token是否存在
|
||||
export const hasToken = () => {
|
||||
return !!uni.getStorageSync('token');
|
||||
};
|
||||
|
||||
// 检查token是否过期
|
||||
export const isTokenExpired = () => {
|
||||
const expiredTime = uni.getStorageSync('token_expired');
|
||||
if (!expiredTime) return true;
|
||||
|
||||
// 将当前时间转换为秒级时间戳,确保与expiredTime单位一致
|
||||
const currentTimeInSeconds = Math.floor(Date.now() / 1000);
|
||||
|
||||
return currentTimeInSeconds >= expiredTime;
|
||||
};
|
||||
|
||||
// 检查是否有有效token
|
||||
export const hasValidToken = () => {
|
||||
return hasToken() && !isTokenExpired();
|
||||
};
|
||||
|
||||
// 清除token信息
|
||||
export const clearToken = () => {
|
||||
uni.removeStorageSync('token');
|
||||
uni.removeStorageSync('member');
|
||||
uni.removeStorageSync('token_expired');
|
||||
};
|
||||
|
||||
// 跳转到登录页面
|
||||
export const redirectToLogin = () => {
|
||||
const currentPage = getCurrentPages().pop();
|
||||
if (currentPage && currentPage.route !== 'pages/login/index') {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 跳转到聊天页面
|
||||
export const redirectToChat = () => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/chat/index'
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user