Merge branch 'develop' of https://gitee.com/Tyssen/yi-shi into develop

This commit is contained in:
Ghost
2025-04-01 09:26:36 +08:00
82 changed files with 863 additions and 14335 deletions

View File

@@ -1,3 +1,4 @@
import { api } from "@/lib/api";
import type {
ApiResponse,
Device,
@@ -7,11 +8,52 @@ import type {
QueryDeviceParams,
CreateDeviceParams,
UpdateDeviceParams,
DeviceStatus, // Added DeviceStatus import
DeviceStatus,
ServerDevice,
ServerDevicesResponse
} from "@/types/device"
const API_BASE = "/api/devices"
// 获取设备列表 - 连接到服务器/v1/devices接口
export const fetchDeviceList = async (page: number = 1, limit: number = 20, keyword?: string): Promise<ServerDevicesResponse> => {
const params = new URLSearchParams();
params.append('page', page.toString());
params.append('limit', limit.toString());
if (keyword) {
params.append('keyword', keyword);
}
return api.get<ServerDevicesResponse>(`/v1/devices?${params.toString()}`);
};
// 获取设备详情 - 连接到服务器/v1/devices/:id接口
export const fetchDeviceDetail = async (id: string | number): Promise<ApiResponse<any>> => {
return api.get<ApiResponse<any>>(`/v1/devices/${id}`);
};
// 更新设备任务配置
export const updateDeviceTaskConfig = async (
id: string | number,
config: {
autoAddFriend?: boolean;
autoReply?: boolean;
momentsSync?: boolean;
aiChat?: boolean;
}
): Promise<ApiResponse<any>> => {
return api.post<ApiResponse<any>>(`/v1/devices/task-config`, {
id,
...config
});
};
// 删除设备
export const deleteDevice = async (id: number): Promise<ApiResponse<any>> => {
return api.delete<ApiResponse<any>>(`/v1/devices/${id}`);
};
// 设备管理API
export const deviceApi = {
// 创建设备
@@ -46,12 +88,22 @@ export const deviceApi = {
// 查询设备列表
async query(params: QueryDeviceParams): Promise<ApiResponse<PaginatedResponse<Device>>> {
const queryString = new URLSearchParams({
...params,
tags: params.tags ? JSON.stringify(params.tags) : "",
dateRange: params.dateRange ? JSON.stringify(params.dateRange) : "",
}).toString()
// 创建一个新对象,用于构建URLSearchParams
const queryParams: Record<string, string> = {};
// 按需将params中的属性添加到queryParams
if (params.keyword) queryParams.keyword = params.keyword;
if (params.status) queryParams.status = params.status;
if (params.type) queryParams.type = params.type;
if (params.page) queryParams.page = params.page.toString();
if (params.pageSize) queryParams.pageSize = params.pageSize.toString();
// 特殊处理需要JSON序列化的属性
if (params.tags) queryParams.tags = JSON.stringify(params.tags);
if (params.dateRange) queryParams.dateRange = JSON.stringify(params.dateRange);
// 构建查询字符串
const queryString = new URLSearchParams(queryParams).toString();
const response = await fetch(`${API_BASE}?${queryString}`)
return response.json()
},

View File

@@ -40,6 +40,7 @@ interface AuthContextType {
updateToken: (newToken: string) => void
}
// 创建默认上下文
const AuthContext = createContext<AuthContextType>({
isAuthenticated: false,
token: null,
@@ -56,20 +57,25 @@ interface AuthProviderProps {
}
export function AuthProvider({ children }: AuthProviderProps) {
// 避免在服务端渲染时设置初始状态
const [token, setToken] = useState<string | null>(null)
const [user, setUser] = useState<User | null>(null)
const [isAuthenticated, setIsAuthenticated] = useState(false)
const [isLoading, setIsLoading] = useState(true)
// 初始页面加载时显示为false避免在服务端渲染和客户端水合时不匹配
const [isLoading, setIsLoading] = useState(false)
const [isInitialized, setIsInitialized] = useState(false)
const router = useRouter()
// 检查token有效性并初始化认证状态
// 初始化认证状态
useEffect(() => {
// 仅在客户端执行初始化
setIsLoading(true)
const initAuth = async () => {
setIsLoading(true)
const storedToken = safeLocalStorage.getItem("token")
if (storedToken) {
try {
try {
const storedToken = safeLocalStorage.getItem("token")
if (storedToken) {
// 验证token是否有效
const isValid = await validateToken()
@@ -89,17 +95,18 @@ export function AuthProvider({ children }: AuthProviderProps) {
// token无效清除
handleLogout()
}
} catch (error) {
console.error("验证token时出错:", error)
handleLogout()
}
} catch (error) {
console.error("验证token时出错:", error)
handleLogout()
} finally {
setIsLoading(false)
setIsInitialized(true)
}
setIsLoading(false)
}
initAuth()
}, [])
}, []) // 空依赖数组,仅在组件挂载时执行一次
const handleLogout = () => {
safeLocalStorage.removeItem("token")
@@ -131,7 +138,11 @@ export function AuthProvider({ children }: AuthProviderProps) {
return (
<AuthContext.Provider value={{ isAuthenticated, token, user, login, logout, updateToken }}>
{isLoading ? <div className="flex h-screen w-screen items-center justify-center">...</div> : children}
{isLoading && isInitialized ? (
<div className="flex h-screen w-screen items-center justify-center">...</div>
) : (
children
)}
</AuthContext.Provider>
)
}

View File

@@ -10,6 +10,8 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { ScrollArea } from "@/components/ui/scroll-area"
import { fetchDeviceDetail, updateDeviceTaskConfig } from "@/api/devices"
import { toast } from "sonner"
interface WechatAccount {
id: string
@@ -35,7 +37,7 @@ interface Device {
features: {
autoAddFriend: boolean
autoReply: boolean
contentSync: boolean
momentsSync: boolean
aiChat: boolean
}
history: {
@@ -43,6 +45,21 @@ interface Device {
action: string
operator: string
}[]
totalFriend: number
thirtyDayMsgCount: number
}
// 这个helper函数用于获取Badge变体类型
function getBadgeVariant(status: string): "default" | "destructive" | "outline" | "secondary" {
if (status === "online" || status === "normal") {
return "default"
} else if (status === "abnormal") {
return "destructive"
} else if (status === "enabled") {
return "outline"
} else {
return "secondary"
}
}
export default function DeviceDetailPage() {
@@ -50,64 +67,238 @@ export default function DeviceDetailPage() {
const router = useRouter()
const [device, setDevice] = useState<Device | null>(null)
const [activeTab, setActiveTab] = useState("info")
const [loading, setLoading] = useState(true)
const [savingFeatures, setSavingFeatures] = useState({
autoAddFriend: false,
autoReply: false,
momentsSync: false,
aiChat: false
})
useEffect(() => {
// 模拟API调用
const mockDevice: Device = {
id: params.id as string,
imei: "sd123123",
name: "设备 1",
status: "online",
battery: 85,
lastActive: "2024-02-09 15:30:45",
historicalIds: ["vx412321", "vfbadasd"],
wechatAccounts: [
{
id: "1",
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
nickname: "老张",
wechatId: "wxid_abc123",
gender: "male",
status: "normal",
addFriendStatus: "enabled",
friendCount: 523,
lastActive: "2024-02-09 15:20:33",
},
{
id: "2",
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
nickname: "老李",
wechatId: "wxid_xyz789",
gender: "male",
status: "abnormal",
addFriendStatus: "disabled",
friendCount: 245,
lastActive: "2024-02-09 14:15:22",
},
],
features: {
autoAddFriend: true,
autoReply: true,
contentSync: false,
aiChat: true,
},
history: [
{
time: "2024-02-09 15:30:45",
action: "开启自动加好友",
operator: "系统",
},
{
time: "2024-02-09 14:20:33",
action: "添加微信号",
operator: "管理员",
},
],
if (!params.id) return
const fetchDevice = async () => {
try {
setLoading(true)
const response = await fetchDeviceDetail(params.id as string)
if (response && response.code === 200 && response.data) {
const serverData = response.data
// 构建符合前端期望格式的设备对象
const formattedDevice: Device = {
id: serverData.id?.toString() || "",
imei: serverData.imei || "",
name: serverData.memo || "未命名设备",
status: serverData.alive === 1 ? "online" : "offline",
battery: serverData.battery || 0,
lastActive: serverData.lastUpdateTime || new Date().toISOString(),
historicalIds: [], // 服务端暂无此数据
wechatAccounts: [], // 默认空数组
history: [], // 服务端暂无此数据
features: {
autoAddFriend: false,
autoReply: false,
momentsSync: false,
aiChat: false
},
totalFriend: serverData.totalFriend || 0,
thirtyDayMsgCount: serverData.thirtyDayMsgCount || 0
}
// 解析features
if (serverData.features) {
// 如果后端直接返回了features对象使用它
formattedDevice.features = {
autoAddFriend: Boolean(serverData.features.autoAddFriend),
autoReply: Boolean(serverData.features.autoReply),
momentsSync: Boolean(serverData.features.momentsSync || serverData.features.contentSync),
aiChat: Boolean(serverData.features.aiChat)
}
} else if (serverData.taskConfig) {
try {
// 解析taskConfig字段
let taskConfig = serverData.taskConfig
if (typeof taskConfig === 'string') {
taskConfig = JSON.parse(taskConfig)
}
if (taskConfig) {
console.log('解析的taskConfig:', taskConfig);
formattedDevice.features = {
autoAddFriend: Boolean(taskConfig.autoAddFriend),
autoReply: Boolean(taskConfig.autoReply),
momentsSync: Boolean(taskConfig.momentsSync),
aiChat: Boolean(taskConfig.aiChat)
}
}
} catch (err) {
console.error('解析taskConfig失败:', err)
}
}
// 如果有微信账号信息,构建微信账号对象
if (serverData.wechatId) {
formattedDevice.wechatAccounts = [
{
id: serverData.wechatId?.toString() || "1",
avatar: "/placeholder.svg", // 默认头像
nickname: serverData.memo || "微信账号",
wechatId: serverData.imei || "",
gender: "male", // 默认性别
status: serverData.alive === 1 ? "normal" : "abnormal",
addFriendStatus: "enabled",
friendCount: serverData.totalFriend || 0,
lastActive: serverData.lastUpdateTime || new Date().toISOString()
}
]
}
setDevice(formattedDevice)
} else {
// 如果API返回错误则使用备用模拟数据
toast.error("获取设备信息失败,显示备用数据")
fallbackToMockDevice()
}
} catch (error) {
console.error("获取设备信息失败:", error)
toast.error("获取设备信息出错,显示备用数据")
fallbackToMockDevice()
} finally {
setLoading(false)
}
}
setDevice(mockDevice)
const fallbackToMockDevice = () => {
const mockDevice: Device = {
id: params.id as string,
imei: "sd123123",
name: "设备 1",
status: "online",
battery: 85,
lastActive: "2024-02-09 15:30:45",
historicalIds: ["vx412321", "vfbadasd"],
wechatAccounts: [
{
id: "1",
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
nickname: "老张",
wechatId: "wxid_abc123",
gender: "male",
status: "normal",
addFriendStatus: "enabled",
friendCount: 523,
lastActive: "2024-02-09 15:20:33",
},
{
id: "2",
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-q2rVrFbfDdAbSnT3ZTNE7gfn3QCbvr.png",
nickname: "老李",
wechatId: "wxid_xyz789",
gender: "male",
status: "abnormal",
addFriendStatus: "disabled",
friendCount: 245,
lastActive: "2024-02-09 14:15:22",
},
],
features: {
autoAddFriend: true,
autoReply: true,
momentsSync: false,
aiChat: true,
},
history: [
{
time: "2024-02-09 15:30:45",
action: "开启自动加好友",
operator: "系统",
},
{
time: "2024-02-09 14:20:33",
action: "添加微信号",
operator: "管理员",
},
],
totalFriend: 768,
thirtyDayMsgCount: 5678
}
setDevice(mockDevice)
}
fetchDevice()
}, [params.id])
if (!device) {
// 处理功能开关状态变化
const handleFeatureChange = async (feature: keyof Device['features'], checked: boolean) => {
if (!device) return
// 避免已经在处理中的功能被重复触发
if (savingFeatures[feature]) {
return
}
setSavingFeatures(prev => ({ ...prev, [feature]: true }))
try {
// 准备更新后的功能状态
const updatedFeatures = { ...device.features, [feature]: checked }
// 创建API请求参数
const configUpdate = { [feature]: checked }
// 立即更新UI状态提供即时反馈
setDevice(prev => prev ? {
...prev,
features: updatedFeatures
} : null)
// 调用API更新服务器配置
const response = await updateDeviceTaskConfig(device.id, configUpdate)
if (response && response.code === 200) {
toast.success(`${getFeatureName(feature)}${checked ? '已启用' : '已禁用'}`)
} else {
// 如果请求失败回滚UI变更
setDevice(prev => prev ? {
...prev,
features: { ...prev.features, [feature]: !checked }
} : null)
// 处理错误信息,使用类型断言解决字段不一致问题
const anyResponse = response as any;
const errorMsg = anyResponse ? (anyResponse.message || anyResponse.msg || '未知错误') : '未知错误';
toast.error(`更新失败: ${errorMsg}`)
}
} catch (error) {
console.error(`更新${getFeatureName(feature)}失败:`, error)
// 异常情况下也回滚UI变更
setDevice(prev => prev ? {
...prev,
features: { ...prev.features, [feature]: !checked }
} : null)
toast.error('更新失败,请稍后重试')
} finally {
setSavingFeatures(prev => ({ ...prev, [feature]: false }))
}
}
// 获取功能中文名称
const getFeatureName = (feature: string): string => {
const nameMap: Record<string, string> = {
autoAddFriend: '自动加好友',
autoReply: '自动回复',
momentsSync: '朋友圈同步',
aiChat: 'AI会话'
}
return nameMap[feature] || feature
}
if (loading || !device) {
return <div>...</div>
}
@@ -137,12 +328,14 @@ export default function DeviceDetailPage() {
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<h2 className="font-medium truncate">{device.name}</h2>
<Badge variant={device.status === "online" ? "success" : "secondary"}>
<Badge variant={getBadgeVariant(device.status)}>
{device.status === "online" ? "在线" : "离线"}
</Badge>
</div>
<div className="text-sm text-gray-500 mt-1">IMEI: {device.imei}</div>
<div className="text-sm text-gray-500">ID: {device.historicalIds.join(", ")}</div>
{device.historicalIds && device.historicalIds.length > 0 && (
<div className="text-sm text-gray-500">ID: {device.historicalIds.join(", ")}</div>
)}
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-4">
@@ -173,28 +366,68 @@ export default function DeviceDetailPage() {
<Label></Label>
<div className="text-sm text-gray-500"></div>
</div>
<Switch checked={device.features.autoAddFriend} />
<div className="flex items-center">
{savingFeatures.autoAddFriend && (
<div className="w-4 h-4 mr-2 rounded-full border-2 border-blue-500 border-t-transparent animate-spin"></div>
)}
<Switch
checked={Boolean(device.features.autoAddFriend)}
onCheckedChange={(checked) => handleFeatureChange('autoAddFriend', checked)}
disabled={savingFeatures.autoAddFriend}
className="data-[state=checked]:bg-blue-500 transition-all duration-200"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label></Label>
<div className="text-sm text-gray-500"></div>
</div>
<Switch checked={device.features.autoReply} />
<div className="flex items-center">
{savingFeatures.autoReply && (
<div className="w-4 h-4 mr-2 rounded-full border-2 border-blue-500 border-t-transparent animate-spin"></div>
)}
<Switch
checked={Boolean(device.features.autoReply)}
onCheckedChange={(checked) => handleFeatureChange('autoReply', checked)}
disabled={savingFeatures.autoReply}
className="data-[state=checked]:bg-blue-500 transition-all duration-200"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label></Label>
<div className="text-sm text-gray-500"></div>
</div>
<Switch checked={device.features.contentSync} />
<div className="flex items-center">
{savingFeatures.momentsSync && (
<div className="w-4 h-4 mr-2 rounded-full border-2 border-blue-500 border-t-transparent animate-spin"></div>
)}
<Switch
checked={Boolean(device.features.momentsSync)}
onCheckedChange={(checked) => handleFeatureChange('momentsSync', checked)}
disabled={savingFeatures.momentsSync}
className="data-[state=checked]:bg-blue-500 transition-all duration-200"
/>
</div>
</div>
<div className="flex items-center justify-between">
<div className="space-y-0.5">
<Label>AI会话</Label>
<div className="text-sm text-gray-500">AI智能对话</div>
</div>
<Switch checked={device.features.aiChat} />
<div className="flex items-center">
{savingFeatures.aiChat && (
<div className="w-4 h-4 mr-2 rounded-full border-2 border-blue-500 border-t-transparent animate-spin"></div>
)}
<Switch
checked={Boolean(device.features.aiChat)}
onCheckedChange={(checked) => handleFeatureChange('aiChat', checked)}
disabled={savingFeatures.aiChat}
className="data-[state=checked]:bg-blue-500 transition-all duration-200"
/>
</div>
</div>
</div>
</Card>
@@ -203,33 +436,39 @@ export default function DeviceDetailPage() {
<TabsContent value="accounts">
<Card className="p-4">
<ScrollArea className="h-[calc(100vh-300px)]">
<div className="space-y-4">
{device.wechatAccounts.map((account) => (
<div key={account.id} className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
<img
src={account.avatar || "/placeholder.svg"}
alt={account.nickname}
className="w-12 h-12 rounded-full"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div className="font-medium truncate">{account.nickname}</div>
<Badge variant={account.status === "normal" ? "success" : "destructive"}>
{account.status === "normal" ? "正常" : "异常"}
</Badge>
</div>
<div className="text-sm text-gray-500 mt-1">: {account.wechatId}</div>
<div className="text-sm text-gray-500">: {account.gender === "male" ? "男" : "女"}</div>
<div className="flex items-center justify-between mt-2">
<span className="text-sm text-gray-500">: {account.friendCount}</span>
<Badge variant={account.addFriendStatus === "enabled" ? "outline" : "secondary"}>
{account.addFriendStatus === "enabled" ? "可加友" : "已停用"}
</Badge>
{device.wechatAccounts && device.wechatAccounts.length > 0 ? (
<div className="space-y-4">
{device.wechatAccounts.map((account) => (
<div key={account.id} className="flex items-start space-x-3 p-3 bg-gray-50 rounded-lg">
<img
src={account.avatar || "/placeholder.svg"}
alt={account.nickname}
className="w-12 h-12 rounded-full"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between">
<div className="font-medium truncate">{account.nickname}</div>
<Badge variant={getBadgeVariant(account.status)}>
{account.status === "normal" ? "正常" : "异常"}
</Badge>
</div>
<div className="text-sm text-gray-500 mt-1">: {account.wechatId}</div>
<div className="text-sm text-gray-500">: {account.gender === "male" ? "男" : "女"}</div>
<div className="flex items-center justify-between mt-2">
<span className="text-sm text-gray-500">: {account.friendCount}</span>
<Badge variant={getBadgeVariant(account.addFriendStatus)}>
{account.addFriendStatus === "enabled" ? "可加友" : "已停用"}
</Badge>
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p></p>
</div>
)}
</ScrollArea>
</Card>
</TabsContent>
@@ -237,21 +476,27 @@ export default function DeviceDetailPage() {
<TabsContent value="history">
<Card className="p-4">
<ScrollArea className="h-[calc(100vh-300px)]">
<div className="space-y-4">
{device.history.map((record, index) => (
<div key={index} className="flex items-start space-x-3">
<div className="p-2 bg-blue-50 rounded-full">
<History className="w-4 h-4 text-blue-600" />
</div>
<div className="flex-1">
<div className="text-sm font-medium">{record.action}</div>
<div className="text-xs text-gray-500 mt-1">
: {record.operator} · {record.time}
{device.history && device.history.length > 0 ? (
<div className="space-y-4">
{device.history.map((record, index) => (
<div key={index} className="flex items-start space-x-3">
<div className="p-2 bg-blue-50 rounded-full">
<History className="w-4 h-4 text-blue-600" />
</div>
<div className="flex-1">
<div className="text-sm font-medium">{record.action}</div>
<div className="text-xs text-gray-500 mt-1">
: {record.operator} · {record.time}
</div>
</div>
</div>
</div>
))}
</div>
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p></p>
</div>
)}
</ScrollArea>
</Card>
</TabsContent>
@@ -264,7 +509,7 @@ export default function DeviceDetailPage() {
<span className="text-sm"></span>
</div>
<div className="text-2xl font-bold text-blue-600 mt-2">
{device?.wechatAccounts?.reduce((sum, account) => sum + account.friendCount, 0)}
{device.totalFriend || 0}
</div>
</Card>
<Card className="p-4">
@@ -272,7 +517,9 @@ export default function DeviceDetailPage() {
<MessageCircle className="w-4 h-4" />
<span className="text-sm"></span>
</div>
<div className="text-2xl font-bold text-blue-600 mt-2">5,678</div>
<div className="text-2xl font-bold text-blue-600 mt-2">
{device.thirtyDayMsgCount || 0}
</div>
</Card>
</div>
</div>

View File

@@ -1,6 +1,6 @@
"use client"
import { useState, useEffect } from "react"
import { useState, useEffect, useRef, useCallback } from "react"
import { useRouter } from "next/navigation"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@@ -11,21 +11,12 @@ import { Checkbox } from "@/components/ui/checkbox"
import { toast } from "@/components/ui/use-toast"
import { Badge } from "@/components/ui/badge"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { fetchDeviceList, deleteDevice } from "@/api/devices"
import { ServerDevice } from "@/types/device"
interface Device {
id: string
imei: string
name: string
remark: string
status: "online" | "offline"
battery: number
wechatId: string
friendCount: number
todayAdded: number
messageCount: number
lastActive: string
addFriendStatus: "normal" | "abnormal"
avatar?: string
// 设备接口更新为与服务端接口对应的类型
interface Device extends ServerDevice {
status: "online" | "offline";
}
export default function DevicesPage() {
@@ -33,58 +24,158 @@ export default function DevicesPage() {
const [devices, setDevices] = useState<Device[]>([])
const [isAddDeviceOpen, setIsAddDeviceOpen] = useState(false)
const [stats, setStats] = useState({
totalDevices: 42,
onlineDevices: 35,
totalDevices: 0,
onlineDevices: 0,
})
const [searchQuery, setSearchQuery] = useState("")
const [statusFilter, setStatusFilter] = useState("all")
const [currentPage, setCurrentPage] = useState(1)
const [selectedDevices, setSelectedDevices] = useState<string[]>([])
const devicesPerPage = 10
const [selectedDevices, setSelectedDevices] = useState<number[]>([])
const [isLoading, setIsLoading] = useState(false)
const [hasMore, setHasMore] = useState(true)
const [totalCount, setTotalCount] = useState(0)
const observerTarget = useRef<HTMLDivElement>(null)
// 使用ref来追踪当前页码避免依赖effect循环
const pageRef = useRef(1)
const devicesPerPage = 20 // 每页显示20条记录
// 获取设备列表
const loadDevices = useCallback(async (page: number, refresh: boolean = false) => {
// 检查是否已经在加载中,避免重复请求
if (isLoading) return;
try {
setIsLoading(true)
const response = await fetchDeviceList(page, devicesPerPage, searchQuery)
if (response.code === 200 && response.data) {
// 转换数据格式确保status类型正确
const serverDevices = response.data.list.map(device => ({
...device,
status: device.alive === 1 ? "online" as const : "offline" as const
}))
// 更新设备列表
if (refresh) {
setDevices(serverDevices)
} else {
setDevices(prev => [...prev, ...serverDevices])
}
// 更新统计信息
const total = response.data.total
const online = response.data.list.filter(d => d.alive === 1).length
setStats({
totalDevices: total,
onlineDevices: online
})
// 更新分页信息
setTotalCount(response.data.total)
// 更新hasMore状态确保有更多数据且返回的数据数量等于每页数量
const hasMoreData = serverDevices.length > 0 &&
serverDevices.length === devicesPerPage &&
(page * devicesPerPage) < response.data.total;
setHasMore(hasMoreData)
// 更新当前页码的ref值
pageRef.current = page
} else {
toast({
title: "获取设备列表失败",
description: response.msg || "请稍后重试",
variant: "destructive",
})
}
} catch (error) {
console.error("获取设备列表失败", error)
toast({
title: "获取设备列表失败",
description: "请检查网络连接后重试",
variant: "destructive",
})
} finally {
setIsLoading(false)
}
// 移除isLoading依赖只保留真正需要的依赖
}, [searchQuery, devicesPerPage])
// 加载下一页数据的函数使用ref来追踪页码避免依赖循环
const loadNextPage = useCallback(() => {
// 如果正在加载或者没有更多数据,直接返回
if (isLoading || !hasMore) return;
// 使用ref来获取下一页码避免依赖currentPage
const nextPage = pageRef.current + 1;
// 设置UI显示的当前页
setCurrentPage(nextPage);
// 加载下一页数据
loadDevices(nextPage, false);
// 只依赖必要的状态
}, [hasMore, isLoading, loadDevices]);
// 初始加载和搜索时刷新列表
useEffect(() => {
// 模拟API调用
const fetchDevices = async () => {
const mockDevices = Array.from({ length: 42 }, (_, i) => ({
id: `device-${i + 1}`,
imei: `sd${123123 + i}`,
name: `设备 ${i + 1}`,
remark: `备注 ${i + 1}`,
status: Math.random() > 0.2 ? "online" : "offline",
battery: Math.floor(Math.random() * 100),
wechatId: `wxid_${Math.random().toString(36).substr(2, 8)}`,
friendCount: Math.floor(Math.random() * 1000),
todayAdded: Math.floor(Math.random() * 50),
messageCount: Math.floor(Math.random() * 200),
lastActive: new Date(Date.now() - Math.random() * 86400000).toLocaleString(),
addFriendStatus: Math.random() > 0.2 ? "normal" : "abnormal",
avatar: "https://hebbkx1anhila5yf.public.blob.vercel-storage.com/image-kYhfQsrrByfbzefv6MEV7W7ogz0IRt.png",
}))
setDevices(mockDevices)
// 重置页码
setCurrentPage(1)
pageRef.current = 1
// 加载第一页数据
loadDevices(1, true)
}, [searchQuery, loadDevices])
// 无限滚动加载实现
useEffect(() => {
// 如果没有更多数据或者正在加载不创建observer
if (!hasMore || isLoading) return;
let isMounted = true; // 追踪组件是否已挂载
// 创建观察器观察加载点
const observer = new IntersectionObserver(
entries => {
// 如果交叉了,且有更多数据,且当前不在加载状态,且组件仍然挂载
if (entries[0].isIntersecting && hasMore && !isLoading && isMounted) {
loadNextPage();
}
},
{ threshold: 0.5 }
)
// 只在客户端时观察节点
if (typeof window !== 'undefined' && observerTarget.current) {
observer.observe(observerTarget.current)
}
fetchDevices()
}, [])
// 清理观察器
return () => {
isMounted = false;
observer.disconnect();
}
}, [hasMore, isLoading, loadNextPage])
// 刷新设备列表
const handleRefresh = () => {
setCurrentPage(1)
pageRef.current = 1
loadDevices(1, true)
toast({
title: "刷新成功",
description: "设备列表已更新",
})
}
const filteredDevices = devices.filter((device) => {
const matchesSearch =
device.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.imei.toLowerCase().includes(searchQuery.toLowerCase()) ||
device.wechatId.toLowerCase().includes(searchQuery.toLowerCase())
const matchesStatus = statusFilter === "all" || device.status === statusFilter
return matchesSearch && matchesStatus
// 筛选设备
const filteredDevices = devices.filter(device => {
const matchesStatus = statusFilter === "all" ||
(statusFilter === "online" && device.alive === 1) ||
(statusFilter === "offline" && device.alive === 0)
return matchesStatus
})
const paginatedDevices = filteredDevices.slice((currentPage - 1) * devicesPerPage, currentPage * devicesPerPage)
const handleBatchDelete = () => {
// 处理批量删除
const handleBatchDelete = async () => {
if (selectedDevices.length === 0) {
toast({
title: "请选择设备",
@@ -93,14 +184,40 @@ export default function DevicesPage() {
})
return
}
toast({
title: "批量删除成功",
description: `已删除 ${selectedDevices.length} 个设备`,
})
setSelectedDevices([])
// 这里需要实现批量删除逻辑
// 目前只是单个删除的循环
let successCount = 0
for (const deviceId of selectedDevices) {
try {
const response = await deleteDevice(deviceId)
if (response.code === 200) {
successCount++
}
} catch (error) {
console.error(`删除设备 ${deviceId} 失败`, error)
}
}
// 删除后刷新列表
if (successCount > 0) {
toast({
title: "批量删除成功",
description: `已删除 ${successCount} 个设备`,
})
setSelectedDevices([])
handleRefresh()
} else {
toast({
title: "批量删除失败",
description: "请稍后重试",
variant: "destructive",
})
}
}
const handleDeviceClick = (deviceId: string) => {
// 设备详情页跳转
const handleDeviceClick = (deviceId: number) => {
router.push(`/devices/${deviceId}`)
}
@@ -167,10 +284,10 @@ export default function DevicesPage() {
</Select>
<div className="flex items-center space-x-2">
<Checkbox
checked={selectedDevices.length === paginatedDevices.length}
checked={selectedDevices.length === filteredDevices.length && filteredDevices.length > 0}
onCheckedChange={(checked) => {
if (checked) {
setSelectedDevices(paginatedDevices.map((d) => d.id))
setSelectedDevices(filteredDevices.map((d) => d.id))
} else {
setSelectedDevices([])
}
@@ -190,7 +307,7 @@ export default function DevicesPage() {
</div>
<div className="space-y-2">
{paginatedDevices.map((device) => (
{filteredDevices.map((device) => (
<Card
key={device.id}
className="p-3 hover:shadow-md transition-shadow cursor-pointer relative"
@@ -210,70 +327,52 @@ export default function DevicesPage() {
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<div className="font-medium truncate">{device.name}</div>
<Badge variant={device.status === "online" ? "success" : "secondary"} className="ml-2">
<div className="font-medium truncate">{device.memo}</div>
<Badge variant={device.status === "online" ? "default" : "secondary"} className="ml-2">
{device.status === "online" ? "在线" : "离线"}
</Badge>
</div>
<div className="text-sm text-gray-500">IMEI: {device.imei}</div>
<div className="text-sm text-gray-500">: {device.wechatId}</div>
<div className="text-sm text-gray-500">: {device.wechatId || "未绑定"}</div>
<div className="flex items-center justify-between mt-1 text-sm">
<span className="text-gray-500">: {device.friendCount}</span>
<span className="text-gray-500">: +{device.todayAdded}</span>
<span className="text-gray-500">: {device.totalFriend}</span>
</div>
</div>
</div>
</Card>
))}
</div>
<div className="flex justify-between items-center pt-2">
<Button
variant="outline"
size="sm"
onClick={() => setCurrentPage((prev) => Math.max(1, prev - 1))}
disabled={currentPage === 1}
>
</Button>
<span className="text-sm text-gray-500">
{currentPage} / {Math.ceil(filteredDevices.length / devicesPerPage)}
</span>
<Button
variant="outline"
size="sm"
onClick={() =>
setCurrentPage((prev) => Math.min(Math.ceil(filteredDevices.length / devicesPerPage), prev + 1))
}
disabled={currentPage === Math.ceil(filteredDevices.length / devicesPerPage)}
>
</Button>
{/* 加载更多观察点 */}
<div ref={observerTarget} className="h-10 flex items-center justify-center">
{isLoading && <div className="text-sm text-gray-500">...</div>}
{!hasMore && devices.length > 0 && <div className="text-sm text-gray-500"></div>}
{!hasMore && devices.length === 0 && <div className="text-sm text-gray-500"></div>}
</div>
</div>
</div>
</Card>
</div>
{/* 添加设备对话框 */}
<Dialog open={isAddDeviceOpen} onOpenChange={setIsAddDeviceOpen}>
<DialogContent className="sm:max-w-[390px]">
<DialogContent>
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div className="flex flex-col items-center justify-center p-6 space-y-6">
<div className="w-48 h-48 bg-gray-100 rounded-lg flex items-center justify-center">
<QrCode className="w-12 h-12 text-gray-400" />
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium"></label>
<Input placeholder="请输入设备名称" />
</div>
<p className="text-sm text-gray-500 text-center">
使
<br />
ID
</p>
<Input placeholder="请输入设备ID" className="max-w-[280px]" />
<div className="flex space-x-2">
<div className="space-y-2">
<label className="text-sm font-medium">IMEI</label>
<Input placeholder="请输入设备IMEI" />
</div>
<div className="flex justify-end space-x-2">
<Button variant="outline" onClick={() => setIsAddDeviceOpen(false)}>
</Button>
<Button></Button>
<Button></Button>
</div>
</div>
</DialogContent>

View File

@@ -9,7 +9,7 @@ import LayoutWrapper from "./components/LayoutWrapper"
export const metadata: Metadata = {
title: "存客宝",
description: "智能客户管理系统",
generator: 'v0.dev'
generator: 'v0.dev'
}
export default function RootLayout({
@@ -18,7 +18,7 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<html lang="zh-CN" suppressHydrationWarning>
<body className="bg-gray-100">
<AuthProvider>
<ErrorBoundary>

View File

@@ -1,6 +1,6 @@
"use client"
import { useState } from "react"
import { useState, useEffect } from "react"
import { ChevronRight, Settings, Bell, LogOut } from "lucide-react"
import { Card } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
@@ -8,6 +8,8 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { useRouter } from "next/navigation"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useAuth } from "@/app/components/AuthProvider"
import ClientOnly from "@/components/ClientOnly"
import { getClientRandomId } from "@/lib/utils"
const menuItems = [
{ href: "/devices", label: "设备管理" },
@@ -20,7 +22,13 @@ export default function ProfilePage() {
const router = useRouter()
const { isAuthenticated, user, logout } = useAuth()
const [showLogoutDialog, setShowLogoutDialog] = useState(false)
const [accountId] = useState(() => user?.account || Math.floor(10000000 + Math.random() * 90000000).toString())
// 处理身份验证状态将路由重定向逻辑移至useEffect
useEffect(() => {
if (!isAuthenticated) {
router.push("/login")
}
}, [isAuthenticated, router])
const handleLogout = () => {
logout() // 使用AuthProvider中的logout方法删除本地保存的用户信息
@@ -28,11 +36,6 @@ export default function ProfilePage() {
router.push("/login")
}
if (!isAuthenticated) {
router.push("/login")
return null
}
return (
<div className="flex-1 bg-gradient-to-b from-blue-50 to-white pb-16">
<header className="sticky top-0 z-10 bg-white/80 backdrop-blur-sm border-b">
@@ -54,12 +57,16 @@ export default function ProfilePage() {
<Card className="p-6">
<div className="flex items-center space-x-4">
<Avatar className="w-20 h-20">
<AvatarImage src={user?.avatar || "https://images.unsplash.com/photo-1568602471122-7832951cc4c5?w=400&h=400&auto=format&fit=crop"} />
<AvatarFallback>{user?.username?.slice(0, 2) || "KR"}</AvatarFallback>
<AvatarImage src={user?.avatar || ""} />
<AvatarFallback>{user?.username ? user.username.slice(0, 2) : "用户"}</AvatarFallback>
</Avatar>
<div className="flex-1">
<h2 className="text-xl font-semibold text-blue-600">{user?.username || "用户"}</h2>
<p className="text-gray-500">: {user?.account || accountId}</p>
<p className="text-gray-500">
: <ClientOnly fallback="加载中...">
{user?.account || Math.floor(10000000 + Math.random() * 90000000).toString()}
</ClientOnly>
</p>
<div className="mt-2">
<Button variant="outline" size="sm">
@@ -78,7 +85,6 @@ export default function ProfilePage() {
onClick={() => (item.href ? router.push(item.href) : null)}
>
<div className="flex items-center">
{item.icon && <span className="mr-2">{item.icon}</span>}
<span>{item.label}</span>
</div>
<ChevronRight className="w-5 h-5 text-gray-400" />

View File

@@ -0,0 +1,18 @@
"use client"
import { useState, useEffect, type ReactNode } from 'react'
/**
* ClientOnly组件
* 该组件专门用于包装那些只能在客户端渲染的内容,避免水合不匹配错误
* 例如使用了Math.random()、Date.now()或window对象的组件
*/
export default function ClientOnly({ children, fallback = null }: { children: ReactNode, fallback?: ReactNode }) {
const [isClient, setIsClient] = useState(false)
useEffect(() => {
setIsClient(true)
}, [])
return isClient ? <>{children}</> : <>{fallback}</>
}

View File

@@ -97,6 +97,11 @@ export const loginApi = {
// 验证 Token 是否有效
export const validateToken = async (): Promise<boolean> => {
// 如果在服务端直接返回false避免在服务端发起不必要的请求
if (typeof window === 'undefined') {
return false;
}
try {
const response = await loginApi.getUserInfo();
return response.code === 200;
@@ -107,6 +112,11 @@ export const validateToken = async (): Promise<boolean> => {
// 刷新令牌
export const refreshAuthToken = async (): Promise<boolean> => {
// 如果在服务端直接返回false
if (typeof window === 'undefined') {
return false;
}
try {
const response = await loginApi.refreshToken();
if (response.code === 200 && response.data?.token) {

View File

@@ -7,8 +7,11 @@ export const handleTokenExpired = () => {
localStorage.removeItem('token');
localStorage.removeItem('user');
// 跳转到登录页
window.location.href = '/login';
// 使用客户端导航而不是直接修改window.location
// 避免在服务端渲染时执行
setTimeout(() => {
window.location.href = '/login';
}, 0);
}
};
@@ -16,7 +19,10 @@ export const handleTokenExpired = () => {
export const handleApiResponse = <T>(response: Response, result: any): T => {
// 处理token过期情况
if (result && (result.code === 401 || result.msg?.includes('token'))) {
handleTokenExpired();
// 仅在客户端处理token过期
if (typeof window !== 'undefined') {
handleTokenExpired();
}
throw new Error(result.msg || '登录已过期,请重新登录');
}
@@ -30,7 +36,10 @@ export const handleApiError = (error: unknown): never => {
if (error instanceof Error) {
// 如果是未授权错误可能是token过期
if (error.message.includes('401') || error.message.includes('token') || error.message.includes('授权')) {
handleTokenExpired();
// 仅在客户端处理token过期
if (typeof window !== 'undefined') {
handleTokenExpired();
}
}
throw error;
}

View File

@@ -5,3 +5,31 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// 用于在客户端和服务器端获取一致的日期格式
// 避免因为时区差异导致的水合不匹配
export function getFormattedDate(date: Date | string | number, format: Intl.DateTimeFormatOptions = {}) {
// 使用传入的日期创建一个新的日期对象
const dateObj = new Date(date);
// 使用en-US区域设置创建字符串确保客户端和服务器端一致
return dateObj.toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
...format
});
}
// 安全的客户端检查
export function isClient() {
return typeof window !== 'undefined';
}
// 防止在服务端使用Math.random导致水合不匹配
export function getClientRandomId(prefix = '') {
if (isClient()) {
return `${prefix}${Math.random().toString(36).substring(2, 9)}`;
}
return `${prefix}placeholder`;
}

View File

@@ -12,6 +12,28 @@ export enum DeviceType {
IOS = "ios",
}
// 服务端API返回的设备类型
export interface ServerDevice {
id: number;
imei: string;
memo: string;
wechatId: string;
alive: number;
totalFriend: number;
}
// 服务端API返回的设备列表响应
export interface ServerDevicesResponse {
code: number;
msg: string;
data: {
list: ServerDevice[];
total: number;
page: number;
limit: number;
};
}
// 设备基础信息
export interface Device {
id: string

View File

@@ -1,3 +0,0 @@
# 生产环境配置
VUE_APP_BASE_API = 'https://api.cunkebao.com'
VUE_APP_ENV = 'production'

View File

@@ -1,2 +0,0 @@
node_modules
.env

View File

@@ -1,69 +0,0 @@
<script>
export default {
onLaunch: function() {
console.log('App Launch');
// 注释掉登录状态检查,取消登录拦截
// this.checkLoginStatus();
// 设置全局主题色
uni.$u.setConfig({
// 修改uView默认主题色为我们的主题色
config: {
color: {
"u-primary": "#2563eb",
"u-warning": "#ff9900",
"u-success": "#07c160",
"u-error": "#fa5151",
"u-info": "#909399"
}
}
})
},
onShow: function() {
console.log('App Show');
},
onHide: function() {
console.log('App Hide');
},
methods: {
// 检查登录状态(已禁用)
checkLoginStatus() {
// 从本地获取token
const token = uni.getStorageSync('token');
if (!token) {
// 如果没有token跳转到登录页
uni.reLaunch({
url: '/pages/login/index'
});
}
}
}
}
</script>
<style lang="scss">
/* 导入全局样式变量 */
@import "./uni.scss";
/* 导入字体 */
@import "./static/fonts/fonts.css";
/* 全局样式 */
page {
background-color: $bg-color;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica,
Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB',
'Microsoft Yahei', sans-serif;
font-size: $font-size-base;
color: $text-color-main;
line-height: 1.8;
}
/* 统一边距 */
.container {
padding: $spacing-base;
}
/* 引入uView基础样式 */
@import "uview-ui/index.scss";
</style>

View File

@@ -1,86 +0,0 @@
# 存客宝 UniApp
基于uni-app框架开发的存客宝移动端应用支持H5、微信小程序、App等多端部署。
## 项目结构
```
├── api # API接口目录
├── components # 自定义组件
├── pages # 页面文件目录
│ ├── index # 首页
│ ├── login # 登录页面
│ └── agreement # 协议页面
├── static # 静态资源
│ ├── images # 图片
│ └── icons # 图标
├── store # Vuex状态管理
├── utils # 工具函数
│ ├── auth.js # 认证相关工具函数
│ ├── common.js # 通用工具函数
│ └── request.js # 请求工具函数
├── App.vue # 应用配置用来配置App全局样式以及监听应用生命周期
├── main.js # Vue初始化入口文件
├── manifest.json # 配置应用名称、appid、logo、版本等打包信息
├── pages.json # 配置页面路由、导航条、选项卡等页面类信息
└── uni.scss # 全局样式变量
```
## 功能特性
- **支持多种登录方式**手机号验证码登录、密码登录、微信授权登录、Apple登录
- **完整的token认证机制**JWT令牌管理自动刷新token过期处理
- **统一的网络请求封装**:请求拦截器,响应拦截器,错误处理
- **多端适配**一套代码同时支持H5、微信小程序、App
- **UI框架集成**基于uView 2.x UI框架提供丰富的组件和样式
- **主题定制**:全局样式变量,支持自定义主题
## 开发环境
- **Node.js**: v14.x及以上
- **HBuilderX**: 3.x及以上版本
### 安装依赖
1. 使用HBuilderX打开项目
2. 点击菜单栏 "工具 -> 插件安装",安装"scss/sass编译"插件
3. 点击菜单栏 "工具 -> 插件安装",安装"uView-UI"插件
## 运行和发布
### 运行到浏览器
1. 在HBuilderX中点击"运行 -> 运行到浏览器"
2. 选择浏览器如Chrome
### 运行到微信小程序
1. 在HBuilderX中点击"运行 -> 运行到小程序模拟器 -> 微信开发者工具"
2. 确保已安装并配置了微信开发者工具
### 发布为H5
1. 在HBuilderX中点击"发行 -> 网站H5发布"
2. 配置发布信息,点击发布
### 发布为微信小程序
1. 在HBuilderX中点击"发行 -> 小程序发布 -> 微信小程序"
2. 配置小程序AppID等信息点击发布
### 发布为App
1. 在HBuilderX中点击"发行 -> 原生App-云打包"
2. 配置证书等信息选择打包平台Android/iOS点击发布
## 技术栈
- **uni-app**:跨平台前端框架
- **Vue.js**:前端框架
- **Vuex**:状态管理
- **uView UI**UI组件库
- **SCSS**CSS预处理器
## License
MIT

View File

@@ -1,84 +0,0 @@
import request from '@/utils/request'
/**
* 获取设备列表
* @param {Object} params 查询参数
* @param {string} params.keyword 关键词搜索同时搜索IMEI和备注
* @param {number} params.alive 设备在线状态可选1:在线 0:离线)
* @param {number} params.page 页码
* @param {number} params.limit 每页数量
* @returns {Promise} 设备列表
*
* 注意: params 参数会被自动添加到URL查询字符串中如 /v1/devices?keyword=xxx&alive=1&page=1&limit=20
*/
export function getDeviceList(params) {
return request({
url: '/v1/devices',
method: 'GET',
params
})
}
/**
* 获取设备总数
* @param {Object} params 查询参数
* @param {number} params.alive 设备在线状态可选1:在线 0:离线)
* @returns {Promise} 设备总数
*/
export function getDeviceCount(params) {
return request({
url: '/v1/devices/count',
method: 'GET',
params
})
}
/**
* 获取设备详情
* @param {number} id 设备ID
* @returns {Promise} 设备详情
*/
export function getDeviceDetail(id) {
return request({
url: `/v1/devices/${id}`,
method: 'GET'
})
}
/**
* 删除设备
* @param {number} id 设备ID
* @returns {Promise} 删除结果
*/
export function deleteDevice(id) {
return request({
url: `/v1/devices/${id}`,
method: 'DELETE'
})
}
/**
* 刷新设备状态
* @returns {Promise} 刷新结果
*/
export function refreshDevices() {
return request({
url: '/v1/devices/refresh',
method: 'PUT'
})
}
/**
* 添加设备
* @param {Object} data 设备数据
* @param {string} data.imei 设备IMEI
* @param {string} data.memo 设备备注
* @returns {Promise} 添加结果
*/
export function addDevice(data) {
return request({
url: '/v1/devices',
method: 'POST',
data
})
}

View File

@@ -1,108 +0,0 @@
import request from '@/utils/request'
/**
* 用户登录
* @param {Object} data 登录数据
* @param {string} data.account 账号(手机号)
* @param {string} data.password 密码
* @param {number} data.typeId 用户类型
* @returns {Promise} 登录结果
*/
export function login(data) {
return request({
url: '/v1/auth/login',
method: 'POST',
data
})
}
/**
* 手机号验证码登录
* @param {Object} data 登录数据
* @param {string} data.account 手机号
* @param {string} data.code 验证码
* @param {number} data.typeId 用户类型
* @returns {Promise} 登录结果
*/
export function mobileLogin(data) {
return request({
url: '/v1/auth/mobile-login',
method: 'POST',
data
})
}
/**
* 发送验证码
* @param {Object} data 数据
* @param {string} data.account 手机号
* @param {string} data.type 验证码类型(login:登录,register:注册)
* @returns {Promise} 发送结果
*/
export function sendCode(data) {
return request({
url: '/v1/auth/code',
method: 'POST',
data
})
}
/**
* 获取用户信息
* @returns {Promise} 用户信息
*/
export function getUserInfo() {
return request({
url: '/v1/auth/info',
method: 'GET'
})
}
/**
* 刷新token
* @returns {Promise} 刷新结果
*/
export function refreshToken() {
return request({
url: '/v1/auth/refresh',
method: 'POST'
})
}
/**
* 退出登录
* @returns {Promise} 退出结果
*/
export function logout() {
return new Promise(resolve => {
resolve({ code: 200, msg: '退出成功' })
})
}
/**
* 微信登录
* @param {Object} data 登录数据
* @param {string} data.code 微信授权码
* @returns {Promise} 登录结果
*/
export function wechatLogin(data) {
return request({
url: '/v1/auth/wechat-login',
method: 'POST',
data
})
}
/**
* Apple登录
* @param {Object} data 登录数据
* @param {string} data.identityToken Apple身份令牌
* @returns {Promise} 登录结果
*/
export function appleLogin(data) {
return request({
url: '/v1/auth/apple-login',
method: 'POST',
data
})
}

View File

@@ -1,94 +0,0 @@
<template>
<view class="custom-tab-bar">
<view
class="tab-item"
:class="{ active: active === 'home' }"
@click="switchTab('/pages/index/index', 'home')"
>
<u-icon :name="active === 'home' ? 'home-fill' : 'home'" :size="48" :color="active === 'home' ? '#4080ff' : '#999999'"></u-icon>
<text class="tab-text" :class="{ 'active-text': active === 'home' }">首页</text>
</view>
<view
class="tab-item"
:class="{ active: active === 'market' }"
@click="switchTab('/pages/scenarios/index', 'market')"
>
<u-icon :name="active === 'market' ? 'tags-fill' : 'tags'" :size="48" :color="active === 'market' ? '#4080ff' : '#999999'"></u-icon>
<text class="tab-text" :class="{ 'active-text': active === 'market' }">场景获客</text>
</view>
<view
class="tab-item"
:class="{ active: active === 'work' }"
@click="switchTab('/pages/work/index', 'work')"
>
<u-icon :name="active === 'work' ? 'grid-fill' : 'grid'" :size="48" :color="active === 'work' ? '#4080ff' : '#999999'"></u-icon>
<text class="tab-text" :class="{ 'active-text': active === 'work' }">工作台</text>
</view>
<view
class="tab-item"
:class="{ active: active === 'profile' }"
@click="switchTab('/pages/profile/index', 'profile')"
>
<u-icon :name="active === 'profile' ? 'account-fill' : 'account'" :size="48" :color="active === 'profile' ? '#4080ff' : '#999999'"></u-icon>
<text class="tab-text" :class="{ 'active-text': active === 'profile' }">我的</text>
</view>
</view>
</template>
<script>
export default {
name: 'CustomTabBar',
props: {
active: {
type: String,
default: 'home'
}
},
methods: {
switchTab(url, tab) {
if (this.active !== tab) {
uni.reLaunch({
url: url
});
// 也可以通过事件通知父组件
this.$emit('change', tab);
}
}
}
}
</script>
<style lang="scss" scoped>
.custom-tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
display: flex;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 999;
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 10rpx;
.tab-text {
font-size: 26rpx;
color: #777;
&.active-text {
color: #4080ff;
}
}
}
}
</style>

View File

@@ -1,253 +0,0 @@
<template>
<view class="line-chart">
<view class="chart-container">
<view class="y-axis">
<view class="axis-label" v-for="(value, index) in yAxisLabels" :key="'y-'+index">
{{ value }}
</view>
</view>
<view class="chart-body">
<view class="grid-lines">
<view class="grid-line" v-for="(value, index) in yAxisLabels" :key="'grid-'+index"></view>
</view>
<view class="line-container">
<view class="line-path">
<view class="line-segment"
v-for="(point, index) in normalizedPoints"
:key="'line-'+index"
v-if="index < normalizedPoints.length - 1"
:style="{
left: `${index * 100 / (points.length - 1)}%`,
width: `${100 / (points.length - 1)}%`,
bottom: `${point * 100}%`,
height: `${(normalizedPoints[index + 1] - point) * 100}%`,
transform: `skewX(${(normalizedPoints[index + 1] - point) > 0 ? 45 : -45}deg)`,
transformOrigin: 'bottom left'
}"
></view>
</view>
<view class="data-points">
<view class="data-point"
v-for="(point, index) in normalizedPoints"
:key="'point-'+index"
:style="{
left: `${index * 100 / (points.length - 1)}%`,
bottom: `${point * 100}%`
}"
>
<view class="point-inner"></view>
</view>
</view>
</view>
</view>
<view class="x-axis">
<view class="axis-label"
v-for="(label, index) in xAxisLabels"
:key="'x-'+index"
:style="{ left: `${index * 100 / (xAxisLabels.length - 1)}%` }"
>
{{ label }}
</view>
</view>
</view>
</view>
</template>
<script>
export default {
name: 'LineChart',
props: {
points: {
type: Array,
required: true
},
xAxisLabels: {
type: Array,
default: () => []
},
color: {
type: String,
default: '#4080ff'
},
yAxisCount: {
type: Number,
default: 6
}
},
computed: {
// 计算最大最小值
max() {
return Math.max(...this.points, 0);
},
min() {
return Math.min(...this.points, 0);
},
// 归一化数据点转换为0-1之间的值
normalizedPoints() {
const range = this.max - this.min;
if (range === 0) return this.points.map(() => 0.5);
return this.points.map(point => (point - this.min) / range);
},
// 计算Y轴标签
yAxisLabels() {
const labels = [];
const range = this.max - this.min;
const step = range / (this.yAxisCount - 1);
for (let i = 0; i < this.yAxisCount; i++) {
const value = Math.round(this.min + (step * i));
labels.unshift(value);
}
return labels;
}
}
}
</script>
<style lang="scss" scoped>
.line-chart {
width: 100%;
height: 100%;
padding: 20rpx;
.chart-container {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
.y-axis {
position: absolute;
left: 0;
top: 0;
bottom: 40rpx;
width: 60rpx;
display: flex;
flex-direction: column;
justify-content: space-between;
.axis-label {
font-size: 22rpx;
color: #777;
text-align: right;
padding-right: 10rpx;
}
}
.chart-body {
flex: 1;
margin-left: 60rpx;
margin-bottom: 40rpx;
position: relative;
border-left: 1px solid #eeeeee;
border-bottom: 1px solid #eeeeee;
.grid-lines {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
.grid-line {
position: absolute;
left: 0;
right: 0;
border-top: 1px dashed #eeeeee;
&:nth-child(1) {
bottom: 0;
}
&:nth-child(2) {
bottom: 25%;
}
&:nth-child(3) {
bottom: 50%;
}
&:nth-child(4) {
bottom: 75%;
}
&:nth-child(5) {
bottom: 100%;
}
}
}
.line-container {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
.line-path {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
.line-segment {
position: absolute;
background-color: v-bind(color);
height: 4rpx;
}
}
.data-points {
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
.data-point {
position: absolute;
transform: translate(-50%, 50%);
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background-color: #fff;
border: 4rpx solid v-bind(color);
.point-inner {
position: absolute;
left: 2rpx;
top: 2rpx;
right: 2rpx;
bottom: 2rpx;
border-radius: 50%;
background-color: v-bind(color);
}
}
}
}
}
.x-axis {
height: 40rpx;
margin-left: 60rpx;
position: relative;
.axis-label {
position: absolute;
transform: translateX(-50%);
font-size: 22rpx;
color: #777;
text-align: center;
top: 10rpx;
}
}
}
</style>

View File

@@ -1,96 +0,0 @@
<template>
<view class="tab-bar">
<view
class="tab-item"
:class="{ active: active === 'home' }"
@click="switchTab('/pages/index/index', 'home')"
>
<u-icon :name="active === 'home' ? 'home-fill' : 'home'" :size="48" :color="active === 'home' ? '#4080ff' : '#999999'"></u-icon>
<text class="tab-text" :class="{ 'active-text': active === 'home' }">首页</text>
</view>
<view
class="tab-item"
:class="{ active: active === 'market' }"
@click="switchTab('/pages/scenarios/index', 'market')"
>
<u-icon :name="active === 'market' ? 'tags-fill' : 'tags'" :size="48" :color="active === 'market' ? '#4080ff' : '#999999'"></u-icon>
<text class="tab-text" :class="{ 'active-text': active === 'market' }">场景获客</text>
</view>
<view
class="tab-item"
:class="{ active: active === 'work' }"
@click="switchTab('/pages/index/index', 'work')"
>
<u-icon :name="active === 'work' ? 'grid-fill' : 'grid'" :size="48" :color="active === 'work' ? '#4080ff' : '#999999'"></u-icon>
<text class="tab-text" :class="{ 'active-text': active === 'work' }">工作台</text>
</view>
<view
class="tab-item"
:class="{ active: active === 'profile' }"
@click="switchTab('/pages/profile/index', 'profile')"
>
<u-icon :name="active === 'profile' ? 'account-fill' : 'account'" :size="48" :color="active === 'profile' ? '#4080ff' : '#999999'"></u-icon>
<text class="tab-text" :class="{ 'active-text': active === 'profile' }">我的</text>
</view>
</view>
</template>
<script>
export default {
name: 'TabBar',
props: {
active: {
type: String,
default: 'home'
}
},
methods: {
switchTab(url, tab) {
if (this.active !== tab) {
uni.switchTab({
url: url
});
// 也可以通过事件通知父组件
this.$emit('change', tab);
}
}
}
}
</script>
<style lang="scss" scoped>
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 150rpx;
background-color: #fff;
display: flex;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.05);
z-index: 99;
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-top: 10rpx;
.tab-text {
font-size: 26rpx;
color: #777;
margin-top: 10rpx;
&.active-text {
color: #4080ff;
}
}
}
}
</style>

View File

@@ -1,49 +0,0 @@
import Vue from 'vue'
import App from './App'
// 引入uView UI
import uView from 'uview-ui'
Vue.use(uView)
// 设置为 false 以阻止 Vue 在启动时生成生产提示
Vue.config.productionTip = false
// 导入全局样式
import './uni.scss'
// 导入请求拦截和封装
import Request from './utils/request'
Vue.prototype.$request = Request
// 导入API
import * as UserApi from './api/user'
Vue.prototype.$userApi = UserApi
// 导入工具函数
import Utils from './utils/common'
Vue.prototype.$utils = Utils
// 导入权限检查
import Auth from './utils/auth'
Vue.prototype.$auth = Auth
App.mpType = 'app'
// #ifdef MP
// 引入uView对小程序分享的mixin封装
const mpShare = require('uview-ui/libs/mixin/mpShare.js')
Vue.mixin(mpShare)
// #endif
const app = new Vue({
...App
})
// 挂载uView到Vue原型使用时可以使用this.$u访问
Vue.prototype.$u = Vue.prototype.$u || {}
// 如果采用了自定义主题,必须加入这个
import uviewTheme from './uni.scss'
Vue.prototype.$u.config.unit = 'rpx'
app.$mount()

View File

@@ -1,74 +0,0 @@
{
"name" : "存客宝",
"appid" : "",
"description" : "存客宝应用",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {
"dSYMs" : false
},
"sdkConfigs" : {
"ad" : {}
}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"h5" : {
"router" : {
"base" : "/"
},
"template" : "index.html",
"optimization" : {
"treeShaking" : {
"enable" : true
}
},
"title" : "存客宝"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +0,0 @@
{
"name": "cunkebao",
"version": "1.0.0",
"description": "存客宝 - 基于 uni-app 的跨平台应用",
"main": "main.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
"uni-app",
"vue",
"小程序",
"APP",
"H5"
],
"author": "CunkeBao Team",
"license": "MIT",
"dependencies": {
"uview-ui": "^2.0.38"
},
"devDependencies": {
"@dcloudio/uni-cli-i18n": "^2.0.2-4050620250311002",
"@dcloudio/uni-cli-shared": "^2.0.2-4050620250311002",
"@dcloudio/vue-cli-plugin-uni": "^2.0.2-4050620250311002",
"autoprefixer": "^9.8.8",
"postcss": "^7.0.39",
"postcss-comment": "^2.0.0",
"postcss-import": "^12.0.1",
"sass": "^1.86.0",
"sass-loader": "^10.5.2"
}
}

View File

@@ -1,122 +0,0 @@
{
"pages": [
{
"path": "pages/login/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/profile/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/device/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "设备管理"
}
},
{
"path": "pages/device/detail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "设备详情"
}
},
{
"path": "pages/wechat/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "微信号管理"
}
},
{
"path": "pages/wechat/detail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "账号详情"
}
},
{
"path": "pages/traffic/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "流量池"
}
},
{
"path": "pages/traffic/create",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "新建分发"
}
},
{
"path": "pages/content/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "内容库"
}
},
{
"path": "pages/content/detail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "内容库详情"
}
},
{
"path": "pages/scenarios/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "场景获客"
}
},
{
"path": "pages/scenarios/detail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "获客详情"
}
},
{
"path": "pages/scenarios/create",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "新建计划"
}
},
{
"path": "pages/work/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "工作台"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "存客宝",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"easycom": {
"autoscan": true,
"custom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
}
}

View File

@@ -1,201 +0,0 @@
<template>
<view class="privacy-container">
<u-navbar
title="隐私政策"
bgColor="#ffffff"
></u-navbar>
<view class="content">
<view class="title">存客宝隐私政策</view>
<view class="date">生效日期2023年1月1日</view>
<view class="section">
<view class="section-title">引言</view>
<view class="paragraph">
存客宝以下简称"我们"非常重视您的隐私和个人信息保护本隐私政策旨在向您说明我们如何收集使用存储共享和保护您的个人信息以及您享有的相关权利
</view>
<view class="paragraph">
请您在使用我们的服务前仔细阅读并了解本隐私政策的全部内容如您对本隐私政策有任何疑问可随时联系我们的客服
</view>
</view>
<view class="section">
<view class="section-title">我们收集的信息</view>
<view class="paragraph">
2.1 您主动提供的信息当您注册账号使用服务参与活动或与我们沟通时您可能会向我们提供手机号码姓名联系地址等信息
</view>
<view class="paragraph">
2.2 在您使用服务过程中收集的信息包括设备信息日志信息位置信息等这些信息是我们提供服务所必须的基础信息
</view>
<view class="paragraph">
2.3 来自第三方的信息在获得您的授权或法律允许的情况下我们可能从关联方合作伙伴等第三方获得您的相关信息
</view>
</view>
<view class="section">
<view class="section-title">我们如何使用您的信息</view>
<view class="paragraph">
3.1 向您提供服务包括账号注册与管理客户服务订单管理等
</view>
<view class="paragraph">
3.2 产品开发与优化我们会使用您的信息来开发和改进产品功能提升用户体验
</view>
<view class="paragraph">
3.3 安全保障我们使用您的信息用于身份验证客户服务安全防范诈骗监测存档和备份等用途
</view>
<view class="paragraph">
3.4 向您推送消息我们可能会向您发送服务相关通知活动信息等
</view>
</view>
<view class="section">
<view class="section-title">信息的共享与披露</view>
<view class="paragraph">
4.1 在以下情况下我们可能会共享您的信息
</view>
<view class="paragraph">
- 获得您的明确同意
</view>
<view class="paragraph">
- 根据法律法规的要求强制性的行政或司法要求
</view>
<view class="paragraph">
- 与我们的关联公司共享但我们只会共享必要的信息并要求他们遵守本隐私政策
</view>
<view class="paragraph">
- 与授权合作伙伴共享但我们只会共享为实现服务所必要的信息
</view>
</view>
<view class="section">
<view class="section-title">信息的存储</view>
<view class="paragraph">
5.1 存储地点我们会按照法律法规的规定将境内收集的用户个人信息存储在中国境内
</view>
<view class="paragraph">
5.2 存储期限我们仅在为实现服务目的所必需的期间内保留您的个人信息除非法律要求或允许在更长的期间内保留这些信息
</view>
</view>
<view class="section">
<view class="section-title">信息安全</view>
<view class="paragraph">
6.1 我们努力为您提供信息安全保障以防止信息的丢失不当使用未经授权的访问或披露
</view>
<view class="paragraph">
6.2 我们使用各种安全技术和程序以防信息的丢失不当使用未经授权的访问或披露
</view>
<view class="paragraph">
6.3 请您理解由于技术的限制以及可能存在的各种恶意手段即使我们已经尽最大努力加强安全措施也不可能始终保证信息的百分之百安全
</view>
</view>
<view class="section">
<view class="section-title">您的权利</view>
<view class="paragraph">
7.1 您可以通过我们提供的功能或向我们的客服提出请求访问更正删除您的个人信息或者撤回您的授权同意
</view>
<view class="paragraph">
7.2 请您理解特定的业务功能和服务将需要您的信息才能得以完成当您撤回同意或授权后我们无法继续为您提供相应的功能和服务也不再处理您相应的个人信息但您撤回同意或授权的决定不会影响我们此前基于您的授权而开展的个人信息处理
</view>
</view>
<view class="section">
<view class="section-title">未成年人保护</view>
<view class="paragraph">
8.1 我们非常重视对未成年人个人信息的保护如您为未满18周岁的未成年人在使用我们的服务前应在您的父母或其他监护人监护指导下共同阅读本隐私政策并征得您的监护人同意
</view>
<view class="paragraph">
8.2 如果我们发现自己在未事先获得可证实的父母或监护人同意的情况下收集了未成年人的个人信息则会尽快删除相关数据
</view>
</view>
<view class="section">
<view class="section-title">隐私政策的更新</view>
<view class="paragraph">
9.1 我们可能会不时更新本隐私政策当我们更新隐私政策时我们将在平台发布最新版本并更新生效日期
</view>
<view class="paragraph">
9.2 对于重大变更我们还会提供更为显著的通知包括对于特定服务我们会通过电子邮件或站内信方式发送通知说明隐私政策的具体变更内容
</view>
<view class="paragraph">
9.3 本隐私政策所指的重大变更包括但不限于我们的服务模式发生重大变化个人信息共享转让或公开披露的主要对象发生变更等
</view>
</view>
<view class="section">
<view class="section-title">如何联系我们</view>
<view class="paragraph">
10.1 如您对本隐私政策有任何疑问意见或建议可通过以下方式与我们联系
</view>
<view class="paragraph">
- 客服电话400-123-4567
</view>
<view class="paragraph">
- 电子邮箱privacy@cunkebao.com
</view>
<view class="paragraph">
10.2 一般情况下我们将在收到您的问题意见或建议后15个工作日内予以回复
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.privacy-container {
background-color: #ffffff;
min-height: 100vh;
}
.content {
padding: 30rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
text-align: center;
margin-bottom: 20rpx;
color: #333333;
}
.date {
font-size: 24rpx;
color: #e9e9e9;
text-align: center;
margin-bottom: 60rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333333;
}
.paragraph {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
margin-bottom: 20rpx;
text-align: justify;
}
</style>

View File

@@ -1,154 +0,0 @@
<template>
<view class="agreement-container">
<u-navbar
title="用户协议"
bgColor="#ffffff"
></u-navbar>
<view class="content">
<view class="title">存客宝用户协议</view>
<view class="date">生效日期2023年1月1日</view>
<view class="section">
<view class="section-title">协议的接受与变更</view>
<view class="paragraph">
1.1 存客宝用户协议以下简称"本协议"是您与存客宝平台以下简称"我们"之间就存客宝平台服务等相关事宜所订立的契约
</view>
<view class="paragraph">
1.2 您应当在使用存客宝平台服务之前认真阅读本协议全部内容如您对本协议有任何疑问可随时咨询我们的客服
</view>
<view class="paragraph">
1.3 您点击"同意""下一步"或您使用存客宝平台服务即视为您已阅读并同意签署本协议本协议自您确认同意之时起生效
</view>
</view>
<view class="section">
<view class="section-title">账号注册与使用</view>
<view class="paragraph">
2.1 您应当保证您具有完全民事行为能力能够独立承担民事责任并独立承担使用存客宝平台服务的一切法律责任
</view>
<view class="paragraph">
2.2 您注册成功后我们将给予您一个用户账号及相应的密码该用户账号和密码由您负责保管
</view>
<view class="paragraph">
2.3 您应当对您的账号负责并就账号项下的一切行为负全部责任因您保管不当等自身原因导致的任何损失或损害我们不承担责任
</view>
</view>
<view class="section">
<view class="section-title">服务内容</view>
<view class="paragraph">
3.1 存客宝平台服务的具体内容由我们根据实际情况提供包括但不限于信息发布交易撮合数据统计等
</view>
<view class="paragraph">
3.2 我们有权不经事先通知随时变更中断或终止部分或全部的服务
</view>
</view>
<view class="section">
<view class="section-title">用户行为规范</view>
<view class="paragraph">
4.1 您在使用存客宝平台服务时必须遵守中华人民共和国相关法律法规
</view>
<view class="paragraph">
4.2 您不得利用存客宝平台服务从事违法违规行为包括但不限于发布违法信息侵犯他人知识产权等
</view>
</view>
<view class="section">
<view class="section-title">知识产权</view>
<view class="paragraph">
5.1 存客宝平台所包含的全部智力成果包括但不限于程序源代码图标图饰图像图表文字等均受著作权法商标法专利法及其他知识产权法律法规的保护
</view>
</view>
<view class="section">
<view class="section-title">隐私保护</view>
<view class="paragraph">
6.1 保护您的隐私是我们的重要原则我们会采取合理的措施保护您的个人信息
</view>
<view class="paragraph">
6.2 有关隐私保护的详细政策请参见隐私政策
</view>
</view>
<view class="section">
<view class="section-title">协议修改</view>
<view class="paragraph">
7.1 我们有权随时修改本协议并在修改后的协议生效前通过适当方式通知您
</view>
<view class="paragraph">
7.2 如您不同意修改后的协议可以选择停止使用存客宝平台服务如您继续使用存客宝平台服务则视为您已同意修改后的协议
</view>
</view>
<view class="section">
<view class="section-title">法律适用与争议解决</view>
<view class="paragraph">
8.1 本协议的成立生效履行解释及纠纷解决适用中华人民共和国大陆地区法律
</view>
<view class="paragraph">
8.2 若您和我们之间发生任何纠纷或争议首先应友好协商解决协商不成的您同意将纠纷或争议提交至本协议签订地有管辖权的人民法院管辖
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
}
},
methods: {
}
}
</script>
<style lang="scss" scoped>
.agreement-container {
background-color: #ffffff;
min-height: 100vh;
}
.content {
padding: 30rpx;
}
.title {
font-size: 40rpx;
font-weight: bold;
text-align: center;
margin-bottom: 20rpx;
color: #333333;
}
.date {
font-size: 24rpx;
color: #e9e9e9;
text-align: center;
margin-bottom: 60rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333333;
}
.paragraph {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
margin-bottom: 20rpx;
text-align: justify;
}
</style>

View File

@@ -1,325 +0,0 @@
<template>
<u-popup
:show="show"
@close="onClose"
mode="bottom"
:safeAreaInsetBottom="true"
:round="10"
:closeable="true"
closeIconPos="top-right"
closeIconColor="#999"
:maskCloseAble="true"
height="85%"
>
<view class="friend-selector">
<!-- 标题栏 -->
<view class="selector-header">
<text class="selector-title">选择微信好友</text>
<view class="close-icon" @click="onClose">
<u-icon name="close" size="28" color="#999"></u-icon>
</view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<u-search
v-model="searchKeyword"
placeholder="搜索好友"
:showAction="false"
clearabled
shape="round"
:clearabled="true"
height="70"
bgColor="#f4f4f4"
></u-search>
</view>
<!-- 好友列表 -->
<scroll-view scroll-y class="friend-list">
<view
class="friend-item"
v-for="(friend, index) in filteredFriends"
:key="index"
@click="toggleSelect(friend)"
>
<view class="friend-checkbox">
<u-radio
:name="friend.id"
v-model="friend.selected"
:disabled="disabled"
@change="() => toggleSelect(friend)"
shape="circle"
activeColor="#4080ff"
></u-radio>
</view>
<view class="friend-avatar">
<image :src="friend.avatar || '/static/images/avatar.png'" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="friend-info">
<view class="friend-name">{{friend.name}}</view>
<view class="friend-id">{{friend.wechatId}}</view>
<view class="friend-client">归属客户{{friend.client}}</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="action-buttons">
<view class="cancel-btn" @click="onCancel">取消</view>
<view class="confirm-btn" @click="onConfirm">确定</view>
</view>
</view>
</u-popup>
</template>
<script>
export default {
name: 'FriendSelector',
props: {
show: {
type: Boolean,
default: false
},
selected: {
type: Array,
default: () => []
},
multiple: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
searchKeyword: '',
friends: [
{
id: '1',
name: '好友1',
wechatId: 'wxid_0y06hq00',
client: '客户1',
avatar: '/static/images/avatar.png',
selected: false
},
{
id: '2',
name: '好友2',
wechatId: 'wxid_mt5oz9fz',
client: '客户2',
avatar: '/static/images/avatar.png',
selected: false
},
{
id: '3',
name: '好友3',
wechatId: 'wxid_bma8xfh8',
client: '客户3',
avatar: '/static/images/avatar.png',
selected: false
},
{
id: '4',
name: '好友4',
wechatId: 'wxid_9xazw62h',
client: '客户4',
avatar: '/static/images/avatar.png',
selected: false
},
{
id: '5',
name: '好友5',
wechatId: 'wxid_v1fv02q3',
avatar: '/static/images/avatar.png',
selected: false
}
]
}
},
computed: {
filteredFriends() {
if (!this.searchKeyword) {
return this.friends;
}
return this.friends.filter(friend =>
friend.name.includes(this.searchKeyword) ||
friend.wechatId.includes(this.searchKeyword) ||
(friend.client && friend.client.includes(this.searchKeyword))
);
}
},
watch: {
show(newVal) {
if (newVal) {
this.initSelection();
}
},
selected: {
handler: function(newVal) {
this.initSelection();
},
immediate: true
}
},
methods: {
// 初始化选择状态
initSelection() {
this.friends.forEach(friend => {
friend.selected = this.selected.includes(friend.id);
});
},
// 切换选择状态
toggleSelect(friend) {
if (this.disabled) return;
if (!this.multiple) {
// 单选模式
this.friends.forEach(item => {
item.selected = item.id === friend.id;
});
} else {
// 多选模式
friend.selected = !friend.selected;
}
},
// 取消按钮
onCancel() {
this.$emit('cancel');
this.$emit('update:show', false);
},
// 确定按钮
onConfirm() {
const selectedFriends = this.friends.filter(friend => friend.selected);
this.$emit('confirm', selectedFriends);
this.$emit('update:show', false);
},
// 关闭弹窗
onClose() {
this.$emit('cancel');
this.$emit('update:show', false);
}
}
}
</script>
<style lang="scss" scoped>
.friend-selector {
display: flex;
flex-direction: column;
height: 100%;
.selector-header {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
position: relative;
border-bottom: 1px solid #f0f0f0;
.selector-title {
font-size: 34rpx;
font-weight: 500;
color: #333;
}
.close-icon {
position: absolute;
right: 30rpx;
top: 30rpx;
padding: 10rpx;
}
}
.search-box {
padding: 20rpx 30rpx;
}
.friend-list {
flex: 1;
padding: 0 30rpx;
overflow-y: auto;
.friend-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f5f5f5;
.friend-checkbox {
margin-right: 20rpx;
}
.friend-avatar {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
border-radius: 50%;
overflow: hidden;
background-color: #f5f5f5;
.avatar-img {
width: 100%;
height: 100%;
}
}
.friend-info {
flex: 1;
.friend-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 4rpx;
}
.friend-id {
font-size: 26rpx;
color: #999;
margin-bottom: 4rpx;
}
.friend-client {
font-size: 26rpx;
color: #999;
}
}
}
}
.action-buttons {
display: flex;
padding: 20rpx 30rpx;
border-top: 1px solid #f0f0f0;
.cancel-btn, .confirm-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 40rpx;
font-size: 30rpx;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
margin-right: 20rpx;
}
.confirm-btn {
background-color: #4080ff;
color: #fff;
}
}
}
</style>

View File

@@ -1,325 +0,0 @@
<template>
<u-popup
:show="show"
@close="onClose"
mode="bottom"
:safeAreaInsetBottom="true"
:round="10"
:closeable="true"
closeIconPos="top-right"
closeIconColor="#999"
:maskCloseAble="true"
height="85%"
>
<view class="group-selector">
<!-- 标题栏 -->
<view class="selector-header">
<text class="selector-title">选择聊天群</text>
<view class="close-icon" @click="onClose">
<u-icon name="close" size="28" color="#999"></u-icon>
</view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<u-search
v-model="searchKeyword"
placeholder="搜索聊天群"
:showAction="false"
clearabled
shape="round"
:clearabled="true"
height="70"
bgColor="#f4f4f4"
></u-search>
</view>
<!-- 群列表 -->
<scroll-view scroll-y class="group-list">
<view
class="group-item"
v-for="(group, index) in filteredGroups"
:key="index"
@click="toggleSelect(group)"
>
<view class="group-checkbox">
<u-radio
:name="group.id"
v-model="group.selected"
:disabled="disabled"
@change="() => toggleSelect(group)"
shape="circle"
activeColor="#4080ff"
></u-radio>
</view>
<view class="group-avatar">
<image :src="group.avatar || '/static/images/avatar.png'" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="group-info">
<view class="group-name">{{group.name}}</view>
<view class="group-id">{{group.groupId}}</view>
<view class="group-member-count">{{group.memberCount}}</view>
</view>
</view>
</scroll-view>
<!-- 底部按钮 -->
<view class="action-buttons">
<view class="cancel-btn" @click="onCancel">取消</view>
<view class="confirm-btn" @click="onConfirm">确定</view>
</view>
</view>
</u-popup>
</template>
<script>
export default {
name: 'GroupSelector',
props: {
show: {
type: Boolean,
default: false
},
selected: {
type: Array,
default: () => []
},
multiple: {
type: Boolean,
default: true
},
disabled: {
type: Boolean,
default: false
}
},
data() {
return {
searchKeyword: '',
groups: [
{
id: '1',
name: '产品讨论群',
groupId: '12345678910',
memberCount: 120,
avatar: '/static/images/avatar.png',
selected: false
},
{
id: '2',
name: '客户交流群',
groupId: '28374656374',
memberCount: 88,
avatar: '/static/images/avatar.png',
selected: false
},
{
id: '3',
name: '企业内部群',
groupId: '98374625162',
memberCount: 56,
avatar: '/static/images/avatar.png',
selected: false
},
{
id: '4',
name: '推广活动群',
groupId: '38273645123',
memberCount: 240,
avatar: '/static/images/avatar.png',
selected: false
},
{
id: '5',
name: '售后服务群',
groupId: '73645182934',
memberCount: 178,
avatar: '/static/images/avatar.png',
selected: false
}
]
}
},
computed: {
filteredGroups() {
if (!this.searchKeyword) {
return this.groups;
}
return this.groups.filter(group =>
group.name.includes(this.searchKeyword) ||
group.groupId.includes(this.searchKeyword)
);
}
},
watch: {
show(newVal) {
if (newVal) {
this.initSelection();
}
},
selected: {
handler: function(newVal) {
this.initSelection();
},
immediate: true
}
},
methods: {
// 初始化选择状态
initSelection() {
this.groups.forEach(group => {
group.selected = this.selected.includes(group.id);
});
},
// 切换选择状态
toggleSelect(group) {
if (this.disabled) return;
if (!this.multiple) {
// 单选模式
this.groups.forEach(item => {
item.selected = item.id === group.id;
});
} else {
// 多选模式
group.selected = !group.selected;
}
},
// 取消按钮
onCancel() {
this.$emit('cancel');
this.$emit('update:show', false);
},
// 确定按钮
onConfirm() {
const selectedGroups = this.groups.filter(group => group.selected);
this.$emit('confirm', selectedGroups);
this.$emit('update:show', false);
},
// 关闭弹窗
onClose() {
this.$emit('cancel');
this.$emit('update:show', false);
}
}
}
</script>
<style lang="scss" scoped>
.group-selector {
display: flex;
flex-direction: column;
height: 100%;
.selector-header {
display: flex;
justify-content: center;
align-items: center;
padding: 30rpx;
position: relative;
border-bottom: 1px solid #f0f0f0;
.selector-title {
font-size: 34rpx;
font-weight: 500;
color: #333;
}
.close-icon {
position: absolute;
right: 30rpx;
top: 30rpx;
padding: 10rpx;
}
}
.search-box {
padding: 20rpx 30rpx;
}
.group-list {
flex: 1;
padding: 0 30rpx;
overflow-y: auto;
.group-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1px solid #f5f5f5;
.group-checkbox {
margin-right: 20rpx;
}
.group-avatar {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
border-radius: 10rpx;
overflow: hidden;
background-color: #f5f5f5;
.avatar-img {
width: 100%;
height: 100%;
}
}
.group-info {
flex: 1;
.group-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 4rpx;
}
.group-id {
font-size: 26rpx;
color: #999;
margin-bottom: 4rpx;
}
.group-member-count {
font-size: 26rpx;
color: #999;
}
}
}
}
.action-buttons {
display: flex;
padding: 20rpx 30rpx;
border-top: 1px solid #f0f0f0;
.cancel-btn, .confirm-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 40rpx;
font-size: 30rpx;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
margin-right: 20rpx;
}
.confirm-btn {
background-color: #4080ff;
color: #fff;
}
}
}
</style>

View File

@@ -1,504 +0,0 @@
<template>
<view class="detail-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="42" color="black"></u-icon>
</view>
<view class="title">内容库详情</view>
<view class="header-right">
<view class="save-btn" @click="saveContent">
<u-icon name="checkbox-mark" size="28" color="#fff"></u-icon>
<text class="save-text">保存</text>
</view>
</view>
</view>
<!-- 表单内容 -->
<view class="form-container">
<!-- 内容库名称 -->
<view class="form-item">
<view class="form-label">内容库名称</view>
<view class="form-input-box">
<input type="text" v-model="form.title" placeholder="示例内容库" class="form-input" />
</view>
</view>
<!-- 数据来源配置 -->
<view class="form-item">
<view class="form-label">数据来源配置</view>
<view class="source-buttons">
<view class="source-btn" :class="{ active: form.dataSource === 'friend' }" @click="setDataSource('friend')">
<text>选择微信好友</text>
</view>
<view class="source-btn" :class="{ active: form.dataSource === 'group' }" @click="setDataSource('group')">
<text>选择聊天群</text>
</view>
</view>
<!-- 当选择微信好友时显示 -->
<view class="friend-list-box" v-if="form.dataSource === 'friend'">
<view class="friend-select-btn" @click="showFriendSelector">
<text>选择微信好友</text>
</view>
<view class="selected-friends" v-if="form.selectedFriends.length > 0">
<view class="friend-tag" v-for="(friend, index) in form.selectedFriends" :key="index">
<text>{{friend.name || friend}}</text>
</view>
</view>
</view>
<!-- 当选择聊天群时显示 -->
<view class="group-list-box" v-if="form.dataSource === 'group'">
<view class="group-select-btn" @click="showGroupSelector">
<text>选择聊天群</text>
</view>
<view class="selected-groups" v-if="form.selectedGroups.length > 0">
<view class="group-tag" v-for="(group, index) in form.selectedGroups" :key="index">
<text>{{group.name || group}}</text>
</view>
</view>
</view>
</view>
<!-- 关键字设置 -->
<view class="form-item">
<view class="form-label">
<text>关键字设置</text>
<u-icon name="arrow-down" size="28" color="#666" @click="toggleKeywordSection"></u-icon>
</view>
</view>
<!-- 是否启用AI -->
<view class="form-item">
<view class="form-label-with-desc">
<view class="label-row">
<text>是否启用AI</text>
<u-switch v-model="form.enableAI" activeColor="#4080ff"></u-switch>
</view>
<view class="label-desc">
<text>当启用AI之后该内容库下的所有内容都会通过AI重新生成内容</text>
</view>
</view>
</view>
<!-- AI提示词 -->
<view class="form-item" v-if="form.enableAI">
<view class="form-label">AI 提示词</view>
<view class="form-textarea-box">
<textarea v-model="form.aiPrompt" placeholder="AI提示词示例" class="form-textarea" />
</view>
</view>
<!-- 时间限制 -->
<view class="form-item">
<view class="form-label">时间限制</view>
<view class="form-date-box" @click="showDatePicker">
<u-icon name="calendar" size="28" color="#666"></u-icon>
<text class="date-text">{{form.dateRange || '选择日期范围'}}</text>
</view>
</view>
<!-- 是否启用 -->
<view class="form-item">
<view class="form-label-with-switch">
<text>是否启用</text>
<u-switch v-model="form.isEnabled" activeColor="#4080ff"></u-switch>
</view>
</view>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="work"></CustomTabBar>
<!-- 微信好友选择器 -->
<FriendSelector
:show="showFriendSelectorFlag"
:selected="selectedFriendIds"
:multiple="true"
@update:show="showFriendSelectorFlag = $event"
@confirm="handleFriendConfirm"
@cancel="showFriendSelectorFlag = false"
/>
<!-- 聊天群选择器 -->
<GroupSelector
:show="showGroupSelectorFlag"
:selected="selectedGroupIds"
:multiple="true"
@update:show="showGroupSelectorFlag = $event"
@confirm="handleGroupConfirm"
@cancel="showGroupSelectorFlag = false"
/>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
import FriendSelector from './components/FriendSelector.vue'
import GroupSelector from './components/GroupSelector.vue'
export default {
components: {
CustomTabBar,
FriendSelector,
GroupSelector
},
data() {
return {
id: '', // 编辑时的内容库ID
form: {
title: '', // 内容库名称
dataSource: 'friend', // 数据来源: friend-微信好友, group-聊天群
selectedFriends: [], // 已选择的好友
selectedGroups: [], // 已选择的群组
enableAI: true, // 是否启用AI
aiPrompt: 'AI提示词示例', // AI提示词
isKeywordExpanded: false, // 关键字设置是否展开
dateRange: '', // 时间限制
isEnabled: true // 是否启用
},
isEdit: false, // 是否为编辑模式
showFriendSelectorFlag: false, // 是否显示好友选择器
showGroupSelectorFlag: false, // 是否显示群组选择器
}
},
computed: {
// 获取已选择的好友ID列表
selectedFriendIds() {
return this.form.selectedFriends.map(friend => {
return typeof friend === 'object' ? friend.id : friend;
});
},
// 获取已选择的群组ID列表
selectedGroupIds() {
return this.form.selectedGroups.map(group => {
return typeof group === 'object' ? group.id : group;
});
}
},
onLoad(options) {
if (options.id) {
this.id = options.id;
this.isEdit = true;
this.loadContentDetail();
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 设置数据来源
setDataSource(source) {
this.form.dataSource = source;
},
// 显示好友选择器
showFriendSelector() {
this.showFriendSelectorFlag = true;
},
// 显示群组选择器
showGroupSelector() {
this.showGroupSelectorFlag = true;
},
// 处理好友选择确认
handleFriendConfirm(selectedFriends) {
this.form.selectedFriends = selectedFriends;
},
// 处理群组选择确认
handleGroupConfirm(selectedGroups) {
this.form.selectedGroups = selectedGroups;
},
// 切换关键字设置区域
toggleKeywordSection() {
this.form.isKeywordExpanded = !this.form.isKeywordExpanded;
},
// 显示日期选择器
showDatePicker() {
// 这里应该调用日期选择器组件
uni.showToast({
title: '日期选择功能开发中',
icon: 'none'
});
},
// 保存内容库
saveContent() {
// 表单验证
if (!this.form.title) {
uni.showToast({
title: '请输入内容库名称',
icon: 'none'
});
return;
}
// 验证是否选择了数据来源
if (this.form.dataSource === 'friend' && this.form.selectedFriends.length === 0) {
uni.showToast({
title: '请选择微信好友',
icon: 'none'
});
return;
}
if (this.form.dataSource === 'group' && this.form.selectedGroups.length === 0) {
uni.showToast({
title: '请选择聊天群',
icon: 'none'
});
return;
}
// 在实际应用中,这里应该提交表单数据到服务器
uni.showLoading({
title: '保存中...'
});
// 模拟API请求
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 1000);
},
// 加载内容库详情数据
loadContentDetail() {
// 在实际应用中,这里应该从服务器获取内容库详情
console.log('加载内容库详情:', this.id);
// 模拟数据加载
if (this.isEdit) {
// 模拟已有数据
this.form = {
title: '示例内容库',
dataSource: 'friend',
selectedFriends: ['张三', '李四'],
selectedGroups: [],
enableAI: true,
aiPrompt: 'AI提示词示例',
isKeywordExpanded: false,
dateRange: '',
isEnabled: true
};
}
}
}
}
</script>
<style lang="scss" scoped>
.detail-container {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.back-icon {
width: 60rpx;
color: #000;
padding: 10rpx;
border-radius: 50%;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
}
.title {
font-size: 38rpx;
font-weight: 600;
margin-left: -60rpx; /* 使标题居中 */
flex: 1;
text-align: center;
}
.header-right {
.save-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: #4080ff;
border-radius: 30rpx;
padding: 12rpx 24rpx;
color: #fff;
.save-text {
font-size: 28rpx;
}
}
}
}
.form-container {
padding: 20rpx 30rpx;
.form-item {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.form-label {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
display: flex;
justify-content: space-between;
align-items: center;
}
.form-label-with-desc {
.label-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 10rpx;
}
.label-desc {
font-size: 24rpx;
color: #999;
line-height: 1.4;
}
}
.form-label-with-switch {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 30rpx;
font-weight: 500;
color: #333;
}
.form-input-box {
.form-input {
width: 100%;
height: 80rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
}
.form-textarea-box {
.form-textarea {
width: 100%;
height: 200rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
}
}
.source-buttons {
display: flex;
margin-bottom: 20rpx;
.source-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
font-size: 28rpx;
color: #666;
&:first-child {
border-top-left-radius: 8rpx;
border-bottom-left-radius: 8rpx;
border-right: 1px solid #e0e0e0;
}
&:last-child {
border-top-right-radius: 8rpx;
border-bottom-right-radius: 8rpx;
}
&.active {
background-color: #e6f7ff;
color: #4080ff;
font-weight: 500;
}
}
}
.friend-list-box, .group-list-box {
.friend-select-btn, .group-select-btn {
width: 100%;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.selected-friends, .selected-groups {
display: flex;
flex-wrap: wrap;
.friend-tag, .group-tag {
background-color: #f5f5f5;
border-radius: 30rpx;
padding: 10rpx 20rpx;
margin-right: 15rpx;
margin-bottom: 15rpx;
font-size: 26rpx;
color: #666;
}
}
}
.form-date-box {
width: 100%;
height: 80rpx;
display: flex;
align-items: center;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
.date-text {
margin-left: 10rpx;
font-size: 28rpx;
color: #999;
}
}
}
}
</style>

View File

@@ -1,741 +0,0 @@
<template>
<view class="content-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="42" color="black"></u-icon>
</view>
<view class="title">内容库</view>
<view class="header-right">
<view class="add-btn" @click="createContent">
<text class="add-icon">+</text>
<text class="add-text">新建</text>
</view>
</view>
</view>
<!-- 内容区 -->
<view class="content-wrapper">
<!-- 搜索框 -->
<view class="search-box">
<u-search
v-model="searchKeyword"
placeholder="搜索内容库..."
:showAction="false"
shape="round"
:clearabled="true"
height="70"
bgColor="#f4f4f4"
></u-search>
<view class="filter-btn" @click="showFilter">
<u-icon name="filter" size="36" color="#000"></u-icon>
</view>
<view class="refresh-btn" @click="refreshData">
<u-icon name="reload" size="36" color="#000"></u-icon>
</view>
</view>
<!-- 标签页 -->
<view class="tabs">
<view
class="tab-item"
:class="{ active: currentTab === 'all' }"
@click="switchTab('all')"
>
全部
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'friends' }"
@click="switchTab('friends')"
>
微信好友
</view>
<view
class="tab-item"
:class="{ active: currentTab === 'groups' }"
@click="switchTab('groups')"
>
聊天群
</view>
</view>
<!-- 内容列表 -->
<view class="content-list" v-if="filteredContents.length > 0">
<view class="content-item" v-for="(item, index) in filteredContents" :key="index">
<view class="content-header">
<text class="content-title">{{item.title}}</text>
<view class="usage-tag" :class="item.used ? 'used' : 'unused'">
{{item.used ? '已使用' : '未使用'}}
</view>
<view class="more-icon" @click.stop="showOptions(item)">
<u-icon name="more-dot-fill" size="32" color="#333"></u-icon>
</view>
</view>
<view class="content-info">
<view class="info-row">
<text class="info-label">来源</text>
<view class="source-avatars">
<image v-for="(avatar, i) in item.sourceAvatars" :key="i" :src="avatar" mode="aspectFill" class="source-avatar"></image>
</view>
</view>
<view class="info-row">
<text class="info-label">创建人</text>
<text class="info-value">{{item.creator}}</text>
</view>
<view class="info-row">
<text class="info-label">内容数量</text>
<text class="info-value">{{item.contentCount}}</text>
</view>
<view class="info-row">
<text class="info-label">更新时间</text>
<text class="info-value">{{item.updateTime}}</text>
</view>
</view>
</view>
</view>
<!-- 加载中提示 -->
<view class="loading-container" v-else-if="loading">
<text class="loading-text">加载中...</text>
</view>
<!-- 空状态 -->
<view class="empty-container" v-else>
<image src="/static/images/empty.png" mode="aspectFit" class="empty-img"></image>
<text class="empty-text">暂无内容</text>
</view>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="work"></CustomTabBar>
<!-- 筛选弹窗 -->
<u-popup :show="showFilterPopup" @close="closeFilterPopup" mode="bottom">
<view class="popup-content">
<view class="popup-header">
<text class="popup-title">筛选条件</text>
<view class="popup-close" @click="closeFilterPopup">
<u-icon name="close" size="28" color="#999"></u-icon>
</view>
</view>
<view class="popup-body">
<view class="filter-section">
<view class="filter-title">使用状态</view>
<view class="filter-options">
<view
class="filter-option"
:class="{ active: selectedUsage === 'all' }"
@click="selectUsage('all')"
>
全部
</view>
<view
class="filter-option"
:class="{ active: selectedUsage === 'used' }"
@click="selectUsage('used')"
>
已使用
</view>
<view
class="filter-option"
:class="{ active: selectedUsage === 'unused' }"
@click="selectUsage('unused')"
>
未使用
</view>
</view>
</view>
<view class="filter-section">
<view class="filter-title">更新时间</view>
<view class="filter-options">
<view
class="filter-option"
:class="{ active: selectedTime === 'all' }"
@click="selectTime('all')"
>
全部时间
</view>
<view
class="filter-option"
:class="{ active: selectedTime === 'today' }"
@click="selectTime('today')"
>
今天
</view>
<view
class="filter-option"
:class="{ active: selectedTime === 'week' }"
@click="selectTime('week')"
>
本周
</view>
<view
class="filter-option"
:class="{ active: selectedTime === 'month' }"
@click="selectTime('month')"
>
本月
</view>
</view>
</view>
<view class="filter-buttons">
<view class="reset-btn" @click="resetFilter">重置</view>
<view class="confirm-btn" @click="applyFilter">确定</view>
</view>
</view>
</view>
</u-popup>
<!-- 内容操作弹窗 -->
<u-popup :show="showActionPopup" @close="closeActionPopup" mode="bottom">
<view class="action-list">
<view class="action-item" @click="editContent">
<u-icon name="edit-pen" size="32" color="#333"></u-icon>
<text>编辑内容</text>
</view>
<view class="action-item" @click="shareContent">
<u-icon name="share" size="32" color="#333"></u-icon>
<text>分享内容</text>
</view>
<view class="action-item delete" @click="deleteContent">
<u-icon name="trash" size="32" color="#fa5151"></u-icon>
<text>删除内容</text>
</view>
<view class="action-item cancel" @click="closeActionPopup">
<text>取消</text>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
export default {
components: {
CustomTabBar
},
data() {
return {
searchKeyword: '',
currentTab: 'all',
showFilterPopup: false,
showActionPopup: false,
selectedUsage: 'all',
selectedTime: 'all',
tempSelectedUsage: 'all',
tempSelectedTime: 'all',
loading: true,
currentContent: null,
contents: [
{
id: 1,
title: '微信好友广告',
used: true,
sourceAvatars: ['/static/images/avatar.png'],
creator: '海尼',
contentCount: 0,
updateTime: '2024-02-09 12:30'
},
{
id: 2,
title: '开发群',
used: true,
sourceAvatars: ['/static/images/avatar.png'],
creator: 'karuo',
contentCount: 0,
updateTime: '2024-02-09 12:30'
}
]
}
},
computed: {
filteredContents() {
let result = [...this.contents];
// 搜索关键词筛选
if (this.searchKeyword) {
result = result.filter(item =>
item.title.includes(this.searchKeyword) ||
item.creator.includes(this.searchKeyword)
);
}
// 标签页筛选
if (this.currentTab === 'friends') {
result = result.filter(item => item.title.includes('好友'));
} else if (this.currentTab === 'groups') {
result = result.filter(item => item.title.includes('群'));
}
// 使用状态筛选
if (this.selectedUsage === 'used') {
result = result.filter(item => item.used);
} else if (this.selectedUsage === 'unused') {
result = result.filter(item => !item.used);
}
// 时间筛选
if (this.selectedTime !== 'all') {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const weekAgo = today - 7 * 24 * 60 * 60 * 1000;
const monthAgo = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()).getTime();
result = result.filter(item => {
const itemTime = new Date(item.updateTime).getTime();
if (this.selectedTime === 'today') {
return itemTime >= today;
} else if (this.selectedTime === 'week') {
return itemTime >= weekAgo;
} else if (this.selectedTime === 'month') {
return itemTime >= monthAgo;
}
return true;
});
}
return result;
}
},
onLoad() {
this.loadData();
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 刷新数据
refreshData() {
this.loading = true;
setTimeout(() => {
this.loading = false;
uni.showToast({
title: '刷新成功',
icon: 'none'
});
}, 1000);
},
// 显示筛选弹窗
showFilter() {
this.tempSelectedUsage = this.selectedUsage;
this.tempSelectedTime = this.selectedTime;
this.showFilterPopup = true;
},
// 关闭筛选弹窗
closeFilterPopup() {
this.showFilterPopup = false;
},
// 切换标签页
switchTab(tab) {
this.currentTab = tab;
},
// 选择使用状态
selectUsage(usage) {
this.tempSelectedUsage = usage;
},
// 选择时间范围
selectTime(time) {
this.tempSelectedTime = time;
},
// 重置筛选条件
resetFilter() {
this.tempSelectedUsage = 'all';
this.tempSelectedTime = 'all';
},
// 应用筛选条件
applyFilter() {
this.selectedUsage = this.tempSelectedUsage;
this.selectedTime = this.tempSelectedTime;
this.closeFilterPopup();
},
// 显示内容操作弹窗
showOptions(content) {
this.currentContent = content;
this.showActionPopup = true;
},
// 关闭内容操作弹窗
closeActionPopup() {
this.showActionPopup = false;
},
// 创建内容
createContent() {
uni.navigateTo({
url: '/pages/content/detail'
});
},
// 编辑内容
editContent() {
uni.showToast({
title: `编辑内容:${this.currentContent.title}`,
icon: 'none'
});
this.closeActionPopup();
},
// 分享内容
shareContent() {
uni.showToast({
title: `分享内容:${this.currentContent.title}`,
icon: 'none'
});
this.closeActionPopup();
},
// 删除内容
deleteContent() {
uni.showModal({
title: '提示',
content: `确定要删除内容"${this.currentContent.title}"吗?`,
success: (res) => {
if (res.confirm) {
this.contents = this.contents.filter(item => item.id !== this.currentContent.id);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
this.closeActionPopup();
}
});
},
// 加载数据
loadData() {
this.loading = true;
setTimeout(() => {
this.loading = false;
}, 1000);
}
}
}
</script>
<style lang="scss" scoped>
.content-container {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.back-icon {
width: 60rpx;
color: #000;
padding: 10rpx;
border-radius: 50%;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
}
.title {
font-size: 38rpx;
font-weight: 600;
margin-left: -60rpx; /* 使标题居中 */
flex: 1;
text-align: center;
}
.header-right {
.add-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: #4080ff;
border-radius: 30rpx;
padding: 12rpx 24rpx;
color: #fff;
.add-icon {
font-size: 36rpx;
font-weight: bold;
margin-right: 6rpx;
line-height: 1;
}
.add-text {
font-size: 28rpx;
}
}
}
}
.content-wrapper {
padding: 20rpx 0;
}
.search-box {
display: flex;
align-items: center;
padding: 0 30rpx 20rpx;
.u-search {
flex: 1;
}
.filter-btn, .refresh-btn {
margin-left: 20rpx;
padding: 10rpx;
}
}
.tabs {
display: flex;
margin: 0 30rpx 20rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 28rpx;
color: #666;
&.active {
color: #4080ff;
font-weight: 500;
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 40rpx;
height: 4rpx;
background-color: #4080ff;
border-radius: 2rpx;
}
}
}
}
.content-list {
padding: 0 30rpx;
.content-item {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
.content-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.content-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
flex: 1;
}
.usage-tag {
font-size: 24rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
margin-right: 16rpx;
&.used {
background-color: #e6f7ff;
color: #1890ff;
}
&.unused {
background-color: #f6ffed;
color: #52c41a;
}
}
.more-icon {
padding: 10rpx;
}
}
.content-info {
.info-row {
display: flex;
align-items: center;
margin-bottom: 10rpx;
font-size: 28rpx;
color: #666;
.info-label {
color: #999;
min-width: 150rpx;
}
.source-avatars {
display: flex;
.source-avatar {
width: 40rpx;
height: 40rpx;
border-radius: 50%;
margin-right: 10rpx;
}
}
}
}
}
}
.loading-container, .empty-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 300rpx;
.loading-text, .empty-text {
font-size: 30rpx;
color: #999;
margin-top: 20rpx;
}
.empty-img {
width: 200rpx;
height: 200rpx;
}
}
.popup-content {
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #f0f0f0;
.popup-title {
font-size: 32rpx;
font-weight: 500;
}
.popup-close {
padding: 10rpx;
}
}
.popup-body {
padding: 30rpx;
.filter-section {
margin-bottom: 30rpx;
.filter-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
}
.filter-options {
display: flex;
flex-wrap: wrap;
.filter-option {
padding: 16rpx 30rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
margin-bottom: 20rpx;
&.active {
background-color: #e6f7ff;
color: #4080ff;
font-weight: 500;
}
}
}
}
.filter-buttons {
display: flex;
margin-top: 40rpx;
.reset-btn, .confirm-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
border-radius: 8rpx;
}
.reset-btn {
background-color: #f5f5f5;
color: #666;
margin-right: 20rpx;
}
.confirm-btn {
background-color: #4080ff;
color: #fff;
}
}
}
}
.action-list {
background-color: #fff;
border-top-left-radius: 16rpx;
border-top-right-radius: 16rpx;
.action-item {
display: flex;
align-items: center;
justify-content: center;
padding: 30rpx 0;
font-size: 32rpx;
color: #333;
border-bottom: 1px solid #f5f5f5;
u-icon {
margin-right: 15rpx;
}
&.delete {
color: #fa5151;
}
&.cancel {
color: #666;
margin-top: 16rpx;
border-bottom: none;
}
}
}
</style>

View File

@@ -1,668 +0,0 @@
<template>
<view class="device-detail-container">
<!-- 顶部导航栏 -->
<view class="navbar">
<view class="navbar-left" @click="goBack">
<u-icon name="arrow-left" size="44" color="#333"></u-icon>
</view>
<text class="navbar-title">设备详情</text>
<view class="navbar-right">
<u-icon name="setting" size="44" color="#333" @click="openSettings"></u-icon>
</view>
</view>
<!-- 设备信息卡片 -->
<view class="device-card">
<view class="device-icon-box">
<u-icon name="smartphone" size="54" color="#4080ff"></u-icon>
</view>
<view class="device-title">
<text class="device-name">{{ deviceInfo.name }}</text>
<text class="device-status" :class="deviceInfo.status === '在线' ? 'online' : 'offline'">{{ deviceInfo.status }}</text>
</view>
<view class="device-info-row">
<text class="device-info-label">IMEI: </text>
<text class="device-info-value">{{ deviceInfo.imei }}</text>
</view>
<view class="device-info-row">
<text class="device-info-label">历史ID: </text>
<text class="device-info-value">{{ deviceInfo.historyId }}</text>
</view>
<view class="device-stats">
<!-- 电量指示器 -->
<view class="battery-indicator">
<u-icon name="integral" size="40" :color="deviceInfo.status === '在线' ? '#07c160' : '#909399'"></u-icon>
<text class="battery-percentage">{{ deviceInfo.battery }}</text>
</view>
<!-- 网络状态 -->
<view class="wifi-indicator">
<u-icon name="wifi" size="40" :color="deviceInfo.status === '在线' ? '#4080ff' : '#909399'"></u-icon>
<text class="wifi-status">{{ deviceInfo.wifiStatus }}</text>
</view>
</view>
<view class="device-last-active">
<text class="last-active-label">最后活跃: </text>
<text class="last-active-time">{{ deviceInfo.lastActive }}</text>
</view>
</view>
<!-- 标签切换栏 -->
<u-tabs
:list="tabsList"
:current="tabCurrent"
@change="handleTabChange"
activeStyle="color: #333; font-weight: bold;"
inactiveStyle="color: #888;"
itemStyle="height: 90rpx; font-size: 30rpx;"
lineColor="#4080ff"
lineWidth="60rpx"
lineHeight="4rpx"
></u-tabs>
<!-- 基本信息内容 -->
<view v-if="tabCurrent === 0" class="tab-content">
<view class="features-list">
<!-- 自动加好友 -->
<view class="feature-item">
<view class="feature-left">
<text class="feature-title">自动加好友</text>
<text class="feature-desc">自动通过好友验证</text>
</view>
<view class="feature-right">
<u-switch v-model="features.autoAddFriend" activeColor="#4080ff"></u-switch>
</view>
</view>
<!-- 自动回复 -->
<view class="feature-item">
<view class="feature-left">
<text class="feature-title">自动回复</text>
<text class="feature-desc">自动回复好友消息</text>
</view>
<view class="feature-right">
<u-switch v-model="features.autoReply" activeColor="#4080ff"></u-switch>
</view>
</view>
<!-- 朋友圈同步 -->
<view class="feature-item">
<view class="feature-left">
<text class="feature-title">朋友圈同步</text>
<text class="feature-desc">自动同步朋友圈内容</text>
</view>
<view class="feature-right">
<u-switch v-model="features.momentSync" activeColor="#4080ff"></u-switch>
</view>
</view>
<!-- AI会话 -->
<view class="feature-item">
<view class="feature-left">
<text class="feature-title">AI会话</text>
<text class="feature-desc">启用AI智能对话</text>
</view>
<view class="feature-right">
<u-switch v-model="features.aiChat" activeColor="#4080ff"></u-switch>
</view>
</view>
</view>
<!-- 统计数据卡片 -->
<view class="stats-container">
<view class="stats-card">
<view class="stats-icon">
<u-icon name="account" size="40" color="#4080ff"></u-icon>
</view>
<text class="stats-title">好友总数</text>
<text class="stats-value">768</text>
</view>
<view class="stats-card">
<view class="stats-icon">
<u-icon name="chat" size="40" color="#4080ff"></u-icon>
</view>
<text class="stats-title">消息数量</text>
<text class="stats-value">5,678</text>
</view>
</view>
</view>
<!-- 关联账号内容 -->
<view v-if="tabCurrent === 1" class="tab-content">
<view class="wechat-accounts-list">
<!-- 账号项 1 -->
<view class="wechat-account-item">
<view class="wechat-avatar">
<u-avatar src="/static/images/avatar.png" size="80"></u-avatar>
</view>
<view class="wechat-info">
<view class="wechat-name-row">
<text class="wechat-name">老张</text>
<text class="wechat-status normal">正常</text>
</view>
<view class="wechat-id-row">
<text class="wechat-id-label">微信号: </text>
<text class="wechat-id-value">wxid_abc123</text>
</view>
<view class="wechat-gender-row">
<text class="wechat-gender-label">性别: </text>
<text class="wechat-gender-value"></text>
</view>
<view class="wechat-friends-row">
<text class="wechat-friends-label">好友数: </text>
<text class="wechat-friends-value">523</text>
<text class="wechat-add-friends">可加友</text>
</view>
</view>
</view>
<!-- 账号项 2 -->
<view class="wechat-account-item">
<view class="wechat-avatar">
<u-avatar src="/static/images/avatar.png" size="80"></u-avatar>
</view>
<view class="wechat-info">
<view class="wechat-name-row">
<text class="wechat-name">老李</text>
<text class="wechat-status warning">异常</text>
</view>
<view class="wechat-id-row">
<text class="wechat-id-label">微信号: </text>
<text class="wechat-id-value">wxid_xyz789</text>
</view>
<view class="wechat-gender-row">
<text class="wechat-gender-label">性别: </text>
<text class="wechat-gender-value"></text>
</view>
<view class="wechat-friends-row">
<text class="wechat-friends-label">好友数: </text>
<text class="wechat-friends-value">245</text>
<text class="wechat-add-friends used">已使用</text>
</view>
</view>
</view>
</view>
</view>
<!-- 操作记录内容 -->
<view v-if="tabCurrent === 2" class="tab-content">
<view class="operation-logs">
<!-- 操作记录项 1 -->
<view class="operation-item">
<view class="operation-icon">
<u-icon name="reload" size="36" color="#4080ff"></u-icon>
</view>
<view class="operation-info">
<text class="operation-title">开启自动加好友</text>
<text class="operation-meta">操作人: 系统 · 2024-02-09 15:30:45</text>
</view>
</view>
<!-- 操作记录项 2 -->
<view class="operation-item">
<view class="operation-icon">
<u-icon name="reload" size="36" color="#4080ff"></u-icon>
</view>
<view class="operation-info">
<text class="operation-title">添加微信号</text>
<text class="operation-meta">操作人: 管理员 · 2024-02-09 14:20:33</text>
</view>
</view>
</view>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="home"></CustomTabBar>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue';
export default {
components: {
CustomTabBar
},
data() {
return {
tabsList: [
{ name: '基本信息' },
{ name: '关联账号' },
{ name: '操作记录' }
],
tabCurrent: 0,
features: {
autoAddFriend: true,
autoReply: true,
momentSync: false,
aiChat: true
},
deviceInfo: {
name: '设备 1',
imei: 'sd123123',
historyId: 'vx412321, vfbadasd',
battery: '85%',
wifiStatus: '已连接',
lastActive: '2024-02-09 15:30:45',
status: '在线'
},
wechatAccounts: [
{
avatar: '/static/images/avatar.png',
name: '老张',
status: '正常',
id: 'wxid_abc123',
gender: '男',
friends: 523,
canAddFriends: true
},
{
avatar: '/static/images/avatar.png',
name: '老李',
status: '异常',
id: 'wxid_xyz789',
gender: '男',
friends: 245,
canAddFriends: false
}
],
operationLogs: [
{
title: '开启自动加好友',
operator: '系统',
time: '2024-02-09 15:30:45'
},
{
title: '添加微信号',
operator: '管理员',
time: '2024-02-09 14:20:33'
}
],
deviceId: null
}
},
onLoad(options) {
// 获取路由参数中的设备ID
if (options && options.id) {
this.deviceId = options.id;
console.log('设备ID:', this.deviceId);
}
// 加载设备详情数据
this.loadDeviceDetail();
},
methods: {
goBack() {
uni.navigateBack();
},
openSettings() {
uni.showToast({
title: '设置功能即将上线',
icon: 'none',
duration: 2000
});
},
handleTabChange(index) {
this.tabCurrent = index;
},
loadDeviceDetail() {
// 这里模拟API调用获取设备详情数据
uni.showLoading({
title: '加载中...'
});
// 模拟网络请求延迟
setTimeout(() => {
// 根据设备ID获取不同的设备数据
// 在实际应用中,这里应该是从服务器获取数据
if (this.deviceId) {
// 使用预设数据实际项目中应替换为API调用
console.log('加载设备ID为', this.deviceId, '的详情数据');
// 模拟不同的设备数据
if (this.deviceId === '1') {
// 设备1数据保持不变已在data中预设
} else if (this.deviceId === '2') {
this.deviceInfo.name = '设备 2';
this.deviceInfo.imei = 'sd123124';
this.deviceInfo.battery = '65%';
this.deviceInfo.lastActive = '2024-02-08 10:15:23';
} else if (this.deviceId === '3') {
this.deviceInfo.name = '设备 3';
this.deviceInfo.imei = 'sd123125';
this.deviceInfo.battery = '92%';
this.deviceInfo.lastActive = '2024-02-09 08:45:12';
} else if (this.deviceId === '4') {
this.deviceInfo.name = '设备 4';
this.deviceInfo.imei = 'sd123126';
this.deviceInfo.battery = '23%';
this.deviceInfo.status = '离线';
this.deviceInfo.wifiStatus = '未连接';
this.deviceInfo.lastActive = '2024-02-07 16:20:35';
}
}
uni.hideLoading();
}, 500);
}
}
}
</script>
<style lang="scss" scoped>
.device-detail-container {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
/* 顶部导航栏 */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
position: relative;
.navbar-left {
width: 80rpx;
}
.navbar-title {
font-size: 36rpx;
font-weight: bold;
}
.navbar-right {
width: 80rpx;
display: flex;
justify-content: flex-end;
}
}
/* 设备信息卡片 */
.device-card {
margin: 20rpx;
padding: 30rpx;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.device-icon-box {
display: flex;
justify-content: center;
align-items: center;
width: 120rpx;
height: 120rpx;
border-radius: 60rpx;
background-color: #f0f5ff;
margin: 0 auto;
}
.device-title {
display: flex;
justify-content: center;
align-items: center;
margin: 16rpx 0;
.device-name {
font-size: 36rpx;
font-weight: bold;
margin-right: 16rpx;
}
.device-status {
font-size: 26rpx;
padding: 4rpx 16rpx;
border-radius: 20rpx;
&.online {
background-color: rgba(7, 193, 96, 0.1);
color: #07c160;
}
&.offline {
background-color: rgba(144, 147, 153, 0.1);
color: #909399;
}
}
}
.device-info-row {
display: flex;
justify-content: center;
margin: 8rpx 0;
font-size: 28rpx;
color: #666;
.device-info-value {
color: #333;
}
}
.device-stats {
display: flex;
justify-content: center;
margin: 20rpx 0;
.battery-indicator, .wifi-indicator {
display: flex;
align-items: center;
margin: 0 30rpx;
.battery-percentage, .wifi-status {
margin-left: 8rpx;
font-size: 28rpx;
color: #333;
}
}
}
.device-last-active {
text-align: center;
font-size: 26rpx;
color: #999;
margin-top: 16rpx;
.last-active-time {
color: #666;
}
}
}
/* 选项卡内容 */
.tab-content {
padding: 20rpx;
}
/* 功能设置列表 */
.features-list {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 30rpx;
.feature-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.feature-left {
display: flex;
flex-direction: column;
.feature-title {
font-size: 32rpx;
color: #333;
margin-bottom: 4rpx;
}
.feature-desc {
font-size: 24rpx;
color: #999;
}
}
}
}
/* 统计数据卡片 */
.stats-container {
display: flex;
justify-content: space-between;
.stats-card {
flex: 1;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin: 0 10rpx;
text-align: center;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.stats-icon {
margin-bottom: 16rpx;
}
.stats-title {
font-size: 26rpx;
color: #666;
display: block;
margin-bottom: 10rpx;
}
.stats-value {
font-size: 48rpx;
color: #4080ff;
font-weight: bold;
font-family: 'Digital-Bold', sans-serif;
}
}
}
/* 微信账号列表 */
.wechat-accounts-list {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.wechat-account-item {
display: flex;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.wechat-avatar {
margin-right: 20rpx;
}
.wechat-info {
flex: 1;
.wechat-name-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
.wechat-name {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.wechat-status {
font-size: 24rpx;
padding: 4rpx 16rpx;
border-radius: 20rpx;
&.normal {
background-color: rgba(7, 193, 96, 0.1);
color: #07c160;
}
&.warning {
background-color: rgba(250, 81, 81, 0.1);
color: #fa5151;
}
}
}
.wechat-id-row, .wechat-gender-row, .wechat-friends-row {
font-size: 26rpx;
color: #666;
margin: 6rpx 0;
}
.wechat-friends-row {
display: flex;
justify-content: space-between;
align-items: center;
.wechat-add-friends {
padding: 4rpx 16rpx;
border-radius: 20rpx;
font-size: 24rpx;
&:not(.used) {
background-color: rgba(7, 193, 96, 0.1);
color: #07c160;
}
&.used {
background-color: rgba(144, 147, 153, 0.1);
color: #909399;
}
}
}
}
}
}
/* 操作记录 */
.operation-logs {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.operation-item {
display: flex;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.operation-icon {
margin-right: 20rpx;
}
.operation-info {
flex: 1;
.operation-title {
font-size: 30rpx;
color: #333;
margin-bottom: 6rpx;
}
.operation-meta {
font-size: 24rpx;
color: #999;
}
}
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,429 +0,0 @@
<template>
<view class="index-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="title align-left">存客宝</view>
<view class="header-icons">
<u-icon name="bell" size="50" color="#000000" class="icon-bell" @click="goToNotification"></u-icon>
</view>
</view>
<!-- 数据概览卡片 -->
<view class="data-cards">
<view class="data-card">
<view class="data-title">设备数量</view>
<view class="data-content">
<text class="data-number digital-number">42</text>
<image src="/static/images/icons/smartphone.svg" class="device-icon"></image>
</view>
</view>
<view class="data-card">
<view class="data-title">微信号数量</view>
<view class="data-content">
<text class="data-number digital-number">42</text>
<image src="/static/images/icons/users.svg" class="team-icon"></image>
</view>
</view>
<view class="data-card">
<view class="data-title">在线微信号</view>
<view class="data-content">
<text class="data-number digital-number">35</text>
<image src="/static/images/icons/heartbeat.svg" class="heartbeat-icon"></image>
</view>
<view class="progress-container">
<view class="progress-bar">
<view class="progress-fill" :style="{ width: onlineRate + '%' }"></view>
</view>
</view>
</view>
</view>
<!-- 场景获客统计卡片 -->
<view class="stat-card">
<view class="card-title align-left">场景获客统计</view>
<view class="stat-grid">
<view class="stat-item">
<view class="stat-icon bg-green">
<u-icon name="integral-fill" size="38" color="#07c160"></u-icon>
</view>
<view class="stat-number">234</view>
<view class="stat-label">公众号获客</view>
</view>
<view class="stat-item">
<view class="stat-icon bg-yellow">
<u-icon name="coupon" size="38" color="#ff9900"></u-icon>
</view>
<view class="stat-number">167</view>
<view class="stat-label">海报获客</view>
</view>
<view class="stat-item">
<view class="stat-icon bg-black">
<u-icon name="play-right" size="38" color="#000000"></u-icon>
</view>
<view class="stat-number">156</view>
<view class="stat-label">抖音获客</view>
</view>
<view class="stat-item">
<view class="stat-icon bg-red">
<u-icon name="heart" size="38" color="#fa5151"></u-icon>
</view>
<view class="stat-number">89</view>
<view class="stat-label">小红书获客</view>
</view>
</view>
</view>
<!-- 每日获客趋势卡片 -->
<view class="trend-card">
<view class="card-title align-left">每日获客趋势</view>
<view class="chart-container">
<!-- 使用自定义LineChart组件代替uChart -->
<LineChart
:points="weekTrendData"
:xAxisLabels="weekDays"
color="#2563EB"
class="custom-chart"
></LineChart>
</view>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="home"></CustomTabBar>
</view>
</template>
<script>
import LineChart from '@/components/LineChart.vue'
import CustomTabBar from '@/components/CustomTabBar.vue'
import Auth from '@/utils/auth'
import { getUserInfo, logout } from '@/api/user'
export default {
components: {
LineChart,
CustomTabBar
},
data() {
return {
weekTrendData: [120, 150, 180, 210, 240, 210, 190],
weekDays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
deviceCount: 42,
wechatCount: 42,
onlineCount: 35,
onlineRate: 83, // 计算在线率百分比:(35/42)*100 约等于 83
channelStats: [
{ icon: 'integral-fill', color: 'green', count: 234, label: '公众号获客' },
{ icon: 'coupon', color: 'yellow', count: 167, label: '海报获客' },
{ icon: 'play-right', color: 'black', count: 156, label: '抖音获客' },
{ icon: 'heart', color: 'red', count: 89, label: '小红书获客' }
],
userInfo: null
}
},
onLoad() {
// 检查登录状态
if (!Auth.isLogin()) {
uni.reLaunch({
url: '/pages/login/index'
});
return;
}
// 获取用户信息
this.fetchUserInfo();
// 加载数据
this.loadData();
},
methods: {
// 获取用户信息
fetchUserInfo() {
// 先尝试从缓存获取
this.userInfo = Auth.getUserInfo();
// 然后从服务器获取最新信息
getUserInfo().then(res => {
if (res.code === 200) {
this.userInfo = res.data;
Auth.setUserInfo(res.data);
}
}).catch(err => {
console.error('获取用户信息失败:', err);
});
},
// 加载数据
loadData() {
// 这里可以添加API调用获取实际数据
console.log('加载首页数据');
// 示例数据已在data中预设
},
// 跳转到通知页面
goToNotification() {
uni.navigateTo({
url: '/pages/notification/index'
});
},
// 退出登录
handleLogout() {
uni.showModal({
title: '提示',
content: '确认退出登录吗?',
success: (res) => {
if (res.confirm) {
// 直接清除本地保存的登录信息
Auth.removeAll();
// 显示退出成功提示
uni.showToast({
title: '退出成功',
icon: 'success',
duration: 1500
});
// 跳转到登录页面
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/index'
});
}, 1500);
}
}
});
}
}
}
</script>
<style lang="scss" scoped>
.index-container {
min-height: 100vh;
background-color: #f9fafb;
position: relative;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #e9e9e9;
.title {
font-size: 45rpx;
font-weight: bold;
color: #2664ec;
&.align-left {
text-align: left;
}
}
.header-icons {
display: flex;
color: black;
align-items: center;
.user-info {
display: flex;
align-items: center;
margin-right: 20rpx;
padding: 8rpx 20rpx;
background-color: #f5f5f5;
border-radius: 30rpx;
.user-name {
font-size: 28rpx;
color: #333;
margin-right: 8rpx;
}
}
.icon-bell {
margin-left: 10rpx;
}
}
}
.data-cards {
display: flex;
justify-content: space-between;
margin: 20rpx;
.data-card {
flex: 1;
background-color: #fff;
border-radius: 30rpx;
padding: 30rpx 30rpx;
margin: 15rpx;
box-shadow: 0 2rpx 7rpx rgba(0, 0, 0, 0.15);
.data-title {
font-size: 28rpx;
color: black;
text-align: center;
}
.data-content {
display: flex;
align-items: center;
justify-content: space-between;
.data-number {
font-size: 58rpx;
font-weight: bold;
color: #2563EB;
height: 80rpx;
line-height: 80rpx;
}
.device-icon {
width: 76rpx;
height: 60rpx;
margin-right: 6rpx;
}
.team-icon {
width: 76rpx;
height: 60rpx;
margin-right: 6rpx;
}
.heartbeat-icon {
width: 76rpx;
height: 60rpx;
margin-right: 6rpx;
animation: pulse 1.5s ease-in-out infinite;
}
}
.progress-container {
display: flex;
align-items: center;
margin-top: 16rpx;
.progress-bar {
flex: 1;
height: 10rpx;
background-color: #eeeeee;
border-radius: 5rpx;
overflow: hidden;
.progress-fill {
height: 100%;
background-color: #2664ec;
border-radius: 5rpx;
}
}
.progress-text {
margin-left: 10rpx;
font-size: 24rpx;
color: #666;
}
}
}
}
.stat-card, .trend-card {
margin: 35rpx;
background-color: #fff;
border-radius: 30rpx;
padding: 25rpx 40rpx;
box-shadow: 0 2rpx 7rpx rgba(0, 0, 0, 0.15);
.card-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 30rpx;
color: #333;
text-align: center;
&.align-left {
text-align: left;
}
}
}
.stat-grid {
display: flex;
flex-wrap: wrap;
.stat-item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 0;
.stat-icon {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16rpx;
&.bg-green {
background-color: rgba(7, 193, 96, 0.2);
}
&.bg-yellow {
background-color: rgba(255, 153, 0, 0.2);
}
&.bg-black {
background-color: rgba(0, 0, 0, 0.2);
}
&.bg-red {
background-color: rgba(250, 81, 81, 0.2);
}
}
.stat-number {
font-size: 34rpx;
font-weight: normal;
color: #333;
margin-bottom: 8rpx;
}
.stat-label {
font-size: 24rpx;
color: #777;
}
}
}
.chart-container {
height: 500rpx;
display: flex;
justify-content: center;
align-items: center;
.custom-chart {
width: 100%;
height: 100%;
}
}
.tab-text {
font-size: 26rpx;
color: #777;
margin-top: 10rpx;
&.active-text {
color: #4080ff;
}
}
</style>

View File

@@ -1,504 +0,0 @@
<template>
<view class="login-container">
<!-- 登录方式切换 -->
<u-tabs
:list="tabsList"
:current="Number(current)"
@change="handleTabChange"
activeStyle="color: #4080ff; font-size: 36rpx"
inactiveStyle="color: #e9e9e9; font-size: 36rpx"
itemStyle="height: 96rpx; padding: 0 30rpx;"
lineColor="#4080ff"
lineWidth="48rpx"
lineHeight="4rpx"
:itemWidth="500"
></u-tabs>
<!-- 提示文字 -->
<view class="login-hint">
你所在地区仅支持 手机号 / 微信 / Apple 登录
</view>
<!-- 表单区域 -->
<view class="login-form">
<!-- 手机号输入 -->
<view class="input-box">
<u--input
v-model="form.account"
placeholder="+86手机号"
prefixIcon="phone"
prefixIconStyle="font-size: 40rpx; color: #909399; padding-right: 16rpx;"
clearable
type="number"
maxlength="11"
border="none"
fontSize="30rpx"
></u--input>
</view>
<!-- 验证码输入 -->
<view v-if="current == 0" class="input-box code-box">
<u--input
v-model="form.code"
placeholder="验证码"
clearable
type="number"
maxlength="6"
border="none"
inputAlign="left"
fontSize="30rpx"
></u--input>
<view class="code-btn-wrap">
<u-button
@tap="getCode"
:text="codeTips"
type="primary"
size="mini"
:disabled="!isValidMobile || sending"
customStyle="width: 180rpx;height: 68rpx;font-size: 28rpx;"
></u-button>
</view>
</view>
<!-- 密码输入 -->
<view v-if="current == 1" class="input-box">
<u--input
v-model="form.password"
placeholder="密码"
:password="!showPassword"
clearable
border="none"
fontSize="30rpx"
suffixIcon="eye"
@clickSuffixIcon="showPassword = !showPassword"
suffixIconStyle="font-size: 40rpx;"
></u--input>
</view>
<!-- 用户协议 -->
<view class="agreement">
<u-checkbox-group
v-model="isAgree"
placement="column"
@change="checkboxChange"
>
<u-checkbox
key="1"
shape="circle"
activeColor="#4080ff"
size="35"
iconSize="30"
></u-checkbox>
</u-checkbox-group>
<text class="agreement-text">
已阅读并同意
<text class="link" @click="goToUserAgreement">用户协议</text>
<text class="link" @click="goToPrivacyPolicy">隐私政策</text>
</text>
</view>
<!-- 登录按钮 -->
<u-button
text="登录"
type="info"
:disabled="!canLogin"
@click="handleLogin"
customStyle="width: 100%; margin-top: 40rpx; height: 96rpx; border-radius: 24rpx; font-size: 40rpx; font-weight: bold; background-color: #2563eb; color: #fff;"
></u-button>
<!-- 分割线 -->
<view class="divider">
<view class="line"></view>
<view class="text"></view>
<view class="line"></view>
</view>
<!-- 第三方登录 -->
<view class="other-login">
<!-- 微信登录 -->
<button class="wechat-btn" @click="handleWechatLogin">
<u-icon name="weixin-fill" size="44" color="#07c160" class="wechat-icon"></u-icon>
<text>使用微信登录</text>
</button>
<!-- Apple登录 -->
<button class="apple-btn" @click="handleAppleLogin">
<u-icon name="apple-fill" size="44" color="#333333" class="apple-icon"></u-icon>
<text>使用 Apple 登录</text>
</button>
</view>
<!-- 联系我们 -->
<view class="contact-us" @click="handleContact">联系我们</view>
</view>
</view>
</template>
<script>
import { login, mobileLogin, sendCode } from '@/api/user'
import Auth from '@/utils/auth'
export default {
data() {
return {
tabsList: [
{ text: '验证码登录', name: '验证码登录', id: 0 },
{ text: '密码登录', name: '密码登录', id: 1 }
],
current: 0,
form: {
account: '',
code: '',
password: '',
typeId: 1 // 默认账号类型为运营后台/操盘手
},
showPassword: false,
isAgree: false,
sending: false,
codeTips: '发送验证码'
}
},
onLoad() {
// 确保初始状态下表单字段正确
this.current = 0;
this.form.password = '';
},
computed: {
isValidMobile() {
return /^1\d{10}$/.test(this.form.account)
},
canLogin() {
if (!this.isAgree || !this.isValidMobile) return false
return this.current == 0 ? !!this.form.code : !!this.form.password
}
},
methods: {
checkboxChange(value) {
console.log('checkboxChange', value)
},
handleTabChange(index) {
this.current = Number(index.index);
// 清除不相关的表单字段
if (this.current == 0) {
this.form.password = '';
} else {
this.form.code = '';
}
// 确保密码输入框的可见状态正确重置
this.showPassword = false;
},
getCode() {
if (this.sending || !this.isValidMobile) return
// 发送验证码接口调用
sendCode({
account: this.form.account,
type: 'login'
}).then(res => {
if (res.code === 200) {
// 发送成功,开始倒计时
this.sending = true
this.codeTips = '60s'
let seconds = 60
const timer = setInterval(() => {
seconds--
this.codeTips = `${seconds}s`
if (seconds <= 0) {
clearInterval(timer)
this.sending = false
this.codeTips = '发送验证码'
}
}, 1000)
// 提示用户
uni.showToast({
title: '验证码已发送',
icon: 'success'
})
} else {
// 发送失败
uni.showToast({
title: res.msg || '验证码发送失败',
icon: 'none'
})
this.sending = false
}
}).catch(err => {
console.error('发送验证码失败:', err)
uni.showToast({
title: '验证码发送失败,请重试',
icon: 'none'
})
this.sending = false
})
},
handleLogin() {
if (!this.canLogin) {
if (!this.isAgree) {
uni.showToast({
title: '请先同意用户协议和隐私政策',
icon: 'none'
})
return
}
if (!this.isValidMobile) {
uni.showToast({
title: '请输入有效的手机号',
icon: 'none'
})
return
}
if (this.current == 0 && !this.form.code) {
uni.showToast({
title: '请输入验证码',
icon: 'none'
})
return
}
if (this.current == 1 && !this.form.password) {
uni.showToast({
title: '请输入密码',
icon: 'none'
})
return
}
return
}
// 显示加载中
uni.showLoading({
title: '登录中...',
mask: true
})
// 根据当前登录方式选择不同的登录API
const loginAction = this.current === 0 ?
mobileLogin({
account: this.form.account,
code: this.form.code,
typeId: this.form.typeId
}) :
login({
account: this.form.account,
password: this.form.password,
typeId: this.form.typeId
});
// 调用登录接口
loginAction.then(res => {
// 隐藏加载提示
uni.hideLoading()
if (res.code === 200) {
// 登录成功保存token和用户信息
Auth.setToken(res.data.token, res.data.token_expired - Math.floor(Date.now() / 1000));
Auth.setUserInfo(res.data.member);
// 显示登录成功提示
uni.showToast({
title: '登录成功',
icon: 'success',
duration: 1500
})
// 延迟跳转到首页
setTimeout(() => {
uni.reLaunch({
url: '/pages/index/index',
success: () => {
console.log('跳转到首页成功')
},
fail: (err) => {
console.error('跳转失败:', err)
uni.showToast({
title: '跳转失败,请重试',
icon: 'none'
})
}
})
}, 1500)
} else {
// 登录失败
uni.showToast({
title: res.msg || '登录失败',
icon: 'none'
})
}
}).catch(err => {
// 隐藏加载提示
uni.hideLoading()
console.error('登录失败:', err)
uni.showToast({
title: '登录失败,请重试',
icon: 'none'
})
})
},
handleWechatLogin() {
console.log('微信登录')
// 仅模拟
uni.showToast({
title: '微信登录',
icon: 'none'
})
},
handleAppleLogin() {
console.log('Apple登录')
// 仅模拟
uni.showToast({
title: 'Apple登录',
icon: 'none'
})
},
goToUserAgreement() {
uni.navigateTo({
url: '/pages/agreement/user'
})
},
goToPrivacyPolicy() {
uni.navigateTo({
url: '/pages/agreement/privacy'
})
},
handleContact() {
uni.showModal({
title: '联系我们',
content: '客服电话400-xxx-xxxx',
showCancel: false
})
}
}
}
</script>
<style lang="scss" scoped>
.login-container {
min-height: 100vh;
background-color: #ffffff;
padding: 0 40rpx;
}
.login-hint {
font-size: 32rpx;
text-align: center;
margin: 30rpx 0;
}
.login-form {
margin-top: 40rpx;
.input-box {
height: 96rpx;
border: 2rpx solid #e9e9e9;
border-radius: 16rpx;
padding: 0 24rpx;
margin-bottom: 30rpx;
display: flex;
align-items: center;
.u-form-item {
flex: 1;
margin-bottom: 0;
}
}
.code-box {
display: flex;
align-items: center;
padding-right: 12rpx;
.u--input {
flex: 1;
}
.code-btn-wrap {
margin-left: 12rpx;
height: 68rpx;
.u-button {
margin: 0;
}
}
}
}
.agreement {
display: flex;
align-items: center;
margin: 40rpx 0;
.agreement-text {
font-size: 28rpx;
color: #777777;
margin-left: 12rpx;
}
.link {
color: #4080ff;
margin: 0 8rpx;
}
}
.divider {
display: flex;
align-items: center;
margin: 60rpx 0;
.line {
flex: 1;
height: 2rpx;
background-color: #eeeeee;
}
.text {
color: #777777;
padding: 0 30rpx;
font-size: 28rpx;
}
}
.other-login {
.wechat-btn, .apple-btn {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 96rpx;
margin-bottom: 20rpx;
border-radius: 24rpx;
font-size: 32rpx;
background-color: #ffffff;
border: 2rpx solid #dddddd;
text {
color: #333333;
margin-left: 16rpx;
}
}
.wechat-btn {
text {
font-size: 32rpx;
font-weight: 500;
}
}
.apple-btn {
text {
color: #333333;
font-size: 32rpx;
font-weight: 500;
}
}
}
.contact-us {
text-align: center;
font-size: 26rpx;
color: #777777;
margin-top: 60rpx;
padding-bottom: 40rpx;
}
</style>

View File

@@ -1,333 +0,0 @@
<template>
<view class="profile-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="title">我的</view>
<view class="header-icons">
<u-icon name="setting" size="45" color="#000000" class="icon-setting" @click="goToSetting"></u-icon>
<u-icon name="bell" size="50" color="#000000" class="icon-bell" @click="goToNotification"></u-icon>
</view>
</view>
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="avatar-wrap">
<template v-if="userInfo && userInfo.avatar">
<image class="avatar" :src="userInfo.avatar"></image>
</template>
<template v-else>
<view class="avatar-icon">
<u-icon name="account-fill" size="60" color="#4080ff"></u-icon>
</view>
</template>
</view>
<view class="user-info">
<view class="username">{{ userInfo && userInfo.username ? userInfo.username : '未设置昵称' }}</view>
<view class="account">账号: {{ userInfo && userInfo.account ? userInfo.account : '未登录' }}</view>
<view class="edit-profile-btn" @click="editProfile">
编辑资料
</view>
</view>
</view>
<!-- 功能菜单列表 -->
<view class="menu-list">
<view class="menu-item" @click="navigateTo('/pages/device/index')">
<view class="menu-left">
<text class="menu-title">设备管理</text>
</view>
<u-icon name="arrow-right" size="30" color="#9fa6b1"></u-icon>
</view>
<view class="menu-item" @click="navigateTo('/pages/wechat/index')">
<view class="menu-left">
<text class="menu-title">微信号管理</text>
</view>
<u-icon name="arrow-right" size="30" color="#9fa6b1"></u-icon>
</view>
<view class="menu-item" @click="navigateTo('/pages/traffic/index')">
<view class="menu-left">
<text class="menu-title">流量池</text>
</view>
<u-icon name="arrow-right" size="30" color="#9fa6b1"></u-icon>
</view>
<view class="menu-item" @click="navigateTo('/pages/content/index')">
<view class="menu-left">
<text class="menu-title">内容库</text>
</view>
<u-icon name="arrow-right" size="30" color="#9fa6b1"></u-icon>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-btn" @click="handleLogout">
<image src="/static/images/icons/logout.svg" class="logout-icon"></image>
<text class="logout-text">退出登录</text>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="profile"></CustomTabBar>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
import Auth from '@/utils/auth'
import { getUserInfo, logout } from '@/api/user'
export default {
components: {
CustomTabBar
},
data() {
return {
userInfo: null
}
},
onShow() {
// 每次显示页面时获取最新的用户信息
this.getUserInfo();
},
onLoad() {
// 检查登录状态
if (!Auth.isLogin()) {
uni.reLaunch({
url: '/pages/login/index'
});
return;
}
// 获取用户信息
this.getUserInfo();
},
methods: {
// 获取用户信息
getUserInfo() {
// 先从本地缓存获取
const cachedUserInfo = Auth.getUserInfo();
if (cachedUserInfo) {
this.userInfo = cachedUserInfo;
}
// 同时从服务器获取最新信息
getUserInfo().then(res => {
if (res.code === 200) {
this.userInfo = res.data;
// 更新本地缓存
Auth.setUserInfo(res.data);
}
}).catch(err => {
console.error('获取用户信息失败:', err);
// 如果获取失败但有缓存,使用缓存数据
if (!this.userInfo) {
this.userInfo = Auth.getUserInfo();
}
});
},
// 跳转到设置页面
goToSetting() {
uni.navigateTo({
url: '/pages/setting/index'
});
},
// 跳转到通知页面
goToNotification() {
uni.navigateTo({
url: '/pages/notification/index'
});
},
// 编辑个人资料
editProfile() {
uni.navigateTo({
url: '/pages/profile/edit'
});
},
// 页面导航
navigateTo(url) {
uni.navigateTo({
url: url
});
},
// 处理退出登录
handleLogout() {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
// 直接清除本地保存的登录信息
Auth.removeAll();
// 显示退出成功提示
uni.showToast({
title: '退出成功',
icon: 'success',
duration: 1500
});
// 跳转到登录页面
setTimeout(() => {
uni.reLaunch({
url: '/pages/login/index'
});
}, 1500);
}
}
});
}
}
}
</script>
<style lang="scss" scoped>
.profile-container {
min-height: 90vh;
background-color: #f9fafb;
position: relative;
padding-top: 46rpx;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #e9e9e9;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
.title {
font-size: 45rpx;
font-weight: bold;
color: #2664ec;
}
.header-icons {
display: flex;
align-items: center;
.icon-setting {
margin-right: 40rpx;
}
}
}
.user-card {
margin: 35rpx;
margin-top: 120rpx;
background-color: #fff;
border-radius: 35rpx;
padding: 50rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.avatar-wrap {
width: 180rpx;
height: 180rpx;
margin-right: 30rpx;
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
}
.avatar-icon {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #f0f5ff;
display: flex;
justify-content: center;
align-items: center;
}
}
.user-info {
flex: 1;
.username {
font-size: 45rpx;
font-weight: bold;
color: #2664ec;
margin-bottom: 4rpx;
}
.account {
font-size: 32rpx;
color: #6b7280;
margin-bottom: 10rpx;
}
.edit-profile-btn {
display: inline-block;
padding: 10rpx 25rpx;
background-color: #f5f5f5;
border-radius: 30rpx;
font-size: 28rpx;
color: #333;
}
}
}
.menu-list {
margin: 35rpx;
background-color: #fff;
border-radius: 35rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.menu-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx 40rpx;
border-bottom: 2rpx solid #e9e9e9;
&:last-child {
border-bottom: none;
}
.menu-left {
display: flex;
align-items: center;
.menu-title {
font-size: 32rpx;
color: #333;
}
}
}
}
.logout-btn {
display: flex;
justify-content: center;
align-items: center;
margin: 60rpx auto;
width: 200rpx;
.logout-icon {
width: 42rpx;
height: 42rpx;
margin-right: 10rpx;
}
.logout-text {
font-size: 32rpx;
color: #ff3c2a;
}
}
</style>

View File

@@ -1,203 +0,0 @@
<template>
<view class="create-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="26"></u-icon>
</view>
<view class="title">新建计划</view>
<view class="header-icons">
<u-icon name="checkmark" size="26" color="#4080ff" class="icon-save" @click="savePlan"></u-icon>
</view>
</view>
<!-- 表单区域 -->
<view class="form-content">
<u-form :model="formData" ref="uForm">
<u-form-item label="计划名称" prop="name">
<u-input v-model="formData.name" placeholder="请输入计划名称" />
</u-form-item>
<u-form-item label="获客渠道" prop="channel">
<u-input v-model="formData.channelName" placeholder="选择获客渠道" @click="showChannelPicker" disabled disabledColor="#ffffff" />
<u-icon slot="right" name="arrow-right" size="24" color="#c8c9cc"></u-icon>
</u-form-item>
<u-form-item label="目标人数" prop="target">
<u-input v-model="formData.target" placeholder="请输入目标获客人数" type="number" />
</u-form-item>
<u-form-item label="开始日期" prop="startDate">
<u-input v-model="formData.startDate" placeholder="选择开始日期" @click="showDatePicker('start')" disabled disabledColor="#ffffff" />
<u-icon slot="right" name="calendar" size="24" color="#c8c9cc"></u-icon>
</u-form-item>
<u-form-item label="结束日期" prop="endDate">
<u-input v-model="formData.endDate" placeholder="选择结束日期" @click="showDatePicker('end')" disabled disabledColor="#ffffff" />
<u-icon slot="right" name="calendar" size="24" color="#c8c9cc"></u-icon>
</u-form-item>
<u-form-item label="描述" prop="description">
<u-input v-model="formData.description" type="textarea" placeholder="请输入计划描述" />
</u-form-item>
</u-form>
</view>
<!-- 渠道选择器 -->
<u-select
:list="channelList"
v-model="showChannelSelect"
@confirm="confirmChannel"
></u-select>
<!-- 日期选择器 -->
<u-calendar
v-model="showDateSelect"
:mode="dateMode"
@confirm="confirmDate"
></u-calendar>
</view>
</template>
<script>
export default {
data() {
return {
formData: {
name: '',
channel: '',
channelName: '',
target: '',
startDate: '',
endDate: '',
description: ''
},
showChannelSelect: false,
showDateSelect: false,
dateMode: 'single',
dateType: 'start',
channelList: [
{ value: 'douyin', label: '抖音获客' },
{ value: 'xiaohongshu', label: '小红书获客' },
{ value: 'phone', label: '电话获客' },
{ value: 'official', label: '公众号获客' },
{ value: 'poster', label: '海报获客' },
{ value: 'wechat-group', label: '微信群获客' }
]
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 显示渠道选择器
showChannelPicker() {
this.showChannelSelect = true;
},
// 确认选择渠道
confirmChannel(e) {
this.formData.channel = e[0].value;
this.formData.channelName = e[0].label;
},
// 显示日期选择器
showDatePicker(type) {
this.dateType = type;
this.showDateSelect = true;
},
// 确认选择日期
confirmDate(e) {
const dateStr = e.year + '-' + e.month + '-' + e.day;
if (this.dateType === 'start') {
this.formData.startDate = dateStr;
} else {
this.formData.endDate = dateStr;
}
},
// 保存计划
savePlan() {
// 表单验证
if (!this.formData.name) {
uni.showToast({
title: '请输入计划名称',
icon: 'none'
});
return;
}
if (!this.formData.channel) {
uni.showToast({
title: '请选择获客渠道',
icon: 'none'
});
return;
}
// 显示保存成功
uni.showLoading({
title: '保存中'
});
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 延迟返回
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 1000);
}
}
}
</script>
<style lang="scss" scoped>
.create-container {
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx;
background-color: #fff;
position: relative;
.back-icon {
width: 48rpx;
}
.title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 40rpx;
font-weight: bold;
color: #333;
}
.header-icons {
display: flex;
align-items: center;
}
}
.form-content {
margin: 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
}
</style>

View File

@@ -1,504 +0,0 @@
<template>
<view class="detail-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="26"></u-icon>
</view>
<view class="title">{{channelInfo.name}}</view>
<view class="header-icons">
<u-icon name="more-circle" size="26" class="icon-more" @click="showOptions"></u-icon>
</view>
</view>
<!-- 数据概览卡片 -->
<view class="data-overview">
<view class="overview-header">
<view class="channel-icon" :class="'bg-' + channelInfo.bgColor">
<u-icon :name="channelInfo.icon" size="32" color="#ffffff"></u-icon>
</view>
<view class="channel-info">
<view class="channel-name">{{channelInfo.name}}</view>
<view class="channel-desc">今日新增客户 {{channelInfo.count}} </view>
</view>
</view>
<view class="overview-data">
<view class="data-col">
<view class="data-value">{{channelInfo.count}}</view>
<view class="data-label">今日获客</view>
</view>
<view class="data-col">
<view class="data-value">{{weekTotal}}</view>
<view class="data-label">本周获客</view>
</view>
<view class="data-col">
<view class="data-value">{{monthTotal}}</view>
<view class="data-label">本月获客</view>
</view>
</view>
</view>
<!-- 趋势图 -->
<view class="trend-card">
<view class="card-title">获客趋势</view>
<view class="tab-wrapper">
<view
class="tab-item"
:class="{ active: timeRange === 'week' }"
@click="changeTimeRange('week')"
>本周</view>
<view
class="tab-item"
:class="{ active: timeRange === 'month' }"
@click="changeTimeRange('month')"
>本月</view>
<view
class="tab-item"
:class="{ active: timeRange === 'year' }"
@click="changeTimeRange('year')"
>全年</view>
</view>
<view class="chart-container">
<LineChart
:points="trendData"
:xAxisLabels="timeRange === 'week' ? weekDays : (timeRange === 'month' ? monthDays : monthNames)"
:color="getColorByChannel(channelInfo.bgColor)"
class="custom-chart"
></LineChart>
</view>
</view>
<!-- 客户列表 -->
<view class="customer-list">
<view class="list-header">
<view class="list-title">新增客户</view>
<view class="list-action" @click="viewAllCustomers">查看全部</view>
</view>
<view class="customer-item" v-for="(item, index) in customers" :key="index">
<view class="customer-avatar">
<template v-if="item.avatar">
<image class="avatar" :src="item.avatar"></image>
</template>
<template v-else>
<view class="avatar-icon">
<u-icon name="account-fill" size="30" color="#4080ff"></u-icon>
</view>
</template>
</view>
<view class="customer-info">
<view class="customer-name">{{item.name}}</view>
<view class="customer-time">{{item.time}}</view>
</view>
<view class="customer-action">
<u-button size="mini" type="primary" text="联系" @click="contactCustomer(item)"></u-button>
</view>
</view>
<view class="empty-tip" v-if="customers.length === 0">
暂无客户数据
</view>
</view>
</view>
</template>
<script>
import LineChart from '@/components/LineChart.vue'
export default {
components: {
LineChart
},
data() {
return {
channelId: '',
timeRange: 'week',
weekDays: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
monthDays: Array.from({length: 30}, (_, i) => `${i+1}`),
monthNames: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'],
weekData: [52, 65, 78, 95, 110, 95, 80],
monthData: [30, 45, 60, 75, 90, 105, 120, 135, 120, 105, 90, 75, 60, 45, 30, 45, 60, 75, 90, 105, 120, 135, 120, 105, 90, 75, 60, 45, 30],
yearData: [100, 120, 150, 170, 190, 210, 230, 250, 270, 290, 310, 330],
customers: [
{ name: '张先生', time: '今天 12:30', avatar: null },
{ name: '李女士', time: '今天 10:15', avatar: null },
{ name: '王先生', time: '今天 09:45', avatar: null },
{ name: '赵女士', time: '今天 08:20', avatar: null }
],
channels: [
{ id: 'douyin', name: '抖音获客', icon: 'play-right', bgColor: 'black', count: 156, increase: '+12.5%' },
{ id: 'xiaohongshu', name: '小红书获客', icon: 'heart', bgColor: 'red', count: 89, increase: '+8.3%' },
{ id: 'phone', name: '电话获客', icon: 'phone', bgColor: 'blue', count: 42, increase: '+15.8%' },
{ id: 'official', name: '公众号获客', icon: 'integral-fill', bgColor: 'green', count: 234, increase: '+15.7%' },
{ id: 'poster', name: '海报获客', icon: 'coupon', bgColor: 'yellow', count: 167, increase: '+10.2%' },
{ id: 'wechat-group', name: '微信群获客', icon: 'weixin-fill', bgColor: 'wechat', count: 145, increase: '+11.2%' }
]
}
},
computed: {
// 获取当前渠道信息
channelInfo() {
const channel = this.channels.find(item => item.id === this.channelId);
return channel || { name: '获客详情', icon: 'home', bgColor: 'blue', count: 0, increase: '+0.0%' };
},
// 根据时间范围获取趋势数据
trendData() {
if (this.timeRange === 'week') {
return this.weekData;
} else if (this.timeRange === 'month') {
return this.monthData;
} else {
return this.yearData;
}
},
// 计算周总量
weekTotal() {
return this.weekData.reduce((sum, num) => sum + num, 0);
},
// 计算月总量
monthTotal() {
return this.monthData.slice(0, 30).reduce((sum, num) => sum + num, 0);
}
},
onLoad(options) {
// 获取渠道ID
if (options.id) {
this.channelId = options.id;
}
// 加载数据
this.loadData();
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 显示更多选项
showOptions() {
uni.showActionSheet({
itemList: ['分享', '设置', '删除'],
success: (res) => {
console.log('选择了第' + (res.tapIndex + 1) + '个选项');
}
});
},
// 切换时间范围
changeTimeRange(range) {
this.timeRange = range;
},
// 查看全部客户
viewAllCustomers() {
uni.navigateTo({
url: `/pages/scenarios/customers?id=${this.channelId}`
});
},
// 联系客户
contactCustomer(customer) {
uni.showModal({
title: '联系客户',
content: `是否联系 ${customer.name}`,
success: (res) => {
if (res.confirm) {
console.log('联系客户:', customer);
}
}
});
},
// 根据渠道类型获取颜色
getColorByChannel(type) {
const colorMap = {
'black': '#000000',
'red': '#fa5151',
'blue': '#4080ff',
'green': '#07c160',
'yellow': '#ff9900',
'wechat': '#07c160'
};
return colorMap[type] || '#4080ff';
},
// 加载数据
loadData() {
// 这里可以添加API调用获取实际数据
console.log('加载获客详情数据:', this.channelId);
// 示例数据已在data中预设
}
}
}
</script>
<style lang="scss" scoped>
.detail-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 40rpx;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx;
background-color: #fff;
position: relative;
.back-icon {
width: 48rpx;
}
.title {
position: absolute;
left: 50%;
transform: translateX(-50%);
font-size: 40rpx;
font-weight: bold;
color: #333;
}
.header-icons {
display: flex;
align-items: center;
}
}
.data-overview {
margin: 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.overview-header {
display: flex;
align-items: center;
margin-bottom: 30rpx;
.channel-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-right: 20rpx;
&.bg-black {
background-color: #000000;
}
&.bg-red {
background-color: #fa5151;
}
&.bg-blue {
background-color: #4080ff;
}
&.bg-green {
background-color: #07c160;
}
&.bg-yellow {
background-color: #ff9900;
}
&.bg-wechat {
background-color: #07c160;
}
}
.channel-info {
.channel-name {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.channel-desc {
font-size: 28rpx;
color: #777;
}
}
}
.overview-data {
display: flex;
justify-content: space-around;
text-align: center;
.data-col {
flex: 1;
.data-value {
font-size: 44rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.data-label {
font-size: 28rpx;
color: #777;
}
}
}
}
.trend-card {
margin: 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.card-title {
font-size: 36rpx;
font-weight: bold;
margin-bottom: 30rpx;
color: #333;
}
.tab-wrapper {
display: flex;
margin-bottom: 20rpx;
.tab-item {
padding: 10rpx 30rpx;
font-size: 28rpx;
color: #666;
border-radius: 8rpx;
margin-right: 20rpx;
&.active {
background-color: #ecf5ff;
color: #4080ff;
}
}
}
.chart-container {
height: 400rpx;
.custom-chart {
width: 100%;
height: 100%;
}
}
}
.customer-list {
margin: 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.list-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.list-action {
font-size: 28rpx;
color: #4080ff;
}
}
.customer-item {
display: flex;
align-items: center;
padding: 20rpx 0;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.customer-avatar {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
.avatar {
width: 100%;
height: 100%;
border-radius: 50%;
}
.avatar-icon {
width: 100%;
height: 100%;
border-radius: 50%;
background-color: #f0f5ff;
display: flex;
justify-content: center;
align-items: center;
}
}
.customer-info {
flex: 1;
.customer-name {
font-size: 32rpx;
color: #333;
margin-bottom: 8rpx;
}
.customer-time {
font-size: 24rpx;
color: #777;
}
}
}
.empty-tip {
text-align: center;
padding: 40rpx 0;
color: #777;
font-size: 28rpx;
}
}
.progress-label {
font-size: 24rpx;
color: #777;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 24rpx;
color: #777;
}
.timeline-time {
font-size: 24rpx;
color: #777;
margin-bottom: 8rpx;
}
.timeline-meta {
font-size: 22rpx;
color: #777;
}
</style>

View File

@@ -1,512 +0,0 @@
<template>
<view class="scenarios-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="42" color="black"></u-icon>
</view>
<view class="title">场景获客</view>
<view class="header-icons">
<u-icon name="plus" size="30" color="#fff" class="icon-add" @click="createNewPlan"></u-icon>
<text>新建计划</text>
</view>
</view>
<!-- 获客渠道卡片列表 -->
<view class="channel-grid">
<!-- 抖音获客 -->
<view class="channel-card" @click="navigateToDetail('douyin')">
<view class="channel-icon bg-black">
<u-icon name="play-right" size="28" color="#ffffff"></u-icon>
</view>
<view class="channel-name">抖音获客</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">156</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+12.5%</text>
</view>
</view>
</view>
<!-- 小红书获客 -->
<view class="channel-card" @click="navigateToDetail('xiaohongshu')">
<view class="channel-icon bg-red">
<u-icon name="heart" size="28" color="#ffffff"></u-icon>
</view>
<view class="channel-name">小红书获客</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">89</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+8.3%</text>
</view>
</view>
</view>
<!-- 电话获客 -->
<view class="channel-card" @click="navigateToDetail('phone')">
<view class="channel-icon bg-blue">
<u-icon name="phone" size="28" color="#ffffff"></u-icon>
</view>
<view class="channel-name">电话获客</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">42</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+15.8%</text>
</view>
</view>
</view>
<!-- 公众号获客 -->
<view class="channel-card" @click="navigateToDetail('official')">
<view class="channel-icon bg-green">
<u-icon name="integral-fill" size="28" color="#ffffff"></u-icon>
</view>
<view class="channel-name">公众号获客</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">234</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+15.7%</text>
</view>
</view>
</view>
<!-- 海报获客 -->
<view class="channel-card" @click="navigateToDetail('poster')">
<view class="channel-icon bg-yellow">
<u-icon name="coupon" size="28" color="#ffffff"></u-icon>
</view>
<view class="channel-name">海报获客</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">167</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+10.2%</text>
</view>
</view>
</view>
<!-- 微信群获客 -->
<view class="channel-card" @click="navigateToDetail('wechat-group')">
<view class="channel-icon bg-wechat">
<u-icon name="weixin-fill" size="28" color="#ffffff"></u-icon>
</view>
<view class="channel-name">微信群获客</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">145</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+11.2%</text>
</view>
</view>
</view>
<!-- AI智能获客 版块标题 -->
<view class="section-title">
<image src="/static/images/icons/robot.svg" class="robot-icon"></image>
<text>AI智能获客</text>
<text class="beta-tag">Beta</text>
</view>
<!-- AI智能加友 -->
<view class="channel-card channel-card-xw" @click="navigateToDetail('ai-friend')">
<view class="channel-icon">
<image src="/static/images/icons/aipa.svg" mode="aspectFit" class="ai-icon"></image>
</view>
<view class="channel-name">AI智能加友</view>
<view class="channel-desc">智能分析目标用户画像自动筛选优质客户</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">245</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+18.5%</text>
</view>
</view>
</view>
<!-- AI智能群控 -->
<view class="channel-card channel-card-xw" @click="navigateToDetail('ai-group')">
<view class="channel-icon">
<image src="/static/images/icons/aipa.svg" mode="aspectFit" class="ai-icon"></image>
</view>
<view class="channel-name">AI智能群控</view>
<view class="channel-desc">一键管理多个群聊自动回复数据分析</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">128</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+9.7%</text>
</view>
</view>
</view>
<!-- AI场景转化 -->
<view class="channel-card channel-card-xw" @click="navigateToDetail('ai-convert')">
<view class="channel-icon">
<image src="/static/images/icons/aipa.svg" mode="aspectFit" class="ai-icon"></image>
</view>
<view class="channel-name">AI场景转化</view>
<view class="channel-desc">多场景智能营销提升获客转化效果</view>
<view class="channel-data">
<view class="data-item">
<view class="data-label">今日</view>
<view class="data-value">134</view>
</view>
<view class="data-trend up">
<image src="/static/images/icons/trend-up.svg" style="width: 32rpx; height: 32rpx;"></image>
<text>+14.3%</text>
</view>
</view>
</view>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="market"></CustomTabBar>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
export default {
components: {
CustomTabBar
},
data() {
return {
channels: [
{ id: 'douyin', name: '抖音获客', icon: 'play-right', bgColor: 'black', count: 156, increase: '+12.5%' },
{ id: 'xiaohongshu', name: '小红书获客', icon: 'heart', bgColor: 'red', count: 89, increase: '+8.3%' },
{ id: 'phone', name: '电话获客', icon: 'phone', bgColor: 'blue', count: 42, increase: '+15.8%' },
{ id: 'official', name: '公众号获客', icon: 'integral-fill', bgColor: 'green', count: 234, increase: '+15.7%' },
{ id: 'poster', name: '海报获客', icon: 'coupon', bgColor: 'yellow', count: 167, increase: '+10.2%' },
{ id: 'wechat-group', name: '微信群获客', icon: 'weixin-fill', bgColor: 'wechat', count: 145, increase: '+11.2%' },
{
id: 'ai-friend',
name: 'AI智能加友',
icon: 'star',
bgColor: 'blue',
count: 245,
increase: '+18.5%',
desc: '智能分析目标用户画像,自动筛选优质客户',
isAI: true
},
{
id: 'ai-group',
name: 'AI群引流',
icon: 'star',
bgColor: 'blue',
count: 178,
increase: '+15.2%',
desc: '智能推聊互动,提高群活跃度和转化率',
isAI: true
},
{
id: 'ai-convert',
name: 'AI场景转化',
icon: 'star',
bgColor: 'blue',
count: 134,
increase: '+14.3%',
desc: '多场景智能营销,提升获客转化效果',
isAI: true
}
]
}
},
onLoad() {
// 页面加载时获取数据
this.loadData();
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 创建新的获客计划
createNewPlan() {
uni.navigateTo({
url: '/pages/scenarios/create'
});
},
// 导航到详情页面
navigateToDetail(channelId) {
uni.navigateTo({
url: `/pages/scenarios/detail?id=${channelId}`
});
},
// 加载数据
loadData() {
// 这里可以添加API调用获取实际数据
console.log('加载场景获客数据');
// 示例数据已在data中预设
}
}
}
</script>
<style lang="scss" scoped>
.scenarios-container {
min-height: 100vh;
background-color: #f9fafb;
position: relative;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #e9e9e9;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 999;
.back-icon {
width: 60rpx;
color: #000;
padding: 10rpx;
border-radius: 50%;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
}
.title {
position: absolute;
font-size: 40rpx;
float: left;
margin-left: 70rpx;
}
.header-icons {
display: flex;
align-items: center;
background-color: #2563EB;
border-radius: 20rpx;
padding: 15rpx 30rpx;
color: #fff;
text-indent: 15rpx;
}
}
.channel-grid {
display: flex;
flex-wrap: wrap;
padding: 15rpx;
margin-top: 130rpx; // 添加顶部边距为固定header留出空间
.channel-card {
width: calc(50% - 30rpx);
margin: 15rpx;
background-color: #fff;
border-radius: 35rpx;
padding: 35rpx 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
// AI智能获客栏目下的卡片特殊背景色
.section-title + & {
background-color: #F8FBFF;
}
.channel-icon {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 15rpx;
&.bg-black {
background-color: #000000;
}
&.bg-red {
background-color: #fa5151;
}
&.bg-blue {
background-color: #4080ff;
}
&.bg-green {
background-color: #07c160;
}
&.bg-yellow {
background-color: #ff9900;
}
&.bg-wechat {
background-color: #07c160;
}
&.bg-purple {
background-color: #8a2be2;
}
}
.channel-name {
font-size: 30rpx;
color: #2563EB;
margin: 15rpx;
}
.channel-desc {
font-size: 24rpx;
color: #666;
text-align: center;
margin-bottom: 15rpx;
height: 80rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.channel-data {
width: 100%;
padding: 0 10rpx;
display: flex;
flex-direction: column;
align-items: center;
.data-item {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.data-label {
display: flex;
align-items: center;
font-size: 24rpx;
color: #999;
&::before {
content: '';
display: inline-block;
width: 36rpx;
height: 36rpx;
margin-right: 6rpx;
background: url('/static/images/icons/user-label.svg') no-repeat center/contain;
}
}
.data-value {
font-size: 38rpx;
font-weight: bold;
color: #333;
}
}
.data-trend {
display: flex;
align-items: center;
font-size: 28rpx;
text-indent: 15rpx;
&.up {
color: #2fc25b;
}
&.down {
color: #fa3534;
}
}
}
}
.channel-card-xw {
background-color: #F8FBFF !important;
border: 6rpx solid #DBEAFE !important;
.channel-icon {
background: none !important;
.ai-icon {
width: 90rpx;
height: 90rpx;
}
}
}
}
.section-title {
width: 100%;
display: flex;
align-items: center;
padding: 30rpx 20rpx 15rpx;
margin: 15rpx 7.5rpx 5rpx;
.robot-icon {
width: 56rpx;
height: 56rpx;
margin-right: 10rpx;
}
text {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.beta-tag {
font-size: 25rpx;
color: #4080ff;
background-color: #ecf5ff;
padding: 5rpx 20rpx;
border-radius: 20rpx;
margin-left: 20rpx;
}
}
.tab-text {
font-size: 26rpx;
color: #777;
margin-top: 10rpx;
&.active-text {
color: #4080ff;
}
}
</style>

View File

@@ -1,578 +0,0 @@
<template>
<view class="traffic-create-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="42" color="black"></u-icon>
</view>
<view class="title">{{pageTitle}}</view>
<view class="header-right">
<view class="save-btn" @click="saveTraffic">
<text class="save-text">保存</text>
</view>
</view>
</view>
<!-- 表单内容 -->
<view class="form-container">
<!-- 流量包名称 -->
<view class="form-item">
<view class="form-label">流量包名称</view>
<view class="form-input-box">
<input type="text" v-model="form.title" placeholder="例如:普通流量包" class="form-input" />
</view>
</view>
<!-- 价格设置 -->
<view class="form-item">
<view class="form-label">价格设置/流量包</view>
<view class="form-input-box">
<input type="digit" v-model="form.price" placeholder="0.00" class="form-input" />
</view>
</view>
<!-- 流量规模 -->
<view class="form-item">
<view class="form-label">流量规模人数</view>
<view class="form-input-box">
<input type="number" v-model="form.quantity" placeholder="0" class="form-input" />
</view>
</view>
<!-- 标签管理 -->
<view class="form-item">
<view class="form-label">标签管理</view>
<view class="tags-container">
<view class="tags-list">
<view
v-for="(tag, index) in form.tags"
:key="index"
class="tag-item"
>
<text class="tag-text">{{tag}}</text>
<text class="tag-delete" @click="removeTag(index)">×</text>
</view>
<view class="tag-add" @click="showAddTagModal">
<text>+ 添加标签</text>
</view>
</view>
</view>
</view>
<!-- 流量来源 -->
<view class="form-item">
<view class="form-label">流量来源</view>
<view class="source-radios">
<view
class="source-radio"
:class="{ active: form.source === 'platform' }"
@click="form.source = 'platform'"
>
<view class="radio-dot">
<view class="inner-dot" v-if="form.source === 'platform'"></view>
</view>
<text>平台</text>
</view>
<view
class="source-radio"
:class="{ active: form.source === 'custom' }"
@click="form.source = 'custom'"
>
<view class="radio-dot">
<view class="inner-dot" v-if="form.source === 'custom'"></view>
</view>
<text>自定义</text>
</view>
</view>
</view>
<!-- 地区限制 -->
<view class="form-item">
<view class="form-label">地区限制</view>
<view class="region-select" @click="showRegionSelector">
<text>{{form.region || '选择地区'}}</text>
<u-icon name="arrow-right" size="28" color="#999"></u-icon>
</view>
</view>
<!-- 流量时间 -->
<view class="form-item">
<view class="form-label">流量时间</view>
<view class="datetime-select" @click="showDatetimePicker">
<text>{{form.datetime || '选择时间'}}</text>
<u-icon name="arrow-right" size="28" color="#999"></u-icon>
</view>
</view>
<!-- 流量限制 -->
<view class="form-item">
<view class="form-label-with-switch">
<text>流量限制</text>
<u-switch v-model="form.enableLimit" activeColor="#4080ff"></u-switch>
</view>
<view class="limit-input-box" v-if="form.enableLimit">
<input type="number" v-model="form.limitPerDay" placeholder="每日限制数量" class="form-input" />
</view>
</view>
</view>
<!-- 添加标签弹窗 -->
<u-popup :show="showTagModal" @close="hideAddTagModal" mode="center">
<view class="tag-modal">
<view class="tag-modal-header">
<text class="modal-title">添加标签</text>
</view>
<view class="tag-modal-body">
<input type="text" v-model="newTagText" placeholder="请输入标签名称" class="tag-input" />
</view>
<view class="tag-modal-footer">
<view class="modal-btn cancel-btn" @click="hideAddTagModal">取消</view>
<view class="modal-btn confirm-btn" @click="addNewTag">确定</view>
</view>
</view>
</u-popup>
</view>
</template>
<script>
export default {
data() {
return {
isEdit: false, // 是否为编辑模式
trafficId: null, // 流量包ID
form: {
title: '', // 流量包名称
price: '', // 价格
quantity: '', // 流量规模
tags: [], // 标签数组
source: 'platform', // 流量来源platform-平台custom-自定义
region: '', // 地区限制
datetime: '', // 流量时间
enableLimit: false, // 是否启用流量限制
limitPerDay: '' // 每日限制数量
},
showTagModal: false, // 是否显示添加标签弹窗
newTagText: '' // 新标签的文本
}
},
onLoad(options) {
// 判断是否为编辑模式
if (options.id) {
this.isEdit = true;
this.trafficId = options.id;
this.loadTrafficData();
}
},
computed: {
// 页面标题
pageTitle() {
return this.isEdit ? '编辑分发' : '新建分发';
}
},
methods: {
// 加载流量包数据
loadTrafficData() {
uni.showLoading({
title: '加载中...'
});
// 模拟API请求获取数据
setTimeout(() => {
// 根据ID获取对应的数据
// 这里使用模拟数据
let mockData = {
1: {
title: '普通流量包',
price: '0.50',
quantity: '10',
tags: ['新用户', '低活跃度', '全国'],
source: 'platform',
region: '全国',
datetime: '2023-09-01',
enableLimit: true,
limitPerDay: '5'
},
2: {
title: '高质量流量',
price: '2.50',
quantity: '25',
tags: ['高消费', '高活跃度', '一线城市'],
source: 'custom',
region: '一线城市',
datetime: '2023-09-05',
enableLimit: false,
limitPerDay: ''
},
3: {
title: '精准营销流量',
price: '3.80',
quantity: '50',
tags: ['潜在客户', '有购买意向', '华东地区'],
source: 'platform',
region: '华东地区',
datetime: '2023-09-10',
enableLimit: true,
limitPerDay: '10'
}
};
// 获取数据并填充表单
if (mockData[this.trafficId]) {
this.form = mockData[this.trafficId];
uni.hideLoading();
} else {
uni.hideLoading();
uni.showToast({
title: '未找到对应数据',
icon: 'none'
});
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
}, 1000);
},
// 返回上一页
goBack() {
uni.navigateBack();
},
// 保存流量包
saveTraffic() {
// 表单验证
if (!this.form.title) {
uni.showToast({
title: '请输入流量包名称',
icon: 'none'
});
return;
}
if (!this.form.price) {
uni.showToast({
title: '请输入价格',
icon: 'none'
});
return;
}
if (!this.form.quantity) {
uni.showToast({
title: '请输入流量规模',
icon: 'none'
});
return;
}
// 在实际应用中,这里应该提交表单数据到服务器
uni.showLoading({
title: this.isEdit ? '更新中...' : '保存中...'
});
// 模拟API请求
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: this.isEdit ? '更新成功' : '创建成功',
icon: 'success'
});
// 返回上一页
setTimeout(() => {
uni.navigateBack();
}, 1500);
}, 1000);
},
// 显示添加标签弹窗
showAddTagModal() {
this.showTagModal = true;
this.newTagText = '';
},
// 隐藏添加标签弹窗
hideAddTagModal() {
this.showTagModal = false;
},
// 添加新标签
addNewTag() {
if (this.newTagText.trim()) {
this.form.tags.push(this.newTagText.trim());
this.hideAddTagModal();
} else {
uni.showToast({
title: '标签名称不能为空',
icon: 'none'
});
}
},
// 删除标签
removeTag(index) {
this.form.tags.splice(index, 1);
},
// 显示地区选择器
showRegionSelector() {
// 这里应该调用地区选择器组件
uni.showToast({
title: '地区选择功能开发中',
icon: 'none'
});
},
// 显示时间选择器
showDatetimePicker() {
// 这里应该调用时间选择器组件
uni.showToast({
title: '时间选择功能开发中',
icon: 'none'
});
}
}
}
</script>
<style lang="scss" scoped>
.traffic-create-container {
min-height: 100vh;
background-color: #f9fafb;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.back-icon {
width: 60rpx;
color: #000;
padding: 10rpx;
border-radius: 50%;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
}
.title {
font-size: 38rpx;
font-weight: 600;
margin-left: -60rpx; /* 使标题居中 */
flex: 1;
text-align: center;
}
.header-right {
.save-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: #4080ff;
border-radius: 30rpx;
padding: 12rpx 24rpx;
color: #fff;
.save-text {
font-size: 28rpx;
}
}
}
}
.form-container {
padding: 20rpx 30rpx;
.form-item {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
.form-label {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
}
.form-label-with-switch {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 20rpx;
}
.form-input-box, .limit-input-box {
.form-input {
width: 100%;
height: 80rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
}
.tags-container {
.tags-list {
display: flex;
flex-wrap: wrap;
.tag-item {
display: flex;
align-items: center;
background-color: #f0f7ff;
border-radius: 8rpx;
padding: 10rpx 16rpx;
margin-right: 16rpx;
margin-bottom: 16rpx;
.tag-text {
font-size: 26rpx;
color: #4080ff;
}
.tag-delete {
font-size: 28rpx;
color: #999;
margin-left: 10rpx;
padding: 0 5rpx;
}
}
.tag-add {
display: flex;
align-items: center;
justify-content: center;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 10rpx 16rpx;
margin-bottom: 16rpx;
text {
font-size: 26rpx;
color: #666;
}
}
}
}
.source-radios {
display: flex;
.source-radio {
display: flex;
align-items: center;
margin-right: 40rpx;
.radio-dot {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid #999;
margin-right: 10rpx;
display: flex;
align-items: center;
justify-content: center;
.inner-dot {
width: 20rpx;
height: 20rpx;
border-radius: 50%;
background-color: #4080ff;
}
}
text {
font-size: 28rpx;
color: #333;
}
&.active {
.radio-dot {
border-color: #4080ff;
}
}
}
}
.region-select, .datetime-select {
display: flex;
justify-content: space-between;
align-items: center;
height: 80rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
text {
font-size: 28rpx;
color: #333;
}
}
}
}
.tag-modal {
width: 600rpx;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
.tag-modal-header {
padding: 30rpx;
text-align: center;
border-bottom: 1rpx solid #f0f0f0;
.modal-title {
font-size: 34rpx;
font-weight: 500;
color: #333;
}
}
.tag-modal-body {
padding: 30rpx;
.tag-input {
width: 100%;
height: 80rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 20rpx;
font-size: 28rpx;
}
}
.tag-modal-footer {
display: flex;
border-top: 1rpx solid #f0f0f0;
.modal-btn {
flex: 1;
height: 90rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 30rpx;
&.cancel-btn {
color: #666;
border-right: 1rpx solid #f0f0f0;
}
&.confirm-btn {
color: #4080ff;
}
}
}
}
</style>

View File

@@ -1,305 +0,0 @@
<template>
<view class="traffic-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="42" color="black"></u-icon>
</view>
<view class="title">流量分发</view>
<view class="header-right">
<view class="add-btn" @click="createTraffic">
<u-icon name="plus" size="28" color="#fff"></u-icon>
<text class="add-text">新建分发</text>
</view>
</view>
</view>
<!-- 流量包列表 -->
<view class="traffic-list">
<!-- 普通流量包 -->
<view class="traffic-item">
<view class="traffic-header">
<text class="traffic-title">普通流量包</text>
<view class="traffic-actions">
<u-icon name="edit-pen" size="36" color="#999" @click="editTraffic(1)"></u-icon>
<u-icon name="trash" size="36" color="#999" @click="deleteTraffic(1)" class="trash-icon"></u-icon>
</view>
</view>
<view class="traffic-price">
<text class="price-symbol">¥</text>
<text class="price-value">0.50</text>
<text class="price-unit">/ 流量包</text>
</view>
<view class="traffic-info">
<text class="total-added">总添加人数: 10 </text>
</view>
<view class="traffic-tags">
<view class="tag">新用户</view>
<view class="tag">低活跃度</view>
<view class="tag">全国</view>
</view>
</view>
<!-- 高质量流量 -->
<view class="traffic-item">
<view class="traffic-header">
<text class="traffic-title">高质量流量</text>
<view class="traffic-actions">
<u-icon name="edit-pen" size="36" color="#999" @click="editTraffic(2)"></u-icon>
<u-icon name="trash" size="36" color="#999" @click="deleteTraffic(2)" class="trash-icon"></u-icon>
</view>
</view>
<view class="traffic-price">
<text class="price-symbol">¥</text>
<text class="price-value">2.50</text>
<text class="price-unit">/ 流量包</text>
</view>
<view class="traffic-info">
<text class="total-added">总添加人数: 25 </text>
</view>
<view class="traffic-tags">
<view class="tag">高消费</view>
<view class="tag">高活跃度</view>
<view class="tag">一线城市</view>
</view>
</view>
<!-- 精准营销流量 -->
<view class="traffic-item">
<view class="traffic-header">
<text class="traffic-title">精准营销流量</text>
<view class="traffic-actions">
<u-icon name="edit-pen" size="36" color="#999" @click="editTraffic(3)"></u-icon>
<u-icon name="trash" size="36" color="#999" @click="deleteTraffic(3)" class="trash-icon"></u-icon>
</view>
</view>
<view class="traffic-price">
<text class="price-symbol">¥</text>
<text class="price-value">3.80</text>
<text class="price-unit">/ 流量包</text>
</view>
<view class="traffic-info">
<text class="total-added">总添加人数: 50 </text>
</view>
<view class="traffic-tags">
<view class="tag">潜在客户</view>
<view class="tag">有购买意向</view>
<view class="tag">华东地区</view>
</view>
</view>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="work"></CustomTabBar>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
export default {
components: {
CustomTabBar
},
data() {
return {
trafficItems: [
{
id: 1,
title: '普通流量包',
price: 0.5,
totalAdded: 10,
tags: ['新用户', '低活跃度', '全国']
},
{
id: 2,
title: '高质量流量',
price: 2.5,
totalAdded: 25,
tags: ['高消费', '高活跃度', '一线城市']
},
{
id: 3,
title: '精准营销流量',
price: 3.8,
totalAdded: 50,
tags: ['潜在客户', '有购买意向', '华东地区']
}
]
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 创建新的流量分发
createTraffic() {
uni.navigateTo({
url: '/pages/traffic/create'
})
},
// 编辑流量分发
editTraffic(id) {
uni.navigateTo({
url: `/pages/traffic/create?id=${id}`
})
},
// 删除流量分发
deleteTraffic(id) {
uni.showModal({
title: '提示',
content: '确定要删除该流量分发吗?',
success: (res) => {
if (res.confirm) {
// 模拟删除操作
this.trafficItems = this.trafficItems.filter(item => item.id !== id);
uni.showToast({
title: '删除成功',
icon: 'success'
});
}
}
});
}
}
}
</script>
<style lang="scss" scoped>
.traffic-container {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.back-icon {
width: 60rpx;
color: #000;
padding: 10rpx;
border-radius: 50%;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
}
.title {
font-size: 38rpx;
font-weight: 600;
margin-left: -60rpx; /* 使标题居中 */
flex: 1;
text-align: center;
}
.header-right {
.add-btn {
display: flex;
align-items: center;
justify-content: center;
background-color: #4080ff;
border-radius: 30rpx;
padding: 12rpx 24rpx;
color: #fff;
.add-text {
font-size: 28rpx;
margin-left: 8rpx;
}
}
}
}
.traffic-list {
padding: 30rpx;
.traffic-item {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
.traffic-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.traffic-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
}
.traffic-actions {
display: flex;
.trash-icon {
margin-left: 20rpx;
}
}
}
.traffic-price {
display: flex;
align-items: baseline;
margin-bottom: 20rpx;
.price-symbol {
font-size: 30rpx;
color: #1cc15e;
font-weight: 600;
}
.price-value {
font-size: 60rpx;
color: #1cc15e;
font-weight: 700;
font-family: 'Digital-Bold', sans-serif;
margin: 0 8rpx;
}
.price-unit {
font-size: 28rpx;
color: #666;
}
}
.traffic-info {
margin-bottom: 20rpx;
.total-added {
font-size: 28rpx;
color: #666;
}
}
.traffic-tags {
display: flex;
flex-wrap: wrap;
.tag {
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 8rpx 16rpx;
font-size: 24rpx;
color: #666;
margin-right: 16rpx;
margin-bottom: 16rpx;
}
}
}
}
</style>

View File

@@ -1,919 +0,0 @@
<template>
<view class="detail-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="42" color="black"></u-icon>
</view>
<view class="title">账号详情</view>
<view class="header-right"></view>
</view>
<!-- 账号信息卡片 -->
<view class="account-card">
<view class="avatar-section">
<image :src="accountInfo.avatar || '/static/images/avatar.png'" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="basic-info">
<view class="name-status">
<text class="account-name">{{accountInfo.name}}</text>
<text class="status-tag" :class="getStatusClass(accountInfo.status)">{{accountInfo.status}}</text>
</view>
<view class="account-id">微信号{{accountInfo.wechatId}}</view>
<view class="action-buttons">
<view class="action-btn device-btn" @click="goToDeviceDetail">
<u-icon name="setting" size="28" color="#333"></u-icon>
<text>设备{{accountInfo.deviceNumber}}</text>
</view>
<view class="action-btn friends-btn" @click="showTransferModal">
<u-icon name="account" size="28" color="#333"></u-icon>
<text>好友转移</text>
</view>
</view>
</view>
</view>
<!-- 好友转移模态框 -->
<view class="transfer-modal" v-if="showTransferModalFlag">
<view class="modal-mask" @click="hideTransferModal"></view>
<view class="modal-content">
<view class="modal-header">
<text class="modal-title">好友转移确认</text>
<view class="close-btn" @click="hideTransferModal">
<text class="close-icon">×</text>
</view>
</view>
<view class="modal-body">
<view class="transfer-title">即将导出该微信号的好友列表用于创建新的获客计划</view>
<view class="account-info-box">
<view class="account-avatar">
<image :src="accountInfo.avatar || '/static/images/avatar.png'" mode="aspectFill" class="avatar-small"></image>
</view>
<view class="account-detail">
<text class="account-name-text">{{accountInfo.name}}</text>
<text class="account-id-text">{{accountInfo.wechatId}}</text>
</view>
</view>
<view class="transfer-tips">
<view class="tip-item">
<text class="dot"></text>
<text class="tip-text">将导出该账号下的所有好友信息</text>
</view>
<view class="tip-item">
<text class="dot"></text>
<text class="tip-text">好友信息将用于创建新的订单获客计划</text>
</view>
<view class="tip-item">
<text class="dot"></text>
<text class="tip-text">导出过程中请勿关闭页面</text>
</view>
</view>
</view>
<view class="modal-footer">
<view class="cancel-btn" @click="hideTransferModal">取消</view>
<view class="confirm-btn" @click="confirmTransfer">确认转移</view>
</view>
</view>
</view>
<!-- 标签栏 -->
<view class="tab-section">
<view class="tab-item" :class="{ active: activeTab === 'overview' }" @click="activeTab = 'overview'">
<text>账号概览</text>
</view>
<view class="tab-item" :class="{ active: activeTab === 'friends' }" @click="activeTab = 'friends'">
<text>好友列表 ({{accountInfo.friendsCount}})</text>
</view>
</view>
<!-- 账号概览内容 -->
<block v-if="activeTab === 'overview'">
<!-- 年龄卡片 -->
<view class="info-card">
<view class="card-header">
<u-icon name="calendar" size="28" color="#666"></u-icon>
<text class="card-title">账号年龄</text>
</view>
<view class="age-content">
<view class="age-value">{{accountInfo.age}}</view>
<view class="age-register">注册时间{{accountInfo.registerDate}}</view>
</view>
</view>
<!-- 活跃度卡片 -->
<view class="info-card">
<view class="card-header">
<u-icon name="chat" size="28" color="#666"></u-icon>
<text class="card-title">活跃程度</text>
</view>
<view class="activity-content">
<view class="activity-value">{{accountInfo.activityRate}}/</view>
<view class="total-days">总聊天数{{accountInfo.totalChatDays}}</view>
</view>
</view>
<!-- 账号权重评估 -->
<view class="info-card">
<view class="card-header">
<u-icon name="star" size="28" color="#FFAD33"></u-icon>
<text class="card-title">账号权重评估</text>
<text class="score">{{accountInfo.score}} </text>
</view>
<view class="evaluate-content">
<view class="evaluate-text">账号状态良好</view>
<!-- 账号年龄评分 -->
<view class="evaluate-item">
<view class="item-header">
<text class="item-name">账号年龄</text>
<text class="item-percent">{{accountInfo.ageScore}}%</text>
</view>
<view class="progress-bar">
<view class="progress-bg"></view>
<view class="progress-value" :style="{width: accountInfo.ageScore + '%', backgroundColor: '#4080ff'}"></view>
</view>
</view>
<!-- 活跃度评分 -->
<view class="evaluate-item">
<view class="item-header">
<text class="item-name">活跃度</text>
<text class="item-percent">{{accountInfo.activityScore}}%</text>
</view>
<view class="progress-bar">
<view class="progress-bg"></view>
<view class="progress-value" :style="{width: accountInfo.activityScore + '%', backgroundColor: '#4080ff'}"></view>
</view>
</view>
<!-- 限制影响评分 -->
<view class="evaluate-item">
<view class="item-header">
<text class="item-name">限制影响</text>
<text class="item-percent">{{accountInfo.limitScore}}%</text>
</view>
<view class="progress-bar">
<view class="progress-bg"></view>
<view class="progress-value" :style="{width: accountInfo.limitScore + '%', backgroundColor: '#4080ff'}"></view>
</view>
</view>
<!-- 实名认证评分 -->
<view class="evaluate-item">
<view class="item-header">
<text class="item-name">实名认证</text>
<text class="item-percent">{{accountInfo.verifyScore}}%</text>
</view>
<view class="progress-bar">
<view class="progress-bg"></view>
<view class="progress-value" :style="{width: accountInfo.verifyScore + '%', backgroundColor: '#4080ff'}"></view>
</view>
</view>
</view>
</view>
<!-- 添加好友统计 -->
<view class="info-card">
<view class="card-header">
<u-icon name="account-add" size="28" color="#4080ff"></u-icon>
<text class="card-title">添加好友统计</text>
<u-icon name="info-circle" size="28" color="#999"></u-icon>
</view>
<view class="friends-stat-content">
<view class="stat-row">
<text class="stat-label">今日已添加</text>
<text class="stat-value blue">{{accountInfo.todayAdded}}</text>
</view>
<view class="stat-row">
<text class="stat-label">添加进度</text>
<text class="stat-progress">{{accountInfo.todayAdded}}/{{accountInfo.dailyLimit}}</text>
</view>
<view class="progress-bar">
<view class="progress-bg"></view>
<view class="progress-value" :style="{width: (accountInfo.todayAdded / accountInfo.dailyLimit * 100) + '%', backgroundColor: '#4080ff'}"></view>
</view>
<view class="limit-tip">
<text>根据当前账号权重({{accountInfo.score}})每日最多可添加 {{accountInfo.dailyLimit}} 个好友</text>
</view>
</view>
</view>
<!-- 限制记录 -->
<view class="info-card">
<view class="card-header">
<u-icon name="warning" size="28" color="#fa5151"></u-icon>
<text class="card-title">限制记录</text>
<text class="limit-count"> {{accountInfo.limitRecords.length}} </text>
</view>
<view class="limit-records">
<view class="limit-item" v-for="(record, index) in accountInfo.limitRecords" :key="index">
<text class="limit-reason">{{record.reason}}</text>
<text class="limit-date">{{record.date}}</text>
</view>
</view>
</view>
</block>
<!-- 好友列表内容 -->
<block v-if="activeTab === 'friends'">
<view class="friends-list-container">
<view class="search-filter">
<u-search
v-model="searchKeyword"
placeholder="搜索好友"
:showAction="false"
shape="round"
bgColor="#f4f4f4"
></u-search>
</view>
<view class="friends-list">
<view class="empty-tip" v-if="filteredFriends.length === 0">
<text>暂无好友数据</text>
</view>
<view class="friend-item" v-for="(friend, index) in filteredFriends" :key="index">
<image :src="friend.avatar" mode="aspectFill" class="friend-avatar"></image>
<view class="friend-info">
<view class="friend-name">{{friend.name}}</view>
<view class="friend-remark">备注{{friend.remark || '无'}}</view>
</view>
<view class="friend-action">
<view class="action-button">
<u-icon name="chat" size="24" color="#4080ff"></u-icon>
<text>聊天</text>
</view>
</view>
</view>
</view>
</view>
</block>
<!-- 底部导航栏 -->
<CustomTabBar active="profile"></CustomTabBar>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
export default {
components: {
CustomTabBar
},
data() {
return {
id: '', // 微信账号ID
activeTab: 'overview', // 当前激活的标签页overview 或 friends
searchKeyword: '', // 搜索关键词
showTransferModalFlag: false, // 是否显示好友转移模态框
accountInfo: {
avatar: '/static/images/avatar.png',
name: '卡若-25vig',
status: '正常',
wechatId: 'wxid_hahphr2h',
deviceNumber: '1',
friendsCount: 192,
age: '2年8个月',
registerDate: '2021-06-15',
activityRate: '42',
totalChatDays: '15,234',
score: 85,
ageScore: 90,
activityScore: 85,
limitScore: 80,
verifyScore: 100,
todayAdded: 12,
dailyLimit: 17,
limitRecords: [
{ reason: '添加好友过于频繁', date: '2024-02-25' },
{ reason: '营销内容违规', date: '2024-01-15' }
]
},
friends: [] // 好友列表
}
},
computed: {
// 过滤后的好友列表
filteredFriends() {
if (!this.searchKeyword) return this.friends;
return this.friends.filter(friend =>
friend.name.includes(this.searchKeyword) ||
(friend.remark && friend.remark.includes(this.searchKeyword))
);
}
},
onLoad(options) {
if (options.id) {
this.id = options.id;
this.loadAccountInfo();
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 获取状态样式类
getStatusClass(status) {
if (status === '正常') return 'status-normal';
if (status === '运营') return 'status-running';
return '';
},
// 跳转到设备详情页
goToDeviceDetail() {
// 根据账号关联的设备编号跳转
uni.navigateTo({
url: `/pages/device/detail?id=${this.accountInfo.deviceNumber}`
});
},
// 显示好友转移模态框
showTransferModal() {
this.showTransferModalFlag = true;
},
// 隐藏好友转移模态框
hideTransferModal() {
this.showTransferModalFlag = false;
},
// 确认好友转移
confirmTransfer() {
// 在实际应用中这里应该调用API进行好友转移
uni.showLoading({
title: '转移中...'
});
// 模拟API调用
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '好友转移成功',
icon: 'success'
});
this.hideTransferModal();
}, 1500);
},
// 加载账号信息
loadAccountInfo() {
// 这里应该是API调用现在使用模拟数据
console.log(`加载账号信息: ${this.id}`);
// 模拟加载好友数据
this.friends = [
{
id: 1,
name: '张三',
avatar: '/static/images/avatar.png',
remark: '客户-北京'
},
{
id: 2,
name: '李四',
avatar: '/static/images/avatar.png',
remark: '客户-上海'
},
{
id: 3,
name: '王五',
avatar: '/static/images/avatar.png',
remark: '客户-广州'
}
];
}
}
}
</script>
<style lang="scss" scoped>
.detail-container {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.back-icon {
width: 60rpx;
color: #000;
padding: 10rpx;
border-radius: 50%;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
}
.title {
font-size: 38rpx;
font-weight: 600;
margin-left: -60rpx; /* 使标题居中 */
flex: 1;
text-align: center;
}
.header-right {
width: 60rpx;
}
}
.account-card {
margin: 30rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
display: flex;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
.avatar-section {
margin-right: 30rpx;
.avatar-img {
width: 120rpx;
height: 120rpx;
border-radius: 50%;
}
}
.basic-info {
flex: 1;
.name-status {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.account-name {
font-size: 36rpx;
font-weight: 600;
margin-right: 20rpx;
}
.status-tag {
font-size: 24rpx;
padding: 2rpx 16rpx;
border-radius: 8rpx;
&.status-normal {
background-color: #4cd964;
color: #fff;
}
&.status-running {
background-color: #ff6b6b;
color: #fff;
}
}
}
.account-id {
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.action-buttons {
display: flex;
.action-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 10rpx 20rpx;
border-radius: 8rpx;
background-color: #f5f5f5;
margin-right: 20rpx;
text {
font-size: 26rpx;
margin-left: 10rpx;
}
}
}
}
}
.tab-section {
display: flex;
background-color: #fff;
margin: 0 30rpx 20rpx;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
.tab-item {
flex: 1;
text-align: center;
padding: 24rpx 0;
font-size: 30rpx;
color: #666;
position: relative;
&.active {
color: #4080ff;
font-weight: 500;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 60rpx;
height: 6rpx;
background-color: #4080ff;
border-radius: 3rpx;
}
}
}
}
.info-card {
margin: 0 30rpx 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
.card-header {
display: flex;
align-items: center;
margin-bottom: 20rpx;
.card-title {
font-size: 30rpx;
font-weight: 500;
margin-left: 10rpx;
flex: 1;
}
.score {
font-size: 36rpx;
font-weight: 600;
color: #4cd964;
}
.limit-count {
font-size: 26rpx;
color: #999;
}
}
.age-content, .activity-content {
padding: 20rpx 0;
.age-value, .activity-value {
font-size: 48rpx;
font-weight: 600;
color: #4080ff;
margin-bottom: 10rpx;
}
.age-register, .total-days {
font-size: 28rpx;
color: #999;
}
}
.evaluate-content {
.evaluate-text {
font-size: 28rpx;
color: #666;
margin-bottom: 20rpx;
}
.evaluate-item {
margin-bottom: 20rpx;
.item-header {
display: flex;
justify-content: space-between;
margin-bottom: 10rpx;
.item-name {
font-size: 28rpx;
color: #666;
}
.item-percent {
font-size: 28rpx;
color: #333;
font-weight: 500;
}
}
}
}
.friends-stat-content {
.stat-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10rpx;
.stat-label {
font-size: 28rpx;
color: #666;
}
.stat-value {
font-size: 36rpx;
font-weight: 600;
&.blue {
color: #4080ff;
}
}
.stat-progress {
font-size: 28rpx;
color: #666;
}
}
.limit-tip {
margin-top: 20rpx;
font-size: 26rpx;
color: #999;
}
}
.limit-records {
.limit-item {
display: flex;
justify-content: space-between;
padding: 20rpx 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.limit-reason {
font-size: 28rpx;
color: #fa5151;
}
.limit-date {
font-size: 28rpx;
color: #999;
}
}
}
}
.progress-bar {
position: relative;
height: 10rpx;
margin: 10rpx 0;
.progress-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #ebeef5;
border-radius: 10rpx;
}
.progress-value {
position: absolute;
left: 0;
top: 0;
height: 100%;
border-radius: 10rpx;
}
}
.friends-list-container {
margin: 0 30rpx;
.search-filter {
margin-bottom: 20rpx;
}
.friends-list {
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
.empty-tip {
padding: 40rpx 0;
text-align: center;
color: #999;
font-size: 28rpx;
}
.friend-item {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.friend-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 20rpx;
}
.friend-info {
flex: 1;
.friend-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.friend-remark {
font-size: 26rpx;
color: #999;
}
}
.friend-action {
.action-button {
display: flex;
flex-direction: column;
align-items: center;
font-size: 24rpx;
color: #4080ff;
}
}
}
}
}
/* 好友转移模态框样式 */
.transfer-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
.modal-mask {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-content {
position: relative;
width: 650rpx;
background-color: #fff;
border-radius: 20rpx;
overflow: hidden;
z-index: 1001;
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f5f5f5;
.modal-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
}
.close-btn {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
.close-icon {
font-size: 44rpx;
color: #999;
line-height: 1;
}
}
}
.modal-body {
padding: 30rpx;
.transfer-title {
font-size: 30rpx;
color: #333;
margin-bottom: 30rpx;
}
.account-info-box {
display: flex;
align-items: center;
background-color: #f8f9fc;
padding: 20rpx;
border-radius: 10rpx;
margin-bottom: 30rpx;
.account-avatar {
margin-right: 20rpx;
.avatar-small {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
}
}
.account-detail {
display: flex;
flex-direction: column;
.account-name-text {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 4rpx;
}
.account-id-text {
font-size: 26rpx;
color: #999;
}
}
}
.transfer-tips {
margin-top: 20rpx;
.tip-item {
display: flex;
align-items: flex-start;
margin-bottom: 15rpx;
.dot {
margin-right: 10rpx;
font-size: 30rpx;
color: #666;
}
.tip-text {
font-size: 28rpx;
color: #666;
line-height: 1.5;
}
}
}
}
.modal-footer {
display: flex;
padding: 20rpx 30rpx 40rpx;
.cancel-btn, .confirm-btn {
flex: 1;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 40rpx;
font-size: 30rpx;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
margin-right: 20rpx;
}
.confirm-btn {
background-color: #4080ff;
color: #fff;
}
}
}
}
</style>

View File

@@ -1,439 +0,0 @@
<template>
<view class="wechat-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="back-icon" @click="goBack">
<u-icon name="arrow-left" size="42" color="black"></u-icon>
</view>
<view class="title">微信号</view>
<view class="header-right"></view>
</view>
<!-- 搜索框 -->
<view class="search-box">
<view class="search-input">
<u-search
v-model="searchKeyword"
placeholder="搜索微信号/昵称"
:showAction="false"
shape="round"
:clearabled="true"
height="70"
bgColor="#f4f4f4"
></u-search>
</view>
<view class="filter-icon">
<u-icon name="filter" size="36" color="#000"></u-icon>
</view>
<view class="refresh-icon">
<u-icon name="reload" size="36" color="#000"></u-icon>
</view>
</view>
<!-- 微信号列表 -->
<view class="wechat-list">
<!-- 微信号项 1 -->
<view class="wechat-item" @click="goToDetail(wechatAccounts[0])">
<view class="wechat-avatar">
<image src="/static/images/avatar.png" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="wechat-info">
<view class="wechat-header">
<view class="wechat-name">卡若-25vig</view>
<view class="wechat-status">
<text class="status-tag status-running">运营</text>
</view>
</view>
<view class="wechat-id">微信号wxid_8evmgz0y</view>
<view class="friends-info">
<text class="friends-count">好友数量6095</text>
<text class="friends-added">今日新增<text class="added-count">+7</text></text>
</view>
<view class="daily-limit">
<text class="limit-text">今日可添加8</text>
<view class="limit-progress">
<view class="progress-bg"></view>
<view class="progress-value" style="width: 35%;"></view>
</view>
<text class="limit-count">7/20</text>
</view>
<view class="device-info">
<text class="device-label">所属设备</text>
<text class="device-value">设备1</text>
<text class="last-active-label">最后活跃</text>
<text class="last-active-value">2025/3/26 12:25:10</text>
</view>
</view>
<view class="wechat-action" @click.stop="transferFriends(wechatAccounts[0].wechatId)">
<view class="transfer-btn">
<u-icon name="swap-right" size="28" color="#333"></u-icon>
<text>好友转移</text>
</view>
</view>
</view>
<!-- 微信号项 2 -->
<view class="wechat-item" @click="goToDetail(wechatAccounts[1])">
<view class="wechat-avatar">
<image src="/static/images/avatar.png" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="wechat-info">
<view class="wechat-header">
<view class="wechat-name">卡若-zok7e</view>
<view class="wechat-status">
<text class="status-tag status-normal">正常</text>
</view>
</view>
<view class="wechat-id">微信号wxid_7mlqr91i</view>
<view class="friends-info">
<text class="friends-count">好友数量4149</text>
<text class="friends-added">今日新增<text class="added-count">+11</text></text>
</view>
<view class="daily-limit">
<text class="limit-text">今日可添加5</text>
<view class="limit-progress">
<view class="progress-bg"></view>
<view class="progress-value" style="width: 55%;"></view>
</view>
<text class="limit-count">11/20</text>
</view>
<view class="device-info">
<text class="device-label">所属设备</text>
<text class="device-value">设备1</text>
<text class="last-active-label">最后活跃</text>
<text class="last-active-value">2025/3/26 11:30:34</text>
</view>
</view>
<view class="wechat-action" @click.stop="transferFriends(wechatAccounts[1].wechatId)">
<view class="transfer-btn">
<u-icon name="swap-right" size="28" color="#333"></u-icon>
<text>好友转移</text>
</view>
</view>
</view>
<!-- 微信号项 3 -->
<view class="wechat-item" @click="goToDetail(wechatAccounts[2])">
<view class="wechat-avatar">
<image src="/static/images/avatar.png" mode="aspectFill" class="avatar-img"></image>
</view>
<view class="wechat-info">
<view class="wechat-header">
<view class="wechat-name">卡若-ip9ob</view>
<view class="wechat-status">
<text class="status-tag status-normal">正常</text>
</view>
</view>
<view class="wechat-id">微信号wxid_jzfn1nmr</view>
<view class="friends-info">
<text class="friends-count">好友数量4131</text>
<text class="friends-added">今日新增<text class="added-count">+11</text></text>
</view>
<view class="daily-limit">
<text class="limit-text">今日可添加11</text>
<view class="limit-progress">
<view class="progress-bg"></view>
<view class="progress-value" style="width: 55%;"></view>
</view>
<text class="limit-count">11/20</text>
</view>
<view class="device-info">
<text class="device-label">所属设备</text>
<text class="device-value">设备1</text>
<text class="last-active-label">最后活跃</text>
<text class="last-active-value">2025/3/26 10:45:22</text>
</view>
</view>
<view class="wechat-action" @click.stop="transferFriends(wechatAccounts[2].wechatId)">
<view class="transfer-btn">
<u-icon name="swap-right" size="28" color="#333"></u-icon>
<text>好友转移</text>
</view>
</view>
</view>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="profile"></CustomTabBar>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
export default {
components: {
CustomTabBar
},
data() {
return {
searchKeyword: '',
wechatAccounts: [
{
name: '卡若-25vig',
status: '运营',
wechatId: 'wxid_8evmgz0y',
friendsCount: 6095,
todayAdded: 7,
todayLimit: 8,
maxLimit: 20,
device: '设备1',
lastActive: '2025/3/26 12:25:10'
},
{
name: '卡若-zok7e',
status: '正常',
wechatId: 'wxid_7mlqr91i',
friendsCount: 4149,
todayAdded: 11,
todayLimit: 5,
maxLimit: 20,
device: '设备1',
lastActive: '2025/3/26 11:30:34'
},
{
name: '卡若-ip9ob',
status: '正常',
wechatId: 'wxid_jzfn1nmr',
friendsCount: 4131,
todayAdded: 11,
todayLimit: 11,
maxLimit: 20,
device: '设备1',
lastActive: '2025/3/26 10:45:22'
}
]
}
},
methods: {
// 返回上一页
goBack() {
uni.navigateBack();
},
// 好友转移
transferFriends(wechatId) {
uni.showToast({
title: `${wechatId} 好友转移功能即将上线`,
icon: 'none',
duration: 2000
});
},
// 跳转到详情页
goToDetail(account) {
uni.navigateTo({
url: `/pages/wechat/detail?id=${account.wechatId}`
});
}
}
}
</script>
<style lang="scss" scoped>
.wechat-container {
min-height: 100vh;
background-color: #f9fafb;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 25rpx 30rpx;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.back-icon {
width: 60rpx;
color: #000;
padding: 10rpx;
border-radius: 50%;
&:active {
background-color: rgba(0, 0, 0, 0.05);
}
}
.title {
font-size: 38rpx;
font-weight: 600;
margin-left: -60rpx; /* 使标题居中 */
flex: 1;
text-align: center;
}
.header-right {
width: 60rpx;
}
}
.search-box {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
background-color: #fff;
.search-input {
flex: 1;
}
.filter-icon, .refresh-icon {
margin-left: 20rpx;
padding: 10rpx;
}
}
.wechat-list {
padding: 20rpx 30rpx;
.wechat-item {
background-color: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
position: relative;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
.wechat-avatar {
width: 80rpx;
height: 80rpx;
margin-right: 20rpx;
.avatar-img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
.wechat-info {
flex: 1;
.wechat-header {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.wechat-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-right: 16rpx;
}
.wechat-status {
.status-tag {
font-size: 24rpx;
padding: 2rpx 12rpx;
border-radius: 6rpx;
&.status-running {
background-color: #ff6b6b;
color: #fff;
}
&.status-normal {
background-color: #4cd964;
color: #fff;
}
}
}
}
.wechat-id {
font-size: 28rpx;
color: #666;
margin-bottom: 8rpx;
}
.friends-info {
display: flex;
align-items: center;
font-size: 28rpx;
color: #666;
margin-bottom: 8rpx;
.friends-count {
margin-right: 20rpx;
}
.added-count {
color: #4cd964;
}
}
.daily-limit {
display: flex;
align-items: center;
font-size: 28rpx;
color: #666;
margin-bottom: 16rpx;
.limit-text {
width: 200rpx;
}
.limit-progress {
flex: 1;
position: relative;
height: 10rpx;
margin: 0 16rpx;
.progress-bg {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: #ebeef5;
border-radius: 10rpx;
}
.progress-value {
position: absolute;
left: 0;
top: 0;
height: 100%;
background-color: #4080ff;
border-radius: 10rpx;
}
}
.limit-count {
width: 100rpx;
text-align: right;
}
}
.device-info {
font-size: 26rpx;
color: #999;
.device-label, .last-active-label {
margin-right: 8rpx;
}
.device-value {
margin-right: 20rpx;
}
}
}
.wechat-action {
display: flex;
align-items: center;
.transfer-btn {
display: flex;
flex-direction: column;
align-items: center;
font-size: 24rpx;
color: #666;
}
}
}
}
</style>

