mirror of
https://github.com/Megghy/vtsuru.live.git
synced 2025-12-06 18:36:55 +08:00
feat: 更新商品管理功能,添加虚拟礼物多Key支持和排序功能
- 在商品模型中添加密钥选择模式和虚拟密钥列表 - 更新商品展示组件,支持置顶标记和价格徽章 - 优化商品管理视图,添加排序功能和清空筛选条件的功能 - 改进礼物添加表单,增加输入验证和错误提示
This commit is contained in:
@@ -650,6 +650,14 @@ export enum GoodsTypes {
|
|||||||
Physical,
|
Physical,
|
||||||
Virtual
|
Virtual
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加密钥选择模式枚举
|
||||||
|
export enum KeySelectionMode {
|
||||||
|
None,
|
||||||
|
Random, // 随机选择
|
||||||
|
Sequential // 顺序选择
|
||||||
|
}
|
||||||
|
|
||||||
export interface PointGoodsSetting {
|
export interface PointGoodsSetting {
|
||||||
guardFree?: { year: number; month: number }
|
guardFree?: { year: number; month: number }
|
||||||
allowGuardLevel?: GuardLevel
|
allowGuardLevel?: GuardLevel
|
||||||
@@ -670,16 +678,22 @@ export interface ResponsePointGoodModel {
|
|||||||
maxBuyCount?: number
|
maxBuyCount?: number
|
||||||
collectUrl?: string
|
collectUrl?: string
|
||||||
embedCollectUrl?: boolean
|
embedCollectUrl?: boolean
|
||||||
|
isPinned: boolean
|
||||||
|
|
||||||
canFreeBuy: boolean
|
canFreeBuy: boolean
|
||||||
allowGuardLevel: GuardLevel
|
allowGuardLevel: GuardLevel
|
||||||
setting: PointGoodsSetting
|
setting: PointGoodsSetting
|
||||||
|
|
||||||
|
// 添加虚拟礼物多Key支持
|
||||||
|
virtualKeys?: string[]
|
||||||
|
keySelectionMode?: KeySelectionMode
|
||||||
|
currentKeyIndex?: number
|
||||||
}
|
}
|
||||||
export interface ImageUploadModel {
|
export interface ImageUploadModel {
|
||||||
existImages: string[]
|
existImages: string[]
|
||||||
newImagesBase64: string[]
|
newImagesBase64: string[]
|
||||||
}
|
}
|
||||||
export interface PointGoodsModel {
|
export interface UploadPointGoodsModel {
|
||||||
id?: number
|
id?: number
|
||||||
name: string
|
name: string
|
||||||
count?: number
|
count?: number
|
||||||
@@ -694,8 +708,14 @@ export interface PointGoodsModel {
|
|||||||
content?: string
|
content?: string
|
||||||
isAllowRebuy: boolean
|
isAllowRebuy: boolean
|
||||||
maxBuyCount?: number
|
maxBuyCount?: number
|
||||||
|
isPinned: boolean
|
||||||
|
|
||||||
setting: PointGoodsSetting
|
setting: PointGoodsSetting
|
||||||
|
|
||||||
|
// 添加虚拟礼物多Key支持
|
||||||
|
virtualKeys?: string[]
|
||||||
|
keySelectionMode?: KeySelectionMode
|
||||||
|
currentKeyIndex?: number
|
||||||
}
|
}
|
||||||
export interface AddressInfo {
|
export interface AddressInfo {
|
||||||
id?: string
|
id?: string
|
||||||
|
|||||||
9
src/components.d.ts
vendored
9
src/components.d.ts
vendored
@@ -20,28 +20,19 @@ declare module 'vue' {
|
|||||||
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
MonacoEditorComponent: typeof import('./components/MonacoEditorComponent.vue')['default']
|
||||||
NAlert: typeof import('naive-ui')['NAlert']
|
NAlert: typeof import('naive-ui')['NAlert']
|
||||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||||
NBadge: typeof import('naive-ui')['NBadge']
|
|
||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
|
||||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
|
||||||
NDivider: typeof import('naive-ui')['NDivider']
|
|
||||||
NEmpty: typeof import('naive-ui')['NEmpty']
|
NEmpty: typeof import('naive-ui')['NEmpty']
|
||||||
NFlex: typeof import('naive-ui')['NFlex']
|
NFlex: typeof import('naive-ui')['NFlex']
|
||||||
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
NFormItemGi: typeof import('naive-ui')['NFormItemGi']
|
||||||
NGridItem: typeof import('naive-ui')['NGridItem']
|
NGridItem: typeof import('naive-ui')['NGridItem']
|
||||||
NIcon: typeof import('naive-ui')['NIcon']
|
NIcon: typeof import('naive-ui')['NIcon']
|
||||||
NImage: typeof import('naive-ui')['NImage']
|
NImage: typeof import('naive-ui')['NImage']
|
||||||
NInput: typeof import('naive-ui')['NInput']
|
|
||||||
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
NPopconfirm: typeof import('naive-ui')['NPopconfirm']
|
||||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||||
NSpace: typeof import('naive-ui')['NSpace']
|
NSpace: typeof import('naive-ui')['NSpace']
|
||||||
NSpin: typeof import('naive-ui')['NSpin']
|
|
||||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
|
||||||
NTabs: typeof import('naive-ui')['NTabs']
|
|
||||||
NTag: typeof import('naive-ui')['NTag']
|
NTag: typeof import('naive-ui')['NTag']
|
||||||
NText: typeof import('naive-ui')['NText']
|
NText: typeof import('naive-ui')['NText']
|
||||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
|
||||||
PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default']
|
PointGoodsItem: typeof import('./components/manage/PointGoodsItem.vue')['default']
|
||||||
PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default']
|
PointHistoryCard: typeof import('./components/manage/PointHistoryCard.vue')['default']
|
||||||
PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default']
|
PointOrderCard: typeof import('./components/manage/PointOrderCard.vue')['default']
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { GoodsTypes, ResponsePointGoodModel } from '@/api/api-models';
|
import { GoodsTypes, ResponsePointGoodModel } from '@/api/api-models';
|
||||||
import { FILE_BASE_URL, IMGUR_URL } from '@/data/constants';
|
import { FILE_BASE_URL, IMGUR_URL } from '@/data/constants';
|
||||||
import { NAlert, NCard, NEllipsis, NEmpty, NFlex, NIcon, NImage, NTag, NText } from 'naive-ui';
|
import { NAlert, NCard, NEllipsis, NEmpty, NFlex, NIcon, NImage, NTag, NText } from 'naive-ui';
|
||||||
import { VehicleShip20Filled } from '@vicons/fluent';
|
import { VehicleShip20Filled, Pin16Filled } from '@vicons/fluent';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
goods: ResponsePointGoodModel | undefined;
|
goods: ResponsePointGoodModel | undefined;
|
||||||
@@ -22,10 +22,13 @@
|
|||||||
v-else
|
v-else
|
||||||
embedded
|
embedded
|
||||||
:style="props.contentStyle"
|
:style="props.contentStyle"
|
||||||
|
size="small"
|
||||||
class="goods-card"
|
class="goods-card"
|
||||||
|
:class="{ 'pinned-card': goods.isPinned }"
|
||||||
>
|
>
|
||||||
<!-- 商品封面 -->
|
<!-- 商品封面 -->
|
||||||
<template #cover>
|
<template #cover>
|
||||||
|
<div class="cover-container">
|
||||||
<NImage
|
<NImage
|
||||||
:src="goods.cover ? FILE_BASE_URL + goods.cover : emptyCover"
|
:src="goods.cover ? FILE_BASE_URL + goods.cover : emptyCover"
|
||||||
:fallback-src="emptyCover"
|
:fallback-src="emptyCover"
|
||||||
@@ -34,65 +37,128 @@
|
|||||||
:preview-disabled="!goods.cover"
|
:preview-disabled="!goods.cover"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</template>
|
<!-- 置顶标记 -->
|
||||||
|
<div
|
||||||
<!-- 商品信息头部 -->
|
v-if="goods.isPinned"
|
||||||
<template #header-extra>
|
class="pin-badge"
|
||||||
<NFlex justify="space-between">
|
|
||||||
<NFlex>
|
|
||||||
<NText depth="3">
|
|
||||||
库存:
|
|
||||||
</NText>
|
|
||||||
<NText v-if="goods.count && goods.count > 0">
|
|
||||||
{{ goods.count }}
|
|
||||||
</NText>
|
|
||||||
<NText
|
|
||||||
v-else-if="goods.count == 0"
|
|
||||||
style="color: #5f5f5f;"
|
|
||||||
>
|
>
|
||||||
无
|
<NIcon :component="Pin16Filled" />
|
||||||
</NText>
|
</div>
|
||||||
<NText v-else>
|
|
||||||
∞
|
|
||||||
</NText>
|
|
||||||
</NFlex>
|
|
||||||
</NFlex>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 商品标题 -->
|
<!-- 价格徽章 -->
|
||||||
<template #header>
|
<div class="price-badge">
|
||||||
<NFlex
|
<NText class="price-text">
|
||||||
align="center"
|
🪙 {{ goods.price > 0 ? goods.price : '免费' }}
|
||||||
:size="5"
|
</NText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标签容器 -->
|
||||||
|
<div class="tags-badge">
|
||||||
|
<!-- 商品类型标签 -->
|
||||||
|
<NTag
|
||||||
|
size="small"
|
||||||
|
:bordered="true"
|
||||||
|
style="background-color: transparent;"
|
||||||
|
:style="{
|
||||||
|
color: goods.type == GoodsTypes.Physical ? '#006633' : '#0066cc',
|
||||||
|
borderColor: goods.type == GoodsTypes.Physical ? '#009966' : '#3399ff',
|
||||||
|
backgroundColor: goods.type == GoodsTypes.Physical ? '#c2e6d290' : '#c2d6eb90'
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<!-- 售罄标签 -->
|
{{ goods.type == GoodsTypes.Physical ? '实物' : '虚拟' }}
|
||||||
|
</NTag>
|
||||||
|
<!-- 状态标签 -->
|
||||||
<NTag
|
<NTag
|
||||||
v-if="goods.count == 0"
|
v-if="goods.count == 0"
|
||||||
size="small"
|
size="small"
|
||||||
type="error"
|
type="error"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
|
style="color: #ffffff; background-color: rgba(255, 85, 85, 0.7);"
|
||||||
>
|
>
|
||||||
已售完
|
已售完
|
||||||
</NTag>
|
</NTag>
|
||||||
|
|
||||||
<!-- 商品类型标签 -->
|
<!-- 舰长限制标签 -->
|
||||||
<NTag
|
<NTag
|
||||||
|
v-if="goods.allowGuardLevel > 0"
|
||||||
size="small"
|
size="small"
|
||||||
:bordered="goods.type != GoodsTypes.Physical"
|
type="warning"
|
||||||
|
:bordered="false"
|
||||||
|
style="color: #333333; background-color: rgba(255, 204, 0, 0.7);"
|
||||||
>
|
>
|
||||||
{{ goods.type == GoodsTypes.Physical ? '实物' : '虚拟' }}
|
{{ goods.allowGuardLevel === 1 ? '总督' : goods.allowGuardLevel === 2 ? '提督' : '舰长' }}专属
|
||||||
</NTag>
|
</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- 商品名称 -->
|
<!-- 商品信息头部 - 改为水平布局 -->
|
||||||
<NEllipsis>
|
<template #header>
|
||||||
|
<NFlex vertical>
|
||||||
|
<!-- 标题行:左侧标题,右侧库存 -->
|
||||||
|
<NFlex
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
class="title-row"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
align="center"
|
||||||
|
class="title-container"
|
||||||
|
>
|
||||||
|
<NEllipsis
|
||||||
|
strong
|
||||||
|
class="goods-title"
|
||||||
|
:line-clamp="1"
|
||||||
|
>
|
||||||
{{ goods.name }}
|
{{ goods.name }}
|
||||||
</NEllipsis>
|
</NEllipsis>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
|
||||||
|
<NFlex
|
||||||
|
align="center"
|
||||||
|
class="stock-info"
|
||||||
|
>
|
||||||
|
<NText
|
||||||
|
depth="3"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
库存:
|
||||||
|
</NText>
|
||||||
|
<NText
|
||||||
|
v-if="goods.count && goods.count > 0"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ goods.count }}
|
||||||
|
</NText>
|
||||||
|
<NText
|
||||||
|
v-else-if="goods.count == 0"
|
||||||
|
size="small"
|
||||||
|
type="error"
|
||||||
|
>
|
||||||
|
无
|
||||||
|
</NText>
|
||||||
|
<NText
|
||||||
|
v-else
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
∞
|
||||||
|
</NText>
|
||||||
|
</NFlex>
|
||||||
|
</NFlex>
|
||||||
|
</NFlex>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- 商品描述和标签 -->
|
<!-- 商品描述和标签 -->
|
||||||
<NFlex vertical>
|
<NFlex
|
||||||
<NEllipsis :line-clamp="2">
|
vertical
|
||||||
|
:gap="8"
|
||||||
|
class="content-section"
|
||||||
|
>
|
||||||
|
<!-- 描述文本 -->
|
||||||
|
<NEllipsis
|
||||||
|
:line-clamp="2"
|
||||||
|
class="description-text"
|
||||||
|
>
|
||||||
<NText
|
<NText
|
||||||
:depth="goods.description ? 1 : 3"
|
:depth="goods.description ? 1 : 3"
|
||||||
:italic="!goods.description"
|
:italic="!goods.description"
|
||||||
@@ -101,30 +167,24 @@
|
|||||||
</NText>
|
</NText>
|
||||||
</NEllipsis>
|
</NEllipsis>
|
||||||
|
|
||||||
<!-- 标签展示 -->
|
<!-- 用户自定义标签展示 -->
|
||||||
<NFlex wrap>
|
<div
|
||||||
<!-- 舰长限制标签 -->
|
v-if="goods.tags && goods.tags.length > 0"
|
||||||
<NTag
|
class="tags-container"
|
||||||
v-if="goods.allowGuardLevel > 0"
|
|
||||||
size="tiny"
|
|
||||||
:color="{ color: '#5f5f5f', textColor: 'gold' }"
|
|
||||||
>
|
>
|
||||||
<template #icon>
|
<div class="tags-wrapper">
|
||||||
<NIcon :component="VehicleShip20Filled" />
|
|
||||||
</template>
|
|
||||||
仅限舰长
|
|
||||||
</NTag>
|
|
||||||
|
|
||||||
<!-- 商品标签 -->
|
|
||||||
<NTag
|
<NTag
|
||||||
v-for="tag in goods.tags"
|
v-for="tag in goods.tags"
|
||||||
:key="tag"
|
:key="tag"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
|
class="user-tag"
|
||||||
|
style="color: #f0f0f0; background-color: rgba(100, 100, 110, 0.7);"
|
||||||
>
|
>
|
||||||
{{ tag }}
|
{{ tag }}
|
||||||
</NTag>
|
</NTag>
|
||||||
</NFlex>
|
</div>
|
||||||
|
</div>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
|
||||||
<!-- 自定义页脚 -->
|
<!-- 自定义页脚 -->
|
||||||
@@ -137,10 +197,118 @@
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.goods-card {
|
.goods-card {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-card:hover {
|
.goods-card:hover {
|
||||||
transform: translateY(-3px);
|
transform: translateY(-3px);
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pinned-card {
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
box-shadow: 0 2px 10px rgba(var(--primary-color-rgb), 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover-container {
|
||||||
|
position: relative;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: var(--error-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
color: white;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
color: white;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-badge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-text {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-row {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-container {
|
||||||
|
max-width: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-title {
|
||||||
|
font-size: 1em;
|
||||||
|
line-height: 1.3;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-section {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-text {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
position: relative;
|
||||||
|
max-height: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tag {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-tag:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stock-info {
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: var(--text-color-3);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { copyToClipboard, getImageUploadModel } from '@/Utils'
|
import { copyToClipboard, getImageUploadModel } from '@/Utils'
|
||||||
import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
|
import { DisableFunction, EnableFunction, useAccount } from '@/api/account'
|
||||||
import { FunctionTypes, GoodsStatus, GoodsTypes, PointGoodsModel, ResponsePointGoodModel } from '@/api/api-models'
|
import { FunctionTypes, GoodsStatus, GoodsTypes, UploadPointGoodsModel, ResponsePointGoodModel, KeySelectionMode } from '@/api/api-models'
|
||||||
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
import { QueryGetAPI, QueryPostAPI } from '@/api/query'
|
||||||
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
|
import EventFetcherStatusCard from '@/components/EventFetcherStatusCard.vue'
|
||||||
import PointGoodsItem from '@/components/manage/PointGoodsItem.vue'
|
import PointGoodsItem from '@/components/manage/PointGoodsItem.vue'
|
||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
UploadFileInfo,
|
UploadFileInfo,
|
||||||
useDialog,
|
useDialog,
|
||||||
useMessage,
|
useMessage,
|
||||||
|
NDynamicTags,
|
||||||
} from 'naive-ui'
|
} from 'naive-ui'
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import PointOrderManage from './PointOrderManage.vue'
|
import PointOrderManage from './PointOrderManage.vue'
|
||||||
@@ -76,13 +77,21 @@ const defaultGoodsModel = {
|
|||||||
status: GoodsStatus.Normal,
|
status: GoodsStatus.Normal,
|
||||||
maxBuyCount: 1,
|
maxBuyCount: 1,
|
||||||
isAllowRebuy: false,
|
isAllowRebuy: false,
|
||||||
|
isPinned: false,
|
||||||
setting: {
|
setting: {
|
||||||
allowGuardLevel: 0
|
allowGuardLevel: 0
|
||||||
},
|
},
|
||||||
} as PointGoodsModel,
|
virtualKeys: [],
|
||||||
|
keySelectionMode: KeySelectionMode.None,
|
||||||
|
currentKeyIndex: 0,
|
||||||
|
name: '',
|
||||||
|
price: 0,
|
||||||
|
tags: [],
|
||||||
|
description: ''
|
||||||
|
} as UploadPointGoodsModel,
|
||||||
fileList: [],
|
fileList: [],
|
||||||
} as { goods: PointGoodsModel; fileList: UploadFileInfo[] }
|
} as { goods: UploadPointGoodsModel; fileList: UploadFileInfo[] }
|
||||||
const currentGoodsModel = ref<{ goods: PointGoodsModel; fileList: UploadFileInfo[] }>(
|
const currentGoodsModel = ref<{ goods: UploadPointGoodsModel; fileList: UploadFileInfo[] }>(
|
||||||
JSON.parse(JSON.stringify(defaultGoodsModel))
|
JSON.parse(JSON.stringify(defaultGoodsModel))
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -478,13 +487,6 @@ onMounted(() => { })
|
|||||||
>
|
>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<NFlex
|
<NFlex
|
||||||
vertical
|
|
||||||
:gap="8"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<span>价格: {{ item.price }}</span>
|
|
||||||
<NFlex
|
|
||||||
justify="space-between"
|
|
||||||
:gap="8"
|
:gap="8"
|
||||||
>
|
>
|
||||||
<NButton
|
<NButton
|
||||||
@@ -509,7 +511,6 @@ onMounted(() => { })
|
|||||||
删除
|
删除
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NFlex>
|
|
||||||
</template>
|
</template>
|
||||||
</PointGoodsItem>
|
</PointGoodsItem>
|
||||||
</NGridItem>
|
</NGridItem>
|
||||||
@@ -629,7 +630,11 @@ onMounted(() => { })
|
|||||||
确定要重置此页面内容?
|
确定要重置此页面内容?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</template>
|
</template>
|
||||||
<NScrollbar style="max-height: 80vh">
|
<div class="scrollable-container">
|
||||||
|
<NScrollbar
|
||||||
|
style="max-height: 70vh; padding-right: 12px;"
|
||||||
|
class="goods-scrollbar"
|
||||||
|
>
|
||||||
<NForm
|
<NForm
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="currentGoodsModel"
|
:model="currentGoodsModel"
|
||||||
@@ -643,11 +648,6 @@ onMounted(() => { })
|
|||||||
>
|
>
|
||||||
基本信息
|
基本信息
|
||||||
</NDivider>
|
</NDivider>
|
||||||
<NFlex
|
|
||||||
vertical
|
|
||||||
:gap="12"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<NFormItem
|
<NFormItem
|
||||||
path="goods.name"
|
path="goods.name"
|
||||||
label="名称"
|
label="名称"
|
||||||
@@ -693,20 +693,23 @@ onMounted(() => { })
|
|||||||
/>
|
/>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</NFlex>
|
|
||||||
|
<NFormItem
|
||||||
|
path="goods.isPinned"
|
||||||
|
label="置顶显示"
|
||||||
|
>
|
||||||
|
<NCheckbox v-model:checked="currentGoodsModel.goods.isPinned">
|
||||||
|
在礼物列表中置顶显示
|
||||||
|
</NCheckbox>
|
||||||
|
</NFormItem>
|
||||||
|
|
||||||
<!-- 详细描述分组 -->
|
<!-- 详细描述分组 -->
|
||||||
<NDivider
|
<NDivider
|
||||||
title-placement="left"
|
title-placement="left"
|
||||||
style="margin: 16px 0"
|
style="margin-top: 0;"
|
||||||
>
|
>
|
||||||
详细描述
|
详细描述
|
||||||
</NDivider>
|
</NDivider>
|
||||||
<NFlex
|
|
||||||
vertical
|
|
||||||
:gap="12"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<NFormItem
|
<NFormItem
|
||||||
path="goods.description"
|
path="goods.description"
|
||||||
label="描述"
|
label="描述"
|
||||||
@@ -737,6 +740,7 @@ onMounted(() => { })
|
|||||||
<NFormItem
|
<NFormItem
|
||||||
path="goods.cover"
|
path="goods.cover"
|
||||||
label="封面"
|
label="封面"
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
>
|
>
|
||||||
<NFlex
|
<NFlex
|
||||||
vertical
|
vertical
|
||||||
@@ -766,7 +770,6 @@ onMounted(() => { })
|
|||||||
</NUpload>
|
</NUpload>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</NFlex>
|
|
||||||
|
|
||||||
<!-- 兑换规则分组 -->
|
<!-- 兑换规则分组 -->
|
||||||
<NDivider
|
<NDivider
|
||||||
@@ -775,11 +778,6 @@ onMounted(() => { })
|
|||||||
>
|
>
|
||||||
兑换规则
|
兑换规则
|
||||||
</NDivider>
|
</NDivider>
|
||||||
<NFlex
|
|
||||||
vertical
|
|
||||||
:gap="12"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<NFormItem
|
<NFormItem
|
||||||
path="goods.type"
|
path="goods.type"
|
||||||
label="礼物类型"
|
label="礼物类型"
|
||||||
@@ -806,6 +804,7 @@ onMounted(() => { })
|
|||||||
<NFormItem
|
<NFormItem
|
||||||
path="goods.guardFree"
|
path="goods.guardFree"
|
||||||
label="特殊权限"
|
label="特殊权限"
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
>
|
>
|
||||||
<NFlex
|
<NFlex
|
||||||
vertical
|
vertical
|
||||||
@@ -891,7 +890,6 @@ onMounted(() => { })
|
|||||||
</NRadioGroup>
|
</NRadioGroup>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</NFlex>
|
|
||||||
|
|
||||||
<!-- 礼物类型特定配置 -->
|
<!-- 礼物类型特定配置 -->
|
||||||
<template v-if="currentGoodsModel.goods.type == GoodsTypes.Physical">
|
<template v-if="currentGoodsModel.goods.type == GoodsTypes.Physical">
|
||||||
@@ -901,11 +899,6 @@ onMounted(() => { })
|
|||||||
>
|
>
|
||||||
实物礼物配置
|
实物礼物配置
|
||||||
</NDivider>
|
</NDivider>
|
||||||
<NFlex
|
|
||||||
vertical
|
|
||||||
:gap="12"
|
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
|
||||||
<NFormItem
|
<NFormItem
|
||||||
path="goods.maxBuyCount"
|
path="goods.maxBuyCount"
|
||||||
label="最大兑换数量"
|
label="最大兑换数量"
|
||||||
@@ -920,6 +913,7 @@ onMounted(() => { })
|
|||||||
<NFormItem
|
<NFormItem
|
||||||
path="address"
|
path="address"
|
||||||
label="收货地址"
|
label="收货地址"
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
>
|
>
|
||||||
<NFlex
|
<NFlex
|
||||||
vertical
|
vertical
|
||||||
@@ -977,7 +971,6 @@ onMounted(() => { })
|
|||||||
</NCheckbox>
|
</NCheckbox>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</template>
|
</template>
|
||||||
</NFlex>
|
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<NDivider
|
<NDivider
|
||||||
@@ -986,14 +979,59 @@ onMounted(() => { })
|
|||||||
>
|
>
|
||||||
虚拟礼物配置
|
虚拟礼物配置
|
||||||
</NDivider>
|
</NDivider>
|
||||||
|
<NFormItem
|
||||||
|
path="goods.keySelectionMode"
|
||||||
|
label="密钥选择模式"
|
||||||
|
>
|
||||||
|
<NRadioGroup v-model:value="currentGoodsModel.goods.keySelectionMode">
|
||||||
|
<NRadioButton :value="KeySelectionMode.None">
|
||||||
|
不使用
|
||||||
|
</NRadioButton>
|
||||||
|
<NRadioButton :value="KeySelectionMode.Random">
|
||||||
|
随机选择
|
||||||
|
</NRadioButton>
|
||||||
|
<NRadioButton :value="KeySelectionMode.Sequential">
|
||||||
|
顺序选择
|
||||||
|
</NRadioButton>
|
||||||
|
</NRadioGroup>
|
||||||
|
</NFormItem>
|
||||||
|
<!-- 添加多Key支持配置 -->
|
||||||
|
<NFormItem
|
||||||
|
v-if="currentGoodsModel.goods.keySelectionMode != KeySelectionMode.None"
|
||||||
|
path="goods.virtualKeys"
|
||||||
|
label="礼物密钥列表 (可选)"
|
||||||
|
>
|
||||||
|
<template #label>
|
||||||
|
礼物密钥列表
|
||||||
|
<NTooltip>
|
||||||
|
<template #trigger>
|
||||||
|
<NIcon :component="Info24Filled" />
|
||||||
|
</template>
|
||||||
|
添加多个密钥,用户购买时会根据选择模式分配一个密钥. 可以留空
|
||||||
|
</NTooltip>
|
||||||
|
</template>
|
||||||
<NFlex
|
<NFlex
|
||||||
vertical
|
vertical
|
||||||
:gap="12"
|
:gap="8"
|
||||||
style="margin-bottom: 16px"
|
|
||||||
>
|
>
|
||||||
|
<NDynamicTags
|
||||||
|
v-model:value="currentGoodsModel.goods.virtualKeys"
|
||||||
|
placeholder="输入密钥后按Enter添加"
|
||||||
|
/>
|
||||||
|
<NText
|
||||||
|
depth="3"
|
||||||
|
style="margin-top: 4px; display: block"
|
||||||
|
>
|
||||||
|
已添加 {{ (currentGoodsModel.goods.virtualKeys || []).length }} 个密钥
|
||||||
|
</NText>
|
||||||
|
</NFlex>
|
||||||
|
</NFormItem>
|
||||||
|
|
||||||
|
|
||||||
<NFormItem
|
<NFormItem
|
||||||
path="goods.content"
|
path="goods.content"
|
||||||
required
|
required
|
||||||
|
style="margin-bottom: 16px;"
|
||||||
>
|
>
|
||||||
<template #label>
|
<template #label>
|
||||||
礼物内容
|
礼物内容
|
||||||
@@ -1001,24 +1039,29 @@ onMounted(() => { })
|
|||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NIcon :component="Info24Filled" />
|
<NIcon :component="Info24Filled" />
|
||||||
</template>
|
</template>
|
||||||
虚拟礼物的具体内容, 网盘链接什么之类的
|
虚拟礼物的具体内容,可使用 {key} 作为占位符
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
</template>
|
</template>
|
||||||
<NInput
|
<NInput
|
||||||
v-model:value="currentGoodsModel.goods.content"
|
v-model:value="currentGoodsModel.goods.content"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
placeholder="写这里咯"
|
placeholder="写这里咯,可使用 {key} 作为占位符,购买时会自动替换为上面密钥列表中的一个"
|
||||||
maxlength="10000"
|
maxlength="10000"
|
||||||
show-count
|
show-count
|
||||||
clearable
|
clearable
|
||||||
/>
|
/>
|
||||||
</NFormItem>
|
</NFormItem>
|
||||||
</NFlex>
|
|
||||||
</template>
|
</template>
|
||||||
|
</NForm>
|
||||||
|
<!-- 添加一个底部间距,让滚动更自然 -->
|
||||||
|
<div style="height: 16px;" />
|
||||||
|
</NScrollbar>
|
||||||
|
<div class="scroll-shadow-top" />
|
||||||
|
<div class="scroll-shadow-bottom" />
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
<NFlex
|
<NFlex
|
||||||
justify="center"
|
justify="center"
|
||||||
style="margin-top: 24px"
|
|
||||||
>
|
>
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
@@ -1029,8 +1072,7 @@ onMounted(() => { })
|
|||||||
{{ currentGoodsModel.goods.id ? '修改' : '创建' }}
|
{{ currentGoodsModel.goods.id ? '修改' : '创建' }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</NForm>
|
</template>
|
||||||
</NScrollbar>
|
|
||||||
</NModal>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -1059,6 +1101,60 @@ onMounted(() => { })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.goods-modal :deep(.n-card-content) {
|
.goods-modal :deep(.n-card-content) {
|
||||||
padding: 0 20px 20px;
|
padding: 0 20px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-modal :deep(.n-card-footer) {
|
||||||
|
padding: 12px 20px 16px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
background-color: var(--action-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-container {
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--body-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-scrollbar {
|
||||||
|
padding: 12px 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--card-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-scrollbar :deep(.n-scrollbar-rail) {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-scrollbar :deep(.n-scrollbar-content) {
|
||||||
|
padding-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-shadow-top {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.06), transparent);
|
||||||
|
z-index: 1;
|
||||||
|
border-top-left-radius: 6px;
|
||||||
|
border-top-right-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroll-shadow-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(to top, rgba(0, 0, 0, 0.06), transparent);
|
||||||
|
z-index: 1;
|
||||||
|
border-bottom-left-radius: 6px;
|
||||||
|
border-bottom-right-radius: 6px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import {
|
|||||||
NCheckboxGroup,
|
NCheckboxGroup,
|
||||||
NDivider,
|
NDivider,
|
||||||
NFlex,
|
NFlex,
|
||||||
|
NForm,
|
||||||
|
NFormItem,
|
||||||
NIcon,
|
NIcon,
|
||||||
NInput,
|
NInput,
|
||||||
NInputGroup,
|
NInputGroup,
|
||||||
@@ -61,7 +63,12 @@ const setting = computed({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 添加礼物表单模型
|
// 添加礼物表单模型
|
||||||
const addGiftModel = ref<{ name: string; point: number }>({ name: '', point: 1 })
|
const addGiftModel = ref<{ name: string; point: number; nameError: string; pointError: string }>({
|
||||||
|
name: '',
|
||||||
|
point: 1,
|
||||||
|
nameError: '',
|
||||||
|
pointError: ''
|
||||||
|
})
|
||||||
|
|
||||||
// 是否可以编辑设置
|
// 是否可以编辑设置
|
||||||
const canEdit = computed(() => {
|
const canEdit = computed(() => {
|
||||||
@@ -101,25 +108,60 @@ async function updateSettings() {
|
|||||||
|
|
||||||
// 添加礼物积分规则
|
// 添加礼物积分规则
|
||||||
async function addGift() {
|
async function addGift() {
|
||||||
|
// 重置错误信息
|
||||||
|
addGiftModel.value.nameError = ''
|
||||||
|
addGiftModel.value.pointError = ''
|
||||||
|
|
||||||
// 表单验证
|
// 表单验证
|
||||||
if (!addGiftModel.value.name) {
|
let hasError = false
|
||||||
message.error('请输入礼物名称')
|
|
||||||
return
|
if (!addGiftModel.value.name.trim()) {
|
||||||
|
addGiftModel.value.nameError = '请输入礼物名称'
|
||||||
|
hasError = true
|
||||||
|
} else if (setting.value.giftPercentMap[addGiftModel.value.name] !== undefined) {
|
||||||
|
addGiftModel.value.nameError = '此礼物名称已存在'
|
||||||
|
hasError = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addGiftModel.value.point > 2147483647) {
|
if (!addGiftModel.value.point) {
|
||||||
message.error('积分不能超过2147483647')
|
addGiftModel.value.pointError = '请输入积分数量'
|
||||||
|
hasError = true
|
||||||
|
} else if (addGiftModel.value.point <= 0) {
|
||||||
|
addGiftModel.value.pointError = '积分必须大于0'
|
||||||
|
hasError = true
|
||||||
|
} else if (addGiftModel.value.point > 2147483647) {
|
||||||
|
addGiftModel.value.pointError = '积分不能超过2147483647'
|
||||||
|
hasError = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasError) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加礼物积分规则
|
// 添加礼物积分规则
|
||||||
setting.value.giftPercentMap[addGiftModel.value.name] = addGiftModel.value.point
|
setting.value.giftPercentMap[addGiftModel.value.name] = addGiftModel.value.point
|
||||||
const success = await updateGift()
|
isLoading.value = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await updateGift()
|
||||||
// 添加成功后清空表单
|
// 添加成功后清空表单
|
||||||
if (success) {
|
if (success) {
|
||||||
addGiftModel.value = { name: '', point: 1 }
|
addGiftModel.value = { name: '', point: 1, nameError: '', pointError: '' }
|
||||||
showAddGiftModal.value = false
|
showAddGiftModal.value = false
|
||||||
|
message.success('礼物添加成功')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加礼物失败:', error)
|
||||||
|
message.error('添加礼物失败,请重试')
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理键盘按下事件
|
||||||
|
function handleKeyDown(event: KeyboardEvent) {
|
||||||
|
if (event.key === 'Enter' && !isLoading.value) {
|
||||||
|
addGift()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -401,6 +443,11 @@ async function SaveComboSetting() {
|
|||||||
vertical
|
vertical
|
||||||
:gap="12"
|
:gap="12"
|
||||||
>
|
>
|
||||||
|
<NFlex
|
||||||
|
justify="space-between"
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
<span class="section-title">自定义礼物列表</span>
|
||||||
<NButton
|
<NButton
|
||||||
type="primary"
|
type="primary"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
@@ -409,6 +456,7 @@ async function SaveComboSetting() {
|
|||||||
>
|
>
|
||||||
添加礼物
|
添加礼物
|
||||||
</NButton>
|
</NButton>
|
||||||
|
</NFlex>
|
||||||
|
|
||||||
<NList bordered>
|
<NList bordered>
|
||||||
<NEmpty
|
<NEmpty
|
||||||
@@ -419,6 +467,11 @@ async function SaveComboSetting() {
|
|||||||
<NListItem
|
<NListItem
|
||||||
v-for="item in Object.entries(setting.giftPercentMap)"
|
v-for="item in Object.entries(setting.giftPercentMap)"
|
||||||
:key="item[0]"
|
:key="item[0]"
|
||||||
|
>
|
||||||
|
<NFlex
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<NFlex
|
<NFlex
|
||||||
align="center"
|
align="center"
|
||||||
@@ -426,44 +479,53 @@ async function SaveComboSetting() {
|
|||||||
>
|
>
|
||||||
<NTag
|
<NTag
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
size="small"
|
size="medium"
|
||||||
type="success"
|
type="success"
|
||||||
>
|
>
|
||||||
{{ item[0] }}
|
{{ item[0] }}
|
||||||
</NTag>
|
</NTag>
|
||||||
|
</NFlex>
|
||||||
|
|
||||||
|
<NFlex
|
||||||
|
align="center"
|
||||||
|
:gap="12"
|
||||||
|
>
|
||||||
<NInputGroup
|
<NInputGroup
|
||||||
style="width: 200px"
|
style="width: 180px"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
>
|
>
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
:value="setting.giftPercentMap[item[0]]"
|
:value="setting.giftPercentMap[item[0]]"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
min="0"
|
min="0"
|
||||||
@update:value="(v) => (setting.giftPercentMap[item[0]] = v ?? 0)"
|
@update:value="(v) => (setting.giftPercentMap[item[0]] = v ? v : 0)"
|
||||||
/>
|
/>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
type="info"
|
||||||
|
size="small"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
@click="updateSettings"
|
@click="updateSettings"
|
||||||
>
|
>
|
||||||
确定
|
更新
|
||||||
</NButton>
|
</NButton>
|
||||||
</NInputGroup>
|
</NInputGroup>
|
||||||
<NPopconfirm @positive-click="deleteGift(item[0])">
|
<NPopconfirm @positive-click="deleteGift(item[0])">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<NButton
|
<NButton
|
||||||
type="error"
|
type="error"
|
||||||
text
|
size="small"
|
||||||
:disabled="!canEdit"
|
:disabled="!canEdit"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<NIcon :component="Delete24Regular" />
|
<NIcon :component="Delete24Regular" />
|
||||||
</template>
|
</template>
|
||||||
|
删除
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
确定要删除这个礼物吗?
|
确定要删除这个礼物吗?
|
||||||
</NPopconfirm>
|
</NPopconfirm>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
</NFlex>
|
||||||
</NListItem>
|
</NListItem>
|
||||||
</NList>
|
</NList>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -475,45 +537,73 @@ async function SaveComboSetting() {
|
|||||||
v-model:show="showAddGiftModal"
|
v-model:show="showAddGiftModal"
|
||||||
preset="card"
|
preset="card"
|
||||||
title="添加礼物"
|
title="添加礼物"
|
||||||
style="max-width: 400px"
|
style="max-width: 480px"
|
||||||
>
|
:mask-closable="false"
|
||||||
<NFlex
|
|
||||||
align="center"
|
|
||||||
vertical
|
|
||||||
:gap="12"
|
|
||||||
>
|
>
|
||||||
|
<NForm>
|
||||||
<NAlert
|
<NAlert
|
||||||
title="注意"
|
title="注意"
|
||||||
type="warning"
|
type="warning"
|
||||||
|
closable
|
||||||
|
style="margin-bottom: 16px"
|
||||||
>
|
>
|
||||||
这里填写的积分是指这个礼物直接对应多少积分, 而不是兑换比例
|
<template #icon>
|
||||||
|
<NIcon :component="Info24Filled" />
|
||||||
|
</template>
|
||||||
|
这里填写的积分是指这个礼物直接对应多少积分,而不是兑换比例
|
||||||
</NAlert>
|
</NAlert>
|
||||||
|
|
||||||
<NInputGroup class="modal-input">
|
<NFormItem
|
||||||
<NInputGroupLabel> 礼物名称 </NInputGroupLabel>
|
label="礼物名称"
|
||||||
|
:validation-status="addGiftModel.nameError ? 'error' : undefined"
|
||||||
|
:feedback="addGiftModel.nameError"
|
||||||
|
required
|
||||||
|
>
|
||||||
<NInput
|
<NInput
|
||||||
v-model:value="addGiftModel.name"
|
v-model:value="addGiftModel.name"
|
||||||
placeholder="礼物名称"
|
placeholder="请输入礼物名称"
|
||||||
|
clearable
|
||||||
|
autofocus
|
||||||
|
@keydown="handleKeyDown"
|
||||||
/>
|
/>
|
||||||
</NInputGroup>
|
</NFormItem>
|
||||||
|
|
||||||
<NInputGroup class="modal-input">
|
<NFormItem
|
||||||
<NInputGroupLabel> 给予积分 </NInputGroupLabel>
|
label="给予积分"
|
||||||
|
:validation-status="addGiftModel.pointError ? 'error' : undefined"
|
||||||
|
:feedback="addGiftModel.pointError"
|
||||||
|
required
|
||||||
|
>
|
||||||
<NInputNumber
|
<NInputNumber
|
||||||
v-model:value="addGiftModel.point"
|
v-model:value="addGiftModel.point"
|
||||||
placeholder="积分数量"
|
placeholder="请输入积分数量"
|
||||||
min="0"
|
min="1"
|
||||||
|
clearable
|
||||||
|
style="width: 100%"
|
||||||
|
@keydown="handleKeyDown"
|
||||||
/>
|
/>
|
||||||
</NInputGroup>
|
</NFormItem>
|
||||||
|
|
||||||
|
<NFlex
|
||||||
|
justify="end"
|
||||||
|
:gap="12"
|
||||||
|
style="margin-top: 24px"
|
||||||
|
>
|
||||||
<NButton
|
<NButton
|
||||||
type="info"
|
@click="showAddGiftModal = false"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</NButton>
|
||||||
|
<NButton
|
||||||
|
type="primary"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
|
:disabled="!addGiftModel.name || !addGiftModel.point || addGiftModel.point <= 0"
|
||||||
@click="addGift"
|
@click="addGift"
|
||||||
>
|
>
|
||||||
确定
|
确定
|
||||||
</NButton>
|
</NButton>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
|
</NForm>
|
||||||
</NModal>
|
</NModal>
|
||||||
</template>
|
</template>
|
||||||
</NFlex>
|
</NFlex>
|
||||||
@@ -539,16 +629,31 @@ async function SaveComboSetting() {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.gift-card {
|
.gift-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-gift-button {
|
.add-gift-button {
|
||||||
max-width: 200px;
|
max-width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-input {
|
.modal-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-text {
|
||||||
|
color: var(--error-color, #d03050);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: -6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 响应式布局优化 */
|
/* 响应式布局优化 */
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const selectedAddress = ref<AddressInfo>() // 选中的地址
|
|||||||
const selectedTag = ref<string>() // 选中的标签
|
const selectedTag = ref<string>() // 选中的标签
|
||||||
const onlyCanBuy = ref(false) // 只显示可兑换
|
const onlyCanBuy = ref(false) // 只显示可兑换
|
||||||
const ignoreGuard = ref(false) // 忽略舰长限制
|
const ignoreGuard = ref(false) // 忽略舰长限制
|
||||||
const priceOrder = ref<'asc' | 'desc' | null>(null) // 价格排序
|
const sortOrder = ref<string | null>(null) // 排序方式
|
||||||
const searchKeyword = ref('') // 搜索关键词
|
const searchKeyword = ref('') // 搜索关键词
|
||||||
|
|
||||||
// --- 计算属性 ---
|
// --- 计算属性 ---
|
||||||
@@ -116,15 +116,52 @@ const selectedItems = computed(() => {
|
|||||||
(item.description && item.description.toLowerCase().includes(searchKeyword.value.toLowerCase())),
|
(item.description && item.description.toLowerCase().includes(searchKeyword.value.toLowerCase())),
|
||||||
)
|
)
|
||||||
|
|
||||||
// 价格排序
|
// 应用排序方式
|
||||||
if (priceOrder.value) {
|
if (sortOrder.value) {
|
||||||
|
switch (sortOrder.value) {
|
||||||
|
case 'price_asc':
|
||||||
|
filteredItems = filteredItems.sort((a, b) => a.price - b.price)
|
||||||
|
break
|
||||||
|
case 'price_desc':
|
||||||
|
filteredItems = filteredItems.sort((a, b) => b.price - a.price)
|
||||||
|
break
|
||||||
|
case 'name_asc':
|
||||||
|
filteredItems = filteredItems.sort((a, b) => a.name.localeCompare(b.name))
|
||||||
|
break
|
||||||
|
case 'name_desc':
|
||||||
|
filteredItems = filteredItems.sort((a, b) => b.name.localeCompare(a.name))
|
||||||
|
break
|
||||||
|
case 'type':
|
||||||
|
filteredItems = filteredItems.sort((a, b) => a.type - b.type)
|
||||||
|
break
|
||||||
|
case 'popular':
|
||||||
|
// 按照热门程度排序(置顶的排在前面)
|
||||||
filteredItems = filteredItems.sort((a, b) => {
|
filteredItems = filteredItems.sort((a, b) => {
|
||||||
return priceOrder.value === 'asc' ? a.price - b.price : b.price - a.price
|
if (a.isPinned && !b.isPinned) return -1
|
||||||
|
if (!a.isPinned && b.isPinned) return 1
|
||||||
|
return 0
|
||||||
})
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredItems
|
// 无论是否有其他排序,置顶礼物始终排在前面
|
||||||
|
return filteredItems.sort((a, b) => {
|
||||||
|
// 先按置顶状态排序
|
||||||
|
if (a.isPinned && !b.isPinned) return -1;
|
||||||
|
if (!a.isPinned && b.isPinned) return 1;
|
||||||
|
// 如果已有排序方式,则不再进行额外排序
|
||||||
|
if (sortOrder.value) return 0;
|
||||||
|
// 默认排序逻辑
|
||||||
|
return 0;
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 获取商品标签颜色
|
||||||
|
function getTagColor(index: number): 'default' | 'info' | 'success' | 'warning' | 'error' | 'primary' {
|
||||||
|
const colors: Array<'default' | 'info' | 'success' | 'warning' | 'error' | 'primary'> = ['default', 'info', 'success', 'warning', 'error'];
|
||||||
|
return colors[index % colors.length];
|
||||||
|
}
|
||||||
|
|
||||||
// --- 方法 ---
|
// --- 方法 ---
|
||||||
|
|
||||||
@@ -297,6 +334,15 @@ function gotoAuthPage() {
|
|||||||
NavigateToNewTab('/bili-user')
|
NavigateToNewTab('/bili-user')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清空筛选条件
|
||||||
|
function clearFilters() {
|
||||||
|
selectedTag.value = undefined
|
||||||
|
searchKeyword.value = ''
|
||||||
|
onlyCanBuy.value = false
|
||||||
|
ignoreGuard.value = false
|
||||||
|
sortOrder.value = null
|
||||||
|
}
|
||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
isLoading.value = true // 开始加载
|
isLoading.value = true // 开始加载
|
||||||
@@ -467,13 +513,17 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
忽略舰长限制
|
忽略舰长限制
|
||||||
</NCheckbox>
|
</NCheckbox>
|
||||||
<!-- 价格排序 -->
|
<!-- 排序方式 -->
|
||||||
<NSelect
|
<NSelect
|
||||||
v-model:value="priceOrder"
|
v-model:value="sortOrder"
|
||||||
:options="[
|
:options="[
|
||||||
{ label: '默认排序', value: 'null' },
|
{ label: '默认排序', value: 'null' },
|
||||||
{ label: '价格 ↑', value: 'asc' },
|
{ label: '价格 ↑', value: 'price_asc' },
|
||||||
{ label: '价格 ↓', value: 'desc' }
|
{ label: '价格 ↓', value: 'price_desc' },
|
||||||
|
{ label: '名称 ↑', value: 'name_asc' },
|
||||||
|
{ label: '名称 ↓', value: 'name_desc' },
|
||||||
|
{ label: '类型', value: 'type' },
|
||||||
|
{ label: '置顶', value: 'popular' }
|
||||||
]"
|
]"
|
||||||
placeholder="排序方式"
|
placeholder="排序方式"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -498,9 +548,9 @@ onMounted(async () => {
|
|||||||
>
|
>
|
||||||
<template #extra>
|
<template #extra>
|
||||||
<NButton
|
<NButton
|
||||||
v-if="goods.length > 0 && (selectedTag || searchKeyword || onlyCanBuy || ignoreGuard || priceOrder)"
|
v-if="goods.length > 0 && (selectedTag || searchKeyword || onlyCanBuy || ignoreGuard || sortOrder)"
|
||||||
size="small"
|
size="small"
|
||||||
@click="() => { selectedTag = undefined; searchKeyword = ''; onlyCanBuy = false; ignoreGuard = false; priceOrder = null; }"
|
@click="clearFilters"
|
||||||
>
|
>
|
||||||
清空筛选条件
|
清空筛选条件
|
||||||
</NButton>
|
</NButton>
|
||||||
@@ -516,6 +566,7 @@ onMounted(async () => {
|
|||||||
:goods="item"
|
:goods="item"
|
||||||
content-style="max-width: 300px; min-width: 250px; height: 380px;"
|
content-style="max-width: 300px; min-width: 250px; height: 380px;"
|
||||||
class="goods-item"
|
class="goods-item"
|
||||||
|
:class="{ 'pinned-item': item.isPinned }"
|
||||||
>
|
>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<NFlex
|
<NFlex
|
||||||
@@ -532,28 +583,11 @@ onMounted(async () => {
|
|||||||
class="exchange-btn"
|
class="exchange-btn"
|
||||||
@click="onBuyClick(item)"
|
@click="onBuyClick(item)"
|
||||||
>
|
>
|
||||||
兑换
|
{{ item.isPinned ? '🔥 兑换' : '兑换' }}
|
||||||
</NButton>
|
</NButton>
|
||||||
</template>
|
</template>
|
||||||
{{ getTooltip(item) }}
|
{{ getTooltip(item) }}
|
||||||
</NTooltip>
|
</NTooltip>
|
||||||
<NFlex
|
|
||||||
align="center"
|
|
||||||
justify="end"
|
|
||||||
class="price-display"
|
|
||||||
>
|
|
||||||
<NTooltip placement="bottom">
|
|
||||||
<template #trigger>
|
|
||||||
<NText
|
|
||||||
class="price-text"
|
|
||||||
:delete="item.canFreeBuy"
|
|
||||||
>
|
|
||||||
🪙 {{ item.price > 0 ? item.price : '免费' }}
|
|
||||||
</NText>
|
|
||||||
</template>
|
|
||||||
{{ item.canFreeBuy ? '你可以免费兑换此礼物' : '所需积分' }}
|
|
||||||
</NTooltip>
|
|
||||||
</NFlex>
|
|
||||||
</NFlex>
|
</NFlex>
|
||||||
</template>
|
</template>
|
||||||
</PointGoodsItem>
|
</PointGoodsItem>
|
||||||
@@ -775,34 +809,146 @@ onMounted(async () => {
|
|||||||
.goods-item {
|
.goods-item {
|
||||||
break-inside: avoid;
|
break-inside: avoid;
|
||||||
background-color: var(--card-color);
|
background-color: var(--card-color);
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.3s ease-in-out;
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-item:hover {
|
.goods-item:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-item {
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
box-shadow: 0 2px 12px rgba(var(--primary-color-rgb), 0.15);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pinned-item::before {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pin-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-content {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-section {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-display {
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-text {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.free-tag {
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-container {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
position: relative;
|
||||||
|
padding: 8px 0;
|
||||||
|
max-height: 120px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-description {
|
||||||
|
line-height: 1.5;
|
||||||
|
font-size: 0.95em;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tags-section {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-tag {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.goods-tag:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
}
|
||||||
|
|
||||||
|
.stock-info {
|
||||||
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.goods-footer {
|
.goods-footer {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
|
border-top: 1px solid var(--border-color-1);
|
||||||
|
background-color: rgba(var(--card-color-rgb), 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
.exchange-btn {
|
.exchange-btn {
|
||||||
min-width: 70px;
|
min-width: 80px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-text {
|
.exchange-btn::after {
|
||||||
font-size: 1.1em;
|
content: '';
|
||||||
font-weight: var(--font-weight-strong);
|
position: absolute;
|
||||||
padding: 0 6px;
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
transparent,
|
||||||
|
rgba(255, 255, 255, 0.2),
|
||||||
|
transparent
|
||||||
|
);
|
||||||
|
transition: all 0.6s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.exchange-btn:not(:disabled):hover::after {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.goods-grid {
|
.goods-grid {
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.price-text {
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user