Merge branch 'develop' of https://gitee.com/Tyssen/yi-shi into develop
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
18
Cunkebao/components/ClientOnly.tsx
Normal 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}</>
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# 生产环境配置
|
||||
VUE_APP_BASE_API = 'https://api.cunkebao.com'
|
||||
VUE_APP_ENV = 'production'
|
||||
2
Cunkebao_Uniapp/.gitignore
vendored
@@ -1,2 +0,0 @@
|
||||
node_modules
|
||||
.env
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
@@ -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" : "存客宝"
|
||||
}
|
||||
}
|
||||
2817
Cunkebao_Uniapp/package-lock.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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')
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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
|
||||
@@ -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 |
@@ -1 +0,0 @@
|
||||
<!-- 首页 tabbar 激活状态图标占位,需要替换为实际的图片 -->
|
||||
@@ -1 +0,0 @@
|
||||
<!-- 首页 tabbar 图标占位,需要替换为实际的图片 -->
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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=
|
||||
@@ -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';
|
||||
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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'); // 更新设备任务配置
|
||||
});
|
||||
|
||||
// 设备微信相关
|
||||
|
||||
@@ -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' => '更新任务配置失败']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||