View File

@@ -1,477 +0,0 @@
<template>
<view class="work-container">
<!-- 顶部标题 -->
<view class="header">
<text class="title">工作台</text>
</view>
<!-- 统计卡片 -->
<view class="stats-cards">
<view class="stats-card">
<view class="stats-label">总任务数</view>
<view class="stats-value blue">42</view>
<view class="progress-bar">
<view class="progress-filled blue" style="width: 70%;"></view>
</view>
<view class="stats-detail">
<text>进行中12 / 已完成30</text>
</view>
</view>
<view class="stats-card light-green">
<view class="stats-label">今日任务</view>
<view class="stats-value green">12</view>
<view class="trend-info">
<u-icon name="arrow-up" color="#2fc25b" size="20"></u-icon>
<text class="trend-text">活跃度 98%</text>
</view>
</view>
</view>
<!-- 常用功能 -->
<view class="section-title">常用功能</view>
<view class="function-grid">
<!-- 流量分发 -->
<view class="function-card" @click="navigateTo('/pages/traffic/index')">
<view class="icon-wrapper light-green">
<text class="icon">$</text>
</view>
<view class="function-content">
<view class="function-name">流量分发</view>
<view class="function-desc">定义你的流量价格</view>
</view>
</view>
<!-- 自动点赞 -->
<view class="function-card" @click="handleAutoLike">
<view class="icon-wrapper light-blue">
<u-icon name="thumb-up" color="#4080ff" size="28"></u-icon>
</view>
<view class="function-content">
<view class="function-name">自动点赞</view>
<view class="function-desc">定时对好友朋友圈点赞</view>
</view>
</view>
<!-- 朋友圈同步 -->
<view class="function-card" @click="handleMomentSync">
<view class="icon-wrapper light-purple">
<u-icon name="reload" color="#7551ff" size="28"></u-icon>
</view>
<view class="function-content">
<view class="function-name">朋友圈同步</view>
<view class="function-desc">多微信朋友圈同步发布</view>
</view>
</view>
<!-- 微信号管理 -->
<view class="function-card" @click="navigateTo('/pages/wechat/index')">
<view class="icon-wrapper light-green">
<u-icon name="weixin-fill" color="#48d2a0" size="28"></u-icon>
</view>
<view class="function-content">
<view class="function-name">微信号管理</view>
<view class="function-desc">管理已绑定的微信账号</view>
</view>
</view>
<!-- 群消息推送 -->
<view class="function-card" @click="handleGroupMessage">
<view class="icon-wrapper light-orange">
<u-icon name="chat" color="#ff9e45" size="28"></u-icon>
</view>
<view class="function-content">
<view class="function-name">群消息推送</view>
<view class="function-desc">批量向群内自动发消息</view>
</view>
</view>
<!-- 自动建群 -->
<view class="function-card" @click="handleAutoGroup">
<view class="icon-wrapper light-green">
<u-icon name="team" color="#48d2a0" size="28"></u-icon>
</view>
<view class="function-content">
<view class="function-name">自动建群</view>
<view class="function-desc">智能匹分好友建群</view>
</view>
</view>
<!-- AI话术助手 -->
<view class="function-card" @click="handleAIChatAssistant">
<view class="icon-wrapper light-blue">
<u-icon name="tv" color="#4080ff" size="28"></u-icon>
</view>
<view class="function-content">
<view class="function-name">AI话术助手</view>
<view class="function-desc">智能回复提高互动质量</view>
</view>
</view>
</view>
<!-- AI智能助手 -->
<view class="section-title">AI 智能助手</view>
<view class="function-grid">
<!-- AI数据分析 -->
<view class="function-card" @click="handleAIDataAnalysis">
<view class="icon-wrapper light-blue">
<u-icon name="tv" color="#4080ff" size="28"></u-icon>
</view>
<view class="function-content">
<view class="function-name">AI数据分析</view>
<view class="function-desc">智能分析客户行为特征</view>
</view>
</view>
<!-- AI策略优化 -->
<view class="function-card" @click="handleAIStrategy">
<view class="icon-wrapper light-cyan">
<u-icon name="tv" color="#36cfc9" size="28"></u-icon>
</view>
<view class="function-content">
<view class="function-name">AI策略优化</view>
<view class="function-desc">智能优化获客策略</view>
</view>
</view>
</view>
<!-- 内容库卡片 -->
<view class="func-card" @click="goToTraffic">
<view class="func-card-icon traffic-icon">
<u-icon name="man-add-fill" color="#fff" size="44"></u-icon>
</view>
<view class="func-card-info">
<text class="func-card-title">流量池</text>
<text class="func-card-desc">管理您的流量用户</text>
</view>
<view class="func-card-arrow">
<u-icon name="arrow-right" color="#ccc" size="28"></u-icon>
</view>
</view>
<view class="func-card" @click="goToContent">
<view class="func-card-icon content-icon">
<u-icon name="folder" color="#fff" size="44"></u-icon>
</view>
<view class="func-card-info">
<text class="func-card-title">内容库</text>
<text class="func-card-desc">管理微信内容素材</text>
</view>
<view class="func-card-arrow">
<u-icon name="arrow-right" color="#ccc" size="28"></u-icon>
</view>
</view>
<!-- 底部TabBar -->
<CustomTabBar active="work"></CustomTabBar>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
export default {
components: {
CustomTabBar
},
data() {
return {
// 将来可从API获取的数据
totalTasks: 42,
completedTasks: 30,
todayTasks: 12,
activeRate: 98
}
},
methods: {
// 页面导航
navigateTo(url) {
uni.navigateTo({
url: url
});
},
// 自动点赞功能处理
handleAutoLike() {
// 由于尚未实现该页面我们显示一个toast提示
this.showFunctionMessage('自动点赞');
},
// 朋友圈同步功能处理
handleMomentSync() {
this.showFunctionMessage('朋友圈同步');
},
// 群消息推送功能处理
handleGroupMessage() {
this.showFunctionMessage('群消息推送');
},
// 自动建群功能处理
handleAutoGroup() {
this.showFunctionMessage('自动建群');
},
// AI话术助手功能处理
handleAIChatAssistant() {
this.showFunctionMessage('AI话术助手');
},
// AI数据分析功能处理
handleAIDataAnalysis() {
this.showFunctionMessage('AI数据分析');
},
// AI策略优化功能处理
handleAIStrategy() {
this.showFunctionMessage('AI策略优化');
},
// 显示功能消息
showFunctionMessage(functionName) {
uni.showToast({
title: `${functionName}功能即将上线`,
icon: 'none',
duration: 2000
});
},
// 跳转到流量池页面
goToTraffic() {
uni.navigateTo({
url: '/pages/traffic/index'
});
},
// 跳转到内容库页面
goToContent() {
uni.navigateTo({
url: '/pages/content/index'
});
}
}
}
</script>
<style lang="scss" scoped>
.work-container {
min-height: 100vh;
background-color: #f9fafb;
padding: 0 30rpx 150rpx;
}
.header {
padding: 30rpx 0 20rpx;
.title {
font-size: 42rpx;
font-weight: 600;
color: #000;
}
}
.stats-cards {
display: flex;
margin: 20rpx 0 30rpx;
.stats-card {
flex: 1;
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx 24rpx;
margin-right: 15rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
&:last-child {
margin-right: 0;
background-color: #f5fffa;
}
.stats-label {
font-size: 28rpx;
color: #666;
margin-bottom: 10rpx;
}
.stats-value {
font-size: 50rpx;
font-weight: bold;
margin-bottom: 10rpx;
&.blue {
color: #4080ff;
}
&.green {
color: #2fc25b;
}
}
.progress-bar {
height: 10rpx;
background-color: #f0f0f0;
border-radius: 10rpx;
overflow: hidden;
margin-bottom: 10rpx;
.progress-filled {
height: 100%;
background-color: #4080ff;
border-radius: 10rpx;
&.blue {
background-color: #4080ff;
}
}
}
.stats-detail {
font-size: 24rpx;
color: #999;
}
.trend-info {
display: flex;
align-items: center;
margin-top: 10rpx;
.trend-text {
font-size: 24rpx;
color: #2fc25b;
margin-left: 6rpx;
}
}
}
}
.section-title {
font-size: 34rpx;
font-weight: 500;
color: #000;
margin: 30rpx 0 20rpx;
}
.function-grid {
display: flex;
flex-wrap: wrap;
margin: 0 -10rpx;
.function-card {
width: calc(50% - 20rpx);
background-color: #fff;
border-radius: 16rpx;
margin: 0 10rpx 20rpx;
padding: 24rpx 20rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
.icon-wrapper {
width: 80rpx;
height: 80rpx;
border-radius: 16rpx;
display: flex;
justify-content: center;
align-items: center;
margin-right: 16rpx;
&.light-green {
background-color: rgba(47, 194, 91, 0.1);
}
&.light-pink {
background-color: rgba(255, 87, 122, 0.1);
}
&.light-purple {
background-color: rgba(112, 102, 224, 0.1);
}
&.light-orange {
background-color: rgba(255, 153, 0, 0.1);
}
&.light-blue {
background-color: rgba(64, 128, 255, 0.1);
}
&.light-cyan {
background-color: rgba(54, 207, 201, 0.1);
}
.icon {
font-size: 34rpx;
color: #2fc25b;
font-weight: bold;
}
}
.function-content {
flex: 1;
.function-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.function-desc {
font-size: 24rpx;
color: #999;
}
}
}
}
.func-card {
display: flex;
align-items: center;
padding: 20rpx;
background-color: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
.func-card-icon {
width: 80rpx;
height: 80rpx;
border-radius: 16rpx;
display: flex;
justify-content: center;
align-items: center;
margin-right: 16rpx;
&.traffic-icon {
background-color: #4080ff;
}
&.content-icon {
background-color: #2fc25b;
}
}
.func-card-info {
flex: 1;
.func-card-title {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 8rpx;
}
.func-card-desc {
font-size: 24rpx;
color: #999;
}
}
.func-card-arrow {
width: 28rpx;
height: 28rpx;
margin-left: 16rpx;
}
}
</style>

