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

This commit is contained in:
Ghost
2025-03-24 14:59:52 +08:00
50 changed files with 6797 additions and 14 deletions

1
Cunkebao/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

69
Cunkebao/App.vue Normal file
View File

@@ -0,0 +1,69 @@
<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>

86
Cunkebao/README.md Normal file
View File

@@ -0,0 +1,86 @@
# 村客宝 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

107
Cunkebao/api/user.js Normal file
View File

@@ -0,0 +1,107 @@
import request from '@/utils/request'
/**
* 用户登录
* @param {Object} data 登录数据
* @param {string} data.username 用户名
* @param {string} data.password 密码
* @param {boolean} data.is_encrypted 密码是否已加密
* @returns {Promise} 登录结果
*/
export function login(data) {
return request({
url: '/api/auth/login',
method: 'POST',
data
})
}
/**
* 手机号验证码登录
* @param {Object} data 登录数据
* @param {string} data.mobile 手机号
* @param {string} data.code 验证码
* @returns {Promise} 登录结果
*/
export function mobileLogin(data) {
return request({
url: '/api/auth/mobile-login',
method: 'POST',
data
})
}
/**
* 发送验证码
* @param {Object} data 数据
* @param {string} data.mobile 手机号
* @param {string} data.type 验证码类型(login:登录,register:注册)
* @returns {Promise} 发送结果
*/
export function sendCode(data) {
return request({
url: '/api/auth/code',
method: 'POST',
data
})
}
/**
* 获取用户信息
* @returns {Promise} 用户信息
*/
export function getUserInfo() {
return request({
url: '/api/auth/info',
method: 'GET'
})
}
/**
* 刷新token
* @returns {Promise} 刷新结果
*/
export function refreshToken() {
return request({
url: '/api/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: '/api/auth/wechat-login',
method: 'POST',
data
})
}
/**
* Apple登录
* @param {Object} data 登录数据
* @param {string} data.identityToken Apple身份令牌
* @returns {Promise} 登录结果
*/
export function appleLogin(data) {
return request({
url: '/api/auth/apple-login',
method: 'POST',
data
})
}

View File

@@ -0,0 +1,94 @@
<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: #999;
&.active-text {
color: #4080ff;
}
}
}
}
</style>

View File

@@ -0,0 +1,253 @@
<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: #999;
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: #999;
text-align: center;
top: 10rpx;
}
}
}
</style>

View File

@@ -0,0 +1,96 @@
<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: #999;
margin-top: 10rpx;
&.active-text {
color: #4080ff;
}
}
}
}
</style>

49
Cunkebao/main.js Normal file
View File

@@ -0,0 +1,49 @@
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()

74
Cunkebao/manifest.json Normal file
View File

@@ -0,0 +1,74 @@
{
"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" : "村客宝"
}
}

1762
Cunkebao/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
Cunkebao/package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"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.31"
},
"devDependencies": {
"sass": "^1.32.13",
"sass-loader": "^10.1.1",
"postcss-comment": "^2.0.0",
"postcss-import": "^14.0.2",
"autoprefixer": "^10.3.1"
}
}

84
Cunkebao/pages.json Normal file
View File

@@ -0,0 +1,84 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/login/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "登录",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/profile/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/agreement/user",
"style": {
"navigationBarTitleText": "用户协议",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/agreement/privacy",
"style": {
"navigationBarTitleText": "隐私政策",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/scenarios/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "场景获客"
}
},
{
"path": "pages/scenarios/detail",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "获客详情"
}
},
{
"path": "pages/scenarios/create",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "新建计划"
}
},
{
"path": "pages/work/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "工作台"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "村客宝",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"easycom": {
"autoscan": true,
"custom": {
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
}
}

View File

@@ -0,0 +1,201 @@
<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: #999999;
text-align: center;
margin-bottom: 60rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333333;
}
.paragraph {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
margin-bottom: 20rpx;
text-align: justify;
}
</style>

View File

@@ -0,0 +1,154 @@
<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: #999999;
text-align: center;
margin-bottom: 60rpx;
}
.section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
margin-bottom: 20rpx;
color: #333333;
}
.paragraph {
font-size: 28rpx;
color: #666666;
line-height: 1.8;
margin-bottom: 20rpx;
text-align: justify;
}
</style>

View File

