Commit d66ac723 by 王立鹏

Merge branch 'feature/23684-【YAYA文化岛】改动汇总' into 'master'

Feature/23684 【yaya文化岛】改动汇总

See merge request !35
parents a24b3b2b e0f5e0ae
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<html lang=""> <html lang="">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="/webicon.png" /> <link rel="icon" href="/src/assets/img/webicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>企业文化平台</title> <title>企业文化平台</title>
</head> </head>
......
...@@ -19,6 +19,9 @@ import type { ...@@ -19,6 +19,9 @@ import type {
SearchMoreVideoItemDto, SearchMoreVideoItemDto,
SecondCommentItemDto, SecondCommentItemDto,
UpdateArticleRecommendAndSortDto, UpdateArticleRecommendAndSortDto,
RelatedColumnItemDto,
RelatedColumnParams,
UpdateArticleShowLatestDto,
} from './types' } from './types'
import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types' import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
import { SpecificVideoRewardEnum, BooleanFlag } from '@/constants' import { SpecificVideoRewardEnum, BooleanFlag } from '@/constants'
...@@ -396,3 +399,25 @@ export const getAtUserList = ( ...@@ -396,3 +399,25 @@ export const getAtUserList = (
data, data,
}) })
} }
/**
* 根据标签获取专栏相关推荐
*/
export const getRelatedColumnList = (data: RelatedColumnParams) => {
return service.request<RelatedColumnItemDto[]>({
url: '/api/cultureArticle/relatedByTag',
method: 'POST',
data,
})
}
/**
* 修改视频是否在首页最新列表展示
*/
export const updateArticleShowLatest = (data: UpdateArticleShowLatestDto) => {
return service.request<boolean>({
url: '/api/cultureArticle/updateShowLatest',
method: 'POST',
data,
})
}
...@@ -125,6 +125,7 @@ export interface AddOrUpdateVideoDto { ...@@ -125,6 +125,7 @@ export interface AddOrUpdateVideoDto {
mainTagId: string | number mainTagId: string | number
tagList: { tagId: number; sort: number }[] | number[] tagList: { tagId: number; sort: number }[] | number[]
relateColumnId?: number relateColumnId?: number
isShowLatest?: BooleanFlag
} }
/** /**
...@@ -212,6 +213,7 @@ export interface ArticleItemDto { ...@@ -212,6 +213,7 @@ export interface ArticleItemDto {
recommendSort: number recommendSort: number
isOfficialAccount: BooleanFlag isOfficialAccount: BooleanFlag
deptId: string deptId: string
isShowLatest?: BooleanFlag
} }
/** /**
...@@ -413,6 +415,7 @@ export interface SearchMoreColumnItemDto { ...@@ -413,6 +415,7 @@ export interface SearchMoreColumnItemDto {
type: ArticleTypeEnum.COLUMN type: ArticleTypeEnum.COLUMN
videoDuration: string videoDuration: string
viewCount: number viewCount: number
publishTime:number
} }
/** /**
...@@ -502,3 +505,18 @@ export interface AtUserInfoDto { ...@@ -502,3 +505,18 @@ export interface AtUserInfoDto {
userId: number userId: number
version: number version: number
} }
export interface RelatedColumnParams {
articleId: number
tagId: number
}
export interface RelatedColumnItemDto {
id: number
title: string
}
export interface UpdateArticleShowLatestDto {
articleId: number
isShowLatest: BooleanFlag
}
...@@ -2,6 +2,7 @@ import service from '@/utils/request/index' ...@@ -2,6 +2,7 @@ import service from '@/utils/request/index'
import type { import type {
BackendAuctionListSearchParams, BackendAuctionListSearchParams,
BackendAuctionListItemDto, BackendAuctionListItemDto,
BackendDeleteAuctionDto,
BackendAddOrUpdateAuctionItemDto, BackendAddOrUpdateAuctionItemDto,
BackendAuctionRecordItemDto, BackendAuctionRecordItemDto,
BackendLotteryPrizeListSearchParams, BackendLotteryPrizeListSearchParams,
...@@ -44,11 +45,11 @@ export const addOrUpdateAuctionItem = (data: BackendAddOrUpdateAuctionItemDto) = ...@@ -44,11 +45,11 @@ export const addOrUpdateAuctionItem = (data: BackendAddOrUpdateAuctionItemDto) =
/** /**
* 删除竞拍物品数据 * 删除竞拍物品数据
*/ */
export const deleteAuction = (idList: number[]) => { export const deleteAuction = (data: BackendDeleteAuctionDto) => {
return service.request({ return service.request({
url: 'api/cultureAuctionItems/deleteAuction', url: 'api/cultureAuctionItems/deleteAuction',
method: 'POST', method: 'POST',
data: { idList }, data,
}) })
} }
......
...@@ -36,8 +36,14 @@ export interface BackendAuctionListItemDto extends PageSearchParams { ...@@ -36,8 +36,14 @@ export interface BackendAuctionListItemDto extends PageSearchParams {
remainingTime: string remainingTime: string
} }
export interface BackendDeleteAuctionDto {
idList: number[]
confirmDelete: boolean
}
export interface BackendAddOrUpdateAuctionItemDto { export interface BackendAddOrUpdateAuctionItemDto {
id?: number id?: number
status?: AuctionStatusEnum
name: string name: string
quantity: number quantity: number
specification: string specification: string
...@@ -83,8 +89,10 @@ export interface BackendAddOrUpdateLotteryConfigDto { ...@@ -83,8 +89,10 @@ export interface BackendAddOrUpdateLotteryConfigDto {
startDate: string startDate: string
endDate: string endDate: string
autoCloseWeekend: BooleanFlag autoCloseWeekend: BooleanFlag
registrationStartHour: string registrationStartHour: number
registrationEndHour: string registrationStartMinute: number
registrationEndHour: number
registrationEndMinute: number
registrationFee: number registrationFee: number
} }
...@@ -98,8 +106,10 @@ export type BackendLotteryConfigDto = { ...@@ -98,8 +106,10 @@ export type BackendLotteryConfigDto = {
prizesImg: string prizesImg: string
prizesName: string prizesName: string
registrationEndHour: number registrationEndHour: number
registrationEndMinute: number
registrationFee: number registrationFee: number
registrationStartHour: number registrationStartHour: number
registrationStartMinute: number
startDate: number startDate: number
status: BooleanFlag status: BooleanFlag
updatedAt: number updatedAt: number
......
...@@ -15,6 +15,7 @@ export interface AddOrUpdatePracticeDto { ...@@ -15,6 +15,7 @@ export interface AddOrUpdatePracticeDto {
tagList: { tagId: number; sort: number }[] tagList: { tagId: number; sort: number }[]
sendType: SendTypeEnum sendType: SendTypeEnum
sendTime: string sendTime: string
deptId?: string
type?: ArticleTypeEnum type?: ArticleTypeEnum
} }
......
...@@ -232,13 +232,48 @@ ...@@ -232,13 +232,48 @@
</div> </div>
<!-- 标签 --> <!-- 标签 -->
<div class="flex flex-wrap gap-2 mt-6"> <div class="flex flex-wrap gap-2 mt-6">
<span <el-tooltip
v-for="item in articleDetail?.tagNameList" v-for="(item, index) in articleDetail?.tagNameList"
:key="item" :key="item"
class="px-3 py-1 text-sm bg-gradient-to-r from-cyan-100 to-blue-100 text-cyan-600 rounded-full hover:shadow-md transition-all cursor-pointer" :content="canLoadRelatedColumns ? '点击查看相关推荐专栏' : ''"
:disabled="!canLoadRelatedColumns"
placement="top"
:show-after="200"
>
<button
type="button"
:disabled="!canLoadRelatedColumns"
:class="[
'px-3 py-1 text-sm bg-gradient-to-r from-cyan-100 to-blue-100 text-cyan-600 rounded-full transition-all',
canLoadRelatedColumns ? 'cursor-pointer hover:shadow-md' : 'cursor-default',
activeKeyword === item ? 'ring-2 ring-cyan-400' : '',
]"
@click="handleTagClick(item, articleDetail.tagIdList[index]!)"
> >
#{{ item }} #{{ item }}
</span> </button>
</el-tooltip>
</div>
<div
v-if="canLoadRelatedColumns && activeKeyword"
v-loading="relatedColumnLoading"
class="mt-4 min-h-24 border-t border-gray-100 pt-4"
>
<h2 class="mb-3 text-sm font-semibold text-gray-700">相关推荐专栏</h2>
<ul v-if="relatedColumnList.length" class="space-y-2">
<li v-for="(item, index) in relatedColumnList" :key="item.id">
<button
type="button"
class="flex w-full items-center gap-2 text-left text-sm text-gray-600 hover:text-blue-600 cursor-pointer"
@click="router.push(`/articleDetail/${item.id}`)"
>
<span class="flex-shrink-0 text-gray-400">{{ index + 1 }}.</span>
<span class="truncate">{{ item.title }}</span>
</button>
</li>
</ul>
<p v-else-if="!relatedColumnLoading" class="text-sm text-gray-400">暂无相关推荐</p>
</div> </div>
</div> </div>
</template> </template>
...@@ -258,7 +293,7 @@ ...@@ -258,7 +293,7 @@
<script setup lang="ts"> <script setup lang="ts">
import dayjs from 'dayjs' import dayjs from 'dayjs'
import type { ArticleItemDto } from '@/api' import { getRelatedColumnList, type ArticleItemDto, type RelatedColumnItemDto } from '@/api'
import { articleTypeListOptions, ArticleTypeEnum, VideoPositionEnum } from '@/constants' import { articleTypeListOptions, ArticleTypeEnum, VideoPositionEnum } from '@/constants'
import ActionMore from '@/components/common/ActionMore/index.vue' import ActionMore from '@/components/common/ActionMore/index.vue'
import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue' import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue'
...@@ -290,6 +325,34 @@ const isReal = computed(() => { ...@@ -290,6 +325,34 @@ const isReal = computed(() => {
const loading = computed(() => !articleDetail.title) const loading = computed(() => !articleDetail.title)
// 只有专栏才能点击标签 获取相关推送
const canLoadRelatedColumns = computed(
() => articleDetail.type === ArticleTypeEnum.COLUMN && !isAudit,
)
// 点击标签后只展示当前关键词下的相关推荐专栏,列表由后端按优先级排好后再截取前三条。
const activeKeyword = ref('')
const relatedColumnLoading = ref(false)
const relatedColumnList = ref<RelatedColumnItemDto[]>([])
const handleTagClick = async (keyword: string, tagId: number) => {
if (!canLoadRelatedColumns.value) return
activeKeyword.value = keyword
relatedColumnList.value = []
relatedColumnLoading.value = true
try {
const { data } = await getRelatedColumnList({
articleId: articleDetail.id,
tagId,
})
relatedColumnList.value = data.slice(0, 3)
} catch {
relatedColumnList.value = []
} finally {
relatedColumnLoading.value = false
}
}
// 是否是作者 // 是否是作者
const isAuthor = computed(() => { const isAuthor = computed(() => {
return articleDetail.createUserId === userInfo.value.userId return articleDetail.createUserId === userInfo.value.userId
......
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { LuckyWheel } from '@lucky-canvas/vue' import { LuckyWheel } from '@lucky-canvas/vue'
import ringTexture from '@/assets/img/lucky-wheel-outer-ring.svg' import ringTexture from '@/assets/img/lucky-wheel-outer-ring.svg'
import spinMusic from '@/assets/audio/lucky-wheel-happy-adventure.mp3'
import type { WheelPrizeItemDto, LuckWheelResultDto, WheelConfigDto } from '@/api' import type { WheelPrizeItemDto, LuckWheelResultDto, WheelConfigDto } from '@/api'
import { participateLuckyWheel, getWheelPrizeList } from '@/api' import { participateLuckyWheel, getWheelPrizeList } from '@/api'
...@@ -56,8 +57,8 @@ const myLucky = ref<InstanceType<typeof LuckyWheel>>() ...@@ -56,8 +57,8 @@ const myLucky = ref<InstanceType<typeof LuckyWheel>>()
const defaultConfig = { const defaultConfig = {
offsetDegree: 0, offsetDegree: 0,
speed: 20, speed: 20,
accelerationTime: 2500, accelerationTime: 1000,
decelerationTime: 2500, decelerationTime: 1000,
} }
const defaultStyle = { const defaultStyle = {
...@@ -124,18 +125,42 @@ const buttons = [ ...@@ -124,18 +125,42 @@ const buttons = [
const isSpinning = ref(false) const isSpinning = ref(false)
let resultPrize: null | LuckWheelResultDto = null let resultPrize: null | LuckWheelResultDto = null
let spinAudio: HTMLAudioElement | null = null
const startSpinMusic = () => {
stopSpinMusic()
spinAudio ||= new Audio(spinMusic)
spinAudio.loop = true
spinAudio.volume = 0.35
spinAudio.play().catch(() => {})
}
const stopSpinMusic = () => {
if (!spinAudio) return
spinAudio.pause()
spinAudio.currentTime = 0
}
const startCallback = async () => { const startCallback = async () => {
if (isSpinning.value) return if (isSpinning.value) return
isSpinning.value = true isSpinning.value = true
startSpinMusic()
myLucky.value?.play() myLucky.value?.play()
setTimeout(async () => { setTimeout(async () => {
try {
const { data } = await participateLuckyWheel() const { data } = await participateLuckyWheel()
const idx = computedPrizes.value.findIndex((item) => item.id === data.prizeId) const idx = computedPrizes.value.findIndex((item) => item.id === data.prizeId)
myLucky.value?.stop(idx) myLucky.value?.stop(idx)
yabiStore.fetchYaBiData() yabiStore.fetchYaBiData()
resultPrize = data resultPrize = data
}, 1000) } catch (error) {
console.error(error)
stopSpinMusic()
isSpinning.value = false
}
}, 600)
} }
// 获取最新的奖品列表 对比 是否更新了 // 获取最新的奖品列表 对比 是否更新了
...@@ -185,7 +210,7 @@ const endCallback = () => { ...@@ -185,7 +210,7 @@ const endCallback = () => {
setTimeout(() => { setTimeout(() => {
isSpinning.value = false isSpinning.value = false
emit('handlePrizeResult', resultPrize as LuckWheelResultDto) emit('handlePrizeResult', resultPrize as LuckWheelResultDto)
}, 500) }, 280)
} }
const initWheelPrizeList = async () => { const initWheelPrizeList = async () => {
...@@ -195,6 +220,14 @@ const initWheelPrizeList = async () => { ...@@ -195,6 +220,14 @@ const initWheelPrizeList = async () => {
onMounted(() => { onMounted(() => {
initWheelPrizeList() initWheelPrizeList()
}) })
onBeforeUnmount(() => {
stopSpinMusic()
})
defineExpose({
stopSpinMusic,
})
</script> </script>
<style scoped> <style scoped>
......
<template> <template>
<Comp :wheelConfig="wheelConfig" @handle-prize-result="handlePrizeResult" /> <Comp ref="luckyWheelRef" :wheelConfig="wheelConfig" @handle-prize-result="handlePrizeResult" />
<Teleport to="body"> <Teleport to="body">
<Transition name="result"> <Transition name="result">
<div <div
...@@ -84,7 +84,7 @@ ...@@ -84,7 +84,7 @@
<p class="blessing-caption-subtitle">抽一支好运电子签,把今天过得亮一点</p> <p class="blessing-caption-subtitle">抽一支好运电子签,把今天过得亮一点</p>
</div> </div>
<el-button class="mt-8" type="primary" @click="showResult = false">知道了</el-button> <el-button class="mt-8" type="primary" @click="closeResult">知道了</el-button>
</template> </template>
<template v-else> <template v-else>
...@@ -92,7 +92,7 @@ ...@@ -92,7 +92,7 @@
<div class="mt-6 text-white text-3xl font-bold"> <div class="mt-6 text-white text-3xl font-bold">
恭喜获得 {{ currentPrize?.prizeName }} 恭喜获得 {{ currentPrize?.prizeName }}
</div> </div>
<el-button class="mt-8" type="primary" @click="showResult = false">知道了</el-button> <el-button class="mt-8" type="primary" @click="closeResult">知道了</el-button>
</template> </template>
</div> </div>
</Transition> </Transition>
...@@ -112,6 +112,7 @@ const { wheelConfig } = defineProps<{ ...@@ -112,6 +112,7 @@ const { wheelConfig } = defineProps<{
const showResult = ref(false) const showResult = ref(false)
const currentPrize = ref<LuckWheelResultDto | null>(null) const currentPrize = ref<LuckWheelResultDto | null>(null)
const luckyWheelRef = ref<InstanceType<typeof Comp>>()
const blessingWrapRef = ref<HTMLElement | null>(null) const blessingWrapRef = ref<HTMLElement | null>(null)
const blessingGlowRef = ref<HTMLElement | null>(null) const blessingGlowRef = ref<HTMLElement | null>(null)
...@@ -305,10 +306,10 @@ const playBlessingAnimation = () => { ...@@ -305,10 +306,10 @@ const playBlessingAnimation = () => {
}) })
blessingTimeline blessingTimeline
.to(scene, { opacity: 1, y: 0, duration: 0.45 }) .to(scene, { opacity: 1, y: 0, duration: 0.35 })
.to(glow, { opacity: 0.95, scale: 1, duration: 0.7, ease: 'sine.out' }, '<') .to(glow, { opacity: 0.95, scale: 1, duration: 0.55, ease: 'sine.out' }, '<')
.to(slot, { opacity: 1, scaleX: 1, duration: 0.52 }, '-=0.3') .to(slot, { opacity: 1, scaleX: 1, duration: 0.42 }, '-=0.26')
.to(fan, { opacity: 1, y: 0, duration: 0.4 }, '<') .to(fan, { opacity: 1, y: 0, duration: 0.34 }, '<')
.to( .to(
fanItems, fanItems,
{ {
...@@ -316,10 +317,10 @@ const playBlessingAnimation = () => { ...@@ -316,10 +317,10 @@ const playBlessingAnimation = () => {
y: (_index, target) => Number(target.getAttribute('data-y') || 0), y: (_index, target) => Number(target.getAttribute('data-y') || 0),
rotate: (_index, target) => Number(target.getAttribute('data-angle') || 0), rotate: (_index, target) => Number(target.getAttribute('data-angle') || 0),
scale: (_index, target) => Number(target.getAttribute('data-scale') || 1), scale: (_index, target) => Number(target.getAttribute('data-scale') || 1),
duration: 0.9, duration: 0.7,
ease: 'back.out(1.08)', ease: 'back.out(1.08)',
stagger: { stagger: {
each: 0.055, each: 0.042,
from: 'center', from: 'center',
}, },
}, },
...@@ -329,16 +330,16 @@ const playBlessingAnimation = () => { ...@@ -329,16 +330,16 @@ const playBlessingAnimation = () => {
fanItems, fanItems,
{ {
x: (_index, target) => Number(target.getAttribute('data-x') || 0) * 0.1, x: (_index, target) => Number(target.getAttribute('data-x') || 0) * 0.1,
duration: 0.32, duration: 0.26,
ease: 'power1.inOut', ease: 'power1.inOut',
stagger: { stagger: {
each: 0.02, each: 0.016,
from: 'edges', from: 'edges',
}, },
yoyo: true, yoyo: true,
repeat: 1, repeat: 1,
}, },
'+=0.26', '+=0.18',
) )
.to( .to(
blessingTagStageRef.value, blessingTagStageRef.value,
...@@ -346,10 +347,10 @@ const playBlessingAnimation = () => { ...@@ -346,10 +347,10 @@ const playBlessingAnimation = () => {
opacity: 1, opacity: 1,
y: 0, y: 0,
filter: 'blur(0px)', filter: 'blur(0px)',
duration: 0.48, duration: 0.36,
ease: 'power2.out', ease: 'power2.out',
}, },
'+=0.02', '+=0.01',
) )
.to( .to(
tag, tag,
...@@ -358,7 +359,7 @@ const playBlessingAnimation = () => { ...@@ -358,7 +359,7 @@ const playBlessingAnimation = () => {
opacity: 0.5, opacity: 0.5,
scale: 0.97, scale: 0.97,
filter: 'blur(6px)', filter: 'blur(6px)',
duration: 0.42, duration: 0.34,
ease: 'power2.out', ease: 'power2.out',
}, },
'<', '<',
...@@ -370,10 +371,10 @@ const playBlessingAnimation = () => { ...@@ -370,10 +371,10 @@ const playBlessingAnimation = () => {
opacity: 1, opacity: 1,
scale: 1.01, scale: 1.01,
filter: 'blur(1px)', filter: 'blur(1px)',
duration: 1.08, duration: 0.82,
ease: 'power3.out', ease: 'power3.out',
}, },
'+=0.1', '+=0.08',
) )
.to( .to(
tag, tag,
...@@ -382,10 +383,10 @@ const playBlessingAnimation = () => { ...@@ -382,10 +383,10 @@ const playBlessingAnimation = () => {
rotation: -1.4, rotation: -1.4,
scale: 1.015, scale: 1.015,
filter: 'blur(0px)', filter: 'blur(0px)',
duration: 0.3, duration: 0.24,
ease: 'power2.out', ease: 'power2.out',
}, },
'-=0.18', '-=0.14',
) )
.to( .to(
tag, tag,
...@@ -394,19 +395,19 @@ const playBlessingAnimation = () => { ...@@ -394,19 +395,19 @@ const playBlessingAnimation = () => {
rotation: 0.4, rotation: 0.4,
scale: 1, scale: 1,
filter: 'blur(0px)', filter: 'blur(0px)',
duration: 0.32, duration: 0.26,
ease: 'back.out(1.5)', ease: 'back.out(1.5)',
}, },
'+=0.02', '+=0.01',
) )
.to(tagTop, { opacity: 1, duration: 0.22 }, '<') .to(tagTop, { opacity: 1, duration: 0.18 }, '<')
.to( .to(
chars, chars,
{ {
opacity: 1, opacity: 1,
y: 0, y: 0,
duration: 0.34, duration: 0.28,
stagger: 0.05, stagger: 0.035,
}, },
'-=0.08', '-=0.08',
) )
...@@ -415,7 +416,7 @@ const playBlessingAnimation = () => { ...@@ -415,7 +416,7 @@ const playBlessingAnimation = () => {
{ {
opacity: 1, opacity: 1,
y: 0, y: 0,
duration: 0.28, duration: 0.22,
}, },
'-=0.12', '-=0.12',
) )
...@@ -424,7 +425,7 @@ const playBlessingAnimation = () => { ...@@ -424,7 +425,7 @@ const playBlessingAnimation = () => {
{ {
opacity: 1, opacity: 1,
y: 0, y: 0,
duration: 0.42, duration: 0.34,
}, },
'-=0.02', '-=0.02',
) )
...@@ -461,6 +462,11 @@ const handlePrizeResult = (prize: LuckWheelResultDto) => { ...@@ -461,6 +462,11 @@ const handlePrizeResult = (prize: LuckWheelResultDto) => {
currentPrize.value = prize currentPrize.value = prize
showResult.value = true showResult.value = true
} }
const closeResult = () => {
showResult.value = false
luckyWheelRef.value?.stopSpinMusic()
}
</script> </script>
<style> <style>
......
...@@ -62,6 +62,21 @@ ...@@ -62,6 +62,21 @@
:height="100" :height="100"
/> />
</div> </div>
<div v-if="userInfo.isOfficialAccount && type === ArticleTypeEnum.PRACTICE" class="mb-3">
<el-select
v-model="form.deptId"
placeholder="请选择所属一级部门"
clearable
class="w-full"
>
<el-option
v-for="item in firstDepListOptions"
:key="item.id"
:value="item.id"
:label="item.name"
/>
</el-select>
</div>
<!-- 标签内容 --> <!-- 标签内容 -->
<div class="mb-2"> <div class="mb-2">
<!-- 选择的标签内容 --> <!-- 选择的标签内容 -->
...@@ -148,7 +163,12 @@ import { useUserStore } from '@/stores' ...@@ -148,7 +163,12 @@ import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import SelectTagsDialog from './components/selectTagsDialog.vue' import SelectTagsDialog from './components/selectTagsDialog.vue'
import { useResetData, useUploadImg } from '@/hooks' import { useResetData, useUploadImg } from '@/hooks'
import { ArticleTypeEnum, ReleaseStatusTypeEnum, SendTypeEnum } from '@/constants' import {
ArticleTypeEnum,
firstDepListOptions,
ReleaseStatusTypeEnum,
SendTypeEnum,
} from '@/constants'
import { useTagsStore } from '@/stores' import { useTagsStore } from '@/stores'
import { addOrUpdatePractice, addOrUpdateArticle } from '@/api' import { addOrUpdatePractice, addOrUpdateArticle } from '@/api'
import type { AddOrUpdatePracticeDto } from '@/api' import type { AddOrUpdatePracticeDto } from '@/api'
...@@ -238,6 +258,7 @@ const [form, , forReset] = useResetData({ ...@@ -238,6 +258,7 @@ const [form, , forReset] = useResetData({
tagList: [], tagList: [],
sendType: SendTypeEnum.IMMEDIATE, sendType: SendTypeEnum.IMMEDIATE,
sendTime: '', sendTime: '',
deptId: '',
}) })
const { imgList, handleFileChange, handleDeleteImg, uploadPercent } = useUploadImg( const { imgList, handleFileChange, handleDeleteImg, uploadPercent } = useUploadImg(
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<div class="w-750px flex items-center gap-3"> <div class="w-750px flex items-center gap-3">
<el-select v-model="searchParams.itemType" placeholder="选择商品类型" clearable> <el-select v-model="searchParams.itemType" placeholder="选择商品类型" clearable>
<el-option label="亚声实物" :value="ShopGoodsTypeEnum.REAL_GOODS" /> <el-option label="亚声实物" :value="ShopGoodsTypeEnum.REAL_GOODS" />
<el-option label="虚拟装饰" :value="ShopGoodsTypeEnum.VIRTUAL_GOODS" /> <el-option label="虚拟商品" :value="ShopGoodsTypeEnum.VIRTUAL_GOODS" />
</el-select> </el-select>
<el-date-picker <el-date-picker
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
<el-table-column prop="itemType" label="商品类型" width="100" align="center"> <el-table-column prop="itemType" label="商品类型" width="100" align="center">
<template #default="scope"> <template #default="scope">
{{ scope.row.itemType === ShopGoodsTypeEnum.REAL_GOODS ? '亚声实物' : '虚拟装饰' }} {{ scope.row.itemType === ShopGoodsTypeEnum.REAL_GOODS ? '亚声实物' : '虚拟商品' }}
</template> </template>
</el-table-column> </el-table-column>
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<div class="w-750px flex items-center gap-3"> <div class="w-750px flex items-center gap-3">
<!-- <el-select v-model="searchParams.type" placeholder="选择商品类型" clearable> <!-- <el-select v-model="searchParams.type" placeholder="选择商品类型" clearable>
<el-option label="亚声实物" :value="ShopGoodsTypeEnum.REAL_GOODS" /> <el-option label="亚声实物" :value="ShopGoodsTypeEnum.REAL_GOODS" />
<el-option label="虚拟装饰" :value="ShopGoodsTypeEnum.VIRTUAL_GOODS" /> <el-option label="虚拟商品" :value="ShopGoodsTypeEnum.VIRTUAL_GOODS" />
</el-select> --> </el-select> -->
<el-date-picker <el-date-picker
......
...@@ -175,10 +175,17 @@ const onBid = async (item: AuctionItemDto) => { ...@@ -175,10 +175,17 @@ const onBid = async (item: AuctionItemDto) => {
} else { } else {
if (val.value > yabiData.value.currentValue) return push.error('您的YA币余额不足') if (val.value > yabiData.value.currentValue) return push.error('您的YA币余额不足')
// 如果还没有出价 只需要出价大于等于起拍价
if(data.currentPrice === 0) {
if(val.value<data.startingPrice) return push.error(
`出价必须大于等于起拍价: 至少【${data.startingPrice}】YA币`,
)
}else{
if (val.value < data.minIncrement + (data.currentPrice || data.startingPrice)) if (val.value < data.minIncrement + (data.currentPrice || data.startingPrice))
return push.error( return push.error(
`出价必须大于等于前最高出价+最低加价幅度: 至少【${data.minIncrement + (data.currentPrice || data.startingPrice)}】YA币`, `出价必须大于等于前最高出价+最低加价幅度: 至少【${data.minIncrement + (data.currentPrice || data.startingPrice)}】YA币`,
) )
}
instance.confirmButtonLoading = true instance.confirmButtonLoading = true
instance.confirmButtonText = 'Loading...' instance.confirmButtonText = 'Loading...'
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { Search, Upload } from '@element-plus/icons-vue' import { Search, Upload } from '@element-plus/icons-vue'
import { usePageSearch, useResetData } from '@/hooks' import { usePageSearch, useResetData } from '@/hooks'
import { addOrUpdateColumn } from '@/api/backend' import { addOrUpdateColumn } from '@/api/backend'
import { getArticleList, updateArticleRecommendAndSort } from '@/api' import { getArticleList, updateArticleRecommendAndSort, updateArticleShowLatest } from '@/api'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend' import type { BackendColumnListItemDto, AddOrUpdateColumnDto } from '@/api/backend'
import dayjs from 'dayjs' import dayjs from 'dayjs'
...@@ -51,6 +51,15 @@ const handleIsRecommendChange = async (val: BooleanFlag, row: ArticleItemDto) => ...@@ -51,6 +51,15 @@ const handleIsRecommendChange = async (val: BooleanFlag, row: ArticleItemDto) =>
refresh() refresh()
} }
const handleIsShowLatestChange = async (val: BooleanFlag, row: ArticleItemDto) => {
await updateArticleShowLatest({
articleId: row.id,
isShowLatest: val,
})
push.success('修改成功')
refresh()
}
// 编辑排序 // 编辑排序
const handleEditSort = async (row: ArticleItemDto) => { const handleEditSort = async (row: ArticleItemDto) => {
const value = await prompt({ const value = await prompt({
...@@ -155,6 +164,17 @@ const handleSelectionChange = (selection: BackendColumnListItemDto[]) => { ...@@ -155,6 +164,17 @@ const handleSelectionChange = (selection: BackendColumnListItemDto[]) => {
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="isShowLatest" label="是否在最新出现" min-width="160">
<template #default="{ row }">
<el-switch
:model-value="row.isShowLatest ?? BooleanFlag.YES"
:active-value="BooleanFlag.YES"
:inactive-value="BooleanFlag.NO"
@change="(val) => handleIsShowLatestChange(val as BooleanFlag, row)"
/>
</template>
</el-table-column>
<el-table-column prop="status" label="是否推荐" min-width="200"> <el-table-column prop="status" label="是否推荐" min-width="200">
<template #header> <template #header>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
......
<script lang="tsx"> <script lang="tsx">
import { KeepAlive, Transition } from 'vue' import { KeepAlive, Transition } from 'vue'
import type { Component as VueComponent } from 'vue' import type { Component as VueComponent } from 'vue'
import webIconUrl from '@/assets/img/webicon.png'
export default defineComponent(() => { export default defineComponent(() => {
interface MenuItem { interface MenuItem {
path: string path: string
...@@ -23,7 +24,6 @@ export default defineComponent(() => { ...@@ -23,7 +24,6 @@ export default defineComponent(() => {
// { path: '/backend/videoManage', title: '视频管理' }, // { path: '/backend/videoManage', title: '视频管理' },
{ path: '/backend/caseManage', title: 'YAYA案例库管理' }, { path: '/backend/caseManage', title: 'YAYA案例库管理' },
// { path: '/backend/goodsManage', title: '积分商城——商品配置' }, // { path: '/backend/goodsManage', title: '积分商城——商品配置' },
{ path: '/backend/goodsDistribution', title: '商品分发' },
// 内容管理 // 内容管理
{ {
path: '/backend/contentsMenu', path: '/backend/contentsMenu',
...@@ -35,7 +35,16 @@ export default defineComponent(() => { ...@@ -35,7 +35,16 @@ export default defineComponent(() => {
{ path: '/backend/contentsMenu/questionManage', title: '问吧管理' }, { path: '/backend/contentsMenu/questionManage', title: '问吧管理' },
], ],
}, },
// 栏目管理
{
path: '/backend/columnsMenu',
title: '栏目管理',
children: [
{ path: '/backend/columnsMenu/columnManage', title: '专栏——栏目管理' },
{ path: '/backend/columnsMenu/interviewManage', title: '专访——栏目管理' },
{ path: '/backend/columnsMenu/videoManage', title: '视频——栏目管理' },
],
},
// 配置管理 // 配置管理
{ {
path: '/backend/settingsMenu', path: '/backend/settingsMenu',
...@@ -47,16 +56,7 @@ export default defineComponent(() => { ...@@ -47,16 +56,7 @@ export default defineComponent(() => {
{ path: '/backend/settingsMenu/luckyWheelManage', title: '转盘抽奖配置' }, { path: '/backend/settingsMenu/luckyWheelManage', title: '转盘抽奖配置' },
], ],
}, },
// 栏目管理 { path: '/backend/goodsDistribution', title: '商品分发' },
{
path: '/backend/columnsMenu',
title: '栏目管理',
children: [
{ path: '/backend/columnsMenu/columnManage', title: '专栏——栏目管理' },
{ path: '/backend/columnsMenu/interviewManage', title: '专访——栏目管理' },
{ path: '/backend/columnsMenu/videoManage', title: '视频——栏目管理' },
],
},
] ]
const activeMenu = computed(() => route.path) const activeMenu = computed(() => route.path)
...@@ -111,7 +111,7 @@ export default defineComponent(() => { ...@@ -111,7 +111,7 @@ export default defineComponent(() => {
<div class="backend-layout"> <div class="backend-layout">
<aside class="backend-sidebar"> <aside class="backend-sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<img src="/webicon.png" alt="SOUNDASIA" class="logo" /> <img src={webIconUrl} alt="SOUNDASIA" class="logo" />
<span class="header-title">企业文化后台管理</span> <span class="header-title">企业文化后台管理</span>
</div> </div>
......
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
{{ {{
// 未展示的时候不展示竞拍状态 // 未展示的时候不展示竞拍状态
row.isDisplay === 0 row.isDisplay === 0
? '' ? '未展示在前台时不展示竞拍状态'
: auctionStatusListOptions.find((item) => item.value === row.status)?.label : auctionStatusListOptions.find((item) => item.value === row.status)?.label
}} }}
</template> </template>
...@@ -195,6 +195,17 @@ ...@@ -195,6 +195,17 @@
inactive-text="否" inactive-text="否"
/> />
</el-form-item> </el-form-item>
<el-form-item v-if="form.id" label="竞拍状态" prop="status">
<el-select v-model="form.status" placeholder="请选择竞拍状态" class="w-60%!">
<el-option
v-for="item in editAuctionStatusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
/>
</el-select>
</el-form-item>
</el-form> </el-form>
</el-scrollbar> </el-scrollbar>
<template #footer> <template #footer>
...@@ -221,7 +232,7 @@ import { ...@@ -221,7 +232,7 @@ import {
} from '@/api/backend' } from '@/api/backend'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import type { BackendAuctionListItemDto, BackendAddOrUpdateAuctionItemDto } from '@/api/backend' import type { BackendAuctionListItemDto, BackendAddOrUpdateAuctionItemDto } from '@/api/backend'
import { auctionStatusListOptions } from '@/constants' import { AuctionStatusEnum, auctionStatusListOptions } from '@/constants'
import UploadFile from '@/components/common/UploadFile/index.vue' import UploadFile from '@/components/common/UploadFile/index.vue'
import AuctionRecordDialog from './components/auctionRecordDialog.vue' import AuctionRecordDialog from './components/auctionRecordDialog.vue'
import dayjs from 'dayjs' import dayjs from 'dayjs'
...@@ -253,19 +264,45 @@ const [form, resetForm] = useResetData<BackendAddOrUpdateAuctionItemDto>({ ...@@ -253,19 +264,45 @@ const [form, resetForm] = useResetData<BackendAddOrUpdateAuctionItemDto>({
isDisplay: 0, isDisplay: 0,
startTime: '', startTime: '',
endTime: '', endTime: '',
status: undefined,
})
// 可编辑的 竞拍状态 只可以选择 「竞拍成功 / 已流拍」
const editAuctionStatusOptions = computed(() => {
const options = auctionStatusListOptions
.filter((item) =>
[AuctionStatusEnum.AUCTION_SUCCESS, AuctionStatusEnum.AUCTION_FAILED].includes(item.value),
)
.map((item) => ({
...item,
disabled: false,
}))
const hasCurrentStatus = options.some((item) => item.value === form.value.status)
if (form.value.status !== undefined && !hasCurrentStatus) {
const currentStatus = auctionStatusListOptions.find((item) => item.value === form.value.status)
options.unshift({
label: currentStatus?.label || `未知状态(${form.value.status})`,
value: form.value.status,
disabled: true,
})
}
return options
}) })
// 表单验证规则 // 表单验证规则
const formRules: FormRules = { const formRules: FormRules = {
name: [{ required: true, message: '请输入名称', trigger: 'blur' }], name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
description: [{ required: true, message: '请输入介绍内容', trigger: 'blur' }], description: [{ required: true, message: '请输入介绍内容', trigger: 'blur' }],
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }], quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }],
specification: [{ required: true, message: '请输入规格', trigger: 'blur' }], // specification: [{ required: true, message: '请输入规格', trigger: 'blur' }],
startingPrice: [{ required: true, message: '请输入起拍价', trigger: 'blur' }], startingPrice: [{ required: true, message: '请输入起拍价', trigger: 'blur' }],
minIncrement: [{ required: true, message: '请输入最低加价幅度', trigger: 'blur' }], minIncrement: [{ required: true, message: '请输入最低加价幅度', trigger: 'blur' }],
bidLimit: [{ required: true, message: '请输入竞拍次数上限', trigger: 'blur' }], bidLimit: [{ required: true, message: '请输入竞拍次数上限', trigger: 'blur' }],
startTime: [{ required: true, message: '请选择竞拍时间', trigger: 'change' }], startTime: [{ required: true, message: '请选择竞拍时间', trigger: 'change' }],
isDisplay: [{ required: true, message: '请选择是否展示在前台', trigger: 'blur' }], isDisplay: [{ required: true, message: '请选择是否展示在前台', trigger: 'blur' }],
imageUrl: [{ required: true, message: '请上传图片', trigger: 'change' }], imageUrl: [{ required: true, message: '请上传图片', trigger: 'change' }],
status: [{ required: true, message: '请选择竞拍状态', trigger: 'change' }],
} }
const dateRange = computed({ const dateRange = computed({
get() { get() {
...@@ -304,21 +341,18 @@ const handleEdit = (row: BackendAddOrUpdateAuctionItemDto) => { ...@@ -304,21 +341,18 @@ const handleEdit = (row: BackendAddOrUpdateAuctionItemDto) => {
// 删除 // 删除
const handleDelete = async (row: BackendAuctionListItemDto) => { const handleDelete = async (row: BackendAuctionListItemDto) => {
const confirmDelete = row.currentPrice > 0
await confirm({ await confirm({
title: '提示', title: '提示',
message: `确定要删除竞拍物品"${row.name}"吗?`, message: confirmDelete
? '该竞拍已有人出价,是否确定删除?删除后无法恢复,并会同步删除竞拍者的记录'
: `确定要删除竞拍物品"${row.name}"吗?`,
type: 'danger', type: 'danger',
}) })
try {
await deleteAuction([row.id]) await deleteAuction({ idList: [row.id], confirmDelete })
refresh() refresh()
push.success('删除成功') push.success('删除成功')
refresh()
} catch (error) {
if (error !== 'cancel') {
push.error('删除失败')
}
}
} }
// 提交表单 // 提交表单
...@@ -384,13 +418,42 @@ const handleBatchShow = async () => { ...@@ -384,13 +418,42 @@ const handleBatchShow = async () => {
} }
const handleBatchDelete = async () => { const handleBatchDelete = async () => {
const bidRows = selectedRows.value.filter((item) => item.currentPrice > 0)
const unbidRows = selectedRows.value.filter((item) => item.currentPrice === 0)
const formatNames = (rows: BackendAuctionListItemDto[]) =>
rows.map((item) => `【${item.name}】`).join('')
let message = `确定要删除竞拍物品${formatNames(unbidRows)}吗?`
if (bidRows.length && unbidRows.length) {
message = `其中${formatNames(bidRows)}这些竞拍已有人出价,是否确定删除?删除后无法恢复,并会同步删除竞拍者的记录,其余${formatNames(unbidRows)}未出价会正常删除,确定删除吗?`
} else if (bidRows.length) {
message = `${formatNames(bidRows)}这些竞拍已有人出价,是否确定删除?删除后无法恢复,并会同步删除竞拍者的记录,确定删除吗?`
}
await confirm({ await confirm({
title: '提示', title: '提示',
message: '确定要删除这些竞拍物品吗?', message,
type: 'danger', type: 'danger',
}) })
await deleteAuction(selectedRows.value.map((item) => item.id)) if (unbidRows.length && bidRows.length) {
await Promise.all([
deleteAuction({
idList: unbidRows.map((item) => item.id),
confirmDelete: false,
}),
deleteAuction({
idList: bidRows.map((item) => item.id),
confirmDelete: true,
}),
])
} else {
const rows = bidRows.length ? bidRows : unbidRows
await deleteAuction({
idList: rows.map((item) => item.id),
confirmDelete: bidRows.length > 0,
})
}
refresh() refresh()
push.success('删除成功') push.success('删除成功')
} }
......
...@@ -38,8 +38,8 @@ ...@@ -38,8 +38,8 @@
start-placeholder="开始时间" start-placeholder="开始时间"
end-placeholder="结束时间" end-placeholder="结束时间"
is-range is-range
format="HH:00" format="HH:mm"
value-format="H" value-format="HH:mm"
/> />
</el-form-item> </el-form-item>
<!-- 报名费用 --> <!-- 报名费用 -->
...@@ -93,6 +93,18 @@ const visible = ref(false) ...@@ -93,6 +93,18 @@ const visible = ref(false)
const formRef = ref<FormInstance>() const formRef = ref<FormInstance>()
const loading = ref(false) const loading = ref(false)
const formatRegistrationTime = (hour: number, minute = 0) =>
`${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`
const parseRegistrationTime = (time: string) => {
const [hour = 0, minute = 0] = time.split(':').map(Number)
return {
hour,
minute,
}
}
const open = async () => { const open = async () => {
const { data } = await getLotteryConfigDetail() const { data } = await getLotteryConfigDetail()
if (!data) { if (!data) {
...@@ -102,7 +114,10 @@ const open = async () => { ...@@ -102,7 +114,10 @@ const open = async () => {
id: data.id, id: data.id,
openRangeTime: [String(data.startDate), String(data.endDate)], openRangeTime: [String(data.startDate), String(data.endDate)],
autoCloseWeekend: data.autoCloseWeekend, autoCloseWeekend: data.autoCloseWeekend,
registrationRangeTime: [String(data.registrationStartHour), String(data.registrationEndHour)], registrationRangeTime: [
formatRegistrationTime(data.registrationStartHour, data.registrationStartMinute),
formatRegistrationTime(data.registrationEndHour, data.registrationEndMinute),
],
registrationFee: data.registrationFee, registrationFee: data.registrationFee,
} }
} }
...@@ -111,12 +126,16 @@ const open = async () => { ...@@ -111,12 +126,16 @@ const open = async () => {
const handleSubmit = async () => { const handleSubmit = async () => {
await formRef.value?.validate() await formRef.value?.validate()
const registrationStartTime = parseRegistrationTime(form.value.registrationRangeTime[0]!)
const registrationEndTime = parseRegistrationTime(form.value.registrationRangeTime[1]!)
const data = { const data = {
startDate: form.value.openRangeTime[0]!, startDate: form.value.openRangeTime[0]!,
endDate: formatSeconds(form.value.openRangeTime[1]!), endDate: formatSeconds(form.value.openRangeTime[1]!),
autoCloseWeekend: form.value.autoCloseWeekend, autoCloseWeekend: form.value.autoCloseWeekend,
registrationStartHour: form.value.registrationRangeTime[0]!, registrationStartHour: registrationStartTime.hour,
registrationEndHour: form.value.registrationRangeTime[1]!, registrationStartMinute: registrationStartTime.minute,
registrationEndHour: registrationEndTime.hour,
registrationEndMinute: registrationEndTime.minute,
registrationFee: form.value.registrationFee, registrationFee: form.value.registrationFee,
} }
loading.value = true loading.value = true
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
clearable clearable
> >
<el-option <el-option
v-for="item in regionListOptions" v-for="item in goodsRegionListOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
...@@ -77,7 +77,7 @@ ...@@ -77,7 +77,7 @@
<el-table-column prop="price" label="兑换所需YA币" min-width="200"> </el-table-column> <el-table-column prop="price" label="兑换所需YA币" min-width="200"> </el-table-column>
<el-table-column prop="showName" label="创建人" min-width="200" /> <el-table-column prop="createUserName" label="创建人" min-width="200" />
<el-table-column prop="createTime" label="创建时间" min-width="200"> <el-table-column prop="createTime" label="创建时间" min-width="200">
<template #default="{ row }"> <template #default="{ row }">
...@@ -147,7 +147,7 @@ ...@@ -147,7 +147,7 @@
<el-form-item label="上架地区" prop="region"> <el-form-item label="上架地区" prop="region">
<el-select v-model="form.region" placeholder="请选择上架地区"> <el-select v-model="form.region" placeholder="请选择上架地区">
<el-option <el-option
v-for="item in regionListOptions" v-for="item in goodsRegionListOptions"
:key="item.value" :key="item.value"
:label="item.label" :label="item.label"
:value="item.value" :value="item.value"
...@@ -197,7 +197,7 @@ import type { BackendShopItemDto } from '@/api/backend' ...@@ -197,7 +197,7 @@ import type { BackendShopItemDto } from '@/api/backend'
import { push } from 'notivue' import { push } from 'notivue'
import { useMessageBox } from '@/hooks' import { useMessageBox } from '@/hooks'
const { confirm } = useMessageBox() const { confirm } = useMessageBox()
const goodsRegionListOptions = [...regionListOptions, { label: '四地通用', value: '四地通用' }]
const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } = const { loading, list, total, reset, goToPage, changePageSize, refresh, searchParams, search } =
usePageSearch(getShopItemList) usePageSearch(getShopItemList)
......
...@@ -100,7 +100,7 @@ ...@@ -100,7 +100,7 @@
<!-- 底部信息 --> <!-- 底部信息 -->
<div class="flex items-center gap-4 text-13px text-gray-500"> <div class="flex items-center gap-4 text-13px text-gray-500">
<span>{{ item.showName }}</span> <span>{{ item.showName }}</span>
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span> <span>{{ dayjs(item.publishTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<el-icon class="text-sm"> <el-icon class="text-sm">
<IEpView /> <IEpView />
......
...@@ -5,6 +5,11 @@ import { ShopGoodsTypeEnum, regionListOptions } from '@/constants' ...@@ -5,6 +5,11 @@ import { ShopGoodsTypeEnum, regionListOptions } from '@/constants'
import type { BackendShopItemDto, ExchangeGoodsParams } from '@/api' import type { BackendShopItemDto, ExchangeGoodsParams } from '@/api'
import type { SetupContext } from 'vue' import type { SetupContext } from 'vue'
const UNIVERSAL_REGION = '四地通用'
const universalRegionOptions = regionListOptions.filter((item) =>
['深圳', '合肥', '武汉', '长沙'].includes(item.value),
)
type ExchangeContentProps = { type ExchangeContentProps = {
item: BackendShopItemDto item: BackendShopItemDto
modelValue: ExchangeGoodsParams modelValue: ExchangeGoodsParams
...@@ -17,7 +22,14 @@ export default function ExchangeContent( ...@@ -17,7 +22,14 @@ export default function ExchangeContent(
{ item, modelValue }: ExchangeContentProps, { item, modelValue }: ExchangeContentProps,
context: SetupContext<ExchangeContentEvents>, context: SetupContext<ExchangeContentEvents>,
) { ) {
const totalPrice = computed(() => item.price * (modelValue.num || 1)) const totalPrice = item.price * (modelValue.num || 1)
const deliveryOfficeOptions =
item.region === UNIVERSAL_REGION
? universalRegionOptions
: (() => {
const currentRegion = regionListOptions.find((region) => region.value === item.region)
return currentRegion ? [currentRegion] : [{ label: item.region, value: item.region }]
})()
return ( return (
<div class="py-4"> <div class="py-4">
...@@ -58,8 +70,9 @@ export default function ExchangeContent( ...@@ -58,8 +70,9 @@ export default function ExchangeContent(
} }
placeholder="请选择办公点" placeholder="请选择办公点"
class="w-full" class="w-full"
disabled={item.region !== UNIVERSAL_REGION}
> >
{regionListOptions.map((office) => ( {deliveryOfficeOptions.map((office) => (
<el-option key={office.value} label={office.label} value={office.value} /> <el-option key={office.value} label={office.label} value={office.value} />
))} ))}
</el-select> </el-select>
...@@ -94,7 +107,7 @@ export default function ExchangeContent( ...@@ -94,7 +107,7 @@ export default function ExchangeContent(
</span> </span>
<div class="flex items-baseline gap-0.5"> <div class="flex items-baseline gap-0.5">
<span class="text-sm text-orange-400">¥</span> <span class="text-sm text-orange-400">¥</span>
<span class="text-2xl font-bold text-orange-500">{totalPrice.value}</span> <span class="text-2xl font-bold text-orange-500">{totalPrice}</span>
<span class="text-xs text-gray-400 ml-1">积分</span> <span class="text-xs text-gray-400 ml-1">积分</span>
</div> </div>
</div> </div>
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<div class="w-750px flex items-center gap-3"> <div class="w-750px flex items-center gap-3">
<el-select v-model="searchParams.itemType" placeholder="选择商品类型" clearable> <el-select v-model="searchParams.itemType" placeholder="选择商品类型" clearable>
<el-option label="亚声实物" :value="ShopGoodsTypeEnum.REAL_GOODS" /> <el-option label="亚声实物" :value="ShopGoodsTypeEnum.REAL_GOODS" />
<el-option label="虚拟装饰" :value="ShopGoodsTypeEnum.VIRTUAL_GOODS" /> <el-option label="虚拟商品" :value="ShopGoodsTypeEnum.VIRTUAL_GOODS" />
</el-select> </el-select>
<el-date-picker <el-date-picker
...@@ -38,7 +38,7 @@ ...@@ -38,7 +38,7 @@
<el-table-column prop="itemType" label="商品类型" width="100" align="center"> <el-table-column prop="itemType" label="商品类型" width="100" align="center">
<template #default="scope"> <template #default="scope">
{{ scope.row.itemType === ShopGoodsTypeEnum.REAL_GOODS ? '亚声实物' : '虚拟装饰' }} {{ scope.row.itemType === ShopGoodsTypeEnum.REAL_GOODS ? '亚声实物' : '虚拟商品' }}
</template> </template>
</el-table-column> </el-table-column>
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
<div class="w-750px flex items-center gap-3"> <div class="w-750px flex items-center gap-3">
<!-- <el-select v-model="searchParams.type" placeholder="选择商品类型" clearable> <!-- <el-select v-model="searchParams.type" placeholder="选择商品类型" clearable>
<el-option label="亚声实物" :value="ShopGoodsTypeEnum.REAL_GOODS" /> <el-option label="亚声实物" :value="ShopGoodsTypeEnum.REAL_GOODS" />
<el-option label="虚拟装饰" :value="ShopGoodsTypeEnum.VIRTUAL_GOODS" /> <el-option label="虚拟商品" :value="ShopGoodsTypeEnum.VIRTUAL_GOODS" />
</el-select> --> </el-select> -->
<el-date-picker <el-date-picker
......
...@@ -4,13 +4,13 @@ ...@@ -4,13 +4,13 @@
<BackButton /> <BackButton />
<!-- 左侧:商品列表区域 --> <!-- 左侧:商品列表区域 -->
<div class="flex-3/4 min-w-0"> <div class="flex-3/4 min-w-0">
<!-- 虚拟装饰区域 --> <!-- 虚拟商品区域 -->
<div class="bg-white/90 rounded-lg p-6 shadow-lg mb-3 border border-white/60"> <div class="bg-white/90 rounded-lg p-6 shadow-lg mb-3 border border-white/60">
<div class="flex items-center gap-3 mb-6"> <div class="flex items-center gap-3 mb-6">
<div <div
class="w-1.5 h-7 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1] rounded-full shadow-sm" class="w-1.5 h-7 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1] rounded-full shadow-sm"
></div> ></div>
<h2 class="text-xl font-bold text-gray-800">虚拟装饰</h2> <h2 class="text-xl font-bold text-gray-800">虚拟商品</h2>
</div> </div>
<template v-if="virtualGoodsList.length"> <template v-if="virtualGoodsList.length">
...@@ -78,27 +78,6 @@ ...@@ -78,27 +78,6 @@
<h2 class="text-xl font-bold text-gray-800">亚声实物</h2> <h2 class="text-xl font-bold text-gray-800">亚声实物</h2>
</div> </div>
<!-- 地区筛选 -->
<div class="flex gap-3 text-sm mb-6 items-center flex-wrap">
<span class="text-gray-600 font-medium">地区:</span>
<button
v-for="item in tabs"
:key="item.value"
class="cursor-pointer px-3 py-1.5 text-sm transition-all relative"
@click="onChangeRegion(item.value)"
:class="{
'text-indigo-600 font-medium': realGoodsSearchParams.region === item.value,
'text-gray-600 hover:text-gray-900': realGoodsSearchParams.region !== item.value,
}"
>
{{ item.label }}
<span
v-if="realGoodsSearchParams.region === item.value"
class="absolute bottom-0 left-0 right-0 h-0.5 bg-gradient-to-b from-[#8b5cf6] to-[#6366f1]"
></span>
</button>
</div>
<template v-if="realGoodsList.length"> <template v-if="realGoodsList.length">
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4"> <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
<div <div
...@@ -211,7 +190,7 @@ ...@@ -211,7 +190,7 @@
<!-- 统计信息(可选) --> <!-- 统计信息(可选) -->
<div class="mt-6 pt-6 border-t border-gray-100 space-y-3"> <div class="mt-6 pt-6 border-t border-gray-100 space-y-3">
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
<span class="text-gray-500">虚拟装饰</span> <span class="text-gray-500">虚拟商品</span>
<span class="font-medium text-gray-700">{{ virtualGoodsTotal }} 件</span> <span class="font-medium text-gray-700">{{ virtualGoodsTotal }} 件</span>
</div> </div>
<div class="flex justify-between items-center text-sm"> <div class="flex justify-between items-center text-sm">
...@@ -231,7 +210,7 @@ ...@@ -231,7 +210,7 @@
<script setup lang="tsx"> <script setup lang="tsx">
import { exchangeGoods, getShopItemList } from '@/api' import { exchangeGoods, getShopItemList } from '@/api'
import { usePageSearch } from '@/hooks' import { usePageSearch } from '@/hooks'
import { regionListOptions, ShopGoodsTypeEnum } from '@/constants' import { ShopGoodsTypeEnum } from '@/constants'
import type { ExchangeGoodsParams, BackendShopItemDto } from '@/api' import type { ExchangeGoodsParams, BackendShopItemDto } from '@/api'
import ExchangeContent from './components/exchangeContent.tsx' import ExchangeContent from './components/exchangeContent.tsx'
import ExchangeGoodsRecordDialog from './components/exchangeGoodsRecordDilaog.vue' import ExchangeGoodsRecordDialog from './components/exchangeGoodsRecordDilaog.vue'
...@@ -242,9 +221,7 @@ import BackButton from '@/components/common/BackButton/index.vue' ...@@ -242,9 +221,7 @@ import BackButton from '@/components/common/BackButton/index.vue'
import { push } from 'notivue' import { push } from 'notivue'
const yabiStore = useYaBiStore() const yabiStore = useYaBiStore()
const { yabiData } = storeToRefs(yabiStore) const { yabiData } = storeToRefs(yabiStore)
const UNIVERSAL_REGION = '四地通用'
const activeTab = ref('深圳')
const tabs = ref(regionListOptions)
const exchangeGoodsRecordDialogRef = useTemplateRef<InstanceType<typeof ExchangeGoodsRecordDialog>>( const exchangeGoodsRecordDialogRef = useTemplateRef<InstanceType<typeof ExchangeGoodsRecordDialog>>(
'exchangeGoodsRecordDialogRef', 'exchangeGoodsRecordDialogRef',
...@@ -276,15 +253,9 @@ const { ...@@ -276,15 +253,9 @@ const {
} = usePageSearch(getShopItemList, { } = usePageSearch(getShopItemList, {
defaultParams: { defaultParams: {
itemType: ShopGoodsTypeEnum.REAL_GOODS, itemType: ShopGoodsTypeEnum.REAL_GOODS,
region: activeTab.value,
}, },
}) })
const onChangeRegion = (region: string) => {
realGoodsSearchParams.value.region = region
realGoodsGoToPage(1)
}
const onOpenExchangeYabiRecordDialog = () => { const onOpenExchangeYabiRecordDialog = () => {
exchangeYabiRecordDialogRef.value?.open() exchangeYabiRecordDialogRef.value?.open()
} }
...@@ -296,12 +267,16 @@ const onOpenExchangeGoodsRecordDialog = () => { ...@@ -296,12 +267,16 @@ const onOpenExchangeGoodsRecordDialog = () => {
// 兑换商品 // 兑换商品
const onExchangeGoods = async (item: BackendShopItemDto) => { const onExchangeGoods = async (item: BackendShopItemDto) => {
if(item.stock === 0) return push.warning('库存为0,不可兑换')
const form = ref<ExchangeGoodsParams>({ const form = ref<ExchangeGoodsParams>({
itemId: item.id, itemId: item.id,
num: 1, num: 1,
}) })
if (item.itemType === ShopGoodsTypeEnum.REAL_GOODS) { const isRealGoods = item.itemType === ShopGoodsTypeEnum.REAL_GOODS
form.value.deliveryInfo = '广州' const isUniversalRegion = item.region === UNIVERSAL_REGION
if (isRealGoods && !isUniversalRegion) {
form.value.deliveryInfo = item.region
} }
await ElMessageBox({ await ElMessageBox({
...@@ -320,6 +295,18 @@ const onExchangeGoods = async (item: BackendShopItemDto) => { ...@@ -320,6 +295,18 @@ const onExchangeGoods = async (item: BackendShopItemDto) => {
return push.error( return push.error(
`您的YA币不足,兑换所需${item.price * form.value.num}YA币,当前YA币${yabiData.value.currentValue}`, `您的YA币不足,兑换所需${item.price * form.value.num}YA币,当前YA币${yabiData.value.currentValue}`,
) )
if (isRealGoods && !form.value.deliveryInfo) return push.warning('请选择配送办公点')
if (isRealGoods && isUniversalRegion) {
try {
await ElMessageBox.confirm('请确认配送办公点准确性,准确无误可继续提交', '提示', {
confirmButtonText: '继续提交',
cancelButtonText: '返回修改',
type: 'warning',
})
} catch {
return
}
}
try { try {
instance.confirmButtonLoading = true instance.confirmButtonLoading = true
await exchangeGoods(form.value) await exchangeGoods(form.value)
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
</template> </template>
<template v-else> <template v-else>
<!-- 回显回来可能会有bug 所以需要key 重新渲染一下--> <!-- 回显回来可能会有bug 所以需要key 重新渲染一下-->
<WangEditor :key v-model="form.content"/> <WangEditor :key v-model="form.content" />
</template> </template>
</el-form-item> </el-form-item>
</div> </div>
......
...@@ -104,7 +104,7 @@ ...@@ -104,7 +104,7 @@
<!-- 底部信息 --> <!-- 底部信息 -->
<div class="flex items-center gap-4 text-13px text-gray-500"> <div class="flex items-center gap-4 text-13px text-gray-500">
<span>{{ item.showName }}</span> <span>{{ item.showName }}</span>
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span> <span>{{ dayjs(item.publishTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<el-icon class="text-sm"> <el-icon class="text-sm">
<IEpView /> <IEpView />
......
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