View File

@@ -1,23 +0,0 @@
const path = require('path')
module.exports = {
parser: require('postcss-comment'),
plugins: [
require('postcss-import')({
resolve(id, basedir, importOptions) {
if (id.startsWith('~@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3))
} else if (id.startsWith('@/')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2))
} else if (id.startsWith('/') && !id.startsWith('//')) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1))
}
return id
}
}),
require('autoprefixer')({
remove: process.env.UNI_PLATFORM !== 'h5'
}),
require('@dcloudio/vue-cli-plugin-uni/packages/postcss')
]
}

View File

@@ -1,27 +0,0 @@
@font-face {
font-family: 'Digital-Bold';
src: url('https://cdn.jsdelivr.net/npm/alibaba-puhuiti@1.0.0/AlibabaPuHuiTi-Bold.ttf') format('truetype');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Digital-Medium';
src: url('https://cdn.jsdelivr.net/npm/alibaba-puhuiti@1.0.0/AlibabaPuHuiTi-Medium.ttf') format('truetype');
font-weight: normal;
font-style: normal;
font-display: swap;
}
.digital-text {
font-family: 'Digital-Bold', sans-serif;
font-weight: bold;
letter-spacing: 0.5px;
}
.digital-number {
font-family: 'Digital-Bold', sans-serif;
font-weight: bold;
letter-spacing: 0.5px;
}

