feat: 支持秒杀活动

This commit is contained in:
richarjiang
2026-04-09 10:24:44 +08:00
parent 23bdd05811
commit 74551085e3
29 changed files with 3521 additions and 760 deletions

View File

@@ -522,16 +522,16 @@ onMounted(fetchCardTypes)
justify-content: space-between;
}
.header--times { background: linear-gradient(90deg, #1a1a2e, #2d2d5e); }
.header--duration { background: linear-gradient(90deg, #6c3483, #9b59b6); }
.header--trial { background: linear-gradient(90deg, #5a7a8a, $primary-dark); }
.header--times { background: linear-gradient(90deg, #E8D5C4, #D4BFA8); }
.header--duration { background: linear-gradient(90deg, #D8C8DC, #C4AECB); }
.header--trial { background: linear-gradient(90deg, #C8D8D2, #A9C4BC); }
.ct-type-label { font-size: 22rpx; font-weight: 600; color: #ffffff; letter-spacing: 2rpx; }
.ct-type-label { font-size: 22rpx; font-weight: 600; color: $brand-color; letter-spacing: 2rpx; }
.ct-status-tag { border-radius: 20rpx; padding: 4rpx 16rpx; }
.tag--on { background: rgba(255,255,255,0.2); }
.tag--off { background: rgba(0,0,0,0.2); }
.ct-status-text { font-size: 20rpx; color: #ffffff; }
.tag--on { background: rgba(74, 64, 53, 0.1); }
.tag--off { background: rgba(74, 64, 53, 0.08); }
.ct-status-text { font-size: 20rpx; color: $brand-color; }
.ct-body { padding: 24rpx; }

View File

@@ -0,0 +1,863 @@
<template>
<view class="page" :style="{ paddingTop: navBarHeight }">
<CustomNavBar title="秒杀管理" show-back />
<!-- Toolbar -->
<view class="toolbar">
<text class="toolbar-hint"> {{ total }} 个秒杀活动</text>
<view class="add-btn" @tap="openAdd">
<text class="add-btn-text"> 新建秒杀</text>
</view>
</view>
<!-- Loading skeleton -->
<view v-if="pageLoading" class="skeleton-list">
<view v-for="i in 3" :key="i" class="skeleton-item" />
</view>
<!-- Empty -->
<view v-else-if="!items.length" class="empty-state">
<text class="empty-icon"></text>
<text class="empty-text">暂无秒杀活动点击右上角新建</text>
</view>
<!-- Flash sale list -->
<view v-else class="fs-list">
<view
v-for="item in items"
:key="item.id"
class="fs-card"
>
<!-- Header band -->
<view class="fs-header" :class="headerStatusClass(item)">
<view class="fs-header-left">
<text class="fs-title">{{ item.title }}</text>
</view>
<view class="fs-status-tag" :class="phaseTagClass(item.phase)">
<text class="fs-status-text">{{ phaseLabel(item.phase) }}</text>
</view>
</view>
<!-- Body -->
<view class="fs-body">
<view class="fs-info-row">
<text class="fs-card-type">关联卡种: {{ item.cardType.name }}</text>
</view>
<view class="fs-price-row">
<view class="fs-price-block">
<text class="fs-price-label">秒杀价</text>
<text class="fs-price-value flash">¥{{ formatPrice(item.flashPrice) }}</text>
</view>
<view class="fs-price-block">
<text class="fs-price-label">原价</text>
<text class="fs-price-value original">¥{{ formatPrice(item.originalPrice) }}</text>
</view>
<view class="fs-price-block">
<text class="fs-price-label">库存</text>
<text class="fs-price-value">{{ item.soldCount }}/{{ item.totalStock }}</text>
</view>
</view>
<!-- Stock progress bar -->
<view class="fs-stock-bar">
<view
class="fs-stock-fill"
:style="{ width: stockPercent(item) }"
/>
</view>
<view class="fs-time-row">
<text class="fs-time">{{ formatDateTime(item.startTime) }} {{ formatDateTime(item.endTime) }}</text>
</view>
</view>
<!-- Actions -->
<view class="fs-actions">
<view class="fs-action-btn edit-btn" @tap.stop="openEdit(item)">
<text class="fs-action-text">编辑</text>
</view>
<view
v-if="item.status === 'DRAFT'"
class="fs-action-btn activate-btn"
@tap.stop="confirmActivate(item)"
>
<text class="fs-action-text">上线</text>
</view>
<view
v-else-if="item.status === 'ACTIVE'"
class="fs-action-btn end-btn"
@tap.stop="confirmEnd(item)"
>
<text class="fs-action-text">结束</text>
</view>
<view
v-if="item.soldCount === 0"
class="fs-action-btn delete-btn"
@tap.stop="confirmDelete(item)"
>
<text class="fs-action-text">删除</text>
</view>
</view>
</view>
</view>
<!-- Add / Edit modal -->
<view v-if="showModal" class="modal-mask" @tap.stop="closeModal">
<view class="modal-container" @tap.stop>
<scroll-view scroll-y class="modal-scroll">
<!-- Header -->
<view class="modal-header">
<text class="modal-title">{{ editTarget ? '编辑秒杀' : '新建秒杀' }}</text>
<view class="modal-close" @tap="closeModal">
<text class="modal-close-icon"></text>
</view>
</view>
<!-- Form fields -->
<view class="modal-body">
<!-- Card type picker -->
<view class="modal-field">
<text class="modal-label">关联卡种</text>
<picker
mode="selector"
:range="cardTypeOptions"
range-key="label"
:value="form.cardTypeIdx"
@change="onCardTypeChange"
:disabled="!!editTarget"
>
<view class="picker-display">
<text class="picker-text">{{ cardTypeOptions[form.cardTypeIdx]?.label || '请选择' }}</text>
<text class="picker-arrow"></text>
</view>
</picker>
</view>
<view class="modal-field">
<text class="modal-label">活动标题</text>
<input
class="modal-input"
v-model="form.title"
placeholder="如:新春限时秒杀"
placeholder-style="color:#bbb"
/>
</view>
<view class="modal-field">
<text class="modal-label">原价</text>
<input
class="modal-input"
type="digit"
v-model="form.originalPriceStr"
placeholder="展示划线价"
placeholder-style="color:#bbb"
/>
</view>
<view class="modal-field">
<text class="modal-label">秒杀价</text>
<input
class="modal-input"
type="digit"
v-model="form.flashPriceStr"
placeholder="实际支付价格"
placeholder-style="color:#bbb"
/>
</view>
<view class="modal-field">
<text class="modal-label">库存数量</text>
<input
class="modal-input"
type="number"
v-model="form.totalStockStr"
placeholder="秒杀总量"
placeholder-style="color:#bbb"
/>
</view>
<view class="modal-field">
<text class="modal-label">开始时间</text>
<view class="datetime-picker-group">
<picker
mode="date"
:value="form.startDate"
@change="onStartDateChange"
>
<text class="datetime-text">{{ form.startDate || '选择日期' }}</text>
</picker>
<picker
mode="time"
:value="form.startTimeStr"
@change="onStartTimeChange"
>
<text class="datetime-text">{{ form.startTimeStr || '选择时间' }}</text>
</picker>
</view>
</view>
<view class="modal-field">
<text class="modal-label">结束时间</text>
<view class="datetime-picker-group">
<picker
mode="date"
:value="form.endDate"
@change="onEndDateChange"
>
<text class="datetime-text">{{ form.endDate || '选择日期' }}</text>
</picker>
<picker
mode="time"
:value="form.endTimeStr"
@change="onEndTimeChange"
>
<text class="datetime-text">{{ form.endTimeStr || '选择时间' }}</text>
</picker>
</view>
</view>
<view class="modal-field">
<text class="modal-label">排序值</text>
<input
class="modal-input"
type="number"
v-model="form.sortOrderStr"
placeholder="越小越靠前"
placeholder-style="color:#bbb"
/>
</view>
<view class="modal-field modal-field--last">
<text class="modal-label">活动说明</text>
<textarea
class="modal-textarea"
v-model="form.description"
placeholder="可选,向用户展示"
placeholder-style="color:#bbb"
:maxlength="500"
auto-height
/>
</view>
</view>
<!-- Action buttons -->
<view class="modal-actions">
<view class="modal-cancel" @tap="closeModal">
<text class="modal-cancel-text">取消</text>
</view>
<view
class="modal-confirm"
:class="{ 'modal-confirm--loading': submitting }"
@tap="submitForm"
>
<text class="modal-confirm-text">{{ submitting ? '保存中...' : '确认保存' }}</text>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import CustomNavBar from '../../components/CustomNavBar.vue'
import { getSystemLayout } from '../../utils/system'
import { useAdminStore } from '../../stores/admin'
import { formatPrice, formatDateTime, getFlashSalePhaseLabel, getStockPercent, formatDateLocal, formatTimeLocal } from '../../utils/format'
import { FlashSaleStatus, FlashSalePhase } from '@mp-pilates/shared'
import type { FlashSaleAdminItem, CardType } from '@mp-pilates/shared'
const adminStore = useAdminStore()
const navBarHeight = ref('64px')
onMounted(() => {
navBarHeight.value = `${getSystemLayout().navBarHeight}px`
})
// ─── Data ────────────────────────────────────────────
const items = ref<FlashSaleAdminItem[]>([])
const total = ref(0)
const pageLoading = ref(false)
const showModal = ref(false)
const submitting = ref(false)
const editTarget = ref<FlashSaleAdminItem | null>(null)
const cardTypes = ref<CardType[]>([])
const cardTypeOptions = computed(() =>
cardTypes.value.map((ct) => ({
label: `${ct.name}(¥${formatPrice(ct.price)}`,
value: ct.id,
})),
)
const defaultForm = () => ({
cardTypeIdx: 0,
title: '',
originalPriceStr: '',
flashPriceStr: '',
totalStockStr: '',
startDate: '',
startTimeStr: '',
endDate: '',
endTimeStr: '',
sortOrderStr: '0',
description: '',
})
const form = ref(defaultForm())
// ─── Data loading ─────────────────────────────────────
async function loadData() {
pageLoading.value = true
try {
const [salesResult, cardTypesResult] = await Promise.all([
adminStore.fetchFlashSales(),
adminStore.fetchCardTypes(),
])
items.value = [...salesResult.data]
total.value = salesResult.total
cardTypes.value = [...cardTypesResult]
} catch {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
pageLoading.value = false
}
}
async function reloadSales() {
try {
const result = await adminStore.fetchFlashSales()
items.value = [...result.data]
total.value = result.total
} catch {
// silent
}
}
// ─── Helpers ──────────────────────────────────────────
function phaseLabel(phase: FlashSalePhase): string {
return getFlashSalePhaseLabel(phase)
}
function phaseTagClass(phase: FlashSalePhase): string {
if (phase === FlashSalePhase.ONGOING) return 'tag--ongoing'
if (phase === FlashSalePhase.UPCOMING) return 'tag--upcoming'
if (phase === FlashSalePhase.SOLD_OUT) return 'tag--soldout'
return 'tag--ended'
}
function headerStatusClass(item: FlashSaleAdminItem): string {
if (item.status === FlashSaleStatus.DRAFT) return 'header--draft'
if (item.status === FlashSaleStatus.ENDED) return 'header--ended'
return 'header--active'
}
function stockPercent(item: FlashSaleAdminItem): string {
return getStockPercent(item.soldCount, item.totalStock)
}
// ─── Modal ────────────────────────────────────────────
function openAdd() {
editTarget.value = null
form.value = defaultForm()
showModal.value = true
}
function openEdit(item: FlashSaleAdminItem) {
editTarget.value = item
const startDt = new Date(item.startTime)
const endDt = new Date(item.endTime)
const ctIdx = cardTypes.value.findIndex((ct) => ct.id === item.cardTypeId)
form.value = {
cardTypeIdx: ctIdx >= 0 ? ctIdx : 0,
title: item.title,
originalPriceStr: String(item.originalPrice / 100),
flashPriceStr: String(item.flashPrice / 100),
totalStockStr: String(item.totalStock),
startDate: formatDateLocal(startDt),
startTimeStr: formatTimeLocal(startDt),
endDate: formatDateLocal(endDt),
endTimeStr: formatTimeLocal(endDt),
sortOrderStr: String(item.sortOrder),
description: item.description ?? '',
}
showModal.value = true
}
function closeModal() {
showModal.value = false
editTarget.value = null
}
function onCardTypeChange(e: { detail: { value: number } }) {
const idx = Number(e.detail.value)
form.value.cardTypeIdx = idx
// Auto-fill original price from card type
const ct = cardTypes.value[idx]
if (ct && !form.value.originalPriceStr) {
form.value.originalPriceStr = String(Number(ct.price) / 100)
}
}
function onStartDateChange(e: { detail: { value: string } }) {
form.value.startDate = e.detail.value
}
function onStartTimeChange(e: { detail: { value: string } }) {
form.value.startTimeStr = e.detail.value
}
function onEndDateChange(e: { detail: { value: string } }) {
form.value.endDate = e.detail.value
}
function onEndTimeChange(e: { detail: { value: string } }) {
form.value.endTimeStr = e.detail.value
}
// ─── Form submit ──────────────────────────────────────
async function submitForm() {
if (submitting.value) return
if (!form.value.title.trim()) {
uni.showToast({ title: '请填写活动标题', icon: 'none' }); return
}
const originalPrice = parseFloat(form.value.originalPriceStr)
if (isNaN(originalPrice) || originalPrice <= 0) {
uni.showToast({ title: '请填写有效原价', icon: 'none' }); return
}
const flashPrice = parseFloat(form.value.flashPriceStr)
if (isNaN(flashPrice) || flashPrice <= 0) {
uni.showToast({ title: '请填写有效秒杀价', icon: 'none' }); return
}
const totalStock = parseInt(form.value.totalStockStr, 10)
if (isNaN(totalStock) || totalStock < 1) {
uni.showToast({ title: '请填写有效库存', icon: 'none' }); return
}
if (!form.value.startDate || !form.value.startTimeStr) {
uni.showToast({ title: '请选择开始时间', icon: 'none' }); return
}
if (!form.value.endDate || !form.value.endTimeStr) {
uni.showToast({ title: '请选择结束时间', icon: 'none' }); return
}
const startTime = `${form.value.startDate}T${form.value.startTimeStr}:00`
const endTime = `${form.value.endDate}T${form.value.endTimeStr}:00`
if (new Date(endTime) <= new Date(startTime)) {
uni.showToast({ title: '结束时间须晚于开始时间', icon: 'none' }); return
}
submitting.value = true
try {
if (editTarget.value) {
await adminStore.updateFlashSale(editTarget.value.id, {
title: form.value.title.trim(),
originalPrice: Math.round(originalPrice * 100),
flashPrice: Math.round(flashPrice * 100),
totalStock,
startTime,
endTime,
description: form.value.description.trim() || undefined,
sortOrder: parseInt(form.value.sortOrderStr, 10) || 0,
})
} else {
const selectedCardType = cardTypes.value[form.value.cardTypeIdx]
if (!selectedCardType) {
uni.showToast({ title: '请选择卡种', icon: 'none' }); return
}
await adminStore.createFlashSale({
cardTypeId: selectedCardType.id,
title: form.value.title.trim(),
originalPrice: Math.round(originalPrice * 100),
flashPrice: Math.round(flashPrice * 100),
totalStock,
startTime,
endTime,
description: form.value.description.trim() || undefined,
sortOrder: parseInt(form.value.sortOrderStr, 10) || 0,
})
}
uni.showToast({ title: '保存成功', icon: 'success' })
closeModal()
await reloadSales()
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : '保存失败'
uni.showToast({ title: msg, icon: 'none' })
} finally {
submitting.value = false
}
}
// ─── Actions ──────────────────────────────────────────
function confirmActivate(item: FlashSaleAdminItem) {
uni.showModal({
title: '确认上线',
content: `上线后「${item.title}」将对用户可见,到达秒杀时间后用户可抢购。`,
confirmText: '上线',
confirmColor: '#27ae60',
success: async (res) => {
if (!res.confirm) return
uni.showLoading({ title: '上线中...' })
try {
await adminStore.updateFlashSale(item.id, { status: FlashSaleStatus.ACTIVE })
uni.hideLoading()
uni.showToast({ title: '已上线', icon: 'success' })
await reloadSales()
} catch {
uni.hideLoading()
uni.showToast({ title: '上线失败', icon: 'none' })
}
},
})
}
function confirmEnd(item: FlashSaleAdminItem) {
uni.showModal({
title: '确认结束',
content: `结束后「${item.title}」将停止售卖,已购买的不受影响。`,
confirmText: '结束',
confirmColor: '#e67e22',
success: async (res) => {
if (!res.confirm) return
uni.showLoading({ title: '结束中...' })
try {
await adminStore.updateFlashSale(item.id, { status: FlashSaleStatus.ENDED })
uni.hideLoading()
uni.showToast({ title: '已结束', icon: 'success' })
await reloadSales()
} catch {
uni.hideLoading()
uni.showToast({ title: '操作失败', icon: 'none' })
}
},
})
}
function confirmDelete(item: FlashSaleAdminItem) {
uni.showModal({
title: '确认删除',
content: `确定删除「${item.title}」?此操作不可恢复。`,
confirmText: '删除',
confirmColor: '#c0392b',
success: async (res) => {
if (!res.confirm) return
uni.showLoading({ title: '删除中...' })
try {
await adminStore.deleteFlashSale(item.id)
uni.hideLoading()
uni.showToast({ title: '已删除', icon: 'success' })
await reloadSales()
} catch {
uni.hideLoading()
uni.showToast({ title: '删除失败', icon: 'none' })
}
},
})
}
// ─── Lifecycle ────────────────────────────────────────
onMounted(loadData)
</script>
<style lang="scss" scoped>
.page {
min-height: 100vh;
background: #f5f3f0;
padding-bottom: 40rpx;
}
/* ── Toolbar ─────────────────────────────── */
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 24rpx 16rpx;
}
.toolbar-hint { font-size: 24rpx; color: #999; }
.add-btn {
background: linear-gradient(135deg, #D4A59A, #C08B7E);
border-radius: 32rpx;
padding: 12rpx 28rpx;
display: flex;
align-items: center;
gap: 8rpx;
}
.add-btn-text { font-size: 26rpx; font-weight: 600; color: #fff; }
/* ── Skeleton ────────────────────────────── */
.skeleton-list { padding: 0 24rpx; }
.skeleton-item {
height: 300rpx;
border-radius: 16rpx;
margin-bottom: 20rpx;
background: linear-gradient(90deg, #f0ece8 25%, #e8e4df 50%, #f0ece8 75%);
background-size: 400% 100%;
animation: shimmer 1.4s infinite;
}
/* ── Empty ───────────────────────────────── */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 100rpx 0;
gap: 20rpx;
}
.empty-icon { font-size: 80rpx; }
.empty-text { font-size: 28rpx; color: #bbb; }
/* ── Flash sale list ─────────────────────── */
.fs-list { padding: 0 24rpx; }
.fs-card {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
}
.fs-header {
padding: 20rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
}
.header--active { background: linear-gradient(90deg, #D4A59A, #C08B7E); }
.header--draft { background: linear-gradient(90deg, #AEA49A, #9E948A); }
.header--ended { background: linear-gradient(90deg, #B0A898, #9A928A); }
.fs-header-left { flex: 1; min-width: 0; }
.fs-title {
font-size: 28rpx;
font-weight: 700;
color: #fff;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fs-status-tag {
border-radius: 20rpx;
padding: 4rpx 16rpx;
flex-shrink: 0;
margin-left: 12rpx;
}
.tag--ongoing { background: rgba(255, 255, 255, 0.3); }
.tag--upcoming { background: rgba(255, 255, 255, 0.2); }
.tag--soldout { background: rgba(0, 0, 0, 0.2); }
.tag--ended { background: rgba(0, 0, 0, 0.3); }
.fs-status-text { font-size: 20rpx; color: #fff; font-weight: 600; }
.fs-body { padding: 24rpx; }
.fs-info-row { margin-bottom: 16rpx; }
.fs-card-type { font-size: 24rpx; color: #888; }
.fs-price-row {
display: flex;
gap: 32rpx;
margin-bottom: 16rpx;
}
.fs-price-block {
display: flex;
flex-direction: column;
gap: 4rpx;
}
.fs-price-label { font-size: 20rpx; color: #aaa; }
.fs-price-value {
font-size: 30rpx;
font-weight: 700;
color: #333;
&.flash { color: #B5725E; }
&.original { color: #aaa; text-decoration: line-through; font-weight: 400; }
}
/* Stock progress bar */
.fs-stock-bar {
height: 8rpx;
background: #f0f0f0;
border-radius: 4rpx;
overflow: hidden;
margin-bottom: 12rpx;
}
.fs-stock-fill {
height: 100%;
background: linear-gradient(90deg, #D4A59A, #C08B7E);
border-radius: 4rpx;
transition: width 0.3s;
}
.fs-time-row { margin-top: 4rpx; }
.fs-time { font-size: 22rpx; color: #999; }
/* ── Actions ─────────────────────────────── */
.fs-actions {
display: flex;
border-top: 1rpx solid #f5f5f5;
}
.fs-action-btn {
flex: 1;
padding: 20rpx 0;
display: flex;
align-items: center;
justify-content: center;
border-right: 1rpx solid #f5f5f5;
&:last-child { border-right: none; }
&:active { background: #f9f9f9; }
}
.fs-action-text { font-size: 26rpx; font-weight: 600; }
.edit-btn .fs-action-text { color: #1a1a2e; }
.activate-btn .fs-action-text { color: #27ae60; }
.end-btn .fs-action-text { color: #e67e22; }
.delete-btn .fs-action-text { color: #c0392b; }
/* ── Modal ───────────────────────────────── */
.modal-mask {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: flex-end;
z-index: 1000;
}
.modal-container {
width: 100%;
max-height: 85vh;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-scroll { flex: 1; max-height: 85vh; }
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 32rpx 16rpx;
position: sticky;
top: 0;
background: #fff;
z-index: 10;
}
.modal-title { font-size: 32rpx; font-weight: 700; color: #1a1a2e; }
.modal-close {
width: 56rpx;
height: 56rpx;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
border-radius: 50%;
}
.modal-close-icon { font-size: 24rpx; color: #999; }
.modal-body { padding: 0 32rpx; }
.modal-field {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #f5f5f5;
gap: 16rpx;
&--last { border-bottom: none; align-items: flex-start; }
}
.modal-label { font-size: 26rpx; color: #555; width: 160rpx; flex-shrink: 0; }
.modal-input { flex: 1; text-align: right; font-size: 26rpx; color: #222; }
.picker-display { display: flex; align-items: center; gap: 8rpx; }
.picker-text { font-size: 26rpx; color: #222; }
.picker-arrow { font-size: 26rpx; color: #bbb; }
.datetime-picker-group {
display: flex;
align-items: center;
gap: 12rpx;
flex: 1;
justify-content: flex-end;
}
.datetime-text {
font-size: 26rpx;
color: #222;
padding: 8rpx 16rpx;
background: #f8f8f8;
border-radius: 8rpx;
}
.modal-textarea {
flex: 1;
font-size: 26rpx;
color: #222;
min-height: 80rpx;
text-align: right;
}
.modal-actions {
display: flex;
gap: 16rpx;
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
background: #fff;
}
.modal-cancel {
flex: 1;
height: 88rpx;
background: #f0f0f0;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
&:active { background: #e8e8e8; }
}
.modal-cancel-text { font-size: 28rpx; color: #555; }
.modal-confirm {
flex: 2;
height: 88rpx;
background: linear-gradient(90deg, #D4A59A, #C08B7E);
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
&:active { opacity: 0.85; }
&--loading { opacity: 0.6; pointer-events: none; }
}
.modal-confirm-text { font-size: 28rpx; font-weight: 700; color: #fff; }
</style>

View File

@@ -131,6 +131,21 @@
<text class="arrow-text"></text>
</view>
</view>
<view class="list-item" @tap="navigate('/pages/admin/flash-sales')">
<view class="item-left">
<view class="item-icon-wrap icon--flash-sale">
<text class="item-icon-text"></text>
</view>
<view class="item-text-group">
<text class="item-title">秒杀管理</text>
<text class="item-desc">创建和管理限时秒杀活动</text>
</view>
</view>
<view class="item-arrow">
<text class="arrow-text"></text>
</view>
</view>
</view>
<!-- Section header: 系统 -->
@@ -330,6 +345,7 @@ onMounted(() => {
.icon--members { background: linear-gradient(135deg, $primary-color, $primary-dark); }
.icon--orders { background: linear-gradient(135deg, #7E9EC4, #6E8EB4); }
.icon--card { background: linear-gradient(135deg, #C48E7E, #B47E6E); }
.icon--flash-sale { background: linear-gradient(135deg, #D4A59A, #C08B7E); }
.icon--studio { background: linear-gradient(135deg, #9E9E7E, #8E8E6E); }
.item-text-group {