This commit is contained in:
王子琦
2026-01-14 14:27:39 +08:00
parent f567e733d3
commit 2343001168
29 changed files with 262 additions and 30 deletions

View File

@@ -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))
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -1,14 +1,16 @@
<template>
<a-card title="订单确认">
<a-list :data-source="addresses">
<template #renderItem="{ item }">
<a-list-item>
<a-radio v-model:checked="selected" :value="item.id">
{{ item.receiverName }} {{ item.receiverPhone }} {{ item.detail }}
</a-radio>
</a-list-item>
</template>
</a-list>
<a-radio-group v-model:value="selected">
<a-list :data-source="addresses">
<template #renderItem="{ item }">
<a-list-item>
<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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>