View File

@@ -1,3 +0,0 @@
<!-- SVG 占位符,需要替换为实际的 Apple 图标 -->
<!-- 这是一个占位文件,实际应该放置 png 格式的 Apple 图标 -->
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAF0WlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4yLWMwMDAgNzkuMWI2NWE3OWI0LCAyMDIyLzA2LzEzLTIyOjAxOjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjQuMCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDMtMjNUMTU6NDc6MjgrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDMtMjNUMTU6NDc6MjgrMDg6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTAzLTIzVDE1OjQ3OjI4KzA4OjAwIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjY5OWIyZjI5LTM4ZTAtNDY4ZC1hMzA0LTNmOGQ2NjQ5MzM4YyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjY5OWIyZjI5LTM4ZTAtNDY4ZC1hMzA0LTNmOGQ2NjQ5MzM4YyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjY5OWIyZjI5LTM4ZTAtNDY4ZC1hMzA0LTNmOGQ2NjQ5MzM4YyIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjY5OWIyZjI5LTM4ZTAtNDY4ZC1hMzA0LTNmOGQ2NjQ5MzM4YyIgc3RFdnQ6d2hlbj0iMjAyMy0wMy0yM1QxNTo0NzoyOCswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI0LjAgKE1hY2ludG9zaCkiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+Af/+/fz7+vn49/b19PPy8fDv7u3s6+rp6Ofm5eTj4uHg397d3Nva2djX1tXU09LR0M/OzczLysnIx8bFxMPCwcC/vr28u7q5uLe2tbSzsrGwr66trKuqqainpqWko6KhoJ+enZybmpmYl5aVlJOSkZCPjo2Mi4qJiIeGhYSDgoGAf359fHt6eXh3dnV0c3JxcG9ubWxramloZ2ZlZGNiYWBfXl1cW1pZWFdWVVRTUlFQT05NTEtKSUhHRkVEQ0JBQD8+PTw7Ojk4NzY1NDMyMTAvLi0sKyopKCcmJSQjIiEgHx4dHBsaGRgXFhUUExIREA8ODQwLCgkIBwYFBAMCAQAAIfkEBQQAAAAsAAAAABAAEAAAAkCcfwCAGQaicDjrIO9zKvRHcd0HjuNZoueZpurnLlr2vSr3TpzneY7jeMrxnGdZ1jOtZ3rW9bzvfOcb5znfO+d5CQA7

