upd
@@ -39,7 +39,7 @@ public class SecurityConfig {
|
||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers("/api/auth/**", "/api/public/**", "/error").permitAll()
|
||||
.requestMatchers("/api/auth/**", "/api/public/**", "/files/**", "/error").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.toyshop.config;
|
||||
package com.toyshop.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@@ -81,7 +81,14 @@ public class AdminController {
|
||||
|
||||
@GetMapping("/products")
|
||||
public ApiResponse<List<Product>> products() {
|
||||
return ApiResponse.ok(productRepository.findAll());
|
||||
List<Product> products = productRepository.findAll();
|
||||
for (Product product : products) {
|
||||
var img = productService.getFirstImage(product);
|
||||
if (img != null) {
|
||||
product.setImageUrl(img.getUrl());
|
||||
}
|
||||
}
|
||||
return ApiResponse.ok(products);
|
||||
}
|
||||
|
||||
@PostMapping("/products")
|
||||
@@ -148,6 +155,16 @@ public class AdminController {
|
||||
return ApiResponse.ok("删除成功", null);
|
||||
}
|
||||
|
||||
@PutMapping("/products/{id}/image")
|
||||
public ApiResponse<?> replaceProductImage(@PathVariable Long id, @RequestBody ProductImage body) {
|
||||
Product product = productRepository.findById(id).orElseThrow();
|
||||
if (body.getUrl() == null || body.getUrl().isBlank()) {
|
||||
return ApiResponse.fail("图片地址不能为空");
|
||||
}
|
||||
productService.replaceProductImage(product, body.getUrl());
|
||||
return ApiResponse.ok("更新成功", null);
|
||||
}
|
||||
|
||||
@GetMapping("/orders")
|
||||
public ApiResponse<List<Order>> orders() {
|
||||
return ApiResponse.ok(orderRepository.findAll());
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.toyshop.controller.admin;
|
||||
package com.toyshop.controller.admin;
|
||||
|
||||
import com.toyshop.dto.ApiResponse;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.toyshop.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -12,6 +13,7 @@ public class Address {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
@JsonIgnore
|
||||
private User user;
|
||||
|
||||
@Column(nullable = false, length = 50)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.toyshop.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@@ -11,10 +13,12 @@ public class CartItem {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
@JsonIgnore
|
||||
private User user;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "product_id", nullable = false)
|
||||
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
|
||||
private Product product;
|
||||
|
||||
@Column(nullable = false)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.toyshop.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -16,6 +17,7 @@ public class Order {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "user_id", nullable = false)
|
||||
@JsonIgnore
|
||||
private User user;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.toyshop.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@@ -12,10 +14,12 @@ public class OrderItem {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "order_id", nullable = false)
|
||||
@JsonIgnore
|
||||
private Order order;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@ManyToOne(fetch = FetchType.EAGER)
|
||||
@JoinColumn(name = "product_id", nullable = false)
|
||||
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
|
||||
private Product product;
|
||||
|
||||
@Column(nullable = false)
|
||||
|
||||
@@ -47,6 +47,9 @@ public class Product {
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
@Transient
|
||||
private String imageUrl;
|
||||
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
@@ -81,4 +84,6 @@ public class Product {
|
||||
public void setOnSale(boolean onSale) { this.onSale = onSale; }
|
||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
||||
public String getImageUrl() { return imageUrl; }
|
||||
public void setImageUrl(String imageUrl) { this.imageUrl = imageUrl; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.toyshop.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@@ -11,6 +12,7 @@ public class ProductImage {
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "product_id", nullable = false)
|
||||
@JsonIgnore
|
||||
private Product product;
|
||||
|
||||
@Column(nullable = false, length = 255)
|
||||
|
||||
@@ -4,7 +4,10 @@ import com.toyshop.entity.ProductImage;
|
||||
import com.toyshop.entity.Product;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ProductImageRepository extends JpaRepository<ProductImage, Long> {
|
||||
List<ProductImage> findByProduct(Product product);
|
||||
Optional<ProductImage> findTopByProductOrderBySortOrderAsc(Product product);
|
||||
void deleteByProduct(Product product);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.toyshop.service;
|
||||
import com.toyshop.entity.*;
|
||||
import com.toyshop.repository.*;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
@@ -23,6 +24,7 @@ public class OrderService {
|
||||
this.addressRepository = addressRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Order createOrder(User user, Long addressId) {
|
||||
Address address = addressRepository.findById(addressId).orElseThrow();
|
||||
if (!address.getUser().getId().equals(user.getId())) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.toyshop.repository.*;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import jakarta.persistence.criteria.Predicate;
|
||||
import java.util.ArrayList;
|
||||
@@ -59,4 +60,18 @@ public class ProductService {
|
||||
public List<Category> allCategories() {
|
||||
return categoryRepository.findAll();
|
||||
}
|
||||
|
||||
public ProductImage getFirstImage(Product product) {
|
||||
return productImageRepository.findTopByProductOrderBySortOrderAsc(product).orElse(null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void replaceProductImage(Product product, String url) {
|
||||
productImageRepository.deleteByProduct(product);
|
||||
ProductImage image = new ProductImage();
|
||||
image.setProduct(product);
|
||||
image.setUrl(url);
|
||||
image.setSortOrder(0);
|
||||
productImageRepository.save(image);
|
||||
}
|
||||
}
|
||||
|
||||
BIN
files/14771fb97130455d9b3108f8ab91a0b8.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
files/172ef1e1cac649b8a3e801b8a0accbb5.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
files/4aecfad04b8a442b951bb31f5a917ba2.jpg
Normal file
|
After Width: | Height: | Size: 418 KiB |
BIN
files/814b34b4b50146f2b2f6981120e14917.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
BIN
files/8388273805a141b391faf586e549f853.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
files/9f81853ab69745c4b4f59d6b6897e96c.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
files/ab2cef0445854980b06548b5f7114b6a.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
files/d180c0ef0ded4b3fac45717b531a9cb0.jpg
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
files/e30bf07aa40747e38ae54de8eb3ad7f1.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
@@ -1,4 +1,5 @@
|
||||
import axios from 'axios'
|
||||
import router from '../router'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:8080',
|
||||
@@ -16,6 +17,14 @@ api.interceptors.request.use((config) => {
|
||||
api.interceptors.response.use(
|
||||
(res) => res.data,
|
||||
(err) => {
|
||||
const status = err?.response?.status
|
||||
if (status === 401 || status === 403) {
|
||||
const role = localStorage.getItem('role')
|
||||
const target = role === 'ADMIN' ? '/admin/login' : '/login'
|
||||
if (router.currentRoute.value.path !== target) {
|
||||
router.push(target)
|
||||
}
|
||||
}
|
||||
const message = err?.response?.data?.message || '请求失败'
|
||||
return Promise.reject(new Error(message))
|
||||
}
|
||||
|
||||
@@ -13,11 +13,15 @@
|
||||
</a-form>
|
||||
<a-table :dataSource="list" :columns="columns" rowKey="id" style="margin-top: 16px">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<img :src="previewUrl(record.imageUrl)" class="thumb" />
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button type="link" danger @click="remove(record)">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<img v-if="form.imageUrl" :src="previewUrl(form.imageUrl)" class="preview" />
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -27,10 +31,11 @@ import api from '../../api'
|
||||
|
||||
const list = ref([])
|
||||
const form = reactive({ imageUrl: '', linkUrl: '', sortOrder: 0 })
|
||||
const uploadUrl = 'http://localhost:8080/api/admin/upload'
|
||||
const baseUrl = 'http://localhost:8080'
|
||||
const uploadUrl = `${baseUrl}/api/admin/upload`
|
||||
const headers = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` }
|
||||
const columns = [
|
||||
{ title: '图片', dataIndex: 'imageUrl' },
|
||||
{ title: '图片', key: 'image' },
|
||||
{ title: '链接', dataIndex: 'linkUrl' },
|
||||
{ title: '排序', dataIndex: 'sortOrder' },
|
||||
{ title: '操作', key: 'action' }
|
||||
@@ -53,11 +58,19 @@ const onUpload = (info) => {
|
||||
if (info.file.status === 'done') {
|
||||
const res = info.file.response
|
||||
if (res && res.success) {
|
||||
form.imageUrl = res.data
|
||||
form.imageUrl = normalizeUrl(res.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${baseUrl}${url}`
|
||||
}
|
||||
|
||||
const previewUrl = (url) => normalizeUrl(url)
|
||||
|
||||
const remove = async (record) => {
|
||||
await api.delete(`/api/admin/carousels/${record.id}`)
|
||||
load()
|
||||
@@ -65,3 +78,18 @@ const remove = async (record) => {
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.thumb {
|
||||
width: 120px;
|
||||
height: 60px;
|
||||
object-fit: cover;
|
||||
}
|
||||
.preview {
|
||||
display: block;
|
||||
width: 240px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
margin-top: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -13,18 +13,41 @@
|
||||
</a-form>
|
||||
<a-table :dataSource="list" :columns="columns" rowKey="id" style="margin-top: 16px">
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'image'">
|
||||
<img v-if="record.imageUrl" :src="previewUrl(record.imageUrl)" class="thumb" />
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-button type="link" @click="addImage(record)">图片</a-button>
|
||||
<a-button type="link" @click="edit(record)">编辑</a-button>
|
||||
<a-button type="link" @click="addImage(record)">更换图片</a-button>
|
||||
<a-button type="link" danger @click="remove(record)">删除</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-modal v-model:open="imgVisible" title="新增图片" @ok="saveImage">
|
||||
<a-modal v-model:open="imgVisible" title="商品图片" @ok="saveImage">
|
||||
<a-upload :action="uploadUrl" :headers="headers" @change="onUpload" :showUploadList="false">
|
||||
<a-button>上传图片</a-button>
|
||||
</a-upload>
|
||||
<a-input v-model:value="imgForm.url" placeholder="图片URL" style="margin-top: 8px" />
|
||||
<a-input-number v-model:value="imgForm.sortOrder" style="margin-top: 8px" />
|
||||
<img v-if="imgForm.url" :src="previewUrl(imgForm.url)" class="preview" />
|
||||
</a-modal>
|
||||
|
||||
<a-modal v-model:open="editVisible" title="编辑商品" @ok="saveEdit">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="名称"><a-input v-model:value="editForm.name" /></a-form-item>
|
||||
<a-form-item label="价格"><a-input-number v-model:value="editForm.price" style="width: 100%" /></a-form-item>
|
||||
<a-form-item label="库存"><a-input-number v-model:value="editForm.stock" style="width: 100%" /></a-form-item>
|
||||
<a-form-item label="分类">
|
||||
<a-select v-model:value="editForm.categoryId" style="width: 100%" allowClear>
|
||||
<a-select-option v-for="c in categories" :key="c.id" :value="c.id">{{ c.name }}</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="适龄"><a-input v-model:value="editForm.ageRange" /></a-form-item>
|
||||
<a-form-item label="安全认证"><a-input v-model:value="editForm.safetyInfo" /></a-form-item>
|
||||
<a-form-item label="描述"><a-textarea v-model:value="editForm.description" /></a-form-item>
|
||||
<a-form-item label="上架">
|
||||
<a-switch v-model:checked="editForm.onSale" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</a-card>
|
||||
</template>
|
||||
@@ -40,13 +63,27 @@ const columns = [
|
||||
{ title: '名称', dataIndex: 'name' },
|
||||
{ title: '价格', dataIndex: 'price' },
|
||||
{ title: '库存', dataIndex: 'stock' },
|
||||
{ title: '图片', key: 'image' },
|
||||
{ title: '操作', key: 'action' }
|
||||
]
|
||||
|
||||
const imgVisible = ref(false)
|
||||
const imgForm = reactive({ productId: null, url: '', sortOrder: 0 })
|
||||
const uploadUrl = 'http://localhost:8080/api/admin/upload'
|
||||
const imgForm = reactive({ productId: null, url: '' })
|
||||
const baseUrl = 'http://localhost:8080'
|
||||
const uploadUrl = `${baseUrl}/api/admin/upload`
|
||||
const headers = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` }
|
||||
const editVisible = ref(false)
|
||||
const editForm = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
price: 0,
|
||||
stock: 0,
|
||||
categoryId: null,
|
||||
ageRange: '',
|
||||
safetyInfo: '',
|
||||
description: '',
|
||||
onSale: true
|
||||
})
|
||||
|
||||
const load = async () => {
|
||||
const res = await api.get('/api/admin/products')
|
||||
@@ -71,24 +108,67 @@ const remove = async (record) => {
|
||||
|
||||
const addImage = (record) => {
|
||||
imgForm.productId = record.id
|
||||
imgForm.url = ''
|
||||
imgForm.sortOrder = 0
|
||||
imgForm.url = record.imageUrl || ''
|
||||
imgVisible.value = true
|
||||
}
|
||||
|
||||
const saveImage = async () => {
|
||||
await api.post(`/api/admin/products/${imgForm.productId}/images`, imgForm)
|
||||
await api.put(`/api/admin/products/${imgForm.productId}/image`, { url: imgForm.url })
|
||||
imgVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
const onUpload = (info) => {
|
||||
if (info.file.status === 'done') {
|
||||
const res = info.file.response
|
||||
if (res && res.success) {
|
||||
imgForm.url = res.data
|
||||
imgForm.url = normalizeUrl(res.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${baseUrl}${url}`
|
||||
}
|
||||
|
||||
const previewUrl = (url) => normalizeUrl(url)
|
||||
|
||||
const edit = (record) => {
|
||||
editForm.id = record.id
|
||||
editForm.name = record.name
|
||||
editForm.price = record.price
|
||||
editForm.stock = record.stock
|
||||
editForm.categoryId = record.category?.id || null
|
||||
editForm.ageRange = record.ageRange || ''
|
||||
editForm.safetyInfo = record.safetyInfo || ''
|
||||
editForm.description = record.description || ''
|
||||
editForm.onSale = record.onSale
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
const saveEdit = async () => {
|
||||
await api.put(`/api/admin/products/${editForm.id}`, editForm)
|
||||
editVisible.value = false
|
||||
load()
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.preview {
|
||||
display: block;
|
||||
width: 200px;
|
||||
height: 120px;
|
||||
object-fit: cover;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
width: 60px;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
<template>
|
||||
<a-card title="订单确认">
|
||||
<a-radio-group v-model:value="selected">
|
||||
<a-list :data-source="addresses">
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-radio v-model:checked="selected" :value="item.id">
|
||||
<a-radio :value="item.id">
|
||||
{{ item.receiverName }} {{ item.receiverPhone }} {{ item.detail }}
|
||||
</a-radio>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-radio-group>
|
||||
<a-button type="primary" @click="createOrder" :disabled="!selected">提交订单</a-button>
|
||||
</a-card>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<a-card title="轮播与公告">
|
||||
<a-carousel autoplay>
|
||||
<div v-for="item in home.carousels" :key="item.id" class="banner">
|
||||
<img :src="item.imageUrl" alt="banner" />
|
||||
<img :src="previewUrl(item.imageUrl)" alt="banner" />
|
||||
</div>
|
||||
</a-carousel>
|
||||
<a-list :data-source="home.notices" style="margin-top: 16px">
|
||||
@@ -46,6 +46,7 @@ const loading = ref(false)
|
||||
const home = reactive({ carousels: [], notices: [], hot: [], newest: [] })
|
||||
const router = useRouter()
|
||||
|
||||
const baseUrl = 'http://localhost:8080'
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -58,6 +59,12 @@ const load = async () => {
|
||||
|
||||
const goDetail = (id) => router.push(`/products/${id}`)
|
||||
|
||||
const previewUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${baseUrl}${url}`
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
|
||||
@@ -6,11 +6,38 @@
|
||||
{{ statusMap[record.status] || record.status }}
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button type="link" @click="viewDetail(record)">详情</a-button>
|
||||
<a-button type="link" v-if="record.status === 'PENDING_PAYMENT'" @click="pay(record)">去支付</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal v-model:open="detailVisible" title="订单详情" footer={null}>
|
||||
<a-descriptions bordered :column="1">
|
||||
<a-descriptions-item label="订单号">{{ detail.order?.orderNo }}</a-descriptions-item>
|
||||
<a-descriptions-item label="金额">¥{{ detail.order?.totalAmount }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">{{ statusMap[detail.order?.status] }}</a-descriptions-item>
|
||||
<a-descriptions-item label="收货人">{{ detail.order?.receiverName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="联系电话">{{ detail.order?.receiverPhone }}</a-descriptions-item>
|
||||
<a-descriptions-item label="地址">{{ detail.order?.receiverAddress }}</a-descriptions-item>
|
||||
<a-descriptions-item label="物流单号">{{ detail.order?.logisticsNo || '-' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-table
|
||||
:dataSource="detail.items"
|
||||
:columns="itemColumns"
|
||||
rowKey="id"
|
||||
style="margin-top: 16px"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'product'">
|
||||
{{ record.product.name }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -18,6 +45,8 @@ import { onMounted, ref } from 'vue'
|
||||
import api from '../../api'
|
||||
|
||||
const orders = ref([])
|
||||
const detailVisible = ref(false)
|
||||
const detail = ref({ order: null, items: [] })
|
||||
const columns = [
|
||||
{ title: '订单号', dataIndex: 'orderNo' },
|
||||
{ title: '金额', dataIndex: 'totalAmount' },
|
||||
@@ -25,6 +54,12 @@ const columns = [
|
||||
{ title: '操作', key: 'action' }
|
||||
]
|
||||
|
||||
const itemColumns = [
|
||||
{ title: '商品', key: 'product' },
|
||||
{ title: '数量', dataIndex: 'quantity' },
|
||||
{ title: '单价', dataIndex: 'price' }
|
||||
]
|
||||
|
||||
const statusMap = {
|
||||
PENDING_PAYMENT: '待付款',
|
||||
PENDING_SHIPMENT: '待发货',
|
||||
@@ -42,5 +77,13 @@ const pay = async (record) => {
|
||||
load()
|
||||
}
|
||||
|
||||
const viewDetail = async (record) => {
|
||||
const res = await api.get(`/api/user/orders/${record.id}`)
|
||||
if (res.success) {
|
||||
detail.value = res.data
|
||||
detailVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<a-col :span="10">
|
||||
<a-carousel>
|
||||
<div v-for="img in images" :key="img.id" class="banner">
|
||||
<img :src="img.url" alt="img" />
|
||||
<img :src="previewUrl(img.url)" alt="img" />
|
||||
</div>
|
||||
</a-carousel>
|
||||
</a-col>
|
||||
@@ -39,6 +39,7 @@ const product = ref({})
|
||||
const images = ref([])
|
||||
const quantity = ref(1)
|
||||
|
||||
const baseUrl = 'http://localhost:8080'
|
||||
const load = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
@@ -61,6 +62,12 @@ const addToCart = async () => {
|
||||
message.success('已加入购物车')
|
||||
}
|
||||
|
||||
const previewUrl = (url) => {
|
||||
if (!url) return ''
|
||||
if (url.startsWith('http')) return url
|
||||
return `${baseUrl}${url}`
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
|
||||