Commit a368866a by 陈岩

chore: init commit

0 parents
Showing 1000 changed files with 4898 additions and 0 deletions

Too many changes to show.

To preserve performance only 1000 of 1000+ files are displayed.

/node_modules/*
/.vscode/*
/unpackage/*
.pnpm-locak.yaml
/.hbuilderx
/.vscode
/.idea
/.DS_Store
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 移除 Android 13+ 的图片/视频读取权限,改用系统照片选择器 -->
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES"
tools:node="remove" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO"
tools:node="remove" />
<!-- 兼容旧版本:移除外部存储读写权限,避免被提升为 READ_MEDIA_* -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
tools:node="remove" />
<application>
<!-- 此文件用于权限移除,不声明组件 -->
</application>
</manifest>
\ No newline at end of file
<script>
import config from './utils/ip-config';
export default {
onLaunch() {
// #ifdef APP-PLUS
this.initPush()
uni.getPushClientId({
success: (res) => {
let push_clientid = res.cid
uni.setStorageSync('clientId', push_clientid)
},
fail(err) {
console.log(err)
}
})
// #endif
// #ifdef APP-PLUS
const dom = uni.requireNativePlugin('dom');
const domModule = uni.requireNativePlugin('dom')
domModule.addRule('fontFace', {
'fontFamily': "DingTalk_JinBuTi",
// 'src': "url('https://store.keliuyun.com/download/app/wechat/DingTalk_JinBuTi.ttf')"
'src':"url('./static/ttf/DingTalk_JinBuTi.ttf')"
});
dom.addRule('fontFace', {
'fontFamily': "D-DIN-PRO-700-Bold", // 自定义字体名称
'src':"url('./static/ttf/D-DIN-PRO-700-Bold.otf')"
// 'src': "url('https://store.keliuyun.com/download/app/wechat/D-DIN-PRO-700-Bold.otf')"
});
// #endif
// #ifdef APP
// 等待跳转动画结束
setTimeout(() => {
// 最长10s关闭启动屏 主动关屏在i18n资源文件中
plus.navigator.closeSplashscreen()
}, 10000)
// #endif
},
onShow: function() {
// #ifdef APP-PLUS
plus.runtime.setBadgeNumber(-1)
// #endif
},
onHide: function() {
console.log('App Hide')
},
methods: {
initPush() {
// 监听推送消息
uni.onPushMessage((res) => {
if (res.type === 'click') {
// 清除app角标数字
if(res.data.payload && res.data.payload.path){
uni.navigateTo({
url: res.data.payload.path
})
}
plus.runtime.setBadgeNumber(0)
return false
}
const pushData = res.data
uni.createPushMessage({
title: pushData.title || '',
content: pushData.content || '',
payload: pushData.payload || {},
sound: 'system',
cover: false,
badge: 1,
success: (res) => {
console.log('本地通知创建成功:', res)
},
fail: (err) => {
console.log('本地通知创建失败:', err)
// 备用方案:使用 plus.push 创建通知
if (plus.push) {
const options = {
title: pushData.title || '新消息',
content: pushData.content || '您有一条新消息',
payload: pushData.payload || {}
}
plus.push.createMessage(options.content, options.payload, {
title: options.title,
cover: false
})
}
}
})
})
},
}
}
</script>
<style lang="scss">
@import "./styles/variable.scss";
/* #ifndef APP-NVUE */
@import "./styles/ttf.scss";
body,
html {
// font-family: 'PingFang_Medium';
// font-family: Helvetica Neue, Helvetica, sans-serif;
font-family: $font-family-base;
}
/* #endif */
/* #ifndef APP-NVUE */
view {
box-sizing: border-box;
// font-family: Helvetica Neue, Helvetica, sans-serif;
font-family: $font-family-base;
}
/* #endif */
view {
// font-family: 'PingFang_Medium';
// font-family: Helvetica Neue, Helvetica, sans-serif;
font-family: $font-family-base;
}
//导航栏字体
.uni-page-head .uni-page-head__title {
font-weight: 700;
}
.p-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
.p-empty-img {
width: 560rpx;
height: 300rpx;
margin: 0 auto;
}
.p-empty-text {
margin-top: 54rpx;
font-size: 28rpx;
color: #90949D;
text-align: center;
}
.chart-text {
margin-top: 0;
font-size: 24rpx;
}
}
</style>
```js
// { value: 'feat', name: 'feat: 新增功能' },
// { value: 'fix', name: 'fix: 修复缺陷' },
// { value: 'docs', name: 'docs: 文档变更' },
// { value: 'style', name: 'style: 代码格式' },
// { value: 'refactor', name: 'refactor: 代码重构' },
// { value: 'perf', name: 'perf: 性能优化' },
// { value: 'test', name: 'test: 添加疏漏测试或已有测试改动' },
// { value: 'build', name: 'build: 构建流程、外部依赖变更 (如升级 npm 包、修改打包配置等)' },
// { value: 'ci', name: 'ci: 修改 CI 配置、脚本' },
// { value: 'revert', name: 'revert: 回滚 commit' },
// { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改 (不影响源文件、测试用例)' },
// { value: 'wip', name: 'wip: 正在开发中' },
// { value: 'workflow', name: 'workflow: 工作流程改进' },
// { value: 'types', name: 'types: 类型定义文件修改' },
```
在微信小程序平台中,echarts在开发者工具中会脱离图层,不用管,在真机中会正常显示
在微信小程序平台中,为了缩小包体积,目前使用的echarts.min.js是压缩过的,仅支持折线图、饼图、柱状图、热力图、桑基图,如果需要其他图表,自行前往[ECharts 在线构建器](https://echarts.apache.org/zh/builder.html)选取,同时请注意小程序包体积限制。
升级打包时,必须修改manifest.json中的 应用版本名称 和 应用版本号 (都需要大于上一次设置的值)
App平台中
由于app不支持i18n的变量替换,因此需要使用replace进行替换,例如:`this.$t('homePage.title').replace('${version}', this.version)`
\ No newline at end of file
{
"prompt" : "none"
}
import request from "@/utils/request";
// 获取集团下的要展示的数据
export function getAccountConfiguredApi(data) {
return request({
url: `/report/reportChart/getConfiguredCharts`,
method: "get",
data
});
}
// 获取门店下要展示的数据
export function getStoreConfiguredApi(data) {
return request({
url: `/report/reportChart/getCharts`,
method: "get",
data
});
}
// 获取门店下的基础数据
export function getStoreBasicDataApi(data) {
return request({
url: `/report/new/report/month/mall/head`,
method: "get",
data
});
}
// 获取门店的营业时间
export function getStoreHoursDateApi(data) {
return request({
url: `/report/b-mall-business-hours/date`,
method: "get",
data
});
}
// 获取集团下的所有门店
export function getAccountStoreApi(data) {
return request({
url: `/report/malls`,
method: "get",
data
});
}
// 获取集团下所有的大区
export function getAccountGroupApi(data) {
return request({
url: `/report/groups`,
method: "get",
data
});
}
// 获取集团下所有的大区,包含门店数量
export function getUserAccountGroupApi(data) {
return request({
url: `/report/b-group/userGroups`,
method: "get",
data
});
}
// 获取集团下的所有品类和标签
export function getLabelByAccountIdApi(data) {
return request({
url: `/report/mallLabel/getLabelByAccountId`,
method: "get",
data
});
}
// 根据标签获取所有门店
export function getMallByGroupAndLabelApi(data) {
return request({
url: `/report/b-group/getMallByGroupAndLabel`,
method: "post",
data
});
}
// 根据标签获取所有门店
// 获取集团卡片列表
export function getAccountCardListApi(data) {
return request({
url: `/report/report/account/overview/card`,
method: "get",
data
});
}
// 获取门店详情
export function getStoreInfoApi(id) {
return request({
url: `/report/b-mall/${id}`,
method: "get"
});
}
// 获取近期客流趋势
export function getFlowTrendApi(data) {
return request({
url: `/report/basePassengerFlow${data.orgId?'':'/account'}/passengerFlow`,
method: "get",
data
});
}
// 获取停留时长分布
export function getResidenceTimeApi(data) {
return request({
url: `/report/basePassengerFlow/residenceTime`,
method: "get",
data
});
}
// 获取分区顾客流向
export function getGateFlowDirectionNewApi(data) {
return request({
url: `/report/mall/gateFlowDirectionNew`,
method: "get",
data
});
}
// 获取门店客群画像
export function getFaceAnalyzeStaMallApi(data) {
return request({
url: `/report/report/faceAnalyzeSta/mall`,
method: "get",
data
});
}
// 获取性别分布
export function getFaceGenderApi(data) {
return request({
url: `/report/account/faceAnalyze/faceGender`,
method: "get",
data
});
}
// 获取年龄分布
export function getFaceAgeApi(data) {
return request({
url: `/report/account/faceAnalyze/faceAge`,
method: "get",
data
});
}
// 门店客流热力
export function getThermodynamicMallApi(data) {
return request({
url: `/report/report/thermodynamic/mall`,
method: "get",
data
});
}
// 门店客流热力
export function getTrafficConversionMallApi(data) {
return request({
url: `/report/traffic/conversion/mall`,
method: "get",
data
});
}
// 区域统计
export function getGateStatisticsApi(data) {
return request({
url: `/report/gate/analyse/statistics`,
method: "get",
data
});
}
// 门店排行
export function getStoreRankApi(data, type = 'month') {
return request({
url: `/report/new/report/${type}/account/body`,
method: "get",
data
});
}
// 获取门店能用的指标
export function getStoreIndicatorsApi(id) {
return request({
url: `/report/b-mall-index/indexConfig/${id}`,
method: "get",
});
}
// 获取集团能用的指标
export function getAccountIndicatorsApi(id) {
return request({
url: `/report/b-mall-index/account/indexConfig/${id}?accountId=${id}`,
method: "get",
});
}
// 获取用户门店数量、设备总量
export function getUserStoreEquipmentNumApi(data) {
return request({
url: `/patrol/app/overview`,
method: "get",
data
})
}
// 获取用户集团待办任务
export function getUserTaskNumApi(data) {
return request({
url: `/patrol/statistics/personPlan`,
method: "get",
data
})
}
import request from "@/utils/request";
// 获取远端文件 assetsIp形式
export function getI18nPropertiesApi(lang) {
return request({
url: `/i18n/server/messages_${lang}.properties`,
method: "get",
}, 'assetsIp');
}
\ No newline at end of file
export * from './i18n'
export * from './login'
export * from './user'
export * from './home'
export * from './version'
export * from './inspection'
export * from './store-analysis'
export * from './message'
import request from "@/utils/request";
// 获取监控点,组织结构
export function doMonitorGroupApi(data) {
return request({
url: `/patrol/patrolGate/gateGroup`,
method: "get",
data
});
}
// 获取巡检单列表
export function doInspectionFormListApi(data) {
// accountId 337, pageSize 999
return request({
url: `/patrol/b-patrol-template/list`,
method: "get",
data
});
}
// 获取整改人列表
export function doRectifiedPersonListApi(data) {
const { restaurantId } = data
return request({
url: `/patrol/s-user/mall/${restaurantId}`,
method: "get",
});
}
// 获取巡检单详情
export function doInspectionFormDetailApi(data) {
const { id } = data
return request({
url: `/patrol/b-patrol-template/${id}`,
method: "get",
data
});
}
// 保存巡检单
export function doInspectionFormSaveApi(data) {
/* const formData = new FormData();
Object.keys(data).forEach(k => {
formData.append(k, data[k])
})
formData.append('name', 'John Doe');
console.log('formData', formData) */
return request({
url: `/patrol/patrolRecord`,
method: "post",
data,
// header: { 'content-type': 'multipart/form-data' },
header: { 'content-type': 'application/x-www-form-urlencoded;charset=utf-8' },
});
}
// 获取巡检列表
export function doGetInspectionListApi(data) {
// ?pageNum=1&pageSize=10&startDate=2025-05-22&endDate=2025-05-29&mallId=12914&accountId=2&patrolType=1
return request({
url: `/patrol/patrolRecord/list`,
method: "get",
data: Object.assign({}, data)
});
}
// 获取巡检详情
export function doGetInspectionDetailApi(id) {
return request({
url: `/patrol/patrolRecord/${id}`,
method: "get",
});
}
// 获取监控点信息
export function doGetPatrolGateApi(id) {
return request({
url: `/patrol/patrolGate/${id}`,
method: "get",
});
}
// 处理巡检
export function doPatrolRecordFormSaveApi(data) {
return request({
url: `/patrol/patrolRecord/handle`,
method: "post",
data,
header: { 'content-type': 'application/x-www-form-urlencoded;charset=utf-8' },
});
}
// 获取收藏门店的接口
export function getPatrolGateMarkListApi(data) {
return request({
url: `/patrol/patrolGate/bookmark/list`,
method: "get",
data
});
}
// 获取收藏门店的接口
export function getPatrolGateMarkMallsApi(data) {
return request({
url: `/patrol/patrolGate/malls`,
method: "get",
data
});
}
// 根据标签获取门店的接口
export function getMallsByLabelIdApi(data) {
return request({
url: `/patrol/app/getMallsByLabelId`,
method: "get",
data
});
}
import request from "@/utils/request";
// 登录
export function doLoginApi(data) {
return request({
url: `/report/users/login`,
method: "post",
data
});
}
// 获取验证码
export function doLoginGetCodeApi(data) {
const type = data.email ? 'email' : 'phone'
return request({
url: `/report/users/verify/send/${type}/code`,
method: "get",
data,
});
}
// 检查验证码
export function doLoginCheckCodeApi(data) {
return request({
url: `/report/users/verify/check/code`,
method: "get",
data,
});
}
// 重置密码
export function doLoginResetApi(data) {
return request({
url: `/report/users/verify/reset`,
method: "post",
data,
});
}
// 验证用户名
export function doLoginCheckNameApi(data) {
return request({
url: `/report/users/verify/contractInfo`,
method: "get",
data,
});
}
// 重置密码
export function doResetPasswordApi(data) {
return request({
url: `/report/users/updateUser`,
method: "post",
data,
});
}
// 忘记密码
export function getVerifyTypeByLoginNameApi(data) {
return request({
url: `/report/users/verify/contractInfo`,
method: "get",
data,
});
}
// 发送验证码
// unid verifyType
export function sendVerifyCodeApi(data) {
return request({
url: `/report/users/verify/send/code`,
method: "get",
data
});
}
// 比较验证码
// loginName/code 返回"275cf4fb-56da-41ac-a176-a4bdef1ff13e"
export function checkVerifyCodeApi(data) {
return request({
url: `/report/users/verify/check/code`,
method: "get",
data,
});
}
// 重置密码
// {
// "loginName": "陈岩",
// "password": "Chenyan1.",
// "code": "275cf4fb-56da-41ac-a176-a4bdef1ff13e"
// }
export function resetPasswordByForgetApi(data) {
return request({
url: `/report/users/verify/check/code`,
method: "post",
data,
});
}
///report/users/verify/reset
\ No newline at end of file
import request from "../utils/request";
// app绑定client_id
export function bindClientIdApi(data) {
return request({
url: `/report/r-user-client`,
method: "post",
data,
});
}
// 获取通知列表
export function getMessageListApi(data) {
return request({
url: `/report/d-task-message/list`,
method: "get",
data,
});
}
// 更新任务状态
export function updateTaskStatusApi(data) {
return request({
url: `/report/rd-task-log`,
method: "post",
data,
});
}
// 批量更新任务状态
export function batchUpdateTaskStatusApi(data) {
return request({
url: `/report/d-task-log/batch-update`,
method: "post",
data,
});
}
// 获取工单的处理记录
export function getTaskLogRecordApi(data) {
return request({
url: `/report/d-task-log/list`,
method: "get",
data,
});
}
// 获取工单的处理记录
export function setTaskStatusApi(data) {
return request({
url: `/report/d-task-log`,
method: "post",
data,
});
}
// 读消息后,标记为已读
export function readMessageApi(data) {
return request({
url: `/report/d-task-message`,
method: "post",
data,
});
}
// 设置防打扰时间
export function setDisturbTimeApi(data) {
return request({
url: `/report/b-task-mute-config`,
method: "post",
data,
});
}
// 获取防打扰时间
export function getDisturbTimeApi(data) {
return request({
url: `/report/b-task-mute-config/list`,
method: "get",
data,
});
}
// 上传图片
export function uploadImageApi(data) {
return request({
url: `/report/common/upload`,
method: "post",
data,
});
}
// 查看mall维度的统计
export function getMallStatisticsApi(data) {
return request({
url: `/report/d-task-record/mallStats`,
method: "get",
data,
});
}
// 查看天维度的统计 -曲线图
export function getDayStatisticsApi(data) {
return request({
url: `/report/d-task-record/dayStats`,
method: "get",
data,
});
}
// 查看当月对应的报警数量
export function getMonthStatsApi(data) {
return request({
url: `/report/d-task-record/monthStats`,
method: "get",
data,
});
}
import request from "../utils/request";
// 获取门店的指标列表
export function getUserIndicatorIndexApi(data) {
return request({
url: `/report/b-gate-index/indexConfig/${data?.id}`,
method: "get",
data
});
}
// 获取区域统计数据
export function getAreaAnalysisStatisApi(data) {
return request({
url: `/report/gate/analyse/statistics`,
method: "get",
data
});
}
import request from "@/utils/request";
// 获取用户组下的信息数据
export function getUserGroupsApi(data) {
return request({
url: `/report/b-group/userGroups`,
method: "get",
data
});
}
// 获取用户的集团分组
export function getUserAccountsApi(data) {
return request({
url: `/report/accounts`,
method: "get",
data
});
}
// 获取用户详细信息
export function getUserInfoApi(id) {
return request({
url: `/report/s-user/${id}`,
method: "get"
});
}
/**
* 用户的门店权限
*/
export function getUserStoreListApi(data) {
return request({
url: `/report/userMalls`,
method: "get",
data
});
}
import request from "@/utils/request";
// 获取app版本信息
export function getAppVersionApi(data) {
return request({
url: `/report/app/lastest/version`,
method: "get",
data
});
}
\ No newline at end of file
<template>
<uv-popup ref="alarmHandlePop" round="24rpx">
<view class="pop_header">
<view style="width: 22px; visibility: hidden"></view>
<text class="pop_header_text">{{ statusText }}</text>
<uv-icon name="close" size="22" class="pop_header_icon" @tap="handleCloseI18nPop"></uv-icon>
</view>
<view class="upload-block">
<view class="upload-title">
<view class="u-t-title">{{ t('format.picture') }}</view>
<text>{{ fileList.length }}/1</text>
</view>
<uv-upload :fileList="fileList" :maxSize="10485760" :maxCount="1" @afterRead="handleUploadImg"
@delete="handleDeleteImg" @oversize="handleOverSize"></uv-upload>
<view class="note-title">
<!-- 确定不需要必填 -->
<!-- <text>*</text> -->
<view class="u-t-title">
{{ t('maintenance.common.remark') }}
</view>
</view>
<uv-textarea maxlength="300" count v-model="comment" :placeholder="t('maintenance.common.input')"></uv-textarea>
</view>
<view class="uni-date-changed uni-date-btn--ok">
<view class="uni-datetime-picker--btn" @tap="handleConfirmAlarm">
<text class="confirm-btn">{{ t("message.confirm") }}</text>
</view>
</view>
</uv-popup>
</template>
<script setup>
import {
ref,
computed,
onMounted
} from "vue";
import {
t
} from "@/plugins/index.js";
import {
setTaskStatusApi
} from "@/api/message.js";
import host from "../utils/ip-config.js";
import {
timeFormat
} from '@/uni_modules/uv-ui-tools/libs/function/index.js';
const emit = defineEmits(["success"]);
/******** 文件处理相关 **********/
const fileList = ref([]);
const handleUploadImg = async (event) => {
const result = await uploadFilePromise(event.file.url);
fileList.value = [{
status: "success",
uploadUrl: result,
url: `${host.assetsIp}/${result}`,
}, ];
};
const handleOverSize = () => {
uni.showToast({
icon: "none",
title: t("message.imgSize") + "10M",
});
};
const handleDeleteImg = () => {
fileList.value = [];
};
const uploadFilePromise = (url) => {
return new Promise((resolve, reject) => {
uni.showLoading({
title: t('button.uploaing')
})
uni.uploadFile({
url: `${host.ip}/report/common/upload`,
filePath: url,
name: "file",
formData: {
pathType: "task",
},
header: {
Authorization: uni.getStorageSync('Authorization') || '',
},
success: (res) => {
console.log(res);
const data = JSON.parse(res.data);
if (data.code === 200) {
resolve(data.msg);
}
},
complete: () => {
uni.hideLoading()
}
});
});
};
/******** 数据提交相关 **********/
const comment = ref("");
const handleConfirmAlarm = async () => {
try {
uni.showLoading({
title: t('button.uploaing')
})
comment.value = comment.value.trim();
// if (!comment.value) {
// uni.showToast({
// icon: 'none',
// title: t('pholder.enterNote')
// })
// return false
// }
const time = timeFormat(new Date().getTime(), 'yyyy-mm-dd hh:MM:ss');
const params = {
taskId: taskId.value,
comment: comment.value,
status: status.value,
id: idValue.value,
counttime: time, //当前设备的时间
};
console.log(params, 'params');
if (fileList.value.length > 0) {
params.photo = fileList.value[0].uploadUrl;
}
const res = await setTaskStatusApi(params);
if (res.code === 200) {
alarmHandlePop.value?.close();
emit("success", params);
}
} catch (err) {
console.log(err);
} finally {
uni.hideLoading()
}
};
/******** 弹窗数据相关 **********/
const alarmHandlePop = ref(null);
const taskId = ref("");
const status = ref("");
const idValue = ref();
const statusText = computed(() => {
return status.value === "RESOLVE" ? t('button.confirm') : t('TaskNotice.falseAlarm');
});
const handleOpen = (id, type, idValueParam) => {
taskId.value = id;
status.value = type;
idValue.value = idValueParam;
alarmHandlePop.value?.open("bottom");
};
const handleClose = () => {
alarmHandlePop.value?.close();
};
const handleCloseI18nPop = () => {
alarmHandlePop.value?.close();
};
defineExpose({
handleOpen,
});
</script>
<style lang="scss" scoped>
.upload-block {
width: 100%;
padding: 32rpx;
.u-t-title {
font-size: 28rpx;
font-family: Inter, Inter;
font-weight: 500;
color: #000000;
}
.upload-title {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 28rpx;
margin-bottom: 20rpx;
text {
color: #90949d;
}
}
.note-title {
display: flex;
align-items: center;
margin: 8rpx 0 24rpx 0;
display: flex;
gap: 2rpx;
text {
color: #ff3838;
font-size: 24rpx;
}
}
}
// uv-upload__success 改这个样式 不要显示
:deep(.uv-upload__success) {
display: none;
}
:deep(.uv-upload__deletable) {
width: 18px;
height: 18px;
.uv-upload__deletable__icon {
transform: scale(1.2);
top: 4rpx;
right: 4rpx;
}
}
</style>
\ No newline at end of file
<template>
<view style="width: 100%;flex: 1;height: 100%;">
<!-- #ifdef MP-WEIXIN -->
<l-echart ref="chartRef" @finished="init" :custom-style="`width:654rpx;height:${height}`"></l-echart>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<l-echart ref="chartRef" @finished="init"></l-echart>
<!-- #endif -->
</view>
</template>
<script setup>
const props=defineProps({
height:{
type:String,
default:'420rpx'
}
})
import {
ref,
onMounted,
shallowRef
} from 'vue';
import lEchart from '../uni_modules/lime-echart/components/l-echart/l-echart.vue';
// #ifndef MP-WEIXIN
import * as echarts from 'echarts';
// #endif
// #ifdef MP-WEIXIN
const echarts = require('../uni_modules/lime-echart/static/echarts.min');
// #endif
const chartRef = ref(null);
const myChart = shallowRef(null);
// 初始化 Promise 及 resolve 函数
const initializePromise = ref(null);
let resolveInitialize = null;
// 保证函数执行时initCharts一定在init后面
onMounted(() => {
initializePromise.value = new Promise((resolve) => {
resolveInitialize = resolve;
});
});
// 默认配置
const defaultOption = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [150, 230, 224, 218, 135, 147, 260],
type: 'line'
}]
};
const init = async (e) => {
if (!chartRef.value) return;
try {
myChart.value = await chartRef.value.init(echarts);
console.log('图表初始化完成');
// 解决 Promise,通知初始化完成
if (resolveInitialize) {
resolveInitialize();
resolveInitialize = null; // 防止重复调用
}
} catch (error) {
console.error('图表初始化失败:', error);
}
};
/**
* 暴露方法,进行图表渲染
*/
const initCharts = async (options = defaultOption) => {
await initializePromise.value; // 等待初始化完成
myChart.value?.setOption(options);
};
defineExpose({
initCharts
});
</script>
\ No newline at end of file
<template>
<view class="header" :style="`background-color:${bgColor}`">
<StatusBar />
<uv-navbar
class="custom-nav-bar"
:title="title"
:bg-color="bgColor"
:titleStyle="{fontSize:'32rpx',fontWeight:'bold',color: titleColor}"
>
<template v-slot:left>
<slot name="left"></slot>
<template v-if="!$slots.left">
<view v-if="isTab"></view>
<uv-icon v-else name="arrow-left" :color="arrowLeftColor" size="20px" @tap="backTo" />
</template>
</template>
<template v-slot:right>
<slot name="right"></slot>
</template>
</uv-navbar>
<view v-if="commonNav" style="height: 44px;"></view>
</view>
</template>
<script setup>
import StatusBar from '@/components/StatusBar.vue';
defineProps({
title: {
type: String,
required: true
},
bgColor:{
type: String,
// default: '#3277FB'
default: '#3277FB'
},
isTab:{
type: Boolean,
default: false
},
titleColor:{
type: String,
default: '#ffffff'
},
arrowLeftColor:{
type: String,
default: '#ffffff'
},
commonNav:{
type: Boolean,
default: true
}
})
const backTo = ()=>{
uni.navigateBack({
delta: 1
})
}
</script>
<style lang="scss" scoped>
</style>
<template>
<uv-popup ref="i18nPopRef" round="24rpx" @maskClick="handleShowTabbar">
<view class="pop_header">
<view style="width: 22px;visibility: hidden;"></view>
<!-- <view style="width: 22px;"></view> -->
<text class="pop_header_text">{{ t('table.language') }}</text>
<uv-icon name="close" size="22" class="pop_header_icon" @tap="handleCloseI18nPop"></uv-icon>
</view>
<view class="national">
<view class="n_item" v-for="(item,index) in i18nList" :key="item.value"
:class="{'n_item_active':index === checkIndex}" @tap="handleChangeI18n(index)">
<image :src="item.nationalFlag" style="width: 52rpx;" mode="widthFix"></image>
<view class="n_label">
<view class="n_item_text" :class="{'n_item_active_text':index === checkIndex}">{{ item.label }}</view>
<view class="n_item_textDesc">{{ item.labelDesc }}</view>
</view>
<image v-if="checkIndex === index" src="@/static/national-flag/check.png" style="width: 36rpx;height:36rpx"
mode="aspectFill"></image>
</view>
</view>
<view class="confirm-button" @tap="handleConfirmSelectType">
<text class="btn">{{ t('message.confirm') }}</text>
</view>
</uv-popup>
</template>
<script setup>
import {
ref,
computed
} from 'vue'
import {
onLoad
} from '@dcloudio/uni-app'
import {
t,
changeLanguage
} from '@/plugins/index.js'
import {
bindClientIdApi
} from '@/api';
import usa from '@/static/national-flag/usa.png'
import japan from '@/static/national-flag/japan.png'
import cn from '@/static/national-flag/cn.png'
const props = defineProps({
reloadPage: {
type: String,
default: ''
},
bindCid: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['changeLang'])
const i18nPopRef = ref(null)
const checkIndex = ref(0)
const handleOpenI18n = () => {
uni.hideTabBar()
const index = i18nList.findIndex(item => item.value === currentLang.value)
checkIndex.value = index === -1 ? 0 : index
i18nPopRef.value?.open('bottom')
}
const handleShowTabbar = ()=>{
uni.showTabBar()
}
const handleCloseI18nPop = () => {
i18nPopRef.value?.close()
handleShowTabbar()
}
const handleChangeI18n = (index) => {
checkIndex.value = index
}
const handleConfirmSelectType = async () => {
try {
const index = i18nList.findIndex(item => item.value === currentLang.value)
currentLang.value = i18nList[checkIndex.value].value
await changeLanguage(currentLang.value, true)
await handleCloseI18nPop()
// 重新载入
if (props.reloadPage) {
uni.setStorageSync('lang', currentLang.value)
// if(props.tab){
// uni.switchTab({
// url: props.reloadPage
// })
// }else{
// // uni.reLaunch({
// // url: props.reloadPage
// // })
// }
}
const item = i18nList.find(item => item.value === currentLang.value)
if(props.bindCid){
await handleRebindCid()
}
await emit('changeLang', item)
} catch (e) {
console.log(e)
}
}
const handleRebindCid = async ()=>{
try {
const clientInfo = uni.getDeviceInfo()
await bindClientIdApi({
clientId: uni.getStorageSync('clientId'),
clientType: clientInfo.osName, // ios or android
deviceInfo: JSON.stringify(clientInfo), // 备用传值
language: uni.getStorageSync('lang'),
status: 1
})
} catch (error) {
console.log('error:', error);
}
}
const currentLang = ref(uni.getStorageSync('lang') || 'en_US')
const currentLangText = computed(() => {
const item = i18nList.find(item => item.value === currentLang.value)
return item?.labelDesc || 'English,US'
})
const currentLangImg = computed(() => {
const item = i18nList.find(item => item.value === currentLang.value)
return item?.nationalFlag || usa
})
const i18nList = [{
label: 'English,US',
labelDesc: 'English,US',
value: 'en_US',
nationalFlag: usa
}, {
label: 'Japan',
labelDesc: 'Japanese',
value: 'ja_JP',
nationalFlag: japan
},
// {
// label: 'China',
// labelDesc: 'Chinese',
// value: 'zh_CN',
// nationalFlag: cn
// }, {
// label: 'China',
// labelDesc: 'Traditional Chinese',
// value: 'zh_TW',
// nationalFlag: cn
// },
]
onLoad(()=>{
const item = i18nList.find(item => item.value === currentLang.value)
emit('changeLang', item)
})
defineExpose({
currentLangImg,
currentLangText,
handleOpenI18n
})
</script>
<style lang="scss" scoped>
.national {
overflow: hidden;
padding: 32rpx 0 32rpx 32rpx;
}
.n_item {
padding: 16rpx 32rpx 16rpx 0;
display: flex;
align-items: center;
border-bottom: 1px solid #EEF0F3;
}
.n_label {
flex: 1;
margin-left: 30rpx;
.n_item_text {
font-family: Inter, Inter;
font-weight: 500;
font-size: 28rpx;
color: #262626;
}
.n_item_textDesc {
font-family: Inter, Inter;
font-weight: 400;
font-size: 24rpx;
color: #90949D;
}
}
.confirm-button {
width: 686rpx;
height: 88rpx;
background-color: #387CF5;
border-radius: 12rpx;
margin: 14rpx 32rpx 50rpx;
text-align: center;
line-height: 88rpx;
.btn {
color: #fff;
text-align: center;
}
}
</style>
<template>
<uv-popup ref="indicatorsFilterRef" @change="handleChange">
<view class="pop_header">
<view style="width: 22px;"></view>
<text class="pop_header_text">{{ defaultTitle }}</text>
<uv-icon name="close" size="22" class="pop_header_icon" @tap="close"></uv-icon>
</view>
<scroll-view scroll-y="true" style="height: 240px;">
<view class="l_type">
<view class="l_type_item" v-for="(item,index) in options" :key="item[optionsProps.value]"
:class="{'l_type_item_active':index === checkIndex}" @tap="handleChangeCheckType(index)">
<text class="l_type_item_text"
:class="{'l_type_item_active_text':index === checkIndex}">{{ item[optionsProps.label] }}</text>
</view>
</view>
</scroll-view>
</uv-popup>
</template>
<script setup>
import {
computed,
ref
} from 'vue'
import {
t
} from '@/plugins/index.js'
const emit = defineEmits(['change','modalChange'])
const props = defineProps({
title: {
type: String,
default: ''
},
options: {
type: Array,
default: () => []
},
optionsProps: {
type: Object,
default: () => ({
label: 'name',
value: 'key'
})
},
})
const defaultTitle = computed(()=>{
return props.title || t('app.title.kpiSelect')
})
const checkIndex = ref(0)
const indicatorsFilterRef = ref(null)
const open = () => {
indicatorsFilterRef.value?.open('bottom')
}
const close = () => {
indicatorsFilterRef.value?.close()
}
const handleChangeCheckType = (index) => {
checkIndex.value = index
emit('change', props.options[index])
close()
}
const handleChange = (e)=>{
emit('modalChange',e.show)
}
defineExpose({
open,
close
})
</script>
<style lang="scss" scoped>
@import '../styles/normal.scss';
</style>
<template>
<view class="i-f-title" @tap="handleSelectFilterItem">
<view v-if="title" class="f-t-title">{{ title }}</view>
<view class="filter">
<text class="filter_text">{{ showText }}</text>
<uv-icon name="arrow-right" class="filter_icon" size="20rpx" color="#90949D" />
</view>
</view>
<IndicatorsFilter ref="filterRef" :options="options" :options-props="optionsProps" @change="handleIndicatorChange"
@modalChange="handleModalChange" />
</template>
<script setup>
import { ref, computed } from "vue";
import IndicatorsFilter from "./IndicatorsFilter.nvue";
const props = defineProps({
title: {
type: String,
default: "",
},
options: {
type: Array,
default: () => [],
},
optionsProps: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["change", "modalChange"]);
const indicatorText = ref("");
const showText = computed(() => {
return indicatorText.value
? indicatorText.value
: props.options?.[0]?.[props.optionsProps.label];
});
const hasFilterSlots = computed(() => props.options.length > 0);
const filterRef = ref(null);
const handleSelectFilterItem = () => {
console.log('handleSelectFilterItem-点击');
filterRef.value?.open();
};
const handleIndicatorChange = (val) => {
indicatorText.value = val[props.optionsProps.label];
emit("change", val[props.optionsProps.value], "indicatorKey");
};
const handleModalChange = (val) => {
emit("modalChange", val);
};
</script>
<style lang="scss" scoped>
.i-f-title{
display: flex;
flex-direction: row;
align-items: center;
flex: 1;
max-height: 60rpx;
justify-content: space-between;
.f-t-title{
font-weight: bold;
font-size: 30rpx;
color: #202328;
line-height: 42rpx;
}
}
.filter {
// margin-left: auto;
flex: 1;
background-color: #fff;
width: 300rpx;
height: 60rpx;
border-radius: 12rpx;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
&_text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 50rpx;
color: #90949D;
font-size: 24rpx;
}
}
</style>
\ No newline at end of file
<template>
<view class="mall-select-group">
<view class="group home-group" style="display: flex;flex-direction: row;justify-content: center;align-items: center;" v-if="isHome">
<slot name="leftIcon"></slot>
<uv-text @tap="handleTapToChoose" :text="displayVal.displayValue" lines="1" style="max-width: 640rpx;" :size="fontSize + 'rpx'" :bold="true" color="#fff"></uv-text>
<image @tap="handleTapToChoose" src="/static/tabbar/arrow-down-w.png" class="dot-img" style="width: 20rpx;min-width: 20rpx;" mode="widthFix"></image>
<slot @tap="handleTapToChoose" name="rightIcon"></slot>
</view>
<view v-else @tap="handleTapToChoose" class="page-group group"
style="display: flex;flex-direction: row;justify-content: center;align-items: center;">
<slot name="leftIcon"></slot>
<text class="group_text" style="font-size: 28rpx;">{{displayVal.displayValue}}</text>
<image src="/static/tabbar/arrow-down-w.png" class="dot-img" style="width: 20rpx;min-width: 20rpx;" mode="widthFix"></image>
<slot name="rightIcon"></slot>
</view>
</view>
</template>
<script setup>
import {
onLoad
} from '@dcloudio/uni-app'
import {
getStageObj
} from '@/utils'
import {
onMounted,
ref
} from 'vue'
const emit = defineEmits(['change'])
const props = defineProps({
currentStore: {
type: Object,
default: () => {
return {}
}
},
chooseGroup: {
type: String,
default: '0'
},
hasStore: {
type: String,
default: '1'
},
isHome: {
type: Boolean,
default: false
},
fontSize: {
type: Number,
default: 32
}
})
const displayVal = ref({
accountId: '',
groupId: '',
displayValue: '',
storeId: ''
})
// 获取当前选择的集团
onLoad(() => {
// 当前集团
const account = getStageObj('account')
// 如果有传递的门店数据,优先展示门店数据
if (props.currentStore.id) {
const {
accountId,
id,
groupId,
name
} = props.currentStore
displayVal.value = {
accountId,
groupId,
displayValue: name,
storeId: id
}
} else if (account.id) {
displayVal.value = {
accountId: account?.id,
displayValue: account?.name
}
}
emit('change', displayVal.value, 'init')
// 选中门店 执行渲染门店相关数据
uni.$on('mallSelect', e => {
let params = {}
switch (e.type) {
case 'account': {
params = {
accountId: account?.id,
groupId: '',
storeId: ''
}
break
}
case 'group': {
params = {
accountId: account?.id,
groupId: e.id,
storeId: ''
}
break
}
case 'store': {
params = {
accountId: account?.id,
groupId: e.groupId,
storeId: e.id
}
uni.setStorageSync('store', JSON.stringify(e))
emit('storeChange', e)
break
}
}
params.displayValue = e.name
displayVal.value = params
emit('change', displayVal.value, 'change')
})
})
/********** 搜索相关 ************/
const searchVal = ref('')
/********** popup相关 ************/
const accountShopRef = ref(null)
onMounted(() => {
// accountShopRef.value?.open('bottom')
})
const handleTapToChoose = () => {
uni.navigateTo({
url: `/subPackages/accountGroup/pages/mall-group/index?chooseGroup=${props.chooseGroup}&hasStore=${props.hasStore}`
})
// accountShopRef.value?.open('bottom')
}
const handleCloseChoose = () => {
accountShopRef.value?.close()
}
</script>
<style lang="scss" scoped>
@import '@/styles/normal.scss';
.group {
flex-direction: row;
align-items: center;
/* #ifdef APP */
lines: 1;
/* #endif */
&_text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #FFFFFF;
font-weight: 700;
font-size: 44rpx;
}
}
.mall-select-group {
flex: 1;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
.group{
&_text {
font-size: 32rpx;
// font-family: PingFang_Medium;
}
}
.dot-img{
margin-left: 10rpx;
}
}
.page-group{
width: 680rpx;
}
</style>
<template>
<view class="page-options" :class="{ 'as-comp': as === 'comp' }" :style="compStyle">
<view class="mall">
<MallSelectNvue :current-store="storeVal" :has-store="hasStore" :choose-group="chooseGroup"
@change="handleMallChange" @storeChange="handlStoreChange">
<template v-if="$slots.leftIcon" #leftIcon>
<slot name="leftIcon"></slot>
</template>
<template v-if="$slots.rightIcon" #rightIcon>
<slot name="rightIcon"></slot>
</template>
</MallSelectNvue>
</view>
<view class="date-picker">
<DatePicker @change="handleDateChange" :has-today="hasToday" @modalChange="handleModalChange"
:type="dateType" ref="datePickerRef">
<template #filter v-if="hasFilterSlots">
<view class="filter" @tap="handleSelectFilterItem">
<text class="filter_text">{{ showText }}</text>
<uv-icon name="arrow-down-fill" class="filter_icon" size="10rpx" color="#202328" />
</view>
</template>
</DatePicker>
</view>
</view>
<IndicatorsFilter ref="filterRef" :options="options" :options-props="optionsProps" @change="handleIndicatorChange"
@modalChange="handleModalChange" />
</template>
<script setup>
import {
onLoad
} from "@dcloudio/uni-app";
import {
getStageObj
} from "@/utils";
import {
getMallByGroupAndLabelApi
} from "@/api";
import {
ref,
onMounted,
computed,
onBeforeUnmount
} from "vue";
import DatePicker from "./DatePicker.nvue";
import MallSelectNvue from "./MallSelect.nvue";
import IndicatorsFilter from "./IndicatorsFilter.nvue";
const props = defineProps({
compStyle: {
type: Object,
default: () => ({})
},
options: {
type: Array,
default: () => [],
},
optionsProps: {
type: Object,
default: () => ({
label: "label",
value: "value",
}),
},
hasToday: {
type: Boolean,
default: false,
},
as: {
type: String,
default: "page", // page:页面,comp: 组件
},
hasStore: {
type: String,
default: "1",
},
chooseGroup: {
type: String,
default: "0",
},
defaultType: {
type: String,
default: "store",
},
// 新增日期参数
defaultDate: {
type: String,
default: "",
},
dateType: {
type: String,
default: "last7days",
},
});
const emit = defineEmits(["change", "modalChange"]);
const indicatorText = ref("");
const showText = computed(() => {
return indicatorText.value ?
indicatorText.value :
props.options?.[0]?.[props.optionsProps.label];
});
onBeforeUnmount((e) => {
console.log("page-remove");
});
// 日期选择器引用
const datePickerRef = ref(null);
// 设置日期类型和日期
const setDateTypeAndDate = (type, date) => {
if (datePickerRef.value) {
console.log('暴露出方法');
datePickerRef.value.setDateType(type, date);
}
};
const handleModalChange = (val) => {
emit("modalChange", val);
};
// 有筛选项
const hasFilterSlots = computed(() => props.options.length > 0);
const filterRef = ref(null);
const handleSelectFilterItem = () => {
filterRef.value?.open();
};
const handleIndicatorChange = (val) => {
storeParams.value = {
...storeParams.value,
indicatorKey: val[props.optionsProps.value],
};
indicatorText.value = val[props.optionsProps.label];
emit("change", storeParams.value, "indicatorKey");
};
const storeVal = ref({});
if (props.defaultType === "store") {
storeVal.value = getStageObj("store") || {};
console.log(storeVal.value, "---");
}
// onLoad(() => {
// })
const flag = ref(false); // 增加自锁
const storeParams = ref({});
const handleMallChange = (e) => {
console.log(e, "mall");
storeParams.value = {
...storeParams.value,
...e,
};
if (flag.value) {
emit("change", storeParams.value, "groupStore");
}
flag.value = true;
};
const handleDateChange = (e) => {
console.log(e, "date");
const [startDate, endDate] = e.range;
storeParams.value = {
...storeParams.value,
startDate,
endDate,
...e,
};
console.log(storeParams.value, 'storeParams.value-----传参');
emit("change", storeParams.value, "datePicker");
};
// 选择的门店更改时,更新storage中
const handlStoreChange = (val) => {
console.log(val, "-s-s-s-s-s-s-s");
uni.setStorageSync("store", JSON.stringify(val));
};
// 暴露方法给父组件
defineExpose({
setDateTypeAndDate
});
</script>
<style lang="scss" scoped>
.mall-select {}
.page-options {
box-sizing: border-box;
width: 750rpx;
// position: sticky;
/* #ifdef H5 */
// top: 44px;
/* #endif */
/* #ifndef H5 */
// top: 0;
/* #endif */
z-index: 999;
background-color: #f2f3f6;
display: flex;
flex-direction: column;
align-items: center;
.mall {
padding-bottom: 20rpx;
width: 750rpx;
height: 72rpx;
z-index: 990;
background-color: #3277fb;
display: flex;
align-items: center;
justify-content: center;
}
.page-options-block {
// height: 42rpx;
height: 72rpx;
}
.date-picker {
width: 750rpx;
padding: 16rpx 20rpx;
}
}
.as-comp {
// height: 84rpx;
/* #ifdef H5 */
top: 0;
/* #endif */
/* #ifndef H5 */
position: sticky;
top: 0;
z-index: 9999;
// background-color: #f00;
/* #endif */
.mall {
height: 0;
padding: 0;
overflow: hidden;
}
.page-options-block {
height: 0;
margin-bottom: 0;
}
}
.filter {
margin-left: auto;
background-color: #fff;
border-radius: 12rpx;
display: flex;
flex-direction: row;
justify-content: flex-end;
align-items: center;
&_text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 50rpx;
color: #202328;
font-size: 24rpx;
}
}
</style>
<template>
<view class="status_bar" :style="{height:height + 'px','background-color': bgColor}" ></view>
<view class="block" :style="{height: height+'px','background-color': bgColor}"></view>
</template>
<script setup>
import {
onMounted,
ref
} from 'vue';
defineProps({
bgColor:{
type:String,
default:'rgba(0,0,0,0)'
}
})
// h5默认设置为0
const height = ref(uni.getSystemInfoSync().statusBarHeight || 0)
// onMounted(() => {
// // #ifdef APP
// height.value = plus.navigator.getStatusbarHeight()
// // #endif
// // #ifdef MP-WEIXIN
// height.value = 25
// // #endif
// })
</script>
<style>
.status_bar {
width: 750rpx;
position: fixed;
top: 0;
left: 0;
z-index: 9999;
/* background-color: transparent; */
/* background-color: rgba(255,0,0,0.3); */
}
</style>
<template>
<view class="rank-list" v-if="data.length > 0">
<view class="rank-item" v-for="(item, index) in data" :key="item.name">
<view class="rank-item-left r-i-title">
<image :src="rankImg[index]" style="width: 50rpx; height: 32rpx;" v-if="index<=2" mode=""></image>
<uv-text v-else :text="'0' + `${index+1}`" color="#4E515E" size="28rpx" style="text-align: center; min-width: 50rpx;"></uv-text>
</view>
<view class="rank-item-right">
<view class="r-i-head">
<uv-text style="padding-right: 20rpx;" size="26rpx" :text="item.name" lines="1"></uv-text>
</view>
<view class="r-i-container">
<view class="percent" :style="{width:getRankPercentVal(item.value)}"></view>
</view>
</view>
<view class="rank-item-last">
<uv-text style="font-weight: 500;" color="#262626" size="26rpx" :text="formatIndicatorValue(indicatorKey,item.value)"></uv-text>
</view>
</view>
</view>
<view class="p-empty" v-else style="margin-top: 40rpx;">
<image class="p-empty-img" src="/static/common/empty.png" mode=""></image>
<text class="p-empty-text chart-text">{{ t('maintenance.monitor.project.empty.text') }}</text>
</view>
</template>
<script setup>
// import {
// rank1,
// rank2,
// rank3
// } from '@/static/base64.js'
import rank1 from '@/static/message/rank1.png'
import rank2 from '@/static/message/rank2.png'
import rank3 from '@/static/message/rank3.png'
import {
computed
} from 'vue'
import {
formatIndicatorValue
} from '@/utils'
import {
t
} from '@/plugins/index.js'
const props = defineProps({
data: {
type: Array,
default: () => []
},
indicatorKey: {
type: String,
required: true
}
})
const rankImg = [rank1, rank2, rank3]
const maxVal = computed(() => {
return Math.max(...props.data.map(item => item.value))
})
function getRankPercentVal(val) {
if (val === 0) {
return 0
}
return maxVal === 0 ? '0rpx' : (((val / maxVal.value) * 654).toFixed(0) + 'rpx')
// return '210px'
}
</script>
<style lang="scss" scoped>
.rank-list {
padding-top: 28rpx;
// padding: 28rpx 0 14rpx;
.rank-item {
margin-bottom: 28rpx;
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
.rank-item-left {
}
.rank-item-right {
flex: 1;
margin: 0 16rpx;
}
.rank-item-last {
max-width: 96rpx;
// align-self: end;
margin-top: 34rpx;
}
.r-i-head {
display: flex;
flex-direction: row;
align-items: center;
margin-bottom: 14rpx;
.r-i-title {
text-align: center;
display: flex;
align-items: center;
flex-direction: row;
padding-right: 20rpx;
}
}
.r-i-container {
min-width: 500rpx;
max-width: 524rpx;
height: 16rpx;
background-color: #F7F8FB;
border-radius: 12rpx;
overflow: hidden;
.percent {
width: 0;
transition-duration: all 1s;
height: 16rpx;
border-radius: 12rpx;
background-color: #4277F7;
}
}
}
}
</style>
\ No newline at end of file
<template>
<view class="body-view" :style="`height:${swiperHeight}px`">
<scroll-view class="scroll-tab" scroll-x="true" scroll-with-animation :scroll-left="scrollLeft">
<view style="display: flex;flex-direction: row;height: 44px;">
<view class="menu-topic-view" v-for="(item,index) in cards" :key="index" @click="swichMenu(index)" :style="`width:${scrollWidth}`">
<view class="menu-topic">
<text class="menu-topic-text" :style="`font-size: ${props.textSize}`"
:class="{'act-menu-topic-text': currentIndex === index}">{{t(item.name)}}</text>
<image src="@/static/common/tab-act.png" v-if="currentIndex === index" style="width: 32rpx; height: 10rpx;"
mode=""></image>
<view class="tab-block" v-else style="width: 32rpx;height: 10rpx">
</view>
</view>
</view>
</view>
</scroll-view>
<swiper class="swiper-box-list" :style="`height:${swiperHeight-44}px`" :current="currentIndex"
@change="swiperChange">
<swiper-item class="swiper-topic-list" v-for="(item,index) in cards" :key="index">
<scroll-view scroll-y="true" class="scroll-swiper-item" :style="`height:${swiperHeight-44}px`">
<!-- 微信小程序是静态语言,不支持动态组件component,所以这里使用静态组件 -->
<!-- 客流页组件 -->
<Flow v-if="(currentIndex === index || loadedComponents.includes(index))&& item.component === 'Flow'" :index="currentIndex" v-bind="item.props || {}"></Flow>
<IosStoreHeatMapNvue v-if="(currentIndex === index || loadedComponents.includes(index))&& item.component === 'IosStoreHeatMapNvue'" :index="currentIndex" v-bind="item.props || {}"></IosStoreHeatMapNvue>
<!-- #ifndef MP-WEIXIN -->
<StoreHeatMap v-if="(currentIndex === index || loadedComponents.includes(index))&& item.component === 'StoreHeatMap'" :index="currentIndex" v-bind="item.props || {}"></StoreHeatMap>
<AreaStatistic v-if="(currentIndex === index || loadedComponents.includes(index))&& item.component === 'AreaStatistic'" :index="currentIndex" v-bind="item.props || {}"></AreaStatistic>
<!-- #endif -->
<!-- 巡店页组件 -->
<StoreList v-if="(currentIndex === index || loadedComponents.includes(index))&& item.component === 'StoreList'" :index="currentIndex" v-bind="item.props || {}"></StoreList>
<Organization v-if="(currentIndex === index || loadedComponents.includes(index))&& item.component === 'Organization'" :index="currentIndex" v-bind="item.props || {}"></Organization>
<MarkStore v-if="(currentIndex === index || loadedComponents.includes(index))&& item.component === 'MarkStore'" :index="currentIndex" v-bind="item.props || {}"></MarkStore>
<TagStoreVue v-if="(currentIndex === index || loadedComponents.includes(index))&& item.component === 'TagStoreVue'" :index="currentIndex" v-bind="item.props || {}"></TagStoreVue>
<!-- <component v-if="currentIndex === index || loadedComponents.includes(index)" :is="item.component" :index="currentIndex" v-bind="item.props || {}"></component> -->
</scroll-view>
</swiper-item>
</swiper>
</view>
</template>
<script setup>
// 客流页组件
import Flow from '../pages/flow/FlowDetail.nvue'
import AreaStatistic from '../pages/flow/AreaStatistic.nvue'
import IosStoreHeatMapNvue from '../pages/flow/IosStoreHeatMap.nvue';
import StoreHeatMap from '@/subPackages/accountGroup/pages/store-analysis/store-heatmap/index.nvue'
// 巡店页组件
import StoreList from '../pages/inspection/indexStore.vue';
import Organization from '../pages/inspection/Organization.nvue'
import MarkStore from '../pages/inspection/MarkStore.vue';
import TagStoreVue from '../pages/inspection/TagStore.vue';
import {
t
} from '@/plugins/index.js'
import {
ref,
watch,
onMounted,
computed
} from 'vue'
const scrollLeft = ref(0)
const props = defineProps({
cards: {
type: Array,
default: () => []
},
textSize: {
type: String,
default: '32rpx'
},
top: {
type: Number,
default: 0
}
})
const scrollWidth = computed(()=>{
const cardsLength = props.cards.length
return cardsLength === 0? '750rpx':cardsLength>=4 ?'187rpx' : (750/cardsLength).toFixed(0)+'rpx'
})
const currentIndex = ref(0)
const loadedComponents = ref([0])
const setActive = (index)=>{
currentIndex.value = 0
loadedComponents.value = [index]
}
const emit = defineEmits(['indexChange'])
watch(currentIndex, (newIndex) => {
if (!loadedComponents.value.includes(newIndex)) {
loadedComponents.value.push(newIndex)
}
emit('indexChange',newIndex)
})
const swiperHeight = ref(0)
onMounted(() => {
const systemInfo = uni.getSystemInfoSync()
swiperHeight.value = systemInfo.windowHeight - props.top
})
function swichMenu(index) {
currentIndex.value = index
// 滑动swiper后,每个选项距离其父元素最左侧的距离
scrollLeft.value = 0;
if (props.cards.length > 4) {
for (let i = 0; i < index; i++) {
scrollLeft.value += 60
};
}
}
function swiperChange(e) {
let index = e.detail.current
swichMenu(index)
}
defineExpose({
setActive
})
</script>
<style lang="scss">
.body-view {
width: 750rpx;
/* #ifdef APP-NVUE */
flex: 1;
/* #endif */
/* #ifndef APP-NVUE */
display: flex;
flex-direction: column;
/* #endif */
/* #ifdef H5 */
height: calc(100vh - 104px);
/* #endif */
background-color: #F2f3f6;
}
.scroll-tab,
.scroll-tab-vue {
width: 750rpx;
height: 44px;
padding-top: 12rpx;
background-color: #3277FB;
display: flex;
flex-direction: row;
.menu-topic-view {
/* #ifndef APP-NVUE */
min-width: 187rpx;
/* #endif */
.menu-topic {
/* #ifndef APP-NVUE */
display: flex;
flex-direction: column;
/* #endif */
flex: 1;
/* #ifndef APP-NVUE */
display: flex;
flex-direction: column;
/* #endif */
align-items: center;
.menu-topic-text {
color: #fff;
padding-bottom: 6rpx;
}
.act-menu-topic-text {
font-weight: 700;
}
}
}
}
.swiper-box-list {
flex: 1;
/* #ifndef APP-NVUE */
min-height: 0;
/* #endif */
width: 750rpx;
}
/* #ifndef APP-NVUE */
.scroll-swiper-item,
.swiper-topic-list {
// flex: 1;
min-height: 0;
}
.content-scroll {
flex: 1;
}
/* #endif */
</style>
<template>
<view class="time-select">
<!-- 触发器部分 -->
<view class="time-trigger" @click="openPopup">
<view class="setting-item">
<view class="setting-left">
<text class="setting-title">{{ title }}</text>
</view>
<view class="setting-right">
<text class="time-text">{{ selectedTime }}</text>
<uv-icon name="arrow-right" color="#90949D" size="28rpx" />
</view>
</view>
</view>
<!-- 弹出层 -->
<uv-popup ref="timeSelectRef" mode="bottom" @close="closePopup" round="10" safe-area-inset-bottom>
<view class="time-picker-container">
<view class="time-picker-header">
<text class="cancel-btn" @click="closePopup">{{ t('button.cancel') }}</text>
<!-- <text class="title-text">{{ title }}</text> -->
<text class="confirm-btn" @click="confirmSelect">{{ t("message.confirm") }}</text>
</view>
<view class="picker-view-wrapper">
<picker-view class="time-picker-view" :value="pickerValue" @change="onPickerChange" :indicator-style="indicatorStyle">
<picker-view-column>
<view class="picker-item" v-for="(hour, index) in hours" :key="'hour-'+index">
<text class="picker-text">{{ hour }}</text>
</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item" v-for="(minute, index) in minutes" :key="'minute-'+index">
<text class="picker-text">{{ minute }}</text>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</uv-popup>
</view>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue';
import { t } from '@/plugins/index.js';
// Props
const props = defineProps({
title: {
type: String,
default: ''
},
value: {
type: String,
default: '00:00'
},
start: {
type: String,
default: '00:00'
},
end: {
type: String,
default: '23:59'
}
});
// 处理初始值,支持 00:00:00 格式
// const formatInitialValue = (value) => {
// if (!value) return '00:00';
// // 如果是 00:00:00 格式,截取前5位
// if (value.length > 5) {
// return value.substring(0, 5);
// }
// return value;
// };
const emit = defineEmits(['change']);
// 弹出层控制
const timeSelectRef = ref(null);
const openPopup = () => {
timeSelectRef.value.open();
};
const closePopup = () => {
timeSelectRef.value.close();
};
// 时间数据
const hours = ref([]);
const minutes = ref([]);
const selectedTime = ref();
const pickerValue = ref([0, 0]);
const indicatorStyle = "height: 40px; border-top: 1px solid #f2f2f2; border-bottom: 1px solid #f2f2f2;";
// 初始化时间数据
onMounted(() => {
initTimeData();
initPickerValue();
});
// 初始化小时和分钟数据
const initTimeData = () => {
// 生成小时数据 (00-23)
for (let i = 0; i < 24; i++) {
hours.value.push(i.toString().padStart(2, '0'));
}
// 生成分钟数据 (00-59)
for (let i = 0; i < 60; i++) {
minutes.value.push(i.toString().padStart(2, '0'));
}
};
// 初始化选择器的值
const initPickerValue = () => {
if (selectedTime.value) {
// 处理可能的 00:00:00 格式,支持接口回显
const timeParts = selectedTime.value.split(':');
const hour = timeParts[0];
const minute = timeParts[1];
pickerValue.value = [parseInt(hour), parseInt(minute)];
}
};
watch(() => props.value, (newVal) => {
if (newVal) {
selectedTime.value = newVal;
console.log(selectedTime.value,'selectedTime.valu11e');
initPickerValue();
}
}, { immediate: true });
// 选择器变化事件
const onPickerChange = (e) => {
const values = e.detail.value;
pickerValue.value = values;
};
// 确认选择
const confirmSelect = () => {
const hour = hours.value[pickerValue.value[0]];
const minute = minutes.value[pickerValue.value[1]];
selectedTime.value = `${hour}:${minute}`;
console.log(selectedTime.value,'selectedTime.value');
emit('change', selectedTime.value);
closePopup();
};
// // 格式化时间显示,根据语言环境
const formatTimeByLocale = (timeString) => {
console.log(timeString,'timeString');
// const locale = uni.getLocale();
// // 如果是英文环境,转换为12小时制
// if (locale.startsWith('en')) {
// const [hours, minutes] = timeString.split(':');
// const hour = parseInt(hours, 10);
// const period = hour >= 12 ? 'PM' : 'AM';
// const hour12 = hour % 12 || 12; // 0点显示为12点
// return `${hour12}:${minutes} ${period}`;
// }
// 其他语言环境保持24小时制
return timeString;
};
</script>
<style lang="scss" scoped>
.time-select {
width: 100%;
}
.time-trigger {
width: 100%;
}
.setting-item {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 30rpx 0;
}
.setting-left {
display: flex;
flex-direction: column;
}
.setting-title {
font-size: 28rpx;
color: #262626;
font-weight: 500;
}
.setting-right {
display: flex;
flex-direction: row;
align-items: center;
}
.time-text {
font-size: 28rpx;
color: #90949d;
}
.arrow-right {
font-size: 36rpx;
color: #90949d;
margin-left: 10rpx;
}
.time-picker-container {
background-color: #ffffff;
padding-bottom: 20rpx;
}
.time-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1px solid #f2f2f2;
}
.cancel-btn {
color: #90949d;
font-size: 28rpx;
width: 80rpx;
text-align: left;
}
.title-text {
color: #262626;
font-size: 30rpx;
font-weight: 500;
flex: 1;
text-align: center;
}
.confirm-btn {
color: #387CF5;
font-size: 28rpx;
width: 80rpx;
text-align: right;
padding-right: 20rpx;
}
.picker-view-wrapper {
width: 100%;
height: 400rpx;
position: relative;
padding: 20rpx 0 0 0;
}
.time-picker-view {
width: 100%;
height: 400rpx;
}
// 必须用line-height 不然会出问题 真机上 配合indicatorStyle属性
.picker-item {
display: flex;
justify-content: center;
align-items: center;
line-height:80rpx;
}
.picker-text {
font-size: 32rpx;
color: #262626;
text-align: center;
}
.border-line {
display: flex;
justify-content: center;
align-items: center;
}
</style>
\ No newline at end of file
<template>
<view class="notice-tip" @tap="handleClick">
<view :class="{'icon-container': count > 0, 'icon-container-large-count': count > 99, 'icon-container-large-count-3': count > 9}">
<uv-icon :name="iconName" :color="iconColor" :size="iconSize" />
<view
v-if="count > 0"
class="badge"
:class="{ 'large-badge': count > 99, 'large-badge-2': count > 999 ,'large-badge-3': count > 9}"
>
<text class="badge-text">{{ displayCount }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { computed } from "vue";
// 定义 props
const props = defineProps({
// 图标名称
iconName: {
type: String,
default: "bell",
},
// 图标颜色
iconColor: {
type: String,
default: "#fff",
},
// 图标大小
iconSize: {
type: [String, Number],
default: "24",
},
// 通知数量
count: {
type: Number,
default: 0,
},
// 最大显示数量,超过显示 999+
maxCount: {
type: Number,
default: 999,
},
});
// 定义 emits
const emit = defineEmits(["click", "tap"]);
// 计算显示的数量
const displayCount = computed(() => {
if (props.count <= 0) return "";
if (props.count > props.maxCount) return `${props.maxCount}+`;
return props.count.toString();
});
// 处理点击事件
const handleClick = () => {
emit("click", props.count);
emit("tap", props.count);
};
</script>
<style lang="scss" scoped>
.notice-tip {
position: relative;
display: inline-block;
cursor: pointer;
}
.icon-container {
position: relative;
display: inline-block;
padding-top: 20rpx;
}
.icon-container-large-count-3 {
position: relative;
display: inline-block;
padding-top: 20rpx;
padding-right: 10rpx;
}
.icon-container-large-count {
position: relative;
display: inline-block;
padding-top: 20rpx;
padding-right: 20rpx;
}
.badge {
position: absolute;
top: 2rpx;
right: 0rpx;
display: flex;
align-items: center;
justify-content: center;
padding: 2rpx 6rpx 2rpx;
background: #ff6772;
border-radius: 12rpx;
box-sizing: border-box;
.badge-text {
font-weight: 400;
font-size: 20rpx;
color: #ffffff;
line-height: 28rpx;
text-align: center;
}
&.large-badge-3 {
min-width: 40rpx;
right: 0rpx;
.badge-text {
}
}
&.large-badge {
min-width: 40rpx;
right: 0rpx;
.badge-text {
font-size: 18rpx;
}
}
&.large-badge-2 {
min-width: 40rpx;
right: 0rpx;
// 缩小 避免溢出
.badge-text {
font-size: 16rpx;
}
}
}
</style>
{}
\ No newline at end of file
{
"project_info": {
"project_number": "1068829040777",
"project_id": "pmistore-406ac",
"storage_bucket": "pmistore-406ac.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:1068829040777:android:2d70521c8c183aac146658",
"android_client_info": {
"package_name": "com.viontech.app.pmistore"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBASaVwPlqwhCSRvsK6aUQey6dlR9_cunM"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
\ No newline at end of file
export * from './navHooks'
\ No newline at end of file
import {
ref
} from 'vue'
export const useNav = () => {
let navHeight = ref(uni.getSystemInfoSync().statusBarHeight || 0)
// #ifdef H5
navHeight.value += 0
// #endif
return {
navHeight
}
}
\ No newline at end of file
import { ref, computed } from "vue";
export const useSystemHeaderInfo = () => {
const miniCapsuleWidth = ref(16);
const capsuleLeftWidth = ref(0);
let statusBarHeight = 44;
let navHeight = 40;
const sysInfo = uni.getSystemInfoSync();
const { screenHeight, safeArea, screenWidth, windowHeight } = sysInfo;
// #ifdef APP-PLUS
statusBarHeight = sysInfo.statusBarHeight || 20;
navHeight = 40;
// #endif
// #ifdef MP
statusBarHeight = sysInfo.statusBarHeight;
const { top, height, left } = uni.getMenuButtonBoundingClientRect();
miniCapsuleWidth.value = screenWidth - left;
navHeight = height ? (top - statusBarHeight) * 2 + height : 40;
capsuleLeftWidth.value = left;
// #endif
const headerNavBarAllHeight = computed(() => {
return statusBarHeight + navHeight;
});
const contentHeight = computed(() => {
return windowHeight - navHeight - 42;
});
return {
statusBarHeight: statusBarHeight,
navHeight: navHeight,
headerNavBarAllHeight,
contentHeight,
miniCapsuleWidth: miniCapsuleWidth.value,
capsuleLeftWidth: capsuleLeftWidth.value,
};
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>
import App from './App'
import { i18n, initI18n } from './plugins/i18n/index.js'
initI18n('main')
import '@/styles/normal.scss'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
import * as Pinia from "pinia";
export function createApp() {
const app = createSSRApp(App)
app.use(Pinia.createPinia());
app.use(i18n)
return {
app,
Pinia,
};
}
// #endif
{
"requires": true,
"lockfileVersion": 1,
"dependencies": {
"@intlify/core-base": {
"version": "11.1.3",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-11.1.3.tgz",
"integrity": "sha512-cMuHunYO7LE80azTitcvEbs1KJmtd6g7I5pxlApV3Jo547zdO3h31/0uXpqHc+Y3RKt1wo2y68RGSx77Z1klyA==",
"requires": {
"@intlify/message-compiler": "11.1.3",
"@intlify/shared": "11.1.3"
}
},
"@intlify/message-compiler": {
"version": "11.1.3",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-11.1.3.tgz",
"integrity": "sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw==",
"requires": {
"@intlify/shared": "11.1.3",
"source-map-js": "^1.0.2"
}
},
"@intlify/shared": {
"version": "11.1.3",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-11.1.3.tgz",
"integrity": "sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA=="
},
"@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="
},
"dayjs": {
"version": "1.11.18",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz",
"integrity": "sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA=="
},
"echarts": {
"version": "5.6.0",
"requires": {
"tslib": "2.3.0",
"zrender": "5.6.1"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
},
"pinyin-pro": {
"version": "3.26.0"
},
"source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="
},
"tslib": {
"version": "2.8.1"
},
"vue-i18n": {
"version": "11.1.3",
"requires": {
"@intlify/core-base": "11.1.3",
"@intlify/shared": "11.1.3",
"@vue/devtools-api": "^6.5.0"
}
},
"zrender": {
"version": "5.6.1",
"requires": {
"tslib": "2.3.0"
},
"dependencies": {
"tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
}
}
}
}
}
{
"dependencies": {
"dayjs": "^1.11.18",
"echarts": "^5.6.0",
"pinyin-pro": "^3.26.0",
"tslib": "^2.8.1",
"vue-i18n": "^11.1.3",
"zrender": "^5.6.1"
}
}
This diff is collapsed. Click to expand it.
<template>
<view class="fixed-date">
<DatePicker @change="handleDateChange" :has-today="false"></DatePicker>
</view>
<view class="h-page">
<view class="mall" style="height: 0;overflow: hidden;">
<MallSelectNvue has-store="1" choose-group="0" @change="handlStoreChange" />
</view>
<AreaStatisticsComp ref="areaStatisticRef" />
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed,
nextTick
} from 'vue'
import {
getUserIndicatorIndexApi,
getAreaAnalysisStatisApi
} from '@/api'
import DatePicker from '@/components/DatePicker.nvue'
import PageOptions from '@/components/PageOptions.vue'
import CustomNavBar from '@/components/CustomNav.nvue'
import MallSelectNvue from '@/components/MallSelect.nvue';
import IndicatorsFilter from '@/components/IndicatorsFilter.nvue'
import AreaStatisticsComp from '@/subPackages/accountGroup/pages/store-analysis/area-statistics/components/AreaStatisticsComp.vue'
import {
getStageObj
} from '../../utils'
const params = ref({})
const flag = ref(false) // 是否初始化完成
const handlStoreChange = (e) => {
params.value = {
...params.value,
...e
}
if(!params.value.storeId){
const store = getStageObj('store')
params.value.storeId = store.id
nextTick(()=>{
areaStatisticRef.value?.initData(params.value)
})
}
}
const handleDateChange = (e) => {
const [startDate, endDate] = e.range
params.value = {
...params.value,
startDate,
endDate,
...e
}
// 日期更改时直接加载数据
if (!flag.value) {
nextTick(()=>{
areaStatisticRef.value?.initData(params.value)
})
}
}
// 组件ref
const areaStatisticRef = ref(null)
</script>
<style lang="scss" scoped>
.fixed-date {
width: 750rpx;
height: 100rpx;
background-color: #f2f3f6;
position: sticky;
left: 0;
top: 0;
padding: 20rpx;
z-index: 100;
}
.h-page {
padding: 0 20rpx;
}
</style>
<template>
<view class="ios-heatmap">
<view class="date-picker">
<DatePicker @change="handleDateChange" :has-today="false" @modalChange="handleModalChange">
<template #filter>
<view class="filter" @tap="handleSelectFilterItem">
<uv-text :text="indicatorText" align="center" size="26rpx" color="#202328" :lines="1"></uv-text>
<uv-icon name="arrow-down-fill" class="filter_icon" size="10rpx" color="#202328" />
</view>
</template>
</DatePicker>
</view>
<view class="mall" style="height: 0;overflow: hidden;">
<MallSelectNvue has-store="1" choose-group="0" @change="handlStoreChange" />
</view>
<IndicatorsFilter ref="filterRef" :options="indicatorsList" @change="handleIndicatorChange"
@modalChange="handleModalChange" />
</view>
</template>
<script setup>
import {
ref,
watch
} from 'vue'
import {
getStageObj,
rpx2Px
} from '@/utils'
import DatePicker from '@/components/DatePicker.nvue'
import MallSelectNvue from '@/components/MallSelect.nvue';
import IndicatorsFilter from '@/components/IndicatorsFilter.nvue'
import {
onHide,
onShow
} from '@dcloudio/uni-app'
import {
t
} from '@/plugins/index.js'
const props = defineProps({
index: {
type: Number,
default: 0
}
})
watch(() => props.index, (newVal) => {
if (newVal !== 1) {
if (webview.value) {
webview.value?.hide()
}
}else {
webview.value?.show()
}
})
const params = ref({})
const flag = ref(false) // 是否初始化完成
const indicatorsList = [{
name: t('table.residenceTime'),
key: 'rt'
}, {
name: t('PreMallIndex.CustomerMantime'),
key: 'tc'
}]
const indicatorText = ref(t('table.residenceTime'))
const indicatorKey = ref('rt')
// storeChange
const handlStoreChange = (e) => {
params.value = {
...params.value,
...e
}
if (!params.value.storeId) {
const store = getStageObj('store')
params.value.storeId = store.id
}
}
let showHideFlag = ref(false)
onHide(() => {
if (webview.value && props.index === 1) {
plus.webview.close(webview.value)
webview.value = null
showHideFlag.value = true
}else{
showHideFlag.value = false
}
})
onShow(() => {
if (showHideFlag.value) {
initHeatMapApp()
}
})
// pickerChange
const handleDateChange = (e) => {
const [startDate, endDate] = e.range
params.value = {
...params.value,
startDate,
endDate,
...e
}
// 日期更改时直接加载数据
if (!flag.value) {
initHeatMapApp()
}
}
const filterRef = ref(null)
const handleSelectFilterItem = () => {
filterRef.value?.open()
}
const handleIndicatorChange = (e) => {
indicatorText.value = e.name
indicatorKey.value = e.key
// 指标更改时直接加载数据
if (!flag.value) {
initHeatMapApp()
}
}
// webview信息 仅ios
const webview = ref(null)
const handleModalChange = (val) => {
val ? webview.value?.hide() : webview.value?.show()
}
const initHeatMapApp = () => {
console.log(params.value);
if (webview.value) {
plus.webview.close(webview.value)
webview.value = null
}
const {
startDate,
endDate,
indicatorKey: indicatorKeys
} = params.value
const token = uni.getStorageSync('Authorization')
const store = getStageObj('store')
const storeId = params.value.storeId || store.id
const statusBarHeight = plus.navigator.getStatusbarHeight()
const top = statusBarHeight + 88 + rpx2Px(100)
// const systemInfo = uni.getSystemInfoSync()
// const tabBarHeight = systemInfo.safeArea.bottom + 50
console.log(
`https://store.keliuyun.com/apph5/heatMap?token=${token}&mallId=${storeId}&startDate=${startDate}&endDate=${endDate}&level=${indicatorKeys||'rt'}`
);
webview.value = plus.webview.create(
`https://store.keliuyun.com/apph5/heatMap?token=${token}&mallId=${storeId}&startDate=${startDate}&endDate=${endDate}&level=${indicatorKeys||'rt'}`,
"store-heatmap", {
top: `${top}px`, // 关键:从导航栏下方开始
bottom: "50px",
left: "0px",
right: "0px",
'uni-app': 'none',
popGesture: 'close',
// backButtonAutoControl:'close',
hardwareAccelerated: true,
wkwebview: true,
zindex: 0
}
)
webview.value.show()
}
</script>
<style scoped>
.date-picker {
padding: 20rpx;
}
.filter {
background-color: #fff;
width: 160rpx;
height: 60rpx;
border-radius: 12rpx;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
margin-left: 16rpx;
padding: 0 10rpx;
}
</style>
\ No newline at end of file
<template>
<view style="width:750rpx; height:750rpx"><l-echart ref="chartRef" :custom-style="`width:750rpx;height:200px`"></l-echart></view>
</template>
<script setup>
import {
onMounted,
ref
} from 'vue';
// #ifdef MP-WEIXIN
const echarts = require('../../uni_modules/lime-echart/static/echarts.min');
// #endif
// #ifndef MP-WEIXIN
import * as echarts from 'echarts'
// #endif
const chartRef = ref(null)
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
confine: true
},
legend: {
data: ['热度', '正面', '负面']
},
grid: {
left: 20,
right: 20,
bottom: 15,
top: 40,
containLabel: true
},
xAxis: [{
type: 'value',
axisLine: {
lineStyle: {
color: '#999999'
}
},
axisLabel: {
color: '#666666'
}
}],
yAxis: [{
type: 'category',
axisTick: {
show: false
},
data: ['汽车之家', '今日头条', '百度贴吧', '一点资讯', '微信', '微博', '知乎'],
axisLine: {
lineStyle: {
color: '#999999'
}
},
axisLabel: {
color: '#666666'
}
}],
series: [{
name: '热度',
type: 'bar',
label: {
normal: {
show: true,
position: 'inside'
}
},
data: [300, 270, 340, 344, 300, 320, 310],
},
{
name: '正面',
type: 'bar',
stack: '总量',
label: {
normal: {
show: true
}
},
data: [120, 102, 141, 174, 190, 250, 220]
},
{
name: '负面',
type: 'bar',
stack: '总量',
label: {
normal: {
show: true,
position: 'left'
}
},
data: [-20, -32, -21, -34, -90, -130, -110]
}
]
};
onMounted(() => {
// 组件能被调用必须是组件的节点已经被渲染到页面上
setTimeout(async () => {
if (!chartRef.value) return
const myChart = await chartRef.value.init(echarts)
myChart.setOption(option)
}, 300)
})
</script>
<style>
</style>
\ No newline at end of file
<template>
<view class="c-list">
<view class="c-l-item" v-for="(item,index) in cardData" :key="item.key">
<view class="c-list_item_title">
<text class="c-list_item_title_text">{{ item.name }}</text>
</view>
<view class="c-list_item_value">
<text class="c-list_item_value_text">{{formatValue(item)}}</text>
</view>
<view class="c-list_item_other" v-if="average_key.includes(item.key)">
<text class="c-list_item_other_text">{{ t('navData.averexposure') }}: {{item.average}}</text>
</view>
</view>
</view>
</template>
<script setup>
import {
formatSeconds,
getStageObj
} from '@/utils'
import {
t
} from '@/plugins/index.js'
const props = defineProps({
cardData: {
type: Array,
default: () => []
}
})
// 需要展示店均的key列表
// 依次: 集团总客流人数、集团总客流、过店人次、批次客流
const average_key = ['account_passenger_number', 'account_passenger_flow', 'mall_customer_group',
'mall_outside_number'
]
// 需要格式化时间key列表
// 依次:顾客停留时间 顾客滞留时间
const time_key = ['custom_residence_time', 'custom_residence_time']
// 需要合并展示的值
// 依次:成人&儿童
const conbine_key = {
'mall_customer_adult_child': ['adult', 'child']
}
const formatValue = (item) => {
// 格式化时间
if (time_key.includes(item.key)) {
return formatSeconds(item.value)
}
// 格式化合并项
if (conbine_key[item.key]) {
return conbine_key[item.key].map(elKey => item[elKey]).join('/')
}
if (item.unit === '%') {
return `${item.value}%`
}
if (item.value > 1000000) {
return (item.value / 10000).toFixed(2) + 'w'
}
return item.value
}
</script>
<style lang="scss">
.c-list {
padding: 28rpx;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
.c-l-item {
width: 320rpx;
height: 194rpx;
padding: 24rpx 0 0 24rpx;
border-radius: 8rpx;
border: 2rpx solid #EDEFF3;
margin-bottom: 14rpx;
display: flex;
flex-direction: column;
.c-list_item_title {
.c-list_item_title_text {
font-size: 26rpx;
color: #656A72;
}
}
.c-list_item_value {
margin: 14rpx 0;
&_text {
color: #202328;
font-family: D-DIN-PRO-700-Bold;
font-size: 44rpx;
}
}
.c-list_item_other {
&_text {
font-size: 24rpx;
color: #90949D;
}
}
}
// &_item {
// /* 三列布局核心计算 */
// height: 160rpx;
// /* 以下保持原有样式 */
// align-items: center;
// &_title {
// flex-direction: row;
// &_text {
// font-size: 26rpx;
// color: #202328;
// }
// &_icon {}
// }
// &_value {
// width: 100%;
// margin: 8rpx 0;
// &_text {
// /* #ifdef APP */
// lines: 1;
// /* #endif */
// /* #ifdef H5 || MP-WEIXIN */
// overflow: hidden;
// text-overflow: ellipsis;
// white-space: nowrap;
// /* #endif */
// font-weight: bold;
// text-align: center;
// font-size: 36rpx;
// color: #202328;
// }
// }
// &_other {
// &_text {
// font-size: 20rpx;
// color: #6d778f;
// flex-direction: row;
// }
// }
// }
}
</style>
<template>
<CardNvue :title="t('PreMenu.regionalStatistics')" :auto-height="true">
<template #content>
<view class="table">
<view class="table-head">
<view class="table-head-item">
<text class="table-head-item-text">{{ t('table.areaName') }}</text>
</view>
<view class="table-head-item">
<text class="table-head-item-text">{{ t('PreMallIndex.CustomerNum') }}</text>
</view>
<view class="table-head-item">
<text class="table-head-item-text">{{ t('PreGateIndex.attentionVisitors') }}</text>
</view>
<view class="table-head-item last-table">
<text class="table-head-item-text">{{ t('PreGateIndex.avgValidDwellTime') }}</text>
</view>
</view>
<view class="table-body" v-if="tableList.length >0">
<view class="table-body-item" v-for="(item,index) in tableList" :key="index">
<view class="table-body-item-value table-body-item-name">
<text class="table-body-item-value-text" lines="1">{{item.gateName}}</text>
</view>
<view class="table-body-item-value">
<text class="table-body-item-value-text">{{item.customerCount}}</text>
</view>
<view class="table-body-item-value">
<text class="table-body-item-value-text">{{item.attentionVisitors}}</text>
</view>
<view class="table-body-item-value">
<text class="table-body-item-value-text">{{formatSeconds(item.avgValidDwellTime)}}</text>
</view>
</view>
</view>
<view class="p-empty" v-else style="margin-top: 40rpx;">
<image class="p-empty-img" src="/static/common/empty.png" mode=""></image>
<text class="p-empty-text chart-text">{{ t('maintenance.monitor.project.empty.text') }}</text>
</view>
</view>
</template>
</CardNvue>
</template>
<script setup>
import {
ref,
onMounted
} from 'vue'
import CardNvue from './Card.nvue';
import {
getGateStatisticsApi
} from '@/api'
import {
formatSeconds
} from '@/utils'
import {
t
} from '@/plugins/index.js'
const tableList = ref([])
onMounted(() => {
// initTableList()
})
const options = ref({})
const initData = (params)=>{
options.value = params
initTableList()
}
defineExpose({
initData
})
const initTableList = async () => {
try {
const params = {
accountIds: options.value.accountId,
endDate: options.value.endDate,
startDate: options.value.startDate,
mallId: options.value.storeId
}
const {
data
} = await getGateStatisticsApi(params)
tableList.value = data
} catch (e) {
console.log(e)
}
}
</script>
<style lang="scss">
@import '../../../styles/table.scss'
</style>
<template>
<view>
<CardNvue :title="t('asis.BasicAnalysis')" :auto-height="true" card-padding="28rpx 0 0" :titleStyle="{padding:'0 28rpx'}">
<template #filter>
<view class="filter filter-border transparent-bg" @tap="handleTapMore" v-if="showMore">
<text class="filter_text">{{ t('app.common.more') }}</text>
<uv-icon name="arrow-right" color="#90949D" size="24rpx" />
</view>
</template>
<template #content>
<AccountBasicData v-if="!options.storeId" :card-data="accountCardComputedList" />
<StoreBasicData v-else :card-data="getStoreCards" :card-data-map="cardDataMap" />
</template>
</CardNvue>
<uv-popup ref="morePopupRef" class="pop_more" round="16rpx">
<view style="width: 710rpx;">
<view class="pop_header" style="width: 100%;">
<view style="width: 22px;"></view>
<text class="pop_header_text">{{ t('asis.BasicAnalysis') }}</text>
<uv-icon name="close" size="22" class="pop_header_icon" @tap="handleCloseMore"></uv-icon>
</view>
<view>
<scroll-view scroll-y="true" style="height: 640rpx;">
<AccountBasicData v-if="!options.storeId" :card-data="accountCardList" />
<StoreBasicData v-else :card-data="storeCards" :card-data-map="cardDataMap" />
</scroll-view>
</view>
</view>
</uv-popup>
</view>
</template>
<script setup>
import {
ref,
onMounted,
computed,
nextTick
} from 'vue';
import {
getAccountCardListApi,
getStoreBasicDataApi
} from '@/api'
import {
formatSeconds,
getStageObj
} from '@/utils'
import CardNvue from './Card.nvue';
import AccountBasicData from './AccountBasicData.nvue'
import StoreBasicData from './StoreBasicData.nvue'
import {
t
} from '@/plugins/index.js'
const props = defineProps({
// 集团卡片数据
storeCards: {
type: Array,
default: () => []
}
})
/********************* 更多 ************************/
const morePopupRef = ref(null)
const handleTapMore = () => {
morePopupRef.value?.open()
}
const handleCloseMore = ()=>{
morePopupRef.value?.close()
}
const showMore = computed(() => {
const isAccount = !options.value.storeId
return (isAccount && accountCardList.value.length > 6) || (!isAccount && props.storeCards.length > 6)
})
const getStoreCards = computed(() => {
return props.storeCards.slice(0, 6) || []
})
const storeChartIds = ref([])
const initStoreCardData = () => {
nextTick(() => {
storeChartIds.value = props.storeCards.map(el => el.id)
getCardDataMap()
})
}
// 获取门店卡片数据·
const cardDataMap = ref({})
const getCardDataMap = async () => {
try {
const {
data
} = await getStoreBasicDataApi({
orgType: 'mall',
mallIds: options.value.storeId,
dateType: props.unit,
orgIds: options.value.storeId,
startDate: options.value.startDate,
endDate: options.value.endDate,
chartIds: storeChartIds.value.join(',')
})
cardDataMap.value = data || {}
} catch (e) {
console.log(e);
}
}
// 获取集团卡片数据
const options = ref({})
const accountId = ref('')
const initAccountCardData = (params) => {
nextTick(() => {
accountId.value = getStageObj('account').id
options.value = params
getAccountCardList()
})
}
// 获取集团卡片列表
const accountCardList = ref([])
const accountCardComputedList = computed(() => {
return accountCardList.value?.slice(0, 4) || []
})
const getAccountCardList = async () => {
try {
const params = {
accountId: accountId.value,
groupId: options.value.groupId ? options.value.groupId : -1,
endDate: options.value.endDate,
startDate: options.value.startDate
}
// 测试参数
// const params = {
// accountId: 382,
// groupId: -1,
// endDate: '2025-05-21',
// startDate: '2025-05-01'
// }
const {
data
} = await getAccountCardListApi(params)
accountCardList.value = data || []
} catch (e) {
console.log(res)
}
}
defineExpose({
initAccountCardData,
initStoreCardData
})
</script>
<style lang="scss" scoped>
@import '@/styles/normal.scss';
.filter {
flex-direction: row;
align-items: center;
.filter_text {
font-size: 24rpx;
color: #6D778F;
margin-right: 10rpx;
}
}
</style>
<template>
<view class="h-card" :style="{height:autoHeight?'':props.height,padding:cardPadding}">
<view class="h-card_title" v-if="props.title" :style="titleStyle">
<!-- // <text class="text">{{ props.title }}</text> -->
<uv-text bold :text="props.title" align="left" size="32rpx" color="#202328" :lines="1"></uv-text>
<image v-if="titleIcon" src="@/static/tabbar/h-title_bg.png" class="img" mode=""></image>
<slot name="filter" />
</view>
<view class="h-card_content" :style="autoHeight?'':'flex:1'">
<slot name="content" />
</view>
</view>
</template>
<script setup>
const props = defineProps({
title: {
type: String,
default: ''
},
cardPadding:{
type:String,
default: '28rpx'
},
autoHeight: {
type: Boolean,
default: false
},
height: {
type: String,
default: '506rpx'
},
titleIcon: {
type: Boolean,
default: false
},
titleStyle:{
type: Object,
default: () => ({
paddingLeft:0
})
}
})
</script>
<style lang="scss" scoped>
.h-card {
width: 710rpx;
background: #FFFFFF;
border-radius: 16rpx;
overflow: hidden;
display: flex;
flex-direction: column;
&_title {
display: flex;
flex-direction: column;
position: relative;
flex-direction: row;
justify-content: space-between;
.text {
color: #202328;
font-size: 32rpx;
font-family: PingFang_Bold;
line-height: 44rpx;
font-weight: bold;
flex:1;
}
.img {
width: 75px;
height: 10px;
position: absolute;
bottom: 6rpx;
left: 14rpx;
}
}
&_content{
min-height: 0;
}
}
</style>
<template>
<view>
<CardNvue :title="t('app.title.genderStatistics')" style="margin-bottom: 20rpx;">
<template #content>
<ChartsNvue ref="genderRef" />
</template>
</CardNvue>
<!-- #ifdef MP-WEIXIN -->
<CardNvue :title="t('app.title.ageStatistics')">
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<CardNvue :title="t('app.title.ageStatistics')" style="padding:24rpx 24rpx 0">
<!-- #endif -->
<template #content>
<ChartsNvue v-if="renderFlag === '1'" ref="ageRef" />
<view class="p-empty" v-if="renderFlag === '0'" style="margin-top: 40rpx;">
<image class="p-empty-img" src="/static/common/empty.png" mode=""></image>
<text class="p-empty-text chart-text">{{ t('maintenance.monitor.project.empty.text') }}</text>
</view>
</template>
</CardNvue>
</view>
</template>
<script setup>
import {
getFaceAgeApi,
getFaceGenderApi,
getFaceAnalyzeStaMallApi
} from '@/api'
import CardNvue from './Card.nvue'
import ChartsNvue from '@/components/Charts.nvue'
import {
nextTick,
onMounted,
ref
} from 'vue'
import {
rpx2Px,
getStageObj
} from '@/utils'
import {
t
} from '@/plugins/index.js'
const renderFlag = ref('')
const options = ref({})
const initData = async (params) => {
options.value = params
if (!params.storeId) {
// 集团图表
await getAcountChartData()
} else {
// 门店图表
await getStoreChartData()
}
}
const getStoreChartData = async () => {
try {
const account = await getStageObj('account')
const params = {
startDate: options.value.startDate,
endDate: options.value.endDate,
orgIds: options.value.storeId,
chartIds: "295,296"
}
// 性别
const {
data
} = await getFaceAnalyzeStaMallApi(params)
const {
faceAge,
faceGender
} = data.body
// 渲染年龄图表
renderAgeCharts(faceAge)
// 渲染性别图表
renderGenderCharts(faceGender)
} catch (e) {
console.log(e);
}
}
const getAcountChartData = async () => {
try {
const account = await getStageObj('account')
// 参数设置位置:
const params = {
startDate: options.value.startDate,
endDate: options.value.endDate,
accountId: account.id,
groupId: options.value.groupId ? options.value.groupId : -1,
}
// 性别
const {
data: faceGender
} = await getFaceGenderApi(params)
// 渲染性别图表
renderGenderCharts(faceGender)
// 年龄
const {
data: faceAge
} = await getFaceAgeApi(params)
// 渲染年龄图表
renderAgeCharts(faceAge)
} catch (e) {
console.log(e);
}
}
/* ********* 性别统计 ********** */
const genderRef = ref(null)
const renderGenderCharts = (val = {
series: [{
data: []
}]
}) => {
let bgColor = '#fff';
let echartData = val.series?.[0]?.data;
let formatPercent = function(num, total) {
return (num * 100 / total).toFixed(2) + '%';
}
let total = echartData?.reduce((a, b) => {
return a + b.value * 1
}, 0) || 0
const option = {
title: {
text: t('table.totalNum'),
textStyle: {
color: '#6D778F',
fontSize: rpx2Px(26),
},
left: 'center',
top: '32%',
subtext: `${total}`,
subtextStyle: {
fontSize: rpx2Px(36),
color: '#1D2129',
fontWeight: 'bold'
}
},
color: ['#387CF5', '#FFA620'],
tooltip: {
trigger: 'item',
confine: true,
formatter: `{b}: {d}%`
},
legend: {
show: true, // 显示图例
selectedMode: true, // 启用切换功能
data: echartData.map(v => v.name), // 动态绑定数据名称
bottom: 0, // 图例置于底部
orient: 'horizontal', // 水平排列
itemWidth: 12, // 图例标记宽度
itemHeight: 12, // 图例标记高度
itemGap: 30,
textStyle: {
color: '#666', // 文字颜色
fontSize: 12
}
},
series: [{
type: 'pie',
radius: ['50%', '80%'], // 保持与原系列相同半径
center: ['50%', '42%'], // 保持中心点一致
data: [{
value: 100, // 值需≥原系列数据总和
itemStyle: {
color: '#f3f5f9' // 设置背景色
}
}],
silent: true,
hoverAnimation: false, // 禁用悬停动画
label: {
show: false
}, // 隐藏标签
labelLine: {
show: false
}, // 隐藏标签引导线
tooltip: {
show: false
} // 禁用提示框
}, {
type: 'pie',
radius: ['55%', '75%'],
center: ['50%', '42%'],
data: echartData,
hoverAnimation: false,
label: {
show: true,
formatter: `{b}\n{d}%`
},
itemStyle: {
borderColor: '#F3F5F9',
borderWidth: 3
},
labelLine: {
show: true,
length: rpx2Px(20),
length2: rpx2Px(20),
smooth: true
},
tooltip: {
show: false
},
}]
};
genderRef.value?.initCharts(option)
}
/* ********* 年龄统计 ********** */
const ageRef = ref(null)
const renderAgeCharts = async (val) => {
if (val.series.length > 0) {
renderFlag.value = '1'
} else {
renderFlag.value = '0'
return
}
const option = rightCenterBarConfig(val, '', 15)
// #ifndef MP-WEIXIN
nextTick(() => {
setTimeout(() => {
ageRef.value?.initCharts(option)
}, 0)
})
// #endif
// #ifdef MP-WEIXIN
let attempts = 0;
while (!ageRef.value && attempts < 40) {
await new Promise(resolve => setTimeout(resolve, 50));
attempts++;
}
if (ageRef.value?.initCharts) {
ageRef.value.initCharts(option);
} else {
console.warn('年龄图表组件未加载完成', ageRef.value);
}
// #endif
}
function rightCenterBarConfig(confineData, nameType, barWidth) {
let x = confineData?.series.map(item => item.name) || [];
let male = confineData?.series.map(item => item.data[0] || 0) || [];
let female = confineData?.series.map(item => item.data[1] || 0) || [];
const percentData = []
const totalData = confineData?.series.reduce((sum, curr) => {
let currTotal = curr.data.reduce((currSum, item) => currSum + item, 0)
percentData.push(currTotal)
return sum + currTotal
}, 0) || 0
return {
tooltip: {
trigger: "axis",
textStyle: {},
confine: true,
triggerOn: 'click'
},
grid: {
bottom: '0%',
left: '0%',
right: '0%',
top: '6%',
},
xAxis: {
type: "value",
splitLine: {
show: false,
},
axisTick: {
show: false
},
axisLine: {
show: false
},
axisLabel: {
show: false,
}
},
yAxis: [{
type: "category",
data: x,
axisTick: {
show: false
},
axisPointer: {
type: "shadow",
},
axisLine: {
show: false,
lineStyle: {
color: "#BDD8FB",
fontSize: 12
}
},
axisLabel: {
fontSize: 13,
textStyle: {
color: "#202328",
align: "left",
verticalAlign: 'top',
padding: [-25, 0, 0, 10]
}
}
}, {
type: 'category',
inverse: true,
axisTick: 'none',
axisLine: 'none',
show: true,
offset: -13,
axisLabel: {
textStyle: {
color: '#6D778F',
fontSize: 13,
align: 'right'
},
verticalAlign: 'bottom',
padding: [20, 0, 13, 0],
// #ifdef APP-NVUE
formatter: `function(value){
if(${totalData} === 0){
return '0%'
}
return (value * 100 / ${totalData}).toFixed(2) + '%';
}`,
// #endif
// #ifndef APP-NVUE
formatter: function(value) {
if (totalData === 0) {
return '0%'
}
return `${(value*100/totalData).toFixed(2)}%`;
},
// #endif
},
data: percentData.reverse(),
}],
series: [{
name: t('ParamName.maleAgeDistribution'),
type: "bar",
stack: "all",
data: male,
barWidth: 16,
itemStyle: {
color: "#4277F7",
borderRadius: [3, 0, 0, 3]
},
},
{
name: t('ParamName.femaleAgeDistribution'),
type: "bar",
stack: "all",
data: female,
barWidth: 16,
itemStyle: {
color: "#FFA620",
borderRadius: [0, 3, 3, 0]
},
showBackground: true,
backgroundStyle: {
color: '#F7F8FB',
borderRadius: [3, 3, 3, 3]
}
}
]
};
};
defineExpose({
initData
})
</script>
\ No newline at end of file
<template>
<CardNvue :title="t('navData.flowConversion')">
<template #content>
<view class="trans" v-if="transData.length >0">
<image src="/static/common/trans-bg.png" style="width: 664rpx;height: 378rpx;" mode=""></image>
<view class="num">
<view class="num-item" v-for="item in transData" :key="item.id">
<text class="title">{{item.name}}</text>
<text class="value">{{item.value}}</text>
</view>
</view>
<view class="trans-item trans1">
<text class="trans_text">{{ t('table.conversionRate') }}: {{getTrans(0)}}</text>
</view>
<view class="trans-item trans2">
<text class="trans_text">{{ t('table.conversionRate') }}: {{getTrans(1)}}</text>
</view>
<view class="trans-item trans3">
<text class="trans_text">{{ t('table.conversionRate') }}: {{getTrans(2)}}</text>
</view>
</view>
<!-- <ChartsNvue ref="chartsRef" /> -->
</template>
</CardNvue>
</template>
<script setup>
import {
getTrafficConversionMallApi
} from '@/api'
import CardNvue from './Card.nvue'
import ChartsNvue from '@/components/Charts.nvue'
import {
onMounted,
ref,
nextTick
} from 'vue'
import {
rpx2Px
} from '@/utils'
import {
t
} from '@/plugins/index.js'
const chartsRef = ref(null)
// 初始化数据
const options = ref({})
const initData = async (params) => {
options.value = params
await nextTick(() => {
setTimeout(() => {
getTrendChartData()
}, 0)
})
}
// 获取转化率
const getTrans = (index) => {
const before = transData.value[index]?.value
const after = transData.value[index + 1]?.value
if (transData.length === 0 || !before || !after) {
return '0%'
}
const valPercent = after / before * 100
if(Number.isNaN(valPercent)){
return '--'
}
return valPercent.toFixed(2) + '%'
}
defineExpose({
initData
})
const transData = ref([])
const getTrendChartData = async () => {
try {
// 参数设置位置:
const optionParams = {
id: options.value.storeId,
startDate: options.value.startDate,
endDate: options.value.endDate,
}
const {
data
} = await getTrafficConversionMallApi(optionParams)
transData.value = data || []
// renderChartData(data)
} catch (e) {
console.log(e);
}
}
</script>
<style lang="scss" scoped>
.trans {
width: 664rpx;
height: 378rpx;
margin-top: 28rpx;
position: relative;
overflow: hidden;
.num {
position: absolute;
top: 0;
left: 0;
width: 400rpx;
height: 378rpx;
padding: 4rpx 0;
.num-item {
height: 76rpx;
margin-bottom: 20rpx;
display: flex;
flex-direction: row;
align-items: center;
.title,
.value {
font-size: 26rpx;
color: #202328;
}
.title {
padding: 0 20rpx 0 28rpx;
}
}
}
.trans-item{
position: absolute;
.trans_text{
font-size: 24rpx;
color: #202328;
}
}
.trans1{
top: 60rpx;
left: 450rpx;
}
.trans2{
top: 168rpx;
left: 430rpx;
}
.trans3{
top: 274rpx;
left: 400rpx;
}
}
</style>
<template>
<CardNvue :title="t('PreMallChart.mall_visitors_flow_direction.sankey')" :auto-height="true">
<template #content>
<ChartsNvue v-if="renderFlag === '1'" height="560rpx" ref="flowDirectRef" />
<view class="p-empty" v-if="renderFlag === '0'" style="margin-top: 40rpx;">
<image class="p-empty-img" src="/static/common/empty.png" mode=""></image>
<text class="p-empty-text chart-text">{{ t('maintenance.monitor.project.empty.text') }}</text>
</view>
</template>
</CardNvue>
</template>
<script setup>
import {
nextTick,
onMounted,
ref
} from 'vue'
import {
getGateFlowDirectionNewApi
} from '@/api'
import CardNvue from './Card.nvue'
import ChartsNvue from '@/components/Charts.nvue'
import {
t
} from '@/plugins/index.js'
const renderFlag = ref('')
const flowDirectRef = ref(null)
onMounted(() => {
// getTrendChartData()
})
const options = ref({})
const initData = (params) => {
options.value = params
nextTick(() => {
getTrendChartData()
})
}
defineExpose({
initData
})
const getTrendChartData = async () => {
try {
// 参数设置位置:
const params = {
accountIds: options.value.accountId,
endDate: options.value.endDate,
startDate: options.value.startDate,
mallId: options.value.storeId
}
const {
data
} = await getGateFlowDirectionNewApi(params)
if (data.length > 0) {
renderFlag.value = '1'
} else {
renderFlag.value = '0'
}
const option = {
tooltip: {
trigger: "item",
confine: true,
triggerOn: 'click'
},
color: [
"#e35241",
"#d83965",
"#f39c38",
"#9036aa",
"#6041b0",
"#4054af",
"#4396ec",
"#4fb9d1",
"#3f9488",
"#97c05c",
"#154bee",
],
animation: false,
series: [{
left: 0,
right: 0,
top: 10,
bottom: 5,
type: "sankey",
focusNodeAdjacency: "allEdges",
nodeAlign: "right",
label: {
position: 'right',
},
data: [],
links: [],
lineStyle: {
color: "source",
curveness: 0.5,
},
}],
}
const idataSet = new Set()
const links = []
data.forEach((item) => {
idataSet.add(item.source)
idataSet.add(item.target + ' ') // 增加空格,保证echarts不报错
links.push({
...item,
target: item.target + ' ',
})
});
option.series[0].data = [...idataSet].map(n => {
const lastStr = n[n.length - 1]
return {
name: n,
label: {
position: lastStr === ' ' ? 'left' : 'right'
}
}
})
option.series[0].links = links;
// #ifndef MP-WEIXIN
nextTick(() => {
setTimeout(() => {
flowDirectRef.value?.initCharts(option)
}, 0)
})
// #endif
// #ifdef MP-WEIXIN
let attempts = 0;
while (!flowDirectRef.value && attempts < 40) {
await new Promise(resolve => setTimeout(resolve, 50));
attempts++;
}
if (flowDirectRef.value?.initCharts) {
flowDirectRef.value?.initCharts(option);
} else {
console.warn('分区流向图表组件未加载完成', flowDirectRef.value);
}
// #endif
} catch (e) {
console.log(e);
}
}
</script>
<style>
</style>
<template>
<view style="margin-bottom: 20rpx;">
<CardNvue :title="t('PreMallChart.mall_period_passenger_flow_heat.table')">
<template #filter>
<view class="filter filter-border transparent-bg" @tap="handleTapToChoose">
<text class="filter_text">{{ selectText || t('maintenance.common.select') }}</text>
<uv-icon name="arrow-right" color="#90949D" size="24rpx" />
</view>
</template>
<template #content>
<ChartsNvue ref="chartsRef" />
</template>
</CardNvue>
<uv-popup ref="typePopRef">
<view class="pop_header">
<view style="width: 22px;"></view>
<text class="pop_header_text">{{ t('app.title.kpiSelect') }}</text>
<uv-icon name="close" size="22" class="pop_header_icon" @tap="handleCloseTypeSelect"></uv-icon>
</view>
<view class="l_type">
<view class="l_type_item" v-for="(item,index) in getIndicator" :key="item.indexKey"
:class="{'l_type_item_active':index === checkIndex}" @tap="handleChangeCheckType(index)">
<text class="l_type_item_text"
:class="{'l_type_item_active_text':index === checkIndex}">{{ item.indexName }}</text>
</view>
</view>
</uv-popup>
</view>
</template>
<script setup>
import {
getThermodynamicMallApi
} from '@/api'
import CardNvue from './Card.nvue'
import ChartsNvue from '@/components/Charts.nvue'
import {
computed,
onMounted,
ref
} from 'vue'
import {
t
} from '@/plugins/index.js'
/**************************** pop选择相关 *********************************/
const typePopRef = ref(null)
const checkIndex = ref('')
const selectIndex = ref(0)
const selectText = computed(() => {
return getIndicator.value?.[selectIndex.value]?.indexName
})
const handleTapToChoose = () => {
checkIndex.value = selectIndex.value || 0
typePopRef.value?.open('bottom')
}
const handleCloseTypeSelect = () => {
typePopRef.value?.close()
}
const handleChangeCheckType = (index) => {
checkIndex.value = index
handleConfirmSelectType()
}
const handleConfirmSelectType = () => {
selectIndex.value = checkIndex.value
typePopRef.value?.close()
getTrendChartData()
}
const selfIndicators = ['PassByCount', 'EnterCount', 'CustomerMantime', 'CustomerNum']
const getIndicator = computed(() => {
return props.indicators.filter(item => selfIndicators.includes(item.indexKey))
})
const props = defineProps({
indicators: {
type: Array,
default: () => []
}
})
const chartsRef = ref(null)
const options = ref({})
const initData = (params) => {
options.value = params
getTrendChartData()
}
defineExpose({
initData
})
const getTrendChartData = async () => {
try {
// 参数设置位置:
const params = {
startDate: options.value.startDate,
endDate: options.value.endDate,
chartIds: 267,
orgIds: options.value.storeId,
index: getIndicator.value?.[selectIndex.value]?.indexKey || '',
}
const {
data
} = await getThermodynamicMallApi(params)
const {
TimeThermodynamic
} = data.body
renderCharts(TimeThermodynamic)
} catch (e) {
console.log(e);
}
}
const renderCharts = (val = {
series: [],
title: '',
xaxis: {
data: []
}
}) => {
// prettier-ignore
const days = val.series.map(el=> el.name) || [];
const daysDataMap = {}
const dataLength = val.xaxis.data.length
for (let item of val.series) {
daysDataMap[item.name] = item.data
}
const dataListByDays = days.map(day => {
return daysDataMap[day] || Array(dataLength).fill(0)
}) || []
const seriesVal = []
dataListByDays.map((_, yIndex) => {
_?.map((item, dataIndex) => {
seriesVal.push([parseInt(yIndex), dataIndex, item || "-"])
})
})
let max = 0
const data = seriesVal.map(function(item) {
max = Math.max(item[2] === '-' ? 0 : item[2], max)
return [item[1], item[0], item[2] || '-'];
});
const option = {
tooltip: {
position: 'top',
confine:true,
triggerOn:'click'
},
grid: {
top: 10,
bottom: 20,
right: 0,
left: 34,
},
xAxis: {
type: 'category',
data: val.xaxis.data,
splitArea: {
show: true
},
axisTick: {
show: false
}
},
yAxis: {
type: 'category',
data: days,
splitArea: {
show: true,
},
axisTick: {
show: false
}
},
visualMap: {
min: 0,
max: max || 1,
show:false,
calculable: true,
type: 'continuous',
orient: 'horizontal',
right: 0,
top: '0',
inRange: {
color: ["#99ddff", "#0022FF"]
}
},
series: [{
name: t('echarts.averageThermal'),
type: 'heatmap',
data: data,
label: {
show: false
},
// emphasis: {
// itemStyle: {
// shadowBlur: 10,
// shadowColor: 'rgba(0, 0, 0, 0.5)'
// }
// }
}]
};
chartsRef.value?.initCharts(option)
}
</script>
<style lang="scss">
@import '@/styles/normal.scss'
</style>
No preview for this file type
No preview for this file type
This diff is collapsed. Click to expand it.
This diff could not be displayed because it is too large.
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!