View File

@@ -1 +0,0 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAACXBIWXMAAAsTAAALEwEAmpwYAAALF0lEQVR4nO1da4hkVxX+9kyGYBJREw0KGtSQGBWD+MMojO6AEklIVBSM+g/xB5I4M1XVnT2JERFJouJPUYkBFRUfSVQQUcnEeXR3Ve/d3TUddU0wGk2IRB/4nM6dWVPTPdN963FP1fmg6E7fulV1vrPOPvvsc/YphUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhoIWyKsv6/eZZZRme7/fD3aVp/E5RtN4qIu4uy/D6sgyvL8v4gqLovs2KKLEZqbOz1bPC4Cc+6e99CXGnKTpvOQA46cuy7LyuKMLHTk7GPSKMUYIyNdU9xwXhR/FJnK33Zyxgt3grzE8JzYMi0nOqoJdleKFLxAM2CU+VRnhfUbRO1+BkBsrWfQQu+aIIb7ZK+BOJ4PoSjrCZXy5CL4gQ7pFULLB9f6fVaj7DN1KmQLlRQB0RUeoApUbImRvUK+LETyrDPeK4ixz+3f9Oe0xAERneUISw3YVjl1I+FOJB29/VfF1C/Ift9qbX+jZVSxkgV7XODJ3wJx2AnbZfF/1O/KJm+3ujiYSX+H3xhNsU/9d26wWbhPBvFEe7s/MaPynZ46wz2judfqfX/v0KgbXj+c6YmQvhA34vFcLk1EbFR02SXdERSXnGJa15/7JtP0Tx/mf7tY+p2t3rvFJC2OF8Vfg/YGp8u90919e2AiTnzLEsw10rTjqc9NF2+7kWeNRt2+NjHt9Ps1OZp1ILykrjbpb9vtcWUHKlTclEPVeF9dq2X/5eAc0BsX073HJ/14PhVLGhCd6gA6A5tztXSYAPxVdwZb4Ym5Ls8xzALwQQWvWo0nPE8Ds+9lFhRMITb/n3jI9V4SFXCSG5B85SBsjYWOdsF4gfnYQQPuPK8J8j9J27PYQnf+p2PebjDiN85Osd3ue/L5aK+/xlbHk9sU3LZwAc3W6W/UvYfyshfcCRHpjudlvnS6Z5NKvb3UtTpiyK+OKiCL/tH5YnlwYJ4bUaZFzdeoHYZ/sMrQ3/O44X/eD9ByN8zzd6YnxscnLMqc1LXJl+Vt6e6HS6l/o1vdoVv1tS3aEHZHR08z6B8LLg19cVK4RdLpRPesj9txLS3xnC3SJylq/3BXyDSJxilbg3pLrH/eZZlrHNj/9GbJbfH6ELmB+LCy1qTzZG8iUg9TGfq7sDkbCPQuJTnLBgH/y8hPgNPsBN0sL9nHxAJmbi+f7gHXYN9/sVv3riqDtXGMPrUvdcAgjtxMSVZwvEA/z9EF3I/DijFNd0fsbBdgF9uoxXujZU/5+YaG2RVCQl5n/sn7tDQvZCwj8MQZaRXC9V9h8YOD4uSWkA0T+/Xl7Q79OHMORmxW3+yXs0Wkxzo6BZMX/ygUqgOcxSV/7VKYb3+Ocf5qgzW8uPnRIY9oO4cULkRaVJyvK2f+j+ZTM6FfoOGpiYuHGm22291qryRhHxnfIwvvvVEX+zYjuFODM+3jnNb+i9zmA/7qXCl9mvN8iQvtffqJI+wzfBe8ZdYZMbzP5QiHo5Ui2fqRzBuoCYFG2jYNjvS4iPyLTm/vzYjJt1GUrdx/p3/5l9Vma4c9Wz/GZL4mvgMykGQNgGdoFS9p1r5JnYWV5Gs+N1N6yLUhfB3FDvUf6z1sJCfI29OfmYfXPkYJQ1hHTGQuhV6sP1KifFBQ5hNdzG5ZjE1Z9Mf1X5qfAV2xef1JVg0Bqr/0f8vaeP+Pw9AhCjq3gNs7wqXLr/Ue/vLY7LWMEoAxKuY5+q9fQTvH788WYuEJtOuZtXxR17FJO+Xa08Bf7AYOVaCvHfcrteiJtkwLjAvUdCfMA9+f+Svj1RxBvHxpqX+A27ztfYJZV6a2qzYgpTLK81MLrL9c7+ZIGTcAcDmGuZj7C3Tm1ZQ7RDiLdv6rZea8+J2/e8hEhVu4D5pNJMD0goyVapjFJXgJjV1f2XwjZrA3INq9qUTz8uK13EzO2jYvyoCpw+RVKytk1HXlbWIHSPPg0r8VT/UPwRfSdUx6Yo2YNgD5sFO1qLgDBzq9oWHIi7WB05Fb96oEMI5WDvuVAHxUa2ISD8AJo/LdpQAkL1+a+y7MadUpgfaHTJjnC1gPCnx41IXxOL8EgTR0vy9wO9LwvF1VFDQNgwZGmNfehndvfT4FcnAyA37EZAmOpb4UYzISAsuYnFWNTl4QeOqISsC0BM95d7JyYmmhAQZl1VHf2nvQ+Gg6e9QLgWaYGQM5B1AcjUVOtsrlvxzqrBMZNFDVIVxO/uLxAmozf7QO3o+ICQp3MBSOEDDZyYHNQgt3JmBITLUbVWXDVe1NcP7w8IcwkjAGQ/mTQJIUwyljNKwh73lLsVDPo/Sd4GdxgA4RM9sBGK8hQhYyAXqBLCKY83jY3FR9sBgZxuFwt6pWKEfTJRgw8Ii3drzxb3Asg+QvzGiQBh0b8LyPdPGBDOnDglzm03J+Lj2wkEkjOWsvXO7HTe4KbskwHyDxeQ1roHhPyqXgDJCQgZLrWRAgm3ZQ2EtUlUVBYrj6QBIYMmy/EQJuP4gMuYLfA2Ub9qwxTXkfkC4YTWiVXL9fHWJPGxDJMpCu4jP8uaZjc0QEaOV0P0Asgg86yOOQQsUGXPeoH9pSgOgJFzfLZWULjSxJWSVNqbk+VrSvUMTAi7ixcO/J6OIQzVVSYLiwKQYCjpQPYGhAvS+/u9FUPMPShDmCbgrq1/5VbGIl9OmBTvwQU9JzjG7Hq7RDz8PvDJZQiEA7L9lDmzm4/0GFgv+fC+M1eKvf5Uf8mW/+xH90Tn7MuBJfX83yxH6acXuPTLfYXeRyeDBRn+uHQ2M++Xk4TwW0bZ/SaOrOBz3e02B9jRlTXZpnw3cLc/jm3qdq4Q8VpfuvVODZU4QkBWhV+fWNn5wX0+JOu9MHRJbFZDCIgqQNYJAVHXGMiJABnGaG4vgDDf4xtSbkBO0DYw+TeFJqPrIR51e/eqoWTLrHD5UaZb0GvmNNQWU8tnpzJ3LiB3D3Ht2VozKFV7WXwwRwEIexZOtNzcGxBObK4nQJiQxJOzwefN4wnOzZ6qWz8+3nnhMJVIWNzEBv9+AFE6kG8QhvwbW0P459nMD7U/o3VfF4CQ58pCKAsB2G0xKyC+b4v0HO7jMhSAuCkYCKp2QDwlRZ46HTmqKGsgyy+F4D4v3N6H1JHMgHDD2fU0uWrTdGmAkGPPg+O+x9H83TCGlUtA3OpcQy2xEZMXdMJAliPvq0lUqzPCupdx1RMAYVEqu6PZ2OAOKAEHhB1RvJF1/p+vNBG3jfF9X6kj6w4QduLTTZxWUXQwAGHbPltduFp9ugHEz+dJ72rniYR0fczWEQMgrFGRSpW1AcLOd7Z78E2o+w0QL1a9jk/1iQBhxzuL7NL0aqcA4h1Yq/x9OtxfgMwupv+xLpnq8+8u+nZIDzrW2FZtWbO9NrPjsIYrnmZJr8MpgHQp7hDpd4DLTgEL6xXu5s6FGlLaqw+xvZ5bnZA31ms/V0/7BRRFo55pQ24Yk9F0lp7s+x9u7Z7srkJgkgzrS8YJijngOWAzzDs6xCEAAA6dSURBVCj/l1C/lW/r1A4a/iJjhUKhUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoVCoVAoFAqFQqFQKBQKBRn/A1JxgAhWnMiQAAAAAElFTkSuQmCC

