整体优化

This commit is contained in:
Qi 2025-08-10 20:42:49 +08:00
parent eb7ced99d4
commit 29fd6a779b
17 changed files with 3569 additions and 814 deletions

3
auto-imports.d.ts vendored
View File

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

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

View File

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

View File

@ -196,15 +196,117 @@ 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([
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: '鄂伦春族刺绣挂画',
@ -229,17 +331,10 @@ const recommendedProducts = ref([
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)
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>

View File

@ -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,27 +178,44 @@
<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"/>
<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
View 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

View File

@ -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) => {
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) {

View File

@ -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,9 +158,10 @@
<el-tab-pane label="规格参数" name="specs">
<div class="specs-table">
<table>
<tbody>
<tr>
<td>产品名称</td>
<td>{{ product.name }}</td>
<td>{{ product.productName || product.name || '产品名称' }}</td>
</tr>
<tr>
<td>产品分类</td>
@ -186,6 +187,7 @@
<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>

View File

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