@@ -0,0 +1,342 @@
<template>
<view class="index-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="title align-left">存客宝</view>
<view class="header-icons">
<u-icon name="bell" size="56" 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'
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: '小红书获客' }
]
}
},
onLoad() {
// 加载数据
this.loadData();
},
methods: {
// 加载数据
loadData() {
// 这里可以添加API调用获取实际数据
console.log('加载首页数据');
// 示例数据已在data中预设
},
// 跳转到通知页面
goToNotification() {
uni.navigateTo({
url: '/pages/notification/index'
});
}
}
}
</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 #e5e5e5;
.title {
font-size: 45rpx;
font-weight: bold;
color: #2664ec;
&.align-left {
text-align: left;
}
}
.header-icons {
display: flex;
color: black;
align-items: center;
}
}
.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: #999;
}
}
}
.chart-container {
height: 500rpx;
display: flex;
justify-content: center;
align-items: center;
.custom-chart {
width: 100%;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,445 @@
<template>
<view class="login-container">
<!-- 登录方式切换 -->
<view class="tabs-container">
<u-tabs
:list="tabsList"
:current="current"
@change="handleTabChange"
activeStyle="color: #4080ff; font-weight: bold; font-size: 36rpx"
inactiveStyle="color: #999999; font-size: 36rpx"
itemStyle="height: 96rpx; padding: 0 30rpx;"
lineColor="#4080ff"
lineWidth="48rpx"
lineHeight="4rpx"
:itemWidth="300"
></u-tabs>
</view>
<!-- 提示文字 -->
<view class="login-hint">
你所在地区仅支持 手机号 / 微信 / Apple 登录
</view>
<!-- 表单区域 -->
<view class="login-form">
<!-- 手机号输入 -->
<u-form-item>
<u--input
v-model="form.mobile"
placeholder="+86手机号"
prefixIcon="phone"
prefixIconStyle="font-size: 52rpx;color: #909399;padding-right: 16rpx;"
clearable
type="number"
maxlength="11"
border="none"
fontSize="36rpx"
></u--input>
</u-form-item>
<!-- 验证码输入 -->
<u-form-item v-if="current === 0">
<u--input
v-model="form.code"
placeholder="验证码"
clearable
type="number"
maxlength="6"
border="none"
inputAlign="left"
fontSize="36rpx"
></u--input>
<template #right>
<view class="code-btn-wrap">
<u-button
@tap="getCode"
:text="codeTips"
type="primary"
size="mini"
:disabled="!isValidMobile || sending"
customStyle="width: 200rpx;height: 76rpx;font-size: 32rpx;"
></u-button>
</view>
</template>
</u-form-item>
<!-- 密码输入 -->
<u-form-item v-if="current === 1">
<u--input
v-model="form.password"
placeholder="密码"
prefixIcon="lock"
prefixIconStyle="font-size: 52rpx;color: #909399;padding-right: 16rpx;"
:password="!showPassword"
clearable
border="none"
fontSize="36rpx"
></u--input>
<template #right>
<u-icon
:name="showPassword ? 'eye' : 'eye-off'"
color="#909399"
size="52"
@click="showPassword = !showPassword"
></u-icon>
</template>
</u-form-item>
<!-- 用户协议 -->
<view class="agreement">
<u-checkbox
v-model="isAgree"
shape="circle"
activeColor="#4080ff"
iconSize="24"
></u-checkbox>
<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;"
></u-button>
<!-- 分割线 -->
<view class="divider">
<view class="line"></view>
<view class="text"></view>
<view class="line"></view>
</view>
<!-- 第三方登录 -->
<view class="other-login">
<!-- 微信登录 -->
<u-button
text="使用微信登录"
@click="handleWechatLogin"
icon="weixin-fill"
iconSize="52"
plain
customStyle="width: 100%; margin-bottom: 20rpx; height: 96rpx; border-radius: 24rpx; border-color: #07c160; color: #07c160; font-size: 40rpx;"
></u-button>
<!-- Apple登录 -->
<u-button
text="使用 Apple 登录"
@click="handleAppleLogin"
plain
customStyle="width: 100%; margin-bottom: 20rpx; height: 96rpx; border-radius: 24rpx; border-color: #333333; color: #333333; font-size: 40rpx;"
>
<template #icon>
<image src="/static/images/apple.png" class="apple-icon"></image>
</template>
</u-button>
</view>
<!-- 联系我们 -->
<view class="contact-us" @click="handleContact">联系我们</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
tabsList: [
{
name: '验证码登录'
},
{
name: '密码登录'
}
],
current: 0, // 当前选中的选项卡索引
form: {
mobile: '',
password: '',
code: ''
},
showPassword: false, // 是否显示密码
isAgree: false, // 是否同意协议
sending: false, // 是否正在发送验证码
codeTips: '发送验证码' // 验证码按钮文字
}
},
computed: {
// 手机号是否有效
isValidMobile() {
return /^1\d{10}$/.test(this.form.mobile);
},
// 是否可以登录
canLogin() {
if (!this.isAgree || !this.isValidMobile) return false;
if (this.current === 1) {
return !!this.form.password;
} else {
return !!this.form.code;
}
}
},
methods: {
// 处理选项卡切换
handleTabChange(index) {
this.current = index;
},
// 处理登录
handleLogin() {
if (!this.canLogin) return;
if (this.current === 1) {
this.passwordLogin();
} else {
this.codeLogin();
}
},
// 密码登录
passwordLogin() {
uni.showLoading({
title: '登录中...'
});
// 这里替换为实际的登录API调用
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '登录成功',
icon: 'success'
});
// 登录成功后跳转到个人中心页面
uni.switchTab({
url: '/pages/profile/index'
});
}, 1500);
},
// 验证码登录
codeLogin() {
uni.showLoading({
title: '登录中...'
});
// 这里替换为实际的登录API调用
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '登录成功',
icon: 'success'
});
// 登录成功后跳转到个人中心页面
uni.switchTab({
url: '/pages/profile/index'
});
}, 1500);
},
// 获取验证码
getCode() {
if (this.sending) return;
// 验证手机号
if (!this.isValidMobile) {
uni.showToast({
title: '请输入正确的手机号',
icon: 'none'
});
return;
}
uni.showLoading({
title: '发送中...'
});
this.sending = true;
this.codeTips = '60s';
let secondsLeft = 60;
const timer = setInterval(() => {
secondsLeft--;
this.codeTips = `${secondsLeft}s`;
if (secondsLeft <= 0) {
clearInterval(timer);
this.sending = false;
this.codeTips = '发送验证码';
}
}, 1000);
// 这里替换为实际的发送验证码API调用
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '验证码已发送',
icon: 'success'
});
}, 1000);
},
// 微信登录
handleWechatLogin() {
// #ifdef MP-WEIXIN
uni.login({
provider: 'weixin',
success: (res) => {
console.log('微信登录成功', res);
// 获取用户信息
uni.getUserInfo({
provider: 'weixin',
success: (infoRes) => {
console.log('获取用户信息成功', infoRes);
// 处理登录逻辑
}
});
},
fail: (err) => {
console.error('微信登录失败', err);
}
});
// #endif
// #ifdef H5 || APP-PLUS
uni.showToast({
title: '请在微信中打开',
icon: 'none'
});
// #endif
},
// Apple登录
handleAppleLogin() {
// #ifdef APP-PLUS
uni.login({
provider: 'apple',
success: (res) => {
console.log('Apple登录成功', res);
// 处理登录逻辑
},
fail: (err) => {
console.error('Apple登录失败', err);
}
});
// #endif
// #ifdef H5 || MP-WEIXIN
uni.showToast({
title: '请在iOS设备上使用',
icon: 'none'
});
// #endif
},
// 前往用户协议
goToUserAgreement() {
uni.navigateTo({
url: '/pages/agreement/user'
});
},
// 前往隐私政策
goToPrivacyPolicy() {
uni.navigateTo({
url: '/pages/agreement/privacy'
});
},
// 联系我们
handleContact() {
uni.showModal({
title: '联系我们',
content: '客服电话400-123-4567\n工作时间9:00-18:00',
showCancel: false
});
}
}
}
</script>
<style lang="scss" scoped>
.login-container {
padding: 40rpx;
background-color: #ffffff;
min-height: 100vh;
}
.tabs-container {
display: flex;
justify-content: center;
}
.login-hint {
font-size: 32rpx;
color: #999999;
margin: 30rpx 0 60rpx;
text-align: center;
}
.login-form {
.u-form-item {
margin-bottom: 30rpx;
}
.code-btn-wrap {
margin-left: 20rpx;
}
}
.agreement {
display: flex;
align-items: center;
margin: 40rpx 0;
.agreement-text {
font-size: 32rpx;
color: #999999;
margin-left: 12rpx;
}
.link {
color: $primary-color;
}
}
.divider {
display: flex;
align-items: center;
margin: 60rpx 0;
.line {
flex: 1;
height: 2px;
background-color: #eeeeee;
}
.text {
color: #999999;
padding: 0 30rpx;
font-size: 32rpx;
}
}
.apple-icon {
width: 52rpx;
height: 52rpx;
margin-right: 16rpx;
}
.contact-us {
text-align: center;
font-size: 32rpx;
color: #666666;
margin-top: 60rpx;
}
</style>

