Commit d66ac723 by 王立鹏

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

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

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