share-space-vue/src/views/system/store/store_detail.vue
2025-01-21 16:56:57 +08:00

992 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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="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="chart-card" shadow="hover">
<div slot="header" class="card-header">
<span class="title">营收趋势</span>
<el-radio-group v-model="chartTimeRange" size="small" @change="handleChartRangeChange">
<el-radio-button label="week">近7天</el-radio-button>
<el-radio-button label="month">近30天</el-radio-button>
</el-radio-group>
</div>
<div class="chart-container" ref="incomeChart"></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">
<order :storeId="Number(storeId)"></order>
</el-tab-pane>
<el-tab-pane label="房间列表" name="rooms">
<room :storeId="Number(storeId)" :merchantId="storeData.merchantId"></room>
</el-tab-pane>
<el-tab-pane label="设施列表" name="equipments">
<equipment :storeId="Number(storeId)" :merchantId="storeData.merchantId" />
</el-tab-pane>
<el-tab-pane label="设备列表" name="devices">
<device :storeId="Number(storeId)"></device>
</el-tab-pane>
<el-tab-pane label="员工列表" name="users">
<user :storeId="Number(storeId)"></user>
</el-tab-pane>
<el-tab-pane label="卫生间列表" name="toilets">
<toilet :storeId="Number(storeId)"></toilet>
</el-tab-pane>
</el-tabs>
<BindGateDialog
:visible.sync="showBindGateDialog"
:storeName="storeData.storeName"
:storeId="Number(storeId)"
@bind-success="getStoreData"
/>
</div>
</template>
<script>
import {getStore, delStore, bindGateApi, unbindDevice} from "@/api/system/store";
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";
export default {
name: 'StoreDetail',
dicts: ['ss_store_tags', 'ss_room_type'],
components: {
order,
room,
equipment,
device,
user,
toilet,
BindGateDialog,
QrCode
},
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: ''
}
},
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);
}
});
},
beforeDestroy() {
if (this.incomeChart) {
this.incomeChart.dispose();
}
if (this.map) {
this.map.clearMap();
this.map.destroy();
this.map = null;
}
window.removeEventListener('resize', this.resizeChart);
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() {
getStore(this.storeId).then(response => {
this.storeData = response.data;
// 重新初始化地图
if (this.map) {
this.map.clearMap();
this.map.destroy();
this.map = null;
}
this.$nextTick(() => {
this.initMap();
});
});
},
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 + `?storeId=` + store.qrText;
return this.domain ? url : '';
}
}
}
</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;
}
}
:deep(.el-descriptions) {
padding: 20px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
.el-descriptions__label {
font-weight: 500;
color: #606266;
min-width: 90px;
}
.el-descriptions__content {
padding: 16px;
}
.el-descriptions__body {
background-color: #fff;
}
.el-descriptions-item__label {
background-color: #f5f7fa;
font-weight: 500;
}
.el-descriptions-item__content {
display: flex;
align-items: center;
}
}
.info-text {
color: #606266;
font-size: 14px;
}
.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;
}
}
</style>