From c44abae22de4236d81951aac010ca998f5b092e2 Mon Sep 17 00:00:00 2001 From: tx <2622874537@qq.com> Date: Wed, 8 Jan 2025 17:58:32 +0800 Subject: [PATCH] =?UTF-8?q?=E6=88=BF=E9=97=B4=E8=AF=A6=E6=83=85=20=20?= =?UTF-8?q?=E8=AE=BE=E6=96=BD=E8=AF=A6=E6=83=85=20=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/DateRangePicker/index.vue | 64 ++ src/router/index.js | 2 +- .../system/equipment/equipment_detail.vue | 643 +++++++++++++++--- src/views/system/room/room_detail.vue | 9 + .../components/UserDailyRechargeReport.vue | 215 ++++-- src/views/user/user/detail.vue | 547 ++++++++++++--- src/views/user/user/index.vue | 1 + 7 files changed, 1243 insertions(+), 238 deletions(-) create mode 100644 src/components/DateRangePicker/index.vue diff --git a/src/components/DateRangePicker/index.vue b/src/components/DateRangePicker/index.vue new file mode 100644 index 0000000..c736ed8 --- /dev/null +++ b/src/components/DateRangePicker/index.vue @@ -0,0 +1,64 @@ +<template> + <el-date-picker + v-model="dateRange" + type="daterange" + value-format="yyyy-MM-dd" + range-separator="至" + start-placeholder="开始日期" + end-placeholder="结束日期" + :picker-options="pickerOptions" + :clearable="false" + v-on="$listeners" + /> +</template> +<script> +import { getLastDate, getLastMonth } from '@/utils' + +export default { + name: "DateRangePicker", + props: { + value: { + type: Array, + default: null, + }, + }, + data() { + return { + pickerOptions: { + shortcuts: [{ + text: '最近一周', + onClick(picker) { + const end = getLastDate(0); + const start = getLastDate(6); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近一个月', + onClick(picker) { + const end = getLastDate(0); + const start = getLastMonth(1); + picker.$emit('pick', [start, end]); + } + }, { + text: '最近三个月', + onClick(picker) { + const end = getLastDate(0); + const start = getLastMonth(3); + picker.$emit('pick', [start, end]); + } + }] + }, + } + }, + computed: { + dateRange: { + get() { + return this.value + }, + set(val) { + this.$emit('input', val); + } + } + }, +} +</script> diff --git a/src/router/index.js b/src/router/index.js index 0fd3640..c055eb8 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -239,7 +239,7 @@ export const dynamicRoutes = [ permissions: ['system:equipment:query'], children: [ { - path: 'index/:equipmentId(\\d+)', + path: 'index/:roomId(\\d+)', component: () => import('@/views/system/equipment/equipment_detail'), name: 'EquipmentDetail', meta: { title: '设施详情', activeMenu: '/system/equipment' } diff --git a/src/views/system/equipment/equipment_detail.vue b/src/views/system/equipment/equipment_detail.vue index 10949ea..34b566c 100644 --- a/src/views/system/equipment/equipment_detail.vue +++ b/src/views/system/equipment/equipment_detail.vue @@ -1,120 +1,575 @@ <template> <div class="app-container"> - <!-- 基本信息卡片 --> - <el-card class="box-card"> - <div slot="header" class="clearfix"> - <span>基本信息</span> + <el-card class="box-card" shadow="hover"> + <!-- 操作按钮 --> + <div class="operation-buttons"> + <el-button type="primary" icon="el-icon-edit" size="small" @click="handleUpdate" + v-hasPermi="['system:room:edit']">修改设施</el-button> + </div> + + <!-- 设施基本信息 --> + <div class="room-header"> + <el-row :gutter="40"> + <el-col :span="8"> + <div class="image-wrapper"> + <el-image :src="room.picture" fit="cover" class="room-image"> + <div slot="error" class="image-slot"> + <i class="el-icon-picture-outline"></i> + </div> + </el-image> + </div> + </el-col> + <el-col :span="16"> + <div class="room-title"> + <h2>{{ room.roomName }}</h2> + <el-tag :type="room.status === '1' ? 'success' : 'info'" class="status-tag" effect="dark"> + {{ room.status === '1' ? '正常' : '停用' }} + </el-tag> + </div> + <div class="room-info"> + <el-descriptions :column="2" border size="medium"> + <el-descriptions-item label="所属门店"> + <span class="info-text">{{ room.storeName }}</span> + </el-descriptions-item> + <el-descriptions-item label="商户ID"> + <span class="info-text">{{ room.merchantId }}</span> + </el-descriptions-item> + <el-descriptions-item label="设施ID"> + <span class="info-text">{{ room.roomId }}</span> + </el-descriptions-item> + <el-descriptions-item label="设施类型"> + <span class="info-text">{{ getRoomType(room.type) }} / {{ getRoomType2(room.type2) }}</span> + </el-descriptions-item> + <el-descriptions-item label="基础价格"> + <span class="price">¥{{ room.hour }}</span> + <span class="unit">/小时</span> + </el-descriptions-item> + <el-descriptions-item label="销售数量"> + <span class="info-text">{{ room.soldNum || '暂无数据' }}</span> + </el-descriptions-item> + </el-descriptions> + + <div class="tags-section"> + <span class="label">设施标签:</span> + <div class="tags-wrapper"> + <dict-tag v-for="tag in room.tags" :key="tag" :options="dict.type.ss_room_tags" :value="tag" + class="tag-item" /> + </div> + </div> + </div> + </el-col> + </el-row> + </div> + + + + <!-- WiFi信息 --> + <div v-if="room.wifi" class="section-block"> + <h3> + <i class="el-icon-connection"></i> + WiFi信息 + </h3> + <el-descriptions :column="2" border size="medium"> + <el-descriptions-item label="WiFi名称"> + <span class="info-text">{{ room.wifi }}</span> + </el-descriptions-item> + <el-descriptions-item label="WiFi密码"> + <span class="info-text">{{ room.wifiPassword }}</span> + </el-descriptions-item> + </el-descriptions> + </div> + + <!-- 系统信息 --> + <div class="section-block"> + <h3> + <i class="el-icon-info"></i> + 系统信息 + </h3> + <el-descriptions :column="3" border size="medium"> + <el-descriptions-item label="创建人"> + <span class="info-text">{{ room.createBy || '暂无' }}</span> + </el-descriptions-item> + <el-descriptions-item label="创建时间"> + <span class="info-text">{{ room.createTime || '暂无' }}</span> + </el-descriptions-item> + <el-descriptions-item label="更新人"> + <span class="info-text">{{ room.updateBy || '暂无' }}</span> + </el-descriptions-item> + <el-descriptions-item label="更新时间"> + <span class="info-text">{{ room.updateTime || '暂无' }}</span> + </el-descriptions-item> + <el-descriptions-item label="备注" :span="2"> + <span class="info-text">{{ room.remark || '暂无' }}</span> + </el-descriptions-item> + </el-descriptions> </div> - <el-row :gutter="20"> - <el-col :span="8"> - <div class="info-item"> - <span class="label">设施ID:</span> - <span>{{ equipmentData.equipmentId }}</span> - </div> - <div class="info-item"> - <span class="label">设施名称:</span> - <span>{{ equipmentData.name }}</span> - </div> - <div class="info-item"> - <span class="label">设施类型:</span> - <dict-tag :options="dict.type.ss_equipment_type" :value="equipmentData.type"/> - </div> - </el-col> - <el-col :span="8"> - <div class="info-item"> - <span class="label">所属店铺:</span> - <span>{{ equipmentData.storeName }}</span> - </div> - <div class="info-item"> - <span class="label">所属房间:</span> - <span>{{ equipmentData.roomName }}</span> - </div> - <div class="info-item"> - <span class="label">状态:</span> - <dict-tag :options="dict.type.ss_equipment_status" :value="equipmentData.status"/> - </div> - </el-col> - <el-col :span="8"> - <div class="info-item"> - <span class="label">创建时间:</span> - <span>{{ parseTime(equipmentData.createTime) }}</span> - </div> - <div class="info-item"> - <span class="label">更新时间:</span> - <span>{{ parseTime(equipmentData.updateTime) }}</span> - </div> - </el-col> - </el-row> </el-card> - <!-- 设备信息卡片 --> - <el-card class="box-card"> - <div slot="header" class="clearfix"> - <span>设备信息</span> + <!-- 修改设施弹窗 --> + <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body> + <el-form ref="form" :model="form" :rules="rules" label-width="80px"> + <el-form-item label="设施名" prop="roomName"> + <el-input v-model="form.roomName" placeholder="请输入设施名" /> + </el-form-item> + <el-form-item label="店铺" prop="storeId"> + <el-select v-model="form.storeId" clearable filterable placeholder="请选择"> + <el-option v-for="item in storeOptions" :key="item.storeId" :label="item.name" :value="item.storeId" /> + </el-select> + </el-form-item> + <el-form-item label="类型" prop="type"> + <el-select v-model="form.type" placeholder="请选择类型"> + <el-option v-for="dict in dict.type.ss_room_type" :key="dict.value" :label="dict.label" + :value="dict.value"></el-option> + </el-select> + </el-form-item> + <el-form-item label="图片" prop="picture"> + <image-upload v-model="form.picture" /> + </el-form-item> + <el-form-item label="标签" prop="tags"> + <el-select v-model="form.tags" placeholder="请选择标签" clearable multiple style="width: auto; min-width: 240px"> + <el-option v-for="dict in dict.type.ss_room_tags" :key="dict.value" :label="dict.label" + :value="dict.value" /> + </el-select> + </el-form-item> + </el-form> + <div slot="footer" class="dialog-footer"> + <el-button type="primary" @click="submitForm">确 定</el-button> + <el-button @click="cancel">取 消</el-button> </div> - <el-row :gutter="20"> - <el-col :span="8"> - <div class="info-item"> - <span class="label">设备ID:</span> - <span>{{ equipmentData.deviceId }}</span> - </div> - <div class="info-item"> - <span class="label">MAC地址:</span> - <span>{{ equipmentData.device ? equipmentData.device.mac : '-' }}</span> - </div> - </el-col> - <el-col :span="8"> - <div class="info-item"> - <span class="label">SN:</span> - <span>{{ equipmentData.device ? equipmentData.device.sn : '-' }}</span> - </div> - <div class="info-item"> - <span class="label">设备状态:</span> - <dict-tag :options="dict.type.ss_device_status" :value="equipmentData.device ? equipmentData.device.status : ''"/> - </div> - </el-col> - </el-row> - </el-card> + </el-dialog> + <el-tabs v-model="activeTab" class="detail-tabs"> + + <el-tab-pane label="订单列表" name="orders"> + <order :roomId="room.roomId"></order> + </el-tab-pane> + + <el-tab-pane label="设备列表" name="devices"> + <device :roomId="room.roomId"></device> + </el-tab-pane> + <el-tab-pane label="套餐列表" name="rules"> + <rule :roomId="room.roomId"></rule> + </el-tab-pane> + <el-tab-pane label="设施列表" name="equipments"> + <equipment :roomId="room.roomId"></equipment> + </el-tab-pane> + </el-tabs> </div> </template> <script> -import { getEquipment } from "@/api/system/equipment"; - +import { getRoom, updateRoom } from '@/api/system/room' +import { getDicts } from '@/api/system/dict/data' +import { listStore } from "@/api/system/store" +import order from '@/views/system/order/index.vue' +import device from '@/views/system/device/index.vue' +import rule from '@/views/system/rule/index.vue' +import equipment from '@/views/system/equipment/index.vue' export default { - name: "EquipmentDetail", - dicts: ['ss_equipment_type', 'ss_equipment_status', 'ss_device_status'], + name: 'RoomDetail', + dicts: ['ss_room_type', 'ss_room_tags', 'ss_room_status'], + components: { order, device, rule, equipment }, data() { return { - equipmentData: {} - }; + // 是否显示弹出层 + open: false, + // 弹出层标题 + title: "", + room: {}, + // 设施类型字典 + roomTypeOptions: [], + // 设施类型2字典 + roomType2Options: [], + // 设施标签字典 + roomTagOptions: [], + // 店铺选项 + storeOptions: [], + // 表单参数 + form: {}, + // 表单校验 + rules: { + roomName: [ + { required: true, message: "设施名不能为空", trigger: "blur" } + ], + storeId: [ + { required: true, message: "店铺不能为空", trigger: "blur" } + ], + type: [ + { required: true, message: "类型不能为空", trigger: "blur" } + ], + tags: [ + { required: true, message: "标签不能为空", trigger: "change" } + ], + }, + activeTab: 'orders', + } }, + created() { - const equipmentId = this.$route.params.equipmentId; - this.getEquipmentData(equipmentId); + this.getDetail() + this.getDictData() + this.getStoreOptions() }, + methods: { - getEquipmentData(equipmentId) { - getEquipment(equipmentId).then(response => { - this.equipmentData = response.data; + /** 获取设施详情 */ + async getDetail() { + try { + const roomId = this.$route.params.roomId + const response = await getRoom(roomId) + this.room = response.data + } catch (error) { + console.error('获取设施详情失败:', error) + this.$message.error('获取设施详情失败') + } + }, + + /** 获取字典数据 */ + async getDictData() { + try { + const [typeRes, type2Res, tagRes] = await Promise.all([ + getDicts('room_type'), + getDicts('room_type2'), + getDicts('room_tag') + ]) + this.roomTypeOptions = typeRes.data + this.roomType2Options = type2Res.data + this.roomTagOptions = tagRes.data + } catch (error) { + console.error('获取字典数据失败:', error) + this.$message.error('获取字典数据失败') + } + }, + + /** 获取店铺选项 */ + getStoreOptions() { + listStore({ pageNum: 1, pageSize: 999 }).then(response => { + this.storeOptions = response.rows; + }); + }, + + /** 获取设施类型名称 */ + getRoomType(type) { + const found = this.roomTypeOptions.find(item => item.dictValue === type) + return found ? found.dictLabel : type + }, + + /** 获取设施类型2名称 */ + getRoomType2(type) { + const found = this.roomType2Options.find(item => item.dictValue === type) + return found ? found.dictLabel : type + }, + + /** 获取标签名称 */ + getTagName(tag) { + const found = this.roomTagOptions.find(item => item.dictValue === tag) + return found ? found.dictLabel : tag + }, + + /** 修改按钮操作 */ + handleUpdate() { + this.reset(); + this.form = { ...this.room }; + this.open = true; + this.title = "修改设施"; + }, + + // 取消按钮 + cancel() { + this.open = false; + this.reset(); + }, + + // 表单重置 + reset() { + this.form = { + roomId: undefined, + roomName: undefined, + storeId: undefined, + type: undefined, + picture: undefined, + tags: [] + }; + this.resetForm("form"); + }, + + /** 提交按钮 */ + submitForm() { + this.$refs["form"].validate(valid => { + if (valid) { + updateRoom(this.form).then(response => { + this.$modal.msgSuccess("修改成功"); + this.open = false; + this.getDetail(); + }); + } }); } } -}; +} </script> <style lang="scss" scoped> -.app-container { - .box-card { - margin-bottom: 20px; - } - .info-item { - margin-bottom: 15px; - .label { - color: #606266; - margin-right: 10px; - font-size: 14px; + .detail-tabs { + margin-top: 20px; + + .search-form { + margin-bottom: 20px; + + .el-form-item { + margin-bottom: 10px; + } } } +.app-container { + padding: 24px; + background-color: #f5f7fa; + min-height: calc(100vh - 84px); } -</style> \ No newline at end of file + +.operation-buttons { + // margin-left: auto; + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: flex-end; + margin-bottom: 20px; + text-align: right; + .el-button { + margin: 0; + padding: 10px 10px; + } +} + +.box-card { + border-radius: 12px; + padding: 24px; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05); +} + +.box-card:hover { + box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); +} + +.room-header { + margin-bottom: 48px; +} + +.image-wrapper { + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + position: relative; +} + +.image-wrapper:hover { + transform: translateY(-5px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); +} + +.room-image { + width: 100%; + height: 320px; + border-radius: 12px; + object-fit: cover; +} + +.room-title { + display: flex; + align-items: center; + margin-bottom: 28px; + padding-bottom: 16px; + border-bottom: 1px solid #EBEEF5; +} + +.room-title h2 { + margin: 0; + margin-right: 15px; + font-size: 28px; + color: #303133; + font-weight: 600; +} + +.status-tag { + margin-left: auto; + padding: 0 16px; + height: 28px; + line-height: 28px; + font-size: 14px; +} + +.room-info { + margin-top: 28px; +} + +.info-text { + color: #606266; + font-size: 14px; +} + +.tags-section { + margin-top: 28px; + padding: 16px; + background: #f9fafc; + border-radius: 8px; +} + +.tags-wrapper { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 8px; +} + +.label { + color: #606266; + margin-right: 16px; + font-weight: 500; + font-size: 14px; +} + +.tag-item { + margin-right: 10px; + margin-bottom: 8px; + border-radius: 4px; + padding: 0 12px; + height: 28px; + line-height: 26px; +} + +.section-block { + margin-top: 48px; + padding-top: 32px; + border-top: 1px solid #EBEEF5; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 28px; +} + +h3 { + margin: 0; + font-size: 20px; + font-weight: 600; + color: #303133; + display: flex; + align-items: center; +} + +h3 i { + margin-right: 12px; + font-size: 24px; + color: #409EFF; +} + +.total-rules { + margin-top: 3px; +} + +.price { + color: #F56C6C; + font-weight: 600; + font-size: 16px; +} + +.unit { + color: #909399; + margin-left: 4px; + font-size: 14px; +} + +.image-slot { + display: flex; + justify-content: center; + align-items: center; + width: 100%; + height: 100%; + background: #f5f7fa; +} + +.image-slot i { + font-size: 48px; + color: #909399; +} + +.custom-table { + margin-top: 16px; + border-radius: 8px; + overflow: hidden; +} + +:deep(.el-descriptions) { + padding: 20px; + background-color: #fff; + border-radius: 8px; +} + +:deep(.el-descriptions__label) { + font-weight: 500; + color: #606266; +} + +:deep(.el-descriptions__content) { + padding: 12px 16px; +} + +:deep(.el-table th) { + background-color: #f5f7fa !important; + font-weight: 500; +} + +:deep(.el-table--border) { + border-radius: 8px; + overflow: hidden; +} + +:deep(.el-descriptions__body) { + background-color: #fff; +} + +:deep(.el-descriptions-item__label) { + background-color: #f5f7fa; + font-weight: 500; +} + +// 标签选择器样式 +.el-select { + :deep(.el-select__tags) { + flex-wrap: wrap; + + .el-tag { + margin: 2px; + max-width: none; + } + } + + :deep(.el-input__inner) { + height: auto; + min-height: 32px; + padding-top: 4px; + padding-bottom: 4px; + } +} + +.el-select-dropdown__item { + padding: 0 10px; + height: 32px; + line-height: 32px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.el-form-item { + margin-bottom: 18px; + + .el-select { + width: auto; + min-width: 240px; + max-width: 100%; + } +} +</style> \ No newline at end of file diff --git a/src/views/system/room/room_detail.vue b/src/views/system/room/room_detail.vue index 52a80e1..d20fedd 100644 --- a/src/views/system/room/room_detail.vue +++ b/src/views/system/room/room_detail.vue @@ -327,8 +327,17 @@ export default { } .operation-buttons { + // margin-left: auto; + display: flex; + flex-wrap: wrap; + gap: 5px; + justify-content: flex-end; margin-bottom: 20px; text-align: right; + .el-button { + margin: 0; + padding: 10px 10px; + } } .box-card { diff --git a/src/views/user/user/components/UserDailyRechargeReport.vue b/src/views/user/user/components/UserDailyRechargeReport.vue index 4a33d4a..6b3e7fb 100644 --- a/src/views/user/user/components/UserDailyRechargeReport.vue +++ b/src/views/user/user/components/UserDailyRechargeReport.vue @@ -1,64 +1,155 @@ -<!--<template>--> -<!-- <div v-loading="loading" >--> -<!-- <date-range-picker :start-date.sync="queryParams.payDateStart" :end-date.sync="queryParams.payDateEnd" @change="getList"/>--> -<!-- <single-line-chart :labels="labels" :chart-data="chartData" name="收入(元)" height="268px"/>--> -<!-- </div>--> -<!--</template>--> +<template> + <div v-loading="loading"> + <date-range-picker :start-date.sync="queryParams.payDateStart" :end-date.sync="queryParams.payDateEnd" @change="getList"/> + <!-- 替换为 echarts 容器 --> + <div ref="chartRef" style="width: 100%; height: 268px;"></div> + </div> +</template> -<!--<script>--> -<!--import SingleLineChart from '@/components/SingleLineChart/index.vue'--> -<!--import { BonusArrivalType } from '@/utils/constants'--> -<!--import { getLastDateStr } from '@/utils'--> -<!--import DateRangePicker from '@/components/DateRangePicker/index.vue'--> -<!--import { getBonusDailyAmount } from '@/api/system/dashboard'--> +<script> +import * as echarts from 'echarts' +import { BonusArrivalType } from '@/utils/constants' +import { getLastDateStr } from '@/utils' +import DateRangePicker from '@/components/DateRangePicker/index.vue' -<!--export default {--> -<!-- name: "UserDailyRechargeReport",--> -<!-- components: { DateRangePicker, SingleLineChart },--> -<!-- props: {--> -<!-- query: {--> -<!-- type: Object,--> -<!-- default: () => {--> -<!-- return {}--> -<!-- }--> -<!-- }--> -<!-- },--> -<!-- data() {--> -<!-- return {--> -<!-- loading: false,--> -<!-- list: [],--> -<!-- queryParams: {--> -<!-- payDateStart: getLastDateStr(30),--> -<!-- payDateEnd: getLastDateStr(0),--> -<!-- arrivalId: null,--> -<!-- arrivalTypes: BonusArrivalType.userList()--> -<!-- },--> -<!-- }--> -<!-- },--> -<!-- computed: {--> -<!-- labels() {--> -<!-- return this.list.map(item => item.key);--> -<!-- },--> -<!-- chartData() {--> -<!-- return this.list.map(item => item.sum == null ? 0 : item.sum.toFixed(2));--> -<!-- }--> -<!-- },--> -<!-- created() {--> -<!-- this.queryParams = {--> -<!-- ...this.queryParams,--> -<!-- ...this.query--> -<!-- }--> -<!-- this.getList();--> -<!-- },--> -<!-- methods: {--> -<!-- getList() {--> -<!-- this.loading = true;--> -<!-- getBonusDailyAmount(this.queryParams).then(res => {--> -<!-- this.list = res.data;--> -<!-- }).finally(() => {--> -<!-- this.loading = false;--> -<!-- })--> -<!-- }--> -<!-- }--> -<!--}--> -<!--</script>--> +export default { + name: "UserDailyRechargeReport", + components: { DateRangePicker }, + props: { + query: { + type: Object, + default: () => { + return {} + } + } + }, + data() { + return { + loading: false, + list: [], + queryParams: { + payDateStart: getLastDateStr(30), + payDateEnd: getLastDateStr(0), + arrivalId: null, + arrivalTypes: BonusArrivalType.userList() + }, + chart: null + } + }, + computed: { + labels() { + return this.list.map(item => item.key); + }, + chartData() { + return this.list.map(item => item.sum == null ? 0 : Number(item.sum.toFixed(2))); + } + }, + created() { + this.queryParams = { + ...this.queryParams, + ...this.query + } + this.getList(); + }, + mounted() { + this.initChart(); + }, + beforeDestroy() { + if (this.chart) { + this.chart.dispose(); + } + }, + methods: { + initChart() { + this.chart = echarts.init(this.$refs.chartRef); + this.updateChartData(); + }, + updateChartData() { + if (!this.chart) return; + + const option = { + tooltip: { + trigger: 'axis', + formatter: '{b}<br/>{a}: {c}元' + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis: { + type: 'category', + boundaryGap: false, + data: this.labels + }, + yAxis: { + type: 'value', + axisLabel: { + formatter: '{value}元' + } + }, + series: [{ + name: '收入', + type: 'line', + data: this.chartData, + smooth: true, + symbol: 'circle', + symbolSize: 6, + itemStyle: { + color: '#409EFF' + }, + lineStyle: { + width: 2 + }, + areaStyle: { + color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ + offset: 0, + color: 'rgba(64,158,255,0.3)' + }, { + offset: 1, + color: 'rgba(64,158,255,0.1)' + }]) + } + }] + }; + + this.chart.setOption(option); + }, + getList() { + this.loading = true; + // 模拟API调用 + setTimeout(() => { + // 生成最近30天的模拟数据 + const mockData = Array.from({length: 30}, (_, i) => { + const date = new Date(); + date.setDate(date.getDate() - i); + return { + key: `${date.getMonth() + 1}-${date.getDate()}`, + sum: Math.random() * 1000 + } + }).reverse(); + + this.list = mockData; + this.loading = false; + // 更新图表数据 + this.$nextTick(() => { + this.updateChartData(); + }); + }, 500) + } + }, + watch: { + // 监听窗口大小变化,重绘图表 + '$store.state.app.sidebar.opened': { + handler() { + if (this.chart) { + this.$nextTick(() => { + this.chart.resize(); + }); + } + } + } + } +} +</script> \ No newline at end of file diff --git a/src/views/user/user/detail.vue b/src/views/user/user/detail.vue index ec0fd27..802efe0 100644 --- a/src/views/user/user/detail.vue +++ b/src/views/user/user/detail.vue @@ -1,180 +1,565 @@ <template> <div class="app-container" v-loading="loading"> <template> - <el-row :gutter="12"> - <el-col :lg="10" :md="12" :xs="24"> - <el-card class="box-card"> + <!-- 统计卡片行 --> + <el-row :gutter="20" class="statistics-cards"> + <el-col :span="3" v-for="(stat, index) in statisticsData" :key="index"> + <div class="stat-card"> + <div class="stat-icon" :class="stat.color"> + <i :class="stat.icon"></i> + </div> + <div class="stat-content"> + <div class="stat-label">{{ stat.label }}</div> + <div class="stat-value"> + <template v-if="stat.isMoney">¥ {{ detail[stat.field] || '0.00' }}</template> + <template v-else>{{ detail[stat.field] || '0' }} {{ stat.unit }}</template> + </div> + </div> + </div> + </el-col> + </el-row> + + <el-row :gutter="12" class="detail-row"> + <!-- 用户详情卡片 --> + <el-col :lg="10" :md="12" :xs="24" > + <el-card class="box-card detail-card"> <template #header> <el-row type="flex" style="justify-content: space-between"> <div>用户详情</div> <div> - <el-button type="text" icon="el-icon-setting" size="small" style="padding: 5px 0 0;" @click="showConfigDialog = true">用户配置</el-button> + <el-button type="text" icon="el-icon-setting" size="small" style="padding: 5px 0 0;" + @click="showConfigDialog = true">用户配置</el-button> </div> </el-row> </template> - <div class="user-detail"> <div class="user-header"> <el-avatar :size="64" :src="detail.avatar"></el-avatar> <el-row type="flex" class="name-box"> - <span class="user-name">11</span> - <dict-tag :value="detail.type" :options="dict.type.sm_user_type"/> + <span class="user-name">{{ detail.userName }}</span> + <dict-tag :value="detail.userType" :options="dict.type.ss_user_type" style="margin-top: 4px;" /> </el-row> - <div class="phone-number">{{detail.phonenumber}}</div> + <div class="phone-number">{{ detail.phonenumber }}</div> </div> -<!-- <el-row type="flex" style="margin-top: 16px;">--> -<!-- <el-statistic title="店铺数" :value="detail.storeCount" :precision="0" suffix="家"/>--> -<!-- <el-statistic title="设备数" :value="detail.deviceCount" :precision="0" suffix="台"/>--> -<!-- <el-statistic title="账户余额" :value="detail.balance" :precision="2" suffix="元"/>--> -<!-- <el-statistic title="总收入" :value="detail.totalIncome" :precision="2" suffix="元"/>--> -<!-- <el-statistic title="未入账" :value="detail.waitBonusAmount" :precision="2" suffix="元"/>--> -<!-- <el-statistic title="总提现" :value="detail.withDrawlAmount" :precision="2" suffix="元"/>--> -<!-- <el-statistic title="总消费" :value="detail.rechargeAmount" :precision="2" suffix="元"/>--> -<!-- </el-row>--> - <div class="user-description"> <el-descriptions :column="3"> <el-descriptions-item label="充值服务费"> - {{detail.realServiceRate | money | defaultValue}} % + {{ detail.serviceFeeProportion | money | defaultValue }} % </el-descriptions-item> <el-descriptions-item label="代理服务费"> - {{detail.agentServiceRate | money | defaultValue}} % + {{ detail.agentServiceRate | money | defaultValue }} % </el-descriptions-item> <el-descriptions-item label="提现服务费"> - <template v-if="detail.withdrawServiceRate == null || detail.withdrawServiceType == null">跟随渠道</template> + <template v-if="detail.withdrawServiceRate == null || detail.withdrawServiceType == null"> + 跟随渠道 + </template> <template v-else> - <dict-tag :options="dict.type.withdraw_service_type" :value="detail.withdrawServiceType" size="mini"/> - {{detail.withdrawServiceRate}} {{serviceUnit(detail.withdrawServiceType)}} + <dict-tag :options="dict.type.withdraw_service_type" :value="detail.withdrawServiceType" + size="mini" /> + {{ detail.withdrawServiceRate }} {{ serviceUnit(detail.withdrawServiceType) }} </template> </el-descriptions-item> -<!-- <el-descriptions-item label="实名认证">--> -<!-- <boolean-tag :value="detail.isReal" size="small"/>--> -<!-- <el-link--> -<!-- v-if="detail.isReal"--> -<!-- style="margin-left: 4px;"--> -<!-- type="primary"--> -<!-- size="small"--> -<!-- icon="el-icon-link"--> -<!-- @click="handleResetRealName"--> -<!-- >解除实名</el-link>--> -<!-- </el-descriptions-item>--> <el-descriptions-item label="姓名"> - {{detail.realName | dv}} + {{ detail.realName | dv }} </el-descriptions-item> <el-descriptions-item label="身份证"> - {{detail.realIdCard | dv}} + {{ detail.idCard | dv }} </el-descriptions-item> <el-descriptions-item label="实名手机"> - {{detail.realPhone | dv}} + {{ detail.phonenumber | dv }} + </el-descriptions-item> + <el-descriptions-item label="备注" :span="2"> + {{ detail.remark | dv }} </el-descriptions-item> - <el-descriptions-item label="备注" :span="2">{{detail.remark | dv}}</el-descriptions-item> </el-descriptions> </div> </div> </el-card> </el-col> + + <!-- 报表卡片 --> <el-col :lg="14" :md="12" :xs="24"> <el-card class="box-card"> - <el-row type="flex"> - <el-tabs style="width: 100%"> - <el-tab-pane label="日报表" lazy> - <user-daily-recharge-report v-if="detail.userId != null" :query="{arrivalId: detail.userId}"/> - </el-tab-pane> - <el-tab-pane label="月报表" lazy> - <user-recharge-report :mch-id="detail.userId"/> - </el-tab-pane> - </el-tabs> - </el-row> + <el-tabs v-model="activeTab" @tab-click="handleTabClick" style="width: 100%"> + <!-- 日报表 --> + <el-tab-pane label="日报表" name="daily"> + <div class="report-container"> + <div class="filter-section"> + <el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" + end-placeholder="结束日期" :picker-options="pickerOptions" value-format="yyyy-MM-dd" + @change="loadDailyReport" /> + </div> + <div class="chart-container"> + <div ref="dailyChart" style="width: 100%; height: 300px"></div> + </div> + + </div> + </el-tab-pane> + + <!-- 月报表 --> + <el-tab-pane label="月报表" name="monthly"> + <div class="report-container"> + <div class="filter-section"> + <el-date-picker v-model="selectedMonth" type="month" placeholder="选择月份" value-format="yyyy-MM" + @change="loadMonthlyReport" /> + </div> + <div class="chart-container"> + <div ref="monthlyChart" style="width: 100%; height: 300px"></div> + </div> + </div> + </el-tab-pane> + </el-tabs> </el-card> </el-col> </el-row> </template> -<!-- <el-empty v-else description="用户不存在"/>--> -<!-- <!–用户设置–>--> -<!-- <user-config-dialog :show.sync="showConfigDialog" :user-id="detail.userId" @success="getDetail"/>--> + <user-config-dialog :show.sync="showConfigDialog" :user-id="detail.userId" @success="getDetail" /> </div> </template> - <script> import { getUser } from '@/api/system/user' -import Device from '@/views/system/device/index.vue' -// import Withdraw from '@/views/system/withdraw/index.vue' -// import UserDailyRechargeReport from '@/views/system/smUser/components/UserDailyRechargeReport.vue' -// import UserRechargeReport from '@/views/system/smUser/components/userRechargeReport.vue' -// import UserConfigDialog from '@/views/system/smUser/components/UserConfigDialog.vue' -// import { BonusArrivalType, SmUserType } from '@/utils/constants' +import UserConfigDialog from './components/UserConfigDialog.vue' import { $serviceType, $view } from '@/utils/mixins' +import * as echarts from 'echarts' export default { name: 'UserDetail', mixins: [$view, $serviceType], - dicts: ['sm_user_type', 'service_type', 'withdraw_service_type'], + dicts: ['ss_user_type', 'withdraw_service_type'], components: { - Device - + UserConfigDialog }, data() { return { detail: {}, loading: false, showConfigDialog: false, + activeTab: 'monthly', + dateRange: [], + selectedMonth: '', + dailyReportData: [], + monthlyReportData: [], + dailyChart: null, + monthlyChart: null, + + // 统计卡片数据 + statisticsData: [ + { label: '店铺数', field: 'storeCount', icon: 'el-icon-office-building', color: 'blue', unit: '家' }, + { label: '房间数', field: 'roomCount', icon: 'el-icon-house', color: 'pink', unit: '间' }, + { label: '设施数', field: 'facilityCount', icon: 'el-icon-box', color: 'cyan', unit: '个' }, + { label: '设备数', field: 'deviceCount', icon: 'el-icon-monitor', color: 'green', unit: '台' }, + { label: '账户余额', field: 'balance', icon: 'el-icon-wallet', color: 'purple', isMoney: true }, + { label: '总收入', field: 'totalIncome', icon: 'el-icon-money', color: 'orange', isMoney: true }, + { label: '总提现', field: 'totalWithdrawAmount', icon: 'el-icon-bank-card', color: 'red', isMoney: true }, + { label: '总消费', field: 'totalConsumption', icon: 'el-icon-shopping-cart-full', color: 'brown', isMoney: true } + ], + + pickerOptions: { + shortcuts: [{ + text: '最近一周', + onClick(picker) { + const end = new Date() + const start = new Date() + start.setTime(start.getTime() - 3600 * 1000 * 24 * 7) + picker.$emit('pick', [start, end]) + } + }, { + text: '最近一个月', + onClick(picker) { + const end = new Date() + const start = new Date() + start.setTime(start.getTime() - 3600 * 1000 * 24 * 30) + picker.$emit('pick', [start, end]) + } + }] + } } }, computed: { serviceUnit() { return (type) => { - return type === '2' ? '元' : '%'; + return type === '2' ? '元' : '%' } } }, created() { - this.getDetail(); + this.getDetail() + // 设置默认日期范围为最近7天 + const end = new Date() + const start = new Date() + start.setTime(start.getTime() - 3600 * 1000 * 24 * 7) + this.dateRange = [this.formatDate(start), this.formatDate(end)] + // 设置默认月份为当前月 + this.selectedMonth = this.formatDate(new Date()).substring(0, 7) + }, + mounted() { + this.$nextTick(() => { + this.initCharts() + this.loadDailyReport() + this.loadMonthlyReport() + }) }, methods: { - getDetail() { - this.loading = true; - getUser(this.$route.params.userId).then(response => { - this.detail = response.data; - }).finally(() => { - this.loading = false; + + handleTabClick(tab) { + this.$nextTick(() => { + if (tab.name === 'daily') { + if (this.dailyChart) { + this.dailyChart.dispose() + } + this.dailyChart = echarts.init(this.$refs.dailyChart) + this.loadDailyReport() + } else if (tab.name === 'monthly') { + if (this.monthlyChart) { + this.monthlyChart.dispose() + } + this.monthlyChart = echarts.init(this.$refs.monthlyChart) + this.loadMonthlyReport() + } }) }, + getDetail() { + this.loading = true + getUser(this.$route.params.userId).then(response => { + this.detail = response.data + }).finally(() => { + this.loading = false + }) + }, + formatDate(date) { + const year = date.getFullYear() + const month = String(date.getMonth() + 1).padStart(2, '0') + const day = String(date.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` + }, + initCharts() { + // 移除原有的初始化逻辑,改为只初始化当前激活的图表 + this.$nextTick(() => { + if (this.activeTab === 'daily') { + this.dailyChart = echarts.init(this.$refs.dailyChart) + this.loadDailyReport() + } else { + this.monthlyChart = echarts.init(this.$refs.monthlyChart) + this.loadMonthlyReport() + } + }) + + // 监听窗口大小变化 + window.addEventListener('resize', () => { + if (this.dailyChart && this.activeTab === 'daily') { + this.dailyChart.resize() + } + if (this.monthlyChart && this.activeTab === 'monthly') { + this.monthlyChart.resize() + } + }) + }, + loadDailyReport() { + // 模拟日报表数据 + this.dailyReportData = Array.from({ length: 7 }, (_, i) => { + const date = new Date() + date.setDate(date.getDate() - i) + return { + date: this.formatDate(date), + rechargeAmount: Math.floor(Math.random() * 10000), + consumeAmount: Math.floor(Math.random() * 8000), + orderCount: Math.floor(Math.random() * 100) + } + }).reverse() + + // 更新图表 + const option = { + title: { + text: '日报表统计' + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + legend: { + data: ['充值金额', '消费金额', '订单数'] + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis: { + type: 'category', + data: this.dailyReportData.map(item => item.date) + }, + yAxis: [ + { + type: 'value', + name: '金额', + axisLabel: { + formatter: '{value} 元' + } + }, + { + type: 'value', + name: '订单数', + axisLabel: { + formatter: '{value}' + } + } + ], + series: [ + { + name: '充值金额', + type: 'bar', + data: this.dailyReportData.map(item => item.rechargeAmount) + }, + { + name: '消费金额', + type: 'bar', + data: this.dailyReportData.map(item => item.consumeAmount) + }, + { + name: '订单数', + type: 'line', + yAxisIndex: 1, + data: this.dailyReportData.map(item => item.orderCount) + } + ] + } + this.dailyChart.setOption(option) + }, + loadMonthlyReport() { + // 模拟月报表数据 - 获取当月每一天的数据 + const daysInMonth = new Date(this.selectedMonth.split('-')[0], this.selectedMonth.split('-')[1], 0).getDate(); + + this.monthlyReportData = Array.from({length: daysInMonth}, (_, i) => { + return { + date: `${this.selectedMonth}-${String(i + 1).padStart(2, '0')}`, + rechargeAmount: Math.floor(Math.random() * 10000), + consumeAmount: Math.floor(Math.random() * 8000) + } + }) + + // 更新图表 + const option = { + title: { + text: '月度统计' + }, + tooltip: { + trigger: 'axis', + axisPointer: { + type: 'shadow' + } + }, + legend: { + data: ['充值金额', '消费金额'] + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + xAxis: { + type: 'category', + data: this.monthlyReportData.map(item => item.date.split('-')[2]), + name: '日期', + nameLocation: 'end', + nameGap: 5, + axisLabel: { + formatter: '{value}日' + } + }, + yAxis: { + type: 'value', + name: '金额', + axisLabel: { + formatter: '{value}元' + } + }, + series: [ + { + name: '充值金额', + type: 'bar', + barGap: '30%', // 柱间距离 + barWidth: '30%', // 柱宽度 + data: this.monthlyReportData.map(item => item.rechargeAmount) + }, + { + name: '消费金额', + type: 'bar', + barWidth: '30%', // 柱宽度 + data: this.monthlyReportData.map(item => item.consumeAmount) + } + ] + } + this.monthlyChart.setOption(option) +} } } </script> - <style scoped> -.app-container .box-card:nth-child(n + 1) { - margin-top: 1em; +.detail-row { + display: flex; + margin: 0 -6px; /* 抵消 el-row 的默认间距 */ } + +.detail-row > .el-col { + display: flex; + padding: 0 6px; /* 保持列间距 */ +} + +.detail-card { + flex: 1; + display: flex; + flex-direction: column; +} + +.box-card { + width: 100%; + height: 100%; +} + +.user-detail { + flex: 1; + display: flex; + flex-direction: column; +} + +.user-description { + flex: 1; +} +.statistics-cards { + margin-bottom: 20px; +} + +.stat-card { + background: #fff; + border-radius: 4px; + padding: 15px; + display: flex; + align-items: flex-start; + box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1); + margin-bottom: 20px; +} + +.stat-icon { + width: 40px; + height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-right: 8px; +} + +.stat-icon i { + font-size: 20px; + color: #fff; +} + +/* 图标背景色 */ +.stat-icon.blue { + background: linear-gradient(135deg, #409EFF, #36D1DC); +} + +.stat-icon.pink { + background: linear-gradient(135deg, #FF9A9E, #FAD0C4); +} + +.stat-icon.cyan { + background: linear-gradient(135deg, #20B2AA, #90EE90); +} + +.stat-icon.green { + background: linear-gradient(135deg, #84FAB0, #8FD3F4); +} + +.stat-icon.purple { + background: linear-gradient(135deg, #E0C3FC, #8EC5FC); +} + +.stat-icon.orange { + background: linear-gradient(135deg, #FFF1B3, #FFD194); +} + +.stat-icon.red { + background: linear-gradient(135deg, #FF6B6B, #FF8E8E); +} + +.stat-icon.brown { + background: linear-gradient(135deg, #D4A373, #E6B17E); +} + +.stat-content { + flex: 1; +} + +.stat-label { + font-size: 13px; + color: #909399; + margin-bottom: 4px; +} + +.stat-value { + font-size: 16px; + font-weight: bold; + color: #303133; +} + +.report-container { + padding: 20px; +} + +.filter-section { + margin-bottom: 20px; +} + +.chart-container { + margin-bottom: 20px; +} + +.table-container { + margin-top: 20px; +} + +.user-header { + text-align: center; + margin: 0 2em; +} + +.user-header .name-box { + margin: 0 auto; + width: fit-content; +} + .user-name { font-size: 18px; line-height: 30px; vertical-align: center; } + .phone-number { font-size: 18px; line-height: 30px; color: #9B9B9B; } -.user-header { - text-align: center; - margin: 0 2em; -} -.user-header .name-box { - margin: 0 auto; - width: fit-content; -} + .user-detail { position: relative; display: flex; height: fit-content; flex-direction: column; } + .user-detail .user-description { flex: 1; margin-top: 16px; } -</style> + +.app-container .box-card:nth-child(n + 1) { + margin-top: 1em; +} +</style> \ No newline at end of file diff --git a/src/views/user/user/index.vue b/src/views/user/user/index.vue index 00f7af1..03bae84 100644 --- a/src/views/user/user/index.vue +++ b/src/views/user/user/index.vue @@ -135,6 +135,7 @@ icon="el-icon-view" @click="handleView(scope.row)" v-hasPermi="['system:smUser:detail']" + v-if="scope.row.userType == '01'" >详情</el-button> <el-button size="mini"