Merge remote-tracking branch 'origin/master'

This commit is contained in:
SjS 2025-04-04 18:26:22 +08:00
commit 5d9ee85212
16 changed files with 829 additions and 213 deletions

View File

@ -0,0 +1,14 @@
import request from '@/utils/request'
/**
* 获取统计数据
* @param {Object} query 查询参数
* @returns {Promise} 返回统计数据
*/
export function getStat(query) {
return request({
url: '/dashboard/stat',
method: 'get',
params: query
})
}

View File

@ -0,0 +1,147 @@
<template>
<el-select
v-model="selectedValue"
placeholder="请选择"
filterable
:multiple="multiple"
:loading="loading"
@change="handleChange"
@visible-change="handleVisibleChange"
remote
:remote-method="remoteMethod"
>
<div class="select-header">
<div>
{{ total }}条数据
</div>
<el-button v-if="multiple && !isEmpty(options)" style="margin-left: 10px;" size="mini" type="text" @click.stop="handleSelectAll">
{{ isAllSelected ? '取消全选' : '全选' }}
</el-button>
</div>
<el-option
v-for="item in options"
:key="item[prop]"
:value="item[prop]"
:label="item[showProp]"
/>
<el-option v-if="isEmpty(value) && isEmpty(options)" :label="emptyText" disabled :value="null"></el-option>
</el-select>
</template>
<script>
import { isEmpty } from '@/utils';
import { $remoteSelect } from '@/components/BaseRemoteSelect/mixins';
export default {
name: 'BaseRemoteSelect',
mixins: [$remoteSelect],
data() {
return {
options: [],
total: 0,
loading: false,
queryParams: {
pageNum: 1,
pageSize: 100
}
}
},
computed: {
selectedValue: {
get() {
return this.value
},
set(value) {
this.$emit('input', value)
}
},
//
isAllSelected() {
return this.multiple && this.options.length > 0 && Array.isArray(this.value) && this.value.length === this.options.length;
}
},
created() {
this.queryParams[this.keywordProp] = null;
// 使API
if (this.loadApi) {
this.loadData();
}
// 使
else if (!isEmpty(this.initOptions)) {
this.options = this.initOptions;
}
},
methods: {
isEmpty,
//
loadData() {
this.loading = true;
this.loadApi(this.value).then(res => {
this.options = res.data;
this.total = this.options?.length;
}).finally(() => {
this.loading = false;
});
},
//
remoteMethod(val) {
this.queryParams.keyword = val;
this.getOptions();
},
//
handleVisibleChange(visible) {
if (visible) {
this.getOptions();
}
},
//
handleSelectAll() {
if (this.isAllSelected) {
this.handleChange([]);
} else {
const all = this.options.map(item => item[this.prop]);
this.handleChange(all);
}
},
// change
handleChange(value) {
if (this.multiple) {
let list = this.options.filter(item => value.includes(item[this.prop]));
this.$emit('change', list);
} else {
let item = this.options.find(item => value.includes(item[this.prop]));
this.$emit('change', item);
}
},
//
getOptions() {
console.log("getOptions", this.beforeGetOptions());
if (!this.beforeGetOptions()) {
return;
}
this.loading = true;
Object.assign(this.queryParams, this.query);
this.listApi(this.queryParams).then(res => {
this.options = res.rows;
this.total = res.total;
}).finally(() => {
this.loading = false;
});
}
}
}
</script>
<style lang="scss" scoped>
.select-header {
padding: 2px 12px;
border-bottom: 1px solid #EBEEF5;
display: flex;
justify-content: flex-end;
align-items: center;
line-height: 1em;
text-align: center;
color: #8492a6;
font-size: 13px;
}
</style>

View File

@ -0,0 +1,59 @@
export const $remoteSelect = {
props: {
// 选中值
value: {
type: [String, Array],
default: null
},
// 自定义查询参数
query: {
type: Object,
default: () => ({})
},
// 是否多选
multiple: {
type: Boolean,
default: false
},
// 初始化选项
initOptions: {
type: Array,
default: () => []
},
// 选中值的属性
prop: {
type: String,
default: 'id'
},
// 展示值的属性
showProp: {
type: String,
default: 'name'
},
// 搜索关键字属性
keywordProp: {
type: String,
default: 'keyword'
},
// 列表接口
listApi: {
type: Function,
default: () => {}
},
// 加载接口
loadApi: {
type: Function,
default: null
},
// 空数据文本
emptyText: {
type: String,
default: '暂无数据'
},
// 获取选项前回调
beforeGetOptions: {
type: Function,
default: () => {return true;}
}
},
}

View File

