850 lines
24 KiB
Plaintext
850 lines
24 KiB
Plaintext
<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>
|
||
|
||
<!-- 其他步骤占位 -->
|
||
<view v-if="activeStep > 2" class="step-content">
|
||
<view class="placeholder-step">
|
||
<text>步骤 {{ steps[activeStep] }} 逻辑接入中...</text>
|
||
<text class="sub-tip">该模块涉及物流模板、会员等级定价等,将在后续阶段补齐</text>
|
||
</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,
|
||
AdminProduct,
|
||
AdminProductSku
|
||
} 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 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
|
||
})
|
||
|
||
const skus = ref<AdminProductSku[]>([])
|
||
|
||
// 基础资料列表
|
||
const categories = ref<AdminCategory[]>([])
|
||
const labelList = ref<ProductLabel[]>([])
|
||
const specTemplates = ref<ProductSpecTemplate[]>([])
|
||
const paramTemplates = ref<ProductParamTemplate[]>([])
|
||
|
||
// --- UI 控制状态 ---
|
||
const showLabelModal = ref(false)
|
||
const categoryIndex = ref(0)
|
||
|
||
// 计算属性:分类名称列表供 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)
|
||
})
|
||
|
||
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] = await Promise.all([
|
||
fetchAdminCategoryList({ isActive: true }),
|
||
fetchLabels(),
|
||
fetchSpecTemplates(),
|
||
fetchParamTemplates()
|
||
])
|
||
categories.value = cateRes
|
||
labelList.value = labelRes
|
||
specTemplates.value = specRes
|
||
paramTemplates.value = paramRes
|
||
} 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 ?? {}
|
||
|
||
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--
|
||
}
|
||
}
|
||
|
||
async function handleSave() {
|
||
isLoading.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 {
|
||
isLoading.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>
|