1269 lines
34 KiB
Vue
1269 lines
34 KiB
Vue
<template>
|
||
<div class="store-detail">
|
||
<!-- 基本信息卡片 -->
|
||
<el-card class="info-card" shadow="hover">
|
||
<div slot="header" class="card-header">
|
||
<div class="header-left">
|
||
<span class="title">店铺详情</span>
|
||
<el-tag size="small" :type="getStatusType(storeData.status)">
|
||
{{ getStatusText(storeData.status) }}
|
||
</el-tag>
|
||
</div>
|
||
<div class="action-buttons">
|
||
<el-button
|
||
type="success"
|
||
size="small"
|
||
icon="el-icon-unlock"
|
||
v-if="storeData.gateSn"
|
||
@click="handleOpenGate"
|
||
>开大门</el-button>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
icon="el-icon-setting"
|
||
v-if="storeData.gateSn"
|
||
@click="handleGateConfig"
|
||
>大门配置</el-button>
|
||
<el-button type="success" size="small" @click="handleUpdate">修改</el-button>
|
||
<el-button type="danger" size="small" @click="handleDelete">删除</el-button>
|
||
</div>
|
||
</div>
|
||
<el-row :gutter="20">
|
||
<el-col :span="6">
|
||
<div class="store-image">
|
||
<el-carousel height="200px" indicator-position="outside" :autoplay="true" trigger="click" arrow="always">
|
||
<el-carousel-item v-for="(url, index) in storeData.pictures" :key="index">
|
||
<el-image
|
||
:src="url"
|
||
fit="fill"
|
||
style="width: 100%; height: 100%"
|
||
>
|
||
<div slot="error" class="image-slot">
|
||
<i class="el-icon-picture-outline"></i>
|
||
</div>
|
||
</el-image>
|
||
</el-carousel-item>
|
||
</el-carousel>
|
||
</div>
|
||
</el-col>
|
||
<el-col :span="18">
|
||
<div class="store-title">
|
||
<h2>{{ storeData.name }}</h2>
|
||
<!-- <el-tag :type="getStatusType(storeData.status)" class="status-tag" effect="dark">
|
||
<dict-tag :options="dict.type.ss_store_status" :value="storeData.status" />
|
||
</el-tag> -->
|
||
</div>
|
||
<div class="info-content">
|
||
<el-descriptions :column="3" border size="medium">
|
||
<el-descriptions-item label="联系人">
|
||
<span class="info-text">{{ storeData.contactName || '--' }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="联系电话">
|
||
<span class="info-text">{{ storeData.contactMobile || '--' }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="二维码">
|
||
<el-popover placement="top" trigger="hover">
|
||
<div class="qr-code-box">
|
||
<qr-code :text="getCodeText(storeData)" :width="150" :height="150" />
|
||
<p>扫描二维码进入店铺</p>
|
||
</div>
|
||
<el-button slot="reference" type="text" icon="el-icon-picture">查看</el-button>
|
||
</el-popover>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="商户" class="device-detail2">
|
||
<router-link
|
||
v-if="storeData.merchantId"
|
||
:to="'/user/detail/' + storeData.merchantId"
|
||
class="link-type">
|
||
<span >{{ storeData.merchantName || '--' }}</span>
|
||
</router-link>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="客服电话">
|
||
<span class="info-text">{{ storeData.serverPhone || '--' }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="营业时间">
|
||
<span class="info-text">{{ formatBusinessTime(storeData.businessTimeStart, storeData.businessTimeEnd) }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="身份证号">
|
||
<span class="info-text">{{ storeData.idcard || '--' }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="大门" >
|
||
<router-link
|
||
v-if="storeData.gateSn"
|
||
:to="`/system/deviceDetail/index/${storeData.gateId}`"
|
||
class="link-type"
|
||
>
|
||
{{ storeData.gateSn }}
|
||
</router-link>
|
||
<el-tag
|
||
v-if="storeData.gateSn"
|
||
type="text"
|
||
size="mini"
|
||
effect="dark"
|
||
@click="handleUnbindGate"
|
||
style="cursor: pointer; margin-left: 10px;">解绑
|
||
</el-tag>
|
||
<el-button v-else type="primary" @click="showBindGateDialog = true">去绑定</el-button>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="地址" :span="3">
|
||
<span class="info-text">{{ storeData.address || '--' }}</span>
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="标签" :span="2">
|
||
<dict-tag :options="dict.type.ss_store_tags" :value="storeData.tags" />
|
||
</el-descriptions-item>
|
||
<el-descriptions-item label="类型">
|
||
<dict-tag :options="dict.type.ss_room_type" :value="storeData.typeTags" />
|
||
</el-descriptions-item>
|
||
</el-descriptions>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
</el-card>
|
||
|
||
<!-- 顶部统计卡片 -->
|
||
<el-row :gutter="20" class="stat-cards">
|
||
<!-- 今日营收 -->
|
||
<el-col :span="4">
|
||
<el-card shadow="hover" class="stat-card">
|
||
<div class="stat-icon income">
|
||
<i class="el-icon-money"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">今日营收</div>
|
||
<div class="stat-value">¥ {{ storeData.todayIncome || '0.00' }}</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 本月营收 -->
|
||
<el-col :span="4">
|
||
<el-card shadow="hover" class="stat-card">
|
||
<div class="stat-icon income-month">
|
||
<i class="el-icon-wallet"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">本月营收</div>
|
||
<div class="stat-value">¥ {{ storeData.monthIncome || '0.00' }}</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 总营收 -->
|
||
<el-col :span="4">
|
||
<el-card shadow="hover" class="stat-card">
|
||
<div class="stat-icon total-income">
|
||
<i class="el-icon-bank-card"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">总营收</div>
|
||
<div class="stat-value">¥ {{ storeData.totalIncome || '0.00' }}</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 房间统计 -->
|
||
<el-col :span="4">
|
||
<el-card shadow="hover" class="stat-card">
|
||
<div class="stat-icon room">
|
||
<i class="el-icon-office-building"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">房间使用</div>
|
||
<div class="stat-numbers">
|
||
<span class="current">{{ storeData.usingRoomNum || 0 }}</span>
|
||
<span class="divider">/</span>
|
||
<span class="total">{{ storeData.totalRoomNum || 0 }}</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 设施统计 -->
|
||
<el-col :span="4">
|
||
<el-card shadow="hover" class="stat-card">
|
||
<div class="stat-icon facility">
|
||
<i class="el-icon-s-platform"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">设施使用</div>
|
||
<div class="stat-numbers">
|
||
<span class="current">{{ storeData.usingFacilityNum || 0 }}</span>
|
||
<span class="divider">/</span>
|
||
<span class="total">{{ storeData.totalFacilities || 0 }}</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<!-- 设备数量 -->
|
||
<el-col :span="4">
|
||
<el-card shadow="hover" class="stat-card">
|
||
<div class="stat-icon device">
|
||
<i class="el-icon-cpu"></i>
|
||
</div>
|
||
<div class="stat-content">
|
||
<div class="stat-label">设备数量</div>
|
||
<div class="stat-numbers">
|
||
<span class="total">{{ storeData.totalDeviceNum || 0 }}</span>
|
||
</div>
|
||
</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 图表和地图区域 -->
|
||
<el-row :gutter="20" class="chart-section">
|
||
<el-col :span="16">
|
||
<el-card class="box-card" shadow="hover">
|
||
<div slot="header" class="card-header">
|
||
<span>营收趋势</span>
|
||
<div class="date-range">
|
||
<el-radio-group v-model="dateRange" size="small" @change="handleDateRangeChange">
|
||
<el-radio-button label="7">近7天</el-radio-button>
|
||
<el-radio-button label="30">近30天</el-radio-button>
|
||
</el-radio-group>
|
||
</div>
|
||
</div>
|
||
<div id="incomeChart" style="width: 100%; height: 400px;"></div>
|
||
</el-card>
|
||
</el-col>
|
||
|
||
<el-col :span="8">
|
||
<el-card class="map-card" shadow="hover">
|
||
<div slot="header" class="card-header">
|
||
<span class="title">店铺位置</span>
|
||
</div>
|
||
<div class="map-container" id="mapContainer"></div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-tabs v-model="activeTab" class="detail-tabs">
|
||
<el-tab-pane label="订单列表" name="orders" :lazy="true">
|
||
<order v-if="storeId != null"
|
||
:query="{
|
||
storeId: Number(storeId)
|
||
}"/>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="房间列表" name="rooms" :lazy="true">
|
||
<room :query="{
|
||
storeId: Number(storeId),
|
||
merchantId: storeData.merchantId
|
||
}"/>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="大厅设施" name="equipments" :lazy="true">
|
||
<equipment :query="{
|
||
storeId: Number(storeId),
|
||
merchantId: storeData.merchantId
|
||
}"/>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="设备列表" name="devices" :lazy="true">
|
||
<device :query="{
|
||
storeId: Number(storeId)
|
||
}"/>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="员工列表" name="users" :lazy="true">
|
||
<user :query="{
|
||
storeId: Number(storeId)
|
||
}"/>
|
||
</el-tab-pane>
|
||
<el-tab-pane label="卫生间列表" name="toilets" :lazy="true">
|
||
<toilet :query="{
|
||
storeId: Number(storeId)
|
||
}"/>
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
|
||
<BindGateDialog
|
||
:visible.sync="showBindGateDialog"
|
||
:query="{
|
||
merchantId: storeData.merchantId,
|
||
storeName: storeData.storeName,
|
||
storeId: storeData.storeId
|
||
}"
|
||
@bind-success="getStoreData"
|
||
/>
|
||
|
||
<!-- 添加或修改店铺对话框 -->
|
||
<store-form
|
||
:visible.sync="showStoreForm"
|
||
:query="{
|
||
merchantId: storeData.merchantId,
|
||
isAdmin: isAdmin,
|
||
storeId: currentStoreId
|
||
}"
|
||
@success="handleFormSuccess"
|
||
@cancel="handleFormCancel"
|
||
/>
|
||
|
||
<!-- 大门配置弹窗 -->
|
||
<el-dialog
|
||
title="大门配置"
|
||
:visible.sync="gateConfigVisible"
|
||
width="800px"
|
||
append-to-body
|
||
>
|
||
<el-form ref="gateForm" :model="gateForm" :rules="gateRules" label-width="80px">
|
||
<el-form-item label="开锁方式" prop="unlockMode">
|
||
<el-radio-group v-model="gateForm.unlockMode">
|
||
<el-radio
|
||
v-for="dict in dict.type.ss_unlock_mode"
|
||
:key="dict.value"
|
||
:label="dict.value"
|
||
>{{dict.label}}</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="开锁条件" prop="unlockCondition">
|
||
<el-radio-group v-model="gateForm.unlockCondition">
|
||
<el-radio
|
||
v-for="dict in dict.type.ss_unlock_condition"
|
||
:key="dict.value"
|
||
:label="dict.value"
|
||
>{{dict.label}}</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
<el-form-item label="开锁时长" prop="unlockTime">
|
||
<el-input-number
|
||
v-model="gateForm.unlockTime"
|
||
:min="0"
|
||
:precision="0"
|
||
:step="1"
|
||
controls-position="right"
|
||
style="width: 200px">
|
||
<template slot="append">秒</template>
|
||
</el-input-number>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button @click="gateConfigVisible = false">取 消</el-button>
|
||
<el-button type="primary" @click="submitGateConfig">确 定</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
<script>
|
||
import {
|
||
getStore,
|
||
delStore,
|
||
bindGateApi,
|
||
unbindDevice,
|
||
gateAuth,
|
||
openGate,
|
||
updateStore
|
||
} from "@/api/system/store";
|
||
import {getDevice} from "@/api/system/device";
|
||
import * as echarts from 'echarts';
|
||
import AMapLoader from "@amap/amap-jsapi-loader";
|
||
import globalConfig from '@/utils/config/globalConfig';
|
||
import order from '@/views/system/order/index.vue';
|
||
import room from '@/views/system/room/index.vue';
|
||
import equipment from '@/views/system/hallEqu/index.vue';
|
||
import device from '@/views/system/device/index.vue';
|
||
import user from '@/views/user/user/index.vue';
|
||
import toilet from '@/views/system/toilet/index.vue';
|
||
import BindGateDialog from './components/BindGateDialog.vue';
|
||
import QrCode from '@/components/QrCode';
|
||
import { getDomain } from "@/api/common/common";
|
||
import { deviceSwitch } from "@/api/system/device";
|
||
import StoreForm from "@/views/system/store/components/StoreForm.vue";
|
||
|
||
export default {
|
||
name: 'StoreDetail',
|
||
dicts: ['ss_store_status', 'ss_store_tags', 'ss_room_type', 'ss_unlock_mode', 'ss_unlock_condition'],
|
||
components: {
|
||
StoreForm,
|
||
order,
|
||
room,
|
||
equipment,
|
||
device,
|
||
user,
|
||
toilet,
|
||
BindGateDialog,
|
||
QrCode
|
||
},
|
||
computed: {
|
||
isAdmin() {
|
||
return this.$store.state.user.name === 'admin';
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
storeId: Number(this.$route.params.id),
|
||
storeData: {},
|
||
map: null,
|
||
marker: null,
|
||
stats: {
|
||
todayIncome: 0,
|
||
monthIncome: 0,
|
||
totalIncome: 0,
|
||
totalWithdraw: 0,
|
||
totalRoomNum: 0,
|
||
usingRoomNum: 0,
|
||
totalFacilities: 0,
|
||
usingFacilityNum: 0
|
||
},
|
||
chartTimeRange: 'week',
|
||
incomeChart: null,
|
||
activeTab: 'orders',
|
||
showBindGateDialog: false,
|
||
equipmentList: [],
|
||
domain: '',
|
||
gateConfigVisible: false,
|
||
gateForm: {
|
||
unlockMode: "1",
|
||
unlockCondition: "3",
|
||
unlockTime: 0
|
||
},
|
||
gateRules: {
|
||
unlockMode: [
|
||
{ required: true, message: "请选择开锁方式", trigger: "change" }
|
||
],
|
||
unlockCondition: [
|
||
{ required: true, message: "请选择开锁条件", trigger: "change" }
|
||
],
|
||
unlockTime: [
|
||
{ required: true, message: "请输入开锁时长", trigger: "blur" }
|
||
]
|
||
},
|
||
dateRange: '7', // 默认7天
|
||
showStoreForm: false,
|
||
currentStoreId: null
|
||
}
|
||
},
|
||
created() {
|
||
this.storeId = this.$route.params.storeId;
|
||
this.getStoreData();
|
||
this.loadEquipmentList();
|
||
this.getDicts();
|
||
this.getDomain();
|
||
},
|
||
mounted() {
|
||
this.$nextTick(() => {
|
||
this.initMap();
|
||
this.initChart();
|
||
if (this.$refs.incomeChart) {
|
||
this.$refs.incomeChart.addEventListener('scroll', this.handleScroll);
|
||
}
|
||
});
|
||
// 监听窗口大小变化,重绘图表
|
||
window.addEventListener('resize', () => {
|
||
if (this.incomeChart) {
|
||
this.incomeChart.resize();
|
||
}
|
||
});
|
||
},
|
||
beforeDestroy() {
|
||
if (this.incomeChart) {
|
||
this.incomeChart.dispose();
|
||
}
|
||
if (this.map) {
|
||
this.map.clearMap();
|
||
this.map.destroy();
|
||
this.map = null;
|
||
}
|
||
window.removeEventListener('resize', this.handleResize);
|
||
if (this.$refs.incomeChart) {
|
||
this.$refs.incomeChart.removeEventListener('scroll', this.handleScroll);
|
||
}
|
||
},
|
||
methods: {
|
||
handleScroll() {
|
||
if (!this.$refs.incomeChart || !this.incomeChart) return;
|
||
|
||
const scrollTop = this.$refs.incomeChart.scrollTop;
|
||
const scrollHeight = this.$refs.incomeChart.scrollHeight;
|
||
const clientHeight = this.$refs.incomeChart.clientHeight;
|
||
|
||
if (scrollTop + clientHeight >= scrollHeight) {
|
||
console.log('Reached bottom');
|
||
}
|
||
},
|
||
getStatusType(status) {
|
||
const statusMap = {
|
||
0: 'info',
|
||
1: 'success',
|
||
2: 'danger'
|
||
};
|
||
return statusMap[status] || 'info';
|
||
},
|
||
getStatusText(status) {
|
||
const statusMap = {
|
||
0: '停用',
|
||
1: '正常',
|
||
2: '禁用'
|
||
};
|
||
return statusMap[status] || '未知';
|
||
},
|
||
initChart() {
|
||
if (!this.$refs.incomeChart) return;
|
||
|
||
this.incomeChart = echarts.init(this.$refs.incomeChart);
|
||
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: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||
axisLine: {
|
||
lineStyle: {
|
||
color: '#DCDFE6'
|
||
}
|
||
}
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLabel: {
|
||
formatter: '¥{value}'
|
||
},
|
||
axisLine: {
|
||
show: false
|
||
},
|
||
splitLine: {
|
||
lineStyle: {
|
||
color: '#EBEEF5'
|
||
}
|
||
}
|
||
},
|
||
series: [{
|
||
name: '营收',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: [820, 932, 901, 934, 1290, 1330, 1320],
|
||
itemStyle: {
|
||
color: '#409EFF'
|
||
},
|
||
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.incomeChart.setOption(option);
|
||
|
||
window.addEventListener('resize', this.resizeChart);
|
||
},
|
||
resizeChart() {
|
||
if (this.incomeChart) {
|
||
this.incomeChart.resize();
|
||
}
|
||
},
|
||
getStoreData() {
|
||
// 格式化日期
|
||
const 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}`;
|
||
};
|
||
|
||
// 获取时间范围
|
||
const end = new Date();
|
||
const start = new Date();
|
||
start.setDate(start.getDate() - Number(this.dateRange));
|
||
|
||
// 构建请求参数
|
||
const params = {
|
||
startTime: formatDate(start),
|
||
endTime: formatDate(end)
|
||
};
|
||
|
||
getStore(this.storeId, params).then(response => {
|
||
this.storeData = response.data;
|
||
// 初始化收入趋势图表
|
||
this.$nextTick(() => {
|
||
this.initIncomeChart();
|
||
});
|
||
});
|
||
},
|
||
getStoreStats() {
|
||
// Mock数据,实际项目中替换为API调用
|
||
// this.stats = {
|
||
// todayIncome: 1234.56,
|
||
// monthIncome: 45678.90,
|
||
// totalDeviceNum: 100,
|
||
// onlineDevices: 85
|
||
// };
|
||
},
|
||
handleChartRangeChange(range) {
|
||
// Mock数据,实际项目中替换为API调用
|
||
const mockData = {
|
||
week: {
|
||
xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||
series: [820, 932, 901, 934, 1290, 1330, 1320]
|
||
},
|
||
month: {
|
||
xAxis: Array.from({ length: 30 }, (_, i) => `${i + 1}日`),
|
||
series: Array.from({ length: 30 }, () => Math.floor(Math.random() * 2000 + 500))
|
||
}
|
||
};
|
||
|
||
const data = mockData[range];
|
||
this.incomeChart.setOption({
|
||
xAxis: { data: data.xAxis },
|
||
series: [{ data: data.series }]
|
||
});
|
||
},
|
||
formatBusinessTime(start, end) {
|
||
if (!start && !end) return '--';
|
||
return `${start || '--'} - ${end || '--'}`;
|
||
},
|
||
async initMap() {
|
||
try {
|
||
if (!window._AMapSecurityConfig) {
|
||
window._AMapSecurityConfig = {
|
||
securityJsCode: process.env.VUE_APP_AMAP_SECURITY_CODE,
|
||
}
|
||
}
|
||
|
||
const AMap = await AMapLoader.load({
|
||
key: globalConfig.aMap.key,
|
||
version: "2.0",
|
||
plugins: []
|
||
});
|
||
|
||
if (!document.getElementById('mapContainer')) {
|
||
console.error('地图容器不存在');
|
||
return;
|
||
}
|
||
|
||
this.map = new AMap.Map("mapContainer", {
|
||
zoom: 15,
|
||
center: [
|
||
parseFloat(this.storeData.lng) || 116.397428,
|
||
parseFloat(this.storeData.lat) || 39.90923
|
||
],
|
||
resizeEnable: true,
|
||
viewMode: '2D'
|
||
});
|
||
|
||
// 如果有经纬度,添加标记点
|
||
if (parseFloat(this.storeData.lng) && parseFloat(this.storeData.lat)) {
|
||
// 创建标记点
|
||
this.marker = new AMap.Marker({
|
||
position: [parseFloat(this.storeData.lng), parseFloat(this.storeData.lat)],
|
||
title: this.storeData.name
|
||
});
|
||
|
||
// 将标记点添加到地图
|
||
this.map.add(this.marker);
|
||
|
||
// 创建信息窗体
|
||
const infoWindow = new AMap.InfoWindow({
|
||
content: `
|
||
<div class="info-window">
|
||
<h4>${this.storeData.name || '未命名店铺'}</h4>
|
||
<p>${this.storeData.address || '暂无地址'}</p>
|
||
<p>经度:${this.storeData.lng}</p>
|
||
<p>纬度:${this.storeData.lat}</p>
|
||
</div>
|
||
`,
|
||
offset: new AMap.Pixel(0, -30)
|
||
});
|
||
|
||
// 点击标记点时打开信息窗体
|
||
this.marker.on('click', () => {
|
||
infoWindow.open(this.map, this.marker.getPosition());
|
||
});
|
||
|
||
// 自动适应标记点位置
|
||
this.map.setFitView();
|
||
}
|
||
} catch (e) {
|
||
console.error("地图加载失败", e);
|
||
}
|
||
},
|
||
handleEdit() {
|
||
this.$router.push(`/system/store/edit/${this.storeId}`);
|
||
},
|
||
handleDelete() {
|
||
this.$modal.confirm('是否确认删除该店铺?').then(() => {
|
||
return delStore(this.storeId);
|
||
}).then(() => {
|
||
this.$modal.msgSuccess("删除成功");
|
||
this.$router.push('/system/store/list');
|
||
}).catch(() => { });
|
||
},
|
||
bindGate() {
|
||
// 调用绑定大门的接口
|
||
bindGateApi().then(response => {
|
||
this.$modal.msgSuccess("绑定成功");
|
||
this.fetchStoreDetail(); // 重新获取店铺详情
|
||
}).catch(error => {
|
||
this.$modal.msgError("绑定失败");
|
||
});
|
||
},
|
||
loadEquipmentList() {
|
||
// 实现加载设施列表的逻辑
|
||
// 例如:this.equipmentList = response.data;
|
||
},
|
||
handleUnbindGate() {
|
||
this.$modal.confirm('确定要解除该店铺的大门绑定吗?').then(() => {
|
||
// 调用解绑接口
|
||
unbindDevice(this.storeData.gateId).then(() => {
|
||
this.$modal.msgSuccess("解绑成功");
|
||
this.getStoreData(); // 重新获取店铺数据
|
||
}).catch(() => {
|
||
this.$modal.msgError("解绑失败");
|
||
});
|
||
}).catch(() => {});
|
||
},
|
||
getDomain() {
|
||
getDomain().then(response => {
|
||
if (response.data) {
|
||
this.domain = response.data;
|
||
}
|
||
}).catch(() => {
|
||
this.$message.error("获取全局域名失败")
|
||
})
|
||
},
|
||
getCodeText(store) {
|
||
let url = this.domain + `?d=` + store.qrText;
|
||
return this.domain ? url : '';
|
||
},
|
||
handleGateConfig() {
|
||
this.gateConfigVisible = true;
|
||
},
|
||
submitGateConfig() {
|
||
this.$refs["gateForm"].validate(valid => {
|
||
if (valid) {
|
||
// 先发送开关命令
|
||
if (this.storeData.gateId) {
|
||
// 通电开锁(1)时发送断电命令(false),断电开锁(2)时发送通电命令(true)
|
||
const open = this.gateForm.unlockMode === "2";
|
||
deviceSwitch({
|
||
deviceId: this.storeData.gateId,
|
||
open: open
|
||
}).then(() => {
|
||
// 开关命令成功后,保存配置
|
||
this.saveGateConfig();
|
||
}).catch(() => {
|
||
this.$modal.msgError("设备开关操作失败");
|
||
});
|
||
} else {
|
||
this.$modal.msgError("未找到大门设备");
|
||
}
|
||
}
|
||
});
|
||
},
|
||
/** 保存大门配置 */
|
||
saveGateConfig() {
|
||
gateAuth({
|
||
storeId: this.storeData.storeId,
|
||
unlockMode: this.gateForm.unlockMode,
|
||
unlockCondition: this.gateForm.unlockCondition,
|
||
unlockTime: this.gateForm.unlockTime
|
||
}).then(() => {
|
||
this.$modal.msgSuccess("配置成功");
|
||
this.gateConfigVisible = false;
|
||
this.getStoreData(); // 刷新店铺数据
|
||
});
|
||
},
|
||
handleOpenGate() {
|
||
this.$modal.confirm('是否确认为"' + this.storeData.name + '"开门?').then(() => {
|
||
return openGate(this.storeData.storeId);
|
||
}).then(() => {
|
||
this.$modal.msgSuccess("开门成功");
|
||
}).catch(() => {});
|
||
},
|
||
/** 初始化收入趋势图表 */
|
||
initIncomeChart() {
|
||
if (this.incomeChart) {
|
||
this.incomeChart.dispose();
|
||
}
|
||
|
||
this.incomeChart = echarts.init(document.getElementById('incomeChart'));
|
||
|
||
// 处理数据
|
||
const days = this.storeData.inComeList.map(item => {
|
||
// 将日期格式从 "2025-01-19" 转换为 "01-19"
|
||
return item.day.substring(5); // 从第5个字符开始截取,去掉年份
|
||
});
|
||
const incomes = this.storeData.inComeList.map(item => item.orderIncome);
|
||
const orderNums = this.storeData.inComeList.map(item => item.orderNum || 0);
|
||
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'shadow'
|
||
}
|
||
},
|
||
legend: {
|
||
data: ['营收金额', '订单数量']
|
||
},
|
||
grid: {
|
||
left: '3%',
|
||
right: '4%',
|
||
bottom: '3%',
|
||
containLabel: true
|
||
},
|
||
xAxis: {
|
||
type: 'category',
|
||
data: days,
|
||
axisLabel: {
|
||
rotate: 45
|
||
}
|
||
},
|
||
yAxis: [
|
||
{
|
||
type: 'value',
|
||
name: '营收金额(元)',
|
||
position: 'left'
|
||
},
|
||
{
|
||
type: 'value',
|
||
name: '订单数量',
|
||
position: 'right'
|
||
}
|
||
],
|
||
series: [
|
||
{
|
||
name: '营收金额',
|
||
type: 'line',
|
||
smooth: true,
|
||
data: incomes,
|
||
itemStyle: {
|
||
color: '#409EFF'
|
||
}
|
||
},
|
||
{
|
||
name: '订单数量',
|
||
type: 'bar',
|
||
yAxisIndex: 1,
|
||
data: orderNums,
|
||
itemStyle: {
|
||
color: '#67C23A'
|
||
}
|
||
}
|
||
]
|
||
};
|
||
|
||
this.incomeChart.setOption(option);
|
||
},
|
||
/** 切换日期范围 */
|
||
handleDateRangeChange(days) {
|
||
this.dateRange = days;
|
||
const end = new Date();
|
||
const start = new Date();
|
||
start.setDate(start.getDate() - days);
|
||
|
||
// 格式化日期
|
||
const 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}`;
|
||
};
|
||
|
||
// 使用 getStoreData 方法获取数据
|
||
this.getStoreData({
|
||
startTime: formatDate(start),
|
||
endTime: formatDate(end)
|
||
});
|
||
},
|
||
handleFormSuccess() {
|
||
this.getList();
|
||
},
|
||
handleFormCancel() {
|
||
this.showStoreForm = false;
|
||
},
|
||
/** 修改按钮操作 */
|
||
handleUpdate() {
|
||
this.currentStoreId = this.storeId;
|
||
console.log("this.currentStoreId----------"+this.currentStoreId)
|
||
this.showStoreForm = true;
|
||
},
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.store-detail {
|
||
padding: 20px;
|
||
background-color: #f5f7fa;
|
||
min-height: calc(100vh - 84px);
|
||
|
||
.el-row {
|
||
height: 100%;
|
||
|
||
.el-col {
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.store-image {
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
transition: all 0.3s ease;
|
||
height: 200px;
|
||
|
||
&:hover {
|
||
transform: translateY(-5px);
|
||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
:deep(.el-carousel) {
|
||
.el-carousel__indicators {
|
||
bottom: -20px;
|
||
}
|
||
|
||
.el-carousel__arrow {
|
||
background-color: rgba(0, 0, 0, 0.3);
|
||
|
||
&:hover {
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
}
|
||
}
|
||
|
||
.el-carousel__item {
|
||
overflow: hidden;
|
||
|
||
.el-image {
|
||
width: 100%;
|
||
height: 100%;
|
||
display: block;
|
||
|
||
.image-slot {
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: #f5f7fa;
|
||
color: #909399;
|
||
font-size: 30px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: calc(100% - 80px); // 减去标题的高度
|
||
|
||
:deep(.el-descriptions) {
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.el-descriptions__body {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
|
||
.el-descriptions-item {
|
||
flex: 1;
|
||
display: flex;
|
||
|
||
.el-descriptions-item__container {
|
||
flex: 1;
|
||
display: flex;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.store-title {
|
||
display: flex;
|
||
align-items: center;
|
||
padding-bottom: 16px;
|
||
height: 40px;
|
||
|
||
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: 12px;
|
||
}
|
||
}
|
||
|
||
.info-card {
|
||
margin-bottom: 20px;
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.status-tag {
|
||
margin-left: 8px;
|
||
}
|
||
}
|
||
|
||
.action-buttons {
|
||
.el-button {
|
||
margin-left: 10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-item {
|
||
margin-bottom: 16px;
|
||
display: flex;
|
||
align-items: center;
|
||
|
||
.label {
|
||
color: #909399;
|
||
margin-right: 12px;
|
||
min-width: 80px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.value {
|
||
color: #303133;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-tabs {
|
||
margin-top: 20px;
|
||
|
||
.search-form {
|
||
margin-bottom: 20px;
|
||
|
||
.el-form-item {
|
||
margin-bottom: 10px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.stat-cards {
|
||
margin-bottom: 20px;
|
||
|
||
.stat-card {
|
||
height: 120px;
|
||
display: flex;
|
||
padding: 0 10px;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.stat-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 8px;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
margin-right: 16px;
|
||
|
||
i {
|
||
font-size: 24px;
|
||
color: #fff;
|
||
}
|
||
|
||
&.income {
|
||
background: linear-gradient(135deg, #36d1dc, #5b86e5);
|
||
}
|
||
|
||
&.income-month {
|
||
background: linear-gradient(135deg, #ff9a9e, #fad0c4);
|
||
}
|
||
|
||
&.total-income {
|
||
background: linear-gradient(135deg, #a8edea, #fed6e3);
|
||
}
|
||
|
||
&.withdraw {
|
||
background: linear-gradient(135deg, #84fab0, #8fd3f4);
|
||
}
|
||
|
||
&.room {
|
||
background: linear-gradient(135deg, #fbc2eb, #a6c1ee);
|
||
}
|
||
|
||
&.facility {
|
||
background: linear-gradient(135deg, #f6d365, #fda085);
|
||
}
|
||
|
||
&.device {
|
||
background: linear-gradient(135deg, #84fab0, #8fd3f4);
|
||
}
|
||
}
|
||
|
||
.stat-content {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
|
||
.stat-value {
|
||
font-size: 22px;
|
||
font-weight: bold;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.stat-numbers {
|
||
font-size: 22px;
|
||
font-weight: bold;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
line-height: 1.2;
|
||
|
||
.current {
|
||
color: #409EFF;
|
||
}
|
||
|
||
.divider {
|
||
margin: 0 4px;
|
||
color: #909399;
|
||
}
|
||
|
||
.total {
|
||
color: #606266;
|
||
}
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.chart-section {
|
||
margin-bottom: 20px;
|
||
|
||
.card-header {
|
||
.title {
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.chart-card {
|
||
.chart-container {
|
||
height: 350px;
|
||
padding: 10px;
|
||
}
|
||
}
|
||
|
||
.map-card {
|
||
.map-container {
|
||
height: 350px;
|
||
width: 100%;
|
||
position: relative;
|
||
border: 1px solid #EBEEF5;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
:deep(.info-window) {
|
||
padding: 12px;
|
||
min-width: 200px;
|
||
|
||
h4 {
|
||
margin: 0 0 8px;
|
||
color: #303133;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
p {
|
||
margin: 4px 0;
|
||
color: #606266;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
}
|
||
}
|
||
|
||
:deep(.el-tag) {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.link-type {
|
||
color: #409EFF;
|
||
text-decoration: underline;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.link-type:hover {
|
||
color: #66b1ff;
|
||
}
|
||
|
||
.device-detail2 {
|
||
padding: 20px;
|
||
|
||
.link-type {
|
||
color: #11A983;
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.link-type:hover {
|
||
text-decoration: underline;
|
||
}
|
||
}
|
||
|
||
.qr-code-box {
|
||
text-align: center;
|
||
padding: 10px;
|
||
|
||
p {
|
||
margin: 10px 0 0;
|
||
color: #666;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.card-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.date-range {
|
||
.el-radio-group {
|
||
margin-left: 16px;
|
||
}
|
||
}
|
||
|
||
</style>
|