View File

@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 转化漏斗 -->
<path d="M24 24H56L44 44V56L36 60V44L24 24Z"
stroke="#4080ff"
stroke-width="3"
fill="none"/>
<!-- 数据流动线 -->
<path d="M28 28L32 32M36 28L40 32M44 28L48 32"
stroke="#4080ff"
stroke-width="2"
stroke-linecap="round"/>
<!-- 转化指示箭头 -->
<path d="M34 48L40 52L46 48"
stroke="#4080ff"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"/>
<!-- 外圈效果 -->
<circle cx="40" cy="40" r="28"
stroke="#4080ff"
stroke-width="2"
stroke-dasharray="4 4"
fill="none"/>
<!-- 装饰点 -->
<circle cx="32" cy="32" r="2" fill="#4080ff"/>
<circle cx="40" cy="32" r="2" fill="#4080ff"/>
<circle cx="48" cy="32" r="2" fill="#4080ff"/>
</svg>

Before

Width:  |  Height:  |  Size: 978 B

View File

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 大脑图形 -->
<path d="M40 20C48.8366 20 56 27.1634 56 36C56 44.8366 48.8366 52 40 52C31.1634 52 24 44.8366 24 36C24 27.1634 31.1634 20 40 20Z"
stroke="#4080ff"
stroke-width="3"
fill="none"/>
<!-- 连接线路 -->
<path d="M32 32C36 36 44 36 48 32M36 40C40 44 44 40 48 44"
stroke="#4080ff"
stroke-width="3"
stroke-linecap="round"/>
<!-- 圆点装饰 -->
<circle cx="32" cy="32" r="2" fill="#4080ff"/>
<circle cx="48" cy="32" r="2" fill="#4080ff"/>
<circle cx="36" cy="44" r="2" fill="#4080ff"/>
<circle cx="48" cy="44" r="2" fill="#4080ff"/>
<!-- 外圈光环效果 -->
<circle cx="40" cy="36" r="24"
stroke="#4080ff"
stroke-width="2"
stroke-dasharray="4 4"
fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 947 B

View File

@@ -1,51 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 中心控制器 -->
<rect x="32" y="32" width="16" height="16" rx="4"
stroke="#4080ff"
stroke-width="3"
fill="none"/>
<!-- 连接的设备 -->
<circle cx="24" cy="24" r="6"
stroke="#4080ff"
stroke-width="2.5"
fill="none"/>
<circle cx="56" cy="24" r="6"
stroke="#4080ff"
stroke-width="2.5"
fill="none"/>
<circle cx="24" cy="56" r="6"
stroke="#4080ff"
stroke-width="2.5"
fill="none"/>
<circle cx="56" cy="56" r="6"
stroke="#4080ff"
stroke-width="2.5"
fill="none"/>
<!-- 连接线 -->
<line x1="28" y1="28" x2="32" y2="32"
stroke="#4080ff"
stroke-width="2"
stroke-linecap="round"/>
<line x1="52" y1="28" x2="48" y2="32"
stroke="#4080ff"
stroke-width="2"
stroke-linecap="round"/>
<line x1="28" y1="52" x2="32" y2="48"
stroke="#4080ff"
stroke-width="2"
stroke-linecap="round"/>
<line x1="52" y1="52" x2="48" y2="48"
stroke="#4080ff"
stroke-width="2"
stroke-linecap="round"/>
<!-- 脉冲圆环 -->
<circle cx="40" cy="40" r="28"
stroke="#4080ff"
stroke-width="2"
stroke-dasharray="4 4"
fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 45 41" width="45" height="41"><g stroke-width="2" fill="none" stroke-linecap="butt" stroke="#95bcfb" data-c-stroke="95bcfb"><path d="M 26.80 15.12 A 1.41 1.41 0.0 0 1 25.89 14.20 L 23.36 6.16 A 1.41 1.41 0.0 0 0 20.67 6.16 L 18.11 14.19 A 1.41 1.41 0.0 0 1 17.19 15.10 L 9.15 17.63 A 1.41 1.41 0.0 0 0 9.15 20.32 L 17.18 22.88 A 1.41 1.41 0.0 0 1 18.09 23.80 L 20.62 31.84 A 1.41 1.41 0.0 0 0 23.31 31.84 L 25.87 23.81 A 1.41 1.41 0.0 0 1 26.79 22.90 L 34.83 20.37 A 1.41 1.41 0.0 0 0 34.83 17.68 L 26.80 15.12"/><path d="M28.11 10.25Q28.11 11.47 30.14 11.46A.56.56 0 0 1 30.7 12.03Q30.69 13.98 31.99 13.98 33.29 13.98 33.28 12.04A.56.56 0 0 1 33.85 11.47Q35.88 11.49 35.88 10.26 35.88 9.04 33.85 9.05A.56.56 0 0 1 33.29 8.48Q33.31 6.54 32 6.53 30.7 6.53 30.71 8.48A.56.56 0 0 1 30.14 9.04Q28.11 9.03 28.11 10.25M14.4003 27.0011A.76.76 0 0 0 14.3853 25.9265L12.507 24.0999A.76.76 0 0 0 11.4323 24.1149L9.6197 25.9789A.76.76 0 0 0 9.6347 27.0535L11.513 28.8801A.76.76 0 0 0 12.5877 28.8651L14.4003 27.0011M12.63 19.45Q15.63 19.39 18.39 20.52A3.79 3.77-86.1 0 1 20.62 23.09L21.72 27.43A.3.3 0 0 0 22.31 27.41Q22.75 25.03 23.51 22.75C24.4 20.07 27.56 19.89 30.66 19.23A.27.27 0 0 0 30.63 18.69Q28.17 18.46 25.31 17.32C23.17 16.47 22.95 12.98 22.35 10.8A.31.31 0 0 0 21.75 10.8C21.19 12.95 20.54 17.14 18.1 17.65Q13.95 18.52 12.56 19.17A.15.15 0 0 0 12.63 19.45"/></g><path fill="#eff6ff" d="M 45.00 0.00 L 45.00 41.00 L 0.00 41.00 L 0.00 0.00 L 45.00 0.00 Z M 26.80 15.12 A 1.41 1.41 0.0 0 1 25.89 14.20 L 23.36 6.16 A 1.41 1.41 0.0 0 0 20.67 6.16 L 18.11 14.19 A 1.41 1.41 0.0 0 1 17.19 15.10 L 9.15 17.63 A 1.41 1.41 0.0 0 0 9.15 20.32 L 17.18 22.88 A 1.41 1.41 0.0 0 1 18.09 23.80 L 20.62 31.84 A 1.41 1.41 0.0 0 0 23.31 31.84 L 25.87 23.81 A 1.41 1.41 0.0 0 1 26.79 22.90 L 34.83 20.37 A 1.41 1.41 0.0 0 0 34.83 17.68 L 26.80 15.12 Z M 28.11 10.25 Q 28.11 11.47 30.14 11.46 A 0.56 0.56 0.0 0 1 30.70 12.03 Q 30.69 13.98 31.99 13.98 Q 33.29 13.98 33.28 12.04 A 0.56 0.56 0.0 0 1 33.85 11.47 Q 35.88 11.49 35.88 10.26 Q 35.88 9.04 33.85 9.05 A 0.56 0.56 0.0 0 1 33.29 8.48 Q 33.31 6.54 32.00 6.53 Q 30.70 6.53 30.71 8.48 A 0.56 0.56 0.0 0 1 30.14 9.04 Q 28.11 9.03 28.11 10.25 Z M 14.4003 27.0011 A 0.76 0.76 0.0 0 0 14.3853 25.9265 L 12.5070 24.0999 A 0.76 0.76 0.0 0 0 11.4323 24.1149 L 9.6197 25.9789 A 0.76 0.76 0.0 0 0 9.6347 27.0535 L 11.5130 28.8801 A 0.76 0.76 0.0 0 0 12.5877 28.8651 L 14.4003 27.0011 Z" data-c-fill="eff6ff" fill-opacity="0"/><path fill="#3b82f6" d="M 26.80 15.12 L 34.83 17.68 A 1.41 1.41 0.0 0 1 34.83 20.37 L 26.79 22.90 A 1.41 1.41 0.0 0 0 25.87 23.81 L 23.31 31.84 A 1.41 1.41 0.0 0 1 20.62 31.84 L 18.09 23.80 A 1.41 1.41 0.0 0 0 17.18 22.88 L 9.15 20.32 A 1.41 1.41 0.0 0 1 9.15 17.63 L 17.19 15.10 A 1.41 1.41 0.0 0 0 18.11 14.19 L 20.67 6.16 A 1.41 1.41 0.0 0 1 23.36 6.16 L 25.89 14.20 A 1.41 1.41 0.0 0 0 26.80 15.12 Z M 12.63 19.45 Q 15.63 19.39 18.39 20.52 A 3.79 3.77 -86.1 0 1 20.62 23.09 L 21.72 27.43 A 0.30 0.30 0.0 0 0 22.31 27.41 Q 22.75 25.03 23.51 22.75 C 24.40 20.07 27.56 19.89 30.66 19.23 A 0.27 0.27 0.0 0 0 30.63 18.69 Q 28.17 18.46 25.31 17.32 C 23.17 16.47 22.95 12.98 22.35 10.80 A 0.31 0.31 0.0 0 0 21.75 10.80 C 21.19 12.95 20.54 17.14 18.10 17.65 Q 13.95 18.52 12.56 19.17 A 0.15 0.15 0.0 0 0 12.63 19.45 Z" data-c-fill="3b82f6"/><path fill="#3b82f6" d="M 32.00 6.53 Q 33.31 6.54 33.29 8.48 A 0.56 0.56 0.0 0 0 33.85 9.05 Q 35.88 9.04 35.88 10.26 Q 35.88 11.49 33.85 11.47 A 0.56 0.56 0.0 0 0 33.28 12.04 Q 33.29 13.98 31.99 13.98 Q 30.69 13.98 30.70 12.03 A 0.56 0.56 0.0 0 0 30.14 11.46 Q 28.11 11.47 28.11 10.25 Q 28.11 9.03 30.14 9.04 A 0.56 0.56 0.0 0 0 30.71 8.48 Q 30.70 6.53 32.00 6.53 Z" data-c-fill="3b82f6"/><path fill="#eff6ff" d="M 12.56 19.17 Q 13.95 18.52 18.10 17.65 C 20.54 17.14 21.19 12.95 21.75 10.80 A 0.31 0.31 0.0 0 1 22.35 10.80 C 22.95 12.98 23.17 16.47 25.31 17.32 Q 28.17 18.46 30.63 18.69 A 0.27 0.27 0.0 0 1 30.66 19.23 C 27.56 19.89 24.40 20.07 23.51 22.75 Q 22.75 25.03 22.31 27.41 A 0.30 0.30 0.0 0 1 21.72 27.43 L 20.62 23.09 A 3.79 3.77 -86.1 0 0 18.39 20.52 Q 15.63 19.39 12.63 19.45 A 0.15 0.15 0.0 0 1 12.56 19.17 Z" data-c-fill="eff6ff" fill-opacity="0"/><rect fill="#3b82f6" x="-2.07" y="-2.06" transform="rotate(44.2 -26.614 28.034)" width="4.14" height="4.12" rx=".76" data-c-fill="3b82f6"/></svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="6.5" stroke="#e9e9e9" stroke-width="1.5" stroke-dasharray="1 2"/>
<circle cx="8" cy="8" r="2" fill="#e9e9e9"/>
</svg>

Before

Width:  |  Height:  |  Size: 282 B

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 5L9 12V19L15 21V12L21 5H3Z"
stroke="#333333"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="none" />
</svg>

Before

Width:  |  Height:  |  Size: 324 B

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 34 H18 L22 24 L30 44 L38 14 L46 44 L50 34 H60"
stroke="#4080ff"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
fill="none" />
<path d="M5 34 H15 L19 24 L27 44 L35 14 L43 44 L47 34 H63"
stroke="#4080ff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
opacity="0.3"
fill="none" />
</svg>

Before

Width:  |  Height:  |  Size: 566 B

View File

