Files
medical-mall/pages/mall/admin/product/product-management/edit.uvue
2026-02-15 16:37:37 +08:00

959 lines
28 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="product-edit-page">
<view class="page-header">
<view class="back-link" @click="goBack">
<text class="arrow">{"<"}</text>
<text class="back-txt">返回</text>
</view>
<text class="header-title">编辑商品</text>
</view>
<!-- 步骤层 -->
<view class="steps-card">
<view class="step-items">
<view v-for="(step, index) in steps" :key="index" class="step-item" :class="{ active: activeStep === index }">
<text class="step-txt">{{ step }}</text>
<view v-if="index < steps.length - 1" class="step-line"></view>
</view>
</view>
</view>
<!-- 表单内容 -->
<view class="form-card">
<!-- Step 0: 基础信息 -->
<view v-if="activeStep === 0" class="step-content">
<view class="form-item">
<view class="label"><text class="required">*</text><text>商品类型:</text></view>
<view class="input-wrap">
<view class="radio-group">
<view class="radio-item active">
<text class="radio-circle on"></text>
<view class="radio-txt">
<text class="main">普通商品</text>
<text class="sub">(物流发货)</text>
</view>
</view>
</view>
</view>
</view>
<view class="form-item">
<view class="label"><text class="required">*</text><text>商品名称:</text></view>
<view class="input-wrap">
<view class="input-box">
<input class="real-input" v-model="form.name" maxlength="80" placeholder="请输入商品名称" />
<text class="count">{{ form.name.length }}/80</text>
</view>
</view>
</view>
<view class="form-item">
<view class="label"><text class="required">*</text><text>单位:</text></view>
<view class="input-wrap">
<view class="input-box small">
<input class="real-input" v-model="form.unit" maxlength="5" placeholder="件" />
<text class="count">{{ form.unit.length }}/5</text>
</view>
</view>
</view>
<view class="form-item">
<view class="label"><text class="required">*</text><text>商品轮播图:</text></view>
<view class="input-wrap">
<view class="image-uploader">
<view v-for="(img, i) in form.image_urls" :key="i" class="img-item">
<image :src="img" mode="aspectFill" />
<view class="img-close" @click="removeImage(i)">×</view>
</view>
<view v-if="form.image_urls.length < 10" class="upload-btn" @click="onUploadCarousel">
<text class="icon">+</text>
</view>
</view>
<text class="tip">建议尺寸800*800默认首张图为主图最多上传10张</text>
</view>
</view>
<view class="form-item">
<view class="label"><text class="required">*</text><text>商品分类:</text></view>
<view class="input-wrap">
<picker :value="categoryIndex" :range="categoryLabels" @change="onCategoryChange">
<view class="tag-selector">
<view v-if="selectedCategoryName" class="tag-item">
<text>{{ selectedCategoryName }}</text>
</view>
<text class="add-link">{{ selectedCategoryName ? '点击切换分类' : '选择分类' }}</text>
</view>
</picker>
</view>
</view>
<view class="form-item">
<view class="label"><text>商品标签:</text></view>
<view class="input-wrap">
<view class="tag-selector">
<view v-for="(tag, tidx) in form.tags" :key="tidx" class="tag-item">
<text>{{ tag }}</text>
<text class="close" @click="removeTag(tag)">×</text>
</view>
<text class="add-link" @click="showLabelModal = true">选择标签</text>
</view>
</view>
</view>
<view class="form-item">
<view class="label"><text>商品状态:</text></view>
<view class="input-wrap">
<view class="radio-group-simple">
<view class="radio-simple" :class="{ on: form.status === 1 }" @click="form.status = 1">
<text class="dot"></text>
<text>上架</text>
</view>
<view class="radio-simple" :class="{ on: form.status === 2 }" @click="form.status = 2">
<text class="dot"></text>
<text>下架</text>
</view>
</view>
</view>
</view>
</view>
<!-- Step 1: 规格库存 -->
<view v-if="activeStep === 1" class="step-content">
<view class="form-item">
<view class="label"><text>基础售价:</text></view>
<view class="input-wrap">
<view class="input-box small">
<input class="real-input" type="digit" v-model="form.price" placeholder="0.00" />
</view>
</view>
</view>
<view class="form-item">
<view class="label"><text>库存总数:</text></view>
<view class="input-wrap">
<view class="input-box small">
<input class="real-input" type="number" v-model="form.stock" placeholder="0" />
</view>
</view>
</view>
<view class="form-item">
<view class="label"><text>规格模板:</text></view>
<view class="input-wrap">
<picker :range="specTemplateLabels" @change="onSpecTemplateChange">
<button class="btn-white small-btn">快速导入模板</button>
</picker>
</view>
</view>
<!-- SKU 表格区域 -->
<view class="sku-table-wrap">
<view class="sku-table-header">
<text class="sku-th flex-2">规格</text>
<text class="sku-th flex-1">价格</text>
<text class="sku-th flex-1">库存</text>
<text class="sku-th flex-1">编号</text>
</view>
<view v-for="(sku, idx) in skus" :key="idx" class="sku-tr">
<text class="sku-td flex-2">{{ formatSpecs(sku.specifications) }}</text>
<view class="sku-td flex-1">
<input class="sku-input" type="digit" v-model="sku.price" />
</view>
<view class="sku-td flex-1">
<input class="sku-input" type="number" v-model="sku.stock" />
</view>
<view class="sku-td flex-1">
<input class="sku-input" v-model="sku.sku_code" />
</view>
</view>
<view v-if="skus.length === 0" class="empty-sku">
<text>请先选择规格或添加属性</text>
</view>
</view>
</view>
<!-- Step 2: 商品详情 -->
<view v-if="activeStep === 2" class="step-content">
<view class="form-item row-align-start">
<view class="label"><text class="required">*</text><text>商品详情:</text></view>
<view class="input-wrap">
<textarea class="textarea-box" v-model="form.description" placeholder="请输入商品详细描述内容..." />
</view>
</view>
</view>
<!-- Step 3: 物流设置 -->
<view v-if="activeStep === 3" class="step-content">
<view class="form-item">
<view class="label"><text class="required">*</text><text>运费模板:</text></view>
<view class="input-wrap">
<picker :value="shippingIndex" :range="shippingLabels" @change="onShippingChange">
<view class="tag-selector">
<view v-if="form.shipping_template_id" class="tag-item">
<text>{{ shippingLabels[shippingIndex] }}</text>
</view>
<text class="add-link">{{ form.shipping_template_id ? '点击切换模板' : '选择运费模板' }}</text>
</view>
</picker>
</view>
</view>
</view>
<!-- Step 4: 会员价/佣金 -->
<view v-if="activeStep === 4" class="step-content">
<view class="form-item">
<view class="label"><text>会员价设置:</text></view>
<view class="input-wrap">
<button class="btn-white small-btn" @click="goMemberPrice">前往配置会员价</button>
<text class="tip" style="margin-top: 8px; display: block;">点击上方按钮,可以为不同规格的商品设置各级会员的专属价格。</text>
</view>
</view>
</view>
<!-- Step 5: 营销设置 -->
<view v-if="activeStep === 5" class="step-content">
<view class="form-item">
<view class="label"><text>赠送积分:</text></view>
<view class="input-wrap">
<view class="input-box small">
<input class="real-input" type="number" v-model="form.give_integral" placeholder="0" />
</view>
<text class="tip">用户购买该商品后赠送的积分数量</text>
</view>
</view>
</view>
<!-- Step 6: 其他设置 -->
<view v-if="activeStep === 6" class="step-content">
<view class="form-item">
<view class="label"><text>排序权重:</text></view>
<view class="input-wrap">
<view class="input-box small">
<input class="real-input" type="number" v-model="form.sort_order" placeholder="0" />
</view>
<text class="tip">数字越大,商品在列表中越靠前</text>
</view>
</view>
<view class="form-item">
<view class="label"><text>虚拟销量:</text></view>
<view class="input-wrap">
<view class="input-box small">
<input class="real-input" type="number" v-model="form.virtual_sales" placeholder="0" />
</view>
<text class="tip">显示的销量 = 真实销量 + 虚拟销量</text>
</view>
</view>
<view class="form-item">
<view class="label"><text>库存预警:</text></view>
<view class="input-wrap">
<view class="input-box small">
<input class="real-input" type="number" v-model="form.stock_warning" placeholder="10" />
</view>
<text class="tip">当商品总库存低于此数值时,管理后台将进行提醒</text>
</view>
</view>
</view>
</view>
<view class="footer-btns">
<button v-if="activeStep > 0" class="btn-prev" @click="prevStep">上一步</button>
<button v-if="activeStep < steps.length - 1" class="btn-next" @click="nextStep">下一步</button>
<button v-if="activeStep === steps.length - 1" class="btn-save" @click="handleSave">保存</button>
</view>
<!-- 标签选择弹窗 -->
<view v-if="showLabelModal" class="popup-mask" @click="showLabelModal = false">
<view class="popup-card" @click.stop>
<view class="popup-header">
<text class="popup-title">选择商品标签</text>
<text class="popup-close" @click="showLabelModal = false">×</text>
</view>
<scroll-view class="popup-body" scroll-y="true" style="height: 300px;">
<view class="label-grid">
<view
v-for="label in labelList"
:key="label.id"
class="label-check-item"
:class="{ selected: form.tags.includes(label.name) }"
@click="toggleTag(label.name)"
>
<text class="label-check-txt">{{ label.name }}</text>
</view>
</view>
</scroll-view>
<view class="popup-footer">
<button class="btn primary" @click="showLabelModal = false">完成</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
import {
fetchAdminProductDetail,
saveAdminProduct,
fetchShippingTemplates,
AdminProduct,
AdminProductSku,
ShippingTemplate
} from '@/services/admin/productService.uts'
import { fetchAdminCategoryList, AdminCategory } from '@/services/admin/productCategoryService.uts'
import { fetchLabels, ProductLabel } from '@/services/admin/productLabelService.uts'
import { fetchSpecTemplates, fetchParamTemplates, ProductSpecTemplate, ProductParamTemplate } from '@/services/admin/productSpecParamService.uts'
// --- 状态定义 ---
const activeStep = ref(0)
const steps = ['基础信息', '规格库存', '商品详情', '物流设置', '会员价/佣金', '营销设置', '其他设置']
const isLoading = ref(false)
const isSaving = ref(false)
const productId = ref<string | null>(null)
// 响应式表单数据
const form = reactive({
name: '',
subtitle: '',
unit: '件',
category_id: '',
image_urls: [] as string[],
video_urls: [] as string[],
tags: [] as string[],
status: 1, // 1:上架, 2:下架
description: '',
price: 0,
stock: 0,
product_code: '',
attributes: {} as any,
// 扩展字段
shipping_template_id: null as string | null,
give_integral: 0,
stock_warning: 10,
virtual_sales: 0,
sort_order: 0
})
const skus = ref<AdminProductSku[]>([])
// 基础资料列表
const categories = ref<AdminCategory[]>([])
const labelList = ref<ProductLabel[]>([])
const specTemplates = ref<ProductSpecTemplate[]>([])
const paramTemplates = ref<ProductParamTemplate[]>([])
const shippingTemplates = ref<ShippingTemplate[]>([])
// --- UI 控制状态 ---
const showLabelModal = ref(false)
const categoryIndex = ref(0)
const shippingIndex = ref(-1)
// 计算属性:分类名称列表供 picker 使用
const categoryLabels = computed((): string[] => {
return categories.value.map(c => c.name)
})
// 计算属性:当前选中分类名称
const selectedCategoryName = computed((): string => {
const cate = categories.value.find(c => c.id === form.category_id)
return cate != null ? cate.name : ''
})
// 计算属性:规格模板名称列表
const specTemplateLabels = computed((): string[] => {
return specTemplates.value.map(t => t.name)
})
// 计算属性:运费模板列表
const shippingLabels = computed((): string[] => {
return shippingTemplates.value.map(t => t.name)
})
onMounted(() => {
// 1. 获取商品 ID
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const options = currentPage.options as Record<string, string | undefined>
productId.value = options['id'] ?? null
// 2. 初始化所有基础资料
initBaseData()
// 3. 如果是编辑模式,加载详情
if (productId.value != null) {
loadProductDetail(productId.value!)
}
})
async function initBaseData() {
try {
const [cateRes, labelRes, specRes, paramRes, shipRes] = await Promise.all([
fetchAdminCategoryList({ isActive: true }),
fetchLabels(),
fetchSpecTemplates(),
fetchParamTemplates(),
fetchShippingTemplates()
])
categories.value = cateRes
labelList.value = labelRes
specTemplates.value = specRes
paramTemplates.value = paramRes
shippingTemplates.value = shipRes
} catch (e) {
console.error('加载基础资料失败:', e)
}
}
async function loadProductDetail(id: string) {
isLoading.value = true
try {
const res = await fetchAdminProductDetail(id)
if (res.product != null) {
const p = res.product!
form.name = p.name
form.subtitle = p.subtitle ?? ''
form.unit = p.unit ?? '件'
form.category_id = p.category_id
form.image_urls = p.image_urls ?? []
form.video_urls = p.video_urls ?? []
form.tags = p.tags ?? []
form.status = p.status
form.description = p.description ?? ''
form.price = p.price
form.stock = p.stock
form.product_code = p.product_code ?? ''
form.attributes = p.attributes ?? {}
// 扩展字段
form.shipping_template_id = p.shipping_template_id
form.give_integral = p.give_integral
form.stock_warning = p.stock_warning
form.virtual_sales = p.virtual_sales
form.sort_order = p.sort_order
// 匹配运费模板索引
if (p.shipping_template_id != null) {
shippingIndex.value = shippingTemplates.value.findIndex(t => t.id === p.shipping_template_id)
}
skus.value = res.skus
}
} catch (e) {
uni.showToast({ title: '加载详情失败', icon: 'none' })
} finally {
isLoading.value = false
}
}
// --- 交互逻辑 ---
function goBack() {
openRoute('product_productList')
}
function nextStep() {
if (activeStep.value < steps.length - 1) {
activeStep.value++
}
}
function prevStep() {
if (activeStep.value > 0) {
activeStep.value--
}
}
function onShippingChange(e : any) {
const index = parseInt(String(e.detail.value))
shippingIndex.value = index
form.shipping_template_id = shippingTemplates.value[index].id
}
function goMemberPrice() {
if (productId.value == null) {
uni.showToast({ title: '请先保存商品基础信息', icon: 'none' })
return
}
openRoute('product_member_price', { id: productId.value })
}
async function handleSave() {
if (isSaving.value) return
isSaving.value = true
try {
const productData : Partial<AdminProduct> = {
id: productId.value ?? undefined,
...form
}
const success = await saveAdminProduct(productData, skus.value)
if (success) {
uni.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => goBack(), 1500)
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '系统异常', icon: 'none' })
} finally {
isSaving.value = false
}
}
// 模拟图片上传
function onUploadCarousel() {
uni.chooseImage({
count: 10 - form.image_urls.length,
success: (res) => {
form.image_urls.push(...res.tempFilePaths)
}
})
}
function removeImage(index: number) {
form.image_urls.splice(index, 1)
}
function onCategoryChange(e: any) {
const index = parseInt(String(e.detail.value))
categoryIndex.value = index
form.category_id = categories.value[index].id
}
function toggleTag(name: string) {
const index = form.tags.indexOf(name)
if (index > -1) {
form.tags.splice(index, 1)
} else {
form.tags.push(name)
}
}
function removeTag(name: string) {
const index = form.tags.indexOf(name)
if (index > -1) {
form.tags.splice(index, 1)
}
}
// --- 规格与 SKU 核心逻辑 ---
function formatSpecs(specs: any): string {
if (specs == null) return '默认规格'
if (typeof specs === 'object') {
const vals = Object.values(specs as Record<string, any>)
return vals.join(',')
}
return String(specs)
}
function onSpecTemplateChange(e: any) {
const index = parseInt(String(e.detail.value))
const tpl = specTemplates.value[index]
if (tpl == null) return
// 解析规格字符串,例如 "颜色:红色,蓝色;尺寸:S,M"
const specGroups = tpl.specs.split(';')
const dimensions: Array<Array<{key: string, value: string}>> = []
specGroups.forEach(group => {
const parts = group.split(':')
if (parts.length === 2) {
const key = parts[0].trim()
const values = parts[1].split(',')
dimensions.push(values.map(v => ({ key, value: v.trim() })))
}
})
if (dimensions.length === 0) return
// 笛卡尔积生成组合
const combinations = cartesianProduct(dimensions)
// 转换为 SKU 列表
skus.value = combinations.map(combo => {
const specObj: Record<string, string> = {}
combo.forEach(item => {
specObj[item.key] = item.value
})
return {
sku_code: '',
specifications: specObj,
price: form.price || 0,
stock: 0,
status: 1,
image_url: form.image_urls.length > 0 ? form.image_urls[0] : null
} as AdminProductSku
})
uni.showToast({ title: `生成了 ${skus.value.length} 个规格` })
}
/**
* 笛卡尔积辅助函数
*/
function cartesianProduct<T>(arrays: T[][]): T[][] {
const result: T[][] = [[]]
for (const array of arrays) {
const temp: T[][] = []
for (const res of result) {
for (const item of array) {
temp.push([...res, item])
}
}
if (temp.length > 0) result.length = 0, result.push(...temp)
}
return result
}
</script>
<style scoped lang="scss">
.product-edit-page {
padding: 20px;
background-color: #f5f7f9;
min-height: 100vh;
}
.page-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 16px;
margin-bottom: 20px;
.back-link {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
color: #666;
cursor: pointer;
.arrow { font-size: 14px; }
.back-txt { font-size: 14px; }
}
.header-title { font-size: 16px; font-weight: bold; color: #333; }
}
.steps-card {
background: #fff;
padding: 20px;
border-radius: 4px;
margin-bottom: 16px;
}
.step-items {
display: flex;
flex-direction: row;
align-items: center;
}
.step-item {
display: flex;
flex-direction: row;
align-items: center;
.step-txt {
font-size: 14px;
color: #999;
padding: 0 16px;
}
&.active .step-txt {
color: #1890ff;
font-weight: bold;
border-bottom: 2px solid #1890ff;
padding-bottom: 4px;
}
.step-line {
width: 20px;
height: 1px;
background: #e8e8e8;
}
}
.form-card {
background: #fff;
padding: 40px;
border-radius: 4px;
}
.form-item {
display: flex;
flex-direction: row;
margin-bottom: 30px;
.label {
width: 120px;
text-align: right;
font-size: 14px;
color: #333;
padding-top: 8px;
margin-right: 20px;
.required { color: #f5222d; margin-right: 4px; }
}
.input-wrap { flex: 1; }
}
.radio-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
border: 1px solid #1890ff;
border-radius: 4px;
padding: 10px 16px;
width: 160px;
position: relative;
.radio-circle {
width: 14px; height: 14px; border: 1px solid #d9d9d9; border-radius: 50%;
&.on { border-color: #1890ff; background: #1890ff; }
}
.radio-txt {
display: flex;
flex-direction: column;
.main { font-size: 14px; color: #333; }
.sub { font-size: 12px; color: #999; }
}
&::after {
content: '✓';
position: absolute;
right: 0; bottom: 0;
background: #1890ff; color: #fff; font-size: 10px; padding: 0 2px;
}
}
.input-box {
border: 1px solid #d9d9d9;
border-radius: 4px;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 12px;
width: 400px;
height: 36px;
&.small { width: 150px; }
.real-input { flex: 1; font-size: 14px; color: #333; }
.count { font-size: 12px; color: #bfbfbf; }
}
.image-uploader {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 8px;
.img-item {
width: 80px; height: 80px; position: relative;
image { width: 100%; height: 100%; border-radius: 4px; }
.img-close {
position: absolute; right: -6px; top: -6px; width: 16px; height: 16px;
background: rgba(0,0,0,0.5); color: #fff; border-radius: 50%;
display: flex; align-items: center; justify-content: center; font-size: 12px;
}
}
}
.upload-btn {
width: 80px; height: 80px; border: 1px dashed #d9d9d9; border-radius: 4px;
display: flex; align-items: center; justify-content: center;
.icon { font-size: 24px; color: #999; }
&.v-btn { width: 64px; height: 64px; margin-bottom: 8px; .v-icon { font-size: 24px; } }
}
.tip { font-size: 12px; color: #999; }
.tag-selector {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
align-items: center;
.tag-item {
background: #f5f5f5; border: 1px solid #d9d9d9; padding: 2px 10px; border-radius: 4px;
display: flex; flex-direction: row; align-items: center; gap: 6px;
font-size: 14px; color: #666;
.close { color: #999; cursor: pointer; }
}
.add-link { font-size: 14px; color: #1890ff; cursor: pointer; }
}
.mock-btn-select {
border: 1px solid #d9d9d9; border-radius: 4px; padding: 6px 16px;
font-size: 14px; color: #666; display: inline-block;
}
.radio-group-simple {
display: flex;
flex-direction: row;
gap: 20px;
.radio-simple {
display: flex; flex-direction: row; align-items: center; gap: 6px; font-size: 14px; color: #666;
.dot { width: 14px; height: 14px; border: 1px solid #d9d9d9; border-radius: 50%; position: relative; }
&.on {
color: #1890ff;
.dot { border-color: #1890ff; }
.dot::after {
content: ''; position: absolute; left: 3px; top: 3px; width: 6px; height: 6px;
background: #1890ff; border-radius: 50%;
}
}
}
}
.footer-btns {
margin-top: 24px;
display: flex;
flex-direction: row;
justify-content: center;
gap: 16px;
padding-bottom: 40px;
.btn-prev { background: #fff; color: #666; border: 1px solid #d9d9d9; padding: 0 24px; height: 40px; border-radius: 4px; }
.btn-next { background: #1890ff; color: #fff; border: none; padding: 0 24px; height: 40px; border-radius: 4px; }
.btn-save { background: #52c41a; color: #fff; border: none; padding: 0 24px; height: 40px; border-radius: 4px; }
}
/* SKU 表格样式 */
.sku-table-wrap {
margin-top: 20px;
border: 1px solid #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.sku-table-header {
display: flex;
flex-direction: row;
background-color: #fafafa;
height: 44px;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.sku-th {
font-size: 14px;
font-weight: bold;
color: #333;
padding: 0 12px;
text-align: center;
}
.sku-tr {
display: flex;
flex-direction: row;
height: 54px;
align-items: center;
border-bottom: 1px solid #f0f0f0;
}
.sku-td {
padding: 0 12px;
font-size: 14px;
color: #666;
text-align: center;
}
.sku-input {
border: 1px solid #dcdfe6;
border-radius: 4px;
height: 32px;
width: 90%;
font-size: 13px;
text-align: center;
}
.empty-sku {
padding: 40px 0;
text-align: center;
color: #999;
font-size: 14px;
}
.textarea-box {
width: 100%;
height: 300px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 12px;
font-size: 14px;
line-height: 1.6;
}
.placeholder-step {
padding: 100px 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
color: #999;
font-size: 16px;
.sub-tip { font-size: 13px; color: #ccc; }
}
.small-btn {
height: 32px;
line-height: 32px;
padding: 0 12px;
font-size: 13px;
border: 1px solid #dcdfe6;
background: #fff;
border-radius: 4px;
}
.row-align-start {
align-items: flex-start !important;
}
/* 弹窗与标签选择样式 */
.popup-mask {
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: 999;
}
.popup-card {
width: 500px;
background-color: #fff;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.popup-header {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid #f0f0f0;
}
.popup-title { font-size: 16px; font-weight: bold; }
.popup-close { font-size: 20px; color: #999; cursor: pointer; }
.popup-body { padding: 20px; }
.label-grid {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 12px;
}
.label-check-item {
padding: 6px 16px;
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
border-radius: 4px;
&.selected {
background-color: #e6f7ff;
border-color: #1890ff;
.label-check-txt { color: #1890ff; }
}
}
.label-check-txt { font-size: 13px; color: #666; }
.popup-footer {
padding: 16px 20px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
}
.btn.primary {
background-color: #1890ff;
color: #fff;
border: none;
padding: 0 20px;
height: 32px;
border-radius: 4px;
font-size: 14px;
}
.flex-1 { flex: 1; }
.flex-2 { flex: 2; }
</style>