View File

@@ -0,0 +1,297 @@
<template>
<view class="profile-container">
<!-- 顶部导航栏 -->
<view class="header">
<view class="title">我的</view>
<view class="header-icons">
<u-icon name="setting" size="26" class="icon-setting" @click="goToSetting"></u-icon>
<u-icon name="bell" size="26" class="icon-bell" @click="goToNotification"></u-icon>
</view>
</view>
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="avatar-wrap">
<template v-if="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">卡若</view>
<view class="account">账号: 84675209</view>
</view>
<view class="edit-profile-btn" @click="editProfile">
编辑资料
</view>
</view>
<!-- 功能菜单列表 -->
<view class="menu-list">
<view class="menu-item" @click="navigateTo('/pages/device/index')">
<view class="menu-left">
<u-icon name="setting" size="28" color="#4080ff" class="menu-icon"></u-icon>
<text class="menu-title">设备管理</text>
</view>
<u-icon name="arrow-right" size="20" color="#c8c9cc"></u-icon>
</view>
<view class="menu-item" @click="navigateTo('/pages/wechat/index')">
<view class="menu-left">
<u-icon name="weixin-fill" size="28" color="#07c160" class="menu-icon"></u-icon>
<text class="menu-title">微信号管理</text>
</view>
<u-icon name="arrow-right" size="20" color="#c8c9cc"></u-icon>
</view>
<view class="menu-item" @click="navigateTo('/pages/traffic/index')">
<view class="menu-left">
<u-icon name="wifi" size="28" color="#fa3534" class="menu-icon"></u-icon>
<text class="menu-title">流量池</text>
</view>
<u-icon name="arrow-right" size="20" color="#c8c9cc"></u-icon>
</view>
<view class="menu-item" @click="navigateTo('/pages/content/index')">
<view class="menu-left">
<u-icon name="folder" size="28" color="#ff9900" class="menu-icon"></u-icon>
<text class="menu-title">内容库</text>
</view>
<u-icon name="arrow-right" size="20" color="#c8c9cc"></u-icon>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-btn" @click="handleLogout">
<u-icon name="arrow-left" color="#ff3c2a" size="18"></u-icon>
<text class="logout-text">退出登录</text>
</view>
<!-- 底部导航栏 -->
<CustomTabBar active="profile"></CustomTabBar>
</view>
</template>
<script>
import CustomTabBar from '@/components/CustomTabBar.vue'
export default {
components: {
CustomTabBar
},
data() {
return {
userInfo: {
avatar: null,
username: '卡若',
account: '84675209'
}
}
},
onLoad() {
// 获取用户信息
this.getUserInfo();
},
methods: {
// 获取用户信息
getUserInfo() {
// 这里可以添加获取用户信息的API调用
console.log('获取用户信息');
// 示例数据实际应从API获取
this.userInfo = {
avatar: null, // 设置为 null使用默认图标
username: '卡若',
account: '84675209'
};
},
// 跳转到设置页面
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) {
// 清除登录状态
// uni.removeStorageSync('token');
// uni.removeStorageSync('userInfo');
// 跳转到登录页面
uni.reLaunch({
url: '/pages/login/index'
});
}
}
});
}
}
}
</script>
<style lang="scss" scoped>
.profile-container {
min-height: 100vh;
background-color: #f5f5f5;
position: relative;
padding-bottom: 150rpx; /* 为底部导航栏预留空间从110rpx更新为150rpx */
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx;
background-color: #fff;
.title {
font-size: 40rpx;
font-weight: bold;
color: #333;
}
.header-icons {
display: flex;
align-items: center;
.icon-setting {
margin-right: 30rpx;
}
}
}
.user-card {
margin: 20rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 40rpx;
display: flex;
align-items: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.avatar-wrap {
width: 120rpx;
height: 120rpx;
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: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
}
.account {
font-size: 28rpx;
color: #999;
}
}
.edit-profile-btn {
padding: 12rpx 30rpx;
background-color: #f5f5f5;
border-radius: 30rpx;
font-size: 28rpx;
color: #333;
}
}
.menu-list {
margin: 20rpx;
background-color: #fff;
border-radius: 16rpx;
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 #f5f5f5;
&:last-child {
border-bottom: none;
}
.menu-left {
display: flex;
align-items: center;
.menu-icon {
margin-right: 20rpx;
}
.menu-title {
font-size: 32rpx;
color: #333;
}
}
}
}
.logout-btn {
display: flex;
justify-content: center;
align-items: center;
margin: 60rpx auto;
width: 200rpx;
.logout-text {
font-size: 32rpx;
color: #ff3c2a;
margin-left: 10rpx;
}
}
</style>

