add
This commit is contained in:
@@ -1,11 +1,18 @@
|
|||||||
package com.car.config;
|
package com.car.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class WebConfig implements WebMvcConfigurer {
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
private final String uploadDir;
|
||||||
|
|
||||||
|
public WebConfig(org.springframework.core.env.Environment environment) {
|
||||||
|
this.uploadDir = environment.getProperty("file.upload-dir", "");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addCorsMappings(CorsRegistry registry) {
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
registry.addMapping("/**")
|
registry.addMapping("/**")
|
||||||
@@ -14,4 +21,12 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
.allowedHeaders("*")
|
.allowedHeaders("*")
|
||||||
.allowCredentials(true);
|
.allowCredentials(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
if (!uploadDir.isBlank()) {
|
||||||
|
String location = "file:" + (uploadDir.endsWith("/") ? uploadDir : uploadDir + "/");
|
||||||
|
registry.addResourceHandler("/uploads/**").addResourceLocations(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.car.controller;
|
||||||
|
|
||||||
|
import com.car.common.ApiResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class UploadController {
|
||||||
|
|
||||||
|
@Value("${file.upload-dir}")
|
||||||
|
private String uploadDir;
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
public ApiResponse<String> upload(@RequestParam("file") MultipartFile file) throws IOException {
|
||||||
|
if (file.isEmpty()) {
|
||||||
|
return new ApiResponse<>(400, "文件不能为空", null);
|
||||||
|
}
|
||||||
|
File dir = new File(uploadDir);
|
||||||
|
if (!dir.exists() && !dir.mkdirs()) {
|
||||||
|
return new ApiResponse<>(500, "上传目录创建失败", null);
|
||||||
|
}
|
||||||
|
String ext = StringUtils.getFilenameExtension(file.getOriginalFilename());
|
||||||
|
String filename = UUID.randomUUID().toString().replace("-", "");
|
||||||
|
if (ext != null && !ext.isBlank()) {
|
||||||
|
filename += "." + ext;
|
||||||
|
}
|
||||||
|
File dest = new File(dir, filename);
|
||||||
|
file.transferTo(dest);
|
||||||
|
return ApiResponse.ok("/uploads/" + filename);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,6 +74,12 @@ public class UserService {
|
|||||||
if (db == null) {
|
if (db == null) {
|
||||||
throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在");
|
throw new ApiException(ErrorCode.NOT_FOUND, "用户不存在");
|
||||||
}
|
}
|
||||||
|
if ("PENDING".equals(db.getRealNameStatus())) {
|
||||||
|
throw new ApiException(ErrorCode.CONFLICT, "实名认证正在审核中");
|
||||||
|
}
|
||||||
|
if ("APPROVED".equals(db.getRealNameStatus())) {
|
||||||
|
throw new ApiException(ErrorCode.CONFLICT, "实名认证已通过");
|
||||||
|
}
|
||||||
db.setRealName(request.getRealName());
|
db.setRealName(request.getRealName());
|
||||||
db.setIdNumber(request.getIdNumber());
|
db.setIdNumber(request.getIdNumber());
|
||||||
db.setIdFront(request.getIdFront());
|
db.setIdFront(request.getIdFront());
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ server:
|
|||||||
|
|
||||||
spring:
|
spring:
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:mysql://localhost:3306/car_rental?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
url: jdbc:mysql://localhost:3307/car_rental?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
|
||||||
username: root
|
username: root
|
||||||
password: root
|
password: qq5211314
|
||||||
jackson:
|
jackson:
|
||||||
date-format: yyyy-MM-dd HH:mm:ss
|
date-format: yyyy-MM-dd HH:mm:ss
|
||||||
time-zone: Asia/Shanghai
|
time-zone: Asia/Shanghai
|
||||||
@@ -24,3 +24,6 @@ sa-token:
|
|||||||
is-share: true
|
is-share: true
|
||||||
token-prefix: Bearer
|
token-prefix: Bearer
|
||||||
is-log: false
|
is-log: false
|
||||||
|
|
||||||
|
file:
|
||||||
|
upload-dir: ${user.dir}/uploads
|
||||||
|
|||||||
BIN
backend/uploads/02835d06b6604f61b71962f88dc733f1.png
Normal file
BIN
backend/uploads/02835d06b6604f61b71962f88dc733f1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
backend/uploads/d5e5539a596a40dabac9e6023c3ae84c.png
Normal file
BIN
backend/uploads/d5e5539a596a40dabac9e6023c3ae84c.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import { Message } from '@arco-design/web-vue'
|
||||||
import { useAuthStore } from '../store/auth'
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
const http = axios.create({
|
const http = axios.create({
|
||||||
@@ -18,11 +19,16 @@ http.interceptors.response.use(
|
|||||||
(response) => {
|
(response) => {
|
||||||
const res = response.data
|
const res = response.data
|
||||||
if (res.code !== 0) {
|
if (res.code !== 0) {
|
||||||
return Promise.reject(new Error(res.message || '请求失败'))
|
Message.error(res.message || 'Request failed')
|
||||||
|
return Promise.reject(new Error(res.message || 'Request failed'))
|
||||||
}
|
}
|
||||||
return res.data
|
return res.data
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
(error) => {
|
||||||
|
const message = error?.response?.data?.message || error.message || 'Request failed'
|
||||||
|
Message.error(message)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
export default http
|
export default http
|
||||||
|
|||||||
@@ -20,12 +20,14 @@ a {
|
|||||||
gap: 24px;
|
gap: 24px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-bottom: 1px solid #e5e6eb;
|
border-bottom: 1px solid #e5e6eb;
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-actions {
|
.user-actions {
|
||||||
@@ -34,6 +36,17 @@ a {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .arco-menu {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .arco-menu-horizontal {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.divider {
|
.divider {
|
||||||
|
|||||||
31
frontend/src/utils/format.js
Normal file
31
frontend/src/utils/format.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
export const roleMap = {
|
||||||
|
ADMIN: '管理员',
|
||||||
|
USER: '用户'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const userStatusMap = {
|
||||||
|
ACTIVE: '启用',
|
||||||
|
DISABLED: '禁用'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const realNameStatusMap = {
|
||||||
|
NONE: '未提交',
|
||||||
|
PENDING: '审核中',
|
||||||
|
APPROVED: '已通过',
|
||||||
|
REJECTED: '已驳回'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const orderStatusMap = {
|
||||||
|
PENDING_PAY: '待支付',
|
||||||
|
RENTING: '租用中',
|
||||||
|
RETURNED: '已归还',
|
||||||
|
CANCELLED: '已取消'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const carStatusMap = {
|
||||||
|
AVAILABLE: '可租',
|
||||||
|
RENTED: '出租中',
|
||||||
|
MAINTENANCE: '维护中'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mapText = (map, value) => map[value] || value || '-'
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
<p>里程:{{ car.mileage }} km</p>
|
<p>里程:{{ car.mileage }} km</p>
|
||||||
<p>{{ car.description }}</p>
|
<p>{{ car.description }}</p>
|
||||||
<a-tag v-if="car.isSpecial" color="red">特价</a-tag>
|
<a-tag v-if="car.isSpecial" color="red">特价</a-tag>
|
||||||
<a-tag v-if="car.status !== 'AVAILABLE'" color="orange">不可租</a-tag>
|
<a-tag v-if="car.status !== 'AVAILABLE'" color="orange">{{ mapText(carStatusMap, car.status) }}</a-tag>
|
||||||
<div style="margin-top: 12px; display: flex; gap: 12px;">
|
<div style="margin-top: 12px; display: flex; gap: 12px;">
|
||||||
<a-button type="primary" @click="toggleFavorite">
|
<a-button type="primary" @click="toggleFavorite">
|
||||||
{{ isFavorite ? '取消收藏' : '加入收藏' }}
|
{{ isFavorite ? '取消收藏' : '加入收藏' }}
|
||||||
@@ -37,6 +37,7 @@ import { useRoute, useRouter } from 'vue-router'
|
|||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import http from '../api/http'
|
import http from '../api/http'
|
||||||
import { useAuthStore } from '../store/auth'
|
import { useAuthStore } from '../store/auth'
|
||||||
|
import { carStatusMap, mapText } from '../utils/format'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<p>日租金:¥{{ car.pricePerDay }}</p>
|
<p>日租金:¥{{ car.pricePerDay }}</p>
|
||||||
<p>押金:¥{{ car.deposit }}</p>
|
<p>押金:¥{{ car.deposit }}</p>
|
||||||
<a-tag v-if="car.isSpecial" color="red">特价</a-tag>
|
<a-tag v-if="car.isSpecial" color="red">特价</a-tag>
|
||||||
<a-tag v-if="car.status !== 'AVAILABLE'" color="orange">不可租</a-tag>
|
<a-tag v-if="car.status !== 'AVAILABLE'" color="orange">{{ mapText(carStatusMap, car.status) }}</a-tag>
|
||||||
<div style="margin-top: 12px">
|
<div style="margin-top: 12px">
|
||||||
<router-link :to="`/cars/${car.id}`">
|
<router-link :to="`/cars/${car.id}`">
|
||||||
<a-button type="primary">查看详情</a-button>
|
<a-button type="primary">查看详情</a-button>
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import http from '../api/http'
|
import http from '../api/http'
|
||||||
|
import { carStatusMap, mapText } from '../utils/format'
|
||||||
|
|
||||||
const cars = ref([])
|
const cars = ref([])
|
||||||
const query = ref({
|
const query = ref({
|
||||||
|
|||||||
@@ -33,25 +33,38 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import http from '../api/http'
|
import http from '../api/http'
|
||||||
|
import { orderStatusMap, mapText } from '../utils/format'
|
||||||
|
|
||||||
const orders = ref([])
|
const orders = ref([])
|
||||||
const query = ref({ status: '' })
|
const query = ref({ status: '' })
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '订单号', dataIndex: 'orderNo' },
|
{ title: '订单号', dataIndex: 'orderNo' },
|
||||||
{ title: '车辆ID', dataIndex: 'carId' },
|
{ title: '车辆信息', dataIndex: 'carInfo' },
|
||||||
{ title: '起始日期', dataIndex: 'startDate' },
|
{ title: '起始日期', dataIndex: 'startDate' },
|
||||||
{ title: '结束日期', dataIndex: 'endDate' },
|
{ title: '结束日期', dataIndex: 'endDate' },
|
||||||
{ title: '天数', dataIndex: 'days' },
|
{ title: '天数', dataIndex: 'days' },
|
||||||
{ title: '总金额', dataIndex: 'totalAmount' },
|
{ title: '总金额', dataIndex: 'totalAmount' },
|
||||||
{ title: '状态', dataIndex: 'status' },
|
{ title: '状态', dataIndex: 'statusText' },
|
||||||
{ title: '操作', slotName: 'actions' }
|
{ title: '操作', slotName: 'actions' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const loadOrders = async () => {
|
const loadOrders = async () => {
|
||||||
const params = {}
|
const params = {}
|
||||||
if (query.value.status) params.status = query.value.status
|
if (query.value.status) params.status = query.value.status
|
||||||
orders.value = await http.get('/api/user/order', { params })
|
const [orderList, cars] = await Promise.all([
|
||||||
|
http.get('/api/user/order', { params }),
|
||||||
|
http.get('/api/cars')
|
||||||
|
])
|
||||||
|
const carMap = new Map(cars.map((car) => [car.id, car]))
|
||||||
|
orders.value = orderList.map((order) => {
|
||||||
|
const car = carMap.get(order.carId) || {}
|
||||||
|
return {
|
||||||
|
...order,
|
||||||
|
carInfo: car.id ? `${car.brand || ''} ${car.model || ''} (${car.plateNo || ''})` : '未知车辆',
|
||||||
|
statusText: mapText(orderStatusMap, order.status)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const pay = async (record) => {
|
const pay = async (record) => {
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
<div style="height: 16px"></div>
|
<div style="height: 16px"></div>
|
||||||
<a-card>
|
<a-card>
|
||||||
<div class="section-title">实名认证</div>
|
<div class="section-title">实名认证</div>
|
||||||
|
<a-descriptions v-if="user" :data="idDesc" bordered />
|
||||||
|
<div style="height: 12px"></div>
|
||||||
<a-form :model="realName">
|
<a-form :model="realName">
|
||||||
<a-form-item label="真实姓名">
|
<a-form-item label="真实姓名">
|
||||||
<a-input v-model="realName.realName" />
|
<a-input v-model="realName.realName" />
|
||||||
@@ -29,13 +31,41 @@
|
|||||||
<a-input v-model="realName.idNumber" />
|
<a-input v-model="realName.idNumber" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="身份证正面">
|
<a-form-item label="身份证正面">
|
||||||
<a-input v-model="realName.idFront" placeholder="图片URL" />
|
<a-upload
|
||||||
|
:action="uploadUrl"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:show-file-list="false"
|
||||||
|
@success="onFrontSuccess"
|
||||||
|
>
|
||||||
|
<a-button>上传正面</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<a-tag v-if="realName.idFront" color="arcoblue">已上传</a-tag>
|
||||||
|
<a-image
|
||||||
|
v-if="realName.idFront"
|
||||||
|
:src="fileBase + realName.idFront"
|
||||||
|
width="120"
|
||||||
|
style="margin-left: 12px"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="身份证反面">
|
<a-form-item label="身份证反面">
|
||||||
<a-input v-model="realName.idBack" placeholder="图片URL" />
|
<a-upload
|
||||||
|
:action="uploadUrl"
|
||||||
|
:headers="uploadHeaders"
|
||||||
|
:show-file-list="false"
|
||||||
|
@success="onBackSuccess"
|
||||||
|
>
|
||||||
|
<a-button>上传反面</a-button>
|
||||||
|
</a-upload>
|
||||||
|
<a-tag v-if="realName.idBack" color="arcoblue">已上传</a-tag>
|
||||||
|
<a-image
|
||||||
|
v-if="realName.idBack"
|
||||||
|
:src="fileBase + realName.idBack"
|
||||||
|
width="120"
|
||||||
|
style="margin-left: 12px"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-button type="primary" @click="submitRealName">提交审核</a-button>
|
<a-button type="primary" :disabled="submitDisabled" @click="submitRealName">提交审核</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-card>
|
</a-card>
|
||||||
@@ -47,6 +77,7 @@ import { ref, computed, onMounted } from 'vue'
|
|||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import http from '../api/http'
|
import http from '../api/http'
|
||||||
import { useAuthStore } from '../store/auth'
|
import { useAuthStore } from '../store/auth'
|
||||||
|
import { realNameStatusMap, mapText } from '../utils/format'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const user = ref(null)
|
const user = ref(null)
|
||||||
@@ -58,6 +89,13 @@ const realName = ref({
|
|||||||
idBack: ''
|
idBack: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const fileBase = 'http://localhost:8080'
|
||||||
|
const uploadUrl = `${fileBase}/api/upload`
|
||||||
|
const uploadHeaders = computed(() => {
|
||||||
|
if (!auth.token) return {}
|
||||||
|
return { Authorization: `Bearer ${auth.token}` }
|
||||||
|
})
|
||||||
|
|
||||||
const desc = computed(() => {
|
const desc = computed(() => {
|
||||||
if (!user.value) return []
|
if (!user.value) return []
|
||||||
return [
|
return [
|
||||||
@@ -65,14 +103,35 @@ const desc = computed(() => {
|
|||||||
{ label: '手机号', value: user.value.phone || '-' },
|
{ label: '手机号', value: user.value.phone || '-' },
|
||||||
{ label: '邮箱', value: user.value.email || '-' },
|
{ label: '邮箱', value: user.value.email || '-' },
|
||||||
{ label: '余额', value: `¥${user.value.balance}` },
|
{ label: '余额', value: `¥${user.value.balance}` },
|
||||||
{ label: '实名认证状态', value: user.value.realNameStatus }
|
{ label: '实名认证状态', value: mapText(realNameStatusMap, user.value.realNameStatus) }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const idDesc = computed(() => {
|
||||||
|
if (!user.value) return []
|
||||||
|
return [
|
||||||
|
{ label: '真实姓名', value: user.value.realName || '-' },
|
||||||
|
{ label: '身份证号', value: user.value.idNumber || '-' },
|
||||||
|
{ label: '身份证正面', value: user.value.idFront ? '已上传' : '-' },
|
||||||
|
{ label: '身份证反面', value: user.value.idBack ? '已上传' : '-' }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const submitDisabled = computed(() => {
|
||||||
|
if (!user.value) return false
|
||||||
|
return ['PENDING', 'APPROVED'].includes(user.value.realNameStatus)
|
||||||
|
})
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const res = await http.get('/api/user/me')
|
const res = await http.get('/api/user/me')
|
||||||
user.value = res
|
user.value = res
|
||||||
auth.setUser(res)
|
auth.setUser(res)
|
||||||
|
if (user.value.realNameStatus !== 'REJECTED' && user.value.realNameStatus !== 'NONE') {
|
||||||
|
realName.value.realName = user.value.realName || ''
|
||||||
|
realName.value.idNumber = user.value.idNumber || ''
|
||||||
|
realName.value.idFront = user.value.idFront || ''
|
||||||
|
realName.value.idBack = user.value.idBack || ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addBalance = async () => {
|
const addBalance = async () => {
|
||||||
@@ -87,5 +146,25 @@ const submitRealName = async () => {
|
|||||||
load()
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const onFrontSuccess = (file) => {
|
||||||
|
const res = file?.response
|
||||||
|
if (!res || res.code !== 0) {
|
||||||
|
Message.error(res?.message || '上传失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
realName.value.idFront = res.data
|
||||||
|
Message.success('正面上传成功')
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBackSuccess = (file) => {
|
||||||
|
const res = file?.response
|
||||||
|
if (!res || res.code !== 0) {
|
||||||
|
Message.error(res?.message || '上传失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
realName.value.idBack = res.data
|
||||||
|
Message.success('反面上传成功')
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import http from '../../api/http'
|
import http from '../../api/http'
|
||||||
|
import { carStatusMap, mapText } from '../../utils/format'
|
||||||
|
|
||||||
const cars = ref([])
|
const cars = ref([])
|
||||||
const query = ref({ keyword: '', isSpecial: null })
|
const query = ref({ keyword: '', isSpecial: null })
|
||||||
@@ -95,8 +96,8 @@ const columns = [
|
|||||||
{ title: '车型', dataIndex: 'model' },
|
{ title: '车型', dataIndex: 'model' },
|
||||||
{ title: '车牌', dataIndex: 'plateNo' },
|
{ title: '车牌', dataIndex: 'plateNo' },
|
||||||
{ title: '日租金', dataIndex: 'pricePerDay' },
|
{ title: '日租金', dataIndex: 'pricePerDay' },
|
||||||
{ title: '状态', dataIndex: 'status' },
|
{ title: '状态', dataIndex: 'statusText' },
|
||||||
{ title: '特价', dataIndex: 'isSpecial' },
|
{ title: '特价', dataIndex: 'isSpecialText' },
|
||||||
{ title: '操作', slotName: 'actions' }
|
{ title: '操作', slotName: 'actions' }
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -104,7 +105,12 @@ const loadCars = async () => {
|
|||||||
const params = {}
|
const params = {}
|
||||||
if (query.value.keyword) params.keyword = query.value.keyword
|
if (query.value.keyword) params.keyword = query.value.keyword
|
||||||
if (query.value.isSpecial !== null) params.isSpecial = query.value.isSpecial
|
if (query.value.isSpecial !== null) params.isSpecial = query.value.isSpecial
|
||||||
cars.value = await http.get('/api/admin/cars', { params })
|
const list = await http.get('/api/admin/cars', { params })
|
||||||
|
cars.value = list.map((car) => ({
|
||||||
|
...car,
|
||||||
|
statusText: mapText(carStatusMap, car.status),
|
||||||
|
isSpecialText: car.isSpecial ? '是' : '否'
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const openEdit = (record) => {
|
const openEdit = (record) => {
|
||||||
|
|||||||
@@ -25,25 +25,42 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import http from '../../api/http'
|
import http from '../../api/http'
|
||||||
|
import { orderStatusMap, mapText } from '../../utils/format'
|
||||||
|
|
||||||
const orders = ref([])
|
const orders = ref([])
|
||||||
const query = ref({ status: '' })
|
const query = ref({ status: '' })
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '订单号', dataIndex: 'orderNo' },
|
{ title: '订单号', dataIndex: 'orderNo' },
|
||||||
{ title: '用户ID', dataIndex: 'userId' },
|
{ title: '用户信息', dataIndex: 'userInfo' },
|
||||||
{ title: '车辆ID', dataIndex: 'carId' },
|
{ title: '车辆信息', dataIndex: 'carInfo' },
|
||||||
{ title: '起始日期', dataIndex: 'startDate' },
|
{ title: '起始日期', dataIndex: 'startDate' },
|
||||||
{ title: '结束日期', dataIndex: 'endDate' },
|
{ title: '结束日期', dataIndex: 'endDate' },
|
||||||
{ title: '天数', dataIndex: 'days' },
|
{ title: '天数', dataIndex: 'days' },
|
||||||
{ title: '总金额', dataIndex: 'totalAmount' },
|
{ title: '总金额', dataIndex: 'totalAmount' },
|
||||||
{ title: '状态', dataIndex: 'status' }
|
{ title: '状态', dataIndex: 'statusText' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const loadOrders = async () => {
|
const loadOrders = async () => {
|
||||||
const params = {}
|
const params = {}
|
||||||
if (query.value.status) params.status = query.value.status
|
if (query.value.status) params.status = query.value.status
|
||||||
orders.value = await http.get('/api/admin/orders', { params })
|
const [orderList, users, cars] = await Promise.all([
|
||||||
|
http.get('/api/admin/orders', { params }),
|
||||||
|
http.get('/api/admin/users'),
|
||||||
|
http.get('/api/admin/cars')
|
||||||
|
])
|
||||||
|
const userMap = new Map(users.map((user) => [user.id, user]))
|
||||||
|
const carMap = new Map(cars.map((car) => [car.id, car]))
|
||||||
|
orders.value = orderList.map((order) => {
|
||||||
|
const user = userMap.get(order.userId) || {}
|
||||||
|
const car = carMap.get(order.carId) || {}
|
||||||
|
return {
|
||||||
|
...order,
|
||||||
|
userInfo: user.id ? `${user.username || ''} ${user.phone || ''}`.trim() : '未知用户',
|
||||||
|
carInfo: car.id ? `${car.brand || ''} ${car.model || ''} (${car.plateNo || ''})` : '未知车辆',
|
||||||
|
statusText: mapText(orderStatusMap, order.status)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOrders()
|
loadOrders()
|
||||||
|
|||||||
@@ -13,6 +13,14 @@
|
|||||||
</a-card>
|
</a-card>
|
||||||
<div style="height: 16px"></div>
|
<div style="height: 16px"></div>
|
||||||
<a-table :columns="columns" :data="users" row-key="id">
|
<a-table :columns="columns" :data="users" row-key="id">
|
||||||
|
<template #idFront="{ record }">
|
||||||
|
<a-image v-if="record.idFront" :src="fileBase + record.idFront" width="80" />
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
<template #idBack="{ record }">
|
||||||
|
<a-image v-if="record.idBack" :src="fileBase + record.idBack" width="80" />
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
<template #actions="{ record }">
|
<template #actions="{ record }">
|
||||||
<a-button v-if="record.realNameStatus === 'PENDING'" type="primary" @click="approve(record.id)">通过实名</a-button>
|
<a-button v-if="record.realNameStatus === 'PENDING'" type="primary" @click="approve(record.id)">通过实名</a-button>
|
||||||
<a-button v-if="record.realNameStatus === 'PENDING'" type="text" status="danger" @click="reject(record.id)">驳回实名</a-button>
|
<a-button v-if="record.realNameStatus === 'PENDING'" type="text" status="danger" @click="reject(record.id)">驳回实名</a-button>
|
||||||
@@ -27,6 +35,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Message } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import http from '../../api/http'
|
import http from '../../api/http'
|
||||||
|
import { roleMap, userStatusMap, realNameStatusMap, mapText } from '../../utils/format'
|
||||||
|
|
||||||
const users = ref([])
|
const users = ref([])
|
||||||
const query = ref({ keyword: '' })
|
const query = ref({ keyword: '' })
|
||||||
@@ -36,16 +45,26 @@ const columns = [
|
|||||||
{ title: '用户名', dataIndex: 'username' },
|
{ title: '用户名', dataIndex: 'username' },
|
||||||
{ title: '手机号', dataIndex: 'phone' },
|
{ title: '手机号', dataIndex: 'phone' },
|
||||||
{ title: '邮箱', dataIndex: 'email' },
|
{ title: '邮箱', dataIndex: 'email' },
|
||||||
{ title: '角色', dataIndex: 'role' },
|
{ title: '角色', dataIndex: 'roleText' },
|
||||||
{ title: '状态', dataIndex: 'status' },
|
{ title: '状态', dataIndex: 'statusText' },
|
||||||
{ title: '实名状态', dataIndex: 'realNameStatus' },
|
{ title: '实名状态', dataIndex: 'realNameStatusText' },
|
||||||
|
{ title: '身份证正面', dataIndex: 'idFront', slotName: 'idFront' },
|
||||||
|
{ title: '身份证反面', dataIndex: 'idBack', slotName: 'idBack' },
|
||||||
{ title: '操作', slotName: 'actions' }
|
{ title: '操作', slotName: 'actions' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const fileBase = 'http://localhost:8080'
|
||||||
|
|
||||||
const loadUsers = async () => {
|
const loadUsers = async () => {
|
||||||
const params = {}
|
const params = {}
|
||||||
if (query.value.keyword) params.keyword = query.value.keyword
|
if (query.value.keyword) params.keyword = query.value.keyword
|
||||||
users.value = await http.get('/api/admin/users', { params })
|
const list = await http.get('/api/admin/users', { params })
|
||||||
|
users.value = list.map((user) => ({
|
||||||
|
...user,
|
||||||
|
roleText: mapText(roleMap, user.role),
|
||||||
|
statusText: mapText(userStatusMap, user.status),
|
||||||
|
realNameStatusText: mapText(realNameStatusMap, user.realNameStatus)
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
const approve = async (id) => {
|
const approve = async (id) => {
|
||||||
|
|||||||
@@ -4,4 +4,8 @@ import vue from '@vitejs/plugin-vue'
|
|||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [vue()],
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
// 添加 allowedHosts 配置
|
||||||
|
allowedHosts: ['cloud.neuz.cn']
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user