@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 门框 -->
<path d="M20 4H8C6.89543 4 6 4.89543 6 6V26C6 27.1046 6.89543 28 8 28H20"
stroke="#ff3c2a"
stroke-width="2"
stroke-linecap="round"/>
<!-- 箭头 -->
<path d="M26 16H12M26 16L21 11M26 16L21 21"
stroke="#ff3c2a"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 508 B

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 机器人头部 -->
<rect x="8" y="4" width="12" height="14" rx="2" stroke="#4080ff" stroke-width="2" fill="none"/>
<!-- 天线 -->
<path d="M14 2L14 4" stroke="#4080ff" stroke-width="2" stroke-linecap="round"/>
<!-- 眼睛 -->
<circle cx="11" cy="9" r="1.5" fill="#4080ff"/>
<circle cx="17" cy="9" r="1.5" fill="#4080ff"/>
<!-- 显示屏/嘴巴 -->
<rect x="10" y="12" width="8" height="3" rx="1" stroke="#4080ff" stroke-width="1.5" fill="none"/>
<!-- 机器人身体 -->
<rect x="6" y="18" width="16" height="6" rx="2" stroke="#4080ff" stroke-width="2" fill="none"/>
<!-- 按钮/指示灯 -->
<circle cx="11" cy="21" r="1" fill="#4080ff"/>
<circle cx="14" cy="21" r="1" fill="#4080ff"/>
<circle cx="17" cy="21" r="1" fill="#4080ff"/>
</svg>

Before

Width:  |  Height:  |  Size: 913 B

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 20C18 17.7909 19.7909 16 22 16H46C48.2091 16 50 17.7909 50 20V48C50 50.2091 48.2091 52 46 52H22C19.7909 52 18 50.2091 18 48V20Z"
stroke="#4080ff"
stroke-width="4"
stroke-linecap="round"
stroke-linejoin="round"
fill="none" />
<line x1="18" y1="44" x2="50" y2="44"
stroke="#4080ff"
stroke-width="3"
stroke-linecap="round" />
<line x1="18" y1="22" x2="50" y2="22"
stroke="#4080ff"
stroke-width="3"
stroke-linecap="round" />
<rect x="30" y="46" width="8" height="3" rx="1.5"
stroke="#4080ff"
stroke-width="2"
fill="none" />
</svg>

Before

Width:  |  Height:  |  Size: 806 B

View File

@@ -1,35 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 中间大人像 -->
<circle cx="34" cy="25" r="7.5"
stroke="#4080ff"
stroke-width="3.5"
fill="none" />
<path d="M22 49c0-6.6 5.4-12 12-12s12 5.4 12 12"
stroke="#4080ff"
stroke-width="3.5"
stroke-linecap="round"
fill="none" />
<!-- 左侧小人像 -->
<circle cx="18" cy="29" r="5"
stroke="#4080ff"
stroke-width="3"
fill="none" />
<path d="M10 47c0-4.4 3.6-8 8-8s8 3.6 8 8"
stroke="#4080ff"
stroke-width="3"
stroke-linecap="round"
fill="none" />
<!-- 右侧小人像 -->
<circle cx="50" cy="29" r="5"
stroke="#4080ff"
stroke-width="3"
fill="none" />
<path d="M42 47c0-4.4 3.6-8 8-8s8 3.6 8 8"
stroke="#4080ff"
stroke-width="3"
stroke-linecap="round"
fill="none" />
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 17L9 11L13 15L21 7" stroke="#2fc25b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 7H21V14" stroke="#2fc25b" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 370 B

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="8" r="4" stroke="#e9e9e9" stroke-width="1.5" fill="none"/>
<path d="M6 20C6 16.6863 8.68629 14 12 14C15.3137 14 18 16.6863 18 20"
stroke="#e9e9e9"
stroke-width="1.5"
stroke-linecap="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 384 B

View File

@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<!-- 第一个用户图标 -->
<circle cx="24" cy="21" r="7"
stroke="#4080ff"
stroke-width="3.5"
fill="none" />
<path d="M14 46C14 39.4 19.4 34 26 34H28C34.6 34 40 39.4 40 46"
stroke="#4080ff"
stroke-width="3.5"
stroke-linecap="round"
fill="none" />
<!-- 第二个用户图标(小一些,叠加效果) -->
<circle cx="44" cy="21" r="6"
stroke="#4080ff"
stroke-width="3"
fill="none" />
<path d="M34 46C34 39.4 39.4 34 46 34H48C54.6 34 60 39.4 60 46"
stroke="#4080ff"
stroke-width="3"
stroke-linecap="round"
fill="none" />
</svg>

Before

Width:  |  Height:  |  Size: 823 B

View File

@@ -1,21 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="68" height="68" viewBox="0 0 68 68" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M46.4 39.2c0-7.8-7.8-14.2-16.9-14.2s-16.9 6.3-16.9 14.2c0 7.8 7.8 14.2 16.9 14.2 2 0 3.9-0.3 5.7-0.8l5.2 2.9c0.1 0.1 0.3 0.1 0.4 0.1 0.2 0 0.3-0.1 0.4-0.2 0.2-0.2 0.2-0.5 0-0.7l-4-4.5c5.8-2.4 9.2-7.1 9.2-11z"
stroke="#4080ff"
stroke-width="3.5"
stroke-linejoin="round"
fill="none" />
<path d="M28 32.8c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2z"
fill="#4080ff" />
<path d="M35 32.8c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2z"
fill="#4080ff" />
<path d="M55.4 24.2c0-6.2-6.6-11.2-14.6-11.2-8 0-14.6 5-14.6 11.2 0 6.2 6.6 11.2 14.6 11.2 1.6 0 3.2-0.2 4.7-0.6l3.2 2.3c0.1 0.1 0.3 0.1 0.4 0.1 0.1 0 0.3-0.1 0.4-0.2 0.2-0.2 0.2-0.4 0-0.6l-2.5-3.5c4.3-2.1 8.4-5 8.4-8.7z"
stroke="#4080ff"
stroke-width="3.5"
stroke-linejoin="round"
fill="none" />
<path d="M37 23c-0.8 0-1.5 0.7-1.5 1.5s0.7 1.5 1.5 1.5 1.5-0.7 1.5-1.5-0.7-1.5-1.5-1.5z"
fill="#4080ff" />
<path d="M44 23c-0.8 0-1.5 0.7-1.5 1.5s0.7 1.5 1.5 1.5 1.5-0.7 1.5-1.5-0.7-1.5-1.5-1.5z"
fill="#4080ff" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1 +0,0 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEwAACxMBAJqcGAAABX5JREFUeJzt3U2IXGUUxvH/TCYkThLTqMVCoV0oBBcuChVcuHIjCFZw6c6NQlwLQkHowoUiQhcKRRDBlRsRioguBUEEQRAERRQEKX5UxdTYJE0nXw7cQrFkct+Ze+55vw/MMwi995znndOmEySYSAiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiIiItKcfusF7GQQbAGHgTuBW4BDwE3AQeAGYA1YBxaBzcnX34EfgfPAD8AZ4DRwGthMXbzsyRRxMBxVewOeBA4D9wMP1X7wGXwDfAF8DnwInPNdTnscTEcTHG5p2qzaG/AccBQ40mQJue0A7wEfAO8C59zWUZuD8WiCwy1NO1Z7A14AngWmm/rQAv4F3gWOAx97LybC3XnN8TiyuOVpZ2rvxEvARjMf1MZOACeJf3Gr9ZOcgyMh/+SWp52qvRNHgdm6H9CQ7YELwDHgdde1Tbg0Vo7Qd0vuNdTejbeBtxr4jFacAr4HxsDXzBOArdaGzwDTwNXAdcDVwF3A/cC9+T+iGR8DLwLfuq4s4mTIx6WxcoS+W3KvoXbxbwMn6Z6vgVeAj1zXdal2oXYf8SQbE5/k61x4B3gd+Ml1ZREeJ8dEd0vuNdQu/qPAdt0PcbYDvA8cxvX1yiWahdpV3A28OvnfMfBZrkdP2CY+uX/uuqqIZyO+xnx3S+4n1I72AJ9W/QBHm8TXxhdxHiZ28I5ys3eUd5SH0T7sJu4aDyP8UD7i64h3QRjKwT5UvPccvED8TnqtgY9pw3vAPcDXriuL+CniaxzeUZ5GeLCPFe8/B2Pgywb2cAw837b5aAdZYN5Rvj4+Pv6U3WvIcTzYx4r334MxcKbCDubOGHgJVa0J1wE7Od5RbkcY7EXxfnzwN3Bzyd3LcCfxSk3jDdw1pou8ozyPMNiL4v34YQv4rcDeZbk/cB9Vq5jLCvyOcofBIB/Dfa24Lx9s0O0fz/1O4D6qVpGFAr9j36E8kDDci+L+/PAn8H3JPSyhDbfEqVql5QYf6MYID/ay4v58sQWcLrx/1vYauHeqVnG5wQd7UdyXL3aAjwr3z5KqVU5u8MFeFvfljz3go8L9s6JqZdfM8YzwRngjvHG5wd+3q7g/n5wDfor3z4KqldXIZCx33t/c4O/bVdyjTzaZoyPGrKhadg0dzyC3Ed4Ib8zH/Vn1V+HemVO17Fxfk+cG/4DZi3v0ywzwR+H+mVK1xjRzPOeVG/wDZi/u0TeHgF8L98+MqjWmoeM579zgHzB7cY++me3ACadqjWnueO7LDf6BdRf36p9t2v/3r6rWmGsaOp7Lyg3+AfYg7tU/swP+r6NWqFqOxzMzN/gH2sdCNfCxRfs/L65qOWm4FDtyg3+g/SS0WDfXF+6fkarloKFS7MoN/oH201C97ttr+UfCWqFqFdZAKfbkBv9A+02wZhnWS35GoFaqVkFFSnEgN/gH2u9CNcuzQvvvZqladRUoxYHc4B9o/zW5zTquCd2n21QtpdgXa1kj7tFAe67swXuZqiUXsbZFvLs0/wfvkKqlFwtZo9A90p0HRuV2MJ2qpRcLGcN1XuDO06l2VCwVSy8WMEexe6Q7T6jYV7FULGOm0LvHR+V2MC1VSy8WMEexe6Q7T6jYV7FULIO43jvNHQZVS7TvgnJFcb13mjsMqpZo3wVli+I67zR3GFQt0b4LyhXFdd5p7jCoWqJ9F5QriOu709xhULVE+y4oWxDXd6e5w7D8bxhULdG+C8oVxPXdae4waJTvwKtaejFDriCu705zh0HV0osZcgVxfXeaOwyqll7MkCuI67vT3GFQtfRihlxBXN+d5g6DqqUXM+QK4vruNHcYVC29mKG/AO0MF6TIrRxLAAAAAElFTkSuQmCC

View File

@@ -1,26 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="200" height="200" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="200" height="200" rx="10" fill="#F5F5F5"/>
<!-- 二维码方块 -->
<rect x="50" y="50" width="20" height="20" fill="#CCCCCC"/>
<rect x="70" y="50" width="20" height="20" fill="#CCCCCC"/>
<rect x="90" y="50" width="20" height="20" fill="#CCCCCC"/>
<rect x="130" y="50" width="20" height="20" fill="#CCCCCC"/>
<rect x="50" y="70" width="20" height="20" fill="#CCCCCC"/>
<rect x="110" y="70" width="20" height="20" fill="#CCCCCC"/>
<rect x="130" y="70" width="20" height="20" fill="#CCCCCC"/>
<rect x="50" y="90" width="20" height="20" fill="#CCCCCC"/>
<rect x="90" y="90" width="20" height="20" fill="#CCCCCC"/>
<rect x="110" y="90" width="20" height="20" fill="#CCCCCC"/>
<rect x="70" y="110" width="20" height="20" fill="#CCCCCC"/>
<rect x="110" y="110" width="20" height="20" fill="#CCCCCC"/>
<rect x="130" y="110" width="20" height="20" fill="#CCCCCC"/>
<rect x="50" y="130" width="20" height="20" fill="#CCCCCC"/>
<rect x="70" y="130" width="20" height="20" fill="#CCCCCC"/>
<rect x="90" y="130" width="20" height="20" fill="#CCCCCC"/>
<rect x="130" y="130" width="20" height="20" fill="#CCCCCC"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -1 +0,0 @@
<!-- 首页 tabbar 激活状态图标占位,需要替换为实际的图片 -->

View File

@@ -1 +0,0 @@
<!-- 首页 tabbar 图标占位,需要替换为实际的图片 -->

View File

@@ -1 +0,0 @@
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGsWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4yLWMwMDAgNzkuMWI2NWE3OWI0LCAyMDIyLzA2LzEzLTIyOjAxOjAxICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjQuMCAoTWFjaW50b3NoKSIgeG1wOkNyZWF0ZURhdGU9IjIwMjMtMDMtMjNUMTU6NDc6MjgrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjMtMDMtMjNUMTU6NDc6MjgrMDg6MDAiIHhtcDpNb2RpZnlEYXRlPSIyMDIzLTAzLTIzVDE1OjQ3OjI4KzA4OjAwIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjY5OWIyZjI5LTM4ZTAtNDY4ZC1hMzA0LTNmOGQ2NjQ5MzM4YyIgeG1wTU06RG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjY5OWIyZjI5LTM4ZTAtNDY4ZC1hMzA0LTNmOGQ2NjQ5MzM4YyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ4bXAuZGlkOjY5OWIyZjI5LTM4ZTAtNDY4ZC1hMzA0LTNmOGQ2NjQ5MzM4YyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjY5OWIyZjI5LTM4ZTAtNDY4ZC1hMzA0LTNmOGQ2NjQ5MzM4YyIgc3RFdnQ6d2hlbj0iMjAyMy0wMy0yM1QxNTo0NzoyOCswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI0LjAgKE1hY2ludG9zaCkiLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDxwaG90b3Nob3A6RG9jdW1lbnRBbmNlc3RvcnM+IDxyZGY6QmFnPiA8cmRmOmxpPmFkb2JlOmRvY2lkOnBob3Rvc2hvcDo2OTliMmYyOS0zOGUwLTQ2OGQtYTMwNC0zZjhkNjY0OTMzOGM8L3JkZjpsaT4gPC9yZGY6QmFnPiA8L3Bob3Rvc2hvcDpEb2N1bWVudEFuY2VzdG9ycz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7Z5J8XAAAD6UlEQVRoge2ZTWhcVRTHf+fNTJJJk2nTNG0SY2PTD9uQVrQU0UJFpLhwoYjduBTEheDCnQvBlbhzI4ILQXDhxoWoCAqCKFLBWlu1VE2b1tA0bZKmSZN5M/PePS7ee/PmY2YyH0ky6R9u7n3n3I97/+fee859T1SVZQwBPg38CPwNvL1UvbEIuBt4AzCBd4HbgKeWojOxwH3WAD1AK9AOfASMAV8Bty9mp5YiAl7UArcBe4DXgB+AJ4Fji9GZpRYwH3XALuBZ4DPgILB1oTpcagEBoIBhwfAD9wMvAl8DB4A7FqKj5RKQBiaAaSAPCNAEPAS8BHwDPAs0XEyH5RLQD7wPvAUcBk4Ck0AKyOGI2QK8DHwLPAPUL7TT5UQtsBt4HvgG+BjYgSMmAaSBLI6gLcArwHfA00BwIZ0vFwFe1AF34YjZD3wI3AuEcaKTwBGTAW4BXgW+B54Eqhba+XIV4EU9cA/wAvAV8AGwHQjgRCeBIyYL3Aq8BvwIPAFUXkyny12AlyBwL/A88CXwPrANqMCJTgpIYQvaBrwO/AQ8DlRcbMfXioAAcB/wIvAF8B6wFfBjRyeFHZ0dwJvAz8BjQHgxnV5LAvy2iBeAz4F3gS2Ahx2dNHZ0dgJvAb8AjwKhxXZ8rQjwAfcDLwOfAe8AmwEXOzpp7OjsBN4GfgUeAYKX0vm1IMCP7UdeAT4F3gY2AS52dDLY0bkdJ2d+Ax4GApfa+dUuwI8dnVeBT4C3gI2Aix2dLHZ07gDeAX4HHgL8l9P51SogADwAvAZ8DLwJbMCOThY7OnfaZX8ADwK+y+38ahMQwI7O68BHwBvAesDFjs48dtJ/F/gTeADwXknnV5OAIHbSeQM4BLwOrAPcXzPAEeBPYC9XGJ3lRhA76bwJfIgTnbXA74ALmMB2VM8Af+FE50qNzlIjhJ10DgKHgdeAtcDvQBlQCZwD/gb2AN6r7fhqEBDGTjrvA+/ZZWuA34AyoAI4D/wD7MaOztV2fCUJiGAnnfeAd+2yNcCvQBlQCQwCI8Bu7LfKYnR8JQiIYied94F37LI1wC9AGVAFDAGjwC7s6CxWx5dTQBQ76RwC3rbL1gA/A2VANTACjNtlYrE7vpQCYsBDwEHgLbtsNfATUAZUAaPABLATO+kseseXQkAMeBg4ALxpl63GTjoKVANjwCSwAzvpLEnHF1NAHDvpvAm8YZetBo4CeaAGmASmgO3YSWfJOr5YAuLYSecA8LpdFgeOADmgFpgGpoHbsJPOknZ8MQRU4CSd14DX7LI4cBjIAnXADE6Wvw876Sx5x/8DwkGBDkn/cKsAAAAASUVORK5CYII=

View File

@@ -1,68 +0,0 @@
/* 主题色变量 */
$primary-color: #4080ff;
$success-color: #07c160;
$warning-color: #ff9900;
$error-color: #fa5151;
$info-color: #909399;
/* 文字颜色 */
$text-color-main: #333333;
$text-color-regular: #666666;
$text-color-secondary: #e9e9e9;
$text-color-placeholder: #c0c4cc;
$text-color-white: #ffffff;
/* 边框颜色 */
$border-color-base: #dcdfe6;
$border-color-light: #e4e7ed;
$border-color-lighter: #ebeef5;
$border-color-extra-light: #f2f6fc;
/* 背景颜色 */
$bg-color: #f9fafb;
$bg-color-white: #ffffff;
$bg-color-primary: rgba(64, 128, 255, 0.1);
$bg-color-success: rgba(7, 193, 96, 0.1);
$bg-color-warning: rgba(255, 153, 0, 0.1);
$bg-color-error: rgba(250, 81, 81, 0.1);
/* 字体大小 */
$font-size-xs: 20rpx;
$font-size-sm: 24rpx;
$font-size-base: 28rpx;
$font-size-medium: 32rpx;
$font-size-lg: 36rpx;
$font-size-xl: 40rpx;
$font-size-xxl: 48rpx;
/* 圆角大小 */
$border-radius-sm: 4rpx;
$border-radius-base: 8rpx;
$border-radius-lg: 16rpx;
$border-radius-circle: 50%;
/* 间距大小 */
$spacing-xs: 10rpx;
$spacing-sm: 20rpx;
$spacing-base: 30rpx;
$spacing-lg: 40rpx;
$spacing-xl: 50rpx;
/* 字体加粗 */
$font-weight-normal: 400;
$font-weight-medium: 500;
$font-weight-bold: 700;
/* 阴影 */
$shadow-sm: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
$shadow-base: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
$shadow-lg: 0 8rpx 16rpx rgba(0, 0, 0, 0.1);
/* 基础动画 */
$animation-duration-fast: 0.2s;
$animation-duration-base: 0.3s;
$animation-duration-slow: 0.4s;
$animation-timing-function-base: ease-in-out;
/* 导入uView的变量 */
@import 'uview-ui/theme.scss';

View File

@@ -1,140 +0,0 @@
/**
* 认证相关工具函数
*/
import { refreshToken } from '@/api/user';
const TOKEN_KEY = 'token';
const TOKEN_EXPIRES_KEY = 'token_expires';
const USER_INFO_KEY = 'user_info';
/**
* 设置Token
* @param {string} token 令牌
* @param {number} expires 过期时间(秒)
*/
function setToken(token, expires = 7200) {
uni.setStorageSync(TOKEN_KEY, token);
// 计算过期时间点(当前时间 + 有效期)
const expiresTime = Math.floor(Date.now() / 1000) + expires;
uni.setStorageSync(TOKEN_EXPIRES_KEY, expiresTime);
}
/**
* 获取Token
* @returns {string} token值
*/
function getToken() {
return uni.getStorageSync(TOKEN_KEY);
}
/**
* 移除Token
*/
function removeToken() {
uni.removeStorageSync(TOKEN_KEY);
uni.removeStorageSync(TOKEN_EXPIRES_KEY);
}
/**
* 设置用户信息
* @param {Object} userInfo 用户信息
*/
function setUserInfo(userInfo) {
uni.setStorageSync(USER_INFO_KEY, JSON.stringify(userInfo));
}
/**
* 获取用户信息
* @returns {Object} 用户信息
*/
function getUserInfo() {
const userInfo = uni.getStorageSync(USER_INFO_KEY);
return userInfo ? JSON.parse(userInfo) : null;
}
/**
* 移除用户信息
*/
function removeUserInfo() {
uni.removeStorageSync(USER_INFO_KEY);
}
/**
* 移除所有认证信息
*/
function removeAll() {
removeToken();
removeUserInfo();
}
/**
* 刷新Token
* @returns {Promise} 刷新结果
*/
function refreshTokenAsync() {
return new Promise((resolve, reject) => {
refreshToken()
.then(res => {
if (res.code === 200) {
// 更新Token
setToken(res.data.token, res.data.token_expired - Math.floor(Date.now() / 1000));
resolve(res);
} else {
reject(res);
}
})
.catch(err => {
reject(err);
});
});
}
/**
* 判断是否已登录
* @returns {boolean} 是否已登录
*/
function isLogin() {
const token = getToken();
// 如果没有token直接返回未登录
if (!token) return false;
// 检查token是否过期
const expiresTime = uni.getStorageSync(TOKEN_EXPIRES_KEY) || 0;
const nowTime = Math.floor(Date.now() / 1000);
// 如果当前时间超过过期时间,则返回未登录
return nowTime < expiresTime;
}
/**
* 获取用户类型
* @returns {number} 用户类型ID
*/
function getUserType() {
const userInfo = getUserInfo();
return userInfo ? userInfo.typeId || 0 : 0;
}
/**
* 是否为管理员
* @returns {boolean} 是否为管理员
*/
function isAdmin() {
const userInfo = getUserInfo();
return userInfo ? !!userInfo.isAdmin : false;
}
export default {
setToken,
getToken,
removeToken,
setUserInfo,
getUserInfo,
removeUserInfo,
removeAll,
isLogin,
refreshToken: refreshTokenAsync,
getUserType,
isAdmin
};

View File

@@ -1,141 +0,0 @@
/**
* 通用工具函数集合
*/
/**
* 格式化日期
* @param {Date|string|number} date 日期对象/日期字符串/时间戳
* @param {string} format 格式化模板YYYY-MM-DD HH:mm:ss
* @returns {string} 格式化后的日期字符串
*/
function formatDate(date, format = 'YYYY-MM-DD HH:mm:ss') {
if (!date) return '';
// 如果是时间戳或字符串,转为日期对象
if (typeof date === 'string' || typeof date === 'number') {
date = new Date(date);
}
// 定义替换规则
const rules = {
'YYYY': date.getFullYear(),
'MM': padZero(date.getMonth() + 1),
'DD': padZero(date.getDate()),
'HH': padZero(date.getHours()),
'mm': padZero(date.getMinutes()),
'ss': padZero(date.getSeconds())
};
// 替换
return format.replace(/(YYYY|MM|DD|HH|mm|ss)/g, function(key) {
return rules[key];
});
}
/**
* 补零
* @param {number} num 数字
* @returns {string} 补零后的字符串
*/
function padZero(num) {
return String(num).padStart(2, '0');
}
/**
* 格式化手机号
* @param {string} mobile 手机号
* @returns {string} 格式化后的手机号138****8888
*/
function formatMobile(mobile) {
if (!mobile) return '';
return mobile.replace(/^(\d{3})\d{4}(\d{4})$/, '$1****$2');
}
/**
* 生成UUID
* @returns {string} UUID
*/
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 深度克隆对象
* @param {Object} obj 需要克隆的对象
* @returns {Object} 克隆后的对象
*/
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 处理日期
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// 处理数组
if (obj instanceof Array) {
return obj.map(item => deepClone(item));
}
// 处理对象
const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
/**
* 防抖函数
* @param {Function} fn 需要防抖的函数
* @param {number} delay 延迟时间单位ms
* @returns {Function} 防抖后的函数
*/
function debounce(fn, delay = 300) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
/**
* 节流函数
* @param {Function} fn 需要节流的函数
* @param {number} delay 延迟时间单位ms
* @returns {Function} 节流后的函数
*/
function throttle(fn, delay = 300) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
fn.apply(this, args);
lastTime = now;
}
};
}
export default {
formatDate,
formatMobile,
generateUUID,
deepClone,
debounce,
throttle
};

View File

@@ -1,185 +0,0 @@
import Auth from './auth';
// 服务器地址
const BASE_URL = process.env.VUE_APP_BASE_API || 'http://yishi.com';
// 请求超时时间
const TIMEOUT = 10000;
/**
* 请求拦截器
* @param {Object} config 请求配置
* @returns {Object} 处理后的请求配置
*/
function requestInterceptor(config) {
// 获取 token
const token = uni.getStorageSync('token');
// 如果有 token则带上请求头 Authorization: Bearer + token
if (token) {
config.header = {
...config.header,
'Authorization': `Bearer ${token}`
};
}
// 打印请求日志
console.log('请求地址:', `${config.baseURL || BASE_URL}${config.url}`);
return config;
}
/**
* 响应拦截器
* @param {Object} response 响应数据
* @returns {Object|Promise} 处理后的响应数据或Promise
*/
function responseInterceptor(response) {
// 未登录或token失效 - 取消登录拦截
if (response.data.code === 401) {
console.log('登录已过期,需要重新登录');
// 清除登录信息
Auth.removeToken();
Auth.removeUserInfo();
// 跳转到登录页
uni.reLaunch({
url: '/pages/login/index'
});
return Promise.reject(new Error('登录已过期,请重新登录'));
}
// token需要刷新 - 410 状态码
if (response.data.code === 410) {
// 尝试刷新 token
return Auth.refreshToken()
.then(res => {
if (res.code === 200) {
// 更新本地token
Auth.setToken(res.data.token, res.data.token_expired - Math.floor(Date.now() / 1000));
// 使用新token重试原请求
const config = response.config;
config.header.Authorization = `Bearer ${res.data.token}`;
// 重新发起请求
return request(config);
} else {
// 刷新失败,跳转到登录页
uni.reLaunch({
url: '/pages/login/index'
});
return Promise.reject(new Error('登录已过期,请重新登录'));
}
})
.catch(err => {
console.error('刷新token失败', err);
// 清除登录信息
Auth.removeToken();
Auth.removeUserInfo();
// 跳转到登录页
uni.reLaunch({
url: '/pages/login/index'
});
return Promise.reject(new Error('登录已过期,请重新登录'));
});
}
return response.data;
}
/**
* 构建完整的URL包括查询参数
* @param {string} baseUrl 基础URL
* @param {Object} params 查询参数
* @returns {string} 完整的URL
*/
function buildUrlWithParams(baseUrl, params) {
if (!params || Object.keys(params).length === 0) {
return baseUrl;
}
const queryString = Object.keys(params)
.filter(key => params[key] !== undefined && params[key] !== null)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
.join('&');
return queryString ? `${baseUrl}${baseUrl.includes('?') ? '&' : '?'}${queryString}` : baseUrl;
}
/**
* 统一请求函数
* @param {Object} options 请求选项
* @returns {Promise} 请求结果
*/
function request(options) {
// 合并请求选项
const config = {
baseURL: BASE_URL,
timeout: TIMEOUT,
header: {
'Content-Type': 'application/json'
},
...options
};
// 请求拦截
const interceptedConfig = requestInterceptor(config);
// 处理GET请求参数
let url = `${interceptedConfig.baseURL}${interceptedConfig.url}`;
const method = interceptedConfig.method || 'GET';
// 如果是GET请求并且有params参数将其转换为URL查询字符串
if (method.toUpperCase() === 'GET' && interceptedConfig.params) {
url = buildUrlWithParams(url, interceptedConfig.params);
// 打印完整请求URL便于调试
console.log('完整请求URL:', url);
}
// 发起请求
return new Promise((resolve, reject) => {
uni.request({
url: url,
method: method,
data: method.toUpperCase() === 'GET' ? undefined : interceptedConfig.data,
header: interceptedConfig.header,
timeout: interceptedConfig.timeout,
success: (res) => {
try {
const result = responseInterceptor(res);
resolve(result);
} catch (error) {
reject(error);
}
},
fail: (err) => {
// 显示提示
uni.showToast({
title: '网络请求失败',
icon: 'none',
duration: 2000
});
// 增强错误对象,添加更多信息便于调试
const enhancedError = {
...err,
url: url,
method: method,
params: method.toUpperCase() === 'GET' ? interceptedConfig.params : undefined,
data: method.toUpperCase() === 'GET' ? undefined : interceptedConfig.data,
message: err.errMsg || '网络请求失败'
};
console.error('请求失败详情:', enhancedError);
reject(enhancedError);
}
});
});
}
export default request;

View File

@@ -16,6 +16,7 @@ Route::group('v1/', function () {
Route::post('', 'app\\devices\\controller\\Device@save'); // 添加设备
Route::put('refresh', 'app\\devices\\controller\\Device@refresh'); // 刷新设备状态
Route::delete(':id', 'app\\devices\\controller\\Device@delete'); // 删除设备
Route::post('task-config', 'app\\devices\\controller\\Device@updateTaskConfig'); // 更新设备任务配置
});
// 设备微信相关

View File

@@ -222,15 +222,7 @@ class Device extends Controller
'msg' => '设备不存在'
]);
}
// 检查设备是否属于用户所在公司
if ($info['companyId'] != $userInfo['companyId']) {
return json([
'code' => 403,
'msg' => '您没有权限查看该设备'
]);
}
return json([
'code' => 200,
'msg' => '获取成功',
@@ -404,4 +396,73 @@ class Device extends Controller
]);
}
}
/**
* 更新设备任务配置
* @return \think\response\Json
*/
public function updateTaskConfig()
{
// 获取请求参数
$data = $this->request->post();
// 验证参数
if (empty($data['id'])) {
return json(['code' => 400, 'msg' => '设备ID不能为空']);
}
// 转换为整型确保ID格式正确
$deviceId = intval($data['id']);
// 先获取设备信息,确认设备存在且未删除
$device = \app\devices\model\Device::where('id', $deviceId)
->where('isDeleted', 0)
->find();
if (!$device) {
return json(['code' => 404, 'msg' => '设备不存在或已删除']);
}
// 读取原taskConfig如果存在则解析
$taskConfig = [];
if (!empty($device['taskConfig'])) {
$taskConfig = json_decode($device['taskConfig'], true) ?: [];
}
// 更新需要修改的配置项
$updateFields = ['autoAddFriend', 'autoReply', 'momentsSync', 'aiChat'];
$hasUpdate = false;
foreach ($updateFields as $field) {
if (isset($data[$field])) {
// 将值转换为布尔类型存储
$taskConfig[$field] = (bool)$data[$field];
$hasUpdate = true;
}
}
// 如果没有需要更新的字段,直接返回成功
if (!$hasUpdate) {
return json(['code' => 200, 'msg' => '更新成功', 'data' => ['taskConfig' => $taskConfig]]);
}
// 更新设备taskConfig字段
$result = \app\devices\model\Device::where('id', $deviceId)
->update([
'taskConfig' => json_encode($taskConfig),
'updateTime' => time()
]);
if ($result) {
return json([
'code' => 200,
'msg' => '更新任务配置成功',
'data' => [
'taskConfig' => $taskConfig
]
]);
} else {
return json(['code' => 500, 'msg' => '更新任务配置失败']);
}
}
}

View File

@@ -109,9 +109,61 @@ class Device extends Model
*/
public static function getDeviceInfo($id)
{
return self::where('id', $id)
->where('isDeleted', 0)
// 查询设备基础信息与关联的微信账号信息
$device = self::alias('d')
->field([
'd.id', 'd.imei', 'd.memo', 'd.alive', 'd.taskConfig', 'd.lastUpdateTime',
'w.id as wechatId', 'w.thirtyDayMsgCount', 'w.totalFriend', 'd.extra'
])
->leftJoin('tk_wechat_account w', 'd.imei = w.imei')
->where('d.id', $id)
->where('d.isDeleted', 0)
->find();
// 如果设备存在,处理额外信息
if ($device) {
// 解析电量信息
$battery = 0;
if (!empty($device['extra'])) {
$extra = json_decode($device['extra'], true);
if (is_array($extra) && isset($extra['battery'])) {
$battery = intval($extra['battery']);
}
}
$device['battery'] = $battery;
// 解析taskConfig字段获取功能开关
$features = [
'autoAddFriend' => false,
'autoReply' => false,
'contentSync' => false,
'aiChat' => false
];
if (!empty($device['taskConfig'])) {
$taskConfig = json_decode($device['taskConfig'], true);
if (is_array($taskConfig)) {
// 映射taskConfig中的字段到前端需要的features
$features['autoAddFriend'] = isset($taskConfig['autoAddFriend']) ? (bool)$taskConfig['autoAddFriend'] : false;
$features['autoReply'] = isset($taskConfig['autoReply']) ? (bool)$taskConfig['autoReply'] : false;
$features['contentSync'] = isset($taskConfig['momentsSync']) ? (bool)$taskConfig['momentsSync'] : false;
$features['aiChat'] = isset($taskConfig['aiChat']) ? (bool)$taskConfig['aiChat'] : false;
}
}
$device['features'] = $features;
unset($device['extra']);
unset($device['taskConfig']);
// 格式化最后活跃时间
$device['lastUpdateTime'] = !empty($device['lastUpdateTime']) ? date('Y-m-d H:i:s', strtotime($device['lastUpdateTime'])) : date('Y-m-d H:i:s');
// 确保totalFriend和thirtyDayMsgCount有值防止NULL
$device['totalFriend'] = intval($device['totalFriend'] ?? 0);
$device['thirtyDayMsgCount'] = intval($device['thirtyDayMsgCount'] ?? 0);
}
return $device;
}
/**