upd
@@ -39,7 +39,7 @@ public class SecurityConfig {
|
|||||||
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||||
.requestMatchers("/api/auth/**", "/api/public/**", "/error").permitAll()
|
.requestMatchers("/api/auth/**", "/api/public/**", "/files/**", "/error").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
.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.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|||||||
@@ -81,7 +81,14 @@ public class AdminController {
|
|||||||
|
|
||||||
@GetMapping("/products")
|
@GetMapping("/products")
|
||||||
public ApiResponse<List<Product>> 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")
|
@PostMapping("/products")
|
||||||
@@ -148,6 +155,16 @@ public class AdminController {
|
|||||||
return ApiResponse.ok("删除成功", null);
|
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")
|
@GetMapping("/orders")
|
||||||
public ApiResponse<List<Order>> orders() {
|
public ApiResponse<List<Order>> orders() {
|
||||||
return ApiResponse.ok(orderRepository.findAll());
|
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 com.toyshop.dto.ApiResponse;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.toyshop.entity;
|
package com.toyshop.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ public class Address {
|
|||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "user_id", nullable = false)
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
@JsonIgnore
|
||||||
private User user;
|
private User user;
|
||||||
|
|
||||||
@Column(nullable = false, length = 50)
|
@Column(nullable = false, length = 50)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.toyshop.entity;
|
package com.toyshop.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -11,10 +13,12 @@ public class CartItem {
|
|||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "user_id", nullable = false)
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
@JsonIgnore
|
||||||
private User user;
|
private User user;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
@JoinColumn(name = "product_id", nullable = false)
|
@JoinColumn(name = "product_id", nullable = false)
|
||||||
|
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
|
||||||
private Product product;
|
private Product product;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.toyshop.entity;
|
package com.toyshop.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -16,6 +17,7 @@ public class Order {
|
|||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "user_id", nullable = false)
|
@JoinColumn(name = "user_id", nullable = false)
|
||||||
|
@JsonIgnore
|
||||||
private User user;
|
private User user;
|
||||||
|
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package com.toyshop.entity;
|
package com.toyshop.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
@@ -12,10 +14,12 @@ public class OrderItem {
|
|||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "order_id", nullable = false)
|
@JoinColumn(name = "order_id", nullable = false)
|
||||||
|
@JsonIgnore
|
||||||
private Order order;
|
private Order order;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
@JoinColumn(name = "product_id", nullable = false)
|
@JoinColumn(name = "product_id", nullable = false)
|
||||||
|
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
|
||||||
private Product product;
|
private Product product;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ public class Product {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
@PrePersist
|
@PrePersist
|
||||||
public void prePersist() {
|
public void prePersist() {
|
||||||
LocalDateTime now = LocalDateTime.now();
|
LocalDateTime now = LocalDateTime.now();
|
||||||
@@ -81,4 +84,6 @@ public class Product {
|
|||||||
public void setOnSale(boolean onSale) { this.onSale = onSale; }
|
public void setOnSale(boolean onSale) { this.onSale = onSale; }
|
||||||
public LocalDateTime getCreatedAt() { return createdAt; }
|
public LocalDateTime getCreatedAt() { return createdAt; }
|
||||||
public LocalDateTime getUpdatedAt() { return updatedAt; }
|
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;
|
package com.toyshop.entity;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -11,6 +12,7 @@ public class ProductImage {
|
|||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "product_id", nullable = false)
|
@JoinColumn(name = "product_id", nullable = false)
|
||||||
|
@JsonIgnore
|
||||||
private Product product;
|
private Product product;
|
||||||
|
|
||||||
@Column(nullable = false, length = 255)
|
@Column(nullable = false, length = 255)
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import com.toyshop.entity.ProductImage;
|
|||||||
import com.toyshop.entity.Product;
|
import com.toyshop.entity.Product;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface ProductImageRepository extends JpaRepository<ProductImage, Long> {
|
public interface ProductImageRepository extends JpaRepository<ProductImage, Long> {
|
||||||
List<ProductImage> findByProduct(Product product);
|
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.entity.*;
|
||||||
import com.toyshop.repository.*;
|
import com.toyshop.repository.*;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -23,6 +24,7 @@ public class OrderService {
|
|||||||
this.addressRepository = addressRepository;
|
this.addressRepository = addressRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
public Order createOrder(User user, Long addressId) {
|
public Order createOrder(User user, Long addressId) {
|
||||||
Address address = addressRepository.findById(addressId).orElseThrow();
|
Address address = addressRepository.findById(addressId).orElseThrow();
|
||||||
if (!address.getUser().getId().equals(user.getId())) {
|
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.domain.Sort;
|
||||||
import org.springframework.data.jpa.domain.Specification;
|
import org.springframework.data.jpa.domain.Specification;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import jakarta.persistence.criteria.Predicate;
|
import jakarta.persistence.criteria.Predicate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -59,4 +60,18 @@ public class ProductService {
|
|||||||
public List<Category> allCategories() {
|
public List<Category> allCategories() {
|
||||||
return categoryRepository.findAll();
|
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 axios from 'axios'
|
||||||
|
import router from '../router'
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: 'http://localhost:8080',
|
baseURL: 'http://localhost:8080',
|
||||||
@@ -16,6 +17,14 @@ api.interceptors.request.use((config) => {
|
|||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(res) => res.data,
|
(res) => res.data,
|
||||||
(err) => {
|
(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 || '请求失败'
|
const message = err?.response?.data?.message || '请求失败'
|
||||||
return Promise.reject(new Error(message))
|
return Promise.reject(new Error(message))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,15 @@
|
|||||||
</a-form>
|
</a-form>
|
||||||
<a-table :dataSource="list" :columns="columns" rowKey="id" style="margin-top: 16px">
|
<a-table :dataSource="list" :columns="columns" rowKey="id" style="margin-top: 16px">
|
||||||
<template #bodyCell="{ column, record }">
|
<template #bodyCell="{ column, record }">
|
||||||
|
<template v-if="column.key === 'image'">
|
||||||
|
<img :src="previewUrl(record.imageUrl)" class="thumb" />
|
||||||
|
</template>
|
||||||
<template v-if="column.key === 'action'">
|
<template v-if="column.key === 'action'">
|
||||||
<a-button type="link" danger @click="remove(record)">删除</a-button>
|
<a-button type="link" danger @click="remove(record)">删除</a-button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
<img v-if="form.imageUrl" :src="previewUrl(form.imageUrl)" class="preview" />
|
||||||
</a-card>
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -27,10 +31,11 @@ import api from '../../api'
|
|||||||
|
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
const form = reactive({ imageUrl: '', linkUrl: '', sortOrder: 0 })
|
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 headers = { Authorization: `Bearer ${localStorage.getItem('token') || ''}` }
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '图片', dataIndex: 'imageUrl' },
|
{ title: '图片', key: 'image' },
|
||||||
{ title: '链接', dataIndex: 'linkUrl' },
|
{ title: '链接', dataIndex: 'linkUrl' },
|
||||||
{ title: '排序', dataIndex: 'sortOrder' },
|
{ title: '排序', dataIndex: 'sortOrder' },
|
||||||
{ title: '操作', key: 'action' }
|
{ title: '操作', key: 'action' }
|
||||||
@@ -53,11 +58,19 @@ const onUpload = (info) => {
|
|||||||
if (info.file.status === 'done') {
|
if (info.file.status === 'done') {
|
||||||
const res = info.file.response
|
const res = info.file.response
|
||||||
if (res && res.success) {
|
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) => {
|
const remove = async (record) => {
|
||||||
await api.delete(`/api/admin/carousels/${record.id}`)
|
await api.delete(`/api/admin/carousels/${record.id}`)
|
||||||
load()
|
load()
|
||||||
@@ -65,3 +78,18 @@ const remove = async (record) => {
|
|||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</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-form>
|
||||||
<a-table :dataSource="list" :columns="columns" rowKey="id" style="margin-top: 16px">
|
<a-table :dataSource="list" :columns="columns" rowKey="id" style="margin-top: 16px">
|
||||||
<template #bodyCell="{ column, record }">
|
<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'">
|
<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>
|
<a-button type="link" danger @click="remove(record)">删除</a-button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</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-upload :action="uploadUrl" :headers="headers" @change="onUpload" :showUploadList="false">
|
||||||
<a-button>上传图片</a-button>
|
<a-button>上传图片</a-button>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
<a-input v-model:value="imgForm.url" placeholder="图片URL" style="margin-top: 8px" />
|
<img v-if="imgForm.url" :src="previewUrl(imgForm.url)" class="preview" />
|
||||||
<a-input-number v-model:value="imgForm.sortOrder" style="margin-top: 8px" />
|
</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-modal>
|
||||||
</a-card>
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
@@ -40,13 +63,27 @@ const columns = [
|
|||||||
{ title: '名称', dataIndex: 'name' },
|
{ title: '名称', dataIndex: 'name' },
|
||||||
{ title: '价格', dataIndex: 'price' },
|
{ title: '价格', dataIndex: 'price' },
|
||||||
{ title: '库存', dataIndex: 'stock' },
|
{ title: '库存', dataIndex: 'stock' },
|
||||||
|
{ title: '图片', key: 'image' },
|
||||||
{ title: '操作', key: 'action' }
|
{ title: '操作', key: 'action' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const imgVisible = ref(false)
|
const imgVisible = ref(false)
|
||||||
const imgForm = reactive({ productId: null, url: '', sortOrder: 0 })
|
const imgForm = reactive({ productId: null, url: '' })
|
||||||
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 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 load = async () => {
|
||||||
const res = await api.get('/api/admin/products')
|
const res = await api.get('/api/admin/products')
|
||||||
@@ -71,24 +108,67 @@ const remove = async (record) => {
|
|||||||
|
|
||||||
const addImage = (record) => {
|
const addImage = (record) => {
|
||||||
imgForm.productId = record.id
|
imgForm.productId = record.id
|
||||||
imgForm.url = ''
|
imgForm.url = record.imageUrl || ''
|
||||||
imgForm.sortOrder = 0
|
|
||||||
imgVisible.value = true
|
imgVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveImage = async () => {
|
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
|
imgVisible.value = false
|
||||||
|
load()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onUpload = (info) => {
|
const onUpload = (info) => {
|
||||||
if (info.file.status === 'done') {
|
if (info.file.status === 'done') {
|
||||||
const res = info.file.response
|
const res = info.file.response
|
||||||
if (res && res.success) {
|
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)
|
onMounted(load)
|
||||||
</script>
|
</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>
|
<template>
|
||||||
<a-card title="订单确认">
|
<a-card title="订单确认">
|
||||||
<a-list :data-source="addresses">
|
<a-radio-group v-model:value="selected">
|
||||||
<template #renderItem="{ item }">
|
<a-list :data-source="addresses">
|
||||||
<a-list-item>
|
<template #renderItem="{ item }">
|
||||||
<a-radio v-model:checked="selected" :value="item.id">
|
<a-list-item>
|
||||||
{{ item.receiverName }} {{ item.receiverPhone }} {{ item.detail }}
|
<a-radio :value="item.id">
|
||||||
</a-radio>
|
{{ item.receiverName }} {{ item.receiverPhone }} {{ item.detail }}
|
||||||
</a-list-item>
|
</a-radio>
|
||||||
</template>
|
</a-list-item>
|
||||||
</a-list>
|
</template>
|
||||||
|
</a-list>
|
||||||
|
</a-radio-group>
|
||||||
<a-button type="primary" @click="createOrder" :disabled="!selected">提交订单</a-button>
|
<a-button type="primary" @click="createOrder" :disabled="!selected">提交订单</a-button>
|
||||||
</a-card>
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<a-card title="轮播与公告">
|
<a-card title="轮播与公告">
|
||||||
<a-carousel autoplay>
|
<a-carousel autoplay>
|
||||||
<div v-for="item in home.carousels" :key="item.id" class="banner">
|
<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>
|
</div>
|
||||||
</a-carousel>
|
</a-carousel>
|
||||||
<a-list :data-source="home.notices" style="margin-top: 16px">
|
<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 home = reactive({ carousels: [], notices: [], hot: [], newest: [] })
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
const baseUrl = 'http://localhost:8080'
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -58,6 +59,12 @@ const load = async () => {
|
|||||||
|
|
||||||
const goDetail = (id) => router.push(`/products/${id}`)
|
const goDetail = (id) => router.push(`/products/${id}`)
|
||||||
|
|
||||||
|
const previewUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
return `${baseUrl}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,38 @@
|
|||||||
{{ statusMap[record.status] || record.status }}
|
{{ statusMap[record.status] || record.status }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="column.key === 'action'">
|
<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>
|
<a-button type="link" v-if="record.status === 'PENDING_PAYMENT'" @click="pay(record)">去支付</a-button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -18,6 +45,8 @@ import { onMounted, ref } from 'vue'
|
|||||||
import api from '../../api'
|
import api from '../../api'
|
||||||
|
|
||||||
const orders = ref([])
|
const orders = ref([])
|
||||||
|
const detailVisible = ref(false)
|
||||||
|
const detail = ref({ order: null, items: [] })
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '订单号', dataIndex: 'orderNo' },
|
{ title: '订单号', dataIndex: 'orderNo' },
|
||||||
{ title: '金额', dataIndex: 'totalAmount' },
|
{ title: '金额', dataIndex: 'totalAmount' },
|
||||||
@@ -25,6 +54,12 @@ const columns = [
|
|||||||
{ title: '操作', key: 'action' }
|
{ title: '操作', key: 'action' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const itemColumns = [
|
||||||
|
{ title: '商品', key: 'product' },
|
||||||
|
{ title: '数量', dataIndex: 'quantity' },
|
||||||
|
{ title: '单价', dataIndex: 'price' }
|
||||||
|
]
|
||||||
|
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
PENDING_PAYMENT: '待付款',
|
PENDING_PAYMENT: '待付款',
|
||||||
PENDING_SHIPMENT: '待发货',
|
PENDING_SHIPMENT: '待发货',
|
||||||
@@ -42,5 +77,13 @@ const pay = async (record) => {
|
|||||||
load()
|
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)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<a-col :span="10">
|
<a-col :span="10">
|
||||||
<a-carousel>
|
<a-carousel>
|
||||||
<div v-for="img in images" :key="img.id" class="banner">
|
<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>
|
</div>
|
||||||
</a-carousel>
|
</a-carousel>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -39,6 +39,7 @@ const product = ref({})
|
|||||||
const images = ref([])
|
const images = ref([])
|
||||||
const quantity = ref(1)
|
const quantity = ref(1)
|
||||||
|
|
||||||
|
const baseUrl = 'http://localhost:8080'
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
@@ -61,6 +62,12 @@ const addToCart = async () => {
|
|||||||
message.success('已加入购物车')
|
message.success('已加入购物车')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewUrl = (url) => {
|
||||||
|
if (!url) return ''
|
||||||
|
if (url.startsWith('http')) return url
|
||||||
|
return `${baseUrl}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(load)
|
onMounted(load)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||