View File

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

View File

@@ -0,0 +1,482 @@
<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: #999;
}
}
}
.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: #999;
}
}
}
}
.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: #999;
}
}
}
.empty-tip {
text-align: center;
padding: 40rpx 0;
color: #999;
font-size: 28rpx;
}
}
</style>

View File

@@ -0,0 +1,447 @@
<template>
<view class="scenarios-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="plus" size="26" class="icon-add" @click="createNewPlan"></u-icon>
</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">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<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">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<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">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<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">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<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">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<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">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<text>+11.2%</text>
</view>
</view>
</view>
<!-- AI智能获客 版块标题 -->
<view class="section-title">
<u-icon name="robot" size="28" color="#4080ff"></u-icon>
<text>AI智能获客</text>
<text class="beta-tag">Beta</text>
</view>
<!-- AI智能加友 -->
<view class="channel-card" @click="navigateToDetail('ai-friend')">
<view class="channel-icon bg-blue">
<u-icon name="star" size="28" color="#ffffff"></u-icon>
</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">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<text>+18.5%</text>
</view>
</view>
</view>
<!-- AI群引流 -->
<view class="channel-card" @click="navigateToDetail('ai-group')">
<view class="channel-icon bg-blue">
<u-icon name="star" size="28" color="#ffffff"></u-icon>
</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">178</view>
</view>
<view class="data-trend up">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<text>+15.2%</text>
</view>
</view>
</view>
<!-- AI场景转化 -->
<view class="channel-card" @click="navigateToDetail('ai-convert')">
<view class="channel-icon bg-blue">
<u-icon name="star" size="28" color="#ffffff"></u-icon>
</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">
<u-icon name="arrow-up-fill" size="16" color="#2fc25b"></u-icon>
<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: #f5f5f5;
position: relative;
padding-bottom: 150rpx; /* 为底部导航栏预留空间 */
}
.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;
}
}
.channel-grid {
display: flex;
flex-wrap: wrap;
padding: 15rpx;
.channel-card {
width: calc(50% - 15rpx);
margin: 7.5rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx 15rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
.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;
}
}
.channel-name {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 15rpx;
}
.channel-desc {
font-size: 24rpx;
color: #666;
text-align: center;
margin-bottom: 15rpx;
height: 68rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
.channel-data {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.data-item {
display: flex;
align-items: center;
.data-label {
font-size: 24rpx;
color: #999;
}
.data-value {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
}
.data-trend {
display: flex;
align-items: center;
font-size: 22rpx;
&.up {
color: #2fc25b;
}
&.down {
color: #fa3534;
}
}
}
}
}
.section-title {
width: 100%;
display: flex;
align-items: center;
padding: 30rpx 20rpx 15rpx;
margin: 15rpx 7.5rpx 5rpx;
u-icon {
margin-right: 10rpx;
}
text {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.beta-tag {
font-size: 22rpx;
color: #4080ff;
background-color: #ecf5ff;
padding: 2rpx 10rpx;
border-radius: 10rpx;
margin-left: 10rpx;
}
}
</style>

View File

@@ -0,0 +1 @@

View File

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

View File

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

View File

@@ -0,0 +1,2 @@
<!-- SVG 占位符,需要替换为实际的 Apple 图标 -->
<!-- 这是一个占位文件,实际应该放置 png 格式的 Apple 图标 -->

View File

@@ -0,0 +1,16 @@
<?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>

After

Width:  |  Height:  |  Size: 566 B

View File

@@ -0,0 +1,21 @@
<?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>

After

Width:  |  Height:  |  Size: 806 B

View File

@@ -0,0 +1,35 @@
<?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>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,24 @@
<?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>

After

Width:  |  Height:  |  Size: 823 B

View File

@@ -0,0 +1,21 @@
<?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>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

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

View File

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

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

68
Cunkebao/uni.scss Normal file
View File

@@ -0,0 +1,68 @@
/* 主题色变量 */
$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: #999999;
$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';

96
Cunkebao/utils/auth.js Normal file
View File

@@ -0,0 +1,96 @@
/**
* 认证相关工具函数
*/
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();
}
/**
* 判断是否已登录
* @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;
}
export default {
setToken,
getToken,
removeToken,
setUserInfo,
getUserInfo,
removeUserInfo,
removeAll,
isLogin
};

141
Cunkebao/utils/common.js Normal file
View File

@@ -0,0 +1,141 @@
/**
* 通用工具函数集合
*/
/**
* 格式化日期
* @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
};

132
Cunkebao/utils/request.js Normal file
View File

@@ -0,0 +1,132 @@
import Auth from './auth';
// 服务器地址
const BASE_URL = process.env.NODE_ENV === 'development'
? 'http://localhost:8080'
: 'https://api.example.com';
// 请求超时时间
const TIMEOUT = 10000;
/**
* 请求拦截器
* @param {Object} config 请求配置
* @returns {Object} 处理后的请求配置
*/
function requestInterceptor(config) {
// 获取 token
const token = uni.getStorageSync('token');
// 如果有 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('登录已过期,请重新登录'));
*/
// 直接返回响应,不拦截
return response.data;
}
// token需要刷新 - 取消登录拦截
if (response.data.code === 410) {
// 只在控制台打印信息,不进行拦截
console.log('Token需要刷新但不进行拦截');
/*
// 以下代码已注释,取消登录拦截
// 处理token刷新逻辑这里简化处理
uni.reLaunch({
url: '/pages/login/index'
});
return Promise.reject(new Error('登录已过期,请重新登录'));
*/
// 直接返回响应,不拦截
return response.data;
}
return response.data;
}
/**
* 统一请求函数
* @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);
// 发起请求
return new Promise((resolve, reject) => {
uni.request({
url: `${interceptedConfig.baseURL}${interceptedConfig.url}`,
method: interceptedConfig.method || 'GET',
data: 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
});
reject(err);
}
});
});
}
export default request;

View File

@@ -4,7 +4,7 @@ trace = true
[database]
type = mysql
hostname = 127.0.0.1
hostname = 103.144.2.26
database = yi_54iis_com
username = yi_54iis_com
password = c1RbMwrZCCyxF1bC

View File

@@ -13,4 +13,10 @@ Route::group('v1/auth', function () {
// 需要JWT认证的接口
Route::get('info', 'app\\common\\controller\\Auth@info')->middleware(['jwt']); // 获取用户信息
Route::post('refresh', 'app\\common\\controller\\Auth@refresh')->middleware(['jwt']); // 刷新令牌
});
});
// 附件上传相关路由
Route::group('v1/', function () {
Route::post('attachment/upload', 'app\\common\\controller\\Attachment@upload'); // 上传附件
Route::get('attachment/:id', 'app\\common\\controller\\Attachment@info'); // 获取附件信息
})->middleware(['jwt']);

View File

@@ -0,0 +1,140 @@
<?php
namespace app\common\controller;
use think\Controller;
use think\facade\Request;
use app\common\model\Attachment as AttachmentModel;
use app\common\util\AliyunOSS;
class Attachment extends Controller
{
/**
* 上传文件
* @return \think\response\Json
*/
public function upload()
{
try {
// 获取上传文件
$file = Request::file('file');
if (!$file) {
return json([
'code' => 400,
'msg' => '请选择要上传的文件'
]);
}
// 验证文件
$validate = \think\facade\Validate::rule([
'file' => [
'fileSize' => 10485760, // 10MB
'fileExt' => 'jpg,jpeg,png,gif,doc,docx,pdf,zip,rar',
'fileMime' => 'image/jpeg,image/png,image/gif,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/pdf,application/zip,application/x-rar-compressed'
]
]);
if (!$validate->check(['file' => $file])) {
return json([
'code' => 400,
'msg' => $validate->getError()
]);
}
// 生成文件hash
$hashKey = md5_file($file->getRealPath());
// 检查文件是否已存在
$existFile = AttachmentModel::getByHashKey($hashKey);
if ($existFile) {
return json([
'code' => 200,
'msg' => '文件已存在',
'data' => [
'id' => $existFile['id'],
'name' => $existFile['name'],
'url' => $existFile['source']
]
]);
}
// 生成OSS对象名称
$objectName = AliyunOSS::generateObjectName($file->getOriginalName());
// 上传到OSS
$result = AliyunOSS::uploadFile($file->getRealPath(), $objectName);
if (!$result['success']) {
return json([
'code' => 500,
'msg' => '文件上传失败:' . $result['error']
]);
}
// 保存到数据库
$attachmentData = [
'name' => Request::param('name') ?: $file->getOriginalName(),
'hash_key' => $hashKey,
'server' => 'aliyun_oss',
'source' => $result['url'],
'size' => $result['size'],
'suffix' => pathinfo($file->getOriginalName(), PATHINFO_EXTENSION)
];
$attachmentId = AttachmentModel::addAttachment($attachmentData);
if (!$attachmentId) {
return json([
'code' => 500,
'msg' => '保存附件信息失败'
]);
}
return json([
'code' => 200,
'msg' => '上传成功',
'data' => [
'id' => $attachmentId,
'name' => $attachmentData['name'],
'url' => $attachmentData['source']
]
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '上传失败:' . $e->getMessage()
]);
}
}
/**
* 获取附件信息
* @param int $id 附件ID
* @return \think\response\Json
*/
public function info($id)
{
try {
$attachment = AttachmentModel::find($id);
if (!$attachment) {
return json([
'code' => 404,
'msg' => '附件不存在'
]);
}
return json([
'code' => 200,
'msg' => '获取成功',
'data' => $attachment
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '获取失败:' . $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace app\common\model;
use think\Model;
class Attachment extends Model
{
// 设置表名
protected $name = 'attachments';
// 设置主键
protected $pk = 'id';
// 自动写入时间戳
protected $autoWriteTimestamp = 'datetime';
// 定义时间戳字段名
protected $createTime = 'create_at';
protected $updateTime = 'update_at';
protected $deleteTime = 'delete_at';
// 定义字段类型
protected $type = [
'id' => 'integer',
'dl_count' => 'integer',
'size' => 'integer',
'scene' => 'integer',
'create_at' => 'datetime',
'update_at' => 'datetime',
'delete_at' => 'datetime'
];
/**
* 添加附件记录
* @param array $data 附件数据
* @return int|bool
*/
public static function addAttachment($data)
{
$attachment = new self();
return $attachment->allowField(true)->save($data);
}
/**
* 根据hash_key获取附件
* @param string $hashKey
* @return array|null
*/
public static function getByHashKey($hashKey)
{
return self::where('hash_key', $hashKey)
->where('delete_at', null)
->find();
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace app\common\util;
use OSS\OssClient;
use OSS\Core\OssException;
class AliyunOSS
{
// OSS配置信息
const ACCESS_KEY_ID = 'your_access_key_id';
const ACCESS_KEY_SECRET = 'your_access_key_secret';
const ENDPOINT = 'oss-cn-hangzhou.aliyuncs.com';
const BUCKET = 'your_bucket_name';
/**
* 获取OSS客户端实例
* @return OssClient
* @throws OssException
*/
public static function getClient()
{
try {
return new OssClient(
self::ACCESS_KEY_ID,
self::ACCESS_KEY_SECRET,
self::ENDPOINT
);
} catch (OssException $e) {
throw new OssException('创建OSS客户端失败' . $e->getMessage());
}
}
/**
* 上传文件到OSS
* @param string $filePath 本地文件路径
* @param string $objectName OSS对象名称
* @return array
* @throws OssException
*/
public static function uploadFile($filePath, $objectName)
{
try {
$client = self::getClient();
// 上传文件
$result = $client->uploadFile(self::BUCKET, $objectName, $filePath);
// 获取文件访问URL
$url = $client->signUrl(self::BUCKET, $objectName, 3600);
return [
'success' => true,
'url' => $url,
'object_name' => $objectName,
'size' => filesize($filePath),
'mime_type' => mime_content_type($filePath)
];
} catch (OssException $e) {
return [
'success' => false,
'error' => $e->getMessage()
];
}
}
/**
* 生成OSS对象名称
* @param string $originalName 原始文件名
* @return string
*/
public static function generateObjectName($originalName)
{
$ext = pathinfo($originalName, PATHINFO_EXTENSION);
$name = md5(uniqid(mt_rand(), true));
return date('Y/m/d/') . $name . '.' . $ext;
}
}

View File

@@ -1,6 +1,6 @@
<?php
namespace app\common;
namespace app\common\util;
use Darabonba\OpenApi\Models\Config;
use AlibabaCloud\SDK\Dysmsapi\V20170525\Dysmsapi;
@@ -62,4 +62,4 @@ class AliyunSMS {
return FALSE;
}
}
}

View File

@@ -5,15 +5,26 @@
use think\facade\Route;
// 定义RESTful风格的API路由 - 设备管理相关
Route::group('v1/devices', function () {
// 设备列表和查询
Route::get('', 'app\\devices\\controller\\Device@index'); // 获取设备列表
Route::get('count', 'app\\devices\\controller\\Device@count'); // 获取设备总数
Route::get(':id', 'app\\devices\\controller\\Device@read'); // 获取设备详情
// 定义RESTful风格的API路由
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::group('devices', function () {
Route::get('', 'app\\devices\\controller\\Device@index'); // 获取设备列表
Route::get('count', 'app\\devices\\controller\\Device@count'); // 获取设备总数
Route::get(':id', 'app\\devices\\controller\\Device@read'); // 获取设备详情
Route::post('', 'app\\devices\\controller\\Device@save'); // 添加设备
Route::put('refresh', 'app\\devices\\controller\\Device@refresh'); // 刷新设备状态
Route::delete(':id', 'app\\devices\\controller\\Device@delete'); // 删除设备
});
// 设备微信相关
Route::group('device/wechats', function () {
Route::get('count', 'app\\devices\\controller\\DeviceWechat@count'); // 获取在线微信账号数量
Route::get('device-count', 'app\\devices\\controller\\DeviceWechat@deviceCount'); // 获取有登录微信的设备数量
Route::get('', 'app\\devices\\controller\\DeviceWechat@index'); // 获取在线微信账号列表
Route::get(':id', 'app\\devices\\controller\\DeviceWechat@detail'); // 获取微信号详情
Route::put('refresh', 'app\\devices\\controller\\DeviceWechat@refresh'); // 刷新设备微信状态
Route::post('transfer-friends', 'app\\devices\\controller\\DeviceWechat@transferFriends'); // 微信好友转移
});
})->middleware(['jwt']);

View File

@@ -0,0 +1,476 @@
<?php
namespace app\devices\controller;
use think\Controller;
use app\devices\model\WechatAccount;
use think\facade\Request;
use think\Db;
/**
* 设备微信控制器
*/
class DeviceWechat extends Controller
{
/**
* 获取在线微信账号数量
* @return \think\response\Json
*/
public function count()
{
try {
// 获取在线微信账号数量
$count = WechatAccount::getOnlineWechatCount();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'count' => $count
]
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '获取失败:' . $e->getMessage()
]);
}
}
/**
* 获取有登录微信的设备数量
* @return \think\response\Json
*/
public function deviceCount()
{
try {
// 获取有登录微信的设备数量
$count = WechatAccount::getDeviceWithWechatCount();
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'count' => $count
]
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '获取失败:' . $e->getMessage()
]);
}
}
/**
* 刷新设备微信状态
* @return \think\response\Json
*/
public function refresh()
{
try {
return json([
'code' => 200,
'msg' => '刷新成功',
'data' => []
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '获取失败:' . $e->getMessage()
]);
}
}
/**
* 获取在线微信账号列表
* @return \think\response\Json
*/
public function index()
{
try {
// 获取查询条件
$where = [];
// 微信ID
$wechatId = Request::param('wechat_id');
if (!empty($wechatId)) {
$where['wechatId'] = ['like', "%{$wechatId}%"];
}
// 昵称
$nickname = Request::param('nickname');
if (!empty($nickname)) {
$where['nickname|accountNickname'] = ['like', "%{$nickname}%"];
}
// 获取分页参数
$page = (int)Request::param('page', 1);
$limit = (int)Request::param('limit', 10);
// 获取排序参数
$sort = Request::param('sort', 'id');
$order = Request::param('order', 'desc');
// 获取在线微信账号列表
$list = WechatAccount::getOnlineWechatList($where, "{$sort} {$order}", $page, $limit);
// 处理返回数据
$data = [];
foreach ($list->items() as $item) {
// 计算今日可添加好友数量(这里使用一个示例算法,你可以根据实际需求修改)
$canAddFriendCount = 30 - (isset($item['yesterdayMsgCount']) ? intval($item['yesterdayMsgCount']) : 0);
if ($canAddFriendCount < 0) {
$canAddFriendCount = 0;
}
// 计算今日新增好友数量(示例数据,实际需要从数据库获取或通过其他方式计算)
// 这里只是一个示例,你需要根据实际情况替换
$todayNewFriendCount = mt_rand(0, 10); // 随机生成0-10的数字作为示例
$data[] = [
'id' => $item['id'],
'wechatId' => $item['wechatId'],
'nickname' => $item['nickname'] ?: $item['accountNickname'],
'avatar' => $item['avatar'],
'accountUserName' => $item['accountUserName'],
'status' => $item['wechatAlive'] ? '在线' : '离线',
'deviceStatus' => $item['deviceAlive'] ? '在线' : '离线',
'totalFriend' => $item['totalFriend'],
'canAddFriendCount' => $canAddFriendCount,
'deviceInfo' => $item['imei'] . ($item['deviceMemo'] ? " ({$item['deviceMemo']})" : ''),
'todayNewFriendCount' => $todayNewFriendCount
];
}
return json([
'code' => 200,
'msg' => '获取成功',
'data' => [
'total' => $list->total(),
'list' => $data
]
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '获取失败:' . $e->getMessage()
]);
}
}
/**
* 获取微信号详情
* @param int $id 微信号ID
* @return \think\response\Json
*/
public function detail($id)
{
try {
// 获取微信号基本信息
$wechat = WechatAccount::where('id', $id)
->where('isDeleted', 0)
->find();
if (!$wechat) {
return json([
'code' => 404,
'msg' => '微信号不存在'
]);
}
// 计算账号年龄(从创建时间到现在)
$accountAge = 0;
if ($wechat['createTime']) {
$createTime = strtotime($wechat['createTime']);
$now = time();
$accountAge = floor(($now - $createTime) / (24 * 3600));
}
// 计算活跃程度(根据消息数)
$activityLevel = '低';
if ($wechat['thirtyDayMsgCount'] > 1000) {
$activityLevel = '高';
} elseif ($wechat['thirtyDayMsgCount'] > 500) {
$activityLevel = '中';
}
// 评估账号权重(示例算法)
$weight = 0;
// 基础权重
$weight += 10;
// 好友数量权重
$weight += min($wechat['totalFriend'] / 100, 20);
// 活跃度权重
$weight += min($wechat['thirtyDayMsgCount'] / 100, 20);
// 账号年龄权重
$weight += min($accountAge / 30, 10);
// 在线状态权重
if ($wechat['wechatAlive']) {
$weight += 5;
}
// 获取限制记录(示例数据,实际需要从数据库获取)
$restrictions = [
[
'type' => '添加好友限制',
'reason' => '频繁添加好友',
'startTime' => date('Y-m-d H:i:s', strtotime('-1 day')),
'endTime' => date('Y-m-d H:i:s', strtotime('+1 day'))
]
];
// 获取微信好友列表
$friends = Db::table('tk_wechat_friend')
->where('wechatAccountId', $id)
->where('isDeleted', 0)
->field([
'id',
'wechatId',
'nickname',
'avatar',
'gender',
'region',
'signature',
'labels',
'createTime'
])
->select();
// 处理返回数据
$data = [
'basicInfo' => [
'id' => $wechat['id'],
'wechatId' => $wechat['wechatId'],
'nickname' => $wechat['nickname'] ?: $wechat['accountNickname'],
'avatar' => $wechat['avatar'],
'status' => $wechat['wechatAlive'] ? '在线' : '离线',
'deviceStatus' => $wechat['deviceAlive'] ? '在线' : '离线',
'deviceInfo' => $wechat['imei'] . ($wechat['deviceMemo'] ? " ({$wechat['deviceMemo']})" : ''),
'gender' => $wechat['gender'],
'region' => $wechat['region'],
'signature' => $wechat['signature']
],
'statistics' => [
'totalFriend' => $wechat['totalFriend'],
'maleFriend' => $wechat['maleFriend'],
'femaleFriend' => $wechat['femaleFriend'],
'canAddFriendCount' => 30 - (isset($wechat['yesterdayMsgCount']) ? intval($wechat['yesterdayMsgCount']) : 0),
'yesterdayMsgCount' => $wechat['yesterdayMsgCount'],
'sevenDayMsgCount' => $wechat['sevenDayMsgCount'],
'thirtyDayMsgCount' => $wechat['thirtyDayMsgCount']
],
'accountInfo' => [
'age' => $accountAge,
'activityLevel' => $activityLevel,
'weight' => round($weight, 2),
'createTime' => $wechat['createTime'],
'lastUpdateTime' => $wechat['updateTime']
],
'restrictions' => $restrictions,
'friends' => $friends
];
return json([
'code' => 200,
'msg' => '获取成功',
'data' => $data
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '获取失败:' . $e->getMessage()
]);
}
}
/**
* 微信好友转移
* 将一个微信号的好友转移至另一个在线微信号
*
* @return \think\response\Json
*/
public function transferFriends()
{
try {
// 获取请求参数
$sourceWechatId = Request::param('source_id'); // 源微信账号ID
$targetWechatId = Request::param('target_id'); // 目标微信账号ID
// 参数验证
if (empty($sourceWechatId) || empty($targetWechatId)) {
return json([
'code' => 400,
'msg' => '参数错误源微信账号ID和目标微信账号ID不能为空'
]);
}
// 检查源微信账号是否存在
$sourceWechat = WechatAccount::where('id', $sourceWechatId)
->where('isDeleted', 0)
->find();
if (!$sourceWechat) {
return json([
'code' => 404,
'msg' => '源微信账号不存在'
]);
}
// 检查目标微信账号是否存在且在线
$targetWechat = WechatAccount::where('id', $targetWechatId)
->where('isDeleted', 0)
->where('wechatAlive', 1)
->where('deviceAlive', 1)
->find();
if (!$targetWechat) {
return json([
'code' => 404,
'msg' => '目标微信账号不存在或不在线'
]);
}
// 获取源微信账号的好友列表
$friends = Db::table('tk_wechat_friend')
->where('wechatAccountId', $sourceWechatId)
->where('isDeleted', 0)
->select();
// 统计好友数量
$totalFriends = count($friends);
if ($totalFriends == 0) {
return json([
'code' => 400,
'msg' => '源微信账号没有可转移的好友'
]);
}
// 开始事务
Db::startTrans();
try {
$successCount = 0;
$failCount = 0;
$duplicateCount = 0;
$failList = [];
foreach ($friends as $friend) {
// 检查目标微信账号是否已经有此好友
$existFriend = Db::table('tk_wechat_friend')
->where('wechatAccountId', $targetWechatId)
->where('wechatId', $friend['wechatId'])
->where('isDeleted', 0)
->find();
if ($existFriend) {
// 已经存在此好友,跳过
$duplicateCount++;
continue;
}
// 准备插入数据
$newFriend = [
'wechatAccountId' => $targetWechatId,
'alias' => $friend['alias'],
'wechatId' => $friend['wechatId'],
'conRemark' => $friend['conRemark'],
'nickname' => $friend['nickname'],
'pyInitial' => $friend['pyInitial'],
'quanPin' => $friend['quanPin'],
'avatar' => $friend['avatar'],
'gender' => $friend['gender'],
'region' => $friend['region'],
'addFrom' => $friend['addFrom'],
'labels' => $friend['labels'],
'signature' => $friend['signature'],
'isDeleted' => 0,
'isPassed' => $friend['isPassed'],
'accountId' => $friend['accountId'],
'extendFields' => $friend['extendFields'],
'accountUserName' => $friend['accountUserName'],
'accountRealName' => $friend['accountRealName'],
'accountNickname' => $friend['accountNickname'],
'ownerAlias' => $targetWechat['alias'],
'ownerWechatId' => $targetWechat['wechatId'],
'ownerNickname' => $targetWechat['nickname'] ?: $targetWechat['accountNickname'],
'ownerAvatar' => $targetWechat['avatar'],
'phone' => $friend['phone'],
'thirdParty' => $friend['thirdParty'],
'groupId' => $friend['groupId'],
'passTime' => $friend['passTime'],
'additionalPicture' => $friend['additionalPicture'],
'desc' => $friend['desc'],
'country' => $friend['country'],
'province' => $friend['province'],
'city' => $friend['city'],
'createTime' => date('Y-m-d H:i:s'),
'updateTime' => date('Y-m-d H:i:s')
];
// 插入新好友记录
$result = Db::table('tk_wechat_friend')->insert($newFriend);
if ($result) {
$successCount++;
} else {
$failCount++;
$failList[] = [
'id' => $friend['id'],
'wechatId' => $friend['wechatId'],
'nickname' => $friend['nickname']
];
}
}
// 更新两个微信账号的好友数量
$maleFriendsCount = Db::table('tk_wechat_friend')
->where('wechatAccountId', $targetWechatId)
->where('isDeleted', 0)
->where('gender', 1)
->count();
$femaleFriendsCount = Db::table('tk_wechat_friend')
->where('wechatAccountId', $targetWechatId)
->where('isDeleted', 0)
->where('gender', 2)
->count();
$totalFriendsCount = $maleFriendsCount + $femaleFriendsCount;
// 更新目标微信账号的好友数量
WechatAccount::where('id', $targetWechatId)
->update([
'totalFriend' => $totalFriendsCount,
'maleFriend' => $maleFriendsCount,
'femaleFriend' => $femaleFriendsCount,
'updateTime' => date('Y-m-d H:i:s')
]);
// 提交事务
Db::commit();
return json([
'code' => 200,
'msg' => '好友转移成功',
'data' => [
'total' => $totalFriends,
'success' => $successCount,
'fail' => $failCount,
'duplicate' => $duplicateCount,
'failList' => $failList
]
]);
} catch (\Exception $e) {
// 回滚事务
Db::rollback();
throw $e;
}
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '好友转移失败:' . $e->getMessage()
]);
}
}
}

