forked from Qi/Oroqen-Vue
整体优化
This commit is contained in:
parent
eb7ced99d4
commit
29fd6a779b
3
auto-imports.d.ts
vendored
3
auto-imports.d.ts
vendored
@ -5,5 +5,6 @@
|
||||
// Generated by unplugin-auto-import
|
||||
export {}
|
||||
declare global {
|
||||
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const ElMessageBox: typeof import('element-plus/es')['ElMessageBox']
|
||||
}
|
||||
|
1
components.d.ts
vendored
1
components.d.ts
vendored
@ -29,5 +29,6 @@ declare module 'vue' {
|
||||
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
VideoPlayer: typeof import('./src/components/VideoPlayer.vue')['default']
|
||||
}
|
||||
}
|
||||
|
35
src/api/cart.ts
Normal file
35
src/api/cart.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import api from './index'
|
||||
|
||||
// 购物车接口
|
||||
export const cartApi = {
|
||||
// 获取用户购物车
|
||||
getCartByUserId: (userId: string) => {
|
||||
return api.get(`/oroqen/cart/user/${userId}`)
|
||||
},
|
||||
|
||||
// 添加到购物车
|
||||
addToCart: (userId: string, productId: string, quantity: number) => {
|
||||
return api.post('/oroqen/cart/add', null, {
|
||||
params: { userId, productId, quantity }
|
||||
})
|
||||
},
|
||||
|
||||
// 更新购物车商品数量
|
||||
updateCartQuantity: (userId: string, productId: string, quantity: number) => {
|
||||
return api.post('/oroqen/cart/update', null, {
|
||||
params: { userId, productId, quantity }
|
||||
})
|
||||
},
|
||||
|
||||
// 从购物车移除商品
|
||||
removeFromCart: (userId: string, productId: string) => {
|
||||
return api.post('/oroqen/cart/remove', null, {
|
||||
params: { userId, productId }
|
||||
})
|
||||
},
|
||||
|
||||
// 清空购物车
|
||||
clearCart: (userId: string) => {
|
||||
return api.post(`/oroqen/cart/clear/${userId}`)
|
||||
}
|
||||
}
|
52
src/api/culture.ts
Normal file
52
src/api/culture.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import api from './index'
|
||||
|
||||
// 文化内容接口
|
||||
export const cultureApi = {
|
||||
// 获取文化内容列表
|
||||
getCultureList: (params: any) => {
|
||||
return api.get('/oroqen/cultureContent/list', { params })
|
||||
},
|
||||
|
||||
// 获取文化内容详情
|
||||
getCultureById: (id: string) => {
|
||||
return api.get('/oroqen/cultureContent/queryById', { params: { id } })
|
||||
},
|
||||
|
||||
// 获取推荐文化内容
|
||||
getRecommendedCulture: () => {
|
||||
return api.get('/oroqen/cultureContent/recommended')
|
||||
},
|
||||
|
||||
// 根据分类获取文化内容
|
||||
getCultureByCategory: (categoryId: string, params: any) => {
|
||||
return api.get(`/oroqen/cultureContent/category/${categoryId}`, { params })
|
||||
},
|
||||
|
||||
// 点赞文化内容
|
||||
likeCulture: (id: string) => {
|
||||
return api.post(`/oroqen/cultureContent/like/${id}`)
|
||||
},
|
||||
|
||||
// 取消点赞
|
||||
unlikeCulture: (id: string) => {
|
||||
return api.post(`/oroqen/cultureContent/unlike/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 文化内容分类接口
|
||||
export const cultureCategoryApi = {
|
||||
// 获取分类树
|
||||
getCategoryTree: () => {
|
||||
return api.get('/oroqen/cultureCategory/tree')
|
||||
},
|
||||
|
||||
// 获取分类列表
|
||||
getCategoryList: (params: any) => {
|
||||
return api.get('/oroqen/cultureCategory/list', { params })
|
||||
},
|
||||
|
||||
// 获取子分类
|
||||
getChildCategories: (parentId: string) => {
|
||||
return api.get(`/oroqen/cultureCategory/children/${parentId}`)
|
||||
}
|
||||
}
|
32
src/api/favorite.ts
Normal file
32
src/api/favorite.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import api from './index'
|
||||
|
||||
// 用户收藏接口
|
||||
export const favoriteApi = {
|
||||
// 获取用户收藏列表
|
||||
getFavoritesByUserId: (userId: string, favoriteType?: string) => {
|
||||
const params: any = { userId }
|
||||
if (favoriteType) params.favoriteType = favoriteType
|
||||
return api.get('/oroqen/userFavorite/user', { params })
|
||||
},
|
||||
|
||||
// 添加收藏
|
||||
addFavorite: (userId: string, itemId: string, favoriteType: string) => {
|
||||
return api.post('/oroqen/userFavorite/add', null, {
|
||||
params: { userId, itemId, favoriteType }
|
||||
})
|
||||
},
|
||||
|
||||
// 取消收藏
|
||||
removeFavorite: (userId: string, itemId: string, favoriteType: string) => {
|
||||
return api.post('/oroqen/userFavorite/remove', null, {
|
||||
params: { userId, itemId, favoriteType }
|
||||
})
|
||||
},
|
||||
|
||||
// 检查是否已收藏
|
||||
checkFavoriteStatus: (userId: string, itemId: string, favoriteType: string) => {
|
||||
return api.get('/oroqen/userFavorite/status', {
|
||||
params: { userId, itemId, favoriteType }
|
||||
})
|
||||
}
|
||||
}
|
52
src/api/heritage.ts
Normal file
52
src/api/heritage.ts
Normal file
@ -0,0 +1,52 @@
|
||||
import api from './index'
|
||||
|
||||
// 非遗传承人接口
|
||||
export const heritageApi = {
|
||||
// 获取传承人列表
|
||||
getInheritorList: (params: any) => {
|
||||
return api.get('/oroqen/heritageInheritor/list', { params })
|
||||
},
|
||||
|
||||
// 获取传承人详情
|
||||
getInheritorById: (id: string) => {
|
||||
return api.get('/oroqen/heritageInheritor/queryById', { params: { id } })
|
||||
},
|
||||
|
||||
// 获取所有传承人(不分页)
|
||||
getAllInheritors: () => {
|
||||
return api.get('/oroqen/heritageInheritor/list', {
|
||||
params: { pageNo: 1, pageSize: 1000 }
|
||||
})
|
||||
},
|
||||
|
||||
// 非遗项目接口
|
||||
// 获取非遗项目列表
|
||||
getHeritageProjectList: (params: any) => {
|
||||
return api.get('/oroqen/heritageProject/list', { params })
|
||||
},
|
||||
|
||||
// 获取推荐项目列表
|
||||
getRecommendedProjects: () => {
|
||||
return api.get('/oroqen/heritageProject/recommended')
|
||||
},
|
||||
|
||||
// 按级别获取项目列表
|
||||
getProjectsByLevel: (level: string) => {
|
||||
return api.get('/oroqen/heritageProject/byLevel', { params: { level } })
|
||||
},
|
||||
|
||||
// 按类别获取项目列表
|
||||
getProjectsByCategory: (category: string) => {
|
||||
return api.get('/oroqen/heritageProject/byCategory', { params: { category } })
|
||||
},
|
||||
|
||||
// 获取项目详情
|
||||
getProjectById: (id: string) => {
|
||||
return api.get('/oroqen/heritageProject/queryById', { params: { id } })
|
||||
},
|
||||
|
||||
// 获取级别统计
|
||||
getLevelStats: () => {
|
||||
return api.get('/oroqen/heritageProject/levelStats')
|
||||
}
|
||||
}
|
47
src/api/index.ts
Normal file
47
src/api/index.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const api = axios.create({
|
||||
baseURL: 'http://localhost:8080/jeecg-boot', // 后端API地址
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
api.interceptors.request.use(
|
||||
config => {
|
||||
// 可以在这里添加token等认证信息
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
api.interceptors.response.use(
|
||||
response => {
|
||||
const { data } = response
|
||||
if (data.success) {
|
||||
return data.result || data
|
||||
} else {
|
||||
// 创建一个包含完整错误信息的错误对象
|
||||
const error = new Error(data.message || '请求失败')
|
||||
error.code = data.code
|
||||
error.data = data
|
||||
return Promise.reject(error)
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('API Error:', error)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
39
src/api/order.ts
Normal file
39
src/api/order.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import api from './index'
|
||||
|
||||
// 订单接口
|
||||
export const orderApi = {
|
||||
// 获取用户订单列表
|
||||
getOrdersByUserId: (userId: string, status?: string) => {
|
||||
const params: any = { userId }
|
||||
if (status) params.status = status
|
||||
return api.get('/oroqen/order/user', { params })
|
||||
},
|
||||
|
||||
// 创建订单
|
||||
createOrder: (orderData: any) => {
|
||||
return api.post('/oroqen/order/create', orderData)
|
||||
},
|
||||
|
||||
// 获取订单详情
|
||||
getOrderById: (id: string) => {
|
||||
return api.get('/oroqen/order/queryById', { params: { id } })
|
||||
},
|
||||
|
||||
// 取消订单
|
||||
cancelOrder: (id: string) => {
|
||||
return api.post(`/oroqen/order/cancel/${id}`)
|
||||
},
|
||||
|
||||
// 确认收货
|
||||
confirmOrder: (id: string) => {
|
||||
return api.post(`/oroqen/order/confirm/${id}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 订单项接口
|
||||
export const orderItemApi = {
|
||||
// 获取订单项列表
|
||||
getOrderItemsByOrderId: (orderId: string) => {
|
||||
return api.get(`/oroqen/orderItem/order/${orderId}`)
|
||||
}
|
||||
}
|
47
src/api/product.ts
Normal file
47
src/api/product.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import api from './index'
|
||||
|
||||
// 产品接口
|
||||
export const productApi = {
|
||||
// 获取产品列表
|
||||
getProductList: (params: any) => {
|
||||
return api.get('/oroqen/product/list', { params })
|
||||
},
|
||||
|
||||
// 获取产品详情
|
||||
getProductById: (id: string | number) => {
|
||||
return api.get('/oroqen/product/queryById', { params: { id: id.toString() } })
|
||||
},
|
||||
|
||||
// 获取推荐产品
|
||||
getFeaturedProducts: () => {
|
||||
return api.get('/oroqen/product/featured')
|
||||
},
|
||||
|
||||
// 获取热销产品
|
||||
getHotProducts: () => {
|
||||
return api.get('/oroqen/product/hot')
|
||||
},
|
||||
|
||||
// 根据分类获取产品
|
||||
getProductsByCategory: (categoryId: string, params: any) => {
|
||||
return api.get(`/oroqen/product/category/${categoryId}`, { params })
|
||||
}
|
||||
}
|
||||
|
||||
// 产品分类接口
|
||||
export const productCategoryApi = {
|
||||
// 获取分类树
|
||||
getCategoryTree: () => {
|
||||
return api.get('/oroqen/productCategory/tree')
|
||||
},
|
||||
|
||||
// 获取分类列表
|
||||
getCategoryList: (params: any) => {
|
||||
return api.get('/oroqen/productCategory/list', { params })
|
||||
},
|
||||
|
||||
// 获取子分类
|
||||
getChildCategories: (parentId: string) => {
|
||||
return api.get(`/oroqen/productCategory/children/${parentId}`)
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Home from '../views/Home.vue'
|
||||
import Culture from '../views/Culture.vue'
|
||||
import CultureDetail from '../views/CultureDetail.vue'
|
||||
import Heritage from '../views/Heritage.vue'
|
||||
import Products from '../views/Products.vue'
|
||||
import ProductDetail from '../views/ProductDetail.vue'
|
||||
@ -19,6 +20,11 @@ const routes = [
|
||||
name: 'Culture',
|
||||
component: Culture
|
||||
},
|
||||
{
|
||||
path: '/culture/:id',
|
||||
name: 'CultureDetail',
|
||||
component: CultureDetail
|
||||
},
|
||||
{
|
||||
path: '/heritage',
|
||||
name: 'Heritage',
|
||||
|
@ -196,50 +196,145 @@ import { useRouter } from 'vue-router'
|
||||
import { useMainStore } from '../stores'
|
||||
import { User, ShoppingCart } from '@element-plus/icons-vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { cartApi } from '../api/cart'
|
||||
import { productApi } from '../api/product'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useMainStore()
|
||||
|
||||
// 响应式数据
|
||||
const selectAll = ref(false)
|
||||
const loading = ref(false)
|
||||
const cartItems = ref<any[]>([])
|
||||
|
||||
// 推荐商品
|
||||
const recommendedProducts = ref([
|
||||
{
|
||||
id: 2,
|
||||
name: '鄂伦春族刺绣挂画',
|
||||
price: 288,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '手工木雕摆件',
|
||||
price: 128,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '野生蓝莓干',
|
||||
price: 58,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '鄂伦春文化笔记本',
|
||||
price: 35,
|
||||
color: '#8b4513'
|
||||
const recommendedProducts = ref<any[]>([])
|
||||
|
||||
// 获取购物车数据
|
||||
const getCartData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
// 这里应该从用户信息中获取userId,暂时使用固定值
|
||||
const userId = 1
|
||||
const response = await cartApi.getUserCart(userId)
|
||||
|
||||
if (response && response.length > 0) {
|
||||
cartItems.value = response.map((item: any) => ({
|
||||
id: item.id,
|
||||
productId: item.productId,
|
||||
name: item.productName || '商品名称',
|
||||
price: item.price || 0,
|
||||
quantity: item.quantity || 1,
|
||||
stock: item.stock || 99,
|
||||
specs: item.specs || '规格:标准版',
|
||||
craftsman: item.craftsman,
|
||||
originalPrice: item.originalPrice,
|
||||
selected: true,
|
||||
color: getProductColor(item.productId)
|
||||
}))
|
||||
}
|
||||
|
||||
// 如果没有数据,使用默认数据
|
||||
if (cartItems.value.length === 0) {
|
||||
cartItems.value = getDefaultCartItems()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取购物车数据失败:', error)
|
||||
// 使用默认数据
|
||||
cartItems.value = getDefaultCartItems()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// 获取推荐商品
|
||||
const getRecommendedProducts = async () => {
|
||||
try {
|
||||
const response = await productApi.getHotProducts()
|
||||
|
||||
if (response?.records && response.records.length > 0) {
|
||||
recommendedProducts.value = response.records.slice(0, 4).map((product: any) => ({
|
||||
id: product.id,
|
||||
name: product.productName,
|
||||
price: product.price,
|
||||
color: getProductColor(product.id)
|
||||
}))
|
||||
}
|
||||
|
||||
// 如果没有数据,使用默认数据
|
||||
if (recommendedProducts.value.length === 0) {
|
||||
recommendedProducts.value = getDefaultRecommendedProducts()
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取推荐商品失败:', error)
|
||||
recommendedProducts.value = getDefaultRecommendedProducts()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取默认购物车数据
|
||||
const getDefaultCartItems = () => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
productId: 1,
|
||||
name: '精美桦皮首饰盒',
|
||||
price: 168,
|
||||
quantity: 1,
|
||||
stock: 15,
|
||||
specs: '规格:标准版',
|
||||
craftsman: '关小云',
|
||||
originalPrice: 198,
|
||||
selected: true,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
productId: 3,
|
||||
name: '手工木雕摆件',
|
||||
price: 128,
|
||||
quantity: 2,
|
||||
stock: 8,
|
||||
specs: '规格:中号',
|
||||
craftsman: '关金山',
|
||||
selected: true,
|
||||
color: '#2d5016'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 获取默认推荐商品
|
||||
const getDefaultRecommendedProducts = () => {
|
||||
return [
|
||||
{
|
||||
id: 2,
|
||||
name: '鄂伦春族刺绣挂画',
|
||||
price: 288,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '手工木雕摆件',
|
||||
price: 128,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '野生蓝莓干',
|
||||
price: 58,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '鄂伦春文化笔记本',
|
||||
price: 35,
|
||||
color: '#8b4513'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const cartItems = computed(() => {
|
||||
return store.cartItems.map(item => ({
|
||||
...item,
|
||||
selected: item.selected !== false, // 默认选中
|
||||
color: getProductColor(item.id)
|
||||
}))
|
||||
})
|
||||
|
||||
const selectedItems = computed(() => {
|
||||
return cartItems.value.filter(item => item.selected)
|
||||
})
|
||||
@ -265,36 +360,38 @@ const handleSelectAll = (checked: boolean) => {
|
||||
cartItems.value.forEach(item => {
|
||||
item.selected = checked
|
||||
})
|
||||
updateCartSelection()
|
||||
}
|
||||
|
||||
const updateSelection = () => {
|
||||
const selectedCount = cartItems.value.filter(item => item.selected).length
|
||||
selectAll.value = selectedCount === cartItems.value.length
|
||||
updateCartSelection()
|
||||
}
|
||||
|
||||
const updateCartSelection = () => {
|
||||
// 更新store中的选中状态
|
||||
cartItems.value.forEach(item => {
|
||||
const storeItem = store.cartItems.find(si => si.id === item.id)
|
||||
if (storeItem) {
|
||||
storeItem.selected = item.selected
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const increaseQuantity = (item: any) => {
|
||||
const increaseQuantity = async (item: any) => {
|
||||
if (item.quantity < item.stock) {
|
||||
store.updateCartItemQuantity(item.id, item.quantity + 1)
|
||||
try {
|
||||
await cartApi.updateQuantity(item.id, item.quantity + 1)
|
||||
item.quantity += 1
|
||||
ElMessage.success('数量已更新')
|
||||
} catch (error) {
|
||||
console.error('更新数量失败:', error)
|
||||
ElMessage.error('更新失败')
|
||||
}
|
||||
} else {
|
||||
ElMessage.warning('库存不足')
|
||||
}
|
||||
}
|
||||
|
||||
const decreaseQuantity = (item: any) => {
|
||||
const decreaseQuantity = async (item: any) => {
|
||||
if (item.quantity > 1) {
|
||||
store.updateCartItemQuantity(item.id, item.quantity - 1)
|
||||
try {
|
||||
await cartApi.updateQuantity(item.id, item.quantity - 1)
|
||||
item.quantity -= 1
|
||||
ElMessage.success('数量已更新')
|
||||
} catch (error) {
|
||||
console.error('更新数量失败:', error)
|
||||
ElMessage.error('更新失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -310,19 +407,33 @@ const removeFromCart = async (itemId: number) => {
|
||||
}
|
||||
)
|
||||
|
||||
store.removeFromCart(itemId)
|
||||
await cartApi.removeItem(itemId)
|
||||
cartItems.value = cartItems.value.filter(item => item.id !== itemId)
|
||||
ElMessage.success('商品已删除')
|
||||
|
||||
// 重新计算全选状态
|
||||
updateSelection()
|
||||
} catch {
|
||||
// 用户取消删除
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
console.error('删除商品失败:', error)
|
||||
ElMessage.error('删除失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const addRecommendedToCart = (product: any) => {
|
||||
store.addToCart(product)
|
||||
ElMessage.success('已添加到购物车')
|
||||
const addRecommendedToCart = async (product: any) => {
|
||||
try {
|
||||
const userId = 1 // 这里应该从用户信息中获取
|
||||
await cartApi.addToCart({
|
||||
userId,
|
||||
productId: product.id,
|
||||
quantity: 1
|
||||
})
|
||||
ElMessage.success('已添加到购物车')
|
||||
} catch (error) {
|
||||
console.error('添加到购物车失败:', error)
|
||||
ElMessage.error('添加失败')
|
||||
}
|
||||
}
|
||||
|
||||
const checkout = () => {
|
||||
@ -338,8 +449,8 @@ const checkout = () => {
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 初始化选中状态
|
||||
updateSelection()
|
||||
getCartData()
|
||||
getRecommendedProducts()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -100,6 +100,45 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 文化故事展示 -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<h2 class="section-title">文化故事</h2>
|
||||
<div class="culture-stories-grid" v-if="cultureStories.length > 0">
|
||||
<div
|
||||
v-for="story in cultureStories"
|
||||
:key="story.id"
|
||||
class="culture-story-card card"
|
||||
@click="goToCultureDetail(story.id)"
|
||||
>
|
||||
<div class="story-icon">
|
||||
<svg width="60" height="60" viewBox="0 0 60 60">
|
||||
<rect width="60" height="60" :fill="story.color" opacity="0.1" rx="12"/>
|
||||
<text x="30" y="38" text-anchor="middle"
|
||||
:fill="story.color" font-size="24" font-weight="bold">
|
||||
{{ story.title ? story.title.charAt(0) : '文' }}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="story-content">
|
||||
<h3 class="story-title">{{ story.title }}</h3>
|
||||
<p class="story-excerpt">{{ story.excerpt }}</p>
|
||||
<div class="story-meta">
|
||||
<span class="story-date">{{ story.date }}</span>
|
||||
<span class="story-category" v-if="story.categoryName">{{ story.categoryName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="empty-state" v-else-if="!loading">
|
||||
<p>暂无文化故事内容</p>
|
||||
</div>
|
||||
<div class="loading-state" v-if="loading">
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 多媒体资源库 -->
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
@ -139,26 +178,43 @@
|
||||
<section class="section" style="background-color: var(--background-light);">
|
||||
<div class="container">
|
||||
<h2 class="section-title">文化传承人</h2>
|
||||
<div class="inheritors-grid">
|
||||
<div class="inheritors-grid" v-if="inheritors.length > 0">
|
||||
<div v-for="inheritor in inheritors" :key="inheritor.id" class="inheritor-card card">
|
||||
<div class="inheritor-avatar">
|
||||
<div class="avatar-placeholder">
|
||||
<svg viewBox="0 0 120 120" width="120" height="120">
|
||||
<circle cx="60" cy="60" r="50" fill="var(--primary-color)" opacity="0.2"/>
|
||||
<circle cx="60" cy="45" r="15" fill="var(--primary-color)" opacity="0.5"/>
|
||||
<path d="M30 85 Q60 70 90 85 L90 100 L30 100 Z"
|
||||
fill="var(--primary-color)" opacity="0.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="avatar-placeholder">
|
||||
<img v-if="inheritor.avatar" :src="getFileAccessHttpUrl(inheritor.avatar)" :alt="inheritor.name" class="avatar-image" />
|
||||
<svg v-else viewBox="0 0 80 80" width="80" height="80" class="default-avatar">
|
||||
<defs>
|
||||
<linearGradient id="cultureAvatarGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#ff9a9e;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#fecfef;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="40" cy="40" r="40" fill="url(#cultureAvatarGradient)"/>
|
||||
<!-- 简洁的人像图标 -->
|
||||
<circle cx="40" cy="30" r="10" fill="white" opacity="0.9"/>
|
||||
<path d="M 20 65 Q 20 50 40 50 Q 60 50 60 65 L 60 70 Q 60 75 55 75 L 25 75 Q 20 75 20 70 Z" fill="white" opacity="0.9"/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inheritor-info">
|
||||
<h3 class="inheritor-name">{{ inheritor.name }}</h3>
|
||||
<p class="inheritor-title">{{ inheritor.title }}</p>
|
||||
<p class="inheritor-skill">{{ inheritor.skill }}</p>
|
||||
<p class="inheritor-description">{{ inheritor.description }}</p>
|
||||
<p v-if="inheritor.title" class="inheritor-title">{{ inheritor.title }}</p>
|
||||
<p v-if="inheritor.skill" class="inheritor-skill">{{ inheritor.skill }}</p>
|
||||
<p v-if="inheritor.description" class="inheritor-description">{{ inheritor.description }}</p>
|
||||
<div v-if="inheritor.hometown || inheritor.achievements" class="inheritor-extra">
|
||||
<p v-if="inheritor.hometown" class="inheritor-hometown">籍贯:{{ inheritor.hometown }}</p>
|
||||
<p v-if="inheritor.achievements" class="inheritor-achievements">主要成就:{{ inheritor.achievements }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="!loading" class="empty-state">
|
||||
<p>暂无传承人数据</p>
|
||||
</div>
|
||||
<div v-if="loading" class="loading-state">
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -186,115 +242,138 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Picture, VideoPlay, Microphone } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { cultureApi } from '../api/culture'
|
||||
import { heritageApi } from '../api/heritage'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref('hunting')
|
||||
const mediaGalleryVisible = ref(false)
|
||||
const mediaGalleryTitle = ref('')
|
||||
const currentMediaType = ref('')
|
||||
const loading = ref(false)
|
||||
|
||||
// 文化标签页数据
|
||||
const cultureTabs = ref([
|
||||
{
|
||||
id: 'hunting',
|
||||
name: '狩猎文化',
|
||||
title: '森林中的狩猎传统',
|
||||
description: '鄂伦春族是中国最后的狩猎民族,狩猎不仅是生存方式,更是文化核心。他们对森林动物习性了如指掌,形成了独特的狩猎技巧和狩猎伦理。',
|
||||
items: [
|
||||
'传统狩猎工具:弓箭、猎枪、陷阱等',
|
||||
'狩猎技巧:追踪、伪装、团队协作',
|
||||
'狩猎伦理:保护幼崽、适度捕猎',
|
||||
'狩猎仪式:出猎前的祈祷和归来后的感恩'
|
||||
],
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 'festival',
|
||||
name: '节庆仪式',
|
||||
title: '篝火节与传统庆典',
|
||||
description: '篝火节是鄂伦春族最重要的传统节日,族人们围着篝火载歌载舞,庆祝丰收,祈求平安。这些仪式承载着深厚的文化内涵和精神寄托。',
|
||||
items: [
|
||||
'篝火节:每年6月18日,最盛大的民族节日',
|
||||
'萨满仪式:祈福、治病、驱邪的宗教仪式',
|
||||
'成年礼:青年男女的成人仪式',
|
||||
'丰收庆典:庆祝狩猎和采集的丰收'
|
||||
],
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 'food',
|
||||
name: '饮食文化',
|
||||
title: '森林中的美食传统',
|
||||
description: '鄂伦春族的饮食以肉类为主,辅以野菜、野果。他们善于利用森林资源,创造出独特的烹饪方法和食品保存技术。',
|
||||
items: [
|
||||
'手把肉:传统的肉类烹饪方式',
|
||||
'柳蒿芽:春季重要的野菜食材',
|
||||
'烤肉串:篝火旁的传统美食',
|
||||
'肉干制作:传统的食品保存方法'
|
||||
],
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 'housing',
|
||||
name: '居住文化',
|
||||
title: '撮罗子与森林居所',
|
||||
description: '撮罗子是鄂伦春族的传统居所,这种圆锥形建筑适应了游牧狩猎的生活方式,体现了人与自然和谐共处的智慧。',
|
||||
items: [
|
||||
'撮罗子结构:圆锥形框架,桦皮或兽皮覆盖',
|
||||
'建造技巧:快速搭建,便于迁移',
|
||||
'内部布局:火塘居中,功能区域明确',
|
||||
'现代演变:从传统居所到文化象征'
|
||||
],
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 'clothing',
|
||||
name: '服饰文化',
|
||||
title: '精美的传统服饰',
|
||||
description: '鄂伦春族服饰以兽皮为主要材料,装饰精美的刺绣图案。服饰不仅具有实用功能,更承载着丰富的文化内涵和审美价值。',
|
||||
items: [
|
||||
'皮袍:主要服装,保暖实用',
|
||||
'刺绣图案:花卉、动物、几何纹样',
|
||||
'头饰配件:帽子、头巾、发饰',
|
||||
'鞋靴制作:适应森林环境的鞋靴'
|
||||
],
|
||||
color: '#2d5016'
|
||||
}
|
||||
])
|
||||
const cultureTabs = ref<any[]>([])
|
||||
|
||||
// 文化故事数据
|
||||
const cultureStories = ref<any[]>([])
|
||||
|
||||
// 传承人数据
|
||||
const inheritors = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '关小云',
|
||||
title: '国家级非遗传承人',
|
||||
skill: '鄂伦春族桦皮制作技艺',
|
||||
description: '从事桦皮工艺制作40余年,作品精美,技艺精湛,致力于技艺传承和创新发展。'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '孟淑珍',
|
||||
title: '自治区级非遗传承人',
|
||||
skill: '鄂伦春族传统刺绣',
|
||||
description: '精通传统刺绣技艺,图案设计独特,色彩搭配和谐,培养了众多刺绣爱好者。'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '白热布',
|
||||
title: '民族文化研究专家',
|
||||
skill: '鄂伦春族民歌传承',
|
||||
description: '收集整理鄂伦春族民歌200余首,致力于民族音乐的保护和传播工作。'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '关金山',
|
||||
title: '传统工艺大师',
|
||||
skill: '木雕与狩猎工具制作',
|
||||
description: '掌握传统木雕技艺和狩猎工具制作方法,作品展现浓郁的民族特色。'
|
||||
const inheritors = ref<any[]>([])
|
||||
|
||||
// 获取文化故事数据
|
||||
const getCultureStories = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await cultureApi.getCultureList({
|
||||
pageNo: 1,
|
||||
pageSize: 6
|
||||
})
|
||||
|
||||
if (response?.records && response.records.length > 0) {
|
||||
cultureStories.value = response.records.map((story: any) => ({
|
||||
id: story.id,
|
||||
title: story.title,
|
||||
excerpt: story.content ? story.content.substring(0, 100) + '...' : story.description || '暂无描述',
|
||||
date: story.createTime ? new Date(story.createTime).toLocaleDateString() : '未知日期',
|
||||
categoryName: story.categoryName,
|
||||
color: getRandomColor()
|
||||
}))
|
||||
} else {
|
||||
cultureStories.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文化故事失败:', error)
|
||||
ElMessage.error('获取文化故事失败,请稍后重试')
|
||||
cultureStories.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// 跳转到文化详情页
|
||||
const goToCultureDetail = (id: string) => {
|
||||
router.push(`/culture/${id}`)
|
||||
}
|
||||
|
||||
// 获取文化数据
|
||||
const getCultureData = async () => {
|
||||
try {
|
||||
const response = await cultureApi.getCultureList({
|
||||
pageNo: 1,
|
||||
pageSize: 20
|
||||
})
|
||||
|
||||
if (response?.records && response.records.length > 0) {
|
||||
// 将API数据转换为标签页格式
|
||||
cultureTabs.value = response.records.slice(0, 5).map((culture: any, index: number) => ({
|
||||
id: culture.id.toString(),
|
||||
name: culture.title || `文化${index + 1}`,
|
||||
title: culture.title || '文化传统',
|
||||
description: culture.content || culture.description || '暂无描述',
|
||||
items: culture.items ? culture.items.split(',') : ['传统技艺', '文化传承', '民族特色', '历史价值'],
|
||||
color: index % 2 === 0 ? '#2d5016' : '#8b4513'
|
||||
}))
|
||||
} else {
|
||||
cultureTabs.value = []
|
||||
ElMessage.warning('暂无文化数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文化数据失败:', error)
|
||||
ElMessage.error('获取文化数据失败,请稍后重试')
|
||||
cultureTabs.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取传承人数据
|
||||
const getInheritors = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await heritageApi.getInheritorList({
|
||||
pageNo: 1,
|
||||
pageSize: 20,
|
||||
status: 1 // 只获取启用状态的传承人
|
||||
})
|
||||
|
||||
if (response?.records && response.records.length > 0) {
|
||||
inheritors.value = response.records.map((inheritor: any) => ({
|
||||
id: inheritor.id,
|
||||
name: inheritor.name,
|
||||
title: inheritor.heritageLevel ? `${inheritor.heritageLevel}非遗传承人` : '非遗传承人',
|
||||
skill: inheritor.heritageSkills || '传统技艺',
|
||||
description: inheritor.biography || `${inheritor.name}是${inheritor.heritageSkills || '传统技艺'}的传承人,多年以来致力于技艺的传承和发展。`,
|
||||
avatar: inheritor.avatar,
|
||||
heritageLevel: inheritor.heritageLevel,
|
||||
achievements: inheritor.achievements,
|
||||
hometown: inheritor.hometown,
|
||||
gender: inheritor.gender,
|
||||
birthYear: inheritor.birthYear,
|
||||
contactInfo: inheritor.contactInfo
|
||||
}))
|
||||
} else {
|
||||
inheritors.value = []
|
||||
console.log('暂无传承人数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取传承人数据失败:', error)
|
||||
ElMessage.error('获取传承人数据失败,请稍后重试')
|
||||
inheritors.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取随机颜色
|
||||
const getRandomColor = () => {
|
||||
const colors = ['#2d5016', '#8b4513']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
|
||||
// 方法
|
||||
const openMediaGallery = (type: string) => {
|
||||
@ -312,6 +391,13 @@ const openMediaGallery = (type: string) => {
|
||||
}
|
||||
mediaGalleryVisible.value = true
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
getCultureData()
|
||||
getCultureStories()
|
||||
getInheritors()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -442,6 +528,77 @@ const openMediaGallery = (type: string) => {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
/* 文化故事展示 */
|
||||
.culture-stories-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 2rem;
|
||||
}
|
||||
|
||||
.culture-story-card {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
padding: 1.5rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.culture-story-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
|
||||
.story-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.story-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.story-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.8rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.story-excerpt {
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.story-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.story-date {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.story-category {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.loading-state {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* 多媒体资源 */
|
||||
.media-categories {
|
||||
display: grid;
|
||||
@ -497,10 +654,33 @@ const openMediaGallery = (type: string) => {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.avatar-image {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid var(--primary-color);
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
display: inline-block;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.default-avatar {
|
||||
border-radius: 50%;
|
||||
transition: transform 0.3s ease, filter 0.3s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.default-avatar:hover {
|
||||
transform: scale(1.05);
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
|
||||
.inheritor-name {
|
||||
@ -524,6 +704,21 @@ const openMediaGallery = (type: string) => {
|
||||
.inheritor-description {
|
||||
color: var(--text-light);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.inheritor-extra {
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.inheritor-hometown,
|
||||
.inheritor-achievements {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 媒体画廊 */
|
||||
@ -581,7 +776,8 @@ const openMediaGallery = (type: string) => {
|
||||
}
|
||||
|
||||
.media-categories,
|
||||
.inheritors-grid {
|
||||
.inheritors-grid,
|
||||
.culture-stories-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
477
src/views/CultureDetail.vue
Normal file
477
src/views/CultureDetail.vue
Normal file
@ -0,0 +1,477 @@
|
||||
<template>
|
||||
<div class="culture-detail-page">
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="breadcrumb">
|
||||
<div class="container">
|
||||
<router-link to="/">首页</router-link>
|
||||
<span class="separator">/</span>
|
||||
<router-link to="/culture">传统文化</router-link>
|
||||
<span class="separator">/</span>
|
||||
<span class="current">{{ cultureContent.title || '文化详情' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文化内容详情 -->
|
||||
<div class="container">
|
||||
<div class="culture-detail-container" v-if="!loading">
|
||||
<!-- 文化内容卡片 -->
|
||||
<div class="culture-card">
|
||||
<!-- 头部信息 -->
|
||||
<div class="culture-header">
|
||||
<div class="culture-icon">
|
||||
<svg width="60" height="60" viewBox="0 0 60 60">
|
||||
<rect width="60" height="60" :fill="iconColor" opacity="0.1" rx="12"/>
|
||||
<text x="30" y="38" text-anchor="middle"
|
||||
:fill="iconColor" font-size="24" font-weight="bold">
|
||||
{{ cultureContent.title ? cultureContent.title.charAt(0) : '文' }}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="culture-meta">
|
||||
<h1 class="culture-title">{{ cultureContent.title }}</h1>
|
||||
<div class="culture-info">
|
||||
<span class="culture-date">
|
||||
<el-icon><Calendar /></el-icon>
|
||||
{{ formatDate(cultureContent.createTime) }}
|
||||
</span>
|
||||
<span class="culture-category" v-if="cultureContent.categoryName">
|
||||
<el-icon><PriceTag /></el-icon>
|
||||
{{ cultureContent.categoryName }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文化内容 -->
|
||||
<div class="culture-content">
|
||||
<div class="content-section" v-if="cultureContent.description">
|
||||
<h3>简介</h3>
|
||||
<p class="content-text">{{ cultureContent.description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="content-section" v-if="cultureContent.content">
|
||||
<h3>详细内容</h3>
|
||||
<div class="content-text" v-html="formatContent(cultureContent.content)"></div>
|
||||
</div>
|
||||
|
||||
<!-- 图片展示 -->
|
||||
<div class="content-section" v-if="cultureContent.imageUrl">
|
||||
<h3>相关图片</h3>
|
||||
<div class="culture-images">
|
||||
<img :src="cultureContent.imageUrl" :alt="cultureContent.title" class="culture-image">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 标签 -->
|
||||
<div class="content-section" v-if="cultureContent.tags">
|
||||
<h3>相关标签</h3>
|
||||
<div class="culture-tags">
|
||||
<span class="tag" v-for="tag in parseTags(cultureContent.tags)" :key="tag">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 相关文化内容推荐 -->
|
||||
<div class="related-culture" v-if="relatedCultures.length > 0">
|
||||
<h2 class="section-title">相关文化内容</h2>
|
||||
<div class="related-grid">
|
||||
<div
|
||||
class="related-item card"
|
||||
v-for="item in relatedCultures"
|
||||
:key="item.id"
|
||||
@click="goToCultureDetail(item.id)"
|
||||
>
|
||||
<div class="related-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 40 40">
|
||||
<rect width="40" height="40" :fill="getRandomColor()" opacity="0.1" rx="8"/>
|
||||
<text x="20" y="26" text-anchor="middle"
|
||||
:fill="getRandomColor()" font-size="16" font-weight="bold">
|
||||
{{ item.title ? item.title.charAt(0) : '文' }}
|
||||
</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="related-content">
|
||||
<h4 class="related-title">{{ item.title }}</h4>
|
||||
<p class="related-excerpt">{{ getExcerpt(item.content || item.description) }}</p>
|
||||
<span class="related-date">{{ formatDate(item.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div class="loading-container" v-else>
|
||||
<el-icon class="loading-icon" size="48"><Loading /></el-icon>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div class="error-container" v-if="error">
|
||||
<el-icon class="error-icon" size="48"><Warning /></el-icon>
|
||||
<p>{{ error }}</p>
|
||||
<el-button type="primary" @click="loadCultureDetail">重新加载</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Calendar, PriceTag, Loading, Warning } from '@element-plus/icons-vue'
|
||||
import { cultureApi } from '../api/culture'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const cultureContent = ref<any>({})
|
||||
const relatedCultures = ref<any[]>([])
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
|
||||
// 计算属性
|
||||
const iconColor = computed(() => {
|
||||
const colors = ['#8b4513', '#2d5016', '#d4a574', '#5d4e37']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
})
|
||||
|
||||
// 加载文化内容详情
|
||||
const loadCultureDetail = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
|
||||
const id = route.params.id as string
|
||||
if (!id) {
|
||||
error.value = '文化内容ID不存在'
|
||||
return
|
||||
}
|
||||
|
||||
const response = await cultureApi.getCultureById(id)
|
||||
if (response) {
|
||||
cultureContent.value = response
|
||||
// 加载相关内容
|
||||
await loadRelatedCultures()
|
||||
} else {
|
||||
error.value = '文化内容不存在'
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('加载文化内容失败:', err)
|
||||
if (err.message && err.message.includes('未找到对应数据')) {
|
||||
error.value = '文化内容不存在'
|
||||
} else {
|
||||
error.value = '获取文化内容失败,请稍后重试'
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载相关文化内容
|
||||
const loadRelatedCultures = async () => {
|
||||
try {
|
||||
const response = await cultureApi.getRecommendedCulture()
|
||||
if (response && response.length > 0) {
|
||||
// 过滤掉当前文化内容,取前3个
|
||||
relatedCultures.value = response
|
||||
.filter((item: any) => item.id !== cultureContent.value.id)
|
||||
.slice(0, 3)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('加载相关文化内容失败:', err)
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到其他文化详情页
|
||||
const goToCultureDetail = (id: string) => {
|
||||
router.push(`/culture/${id}`)
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString: string) => {
|
||||
if (!dateString) return '未知日期'
|
||||
return new Date(dateString).toLocaleDateString('zh-CN')
|
||||
}
|
||||
|
||||
// 格式化内容(简单的换行处理)
|
||||
const formatContent = (content: string) => {
|
||||
if (!content) return ''
|
||||
return content.replace(/\n/g, '<br>')
|
||||
}
|
||||
|
||||
// 解析标签
|
||||
const parseTags = (tags: string) => {
|
||||
if (!tags) return []
|
||||
return tags.split(',').map(tag => tag.trim()).filter(tag => tag)
|
||||
}
|
||||
|
||||
// 获取摘要
|
||||
const getExcerpt = (content: string) => {
|
||||
if (!content) return '暂无描述'
|
||||
return content.length > 80 ? content.substring(0, 80) + '...' : content
|
||||
}
|
||||
|
||||
// 获取随机颜色
|
||||
const getRandomColor = () => {
|
||||
const colors = ['#8b4513', '#2d5016', '#d4a574', '#5d4e37']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadCultureDetail()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.culture-detail-page {
|
||||
min-height: 100vh;
|
||||
background: var(--background-light);
|
||||
}
|
||||
|
||||
/* 面包屑导航 */
|
||||
.breadcrumb {
|
||||
background: white;
|
||||
padding: 1rem 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin: 0 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.current {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 主容器 */
|
||||
.culture-detail-container {
|
||||
padding: 2rem 0;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* 文化内容卡片 */
|
||||
.culture-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.culture-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.culture-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.culture-meta {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.culture-title {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.culture-info {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.culture-date,
|
||||
.culture-category {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* 内容区域 */
|
||||
.culture-content {
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.content-section h3 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 图片展示 */
|
||||
.culture-images {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.culture-image {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
/* 标签 */
|
||||
.culture-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* 相关文化内容 */
|
||||
.related-culture {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.related-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.related-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.related-item:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-hover);
|
||||
}
|
||||
|
||||
.related-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.related-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.related-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.related-excerpt {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.related-date {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* 加载和错误状态 */
|
||||
.loading-container,
|
||||
.error-container {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.loading-icon,
|
||||
.error-icon {
|
||||
margin-bottom: 1rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.culture-detail-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.culture-card {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.culture-header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.culture-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.culture-info {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.related-item {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
@ -146,7 +146,7 @@
|
||||
<p class="story-excerpt">{{ story.excerpt }}</p>
|
||||
<div class="story-meta">
|
||||
<span class="story-date">{{ story.date }}</span>
|
||||
<a href="#" class="story-link">阅读更多</a>
|
||||
<a href="#" @click.prevent="showStoryDetail(story)" class="story-link">阅读更多</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -178,77 +178,195 @@
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 文化故事详情卡片 -->
|
||||
<el-dialog
|
||||
v-model="storyDetailVisible"
|
||||
:title="selectedStory?.title"
|
||||
width="70%"
|
||||
top="5vh"
|
||||
class="story-detail-dialog"
|
||||
>
|
||||
<div v-if="selectedStory" class="story-detail-content">
|
||||
<!-- 故事头部信息 -->
|
||||
<div class="story-header">
|
||||
<div class="story-image-placeholder">
|
||||
<svg width="100%" height="200" viewBox="0 0 400 200">
|
||||
<rect width="400" height="200" :fill="selectedStory.color" opacity="0.2"/>
|
||||
<circle cx="200" cy="100" r="50" :fill="selectedStory.color" opacity="0.5"/>
|
||||
<text x="200" y="110" text-anchor="middle" :fill="selectedStory.color"
|
||||
font-size="20" font-weight="bold">{{ selectedStory.title.substring(0, 2) }}</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="story-meta-info">
|
||||
<p class="story-date">发布时间:{{ selectedStory.date }}</p>
|
||||
<p class="story-category" v-if="selectedStory.categoryName">分类:{{ selectedStory.categoryName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 故事内容 -->
|
||||
<div class="story-content-section">
|
||||
<h3>内容简介</h3>
|
||||
<p class="story-description">{{ selectedStory.excerpt }}</p>
|
||||
|
||||
<div v-if="storyDetailData.content" class="story-full-content">
|
||||
<h3>详细内容</h3>
|
||||
<div class="content-text" v-html="storyDetailData.content"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="storyDetailData.tags" class="story-tags">
|
||||
<h3>相关标签</h3>
|
||||
<div class="tags-container">
|
||||
<span class="tag" v-for="tag in parseTags(storyDetailData.tags)" :key="tag">
|
||||
{{ tag }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="story-actions">
|
||||
<el-button type="primary" @click="goToFullDetail">查看完整详情</el-button>
|
||||
<el-button @click="storyDetailVisible = false">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="storyLoading" class="loading-container">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMainStore } from '../stores'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Loading } from '@element-plus/icons-vue'
|
||||
import { productApi } from '../api/product'
|
||||
import { cultureApi } from '../api/culture'
|
||||
import { cartApi } from '../api/cart'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useMainStore()
|
||||
|
||||
// 特色产品数据
|
||||
const featuredProducts = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '桦皮工艺盒',
|
||||
description: '传统桦皮制作工艺,精美实用',
|
||||
price: 168,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '鄂伦春族刺绣',
|
||||
description: '手工刺绣,图案精美,寓意吉祥',
|
||||
price: 288,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '传统服饰',
|
||||
description: '复刻传统服饰,展现民族风采',
|
||||
price: 588,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '木雕摆件',
|
||||
description: '手工雕刻,展现森林文化',
|
||||
price: 128,
|
||||
color: '#2d5016'
|
||||
}
|
||||
])
|
||||
const featuredProducts = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 文化故事数据
|
||||
const stories = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '森林中的狩猎文化',
|
||||
excerpt: '鄂伦春族世代生活在大兴安岭的森林中,形成了独特的狩猎文化和生活方式...',
|
||||
date: '2024-01-15',
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '桦皮工艺的传承',
|
||||
excerpt: '桦皮是鄂伦春族生活中不可缺少的材料,从日用品到艺术品,展现了民族智慧...',
|
||||
date: '2024-01-10',
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '篝火节的欢歌',
|
||||
excerpt: '每年的篝火节是鄂伦春族最重要的节日,族人们围着篝火载歌载舞...',
|
||||
date: '2024-01-05',
|
||||
color: '#2d5016'
|
||||
const stories = ref<any[]>([])
|
||||
|
||||
// 故事详情相关
|
||||
const storyDetailVisible = ref(false)
|
||||
const selectedStory = ref<any>(null)
|
||||
const storyDetailData = ref<any>({})
|
||||
const storyLoading = ref(false)
|
||||
|
||||
// 获取热门产品
|
||||
const getFeaturedProducts = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await productApi.getProductList({
|
||||
pageNo: 1,
|
||||
pageSize: 4,
|
||||
status: 1 // 只获取上架的产品
|
||||
})
|
||||
if (response && response.records) {
|
||||
featuredProducts.value = response.records.map((product: any) => ({
|
||||
...product,
|
||||
color: getRandomColor()
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取产品数据失败:', error)
|
||||
ElMessage.error('获取产品数据失败,请稍后重试')
|
||||
featuredProducts.value = []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// 获取推荐文化内容
|
||||
const getRecommendedStories = async () => {
|
||||
try {
|
||||
const response = await cultureApi.getRecommendedCulture()
|
||||
if (response && response.length > 0) {
|
||||
stories.value = response.slice(0, 3).map((story: any) => ({
|
||||
id: story.id,
|
||||
title: story.title,
|
||||
excerpt: story.content ? story.content.substring(0, 100) + '...' : story.description,
|
||||
date: story.createTime ? new Date(story.createTime).toLocaleDateString() : '2024-01-01',
|
||||
color: getRandomColor()
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文化内容失败:', error)
|
||||
ElMessage.error('获取文化内容失败,请稍后重试')
|
||||
stories.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取随机颜色
|
||||
const getRandomColor = () => {
|
||||
const colors = ['#8b4513', '#2d5016', '#8b4513', '#2d5016']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
|
||||
// 添加到购物车
|
||||
const addToCart = (product: any) => {
|
||||
store.addToCart(product)
|
||||
ElMessage.success('已添加到购物车')
|
||||
const addToCart = async (product: any) => {
|
||||
try {
|
||||
// 这里使用默认用户ID,实际应用中应该从用户状态获取
|
||||
const userId = '1'
|
||||
await cartApi.addToCart(userId, product.id, 1)
|
||||
store.addToCart(product)
|
||||
ElMessage.success('已添加到购物车')
|
||||
} catch (error) {
|
||||
console.error('添加到购物车失败:', error)
|
||||
ElMessage.error('添加到购物车失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 显示故事详情
|
||||
const showStoryDetail = async (story: any) => {
|
||||
selectedStory.value = story
|
||||
storyDetailVisible.value = true
|
||||
storyLoading.value = true
|
||||
|
||||
try {
|
||||
// 获取完整的故事详情
|
||||
const response = await cultureApi.getCultureById(story.id)
|
||||
if (response) {
|
||||
storyDetailData.value = response
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取故事详情失败:', error)
|
||||
ElMessage.error('获取故事详情失败,请稍后重试')
|
||||
} finally {
|
||||
storyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到完整详情页
|
||||
const goToFullDetail = () => {
|
||||
if (selectedStory.value) {
|
||||
router.push(`/culture/${selectedStory.value.id}`)
|
||||
storyDetailVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 解析标签
|
||||
const parseTags = (tags: string) => {
|
||||
if (!tags) return []
|
||||
return tags.split(',').map(tag => tag.trim()).filter(tag => tag)
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
getFeaturedProducts()
|
||||
getRecommendedStories()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -510,6 +628,104 @@ const addToCart = (product: any) => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 故事详情对话框样式 */
|
||||
.story-detail-dialog :deep(.el-dialog) {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.story-detail-content {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.story-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.story-image-placeholder {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.story-meta-info {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.story-content-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.story-content-section h3 {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--primary-color);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.story-description {
|
||||
color: var(--text-color);
|
||||
line-height: 1.6;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.story-full-content {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
color: var(--text-color);
|
||||
line-height: 1.8;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.story-tags {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.tags-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.tag {
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
padding: 0.3rem 0.8rem;
|
||||
border-radius: 15px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.story-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.loading-container .el-icon {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.hero-content {
|
||||
@ -535,6 +751,20 @@ const addToCart = (product: any) => {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.story-detail-dialog :deep(.el-dialog) {
|
||||
width: 95% !important;
|
||||
margin: 5vh auto;
|
||||
}
|
||||
|
||||
.story-meta-info {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.story-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
|
@ -8,7 +8,7 @@
|
||||
<span class="separator">/</span>
|
||||
<router-link to="/products">产品中心</router-link>
|
||||
<span class="separator">/</span>
|
||||
<span class="current">{{ product.name }}</span>
|
||||
<span class="current">{{ product.productName || product.name || '产品详情' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -25,7 +25,7 @@
|
||||
<rect width="400" height="400" :fill="product.color" opacity="0.2"/>
|
||||
<circle cx="200" cy="200" r="80" :fill="product.color" opacity="0.5"/>
|
||||
<text x="200" y="210" text-anchor="middle" :fill="product.color"
|
||||
font-size="24" font-weight="bold">{{ product.name.substring(0, 2) }}</text>
|
||||
font-size="24" font-weight="bold">{{ (product.productName || product.name || '产品').substring(0, 2) }}</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@ -47,7 +47,7 @@
|
||||
<span v-if="product.isHot" class="badge hot">热销</span>
|
||||
</div>
|
||||
|
||||
<h1 class="product-title">{{ product.name }}</h1>
|
||||
<h1 class="product-title">{{ product.productName || product.name || '产品名称' }}</h1>
|
||||
<p class="product-subtitle">{{ getCategoryName(product.category) }}</p>
|
||||
|
||||
<div class="price-section">
|
||||
@ -158,34 +158,36 @@
|
||||
<el-tab-pane label="规格参数" name="specs">
|
||||
<div class="specs-table">
|
||||
<table>
|
||||
<tr>
|
||||
<td>产品名称</td>
|
||||
<td>{{ product.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>产品分类</td>
|
||||
<td>{{ getCategoryName(product.category) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>材质</td>
|
||||
<td>{{ product.material || '天然桦皮' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>尺寸</td>
|
||||
<td>{{ product.size || '长20cm × 宽15cm × 高8cm' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>重量</td>
|
||||
<td>{{ product.weight || '约200g' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>产地</td>
|
||||
<td>{{ product.origin || '黑龙江大兴安岭' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>保养方式</td>
|
||||
<td>避免阳光直射,保持干燥,定期清洁</td>
|
||||
</tr>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>产品名称</td>
|
||||
<td>{{ product.productName || product.name || '产品名称' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>产品分类</td>
|
||||
<td>{{ getCategoryName(product.category) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>材质</td>
|
||||
<td>{{ product.material || '天然桦皮' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>尺寸</td>
|
||||
<td>{{ product.size || '长20cm × 宽15cm × 高8cm' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>重量</td>
|
||||
<td>{{ product.weight || '约200g' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>产地</td>
|
||||
<td>{{ product.origin || '黑龙江大兴安岭' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>保养方式</td>
|
||||
<td>避免阳光直射,保持干燥,定期清洁</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
@ -246,12 +248,12 @@
|
||||
<rect width="200" height="150" :fill="relatedProduct.color" opacity="0.2"/>
|
||||
<circle cx="100" cy="75" r="30" :fill="relatedProduct.color" opacity="0.5"/>
|
||||
<text x="100" y="80" text-anchor="middle" :fill="relatedProduct.color"
|
||||
font-size="12" font-weight="bold">{{ relatedProduct.name.substring(0, 2) }}</text>
|
||||
font-size="12" font-weight="bold">{{ (relatedProduct.productName || relatedProduct.name || '产品').substring(0, 2) }}</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h3 class="product-name">{{ relatedProduct.name }}</h3>
|
||||
<h3 class="product-name">{{ relatedProduct.productName || relatedProduct.name || '产品名称' }}</h3>
|
||||
<div class="product-price">¥{{ relatedProduct.price }}</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -272,6 +274,7 @@ import { useRoute, useRouter } from 'vue-router'
|
||||
import { useMainStore } from '../stores'
|
||||
import { User, StarFilled, Loading } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { productApi } from '../api/product'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@ -318,6 +321,9 @@ const craftSteps = ref([
|
||||
}
|
||||
])
|
||||
|
||||
// 相关产品
|
||||
const relatedProducts = ref<any[]>([])
|
||||
|
||||
// 用户评价
|
||||
const reviews = ref([
|
||||
{
|
||||
@ -325,7 +331,7 @@ const reviews = ref([
|
||||
userName: '文化爱好者',
|
||||
rating: 5,
|
||||
date: '2024-01-15',
|
||||
content: '非常精美的桦皮工艺品,做工精细,很有民族特色,值得收藏!'
|
||||
content: '非常精美的工艺品,做工精细,很有民族特色,值得收藏!'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@ -343,191 +349,6 @@ const reviews = ref([
|
||||
}
|
||||
])
|
||||
|
||||
// 相关产品
|
||||
const relatedProducts = ref([
|
||||
{
|
||||
id: 2,
|
||||
name: '鄂伦春族刺绣挂画',
|
||||
price: 288,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '手工木雕摆件',
|
||||
price: 128,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '桦皮茶具套装',
|
||||
price: 368,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '民族风头饰',
|
||||
price: 88,
|
||||
color: '#8b4513'
|
||||
}
|
||||
])
|
||||
|
||||
// 模拟产品数据
|
||||
const mockProducts = [
|
||||
{
|
||||
id: 1,
|
||||
name: '桦皮工艺盒',
|
||||
category: 'handicrafts',
|
||||
description: '传统桦皮制作工艺,精美实用的收纳盒',
|
||||
fullDescription: '这款桦皮工艺盒采用传统的鄂伦春族桦皮制作工艺,选用优质天然桦树皮为原料,经过精心处理和手工制作而成。盒子造型优美,实用性强,既可作为收纳盒使用,也是极具收藏价值的民族工艺品。每一个细节都体现了匠人的精湛技艺和对传统文化的传承。',
|
||||
price: 168,
|
||||
originalPrice: 198,
|
||||
stock: 15,
|
||||
craftsman: '关小云',
|
||||
craftsmanTitle: '桦皮工艺传承人',
|
||||
craftsmanDesc: '从事桦皮工艺制作30余年,作品精美,技艺精湛,多次获得民族工艺品大赛奖项',
|
||||
isNew: true,
|
||||
isHot: false,
|
||||
color: '#8b4513',
|
||||
material: '天然桦皮',
|
||||
size: '长20cm × 宽15cm × 高8cm',
|
||||
weight: '约200g',
|
||||
origin: '黑龙江大兴安岭',
|
||||
culturalMeaning: '桦皮工艺是鄂伦春族传统手工艺的重要组成部分,承载着深厚的民族文化内涵。桦皮盒不仅是实用的生活用品,更是民族智慧的结晶,体现了鄂伦春族人民对自然的敬畏和对美好生活的追求。'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '鄂伦春族刺绣挂画',
|
||||
category: 'handicrafts',
|
||||
description: '手工刺绣,图案精美,寓意吉祥',
|
||||
fullDescription: '这幅刺绣挂画采用传统的鄂伦春族刺绣工艺,以丝线为材料,在优质布料上精心绣制而成。图案设计融合了鄂伦春族的传统文化元素,色彩搭配和谐,寓意吉祥如意。每一针每一线都凝聚着绣娘的心血和对民族文化的热爱。',
|
||||
price: 288,
|
||||
stock: 8,
|
||||
craftsman: '孟淑珍',
|
||||
craftsmanTitle: '刺绣工艺传承人',
|
||||
craftsmanDesc: '从事刺绣工艺25年,擅长传统图案设计,作品多次在民族工艺展览中获奖',
|
||||
isNew: false,
|
||||
isHot: true,
|
||||
color: '#2d5016',
|
||||
material: '优质丝线、棉布',
|
||||
size: '长40cm × 宽30cm',
|
||||
weight: '约150g',
|
||||
origin: '黑龙江大兴安岭',
|
||||
culturalMeaning: '刺绣是鄂伦春族妇女的传统技艺,图案多取材于自然和生活,体现了鄂伦春族人民对美好生活的向往和对自然的崇敬。'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '传统民族服饰',
|
||||
category: 'clothing',
|
||||
description: '复刻传统服饰,展现民族风采',
|
||||
fullDescription: '这套传统民族服饰严格按照鄂伦春族传统服装样式制作,选用优质面料,手工缝制,细节精美。服装设计体现了鄂伦春族的文化特色,穿着舒适,既可用于文化表演,也可作为收藏品。',
|
||||
price: 588,
|
||||
stock: 5,
|
||||
craftsman: '白热布',
|
||||
craftsmanTitle: '民族服饰制作师',
|
||||
craftsmanDesc: '专注民族服饰制作15年,深谙传统服装制作工艺,作品广受好评',
|
||||
isNew: false,
|
||||
isHot: true,
|
||||
color: '#8b4513',
|
||||
material: '优质棉麻、丝绸',
|
||||
size: 'M码(可定制)',
|
||||
weight: '约800g',
|
||||
origin: '黑龙江大兴安岭',
|
||||
culturalMeaning: '鄂伦春族传统服饰承载着深厚的文化内涵,每一个图案和色彩都有其特定的含义,体现了民族的智慧和审美。'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '手工木雕摆件',
|
||||
category: 'handicrafts',
|
||||
description: '精美木雕工艺,展现森林文化',
|
||||
fullDescription: '这件木雕摆件选用优质木材,经过精心雕刻而成。作品造型生动,线条流畅,充分展现了鄂伦春族的森林文化特色。每一个细节都经过匠人的精心打磨,是极具艺术价值的工艺品。',
|
||||
price: 128,
|
||||
stock: 20,
|
||||
craftsman: '关金山',
|
||||
craftsmanTitle: '木雕工艺师',
|
||||
craftsmanDesc: '从事木雕工艺20年,擅长动物和自然题材雕刻,技艺精湛',
|
||||
isNew: false,
|
||||
isHot: false,
|
||||
color: '#2d5016',
|
||||
material: '优质木材',
|
||||
size: '长15cm × 宽10cm × 高12cm',
|
||||
weight: '约300g',
|
||||
origin: '黑龙江大兴安岭',
|
||||
culturalMeaning: '木雕是鄂伦春族传统手工艺之一,多以森林动物和自然景观为题材,体现了民族与自然和谐共生的理念。'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '野生蓝莓干',
|
||||
category: 'food',
|
||||
description: '大兴安岭野生蓝莓,天然无添加',
|
||||
fullDescription: '这款蓝莓干选用大兴安岭原始森林中的野生蓝莓,经过自然晾晒工艺制成。保持了蓝莓的天然营养成分,口感酸甜,是健康的天然零食。无任何人工添加剂,绿色健康。',
|
||||
price: 58,
|
||||
originalPrice: 68,
|
||||
stock: 50,
|
||||
isNew: true,
|
||||
isHot: false,
|
||||
color: '#8b4513',
|
||||
material: '野生蓝莓',
|
||||
size: '净含量200g',
|
||||
weight: '约200g',
|
||||
origin: '黑龙江大兴安岭',
|
||||
culturalMeaning: '蓝莓是大兴安岭的特产,鄂伦春族人民世代采集食用,是大自然赐予的珍贵礼物。'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '鄂伦春文化笔记本',
|
||||
category: 'cultural',
|
||||
description: '融入民族元素的精美笔记本',
|
||||
fullDescription: '这款笔记本封面设计融入了鄂伦春族传统文化元素,采用优质纸张制作,书写流畅。既实用又具有文化纪念意义,是学习工作的好伴侣,也是了解鄂伦春族文化的窗口。',
|
||||
price: 35,
|
||||
stock: 100,
|
||||
isNew: false,
|
||||
isHot: false,
|
||||
color: '#2d5016',
|
||||
material: '优质纸张、硬质封面',
|
||||
size: 'A5规格',
|
||||
weight: '约250g',
|
||||
origin: '黑龙江大兴安岭',
|
||||
culturalMeaning: '笔记本设计元素来源于鄂伦春族传统图案,每一个符号都承载着深厚的文化内涵。'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '桦皮茶具套装',
|
||||
category: 'handicrafts',
|
||||
description: '传统桦皮工艺制作的茶具',
|
||||
fullDescription: '这套桦皮茶具采用传统工艺制作,包含茶壶、茶杯等完整配件。桦皮材质天然环保,具有独特的香味,用于泡茶别有一番风味。是品茶爱好者和文化收藏者的理想选择。',
|
||||
price: 368,
|
||||
stock: 3,
|
||||
craftsman: '关小云',
|
||||
craftsmanTitle: '桦皮工艺传承人',
|
||||
craftsmanDesc: '从事桦皮工艺制作30余年,作品精美,技艺精湛,多次获得民族工艺品大赛奖项',
|
||||
isNew: false,
|
||||
isHot: true,
|
||||
color: '#8b4513',
|
||||
material: '天然桦皮',
|
||||
size: '茶壶高12cm,茶杯直径8cm',
|
||||
weight: '约500g',
|
||||
origin: '黑龙江大兴安岭',
|
||||
culturalMeaning: '桦皮茶具体现了鄂伦春族人民的生活智慧,将实用性与艺术性完美结合。'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '民族风头饰',
|
||||
category: 'clothing',
|
||||
description: '传统图案设计的精美头饰',
|
||||
fullDescription: '这款头饰采用传统鄂伦春族图案设计,手工制作,工艺精美。可用于民族服装搭配,也可作为装饰品收藏。设计典雅,做工精细,充分展现了鄂伦春族的文化魅力。',
|
||||
price: 88,
|
||||
stock: 12,
|
||||
isNew: true,
|
||||
isHot: false,
|
||||
color: '#2d5016',
|
||||
material: '优质布料、装饰珠子',
|
||||
size: '可调节尺寸',
|
||||
weight: '约50g',
|
||||
origin: '黑龙江大兴安岭',
|
||||
culturalMeaning: '头饰在鄂伦春族传统文化中具有重要地位,不同的图案和颜色代表不同的寓意和身份。'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const getCategoryName = (categoryId: string) => {
|
||||
const category = categories.value.find(c => c.id === categoryId)
|
||||
@ -570,20 +391,79 @@ const buyNow = () => {
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
const productId = parseInt(route.params.id as string)
|
||||
onMounted(async () => {
|
||||
const productId = route.params.id as string
|
||||
|
||||
// 模拟从API获取产品数据
|
||||
setTimeout(() => {
|
||||
const foundProduct = mockProducts.find(p => p.id === productId)
|
||||
if (foundProduct) {
|
||||
product.value = foundProduct
|
||||
if (!productId) {
|
||||
ElMessage.error('产品ID无效')
|
||||
router.push('/products')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 从API获取产品数据
|
||||
const response = await productApi.getProductById(productId)
|
||||
if (response && response.id) {
|
||||
product.value = {
|
||||
...response,
|
||||
color: getRandomColor(),
|
||||
// 设置默认值
|
||||
material: response.material || '天然材料',
|
||||
size: response.size || '标准尺寸',
|
||||
weight: response.weight || '约200g',
|
||||
origin: response.origin || '黑龙江大兴安岭',
|
||||
culturalMeaning: response.culturalMeaning || '承载着深厚的鄂伦春族文化内涵'
|
||||
}
|
||||
|
||||
// 获取相关产品
|
||||
await getRelatedProducts(response.categoryId)
|
||||
} else {
|
||||
ElMessage.error('产品不存在')
|
||||
router.push('/products')
|
||||
}
|
||||
}, 500)
|
||||
} catch (error) {
|
||||
console.error('获取产品详情失败:', error)
|
||||
// 检查是否是"未找到对应数据"的错误
|
||||
if (error.message && error.message.includes('未找到对应数据')) {
|
||||
ElMessage.error('产品不存在')
|
||||
} else {
|
||||
ElMessage.error('获取产品详情失败,请稍后重试')
|
||||
}
|
||||
router.push('/products')
|
||||
}
|
||||
})
|
||||
|
||||
// 获取相关产品
|
||||
const getRelatedProducts = async (categoryId: string) => {
|
||||
try {
|
||||
const response = await productApi.getProductList({
|
||||
pageNo: 1,
|
||||
pageSize: 4,
|
||||
categoryId: categoryId,
|
||||
status: 1
|
||||
})
|
||||
if (response?.records && response.records.length > 0) {
|
||||
relatedProducts.value = response.records
|
||||
.filter((p: any) => p.id !== product.value.id)
|
||||
.slice(0, 4)
|
||||
.map((p: any) => ({
|
||||
...p,
|
||||
color: getRandomColor()
|
||||
}))
|
||||
} else {
|
||||
relatedProducts.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取相关产品失败:', error)
|
||||
relatedProducts.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取随机颜色
|
||||
const getRandomColor = () => {
|
||||
const colors = ['#8b4513', '#2d5016']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
@ -21,7 +21,7 @@
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:class="['filter-btn', { active: selectedCategory === category.id }]"
|
||||
@click="selectedCategory = category.id"
|
||||
@click="selectedCategory = category.id; onCategoryChange()"
|
||||
>
|
||||
{{ category.name }}
|
||||
</button>
|
||||
@ -68,7 +68,7 @@
|
||||
<rect width="250" height="200" :fill="product.color" opacity="0.2"/>
|
||||
<circle cx="125" cy="100" r="40" :fill="product.color" opacity="0.5"/>
|
||||
<text x="125" y="110" text-anchor="middle" :fill="product.color"
|
||||
font-size="14" font-weight="bold">{{ product.name.substring(0, 2) }}</text>
|
||||
font-size="14" font-weight="bold">{{ product.productName ? product.productName.substring(0, 2) : '产品' }}</text>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="product-badges">
|
||||
@ -78,14 +78,14 @@
|
||||
</div>
|
||||
|
||||
<div class="product-info">
|
||||
<h3 class="product-name">{{ product.name }}</h3>
|
||||
<p class="product-category">{{ getCategoryName(product.category) }}</p>
|
||||
<p class="product-description">{{ product.description }}</p>
|
||||
<h3 class="product-name">{{ product.productName || '产品名称' }}</h3>
|
||||
<p class="product-category">{{ getCategoryName(product.categoryId) }}</p>
|
||||
<p class="product-description">{{ product.description || '暂无描述' }}</p>
|
||||
|
||||
<div class="product-meta">
|
||||
<div class="craftsman-info" v-if="product.craftsman">
|
||||
<div class="craftsman-info" v-if="product.craftsmanInfo">
|
||||
<el-icon><User /></el-icon>
|
||||
<span>{{ product.craftsman }}</span>
|
||||
<span>{{ product.craftsmanInfo }}</span>
|
||||
</div>
|
||||
<div class="stock-info">
|
||||
<span :class="['stock-status', product.stock > 0 ? 'in-stock' : 'out-stock']">
|
||||
@ -127,6 +127,8 @@ import { useRoute } from 'vue-router'
|
||||
import { useMainStore } from '../stores'
|
||||
import { User } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { productApi, productCategoryApi } from '../api/product'
|
||||
import { cartApi } from '../api/cart'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useMainStore()
|
||||
@ -137,114 +139,99 @@ const priceRange = ref('all')
|
||||
const sortBy = ref('default')
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(12)
|
||||
|
||||
// 产品分类
|
||||
const categories = ref([
|
||||
{ id: 'all', name: '全部产品' },
|
||||
{ id: 'handicrafts', name: '手工艺品' },
|
||||
{ id: 'food', name: '特色食品' },
|
||||
{ id: 'cultural', name: '文创产品' },
|
||||
{ id: 'clothing', name: '服饰配饰' }
|
||||
const categories = ref<any[]>([
|
||||
{ id: 'all', name: '全部产品' }
|
||||
])
|
||||
|
||||
// 产品数据
|
||||
const products = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '桦皮工艺盒',
|
||||
category: 'handicrafts',
|
||||
description: '传统桦皮制作工艺,精美实用的收纳盒',
|
||||
price: 168,
|
||||
originalPrice: 198,
|
||||
stock: 15,
|
||||
craftsman: '关小云',
|
||||
isNew: true,
|
||||
isHot: false,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '鄂伦春族刺绣挂画',
|
||||
category: 'handicrafts',
|
||||
description: '手工刺绣,图案精美,寓意吉祥',
|
||||
price: 288,
|
||||
stock: 8,
|
||||
craftsman: '孟淑珍',
|
||||
isNew: false,
|
||||
isHot: true,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '传统民族服饰',
|
||||
category: 'clothing',
|
||||
description: '复刻传统服饰,展现民族风采',
|
||||
price: 588,
|
||||
stock: 5,
|
||||
craftsman: '白热布',
|
||||
isNew: false,
|
||||
isHot: true,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '手工木雕摆件',
|
||||
category: 'handicrafts',
|
||||
description: '精美木雕工艺,展现森林文化',
|
||||
price: 128,
|
||||
stock: 20,
|
||||
craftsman: '关金山',
|
||||
isNew: false,
|
||||
isHot: false,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '野生蓝莓干',
|
||||
category: 'food',
|
||||
description: '大兴安岭野生蓝莓,天然无添加',
|
||||
price: 58,
|
||||
originalPrice: 68,
|
||||
stock: 50,
|
||||
isNew: true,
|
||||
isHot: false,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '鄂伦春文化笔记本',
|
||||
category: 'cultural',
|
||||
description: '融入民族元素的精美笔记本',
|
||||
price: 35,
|
||||
stock: 100,
|
||||
isNew: false,
|
||||
isHot: false,
|
||||
color: '#2d5016'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '桦皮茶具套装',
|
||||
category: 'handicrafts',
|
||||
description: '传统桦皮工艺制作的茶具',
|
||||
price: 368,
|
||||
stock: 3,
|
||||
craftsman: '关小云',
|
||||
isNew: false,
|
||||
isHot: true,
|
||||
color: '#8b4513'
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
name: '民族风头饰',
|
||||
category: 'clothing',
|
||||
description: '传统图案设计的精美头饰',
|
||||
price: 88,
|
||||
stock: 12,
|
||||
isNew: true,
|
||||
isHot: false,
|
||||
color: '#2d5016'
|
||||
const products = ref<any[]>([])
|
||||
|
||||
// 获取产品分类
|
||||
const getCategories = async () => {
|
||||
try {
|
||||
const response = await productCategoryApi.getCategoryList({
|
||||
pageNo: 1,
|
||||
pageSize: 100
|
||||
})
|
||||
if (response && response.records) {
|
||||
const categoryList = response.records.map((cat: any) => ({
|
||||
id: cat.id,
|
||||
name: cat.categoryName
|
||||
}))
|
||||
categories.value = [
|
||||
{ id: 'all', name: '全部产品' },
|
||||
...categoryList
|
||||
]
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类失败:', error)
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
// 获取产品列表
|
||||
const getProducts = async (loadMore = false) => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params: any = {
|
||||
pageNo: loadMore ? currentPage.value + 1 : 1,
|
||||
pageSize: pageSize.value,
|
||||
status: 1 // 只获取上架的产品
|
||||
}
|
||||
|
||||
// 添加分类筛选
|
||||
if (selectedCategory.value !== 'all') {
|
||||
params.categoryId = selectedCategory.value
|
||||
}
|
||||
|
||||
const response = await productApi.getProductList(params)
|
||||
|
||||
if (response && response.records) {
|
||||
const newProducts = response.records.map((product: any) => ({
|
||||
...product,
|
||||
color: getRandomColor(),
|
||||
isNew: isNewProduct(product.createTime),
|
||||
isHot: product.salesCount > 50 // 假设销量大于50为热销
|
||||
}))
|
||||
|
||||
if (loadMore) {
|
||||
products.value = [...products.value, ...newProducts]
|
||||
currentPage.value++
|
||||
} else {
|
||||
products.value = newProducts
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
hasMore.value = response.current < response.pages
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取产品数据失败:', error)
|
||||
ElMessage.error('获取产品数据失败,请稍后重试')
|
||||
if (!loadMore) {
|
||||
products.value = []
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否为新产品
|
||||
const isNewProduct = (createTime: string) => {
|
||||
if (!createTime) return false
|
||||
const now = new Date()
|
||||
const created = new Date(createTime)
|
||||
const diffDays = (now.getTime() - created.getTime()) / (1000 * 3600 * 24)
|
||||
return diffDays <= 30 // 30天内为新品
|
||||
}
|
||||
|
||||
// 获取随机颜色
|
||||
const getRandomColor = () => {
|
||||
const colors = ['#8b4513', '#2d5016', '#8b4513', '#2d5016']
|
||||
return colors[Math.floor(Math.random() * colors.length)]
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const filteredProducts = computed(() => {
|
||||
@ -252,7 +239,7 @@ const filteredProducts = computed(() => {
|
||||
|
||||
// 分类筛选
|
||||
if (selectedCategory.value !== 'all') {
|
||||
result = result.filter(p => p.category === selectedCategory.value)
|
||||
result = result.filter(p => p.categoryId === selectedCategory.value)
|
||||
}
|
||||
|
||||
// 价格筛选
|
||||
@ -286,27 +273,38 @@ const getCategoryName = (categoryId: string) => {
|
||||
return category ? category.name : ''
|
||||
}
|
||||
|
||||
const addToCart = (product: any) => {
|
||||
const addToCart = async (product: any) => {
|
||||
if (product.stock === 0) {
|
||||
ElMessage.warning('商品暂时缺货')
|
||||
return
|
||||
}
|
||||
|
||||
store.addToCart(product)
|
||||
ElMessage.success('已添加到购物车')
|
||||
try {
|
||||
// 这里使用默认用户ID,实际应用中应该从用户状态获取
|
||||
const userId = '1'
|
||||
await cartApi.addToCart(userId, product.id, 1)
|
||||
store.addToCart(product)
|
||||
ElMessage.success('已添加到购物车')
|
||||
} catch (error) {
|
||||
console.error('添加到购物车失败:', error)
|
||||
ElMessage.error('添加到购物车失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
loading.value = true
|
||||
// 模拟加载更多数据
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
hasMore.value = false
|
||||
}, 1000)
|
||||
getProducts(true)
|
||||
}
|
||||
|
||||
// 监听分类变化
|
||||
const onCategoryChange = () => {
|
||||
getProducts()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
getCategories()
|
||||
getProducts()
|
||||
|
||||
// 处理搜索参数
|
||||
const searchQuery = route.query.search as string
|
||||
if (searchQuery) {
|
||||
|
Loading…
Reference in New Issue
Block a user