@ -1,152 +1,29 @@
<template>
<el-select
v-model="selectedValue"
placeholder="请选择"
filterable
<base-remote-select
prop="id"
show-prop="name"
:list-api="listArea"
:value="value"
:query="query"
:multiple="multiple"
:loading="loading"
@change="handleChange"
@visible-change="handleVisibleChange"
remote
:remote-method="remoteMethod"
>
<div class="select-footer">
<div style="text-align: center; color: #8492a6; font-size: 13px; ">
{{ total }}条数据
</div>
<el-button v-if="multiple && !isEmpty(options)" style="margin-left: 10px;" size="mini" type="text" @click.stop="handleSelectAll">
{{ isAllSelected ? '取消全选' : '全选' }}
</el-button>
</div>
<el-option
v-for="item in options"
:key="item.id"
:value="item.id"
:label="item.name"
/>
<el-option v-if="isEmpty(value) && isEmpty(options)" style="display:none" disabled :value="null"></el-option>
</el-select>
:init-options="initOptions"
v-on="$listeners"
/>
</template>
<script>
import { listArea } from '@/api/bst/area';
import Avatar from '@/components/Avatar';
import {isEmpty} from '@/utils'
import { $remoteSelect } from '@/components/BaseRemoteSelect/mixins';
import BaseRemoteSelect from '@/components/BaseRemoteSelect';
export default {
name: 'AreaRemoteSelect',
mixins: [$remoteSelect],
components: {
Avatar
},
props: {
// id
value: {
type: [String, Array],
default: null
},
//
query: {
type: Object,
default: () => ({})
},
//
multiple: {
type: Boolean,
default: false
},
//
initOptions: {
type: Array,
default: () => []
}
},
data() {
return {
options: [],
total: 0,
loading: false,
queryParams: {
pageNum: 1,
pageSize: 100,
keyword: null
}
}
},
computed: {
selectedValue: {
get() {
return this.value
},
set(value) {
this.$emit('input', value)
}
},
//
isAllSelected() {
return this.multiple && this.options.length > 0 && Array.isArray(this.value) && this.value.length === this.options.length;
}
},
created() {
if (!isEmpty(this.initOptions)) {
this.options = this.initOptions;
}
BaseRemoteSelect
},
methods: {
isEmpty,
//
remoteMethod(val) {
this.queryParams.keyword = val;
this.getOptions();
},
//
handleVisibleChange(visible) {
if (visible) {
this.getOptions();
}
},
//
handleSelectAll() {
if (this.isAllSelected) {
this.handleChange([]);
} else {
const allids = this.options.map(item => item.id);
this.handleChange(allids);
}
},
handleChange(value) {
if (this.multiple) {
let list = this.options.filter(item => value.includes(item.id));
this.$emit('change', list);
} else {
let item = this.options.find(item => value.includes(item.id));
this.$emit('change', item);
}
},
//
getOptions() {
this.loading = true;
this.queryParams = {
...this.queryParams,
...this.query
}
listArea(this.queryParams).then(res => {
this.options = res.rows;
this.total = res.total;
}).finally(() => {
this.loading = false;
});
}
listArea
}
}
</script>
<style lang="scss" scoped>
.select-footer {
padding: 2px 12px;
border-bottom: 1px solid #EBEEF5;
display: flex;
justify-content: flex-end;
align-items: center;
line-height: 1em;
}
</style>
</script>

View File

@ -0,0 +1,30 @@
<template>
<base-remote-select
prop="id"
show-prop="name"
:list-api="listSuit"
:value="value"
:query="query"
:multiple="multiple"
:init-options="initOptions"
:before-get-options="beforeGetOptions"
v-on="$listeners"
/>
</template>
<script>
import { listSuit } from '@/api/bst/suit';
import { $remoteSelect } from '@/components/BaseRemoteSelect/mixins';
import BaseRemoteSelect from '@/components/BaseRemoteSelect';
export default {
name: 'SuitRemoteSelect',
mixins: [$remoteSelect],
components: {
BaseRemoteSelect
},
methods: {
listSuit
}
}
</script>

View File

