Commit 1a96563a by lijiabin

Merge branch 'feature/20520-【YAYA文化岛】新增转盘抽奖模块' into feature/21402-【YAYA文化岛】优化点整理

parents 0521173c dd781b5e
# Generation Info
- **Source:** `sources/vue`
- **Git SHA:** `01abf2d03815d9d0ff0b06362a68d5d9542c9e48`
- **Generated:** 2026-01-31
---
name: vue
description: Vue 3 Composition API, script setup macros, reactivity system, and built-in components. Use when writing Vue SFCs, defineProps/defineEmits/defineModel, watchers, or using Transition/Teleport/Suspense/KeepAlive.
metadata:
author: Anthony Fu
version: "2026.1.31"
source: Generated from https://github.com/vuejs/docs, scripts at https://github.com/antfu/skills
---
# Vue
> Based on Vue 3.5. Always use Composition API with `<script setup lang="ts">`.
## Preferences
- Prefer TypeScript over JavaScript
- Prefer `<script setup lang="ts">` over `<script>`
- For performance, prefer `shallowRef` over `ref` if deep reactivity is not needed
- Always use Composition API over Options API
- Discourage using Reactive Props Destructure
## Core
| Topic | Description | Reference |
|-------|-------------|-----------|
| Script Setup & Macros | `<script setup>`, defineProps, defineEmits, defineModel, defineExpose, defineOptions, defineSlots, generics | [script-setup-macros](references/script-setup-macros.md) |
| Reactivity & Lifecycle | ref, shallowRef, computed, watch, watchEffect, effectScope, lifecycle hooks, composables | [core-new-apis](references/core-new-apis.md) |
## Features
| Topic | Description | Reference |
|-------|-------------|-----------|
| Built-in Components & Directives | Transition, Teleport, Suspense, KeepAlive, v-memo, custom directives | [advanced-patterns](references/advanced-patterns.md) |
## Quick Reference
### Component Template
```vue
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
const props = defineProps<{
title: string
count?: number
}>()
const emit = defineEmits<{
update: [value: string]
}>()
const model = defineModel<string>()
const doubled = computed(() => (props.count ?? 0) * 2)
watch(() => props.title, (newVal) => {
console.log('Title changed:', newVal)
})
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div>{{ title }} - {{ doubled }}</div>
</template>
```
### Key Imports
```ts
// Reactivity
import { ref, shallowRef, computed, reactive, readonly, toRef, toRefs, toValue } from 'vue'
// Watchers
import { watch, watchEffect, watchPostEffect, onWatcherCleanup } from 'vue'
// Lifecycle
import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onBeforeUnmount } from 'vue'
// Utilities
import { nextTick, defineComponent, defineAsyncComponent } from 'vue'
```
---
name: advanced-patterns
description: Vue 3 built-in components (Transition, Teleport, Suspense, KeepAlive) and advanced directives
---
# Built-in Components & Directives
## Transition
Animate enter/leave of a single element or component.
```vue
<template>
<Transition name="fade">
<div v-if="show">Content</div>
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
</style>
```
### CSS Classes
| Class | When |
|-------|------|
| `{name}-enter-from` | Start state for enter |
| `{name}-enter-active` | Active state for enter (add transitions here) |
| `{name}-enter-to` | End state for enter |
| `{name}-leave-from` | Start state for leave |
| `{name}-leave-active` | Active state for leave |
| `{name}-leave-to` | End state for leave |
### Transition Modes
```vue
<!-- Wait for leave to complete before enter -->
<Transition name="fade" mode="out-in">
<component :is="currentView" />
</Transition>
```
### JavaScript Hooks
```vue
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@leave="onLeave"
:css="false"
>
<div v-if="show">Content</div>
</Transition>
<script setup lang="ts">
function onEnter(el: Element, done: () => void) {
// Animate with JS library
gsap.to(el, { opacity: 1, onComplete: done })
}
</script>
```
### Appear on Initial Render
```vue
<Transition appear name="fade">
<div>Shows with animation on mount</div>
</Transition>
```
## TransitionGroup
Animate list items. Each child must have a unique `key`.
```vue
<template>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>
</template>
<style>
.list-enter-active, .list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* Move animation for reordering */
.list-move {
transition: transform 0.3s ease;
}
</style>
```
## Teleport
Render content to a different DOM location.
```vue
<template>
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
Modal content rendered at body
</div>
</Teleport>
</template>
```
### Props
```vue
<!-- CSS selector -->
<Teleport to="#modal-container">
<!-- DOM element -->
<Teleport :to="targetElement">
<!-- Disable teleport conditionally -->
<Teleport to="body" :disabled="isMobile">
<!-- Defer until target exists (Vue 3.5+) -->
<Teleport defer to="#late-rendered-target">
```
## Suspense
Handle async dependencies with loading states. **Experimental feature.**
```vue
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
```
### Async Dependencies
Suspense waits for:
- Components with `async setup()`
- Components using top-level `await` in `<script setup>`
- Async components created with `defineAsyncComponent`
```vue
<!-- AsyncComponent.vue -->
<script setup lang="ts">
const data = await fetch('/api/data').then(r => r.json())
</script>
```
### Events
```vue
<Suspense
@pending="onPending"
@resolve="onResolve"
@fallback="onFallback"
>
...
</Suspense>
```
## KeepAlive
Cache component instances when toggled.
```vue
<template>
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
</template>
```
### Include/Exclude
```vue
<!-- By name (string or regex) -->
<KeepAlive include="ComponentA,ComponentB">
<KeepAlive :include="/^Tab/">
<KeepAlive :include="['TabA', 'TabB']">
<!-- Exclude -->
<KeepAlive exclude="ModalComponent">
<!-- Max cached instances -->
<KeepAlive :max="10">
```
### Lifecycle Hooks
```ts
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// Called when component is inserted from cache
fetchLatestData()
})
onDeactivated(() => {
// Called when component is removed to cache
pauseTimers()
})
```
## v-memo
Skip re-renders when dependencies unchanged. Use for performance optimization.
```vue
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
<!-- Only re-renders when item.selected changes -->
<ExpensiveComponent :item="item" />
</div>
</template>
```
Equivalent to `v-once` when empty:
```vue
<div v-memo="[]">Never updates</div>
```
## v-once
Render once, skip all future updates.
```vue
<span v-once>Static: {{ neverChanges }}</span>
```
## Custom Directives
Create reusable DOM manipulations.
```ts
// Directive definition
const vFocus: Directive<HTMLElement> = {
mounted: (el) => el.focus()
}
// Full hooks
const vColor: Directive<HTMLElement, string> = {
created(el, binding, vnode, prevVnode) {},
beforeMount(el, binding) {},
mounted(el, binding) {
el.style.color = binding.value
},
beforeUpdate(el, binding) {},
updated(el, binding) {
el.style.color = binding.value
},
beforeUnmount(el, binding) {},
unmounted(el, binding) {}
}
```
### Directive Arguments & Modifiers
```vue
<div v-color:background.bold="'red'">
<script setup lang="ts">
const vColor: Directive<HTMLElement, string> = {
mounted(el, binding) {
// binding.arg = 'background'
// binding.modifiers = { bold: true }
// binding.value = 'red'
el.style[binding.arg || 'color'] = binding.value
if (binding.modifiers.bold) {
el.style.fontWeight = 'bold'
}
}
}
</script>
```
### Global Registration
```ts
// main.ts
app.directive('focus', {
mounted: (el) => el.focus()
})
```
<!--
Source references:
- https://vuejs.org/api/built-in-components.html
- https://vuejs.org/guide/built-ins/transition.html
- https://vuejs.org/guide/built-ins/teleport.html
- https://vuejs.org/guide/built-ins/suspense.html
- https://vuejs.org/guide/built-ins/keep-alive.html
- https://vuejs.org/api/built-in-directives.html
- https://vuejs.org/guide/reusability/custom-directives.html
-->
---
name: core-new-apis
description: Vue 3 reactivity system, lifecycle hooks, and composable patterns
---
# Reactivity, Lifecycle & Composables
## Reactivity
### ref vs shallowRef
```ts
import { ref, shallowRef } from 'vue'
// ref - deep reactivity (tracks nested changes)
const user = ref({ name: 'John', profile: { age: 30 } })
user.value.profile.age = 31 // Triggers reactivity
// shallowRef - only .value assignment triggers reactivity (better performance)
const data = shallowRef({ items: [] })
data.value.items.push('new') // Does NOT trigger reactivity
data.value = { items: ['new'] } // Triggers reactivity
```
**Prefer `shallowRef`** for large data structures or when deep reactivity is unnecessary.
### computed
```ts
import { ref, computed } from 'vue'
const count = ref(0)
// Read-only computed
const doubled = computed(() => count.value * 2)
// Writable computed
const plusOne = computed({
get: () => count.value + 1,
set: (val) => { count.value = val - 1 }
})
```
### reactive & readonly
```ts
import { reactive, readonly } from 'vue'
const state = reactive({ count: 0, nested: { value: 1 } })
state.count++ // Reactive
const readonlyState = readonly(state)
readonlyState.count++ // Warning, mutation blocked
```
Note: `reactive()` loses reactivity on destructuring. Use `ref()` or `toRefs()`.
## Watchers
### watch
```ts
import { ref, watch } from 'vue'
const count = ref(0)
// Watch single ref
watch(count, (newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`)
})
// Watch getter
watch(
() => props.id,
(id) => fetchData(id),
{ immediate: true }
)
// Watch multiple sources
watch([firstName, lastName], ([first, last]) => {
fullName.value = `${first} ${last}`
})
// Deep watch with depth limit (Vue 3.5+)
watch(state, callback, { deep: 2 })
// Once (Vue 3.4+)
watch(source, callback, { once: true })
```
### watchEffect
Runs immediately and auto-tracks dependencies.
```ts
import { ref, watchEffect, onWatcherCleanup } from 'vue'
const id = ref(1)
watchEffect(async () => {
const controller = new AbortController()
// Cleanup on re-run or unmount (Vue 3.5+)
onWatcherCleanup(() => controller.abort())
const res = await fetch(`/api/${id.value}`, { signal: controller.signal })
data.value = await res.json()
})
// Pause/resume (Vue 3.5+)
const { pause, resume, stop } = watchEffect(() => {})
pause()
resume()
stop()
```
### Flush Timing
```ts
// 'pre' (default) - before component update
// 'post' - after component update (access updated DOM)
// 'sync' - immediate, use with caution
watch(source, callback, { flush: 'post' })
watchPostEffect(() => {}) // Alias for flush: 'post'
```
## Lifecycle Hooks
```ts
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured,
onActivated, // KeepAlive
onDeactivated, // KeepAlive
onServerPrefetch // SSR only
} from 'vue'
onMounted(() => {
console.log('DOM is ready')
})
onUnmounted(() => {
// Cleanup timers, listeners, etc.
})
// Error boundary
onErrorCaptured((err, instance, info) => {
console.error(err)
return false // Stop propagation
})
```
## Effect Scope
Group reactive effects for batch disposal.
```ts
import { effectScope, onScopeDispose } from 'vue'
const scope = effectScope()
scope.run(() => {
const count = ref(0)
const doubled = computed(() => count.value * 2)
watch(count, () => console.log(count.value))
// Cleanup when scope stops
onScopeDispose(() => {
console.log('Scope disposed')
})
})
// Dispose all effects
scope.stop()
```
## Composables
Composables are functions that encapsulate stateful logic using Composition API.
### Naming Convention
- Start with `use`: `useMouse`, `useFetch`, `useCounter`
### Pattern
```ts
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
const update = (e: MouseEvent) => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
```
### Accept Reactive Input
Use `toValue()` (Vue 3.3+) to normalize refs, getters, or plain values.
```ts
import { ref, watchEffect, toValue, type MaybeRefOrGetter } from 'vue'
export function useFetch(url: MaybeRefOrGetter<string>) {
const data = ref(null)
const error = ref(null)
watchEffect(async () => {
data.value = null
error.value = null
try {
const res = await fetch(toValue(url))
data.value = await res.json()
} catch (e) {
error.value = e
}
})
return { data, error }
}
// Usage - all work:
useFetch('/api/users')
useFetch(urlRef)
useFetch(() => `/api/users/${props.id}`)
```
### Return Refs (Not Reactive)
Always return plain object with refs for destructuring compatibility.
```ts
// Good - preserves reactivity when destructured
return { x, y }
// Bad - loses reactivity when destructured
return reactive({ x, y })
```
<!--
Source references:
- https://vuejs.org/api/reactivity-core.html
- https://vuejs.org/api/reactivity-advanced.html
- https://vuejs.org/api/composition-api-lifecycle.html
- https://vuejs.org/guide/reusability/composables.html
-->
---
name: script-setup-macros
description: Vue 3 script setup syntax and compiler macros for defining props, emits, models, and more
---
# Script Setup & Macros
`<script setup>` is the recommended syntax for Vue SFCs with Composition API. It provides better runtime performance and IDE type inference.
## Basic Syntax
```vue
<script setup lang="ts">
// Top-level bindings are exposed to template
import { ref } from 'vue'
import MyComponent from './MyComponent.vue'
const count = ref(0)
const increment = () => count.value++
</script>
<template>
<button @click="increment">{{ count }}</button>
<MyComponent />
</template>
```
## defineProps
Declare component props with full TypeScript support.
```ts
// Type-based declaration (recommended)
const props = defineProps<{
title: string
count?: number
items: string[]
}>()
// With defaults (Vue 3.5+)
const { title, count = 0 } = defineProps<{
title: string
count?: number
}>()
// With defaults (Vue 3.4 and below)
const props = withDefaults(defineProps<{
title: string
items?: string[]
}>(), {
items: () => [] // Use factory for arrays/objects
})
```
## defineEmits
Declare emitted events with typed payloads.
```ts
// Named tuple syntax (recommended)
const emit = defineEmits<{
update: [value: string]
change: [id: number, name: string]
close: []
}>()
emit('update', 'new value')
emit('change', 1, 'name')
emit('close')
```
## defineModel
Two-way binding prop consumed via `v-model`. Available in Vue 3.4+.
```ts
// Basic usage - creates "modelValue" prop
const model = defineModel<string>()
model.value = 'hello' // Emits "update:modelValue"
// Named model - consumed via v-model:name
const count = defineModel<number>('count', { default: 0 })
// With modifiers
const [value, modifiers] = defineModel<string>()
if (modifiers.trim) {
// Handle trim modifier
}
// With transformers
const [value, modifiers] = defineModel({
get(val) { return val?.toLowerCase() },
set(val) { return modifiers.trim ? val?.trim() : val }
})
```
Parent usage:
```vue
<Child v-model="name" />
<Child v-model:count="total" />
<Child v-model.trim="text" />
```
## defineExpose
Explicitly expose properties to parent via template refs. Components are closed by default.
```ts
import { ref } from 'vue'
const count = ref(0)
const reset = () => { count.value = 0 }
defineExpose({
count,
reset
})
```
Parent access:
```ts
const childRef = ref<{ count: number; reset: () => void }>()
childRef.value?.reset()
```
## defineOptions
Declare component options without a separate `<script>` block. Available in Vue 3.3+.
```ts
defineOptions({
inheritAttrs: false,
name: 'CustomName'
})
```
## defineSlots
Provide type hints for slot props. Available in Vue 3.3+.
```ts
const slots = defineSlots<{
default(props: { item: string; index: number }): any
header(props: { title: string }): any
}>()
```
## Generic Components
Declare generic type parameters using the `generic` attribute.
```vue
<script setup lang="ts" generic="T extends string | number">
defineProps<{
items: T[]
selected: T
}>()
</script>
```
Multiple generics with constraints:
```vue
<script setup lang="ts" generic="T, U extends Record<string, T>">
import type { Item } from './types'
defineProps<{
data: U
key: keyof U
}>()
</script>
```
## Local Custom Directives
Use `vNameOfDirective` naming convention.
```ts
const vFocus = {
mounted: (el: HTMLElement) => el.focus()
}
// Or import and rename
import { myDirective as vMyDirective } from './directives'
```
```vue
<template>
<input v-focus />
</template>
```
## Top-level await
Use `await` directly in `<script setup>`. The component becomes async and must be used with `<Suspense>`.
```vue
<script setup lang="ts">
const data = await fetch('/api/data').then(r => r.json())
</script>
```
<!--
Source references:
- https://vuejs.org/api/sfc-script-setup.html
-->
......@@ -24,10 +24,10 @@ const zipPath = path.resolve(__dirname, 'dist.tar.gz')
const { spawn, servicePath, serviceFilePath, unzipDir } = unzipDirMode
// 服务器连接信息
const connectInfo = {
host: '', // 服务器地址
port: '22',
username: 'root',
password: '', // 服务器密码
host: process.env.DEPLOY_PROD_HOST, // 服务器地址
port: process.env.DEPLOY_PROD_PORT,
username: process.env.DEPLOY_PROD_USERNAME,
password: process.env.DEPLOY_PROD_PASSWORD, // 服务器密码
}
//链接服务器
let conn = new ssh.Client()
......
......@@ -21,11 +21,12 @@
"build:test": "nvm use 20 && vite build --mode test",
"deploy:test": "node deploy/deploytest.js",
"build:prod": "nvm use 20 && vite build --mode production",
"deploy:prod": "node deploy/deployprod.js",
"deploy:prod:update-info": "node deploy/deployprod.js --update-info"
"deploy:prod": "node --env-file=.env.local deploy/deployprod.js",
"deploy:prod:update-info": "node --env-file=.env.local deploy/deployprod.js --update-info"
},
"dependencies": {
"@element-plus/icons-vue": "^2.3.2",
"@lucky-canvas/vue": "^0.1.11",
"@types/crypto-js": "^4.2.2",
"@vueuse/components": "^14.0.0",
"@vueuse/core": "^14.0.0",
......@@ -37,6 +38,7 @@
"crypto-js": "^4.2.0",
"dayjs": "^1.11.19",
"element-plus": "^2.11.5",
"gsap": "^3.14.2",
"inquirer": "^13.0.2",
"notivue": "^2.4.5",
"pinia": "^3.0.3",
......
......@@ -10,6 +10,11 @@
"source": "oxc-project/oxc",
"sourceType": "github",
"computedHash": "1f852a179bed024d1a65c73d345436ab28399f586e34fdcfd9602ed2c73d1fdc"
},
"vue": {
"source": "antfu/skills",
"sourceType": "github",
"computedHash": "f72b54618560c53d65740515ded5bb833750c4574dbe961fd21f022bd2b31a74"
}
}
}
......@@ -9,6 +9,11 @@ import type {
BackendAddOrUpdateLotteryPrizeDto,
BackendAddOrUpdateLotteryConfigDto,
BackendLotteryConfigDto,
BackendWheelPrizeListSearchParams,
BackendWheelPrizeListItemDto,
BackendAddOrUpdateWheelPrizeDto,
BackendAddOrUpdateWheelConfigDto,
BackendWheelConfigDto,
} from './types'
import type { BackendServicePageResult } from '@/utils/request/types'
import { BooleanFlag } from '@/constants'
......@@ -121,3 +126,44 @@ export const setLotteryConfig = (data: BackendAddOrUpdateLotteryConfigDto) => {
data,
})
}
// ========== 大转盘抽奖 ==========
export const getWheelPrizeList = (data: BackendWheelPrizeListSearchParams) => {
return service.request<BackendServicePageResult<BackendWheelPrizeListItemDto>>({
url: '/api/cultureWheelPrizes/listPrizesByPage',
method: 'POST',
data,
})
}
export const addOrUpdateWheelPrize = (data: BackendAddOrUpdateWheelPrizeDto) => {
return service.request({
url: '/api/cultureWheelPrizes/addOrUpdatePrize',
method: 'POST',
data,
})
}
export const deleteWheelPrize = (idList: number[]) => {
return service.request({
url: '/api/cultureWheelPrizes/deletePrizes',
method: 'POST',
data: { idList },
})
}
export const getWheelConfig = () => {
return service.request<BackendWheelConfigDto>({
url: '/api/cultureWheelPrizes/getWheelConfig',
method: 'POST',
})
}
export const setWheelConfig = (data: BackendAddOrUpdateWheelConfigDto) => {
return service.request({
url: '/api/cultureWheelPrizes/setWheelConfig',
method: 'POST',
data,
})
}
......@@ -107,3 +107,42 @@ export type BackendLotteryConfigDto = {
inRegistrationTime: BooleanFlag
registrationTimeDesc: string
} | null
// ========== 大转盘抽奖 ==========
export interface BackendWheelPrizeListSearchParams extends PageSearchParams {
name?: string
isEnabled?: BooleanFlag
}
export interface BackendWheelPrizeListItemDto {
id: number
name: string
imageUrl: string
quantity: number
probability: number
isEnabled: BooleanFlag
createdAt: number
}
export interface BackendAddOrUpdateWheelPrizeDto {
id?: number
name: string
imageUrl: string
quantity: number
probability: number
isEnabled: BooleanFlag
}
export type BackendWheelConfigDto = {
isActivityActive: boolean
startTime: number
endTime: number
costYaCoin: number
} | null
export interface BackendAddOrUpdateWheelConfigDto {
costYaCoin: number
startTime: string
endTime: string
}
// 企业文化接口
export * from './task'
export * from './sign'
export * from './shop'
export * from './tag'
export * from './user'
export * from './case'
export * from './home'
export * from './practice'
export * from './common'
export * from './login'
export * from './article'
export * from './online'
export * from './otherUserPage'
export * from './auction'
export * from './dailyLottery'
export * from './launchCampaign'
export * from './selfMessage'
export * from "./task";
export * from "./sign";
export * from "./shop";
export * from "./tag";
export * from "./user";
export * from "./case";
export * from "./home";
export * from "./practice";
export * from "./common";
export * from "./login";
export * from "./article";
export * from "./online";
export * from "./otherUserPage";
export * from "./auction";
export * from "./dailyLottery";
export * from "./launchCampaign";
export * from "./selfMessage";
export * from "./luckyWheel";
// 导出类型
export * from './task/types'
export * from './shop/types'
export * from './tag/types'
export * from './article/types'
export * from './user/types'
export * from './case/types'
export * from './home/types'
export * from './practice/types'
export * from './common/types'
export * from './login/types'
export * from './article/types'
export * from './online/types'
export * from './otherUserPage/types'
export * from './auction/types'
export * from './dailyLottery/types'
export * from './launchCampaign/types'
export * from './selfMessage/types'
export * from "./task/types";
export * from "./shop/types";
export * from "./tag/types";
export * from "./article/types";
export * from "./user/types";
export * from "./case/types";
export * from "./home/types";
export * from "./practice/types";
export * from "./common/types";
export * from "./login/types";
export * from "./article/types";
export * from "./online/types";
export * from "./otherUserPage/types";
export * from "./auction/types";
export * from "./dailyLottery/types";
export * from "./launchCampaign/types";
export * from "./selfMessage/types";
export * from "./luckyWheel/types";
......@@ -31,6 +31,7 @@ export interface LoginResponseDto {
hiddenName: string
signature: string
refreshToken: string
address: string
}
export interface GetWxSignatureResponseDto {
......
import service from '@/utils/request'
import type {
WheelPrizeItemDto,
WheelConfigDto,
LuckWheelResultDto,
UserWheelRecordItemDto,
} from './types'
import type { BackendServicePageResult, PageSearchParams } from '@/utils/request/types'
// 大转盘相关接口
/**
* 获取转盘列表
*/
export const getWheelPrizeList = () => {
return service.request<WheelPrizeItemDto[]>({
url: `/api/cultureWheelPrizes/getWheelPrizes`,
method: 'POST',
data: {},
})
}
/**
* 前台相关转盘配置
*/
export const getWheelConfig = () => {
return service.request<WheelConfigDto>({
url: `/api/cultureWheelPrizes/getWheelConfig`,
method: 'POST',
data: {},
})
}
/**
* 用户抽奖
*/
export const participateLuckyWheel = () => {
return service.request<LuckWheelResultDto>({
url: `/api/cultureWheelPrizes/participate`,
method: 'POST',
data: {},
})
}
/**
* 获取用户大转盘记录
*/
export const getUserWheelRecordList = (data: PageSearchParams) => {
return service.request<BackendServicePageResult<UserWheelRecordItemDto>>({
url: `/api/cultureWheelPrizes/listWheelRecordsByPage`,
method: 'POST',
data,
})
}
import type { BooleanFlag } from '@/constants/enums'
export interface WheelPrizeItemDto {
createdAt: number
createdUser: string
description: string
id: number | null
imageUrl: string
isEnabled: BooleanFlag
name: string
probability: number
quantity: number
sortOrder: number
type: number
updatedAt: number
}
export type WheelConfigDto = {
isActivityActive: boolean
startTime: number
endTime: number
costYaCoin: number
} | null
export type LuckWheelResultDto = {
blessingText: string | null
coinCost: number
isWin: boolean
prizeId: number | null
prizeImageUrl: string
prizeName: string
}
export interface UserWheelRecordItemDto {
blessingText: string
coinCost: number
createdAt: number
createdAtStr: string
id: number
isWin: BooleanFlag
issueTime: null
issueTimeStr: string
issuerName: null
memo: string
prizeId: null
prizeImageUrl: null
prizeName: null
status: null
userId: number
userName: string
}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
<defs>
<linearGradient id="ringGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#5a4bd8"/>
<stop offset="50%" stop-color="#6858ec"/>
<stop offset="100%" stop-color="#7b6fef"/>
</linearGradient>
</defs>
<circle cx="200" cy="200" r="186" fill="none" stroke="url(#ringGrad)" stroke-width="26"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(11.25 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(22.5 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(33.75 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(45 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(56.25 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(67.5 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(78.75 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(90 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(101.25 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(112.5 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(123.75 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(135 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(146.25 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(157.5 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(168.75 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(180 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(191.25 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(202.5 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(213.75 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(225 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(236.25 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(247.5 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(258.75 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(270 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(281.25 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(292.5 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(303.75 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(315 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(326.25 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#ffd54f" transform="rotate(337.5 200 200)"/>
<circle cx="200" cy="14" r="4.5" fill="#f8bbd0" transform="rotate(348.75 200 200)"/>
</svg>
......@@ -194,6 +194,7 @@ const formattedDuration = computed(() => formatTime(videoDuration.value))
let progressTimer: ReturnType<typeof setInterval> | null = null
const SKIP_TIME = import.meta.env.MODE === 'production' ? 60 : 0
const startTimers = () => {
progressTimer = setInterval(() => {
if (videoRef.value) {
......@@ -202,7 +203,7 @@ const startTimers = () => {
videoDuration.value = duration
if (duration && Number.isFinite(duration)) {
videoProgress.value = (currentTime / duration) * 100
if (!canSkip.value && currentTime >= 60) canSkip.value = true
if (!canSkip.value && currentTime >= SKIP_TIME) canSkip.value = true
}
}
}, 200)
......
......@@ -471,7 +471,7 @@ const {
commentId = 0,
type,
} = defineProps<{
authorId?: string // 文章作者id
authorId?: string | number // 文章作者id
id: number // 文章ID
defaultSize?: number
isQuestion?: boolean // 如果是问题的话 展示有点不一样
......
<template>
<div class="lucky-wheel-wrapper" :class="{ 'scale-80': smallerThanXl }">
<LuckyWheel
ref="myLucky"
width="260px"
height="260px"
:blocks="blocks"
:buttons="buttons"
:prizes="computedPrizes"
:default-config="defaultConfig"
:default-style="defaultStyle"
@end="endCallback"
/>
<!-- 自定义指针 -->
<div class="wheel-pointer">
<div class="pointer-pin" />
<div class="pointer-dot" />
</div>
<button
class="go-btn"
:class="{ 'is-spinning': isSpinning }"
:disabled="isSpinning"
@click="popClick"
>
<span class="go-text">GO</span>
</button>
</div>
</template>
<script lang="ts" setup>
import { LuckyWheel } from '@lucky-canvas/vue'
import ringTexture from '@/assets/img/lucky-wheel-outer-ring.svg'
import type { WheelPrizeItemDto, LuckWheelResultDto, WheelConfigDto } from '@/api'
import { participateLuckyWheel, getWheelPrizeList } from '@/api'
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import { useMessageBox } from '@/hooks'
import { useYaBiStore } from '@/stores'
import { push } from 'notivue'
const { wheelConfig } = defineProps<{
wheelConfig: WheelConfigDto
}>()
const yabiStore = useYaBiStore()
const { confirm } = useMessageBox()
const breakpoints = useBreakpoints(breakpointsTailwind)
const smallerThanXl = breakpoints.smaller('xl')
const wheelPrizeList = ref<WheelPrizeItemDto[]>([])
const emit = defineEmits<{
handlePrizeResult: [LuckWheelResultDto]
}>()
const myLucky = ref<InstanceType<typeof LuckyWheel>>()
const defaultConfig = {
offsetDegree: 0,
speed: 20,
accelerationTime: 2500,
decelerationTime: 2500,
}
const defaultStyle = {
fontColor: '#333',
fontWeight: 'bold',
lineHeight: '14px',
lengthLimit: '60%',
}
const blocks = [
{ padding: '2px', background: '#e8e4ff' },
{
padding: '16px',
background: '#6858ec',
imgs: [
{
src: ringTexture,
width: '100%',
height: '100%',
rotate: true,
},
],
},
{ padding: '3px', background: '#7b6fef' },
]
const computedPrizes = computed(() => {
const width = wheelPrizeList.value.length >= 6 ? '37%' : '30%'
return wheelPrizeList.value.map((item: WheelPrizeItemDto, index: number) => {
return {
background: index % 2 === 0 ? '#ffffff' : '#f3f0ff',
fonts: [
{
text: item.name,
top: '8%',
fontSize: '10px',
fontColor: index % 2 === 0 ? '#6858ec' : '#e5a012',
lineClamp: 1,
},
],
imgs: [
{
src: item.imageUrl,
width,
top: '35%',
},
],
id: item.id,
}
})
})
const buttons = [
{ radius: '25%', background: '#fce4e4' },
{ radius: '20%', background: '#f75a5a' },
{
radius: '18%',
background: '#e63939',
pointer: false,
fonts: [{ text: '', top: '-14px' }],
},
]
const isSpinning = ref(false)
let resultPrize: null | LuckWheelResultDto = null
const startCallback = async () => {
if (isSpinning.value) return
isSpinning.value = true
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)
}
// 获取最新的奖品列表 对比 是否更新了
const popClick = async () => {
if (isSpinning.value) return
if (yabiStore.yabiData.currentValue < wheelConfig!.costYaCoin)
return push.error(
`您的YA币不足,抽奖所需${wheelConfig!.costYaCoin}YA币,当前YA币${yabiStore.yabiData.currentValue}`,
)
const { data } = await getWheelPrizeList()
const newWheelPrizeList = data
const oldWheelPrizeList = wheelPrizeList.value
// 暂时只需要对比 1长度不一致 需要更新 2如果长度一样 如果有一组的名字或者图片 不一样 需要更新
let shouldUpdate = false
if (newWheelPrizeList.length !== oldWheelPrizeList.length) {
shouldUpdate = true
} else {
newWheelPrizeList.forEach((item: WheelPrizeItemDto, index: number) => {
if (
item.name !== oldWheelPrizeList[index]?.name ||
item.imageUrl !== oldWheelPrizeList[index]?.imageUrl
) {
shouldUpdate = true
}
})
}
if (shouldUpdate) {
// 给用户提示 奖池更新了 请重新点击
await confirm({
title: '检测到后台奖池有更新',
message: '请重新点击按钮开始抽奖',
type: 'warning',
showCancelButton: false,
})
wheelPrizeList.value = newWheelPrizeList
} else {
// 二次确认
await confirm({
title: `本次抽奖消耗${wheelConfig?.costYaCoin} YA币,确定开始吗?`,
})
startCallback()
}
}
const endCallback = () => {
setTimeout(() => {
isSpinning.value = false
emit('handlePrizeResult', resultPrize as LuckWheelResultDto)
}, 500)
}
const initWheelPrizeList = async () => {
const { data } = await getWheelPrizeList()
wheelPrizeList.value = data
}
onMounted(() => {
initWheelPrizeList()
})
</script>
<style scoped>
.lucky-wheel-wrapper {
position: relative;
display: inline-block;
width: 260px;
height: 260px;
border-radius: 50%;
box-shadow:
0 2px 8px rgba(104, 88, 236, 0.6),
0 0 16px 1px rgba(104, 88, 236, 0.08);
}
.go-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50px;
height: 50px;
border-radius: 50%;
border: 3px solid #ff8a80;
background: linear-gradient(145deg, #ff5252, #d32f2f);
box-shadow:
0 4px 12px rgba(211, 47, 47, 0.5),
inset 0 2px 4px rgba(255, 255, 255, 0.3);
cursor: pointer;
z-index: 10;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.go-btn:hover:not(:disabled) {
transform: translate(-50%, -50%) scale(1.1);
box-shadow:
0 6px 20px rgba(211, 47, 47, 0.6),
inset 0 2px 4px rgba(255, 255, 255, 0.4);
background: linear-gradient(145deg, #ff6e6e, #e53935);
}
.go-btn:active:not(:disabled) {
transform: translate(-50%, -50%) scale(0.92);
box-shadow:
0 2px 6px rgba(211, 47, 47, 0.4),
inset 0 3px 6px rgba(0, 0, 0, 0.15);
background: linear-gradient(145deg, #d32f2f, #c62828);
}
.go-btn:disabled {
cursor: not-allowed;
opacity: 0.8;
}
.go-btn.is-spinning {
animation: pulse-spin 1.2s ease-in-out infinite;
}
.go-text {
font-size: 22px;
font-weight: 900;
color: #fff;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
letter-spacing: 2px;
user-select: none;
pointer-events: none;
}
@keyframes pulse-spin {
0%,
100% {
transform: translate(-50%, -50%) scale(1);
opacity: 0.8;
}
50% {
transform: translate(-50%, -50%) scale(1.06);
opacity: 1;
}
}
.wheel-pointer {
position: absolute;
top: -6px;
left: 50%;
transform: translateX(-50%);
z-index: 20;
display: flex;
flex-direction: column;
align-items: center;
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.3));
}
.pointer-pin {
width: 0;
height: 0;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
border-top: 22px solid #ffd54f;
position: relative;
}
.pointer-pin::before {
content: '';
position: absolute;
top: -22px;
left: -8px;
width: 0;
height: 0;
border-left: 8px solid transparent;
border-right: 8px solid transparent;
border-top: 18px solid #ffe082;
}
.pointer-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: linear-gradient(145deg, #ffe082, #ffc107);
border: 2px solid #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
margin-top: -3px;
}
</style>
......@@ -66,7 +66,10 @@
</p>
<!-- Actions -->
<div class="mt-5 grid grid-cols-2 gap-2.5">
<div
class="mt-5 grid grid-cols-2 gap-2.5"
:class="{ 'grid-cols-1!': !showCancelButton || !showConfirmButton }"
>
<button
v-if="showCancelButton"
type="button"
......
......@@ -112,6 +112,8 @@ export enum ActivityTypeEnum {
AUCTION = 1,
// 每日抽奖
DAILY_LOTTERY = 2,
// 大转盘
WHEEL = 3,
}
// 竞拍状态枚举
......@@ -150,6 +152,8 @@ export enum GoodsDistributionTypeEnum {
ORDER = 'order',
// 竞拍
AUCTION = 'auction',
// 大转盘
WHEEL = 'wheel',
}
// 特定视频奖励枚举
......
......@@ -233,6 +233,10 @@ export const goodsDistributionTypeListOptions: {
label: '竞拍',
value: GoodsDistributionTypeEnum.AUCTION,
},
{
label: '大转盘',
value: GoodsDistributionTypeEnum.WHEEL,
},
]
// 特定视频奖励列表
......
......@@ -56,6 +56,7 @@
</RewardButton>
<div
v-if="!isWareHouse"
class="group flex items-center cursor-pointer px-2 py-1 sm:px-3 sm:py-2 rounded-lg transition-all duration-200 hover:shadow-lg hover:bg-white/60"
@click="router.push('/auction')"
>
......@@ -175,6 +176,7 @@ const userStore = useUserStore()
const activityStore = useActivityStore()
const { userInfo } = storeToRefs(userStore)
const isWareHouse = userInfo.value.address.includes('仓库')
const router = useRouter()
const route = useRoute()
......
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import LuckyCanvas from '@lucky-canvas/vue'
import './style/index.css'
......@@ -34,6 +34,7 @@ const app = createApp(App)
app.use(notivue)
app.use(createPinia())
app.use(router)
app.use(LuckyCanvas)
// 全局指令挂载
app.directive('parse-comment', vParseComment)
......
......@@ -204,12 +204,6 @@ export const constantsRoute = [
component: () => import('@/views/backend/index.vue'),
redirect: '/backend/tags',
children: [
// {
// path: 'manager',
// name: 'ManagerManagement',
// component: () => import('@/views/backend/manager/index.vue'),
// meta: { title: '企业文化管理员' },
// },
{
path: 'tags',
name: 'OfficialManagement',
......@@ -222,24 +216,6 @@ export const constantsRoute = [
component: () => import('@/views/backend/carousel/index.vue'),
meta: { title: '轮播图设置' },
},
// {
// path: 'topic-admin',
// name: 'TopicAdminManagement',
// component: () => import('@/views/backend/topic-admin/index.vue'),
// meta: { title: '专栏——管理员' },
// },
// {
// path: 'interview-admin',
// name: 'InterviewAdminManagement',
// component: () => import('@/views/backend/interview-admin/index.vue'),
// meta: { title: '专访——管理员' },
// },
// {
// path: 'videoManage',
// name: 'VideoManageManagement',
// component: () => import('@/views/backend/videoManage/index.vue'),
// meta: { title: '视频管理' },
// },
{
path: 'caseManage',
name: 'CaseManageManagement',
......@@ -308,6 +284,13 @@ export const constantsRoute = [
component: () => import('@/views/backend/settingsMenu/dailyLotteryManage/index.vue'),
meta: { title: '每日抽奖配置' },
},
// 转盘抽奖配置
{
path: 'settingsMenu/luckyWheelManage',
name: 'LuckyWheelManageManagement',
component: () => import('@/views/backend/settingsMenu/luckyWheelManage/index.vue'),
meta: { title: '转盘抽奖配置' },
},
/**
* 栏目管理下的子菜单
*/
......
......@@ -44,6 +44,7 @@ export default defineComponent(() => {
{ path: '/backend/settingsMenu/auctionManage', title: '限时竞拍配置' },
{ path: '/backend/settingsMenu/goodsManage', title: '积分商城配置' },
{ path: '/backend/settingsMenu/dailyLotteryManage', title: '每日抽奖配置' },
{ path: '/backend/settingsMenu/luckyWheelManage', title: '转盘抽奖配置' },
],
},
// 栏目管理
......
......@@ -58,10 +58,10 @@
</el-table-column>
<el-table-column label="操作" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)" :disabled="row.isCurrent"
<el-button type="primary" link @click="handleEdit(row)" :disabled="!!row.isCurrent"
>编辑</el-button
>
<el-button type="danger" link @click="handleDelete(row)" :disabled="row.isCurrent"
<el-button type="danger" link @click="handleDelete(row)" :disabled="!!row.isCurrent"
>删除</el-button
>
</template>
......
<template>
<el-dialog v-model="visible" title="抽奖配置" width="500px">
<el-form :model="form" label-width="100px" :rules="formRules" ref="formRef">
<el-form-item label="开放时间" prop="openRangeTime">
<el-date-picker
v-model="form.openRangeTime"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="X"
/>
</el-form-item>
<el-form-item label="参与费用" prop="costYaCoin">
<el-input-number
v-model="form.costYaCoin"
:min="0"
:max="1000000"
controls-position="right"
/>
<span class="ml-2">YA币</span>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="loading">保存</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { getWheelConfig, setWheelConfig } from '@/api/backend'
import { useResetData } from '@/hooks'
import type { FormInstance, FormRules } from 'element-plus'
import { push } from 'notivue'
import { formatSeconds } from '@/utils'
const formRules: FormRules = {
openRangeTime: [{ required: true, message: '请选择开放时间', trigger: 'change' }],
costYaCoin: [{ required: true, message: '请输入参与费用', trigger: 'change' }],
}
const [form, resetForm] = useResetData<{
openRangeTime: string[]
costYaCoin: number
}>({
openRangeTime: [],
costYaCoin: 0,
})
const visible = ref(false)
const formRef = ref<FormInstance>()
const loading = ref(false)
const open = async () => {
const { data } = await getWheelConfig()
if (!data) {
resetForm()
} else {
form.value = {
openRangeTime: [String(data.startTime), String(data.endTime)],
costYaCoin: data.costYaCoin,
}
}
visible.value = true
}
const handleSubmit = async () => {
await formRef.value?.validate()
loading.value = true
try {
await setWheelConfig({
startTime: form.value.openRangeTime[0]!,
endTime: formatSeconds(form.value.openRangeTime[1]!),
costYaCoin: form.value.costYaCoin,
})
push.success('保存成功')
visible.value = false
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
defineExpose({ open })
</script>
......@@ -71,7 +71,7 @@
</div>
</div>
</div>
<div class="right flex-col gap-3 flex basis-1/4 xl:basis-1/4 min-w-0">
<div class="right flex-col gap-3 basis-1/4 xl:basis-1/4 min-w-0 hidden sm:flex">
<!-- 等级等相关信息 -->
<div
ref="levelContainerRef"
......@@ -298,67 +298,87 @@
</transition>
</div>
</div>
<!-- 每日抽奖 -->
<div v-if="lotteryPrizesDetail" class="lottery-container common-box rounded-lg bg-#FFF7E6">
<div class="flex items-center gap-2 mb-3">
<div class="w-1 h-4 bg-gradient-to-b from-amber-400 to-orange-400 rounded-full"></div>
<h1 class="text-sm sm:text-base font-bold">每日抽奖</h1>
</div>
<template v-if="!isWareHouse">
<!-- 每日抽奖 -->
<div
class="flex items-center gap-3 min-w-0 p-3 rounded-lg bg-white/60 border border-amber-100"
v-if="lotteryPrizesDetail"
class="lottery-container common-box rounded-lg bg-#FFF7E6"
>
<el-image
class="w-18 h-18 rounded-lg flex-shrink-0 shadow-sm"
:src="lotteryPrizesDetail.prizesImg"
fit="cover"
:preview-src-list="[lotteryPrizesDetail.prizesImg]"
/>
<div class="flex flex-col min-w-0 gap-1">
<div class="font-semibold text-sm truncate">
{{ lotteryPrizesDetail.prizesName }}
</div>
<div class="text-12px text-gray-500 flex items-center gap-1">
<svg-icon name="small_coin" size="14" />
<span>{{ lotteryPrizesDetail.registrationFee }} YA币</span>
</div>
<div class="text-12px text-gray-400">今日{{ lotteryPrizesDetail.number }}人参与</div>
<div class="flex items-center gap-2 mb-3">
<div class="w-1 h-4 bg-gradient-to-b from-amber-400 to-orange-400 rounded-full"></div>
<h1 class="text-sm sm:text-base font-bold">每日抽奖</h1>
</div>
</div>
<div v-if="lotteryPrizesDetail.winner" class="text-xs text-amber-600 mt-2 px-1 truncate">
昨日中奖:{{ lotteryPrizesDetail.winner }}
</div>
<div class="flex justify-center items-center mt-3">
<el-button
v-if="!lotteryPrizesDetail.isJoin"
class="w-full! border-none! transition-all duration-200 text-xs sm:text-sm bg-[linear-gradient(to_right,#FFD06A_0%,#FFB143_100%)]! text-#333! shadow-[0px_1px_8px_0_rgba(255,173,91,0.25)] hover:-translate-y-0.5 hover:shadow-[0px_4px_10px_0_rgba(255,173,91,0.4)] active:translate-y-0"
type="primary"
@click="handleLottery"
<div
class="flex items-center gap-3 min-w-0 p-3 rounded-lg bg-white/60 border border-amber-100"
>
<svg-icon name="daily_lottery" size="18" class="mr-1" />
参与抽奖
</el-button>
<el-image
class="w-18 h-18 rounded-lg flex-shrink-0 shadow-sm"
:src="lotteryPrizesDetail.prizesImg"
fit="cover"
:preview-src-list="[lotteryPrizesDetail.prizesImg]"
/>
<div class="flex flex-col min-w-0 gap-1">
<div class="font-semibold text-sm truncate">
{{ lotteryPrizesDetail.prizesName }}
</div>
<div class="text-12px text-gray-500 flex items-center gap-1">
<svg-icon name="small_coin" size="14" />
<span>{{ lotteryPrizesDetail.registrationFee }} YA币</span>
</div>
<div class="text-12px text-gray-400">
今日{{ lotteryPrizesDetail.number }}人参与
</div>
</div>
</div>
<div
v-else
class="w-full text-center py-2 rounded-md bg-amber-50 border border-amber-200 border-dashed text-amber-400 text-xs sm:text-sm"
v-if="lotteryPrizesDetail.winner"
class="text-xs text-amber-600 mt-2 px-1 truncate"
>
✓ 今日已参与
昨日中奖:{{ lotteryPrizesDetail.winner }}
</div>
</div>
</div>
<!-- 大转盘 -->
<!-- <div class="lottery-container common-box rounded-lg bg-#F5F0FF">
<div class="flex items-center gap-2 mb-4">
<div class="w-1 h-4 bg-gradient-to-b from-violet-500 to-purple-500 rounded-full"></div>
<h1 class="text-sm sm:text-base font-bold">大转盘</h1>
<div class="flex justify-center items-center mt-3">
<el-button
v-if="!lotteryPrizesDetail.isJoin"
class="w-full! border-none! transition-all duration-200 text-xs sm:text-sm bg-[linear-gradient(to_right,#FFD06A_0%,#FFB143_100%)]! text-#333! shadow-[0px_1px_8px_0_rgba(255,173,91,0.25)] hover:-translate-y-0.5 hover:shadow-[0px_4px_10px_0_rgba(255,173,91,0.4)] active:translate-y-0"
type="primary"
@click="handleLottery"
>
<svg-icon name="daily_lottery" size="18" class="mr-1" />
参与抽奖
</el-button>
<div
v-else
class="w-full text-center py-2 rounded-md bg-amber-50 border border-amber-200 border-dashed text-amber-400 text-xs sm:text-sm"
>
✓ 今日已参与
</div>
</div>
</div>
<div class="flex justify-center">
<LuckyWheel />
<!-- 大转盘 -->
<div
v-if="wheelConfig?.isActivityActive"
class="lottery-container common-box rounded-lg bg-#F5F0FF"
>
<div class="flex items-center gap-2 xl:mb-4">
<div
class="w-1 h-4 bg-gradient-to-b from-violet-500 to-purple-500 rounded-full"
></div>
<h1 class="text-sm sm:text-base font-bold">大转盘</h1>
</div>
<div class="flex justify-center">
<LuckyWheelContainer :wheelConfig="wheelConfig" />
</div>
<div
class="flex items-center justify-center text-sm text-gray-500 xl:mt-4 px-1 truncate"
>
每次抽奖{{ wheelConfig?.costYaCoin }} YA币
</div>
</div>
</div> -->
</template>
</div>
</div>
......@@ -384,6 +404,7 @@ import {
getRecordData,
getUserDailyLotteryInfo,
userJoinLottery,
getWheelConfig,
} from '@/api'
import { TaskTypeEnum, TaskDateLimitTypeText, ArticleTypeEnum } from '@/constants'
import type {
......@@ -392,6 +413,7 @@ import type {
UserAccountDataDto,
UserRecordDataDto,
DailyLotteryInfo,
WheelConfigDto,
} from '@/api'
import { TABS_REF_KEY, levelListOptions } from '@/constants'
import { useScrollTop, useMessageBox } from '@/hooks'
......@@ -399,11 +421,14 @@ import { useQuestionStore, useActivityStore, useYaBiStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { push } from 'notivue'
import { useBreakpoints, breakpointsTailwind } from '@vueuse/core'
// import LuckyWheel from '@/components/common/LuckyWheel/index.vue'
import LuckyWheelContainer from '@/components/common/LuckyWheelContainer/index.vue'
import { RewardButtonEnum } from '@/constants'
import RewardButton from '@/components/common/RewardButton/index.vue'
import { useTourStore } from '@/stores'
import { useTourStore, useUserStore } from '@/stores'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const isWareHouse = userInfo.value.address.includes('仓库')
const { confirm } = useMessageBox()
const tourStore = useTourStore()
const { shouldShowAskTabTour } = storeToRefs(tourStore)
......@@ -540,6 +565,11 @@ const handleLottery = async () => {
push.success('参与每日抽奖成功!')
}
/**
* 大转盘
*/
const wheelConfig = ref<WheelConfigDto>(null)
const handleTask = async (item: TaskItemDto) => {
if (item.currentCount === item.limitCount) return
// 先暂时写死
......@@ -592,7 +622,8 @@ const initPage = () => {
getUserAccountData(),
getRecordData(),
getUserDailyLotteryInfo(),
]).then(([r1, r2, r3, r4]) => {
getWheelConfig(),
]).then(([r1, r2, r3, r4, r5]) => {
if (r1.status === 'fulfilled') {
carouselList.value = r1.value.data
}
......@@ -606,6 +637,9 @@ const initPage = () => {
if (r4.status === 'fulfilled') {
lotteryPrizesDetail.value = r4.value.data
}
if (r5.status === 'fulfilled') {
wheelConfig.value = r5.value.data
}
})
}
......@@ -631,6 +665,7 @@ onActivated(async () => {
refreshTaskData(false)
refreshUserAccountData()
getLotteryPrizesDetail()
if (route.fullPath.includes('#levelContainerRef')) {
await handleBackTop()
open.value = true
......
......@@ -103,7 +103,9 @@ const selectAmount = (amount: number) => {
// 确认打赏
const handleConfirm = async () => {
if (yabiData.value.currentValue < selectedAmount.value) {
push.warning('余额不足,请先充值')
push.error(
`您的YA币不足,打赏所需${selectedAmount.value}YA币,当前YA币${yabiData.value.currentValue}`,
)
return
}
......
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