Files
medical-mall/pages/mall/admin/product-management.uvue

1155 lines
25 KiB
Plaintext

<!-- 商品管理页面 - 基于CRMEB设计 -->
<template>
<view class="product-management">
<!-- 搜索和筛选区域 -->
<view class="search-section">
<view class="search-form">
<view class="search-row">
<view class="form-item">
<text class="label">商品搜索:</text>
<input
v-model="searchForm.keyword"
placeholder="请输入商品名称/关键字/ID"
clearable
class="search-input"
/>
</view>
<view class="form-item">
<text class="label">商品类型:</text>
<picker mode="selector" :range="productTypes" range-key="label" @change="onProductTypeChange">
<view class="picker-text">{{ selectedProductType || '全部' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">商品分类:</text>
<view class="cascader-wrapper">
<picker mode="multiSelector" :range="categoryTree" @change="onCategoryChange">
<view class="picker-text">{{ selectedCategoryText || '请选择分类' }}</view>
</picker>
</view>
</view>
</view>
<!-- 高级搜索选项 -->
<view v-if="showAdvancedSearch" class="search-row advanced">
<view class="form-item">
<text class="label">配送方式:</text>
<picker mode="selector" :range="deliveryTypes" range-key="label" @change="onDeliveryTypeChange">
<view class="picker-text">{{ selectedDeliveryType || '全部' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">商品规格:</text>
<picker mode="selector" :range="specTypes" range-key="label" @change="onSpecTypeChange">
<view class="picker-text">{{ selectedSpecType || '全部' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">会员专属:</text>
<picker mode="selector" :range="vipOptions" range-key="label" @change="onVipChange">
<view class="picker-text">{{ selectedVip || '全部' }}</view>
</picker>
</view>
<view class="form-item">
<text class="label">添加时间:</text>
<view class="date-range">
<picker mode="date" @change="onStartDateChange">
<view class="date-picker">{{ startDate || '开始日期' }}</view>
</picker>
<text class="date-separator">至</text>
<picker mode="date" @change="onEndDateChange">
<view class="date-picker">{{ endDate || '结束日期' }}</view>
</picker>
</view>
</view>
<view class="form-item">
<text class="label">库存范围:</text>
<view class="number-range">
<input
v-model="searchForm.stockMin"
placeholder="最小值"
type="number"
class="number-input"
/>
<text class="range-separator">~</text>
<input
v-model="searchForm.stockMax"
placeholder="最大值"
type="number"
class="number-input"
/>
</view>
</view>
</view>
<view class="form-actions">
<button class="btn btn-primary" @click="handleSearch">搜索</button>
<button class="btn btn-default" @click="handleReset">重置</button>
<text class="toggle-search" @click="showAdvancedSearch = !showAdvancedSearch">
{{ showAdvancedSearch ? '收起' : '展开' }} <text class="icon">{{ showAdvancedSearch ? '▲' : '▼' }}</text>
</text>
</view>
</view>
</view>
<!-- 操作按钮区域 -->
<view class="action-bar">
<view class="action-buttons">
<button class="btn btn-success" @click="addProduct">添加商品</button>
<button class="btn btn-warning" @click="batchOnShelf" :disabled="!selectedProducts.length">批量上架</button>
<button class="btn btn-danger" @click="batchOffShelf" :disabled="!selectedProducts.length">批量下架</button>
<button class="btn btn-info" @click="batchDelete" :disabled="!selectedProducts.length">批量删除</button>
</view>
<view class="data-info">
<text class="total-count">共 {{ totalProducts }} 个商品</text>
<text class="page-info">{{ currentPage }}/{{ totalPages }}</text>
</view>
</view>
<!-- 商品列表 -->
<view class="product-list">
<!-- 表头 -->
<view class="table-header">
<view class="table-row">
<view class="table-cell checkbox-cell">
<checkbox :checked="selectAll" @change="onSelectAllChange" />
</view>
<view class="table-cell product-cell">商品信息</view>
<view class="table-cell">价格</view>
<view class="table-cell">库存</view>
<view class="table-cell">销量</view>
<view class="table-cell">分类</view>
<view class="table-cell">状态</view>
<view class="table-cell">操作</view>
</view>
</view>
<!-- 表格内容 -->
<view class="table-body">
<view v-for="product in productList" :key="product.id" class="table-row data-row">
<view class="table-cell checkbox-cell">
<checkbox :checked="selectedProducts.includes(product.id)" @change="onProductSelectChange(product.id)" />
</view>
<view class="table-cell product-cell">
<view class="product-info">
<image :src="product.image || '/static/default-product.png'" class="product-image" />
<view class="product-details">
<text class="product-name">{{ product.title }}</text>
<text class="product-id">ID: {{ product.id }}</text>
<view class="product-tags">
<text v-if="product.is_hot" class="tag hot">热销</text>
<text v-if="product.is_recommend" class="tag recommend">推荐</text>
<text v-if="product.is_new" class="tag new">新品</text>
</view>
</view>
</view>
</view>
<view class="table-cell">
<view class="price-info">
<text class="price">¥{{ product.price }}</text>
<text v-if="product.ot_price" class="original-price">¥{{ product.ot_price }}</text>
</view>
</view>
<view class="table-cell">
<text class="stock">{{ product.stock }}</text>
</view>
<view class="table-cell">
<text class="sales">{{ product.sales }}</text>
</view>
<view class="table-cell">
<text class="category">{{ getCategoryName(product.category_id) }}</text>
</view>
<view class="table-cell">
<text class="status-tag" :class="product.is_show ? 'active' : 'inactive'">
{{ product.is_show ? '上架' : '下架' }}
</text>
</view>
<view class="table-cell action-cell">
<view class="action-buttons">
<text class="action-link" @click="viewProduct(product.id)">查看</text>
<text class="action-link" @click="editProduct(product.id)">编辑</text>
<text class="action-link" :class="product.is_show ? 'danger' : 'success'"
@click="toggleProductStatus(product.id, product.is_show)">
{{ product.is_show ? '下架' : '上架' }}
</text>
<text class="action-link danger" @click="deleteProduct(product.id)">删除</text>
</view>
</view>
</view>
</view>
</view>
<!-- 分页 -->
<view class="pagination">
<view class="page-buttons">
<button class="page-btn" :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button>
<view class="page-numbers">
<button
v-for="page in visiblePages"
:key="page"
class="page-number"
:class="{ active: page === currentPage }"
@click="goToPage(page)"
>{{ page }}</button>
</view>
<button class="page-btn" :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
</view>
</view>
<!-- 确认删除弹窗 -->
<view v-if="showDeleteConfirm" class="modal-overlay" @click="showDeleteConfirm = false">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">确认删除</text>
<text class="modal-close" @click="showDeleteConfirm = false">✕</text>
</view>
<view class="modal-body">
<text class="confirm-text">确定要删除选中的商品吗?此操作不可恢复。</text>
</view>
<view class="modal-footer">
<button class="btn btn-default" @click="showDeleteConfirm = false">取消</button>
<button class="btn btn-danger" @click="confirmDelete">确认删除</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted, computed } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
// 响应式数据
const showAdvancedSearch = ref(false)
const showDeleteConfirm = ref(false)
const selectAll = ref(false)
const selectedProducts = ref<number[]>([])
const currentPage = ref(1)
const pageSize = ref(20)
const totalProducts = ref(0)
const totalPages = ref(0)
const productToDelete = ref<number | null>(null)
// 搜索表单
const searchForm = ref({
keyword: '',
type: '',
category: [],
delivery: '',
spec: '',
vip: '',
startDate: '',
endDate: '',
stockMin: '',
stockMax: ''
})
// 筛选选项数据
const productTypes = ref([
{ value: '', label: '全部' },
{ value: '0', label: '普通商品' },
{ value: '1', label: '卡密商品' },
{ value: '2', label: '优惠券商品' },
{ value: '3', label: '虚拟商品' }
])
const deliveryTypes = ref([
{ value: '', label: '全部' },
{ value: '1', label: '快递配送' },
{ value: '2', label: '到店自提' }
])
const specTypes = ref([
{ value: '', label: '全部' },
{ value: '0', label: '单规格' },
{ value: '1', label: '多规格' }
])
const vipOptions = ref([
{ value: '', label: '全部' },
{ value: '0', label: '否' },
{ value: '1', label: '是' }
])
// 分类树数据
const categoryTree = ref<any[]>([])
const categories = ref<any[]>([])
// 商品列表
const productList = ref([])
// 选中的筛选值
const selectedProductType = ref('')
const selectedDeliveryType = ref('')
const selectedSpecType = ref('')
const selectedVip = ref('')
const selectedCategory = ref<number[]>([])
const selectedCategoryText = ref('')
const startDate = ref('')
const endDate = ref('')
// 计算属性
const visiblePages = computed(() => {
const pages = []
const start = Math.max(1, currentPage.value - 2)
const end = Math.min(totalPages.value, currentPage.value + 2)
for (let i = start; i <= end; i++) {
pages.push(i)
}
return pages
})
// 方法
const onProductTypeChange = (e: any) => {
selectedProductType.value = productTypes.value[e.detail.value].label
searchForm.value.type = productTypes.value[e.detail.value].value
}
const onDeliveryTypeChange = (e: any) => {
selectedDeliveryType.value = deliveryTypes.value[e.detail.value].label
searchForm.value.delivery = deliveryTypes.value[e.detail.value].value
}
const onSpecTypeChange = (e: any) => {
selectedSpecType.value = specTypes.value[e.detail.value].label
searchForm.value.spec = specTypes.value[e.detail.value].value
}
const onVipChange = (e: any) => {
selectedVip.value = vipOptions.value[e.detail.value].label
searchForm.value.vip = vipOptions.value[e.detail.value].value
}
const onCategoryChange = (e: any) => {
selectedCategory.value = e.detail.value
// 这里需要根据选中的索引构建分类文本和ID
const selectedIds = selectedCategory.value.map((index: number) => {
return categoryTree.value[index]?.id || ''
})
searchForm.value.category = selectedIds
selectedCategoryText.value = selectedIds.join(', ')
}
const onStartDateChange = (e: any) => {
startDate.value = e.detail.value
searchForm.value.startDate = e.detail.value
}
const onEndDateChange = (e: any) => {
endDate.value = e.detail.value
searchForm.value.endDate = e.detail.value
}
const onSelectAllChange = (e: any) => {
selectAll.value = e.detail.value
if (selectAll.value) {
selectedProducts.value = productList.value.map((product: any) => product.id)
} else {
selectedProducts.value = []
}
}
const onProductSelectChange = (productId: number) => {
const index = selectedProducts.value.indexOf(productId)
if (index > -1) {
selectedProducts.value.splice(index, 1)
} else {
selectedProducts.value.push(productId)
}
selectAll.value = selectedProducts.value.length === productList.value.length
}
const handleSearch = () => {
loadProductList()
}
const handleReset = () => {
searchForm.value = {
keyword: '',
type: '',
category: [],
delivery: '',
spec: '',
vip: '',
startDate: '',
endDate: '',
stockMin: '',
stockMax: ''
}
selectedProductType.value = ''
selectedDeliveryType.value = ''
selectedSpecType.value = ''
selectedVip.value = ''
selectedCategory.value = []
selectedCategoryText.value = ''
startDate.value = ''
endDate.value = ''
loadProductList()
}
const addProduct = () => {
uni.navigateTo({
url: '/pages/mall/admin/product-add'
})
}
const batchOnShelf = () => {
if (selectedProducts.value.length === 0) return
batchUpdateStatus(selectedProducts.value, true)
}
const batchOffShelf = () => {
if (selectedProducts.value.length === 0) return
batchUpdateStatus(selectedProducts.value, false)
}
const batchDelete = () => {
if (selectedProducts.value.length === 0) return
showDeleteConfirm.value = true
}
const viewProduct = (productId: number) => {
uni.navigateTo({
url: `/pages/mall/admin/product-detail?id=${productId}`
})
}
const editProduct = (productId: number) => {
uni.navigateTo({
url: `/pages/mall/admin/product-edit?id=${productId}`
})
}
const toggleProductStatus = async (productId: number, currentStatus: boolean) => {
try {
const newStatus = !currentStatus
await supa.from('products').update({ is_show: newStatus }).eq('id', productId)
uni.showToast({
title: newStatus ? '上架成功' : '下架成功',
icon: 'success'
})
loadProductList()
} catch (error) {
console.error('更新商品状态失败:', error)
uni.showToast({
title: '操作失败',
icon: 'error'
})
}
}
const deleteProduct = (productId: number) => {
productToDelete.value = productId
showDeleteConfirm.value = true
}
const confirmDelete = async () => {
try {
if (productToDelete.value) {
// 单个删除
await supa.from('products').update({ is_del: true }).eq('id', productToDelete.value)
} else if (selectedProducts.value.length > 0) {
// 批量删除
await supa.from('products').update({ is_del: true }).in('id', selectedProducts.value)
}
uni.showToast({
title: '删除成功',
icon: 'success'
})
showDeleteConfirm.value = false
productToDelete.value = null
selectedProducts.value = []
selectAll.value = false
loadProductList()
} catch (error) {
console.error('删除商品失败:', error)
uni.showToast({
title: '删除失败',
icon: 'error'
})
}
}
const batchUpdateStatus = async (productIds: number[], status: boolean) => {
try {
await supa.from('products').update({ is_show: status }).in('id', productIds)
uni.showToast({
title: status ? '批量上架成功' : '批量下架成功',
icon: 'success'
})
selectedProducts.value = []
selectAll.value = false
loadProductList()
} catch (error) {
console.error('批量更新状态失败:', error)
uni.showToast({
title: '操作失败',
icon: 'error'
})
}
}
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
loadProductList()
}
}
const getCategoryName = (categoryId: number) => {
const category = categories.value.find((c: any) => c.id === categoryId)
return category?.name || '未分类'
}
// 数据加载方法
const loadCategories = async () => {
try {
const { data } = await supa.from('categories').select('*').order('sort', { ascending: true })
categories.value = data || []
// 构建分类树
categoryTree.value = buildCategoryTree(data || [])
} catch (error) {
console.error('加载分类失败:', error)
}
}
const buildCategoryTree = (categories: any[]) => {
// 简化的分类树构建逻辑
const tree = categories.map(cat => ({
id: cat.id,
name: cat.name,
pid: cat.pid
}))
// 这里应该实现完整的树形结构构建
return tree
}
const loadProductList = async () => {
try {
let query = supa.from('products').select(`
*,
categories(name)
`).eq('is_del', false)
// 搜索条件
if (searchForm.value.keyword) {
query = query.ilike('title', `%${searchForm.value.keyword}%`)
}
if (searchForm.value.type) {
query = query.eq('type', searchForm.value.type)
}
if (searchForm.value.category && searchForm.value.category.length > 0) {
query = query.in('category_id', searchForm.value.category)
}
// 其他筛选条件...
// 分页
const from = (currentPage.value - 1) * pageSize.value
const to = from + pageSize.value - 1
const { data, count } = await query.range(from, to)
productList.value = data || []
totalProducts.value = count || 0
totalPages.value = Math.ceil(totalProducts.value / pageSize.value)
} catch (error) {
console.error('加载商品列表失败:', error)
uni.showToast({
title: '加载失败',
icon: 'error'
})
}
}
// 页面初始化
onMounted(async () => {
await loadCategories()
await loadProductList()
})
</script>
<style lang="scss">
.product-management {
padding: 30rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
// 搜索区域样式
.search-section {
background-color: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.search-form {
.search-row {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
margin-bottom: 20rpx;
&.advanced {
border-top: 1rpx solid #e8e8e8;
padding-top: 20rpx;
}
}
.form-item {
display: flex;
align-items: center;
min-width: 300rpx;
margin-bottom: 20rpx;
.label {
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
white-space: nowrap;
}
}
.search-input {
flex: 1;
height: 60rpx;
border: 1rpx solid #ddd;
border-radius: 6rpx;
padding: 0 20rpx;
font-size: 26rpx;
}
.picker-text {
padding: 0 20rpx;
height: 60rpx;
line-height: 60rpx;
border: 1rpx solid #ddd;
border-radius: 6rpx;
font-size: 26rpx;
color: #333;
min-width: 200rpx;
}
.cascader-wrapper {
flex: 1;
}
.date-range {
display: flex;
align-items: center;
flex: 1;
gap: 10rpx;
.date-picker {
flex: 1;
padding: 0 20rpx;
height: 60rpx;
line-height: 60rpx;
border: 1rpx solid #ddd;
border-radius: 6rpx;
font-size: 26rpx;
color: #333;
}
.date-separator {
color: #666;
}
}
.number-range {
display: flex;
align-items: center;
flex: 1;
gap: 10rpx;
.number-input {
flex: 1;
height: 60rpx;
border: 1rpx solid #ddd;
border-radius: 6rpx;
padding: 0 20rpx;
font-size: 26rpx;
}
.range-separator {
color: #666;
}
}
.form-actions {
display: flex;
align-items: center;
gap: 20rpx;
margin-top: 20rpx;
.btn {
padding: 12rpx 24rpx;
border-radius: 6rpx;
font-size: 26rpx;
border: none;
cursor: pointer;
&.btn-primary {
background-color: #007bff;
color: white;
}
&.btn-default {
background-color: #f5f5f5;
color: #333;
}
}
.toggle-search {
margin-left: auto;
color: #007bff;
font-size: 26rpx;
cursor: pointer;
.icon {
font-size: 20rpx;
}
}
}
}
// 操作栏样式
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
background-color: #fff;
padding: 20rpx 30rpx;
border-radius: 12rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.action-buttons {
display: flex;
gap: 20rpx;
.btn {
padding: 12rpx 24rpx;
border-radius: 6rpx;
font-size: 26rpx;
border: none;
cursor: pointer;
&:disabled {
background-color: #ccc;
color: #666;
cursor: not-allowed;
}
&.btn-success {
background-color: #28a745;
color: white;
}
&.btn-warning {
background-color: #ffc107;
color: #212529;
}
&.btn-danger {
background-color: #dc3545;
color: white;
}
&.btn-info {
background-color: #17a2b8;
color: white;
}
}
}
.data-info {
display: flex;
align-items: center;
gap: 20rpx;
.total-count,
.page-info {
font-size: 26rpx;
color: #666;
}
}
}
// 商品列表样式
.product-list {
background-color: #fff;
border-radius: 12rpx;
overflow: hidden;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
.table-header {
background-color: #f8f9fa;
border-bottom: 1rpx solid #e9ecef;
.table-row {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
font-weight: bold;
font-size: 26rpx;
color: #495057;
}
}
.table-body {
.data-row {
border-bottom: 1rpx solid #e9ecef;
&:hover {
background-color: #f8f9fa;
}
&:last-child {
border-bottom: none;
}
}
}
.table-row {
display: flex;
align-items: center;
padding: 20rpx 30rpx;
min-height: 120rpx;
.table-cell {
flex: 1;
font-size: 26rpx;
color: #495057;
display: flex;
align-items: center;
&.checkbox-cell {
flex: 0 0 60rpx;
justify-content: center;
}
&.product-cell {
flex: 3;
}
&.action-cell {
flex: 0 0 240rpx;
justify-content: flex-end;
}
}
}
.product-info {
display: flex;
align-items: center;
gap: 20rpx;
.product-image {
width: 80rpx;
height: 80rpx;
border-radius: 8rpx;
flex-shrink: 0;
object-fit: cover;
}
.product-details {
display: flex;
flex-direction: column;
gap: 8rpx;
flex: 1;
.product-name {
font-size: 28rpx;
font-weight: bold;
color: #212529;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.product-id {
font-size: 24rpx;
color: #6c757d;
}
.product-tags {
display: flex;
gap: 8rpx;
.tag {
padding: 2rpx 8rpx;
border-radius: 4rpx;
font-size: 20rpx;
font-weight: bold;
&.hot {
background-color: #ffebee;
color: #c62828;
}
&.recommend {
background-color: #e8f5e8;
color: #2e7d32;
}
&.new {
background-color: #fff3e0;
color: #ef6c00;
}
}
}
}
}
.price-info {
display: flex;
flex-direction: column;
gap: 4rpx;
.price {
font-size: 28rpx;
font-weight: bold;
color: #e74c3c;
}
.original-price {
font-size: 24rpx;
color: #999;
text-decoration: line-through;
}
}
.stock,
.sales {
font-weight: bold;
color: #28a745;
}
.category {
color: #6c757d;
}
.status-tag {
padding: 4rpx 12rpx;
border-radius: 12rpx;
font-size: 24rpx;
font-weight: bold;
&.active {
background-color: #d4edda;
color: #155724;
}
&.inactive {
background-color: #f8d7da;
color: #721c24;
}
}
.action-buttons {
display: flex;
gap: 20rpx;
.action-link {
color: #007bff;
font-size: 24rpx;
cursor: pointer;
&:hover {
text-decoration: underline;
}
&.danger {
color: #dc3545;
}
&.success {
color: #28a745;
}
}
}
// 分页样式
.pagination {
display: flex;
justify-content: center;
margin-top: 30rpx;
.page-buttons {
display: flex;
align-items: center;
gap: 10rpx;
}
.page-btn,
.page-number {
padding: 12rpx 20rpx;
border: 1rpx solid #ddd;
background-color: #fff;
color: #333;
border-radius: 6rpx;
font-size: 26rpx;
cursor: pointer;
transition: all 0.2s;
&:disabled {
background-color: #f5f5f5;
color: #999;
cursor: not-allowed;
}
&:hover:not(:disabled) {
background-color: #007bff;
color: white;
border-color: #007bff;
}
&.active {
background-color: #007bff;
color: white;
border-color: #007bff;
}
}
}
// 弹窗样式
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background-color: #fff;
border-radius: 12rpx;
width: 80%;
max-width: 500rpx;
max-height: 60vh;
overflow: hidden;
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #e9ecef;
.modal-title {
font-size: 32rpx;
font-weight: bold;
color: #212529;
}
.modal-close {
font-size: 36rpx;
color: #999;
cursor: pointer;
&:hover {
color: #333;
}
}
}
.modal-body {
padding: 30rpx;
.confirm-text {
font-size: 28rpx;
color: #666;
text-align: center;
}
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 20rpx;
padding: 30rpx;
border-top: 1rpx solid #e9ecef;
.btn {
padding: 12rpx 24rpx;
border-radius: 6rpx;
font-size: 26rpx;
border: none;
cursor: pointer;
&.btn-default {
background-color: #f5f5f5;
color: #333;
}
&.btn-danger {
background-color: #dc3545;
color: white;
}
}
}
}
// 响应式设计
@media (max-width: 750rpx) {
.search-row {
flex-direction: column;
align-items: stretch;
}
.form-item {
min-width: auto;
margin-bottom: 20rpx;
}
.table-row {
flex-wrap: wrap;
padding: 15rpx;
.table-cell {
min-width: 200rpx;
margin-bottom: 10rpx;
&.product-cell {
min-width: 300rpx;
}
}
}
.action-bar {
flex-direction: column;
gap: 20rpx;
align-items: stretch;
.action-buttons {
justify-content: center;
flex-wrap: wrap;
}
}
}
</style>