View File

@@ -0,0 +1,129 @@
<?php
namespace app\devices\model;
use think\Model;
use think\Db;
/**
* 微信账号模型类
*/
class WechatAccount extends Model
{
// 设置表名
protected $name = 'wechat_account';
// 设置主键
protected $pk = 'id';
// 自动写入时间戳
protected $autoWriteTimestamp = 'datetime';
// 定义时间戳字段名
protected $createTime = 'createTime';
protected $updateTime = 'updateTime';
// 定义字段类型
protected $type = [
'id' => 'integer',
'deviceAccountId' => 'integer',
'keFuAlive' => 'integer',
'deviceAlive' => 'integer',
'wechatAlive' => 'integer',
'yesterdayMsgCount' => 'integer',
'sevenDayMsgCount' => 'integer',
'thirtyDayMsgCount' => 'integer',
'totalFriend' => 'integer',
'maleFriend' => 'integer',
'femaleFriend' => 'integer',
'gender' => 'integer',
'currentDeviceId' => 'integer',
'isDeleted' => 'integer',
'groupId' => 'integer'
];
/**
* 获取在线微信账号数量
*
* @param array $where 额外的查询条件
* @return int 微信账号数量
*/
public static function getOnlineWechatCount($where = [])
{
$condition = [
'deviceAlive' => 1,
'wechatAlive' => 1,
'isDeleted' => 0
];
// 合并额外条件
if (!empty($where)) {
$condition = array_merge($condition, $where);
}
return self::where($condition)->count();
}
/**
* 获取有登录微信的设备数量
*
* @param array $where 额外的查询条件
* @return int 设备数量
*/
public static function getDeviceWithWechatCount($where = [])
{
$condition = [
'deviceAlive' => 1,
'isDeleted' => 0
];
// 合并额外条件
if (!empty($where)) {
$condition = array_merge($condition, $where);
}
return self::where($condition)->count();
}
/**
* 获取在线微信账号列表
*
* @param array $where 额外的查询条件
* @param string $order 排序方式
* @param int $page 页码
* @param int $limit 每页数量
* @return \think\Paginator 分页对象
*/
public static function getOnlineWechatList($where = [], $order = 'id desc', $page = 1, $limit = 10)
{
$condition = [
'wechatAlive' => 1,
'deviceAlive' => 1,
'isDeleted' => 0
];
// 合并额外条件
if (!empty($where)) {
$condition = array_merge($condition, $where);
}
return self::where($condition)
->field([
'id',
'wechatId',
'accountNickname',
'nickname',
'accountUserName',
'avatar',
'wechatAlive',
'deviceAlive',
'totalFriend',
'maleFriend',
'femaleFriend',
'imei',
'deviceMemo',
'yesterdayMsgCount'
])
->order($order)
->paginate($limit, false, ['page' => $page]);
}
}