@ -1,9 +1,10 @@
<template>
<div class="statistics-card">
<div class="statistics-card" :style="cardStyle">
<div class="card-content">
<div class="info">
<div class="value">{{ value }}</div>
<div class="label">{{ label }}</div>
<div class="value" :style="valueStyle">{{ value }}</div>
<div class="label" :style="labelStyle">{{ label }}</div>
<div v-if="subtitle" class="subtitle" :style="subtitleStyle">{{ subtitle }}</div>
</div>
<div class="icon-wrapper">
<i :class="icon" :style="iconStyle"></i>
@ -24,6 +25,10 @@ export default {
type: String,
required: true
},
subtitle: {
type: String,
default: ''
},
icon: {
type: String,
required: true
@ -35,15 +40,64 @@ export default {
endColor: {
type: String,
required: true
},
width: {
type: [Number, String],
default: '100%'
},
height: {
type: [Number, String],
default: 120
}
},
computed: {
//
sizeInPx() {
const width = typeof this.width === 'number' ? `${this.width}px` : this.width;
const height = typeof this.height === 'number' ? `${this.height}px` : this.height;
return { width, height };
},
//
fontSizes() {
const height = typeof this.height === 'number' ? this.height : parseInt(this.height);
const baseHeight = 120; //
const scale = height / baseHeight;
return {
value: Math.max(16, Math.round(28 * scale)),
label: Math.max(12, Math.round(16 * scale)),
subtitle: Math.max(10, Math.round(12 * scale)),
icon: Math.max(32, Math.round(64 * scale))
};
},
cardStyle() {
return {
background: `linear-gradient(135deg, ${this.startColor}15, ${this.endColor}15)`,
width: this.sizeInPx.width,
height: this.sizeInPx.height
}
},
iconStyle() {
return {
background: `linear-gradient(135deg, ${this.startColor}, ${this.endColor})`,
'-webkit-background-clip': 'text',
'-webkit-text-fill-color': 'transparent',
'background-clip': 'text'
'background-clip': 'text',
'font-size': `${this.fontSizes.icon}px`
}
},
valueStyle() {
return {
'font-size': `${this.fontSizes.value}px`
}
},
labelStyle() {
return {
'font-size': `${this.fontSizes.label}px`
}
},
subtitleStyle() {
return {
'font-size': `${this.fontSizes.subtitle}px`
}
}
}
@ -52,14 +106,13 @@ export default {
<style lang="scss" scoped>
.statistics-card {
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0,0,0,.1);
padding: 12px;
height: 70px;
border-radius: 8px;
padding: 20px;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
backdrop-filter: blur(5px);
box-sizing: border-box;
&:hover {
transform: translateY(-2px);
@ -70,33 +123,39 @@ export default {
position: relative;
height: 100%;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: center;
.info {
position: relative;
z-index: 2;
.value {
font-size: 18px;
font-weight: bold;
color: #303133;
line-height: 1.2;
margin-bottom: 4px;
margin-bottom: 8px;
}
.label {
font-size: 12px;
color: #606266;
margin-bottom: 4px;
}
.subtitle {
color: #909399;
opacity: 0.8;
}
}
.icon-wrapper {
position: absolute;
right: -8px;
bottom: -12px;
right: -15px;
bottom: -20px;
z-index: 1;
i {
font-size: 48px;
opacity: 0.15;
transition: all 0.3s ease;
}
@ -106,7 +165,7 @@ export default {
&:hover {
.icon-wrapper i {
opacity: 0.25;
transform: scale(1.05);
transform: scale(1.05) rotate(5deg);
}
}
}

View File

@ -141,31 +141,30 @@ export const OrderStatus = {
FINISHED: "FINISHED", // 已结束
CANCEL: "CANCEL", // 已取消
WAIT_VERIFY: "WAIT_VERIFY", // 待审核
// 允许支付的订单状态
canPay() {
return [this.WAIT_PAY];
},
// 正在使用中的订单状态
inUse() {
return [this.PROCESSING];
},
// 可以支付成功的订单状态
canPaySuccess() {
return [this.WAIT_PAY];
},
// 可以结束的订单状态
canEnd() {
return [this.PROCESSING];
},
// 可以退款的订单状态
canRefund() {
return [this.FINISHED];
}
},
// 可以审核的订单状态
canVerify() {
return [this.WAIT_VERIFY];
},
}
// 支付业务类型
@ -192,3 +191,25 @@ export const BonusArrivalType = {
}
}
// 统计数据键值
export const StatKeys = {
ORDER_USER_COUNT: "order_user_count", // 累计订单用户
ORDER_COUNT: "order_count", // 订单数量
ORDER_PAY_AMOUNT: "order_pay_amount", // 订单支付金额
ORDER_REFUNDED_AMOUNT: "order_refunded_amount", // 订单已退款金额
ORDER_REFUNDING_AMOUNT: "order_refunding_amount", // 订单退款中金额
ORDER_TODAY_PAY_AMOUNT: "order_today_pay_amount", // 订单今日支付金额
ORDER_TODAY_REFUND_AMOUNT: "order_today_refund_amount", // 订单今日退款金额
ORDER_TODAY_REFUNDING_AMOUNT: "order_today_refunding_amount", // 订单今日退款中金额
BONUS_COUNT: "bonus_count", // 分成数量
BONUS_AMOUNT: "bonus_amount", // 分成总金额
BONUS_REFUND_AMOUNT: "bonus_refund_amount", // 分成总退款
USER_COUNT: "user_count", // 用户数量
USER_TODAY_COUNT: "user_today_count", // 今日新增用户数量
USER_BALANCE: "user_balance", // 用户余额
DEVICE_COUNT: "device_count", // 设备数量
DEVICE_STATUS_COUNT: "device_status_count", // 设备状态数量
DEVICE_ONLINE_STATUS_COUNT: "device_online_status_count", // 设备在线状态数量
AREA_COUNT: "area_count", // 运营区数量
MODEL_COUNT: "model_count", // 型号数量
}

View File

@ -17,8 +17,8 @@
<form-col :span="span" label="联系人" prop="contact">
<el-input v-model="form.contact" placeholder="请输入联系人" />
</form-col>
<form-col :span="span" label="联系联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系联系方式" />
<form-col :span="span" label="联系方式" prop="phone">
<el-input v-model="form.phone" placeholder="请输入联系方式" />
</form-col>
<form-col :span="span" label="状态" prop="status">
<el-radio-group v-model="form.status">

View File

@ -93,7 +93,7 @@
<el-card class="box-card" v-if="detail.id">
<el-tabs lazy>
<el-tab-pane label="设备轨迹">
<device-location :query="{ mac: detail.mac }" :area-id="detail.areaId" />
<device-location :query="{ eqMac: detail.mac }" :area-id="detail.areaId" />
</el-tab-pane>
<el-tab-pane label="命令日志">
<command-log :query="{ eqMac: detail.mac }" />

View File

@ -1,15 +1,241 @@
<template>
<div>
<div class="app-container" style="min-height: 600px;" v-loading="loading">
<el-row :gutter="20" v-if="stat">
<el-col :span="18">
<el-row :gutter="10">
<!-- 用户相关统计 -->
<el-col :span="span">
<statistics-card
:value="stat.user.balance"
label="用户总余额"
icon="el-icon-wallet"
start-color="#1890FF"
end-color="#36CBCB"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.user.count"
label="用户总数"
icon="el-icon-user"
start-color="#722ED1"
end-color="#EB2F96"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.user.todayCount"
label="今日新增用户"
icon="el-icon-user-plus"
start-color="#FA541C"
end-color="#FAAD14"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.area.count"
label="运营区数量"
icon="el-icon-location"
start-color="#13C2C2"
end-color="#52C41A"
/>
</el-col>
</el-row>
<el-row :gutter="10" style="margin-top: 10px;">
<!-- 订单相关统计 -->
<el-col :span="span">
<statistics-card
:value="formatAmount(stat.order.payAmount - (stat.order.refundingAmount) - (stat.order.refundedAmount))"
label="订单总收入"
icon="el-icon-money"
start-color="#FF4D4F"
end-color="#FF7A45"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="formatAmount(stat.order.todayPayAmount - (stat.order.todayRefundingAmount) - (stat.order.todayRefundedAmount))"
label="今日订单收入"
icon="el-icon-date"
start-color="#2F54EB"
end-color="#597EF7"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.device.count"
label="车辆总数"
icon="el-icon-bicycle"
start-color="#52C41A"
end-color="#73D13D"
/>
</el-col>
<el-col :span="span">
<statistics-card
:value="stat.model.count"
label="车型总数"
icon="el-icon-menu"
start-color="#722ED1"
end-color="#85A5FF"
/>
</el-col>
</el-row>
<!-- 车辆状态统计 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>车辆状态统计</span>
</div>
<el-row :gutter="20">
<el-col :span="4" v-for="status in deviceStatusList" :key="status">
<div class="status-item">
<div class="status-count">{{ stat.device.statusCount[status] || 0 }}</div>
<div class="status-label">{{ getDeviceStatusLabel(status) }}</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
<!-- 车辆在线状态统计 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="24">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>车辆在线状态统计</span>
</div>
<el-row :gutter="20">
<el-col :span="12" v-for="status in ['1', '0']" :key="status">
<div class="status-item">
<div class="status-count">{{ stat.device.onlineStatusCount[status] || 0 }}</div>
<div class="status-label">{{ status === '1' ? '在线' : '离线' }}</div>
</div>
</el-col>
</el-row>
</el-card>
</el-col>
</el-row>
</el-col>
<el-col :span="6">
</el-col>
</el-row>
</div>
</template>
<script>
import { getStat } from '@/api/dashboard/dashboard'
import { StatKeys } from '@/utils/enums'
import { DeviceStatus } from '@/utils/enums'
import StatisticsCard from '@/components/StatisticsCard'
export default {
name: 'Index',
components: {
StatisticsCard
},
data() {
return {
span: 6,
loading: false,
stat: null,
deviceStatusList: [
DeviceStatus.STORAGE, //
DeviceStatus.AVAILABLE, //
DeviceStatus.RESERVED, //
DeviceStatus.IN_USE, //
DeviceStatus.TEMP_LOCKED, //
DeviceStatus.DISPATCHING, //
DeviceStatus.DISABLED //
],
queryParams: {
keys: [
StatKeys.ORDER_COUNT,
StatKeys.ORDER_PAY_AMOUNT,
StatKeys.ORDER_REFUNDED_AMOUNT,
StatKeys.ORDER_REFUNDING_AMOUNT,
StatKeys.ORDER_TODAY_PAY_AMOUNT,
StatKeys.ORDER_TODAY_REFUND_AMOUNT,
StatKeys.ORDER_TODAY_REFUNDING_AMOUNT,
StatKeys.BONUS_COUNT,
StatKeys.BONUS_AMOUNT,
StatKeys.BONUS_REFUND_AMOUNT,
StatKeys.USER_COUNT,
StatKeys.USER_TODAY_COUNT,
StatKeys.USER_BALANCE,
StatKeys.DEVICE_COUNT,
StatKeys.DEVICE_STATUS_COUNT,
StatKeys.DEVICE_ONLINE_STATUS_COUNT,
StatKeys.AREA_COUNT,
StatKeys.MODEL_COUNT
]
}
}
},
created() {
this.getStat()
},
methods: {
getStat() {
this.loading = true;
getStat(this.queryParams).then(res => {
this.stat = res.data
}).finally(() => {
this.loading = false;
})
},
formatAmount(amount) {
return Number(amount).toFixed(2)
},
getDeviceStatusLabel(status) {
const statusMap = {
[DeviceStatus.STORAGE]: '仓库中',
[DeviceStatus.AVAILABLE]: '待骑行',
[DeviceStatus.RESERVED]: '预约中',
[DeviceStatus.IN_USE]: '骑行中',
[DeviceStatus.TEMP_LOCKED]: '临时锁车',
[DeviceStatus.DISPATCHING]: '调度中',
[DeviceStatus.DISABLED]: '禁用'
}
return statusMap[status] || '未知状态'
}
}
}
</script>
<style lang="scss" scoped>
.el-row {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
.box-card {
.status-item {
text-align: center;
padding: 15px;
background: #f5f7fa;
border-radius: 4px;
margin-bottom: 10px;
.status-count {
font-size: 24px;
font-weight: bold;
color: #303133;
margin-bottom: 8px;
}
.status-label {
font-size: 14px;
color: #606266;
}
}
}
</style>

View File

@ -47,12 +47,13 @@
</el-input>
</form-col>
<form-col :span="span" label="套餐" prop="suitIds">
<suit-input
<suit-remote-select
style="width: 100%;"
v-model="form.suitIds"
:text="suitNames"
multiple
:query="suitQuery"
:before-open="beforeOpenSuit"
multiple
:init-options="initSuitOptions"
:before-get-options="beforeOpenSuit"
/>
</form-col>
</el-row>
@ -72,6 +73,7 @@ import AreaRemoteSelect from '@/components/Business/Area/AreaRemoteSelect.vue';
import { RoleKeys } from '@/utils/enums';
import SuitInput from '@/components/Business/Suit/SuitInput.vue';
import { mapGetters } from 'vuex';
import SuitRemoteSelect from '@/components/Business/Suit/SuitRemoteSelect.vue';
export default {
name: "ModelEditDialog",
@ -79,7 +81,8 @@ export default {
FormCol,
UserInput,
AreaRemoteSelect,
SuitInput
SuitInput,
SuitRemoteSelect
},
props: {
visible: {
@ -131,7 +134,7 @@ export default {
}
},
computed: {
...mapGetters(['userId']),
...mapGetters(['userId', 'nickName']),
dialogVisible: {
get() {
return this.visible;
@ -146,11 +149,15 @@ export default {
userId: this.form.userId
}
},
suitNames() {
//
initSuitOptions() {
if (this.form.suitList == null || this.form.suitList.length === 0) {
return "";
return [];
}
return this.form.suitList.map(item => item.name).join(',');
return this.form.suitList.map(item => ({
id: item.id,
name: item.name
}));
}
},
methods: {
@ -161,6 +168,7 @@ export default {
this.$message.warning("由于更换了所属用户,套餐数据已清空");
}
},
//
beforeOpenSuit() {
if (this.form.userId == null) {
this.$modal.msgError("请先选择所属用户");
@ -194,7 +202,7 @@ export default {
// dto
suitIds: [],
// vo
suitNames: null,
userName: this.nickName,
//
...this.initData
};

View File

@ -0,0 +1,115 @@
<template>
<el-dialog
title="审核"
:visible.sync="dialogVisible"
width="500px"
append-to-body
@open="handleOpen"
>
<el-form :model="form" :rules="rules" ref="form" label-width="6em" v-loading="loading" size="small">
<el-form-item label="审核意见" prop="remark">
<el-input v-model="form.remark" type="textarea" placeholder="请输入审核意见" show-word-limit maxlength="200" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="handleSubmit(true)">通过</el-button>
<el-button type="danger" @click="handleSubmit(false)">驳回</el-button>
</div>
</el-dialog>
</template>
<script>
import {getOrder, verifyOrder} from '@/api/bst/order'
export default {
props: {
id: {
type: String,
default: ''
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
detail: {},
form: {},
loading: false,
rules: {
remark: [
{ required: true, message: '请输入备注', trigger: 'blur' },
],
}
}
},
computed: {
dialogVisible: {
get() {
return this.visible;
},
set(val) {
this.$emit('update:visible', val);
}
},
// 退
canRefundAmount() {
let payAmount = this.detail.payAmount || 0;
let payRefunded = this.detail.payRefunded || 0;
let payRefunding = this.detail.payRefunding || 0;
return payAmount - payRefunded - payRefunding;
}
},
methods: {
getDetail() {
this.loading = true;
getOrder(this.id).then(response => {
this.detail = response.data;
this.form.amount = this.canRefundAmount;
}).finally(() => {
this.loading = false;
});
},
handleOpen() {
this.getDetail();
this.reset();
},
reset() {
this.form = {
id: this.id,
pass: null,
remark: null,
};
this.resetForm('form');
},
handleSubmit(pass) {
this.$refs.form.validate().then(() => {
this.$confirm(`确定${pass ? '通过' : '驳回'}吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.form.pass = pass;
this.loading = true;
verifyOrder(this.form).then((response) => {
if (response.code == 200) {
this.$message.success('操作成功');
this.dialogVisible = false;
this.$emit('success');
}
}).finally(() => {
this.loading = false;
});
});
});
}
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -231,6 +231,14 @@
v-has-permi="['bst:order:refund']"
v-show="OrderStatus.canRefund().includes(scope.row.status)"
>退款</el-button>
<el-button
size="mini"
type="text"
icon="el-icon-wallet"
@click="handleVerify(scope.row)"
v-has-permi="['bst:order:verify']"
v-show="OrderStatus.canVerify().includes(scope.row.status)"
>审核</el-button>
</template>
</el-table-column>
</el-table>
@ -244,6 +252,8 @@
/>
<order-refund-dialog :id="row.id" :visible.sync="showRefundDialog" @success="getList" />
<order-verify-dialog :id="row.id" :visible.sync="showVerifyDialog" @success="getList" />
</div>
</template>
@ -252,7 +262,8 @@ import { listOrder, endOrder } from "@/api/bst/order";
import { $showColumns } from '@/utils/mixins';
import FormCol from "@/components/FormCol/index.vue";
import { OrderStatus } from "@/utils/enums";
import OrderRefundDialog from "./components/OrderRefundDialog.vue";
import OrderRefundDialog from "@/views/bst/order/components/OrderRefundDialog.vue";
import OrderVerifyDialog from "@/views/bst/order/components/OrderVerifyDialog.vue";
//
const defaultSort = {
@ -264,7 +275,7 @@ export default {
name: "Order",
mixins: [$showColumns],
dicts: ['order_status', 'suit_type', 'order_return_type', 'order_return_mode', 'suit_rental_unit', 'suit_riding_rule'],
components: {FormCol, OrderRefundDialog},
components: {FormCol, OrderRefundDialog, OrderVerifyDialog},
data() {
return {
span: 24,
@ -274,7 +285,7 @@ export default {
{key: 'id', visible: false, label: 'ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: "80"},
{key: 'no', visible: true, label: '订单号', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'suitName', visible: true, label: '套餐', minWidth: "200", sortable: true, overflow: false, align: 'left', width: null},
{key: 'device', visible: true, label: '设备', minWidth: "150", sortable: false, overflow: false, align: 'left', width: null},
{key: 'device', visible: true, label: '当前设备', minWidth: "150", sortable: false, overflow: false, align: 'left', width: null},
{key: 'totalFee', visible: true, label: '费用', minWidth: "230", sortable: false, overflow: false, align: 'left', width: null},
{key: 'useInfo', visible: true, label: '使用', minWidth: "130", sortable: false, overflow: false, align: 'left', width: null},
{key: 'time', visible: true, label: '时间', minWidth: "180", sortable: false, overflow: false, align: 'left', width: null},
@ -330,13 +341,18 @@ export default {
},
row: {},
showRefundDialog: false,
showVerifyDialog: false,
};
},
created() {
this.getList();
},
methods: {
handleVerify(row) {
this.showVerifyDialog = true;
},
handleView(row) {
this.row = row;
this.$router.push(`/view/order/${row.id}`)
},
handleEnd(row) {

View File

@ -3,8 +3,37 @@
<el-row :gutter="10">
<el-col :span="18">
<el-card>
<el-row class="mb10" type="flex" justify="end">
<el-button
size="small"
plain
type="danger"
icon="el-icon-close"
@click="handleEnd(detail)"
v-has-permi="['bst:order:end']"
v-show="OrderStatus.canEnd().includes(detail.status)"
>结束</el-button>
<el-button
size="small"
plain
type="warning"
icon="el-icon-wallet"
@click="handleRefund(detail)"
v-has-permi="['bst:order:refund']"
v-show="OrderStatus.canRefund().includes(detail.status)"
>退款</el-button>
<el-button
size="small"
plain
type="warning"
icon="el-icon-wallet"
@click="handleVerify(detail)"
v-has-permi="['bst:order:verify']"
v-show="OrderStatus.canVerify().includes(detail.status)"
>审核</el-button>
</el-row>
<collapse-panel :value="true" title="基础信息">
<el-descriptions :column="3" >
<el-descriptions :column="4" >
<el-descriptions-item label="订单编号">{{ detail.no | dv}}</el-descriptions-item>
<el-descriptions-item label="订单状态">
<dict-tag :options="dict.type.order_status" :value="detail.status" size="small"/>
@ -37,7 +66,7 @@
</collapse-panel>
<collapse-panel :value="true" title="套餐信息">
<el-descriptions :column="3" >
<el-descriptions :column="4" >
<el-descriptions-item label="套餐名称">
{{ detail.suitName }}
<dict-tag :options="dict.type.suit_type" :value="detail.suitType" size="mini" style="margin-left: 4px;"/>
@ -53,7 +82,7 @@
</collapse-panel>
<collapse-panel :value="true" title="归还信息">
<el-descriptions :column="3" >
<el-descriptions :column="4" >
<el-descriptions-item label="归还方式">
<dict-tag :options="dict.type.order_return_mode" :value="detail.returnMode" size="small"/>
</el-descriptions-item>
@ -100,31 +129,38 @@
<el-card class="box-card" v-if="detail.id" style="margin-top: 10px;">
<el-tabs lazy>
<el-tab-pane label="车辆轨迹">
<el-tab-pane label="车辆轨迹" v-if="checkPermi(['bst:locationLog:list'])">
<device-location :query="{orderId: detail.id}" :area-id="detail.areaId" />
</el-tab-pane>
<el-tab-pane label="收益信息">
<el-tab-pane label="收益信息" v-if="checkPermi(['bst:bonus:list'])">
<bonus :query="{bstId: detail.id, bstType: BonusBstType.ORDER}" />
</el-tab-pane>
<el-tab-pane label="订单车辆">
<el-tab-pane label="订单车辆" v-if="checkPermi(['bst:orderDevice:list'])">
<order-device :query="{orderId: detail.id}" />
</el-tab-pane>
<el-tab-pane label="支付信息">
<el-tab-pane label="支付信息" v-if="checkPermi(['bst:pay:list'])">
<pay :query="{bstId: detail.id, bstType: PayBstType.ORDER}"/>
</el-tab-pane>
</el-tabs>
</el-card>
<order-refund-dialog :id="detail.id" :visible.sync="showRefundDialog" @success="getDetail" />
<order-verify-dialog :id="detail.id" :visible.sync="showVerifyDialog" @success="getDetail" />
</div>
</template>
<script>
import { getOrder } from '@/api/bst/order'
import CollapsePanel from '@/components/CollapsePanel/index.vue'
import {SuitRidingRule, PayBstType, BonusBstType} from '@/utils/enums'
import {SuitRidingRule, PayBstType, BonusBstType, OrderStatus} from '@/utils/enums'
import OrderDevice from '@/views/bst/orderDevice/index.vue'
import Pay from '@/views/bst/pay/index.vue'
import Bonus from '@/views/bst/bonus/index.vue'
import DeviceLocation from '@/views/bst/device/view/components/DeviceLocation.vue'
import OrderRefundDialog from '@/views/bst/order/components/OrderRefundDialog.vue'
import OrderVerifyDialog from '@/views/bst/order/components/OrderVerifyDialog.vue'
export default {
name: 'OrderView',
@ -134,16 +170,21 @@ export default {
OrderDevice,
Pay,
Bonus,
DeviceLocation
DeviceLocation,
OrderRefundDialog,
OrderVerifyDialog
},
data() {
return {
OrderStatus,
id: null,
detail: {},
loading: false,
SuitRidingRule,
PayBstType,
BonusBstType
BonusBstType,
showRefundDialog: false,
showVerifyDialog: false,
}
},
created() {
@ -158,7 +199,25 @@ export default {
}).finally(() => {
this.loading = false
})
}
},
handleRefund(row) {
this.showRefundDialog = true;
},
handleVerify(row) {
this.showVerifyDialog = true;
},
handleEnd(row) {
this.$confirm(`确定结束订单${row.no}吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
endOrder(row.id).then(response => {
this.$message.success("结束成功");
this.getList();
});
});
},
}
}
</script>

View File

@ -1,12 +1,12 @@
<template>
<el-dialog :title="title" @open="handleOpen" :visible.sync="dialogVisible" width="700px" append-to-body :close-on-click-modal="false">
<el-dialog :title="title" @open="handleOpen" :visible.sync="dialogVisible" width="500px" append-to-body :close-on-click-modal="false">
<el-form ref="form" :model="form" :rules="rules" size="small" label-width="6em" v-loading="loading">
<el-row :gutter="12">
<form-col :span="24" label="归属部门" prop="deptId" v-if="isAdmin">
<dept-select v-model="form.deptId" check-strictly/>
</form-col>
<form-col :span="span" label="昵称" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入昵称" maxlength="30" />
<form-col :span="span" label="姓名" prop="nickName">
<el-input v-model="form.nickName" placeholder="请输入姓名" maxlength="30" />
</form-col>
<form-col :span="span" label="角色" prop="roleIds">
<el-select v-model="form.roleIds" size="small" multiple placeholder="请选择角色" style="width: 100%">
@ -19,10 +19,10 @@
></el-option>
</el-select>
</form-col>
<form-col :span="span" label="登录账号" prop="userName">
<form-col :span="span" label="账号" prop="userName">
<el-input v-model="form.userName" placeholder="请输入登录账号" maxlength="30" />
</form-col>
<form-col :span="span" label="登录密码" prop="password" v-if="form.userId == null">
<form-col :span="span" label="密码" prop="password" v-if="form.userId == null">
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password/>
</form-col>
</el-row>
@ -35,12 +35,6 @@
<template slot="append">%</template>
</el-input>
</form-col>
<form-col :span="span" label="手机号码" prop="phonenumber">
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
</form-col>
<form-col :span="span" label="邮箱" prop="email">
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
</form-col>
<form-col :span="span" label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio
@ -88,7 +82,7 @@ export default {
},
data() {
return {
span: 12,
span: 24,
//
deptOptions: [],
//

View File

@ -17,18 +17,10 @@
@change="handleQuery"
/>
</el-form-item>
<el-form-item label="登录账号" prop="userName" v-if="isShow('userName')">
<el-form-item label="账号" prop="userName" v-if="isShow('userName')">
<el-input
v-model="queryParams.userName"
placeholder="请输入登录账号"
clearable
@keyup.enter.native="handleQuery"
/>
</el-form-item>
<el-form-item label="手机号码" prop="phonenumber" v-if="isShow('phonenumber')">
<el-input
v-model="queryParams.phonenumber"
placeholder="请输入手机号码"
placeholder="请输入账号"
clearable
@keyup.enter.native="handleQuery"
/>
@ -272,16 +264,15 @@ export default {
columns: [
{key: 'userId', visible: false, label: 'ID', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'nickName', visible: true, label: '姓名', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'phonenumber', visible: true, label: '手机号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'userName', visible: true, label: '账号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'roles', visible: true, label: '角色', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'point', visible: true, label: '分成比例', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'balance', visible: true, label: '余额', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'status', visible: true, label: '状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'isReal', visible: true, label: '实名状态', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'agentName', visible: true, label: '所属代理', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'userName', visible: true, label: '登录账号', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'deptName', visible: true, label: '归属部门', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'email', visible: true, label: '邮箱', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'email', visible: false, label: '邮箱', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'loginIp', visible: true, label: '登录IP', minWidth: null, sortable: true, overflow: false, align: 'center', width: null},
{key: 'loginDate', visible: true, label: '登录时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},
{key: 'createTime', visible: true, label: '创建时间', minWidth: null, sortable: true, overflow: false, align: 'center', width: "100"},