Commit c7fd6aad by lijiabin

【需求 17679】 feat: 优化完成积分商城页面

parent 22115883
import service from '@/utils/request/index' import service from '@/utils/request/index'
import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types' import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
import type { ShopItem, YaBiData } from './types' import type { ExchangeGoodsParams, ShopItemDto, ShopSearchParams, YaBiData } from './types'
/** /**
* 积分商城列表 * 积分商城列表
*/ */
export const getShopItemList = (data: PageSearchParams) => { export const getShopItemList = (data: ShopSearchParams) => {
return service.request<BackendServicePageResult<ShopItem>>({ return service.request<BackendServicePageResult<ShopItemDto>>({
url: '/api/culture/shop/item/pageList', url: '/api/culture/shop/item/pageList',
method: 'POST', method: 'POST',
data, data,
...@@ -33,3 +33,14 @@ export const getYaBiData = () => { ...@@ -33,3 +33,14 @@ export const getYaBiData = () => {
data: {}, data: {},
}) })
} }
/**
* 兑换商品
*/
export const exchangeGoods = (data: ExchangeGoodsParams) => {
return service.request({
url: '/api/culture/shop/item/exchange',
method: 'POST',
data,
})
}
import type { PageSearchParams } from '@/utils/request/types'
import { ShopGoodsTypeEnum } from '@/constants'
/**
* 请求参数类型
*/
export interface ShopSearchParams extends PageSearchParams {
region: string
itemType: ShopGoodsTypeEnum
}
/** /**
* 积分商城商品类型 * 积分商城商品类型
*/ */
export interface ShopItem { export interface ShopItemDto {
id: number id: number
description: string description: string
enable: number enable: number
...@@ -22,3 +33,12 @@ export interface ShopItem { ...@@ -22,3 +33,12 @@ export interface ShopItem {
export interface YaBiData { export interface YaBiData {
currentValue: number currentValue: number
} }
/**
* 兑换商品请求参数类型
*/
export interface ExchangeGoodsParams {
itemId: number
num: number
deliveryInfo?: string
}
...@@ -12,12 +12,14 @@ type TabsProps = { ...@@ -12,12 +12,14 @@ type TabsProps = {
type TabsEmits = { type TabsEmits = {
'update:modelValue': [TypeOfValue] 'update:modelValue': [TypeOfValue]
change: [TypeOfValue] change: [TypeOfValue]
setA: [string]
} }
const BASE_TAB_CALASSES = const BASE_TAB_CALASSES =
'tab-item cursor-pointer bg-white rounded-lg flex items-center justify-center px-4 py-2 min-w-20 h-8 transition-all duration-300 hover:shadow-md hover:bg-#D9EFFD/20 hover:text-#000/80 hover:-translate-y-0.5 active:scale-95 font-medium text-14px text-#000/55 shadow-sm' 'tab-item cursor-pointer bg-#d9effd/80 rounded-lg flex items-center justify-center px-4 py-2 min-w-20 h-8 transition-all duration-300 hover:shadow-md hover:bg-blue-100 hover:text-blue-700 hover:-translate-y-0.5 active:scale-95 font-medium text-14px text-gray-700 shadow-sm border border-blue-100'
const ACTIVE_TAB_CLASSES = ' !bg-#D9EFFD shadow-lg transform -translate-y-1 !text-#000'
const ACTIVE_TAB_CLASSES =
' !bg-gradient-to-r !from-[#3b82f6]/90 !to-[#60a5fa]/90 !shadow-lg transform -translate-y-1 !text-white !border-transparent'
// <div class="left flex gap-3"> 未设置排列方式 需要给父组件设置 flex布局 // <div class="left flex gap-3"> 未设置排列方式 需要给父组件设置 flex布局
export default function Tabs({ tabs, modelValue }: TabsProps, { emit }: SetupContext<TabsEmits>) { export default function Tabs({ tabs, modelValue }: TabsProps, { emit }: SetupContext<TabsEmits>) {
return tabs.map((tab) => ( return tabs.map((tab) => (
......
...@@ -2,85 +2,84 @@ ...@@ -2,85 +2,84 @@
* 确认兑换商品的弹窗内容 * 确认兑换商品的弹窗内容
*/ */
import { ShopGoodsTypeEnum, regionListOptions } from '@/constants' import { ShopGoodsTypeEnum, regionListOptions } from '@/constants'
import type { ShopItem } from '@/api' import type { ExchangeGoodsParams, ShopItemDto } from '@/api'
import type { SetupContext } from 'vue' import type { SetupContext } from 'vue'
import ask from '@/assets/img/culture/ask.png' import ask from '@/assets/img/culture/ask.png'
type ExchangeContentProps = { type ExchangeContentProps = {
item: ShopItem item: ShopItemDto
type: ShopGoodsTypeEnum modelValue: ExchangeGoodsParams
modelValue: {
region: string
num: number
}
} }
type ExchangeContentEvents = { type ExchangeContentEvents = {
'update:modelValue'(data: { region: string; num: number }): void 'update:modelValue'(data: ExchangeGoodsParams): void
} }
export default function ExchangeContent( export default function ExchangeContent(
{ item, modelValue, type }: ExchangeContentProps, { item, modelValue }: ExchangeContentProps,
context: SetupContext<ExchangeContentEvents>, context: SetupContext<ExchangeContentEvents>,
) { ) {
return ( return (
<div class="exchange-content py-6"> <div class="exchange-content py-6 px-4">
{/* 商品图片区域 - 同上 */} {/* 商品图片区域 */}
<div class="flex justify-center mb-8"> <div class="flex justify-center mb-8">
<div class="relative"> <div class="relative">
<div class="w-32 h-32 bg-gradient-to-br from-orange-100 to-pink-100 rounded-3xl flex items-center justify-center"> <div class="w-32 h-32 bg-gradient-to-br from-orange-100 to-pink-100 rounded-3xl flex items-center justify-center shadow-lg">
<div class="w-20 h-20 bg-white rounded-2xl flex items-center justify-center shadow-sm"> <div class="w-20 h-20 bg-white rounded-2xl flex items-center justify-center shadow-sm">
<img src={ask} alt={item.name} class="w-16 h-16 object-contain" /> <img src={ask} alt={item.name} class="w-16 h-16 object-contain" />
</div> </div>
</div> </div>
<div class="absolute -top-2 -right-2 w-7 h-7 bg-blue-500 rounded-full flex items-center justify-center"> <div class="absolute -top-2 -right-2 w-7 h-7 bg-blue-500 rounded-full flex items-center justify-center shadow-md">
<span class="text-white text-sm font-medium">6</span> <span class="text-white text-sm font-medium">6</span>
</div> </div>
</div> </div>
</div> </div>
{/* 商品信息 */} {/* 商品信息 */}
<div class="space-y-4 mb-8"> <div class="space-y-3 mb-8">
<div class="flex items-center justify-center gap-3"> <div class="flex items-center gap-3 px-4">
<div class="w-1.5 h-1.5 bg-gray-400 rounded-full"></div> <div class="w-1.5 h-1.5 bg-gray-400 rounded-full flex-shrink-0"></div>
<span class="text-gray-600 text-sm">名称:</span> <span class="text-gray-600 text-sm min-w-12">名称:</span>
<span class="font-medium text-gray-900">{item.name}</span> <span class="font-medium text-gray-900 flex-1">{item.name}</span>
</div> </div>
<div class="flex items-center justify-center gap-3"> <div class="flex items-center gap-3 px-4">
<div class="w-1.5 h-1.5 bg-orange-400 rounded-full"></div> <div class="w-1.5 h-1.5 bg-orange-400 rounded-full flex-shrink-0"></div>
<span class="text-gray-600 text-sm">积分:</span> <span class="text-gray-600 text-sm min-w-12">积分:</span>
<span class="font-semibold text-orange-500 text-lg">{item.price}YA币</span> <span class="font-semibold text-orange-500 text-lg">{item.price}YA币</span>
</div> </div>
</div> </div>
{/* 办公点选择 */} {/* 办公点选择和数量 */}
{type === ShopGoodsTypeEnum.REAL_GOODS && ( {item.itemType === ShopGoodsTypeEnum.REAL_GOODS && (
<div class="mb-6"> <div class=" rounded-2xl px-5 mx-2 space-y-4">
<div class="text-center text-gray-700 text-sm font-medium mb-4">选择办公点:</div> {/* 办公点选择 */}
<div class=" w-full"> <div>
<label class="text-gray-700 text-sm font-medium mb-2 block">办公点</label>
<el-select <el-select
modelValue={modelValue.region} modelValue={modelValue.deliveryInfo}
onUpdate:modelValue={(value: string) => onUpdate:modelValue={(value: string) =>
context.emit('update:modelValue', { ...modelValue, region: value }) context.emit('update:modelValue', { ...modelValue, deliveryInfo: value })
} }
placeholder="请选择办公点" placeholder="请选择办公点"
class="w-full"
> >
{regionListOptions.map((office) => ( {regionListOptions.map((office) => (
<el-option <el-option key={office.value} label={office.label} value={office.value} />
class="text-center w-full"
key={office.value}
label={office.label}
value={office.value}
/>
))} ))}
</el-select> </el-select>
选择数量 </div>
{/* 数量选择 */}
<div>
<label class="text-gray-700 text-sm font-medium mb-2 block">选择数量</label>
<el-input-number <el-input-number
min={1}
modelValue={modelValue.num} modelValue={modelValue.num}
onUpdate:modelValue={(value: number) => onUpdate:modelValue={(value: number) =>
context.emit('update:modelValue', { ...modelValue, num: value }) context.emit('update:modelValue', { ...modelValue, num: value })
} }
placeholder="请选择数量" class="w-full"
controls-position="right"
/> />
</div> </div>
</div> </div>
...@@ -91,19 +90,15 @@ export default function ExchangeContent( ...@@ -91,19 +90,15 @@ export default function ExchangeContent(
ExchangeContent.props = { ExchangeContent.props = {
item: { item: {
type: Object as PropType<ShopItem>, type: Object as PropType<ShopItemDto>,
required: true, required: true,
}, },
modelValue: { modelValue: {
type: Object as PropType<{ region: string; num: number }>, type: Object as PropType<ExchangeGoodsParams>,
required: true,
},
type: {
type: Number,
required: true, required: true,
}, },
} }
ExchangeContent.emits = { ExchangeContent.emits = {
'update:modelValue': (value: { region: string; num: number }) => value, 'update:modelValue': (value: ExchangeGoodsParams) => value,
} }
<template> <template>
<div class="min-h-screen bg-gradient-to-br from-[#e8f1ff] to-[#f3ebff] p-6"> <div class="min-h-screen bg-white p-6">
<div class="max-w-[1440px] mx-auto flex flex-col lg:flex-row gap-4 transition-all duration-500"> <div class="max-w-[1440px] mx-auto">
<!-- 左侧主要内容 --> <!-- 顶部积分卡片 -->
<div class="flex-1 basis-full lg:basis-3/4 transition-all duration-500"> <div
<!-- 当前积分 --> class="bg-gradient-to-r from-purple-50 to-blue-50 rounded-2xl p-5 shadow-sm mb-8 border border-purple-100"
<div class="bg-white rounded-2xl p-6 shadow-md flex justify-between items-center mb-6"> >
<span class="text-lg font-semibold text-gray-700"> <div class="flex justify-between items-center flex-wrap gap-4">
当前YA币: <div class="flex items-baseline gap-3">
<span class="text-[#8b5cf6] text-2xl font-bold">{{ currentYaBi }}</span> <span class="text-gray-700 text-base font-medium">当前YA币:</span>
</span> <span class="text-[#8b5cf6] text-4xl font-bold">{{ currentYaBi }}</span>
<button </div>
class="px-4 py-2 !outline-none bg-gradient-to-r from-[#8b5cf6] to-[#6366f1] text-white rounded-full text-sm hover:bg-gradient-to-r cursor-pointer hover:shadow-lg transition-all duration-300" <div class="flex gap-3">
@click="onOpenExchangeRecordDialog" <button
> class="cursor-pointer px-6 py-2.5 bg-white text-[#8b5cf6] rounded-full text-sm font-medium border-2 border-[#8b5cf6] hover:bg-[#8b5cf6] hover:text-white transition-all duration-300 shadow-sm hover:shadow-md"
YA币收支记录 @click="onOpenExchangeRecordDialog"
</button> >
商品领取列表
</button>
<button
class="cursor-pointer px-6 py-2.5 bg-gradient-to-r from-[#8b5cf6] to-[#6366f1] text-white rounded-full text-sm font-medium hover:shadow-lg transition-all duration-300"
@click="onOpenExchangeRecordDialog"
>
YA币收支记录
</button>
</div>
</div>
</div>
<!-- 虚拟装饰区域 -->
<div class="mb-10">
<div class="flex items-center gap-3 mb-5">
<div class="w-1 h-6 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1] rounded-full"></div>
<h2 class="text-xl font-bold text-gray-800">虚拟装饰</h2>
</div> </div>
<!-- 头像装饰 --> <div
<div class="mb-6"> class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5"
<h2 class="text-lg font-semibold mb-3 text-gray-700">虚拟装饰</h2> >
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-4"> <div
v-for="item in virtualGoodsList"
:key="item.id"
class="group bg-gradient-to-br from-purple-50 to-pink-50 rounded-2xl p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-purple-200 hover:-translate-y-1"
@click="onExchangeGoods(item)"
>
<div class="w-24 h-24 mb-3 flex items-center justify-center">
<img
src="@/assets/img/culture/ask.png"
alt=""
class="w-full h-full object-contain group-hover:scale-110 transition-transform duration-300"
/>
</div>
<div <div
v-for="item in virtualGoodsList" class="text-sm text-gray-700 mb-3 font-medium text-center line-clamp-2 min-h-[40px]"
:key="item.id"
class="bg-white rounded-xl shadow p-3 flex flex-col items-center hover:shadow-lg transition-all cursor-pointer"
@click="onExchangeGoods(item, ShopGoodsTypeEnum.VIRTUAL_GOODS)"
> >
<img src="@/assets/img/culture/ask.png" alt="" class="w-20 h-20" /> {{ item.name }}
<div class="text-sm text-gray-600 mb-2">{{ item.name }}</div>
<div class="bg-pink-100 text-pink-600 text-xs px-3 py-1 rounded-full">
{{ item.price }}积分
</div>
</div> </div>
</div> <div
<!-- 分页 --> class="bg-gradient-to-r from-pink-500 to-rose-500 text-white text-xs px-4 py-1.5 rounded-full font-medium shadow-sm"
<div class="flex justify-end mt-4"> >
<div class="bg-white rounded-xl shadow-sm border border-gray-100 p-3 w-fit"> {{ item.price }}积分
<el-pagination
v-model:current-page="virtualGoodsSearchParams.current"
v-model:page-size="virtualGoodsSearchParams.size"
:total="virtualGoodsTotal"
@current-change="virtualGoodsGoToPage"
@size-change="virtualGoodsChangePageSize"
/>
</div> </div>
</div> </div>
</div> </div>
<!-- 实物奖品 --> <!-- 分页 -->
<div> <div class="flex justify-end mt-6">
<h2 class="text-lg font-semibold mb-3 text-gray-700">亚声实物</h2> <div class="bg-gray-50 rounded-xl shadow-sm border border-gray-200 p-3">
<div class="flex gap-3 text-sm text-gray-500 mb-4 items-center"> <el-pagination
地区:<Tabs size="small"
:tabs="tabs" v-model:current-page="virtualGoodsSearchParams.current"
v-model="realGoodsSearchParams.region as string" v-model:page-size="virtualGoodsSearchParams.size"
@change="onChangeRegion" :total="virtualGoodsTotal"
@current-change="virtualGoodsGoToPage"
@size-change="virtualGoodsChangePageSize"
/> />
</div> </div>
</div>
</div>
<!-- 实物奖品区域 -->
<div class="mb-10">
<div class="flex items-end gap-3 mb-5">
<div class="w-1 h-6 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1] rounded-full"></div>
<h2 class="text-xl font-bold text-gray-800">亚声实物</h2>
</div>
<!-- 地区筛选 -->
<div class="flex gap-3 text-sm mb-6 items-center flex-wrap">
<span class="text-gray-600 font-medium">地区:</span>
<!-- <el-tabs type="card" v-model="realGoodsSearchParams.region" @change="onChangeRegion">
<el-tab-pane
v-for="item in tabs"
:key="item.value"
:label="item.label"
:name="item.value"
/>
</el-tabs> -->
<Tabs
:tabs="tabs"
v-model="realGoodsSearchParams.region"
@change="onChangeRegion"
class="bg-pink-500"
/>
</div>
<div v-show="realGoodsList.length">
<div <div
v-show="realGoodsList.length" class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-5"
class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4"
> >
<div <div
v-for="item in realGoodsList" v-for="item in realGoodsList"
:key="item.id" :key="item.id"
class="bg-white rounded-xl shadow p-3 flex flex-col items-center hover:shadow-lg transition-all cursor-pointer" class="group bg-gradient-to-br from-blue-50 to-indigo-50 rounded-2xl p-5 flex flex-col items-center hover:shadow-xl transition-all duration-300 cursor-pointer border border-transparent hover:border-blue-200 hover:-translate-y-1"
@click="onExchangeGoods(item, ShopGoodsTypeEnum.REAL_GOODS)" @click="onExchangeGoods(item)"
> >
<img src="@/assets/img/culture/ask.png" alt="" class="w-20 h-20" /> <div class="w-24 h-24 mb-3 flex items-center justify-center">
<div class="text-sm text-gray-600 mb-2">{{ item.name }}</div> <img
<div class="bg-pink-100 text-pink-600 text-xs px-3 py-1 rounded-full"> src="@/assets/img/culture/ask.png"
alt=""
class="w-full h-full object-contain group-hover:scale-110 transition-transform duration-300"
/>
</div>
<div
class="text-sm text-gray-700 mb-3 font-medium text-center line-clamp-2 min-h-[40px]"
>
{{ item.name }}
</div>
<div
class="bg-gradient-to-r from-pink-500 to-rose-500 text-white text-xs px-4 py-1.5 rounded-full font-medium shadow-sm"
>
{{ item.price }}积分 {{ item.price }}积分
</div> </div>
</div> </div>
</div> </div>
<div v-show="!realGoodsList.length" class="flex justify-center items-center">
<el-empty description="暂无数据" /> <!-- 分页 -->
<div class="flex justify-center mt-6">
<div class="bg-gray-50 rounded-xl shadow-sm border border-gray-200 p-3">
<el-pagination
size="small"
v-model:current-page="realGoodsSearchParams.current"
v-model:page-size="realGoodsSearchParams.size"
:total="realGoodsTotal"
@current-change="realGoodsGoToPage"
@size-change="realGoodsChangePageSize"
/>
</div>
</div> </div>
</div> </div>
<!-- 空状态 -->
<div v-show="!realGoodsList.length" class="flex justify-center items-center py-20">
<el-empty description="暂无数据" />
</div>
</div> </div>
<!-- 底部提示 -->
<div class="mt-16 text-center">
<div
class="inline-flex items-center gap-2 bg-blue-50 border border-blue-200 rounded-full px-6 py-3 text-sm text-gray-600"
>
实物商品兑换后请联系相关负责人领取奖励 —
<span class="text-[#6366f1] cursor-pointer font-medium hover:underline">
联系人对照表
</span>
</div>
</div>
<ExchangeRecordDialog ref="exchangeRecordDialogRef" />
</div> </div>
<div class="mt-40 text-center text-sm text-gray-500 mb-4">
实物商品兑换后清联系相关负责人领取奖励 ——
<span plain type="primary" class="text-blue-400 cursor-pointer">联系人对照表</span>
</div>
<ExchangeRecordDialog ref="exchangeRecordDialogRef" />
</div> </div>
</template> </template>
<script setup lang="tsx"> <script setup lang="tsx">
import { getShopItemList, getYaBiData } from '@/api' import { exchangeGoods, getShopItemList, getYaBiData } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { regionListOptions, ShopGoodsTypeEnum } from '@/constants' import { regionListOptions, ShopGoodsTypeEnum } from '@/constants'
import type { ShopItem } from '@/api' import type { ExchangeGoodsParams, ShopItemDto } from '@/api'
import Tabs from '@/components/common/Tabs' import Tabs from '@/components/common/Tabs'
import ExchangeRecordDialog from './components/exchangeRecordDilaog.vue' import ExchangeRecordDialog from './components/exchangeRecordDilaog.vue'
...@@ -115,7 +198,7 @@ const { ...@@ -115,7 +198,7 @@ const {
total: virtualGoodsTotal, total: virtualGoodsTotal,
} = usePageSearch(getShopItemList, { } = usePageSearch(getShopItemList, {
defaultParams: { defaultParams: {
goodsType: ShopGoodsTypeEnum.VIRTUAL_GOODS, itemType: ShopGoodsTypeEnum.VIRTUAL_GOODS,
}, },
}) })
...@@ -128,7 +211,7 @@ const { ...@@ -128,7 +211,7 @@ const {
total: realGoodsTotal, total: realGoodsTotal,
} = usePageSearch(getShopItemList, { } = usePageSearch(getShopItemList, {
defaultParams: { defaultParams: {
goodsType: ShopGoodsTypeEnum.REAL_GOODS, itemType: ShopGoodsTypeEnum.REAL_GOODS,
region: activeTab.value, region: activeTab.value,
}, },
}) })
...@@ -149,29 +232,51 @@ const getYaBiDataFn = async () => { ...@@ -149,29 +232,51 @@ const getYaBiDataFn = async () => {
// 兑换商品 // 兑换商品
const onExchangeGoods = async (item: ShopItem, type: ShopGoodsTypeEnum) => { const onExchangeGoods = async (item: ShopItemDto) => {
// if (currentYaBi.value < item.price) return ElMessage.error('YA币不足') console.log(item)
const form = ref({ const form = ref<ExchangeGoodsParams>({
region: '广州',
itemId: item.id, itemId: item.id,
num: 1, num: 1,
}) })
if (item.itemType === ShopGoodsTypeEnum.REAL_GOODS) {
form.value.deliveryInfo = '广州'
}
await ElMessageBox({ await ElMessageBox({
title: 'YA币兑换', title: 'YA币兑换',
customStyle: {
width: '1000px',
},
message: () => ( message: () => (
// @ts-ignore // @ts-ignore
<ExchangeContent item={item} type={type} v-model={form.value}></ExchangeContent> <ExchangeContent item={item} v-model={form.value}></ExchangeContent>
), ),
confirmButtonText: '确认兑换', confirmButtonText: '确认兑换',
cancelButtonText: '取消', cancelButtonText: '取消',
showCancelButton: true, showCancelButton: true,
center: true, center: true,
beforeClose: async (action, instance, done) => {
if (action === 'cancel') return done()
if (currentYaBi.value < item.price * form.value.num)
return ElMessage.error(
`您的YA币不足,兑换所需${item.price * form.value.num}YA币,当前YA币${currentYaBi.value}`,
)
try {
instance.confirmButtonLoading = true
await exchangeGoods(form.value)
ElMessage.success('兑换成功')
getYaBiDataFn()
done()
} catch (error) {
console.log(error)
} finally {
instance.confirmButtonLoading = false
}
},
}) })
console.log(form.value)
} }
onMounted(async () => { onMounted(async () => {
getYaBiDataFn() getYaBiDataFn()
}) })
</script> </script>
<style></style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment