完成店铺创建

This commit is contained in:
2026-03-13 16:32:37 +08:00
parent 3617a6a086
commit b180aeabd8
7 changed files with 1713 additions and 40 deletions

View File

@@ -0,0 +1,860 @@
<!-- 店铺管理页:查看 / 编辑店铺信息 -->
<template>
<view class="shop-manage-page">
<!-- 加载中 -->
<view v-if="pageLoading" class="loading-wrap">
<text class="loading-txt">加载中...</text>
</view>
<!-- 无店铺:引导空态 -->
<template v-else-if="!hasShop">
<view class="empty-wrap">
<text class="empty-icon">🏪</text>
<text class="empty-title">您还没有创建店铺</text>
<text class="empty-desc">注册店铺后即可发布商品、管理订单</text>
<button class="btn-create" @click="goToCreate">立即创建店铺</button>
</view>
</template>
<!-- 有店铺:查看 / 编辑 -->
<template v-else>
<!-- 标题行 -->
<view class="page-header">
<text class="page-title">店铺管理</text>
<view class="header-actions">
<button v-if="!isEditing" class="btn-edit" @click="enterEdit">编辑信息</button>
<view v-else class="edit-btns">
<button class="btn-cancel" @click="cancelEdit">取消</button>
<button class="btn-save" :class="{ disabled: saving }" :disabled="saving" @click="saveShop">
{{ saving ? '保存中...' : '保存' }}
</button>
</view>
</view>
</view>
<!-- 店铺基础信息卡 -->
<view class="form-card">
<view class="card-title">基础信息</view>
<!-- Logo -->
<view class="form-item">
<view class="form-label">店铺 Logo</view>
<view v-if="!isEditing" class="view-mode">
<image
v-if="form.shop_logo"
:src="form.shop_logo"
class="preview-logo"
mode="aspectFill"
/>
<text v-else class="empty-val">未设置</text>
</view>
<view v-else>
<view class="upload-area" @click="pickLogoImage">
<image v-if="form.shop_logo" :src="form.shop_logo" class="preview-logo upload-preview" mode="aspectFill" />
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-txt">上传 Logo</text>
</view>
</view>
</view>
</view>
<!-- 店铺名称 -->
<view class="form-item">
<view class="form-label"><text class="required" v-if="isEditing">*</text><text>店铺名称</text></view>
<text v-if="!isEditing" class="view-val">{{ form.shop_name || '-' }}</text>
<input v-else class="form-input" v-model="form.shop_name" placeholder="请输入店铺名称" maxlength="50" />
</view>
<!-- 店铺简介 -->
<view class="form-item">
<view class="form-label">店铺简介</view>
<text v-if="!isEditing" class="view-val multiline">{{ form.description || '-' }}</text>
<textarea v-else class="form-textarea" v-model="form.description" placeholder="请输入店铺简介" maxlength="200"></textarea>
</view>
</view>
<!-- 联系方式卡 -->
<view class="form-card">
<view class="card-title">联系方式</view>
<view class="form-item">
<view class="form-label">联系人</view>
<text v-if="!isEditing" class="view-val">{{ form.contact_name || '-' }}</text>
<input v-else class="form-input" v-model="form.contact_name" placeholder="请输入联系人" maxlength="50" />
</view>
<view class="form-item">
<view class="form-label">联系电话</view>
<text v-if="!isEditing" class="view-val">{{ form.contact_phone || '-' }}</text>
<input v-else class="form-input" v-model="form.contact_phone" placeholder="请输入联系电话" maxlength="20" type="number" />
</view>
<view class="form-item">
<view class="form-label">联系邮箱</view>
<text v-if="!isEditing" class="view-val">{{ form.contact_email || '-' }}</text>
<input v-else class="form-input" v-model="form.contact_email" placeholder="选填" maxlength="100" />
</view>
</view>
<!-- 资质信息卡 -->
<view class="form-card">
<view class="card-title">资质信息</view>
<view class="form-item">
<view class="form-label">营业执照号</view>
<text v-if="!isEditing" class="view-val">{{ form.business_license || '-' }}</text>
<input v-else class="form-input" v-model="form.business_license" placeholder="营业执照注册号" maxlength="50" />
</view>
<!-- 扩展资质(存在 verification_data 里) -->
<view class="form-item">
<view class="form-label">店铺类型</view>
<text v-if="!isEditing" class="view-val">{{ getShopTypeLabel(extData.shop_type) }}</text>
<picker v-else :range="shopTypeLabels" :value="shopTypeIndex" @change="onShopTypeChange">
<view class="picker-trigger">
<text>{{ getShopTypeLabel(extData.shop_type) }}</text>
<text class="picker-arrow">▼</text>
</view>
</picker>
</view>
<view class="form-item">
<view class="form-label">身份证正面</view>
<view v-if="!isEditing">
<image v-if="extData.id_card_front" :src="extData.id_card_front" class="cert-img" mode="aspectFill" />
<text v-else class="empty-val">未上传</text>
</view>
<view v-else class="upload-area-wide" @click="pickIdCardFront">
<image v-if="extData.id_card_front" :src="extData.id_card_front" class="cert-img" mode="aspectFill" />
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-txt">上传身份证正面</text>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">身份证反面</view>
<view v-if="!isEditing">
<image v-if="extData.id_card_back" :src="extData.id_card_back" class="cert-img" mode="aspectFill" />
<text v-else class="empty-val">未上传</text>
</view>
<view v-else class="upload-area-wide" @click="pickIdCardBack">
<image v-if="extData.id_card_back" :src="extData.id_card_back" class="cert-img" mode="aspectFill" />
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-txt">上传身份证反面</text>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">营业执照图片</view>
<view v-if="!isEditing">
<image v-if="extData.license_image" :src="extData.license_image" class="cert-img" mode="aspectFill" />
<text v-else class="empty-val">未上传</text>
</view>
<view v-else class="upload-area-wide" @click="pickLicenseImage">
<image v-if="extData.license_image" :src="extData.license_image" class="cert-img" mode="aspectFill" />
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-txt">上传营业执照</text>
</view>
</view>
</view>
<view class="form-item">
<view class="form-label">品牌授权书</view>
<view v-if="!isEditing">
<image v-if="extData.brand_authorization" :src="extData.brand_authorization" class="cert-img" mode="aspectFill" />
<text v-else class="empty-val">未上传</text>
</view>
<view v-else class="upload-area-wide" @click="pickBrandAuth">
<image v-if="extData.brand_authorization" :src="extData.brand_authorization" class="cert-img" mode="aspectFill" />
<view v-else class="upload-placeholder">
<text class="upload-icon">+</text>
<text class="upload-txt">上传品牌授权书</text>
</view>
</view>
</view>
</view>
<!-- 店铺状态信息(只读) -->
<view class="form-card info-card">
<view class="card-title">店铺状态</view>
<view class="info-row">
<text class="info-label">店铺ID</text>
<text class="info-val">{{ shopId }}</text>
</view>
<view class="info-row">
<text class="info-label">商家ID</text>
<text class="info-val">{{ merchantId }}</text>
</view>
<view class="info-row">
<text class="info-label">当前状态</text>
<text class="info-val" :class="form.status === 1 ? 'status-normal' : 'status-off'">
{{ form.status === 1 ? '正常营业' : form.status === 2 ? '暂停营业' : '已关闭' }}
</text>
</view>
<view class="info-row">
<text class="info-label">商品数量</text>
<text class="info-val">{{ form.product_count }}</text>
</view>
<view class="info-row">
<text class="info-label">创建时间</text>
<text class="info-val">{{ formatDate(form.created_at) }}</text>
</view>
</view>
</template>
</view>
</template>
<script setup lang="uts">
import { ref, onMounted } from 'vue'
import supa from '@/components/supadb/aksupainstance.uts'
import { SUPA_URL } from '@/ak/config.uts'
import { openRoute } from '@/layouts/admin/store/adminNavStore.uts'
// ===== 页面状态 =====
const pageLoading = ref(true)
const hasShop = ref(false)
const isEditing = ref(false)
const saving = ref(false)
// ===== 从 ml_shops 回读的字段 =====
const shopId = ref('')
const merchantId = ref('')
// ===== 店铺表单(绑定到对应的 ml_shops 字段)=====
const form = ref({
shop_name: '',
shop_logo: '',
description: '',
contact_name: '',
contact_phone: '',
contact_email: '',
business_license: '',
status: 1,
product_count: 0,
created_at: ''
})
// 编辑前的快照(用于取消时恢复)
const formSnapshot = ref({ ...form.value })
// ===== 扩展资质(存储在 ml_user_profiles.verification_data=====
const extData = ref({
shop_type: 'personal',
id_card_front: '',
id_card_back: '',
license_image: '',
brand_authorization: ''
})
const extSnapshot = ref({ ...extData.value })
// ===== 店铺类型配置 =====
const shopTypeOptions = [
{ value: 'personal', label: '个人店' },
{ value: 'individual', label: '个体工商户' },
{ value: 'enterprise', label: '普通企业店' },
{ value: 'flagship', label: '旗舰店' },
{ value: 'exclusive', label: '专卖店' },
{ value: 'specialty', label: '专营店' },
]
const shopTypeLabels = shopTypeOptions.map(o => o.label)
const shopTypeIndex = ref(0)
function getShopTypeLabel(val: string): string {
const found = shopTypeOptions.find(o => o.value === val)
return found ? found.label : '个人店'
}
function onShopTypeChange(e: any) {
const idx = e.detail.value as number
shopTypeIndex.value = idx
extData.value.shop_type = shopTypeOptions[idx].value
}
// ===== 加载当前店铺 =====
onMounted(async () => {
await loadShop()
})
async function loadShop() {
pageLoading.value = true
try {
const userId = supa.getSession().user?.getString('id')
if (!userId) {
pageLoading.value = false
return
}
// 根据 merchant_id 查询 ml_shopsmerchant_id = ak_users.id = auth user id
const shopRes = await supa.from('ml_shops')
.select('*')
.eq('merchant_id', userId)
.single()
.execute()
if (shopRes.error != null || !shopRes.data) {
hasShop.value = false
pageLoading.value = false
return
}
// 解析返回数据
const rawData = shopRes.data
let shopRow: UTSJSONObject | null = null
if (Array.isArray(rawData)) {
if ((rawData as Array<any>).length === 0) {
hasShop.value = false
pageLoading.value = false
return
}
shopRow = (rawData as Array<UTSJSONObject>)[0]
} else {
shopRow = rawData as UTSJSONObject
}
if (!shopRow) {
hasShop.value = false
pageLoading.value = false
return
}
hasShop.value = true
shopId.value = shopRow.getString('id') ?? ''
merchantId.value = shopRow.getString('merchant_id') ?? userId
form.value = {
shop_name: shopRow.getString('shop_name') ?? '',
shop_logo: shopRow.getString('shop_logo') ?? '',
description: shopRow.getString('description') ?? '',
contact_name: shopRow.getString('contact_name') ?? '',
contact_phone: shopRow.getString('contact_phone') ?? '',
contact_email: shopRow.getString('contact_email') ?? '',
business_license: shopRow.getString('business_license') ?? '',
status: shopRow.getNumber('status') ?? 1,
product_count: shopRow.getNumber('product_count') ?? 0,
created_at: shopRow.getString('created_at') ?? ''
}
formSnapshot.value = { ...form.value }
// 加载扩展资质
await loadExtData(userId)
} catch (e: any) {
console.error('[ShopManage] 加载店铺失败:', e)
hasShop.value = false
} finally {
pageLoading.value = false
}
}
async function loadExtData(userId: string) {
try {
const profileRes = await supa.from('ml_user_profiles')
.select('verification_data')
.eq('user_id', userId)
.single()
.execute()
if (profileRes.error != null || !profileRes.data) return
const rawData = profileRes.data
let profileRow: UTSJSONObject | null = null
if (Array.isArray(rawData)) {
if ((rawData as Array<any>).length > 0) {
profileRow = (rawData as Array<UTSJSONObject>)[0]
}
} else {
profileRow = rawData as UTSJSONObject
}
if (!profileRow) return
const vd = profileRow.getJSON('verification_data')
if (vd != null) {
extData.value = {
shop_type: vd.getString('shop_type') ?? 'personal',
id_card_front: vd.getString('id_card_front') ?? '',
id_card_back: vd.getString('id_card_back') ?? '',
license_image: vd.getString('license_image') ?? '',
brand_authorization: vd.getString('brand_authorization') ?? ''
}
// 同步 picker index
const idx = shopTypeOptions.findIndex(o => o.value === extData.value.shop_type)
shopTypeIndex.value = idx >= 0 ? idx : 0
}
extSnapshot.value = { ...extData.value }
} catch (e: any) {
console.warn('[ShopManage] 加载扩展资质失败:', e)
}
}
// ===== 编辑模式 =====
function enterEdit() {
formSnapshot.value = { ...form.value }
extSnapshot.value = { ...extData.value }
isEditing.value = true
}
function cancelEdit() {
form.value = { ...formSnapshot.value }
extData.value = { ...extSnapshot.value }
const idx = shopTypeOptions.findIndex(o => o.value === extData.value.shop_type)
shopTypeIndex.value = idx >= 0 ? idx : 0
isEditing.value = false
}
// ===== 图片选择 =====
function pickLogoImage() {
uni.chooseImage({
count: 1,
success: (res) => {
const paths = res.tempFilePaths as string[]
if (paths.length > 0) form.value.shop_logo = paths[0]
}
})
}
function pickIdCardFront() {
uni.chooseImage({
count: 1,
success: (res) => {
const paths = res.tempFilePaths as string[]
if (paths.length > 0) extData.value.id_card_front = paths[0]
}
})
}
function pickIdCardBack() {
uni.chooseImage({
count: 1,
success: (res) => {
const paths = res.tempFilePaths as string[]
if (paths.length > 0) extData.value.id_card_back = paths[0]
}
})
}
function pickLicenseImage() {
uni.chooseImage({
count: 1,
success: (res) => {
const paths = res.tempFilePaths as string[]
if (paths.length > 0) extData.value.license_image = paths[0]
}
})
}
function pickBrandAuth() {
uni.chooseImage({
count: 1,
success: (res) => {
const paths = res.tempFilePaths as string[]
if (paths.length > 0) extData.value.brand_authorization = paths[0]
}
})
}
// ===== 上传图片到 Supabase storage =====
async function uploadImage(localPath: string, folder: string): Promise<string> {
if (!localPath) return ''
if (localPath.startsWith('http') && !localPath.startsWith('blob:')) return localPath
const extMatch = localPath.match(/\.(\w+)$/)
const ext = extMatch ? extMatch[1] : 'jpg'
const uuid = Math.random().toString(36).substring(2, 10)
const remotePath = `shop/${folder}_${Date.now()}_${uuid}.${ext}`
const res = await supa.storage.from('zhipao').upload(remotePath, localPath, {})
if (res.error != null) throw new Error('图片上传失败')
return `${SUPA_URL}/storage/v1/object/public/zhipao/${remotePath}`
}
// ===== 保存 =====
async function saveShop() {
if (!form.value.shop_name.trim()) {
uni.showToast({ title: '请输入店铺名称', icon: 'none' })
return
}
const userId = supa.getSession().user?.getString('id')
if (!userId) {
uni.showToast({ title: '未获取到登录信息', icon: 'none' })
return
}
saving.value = true
uni.showLoading({ title: '保存中...' })
try {
// 1. 上传有变更的图片(本地路径需上传,已有 URL 直接复用)
const [logoUrl, idFrontUrl, idBackUrl, licenseUrl, brandUrl] = await Promise.all([
form.value.shop_logo ? uploadImage(form.value.shop_logo, 'logo') : Promise.resolve(''),
extData.value.id_card_front ? uploadImage(extData.value.id_card_front, 'id_front') : Promise.resolve(''),
extData.value.id_card_back ? uploadImage(extData.value.id_card_back, 'id_back') : Promise.resolve(''),
extData.value.license_image ? uploadImage(extData.value.license_image, 'license') : Promise.resolve(''),
extData.value.brand_authorization ? uploadImage(extData.value.brand_authorization, 'brand_auth') : Promise.resolve(''),
])
// 更新本地表单中的 URL去掉临时路径
if (logoUrl) form.value.shop_logo = logoUrl
if (idFrontUrl) extData.value.id_card_front = idFrontUrl
if (idBackUrl) extData.value.id_card_back = idBackUrl
if (licenseUrl) extData.value.license_image = licenseUrl
if (brandUrl) extData.value.brand_authorization = brandUrl
// 2. 更新 ml_shops
const shopUpdatePayload = {
shop_name: form.value.shop_name.trim(),
shop_logo: form.value.shop_logo,
description: form.value.description,
contact_name: form.value.contact_name,
contact_phone: form.value.contact_phone,
contact_email: form.value.contact_email,
business_license: form.value.business_license
} as UTSJSONObject
const updateRes = await supa.from('ml_shops')
.update(shopUpdatePayload)
.eq('merchant_id', userId)
.execute()
if (updateRes.error != null) {
console.error('[ShopManage] 更新 ml_shops 失败:', updateRes.error)
const errMsg = updateRes.error?.getString ? updateRes.error.getString('message') ?? '保存失败' : '保存失败'
uni.showToast({ title: '保存失败:' + errMsg, icon: 'none' })
return
}
// 验证更新结果
if (!updateRes.data) {
console.error('[ShopManage] 更新 ml_shops 未返回数据')
} else {
console.log('[ShopManage] 更新成功,返回数据:', updateRes.data)
}
// 3. 更新 ml_user_profiles.verification_data
const extPayloadData = {
shop_type: extData.value.shop_type,
id_card_front: extData.value.id_card_front,
id_card_back: extData.value.id_card_back,
license_image: extData.value.license_image,
brand_authorization: extData.value.brand_authorization,
updated_at: new Date().toISOString()
} as UTSJSONObject
// 先查询是否已有对应记录
const profCheckRes = await supa.from('ml_user_profiles')
.select('id')
.eq('user_id', userId)
.single()
.execute()
if (profCheckRes.data != null && !profCheckRes.error) {
const profRes = await supa.from('ml_user_profiles')
.update({ verification_data: extPayloadData } as UTSJSONObject)
.eq('user_id', userId)
.execute()
if (profRes.error != null) {
console.warn('[ShopManage] 更新扩展资质失败(不影响主信息):', profRes.error)
}
} else {
const profRes = await supa.from('ml_user_profiles')
.insert({ user_id: userId, verification_data: extPayloadData } as UTSJSONObject)
.execute()
if (profRes.error != null) {
console.warn('[ShopManage] 写入扩展资质失败(不影响主信息):', profRes.error)
}
}
isEditing.value = false
formSnapshot.value = { ...form.value }
extSnapshot.value = { ...extData.value }
// 4. 重新拉取最新数据回显
await loadShop()
uni.showToast({ title: '保存成功', icon: 'success' })
} catch (e: any) {
console.error('[ShopManage] 保存异常:', e)
uni.showToast({ title: e?.message ?? '保存失败,请重试', icon: 'none' })
} finally {
saving.value = false
uni.hideLoading()
}
}
// ===== 导航 =====
function goToCreate() {
openRoute('shop_create')
}
// ===== 工具 =====
function formatDate(dateStr: string): string {
if (!dateStr) return '-'
try {
const d = new Date(dateStr)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
} catch (_) {
return dateStr
}
}
</script>
<style scoped lang="scss">
.shop-manage-page {
padding: 0;
background: transparent;
}
/* 加载 */
.loading-wrap {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
}
.loading-txt { font-size: 14px; color: #999; }
/* 空态 */
.empty-wrap {
display: flex;
flex-direction: column;
align-items: center;
padding: 80px 40px;
background: #fff;
border-radius: 4px;
}
.empty-icon { font-size: 56px; margin-bottom: 16px; }
.empty-title { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 8px; }
.empty-desc { font-size: 13px; color: #999; margin-bottom: 28px; }
.btn-create {
padding: 0 32px;
height: 40px;
background: #1890ff;
color: #fff;
border-radius: 4px;
font-size: 14px;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
/* 页面头部 */
.page-header {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.page-title {
font-size: 22px;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
flex-direction: row;
gap: 8px;
}
.btn-edit {
padding: 0 20px;
height: 36px;
background: #1890ff;
color: #fff;
border-radius: 4px;
font-size: 13px;
border: none;
display: flex;
align-items: center;
justify-content: center;
}
.edit-btns {
display: flex;
flex-direction: row;
gap: 8px;
}
.btn-cancel {
padding: 0 16px;
height: 36px;
background: #fff;
color: #606266;
border: 1px solid #dcdfe6;
border-radius: 4px;
font-size: 13px;
display: flex;
align-items: center;
justify-content: center;
}
.btn-save {
padding: 0 20px;
height: 36px;
background: #52c41a;
color: #fff;
border-radius: 4px;
font-size: 13px;
border: none;
display: flex;
align-items: center;
justify-content: center;
&.disabled { background: #b7eb8f; cursor: not-allowed; }
}
/* 卡片 */
.form-card {
background: #fff;
border-radius: 4px;
padding: 20px 24px;
margin-bottom: 16px;
}
.card-title {
font-size: 15px;
font-weight: 600;
color: #333;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.required { color: #ff4d4f; margin-right: 4px; }
/* 表单项 */
.form-item {
margin-bottom: 16px;
&:last-child { margin-bottom: 0; }
}
.form-label {
font-size: 13px;
color: #606266;
margin-bottom: 8px;
display: flex;
flex-direction: row;
align-items: center;
gap: 2px;
}
.form-input {
width: 100%;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 13px;
color: #333;
background: #fff;
box-sizing: border-box;
}
.form-textarea {
width: 100%;
height: 80px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 8px 12px;
font-size: 13px;
color: #333;
background: #fff;
box-sizing: border-box;
resize: none;
}
/* 只读模式展示 */
.view-val {
font-size: 14px;
color: #333;
line-height: 1.5;
}
.view-val.multiline { white-space: pre-wrap; }
.empty-val { font-size: 13px; color: #aaa; }
/* Logo 预览 */
.preview-logo {
width: 80px;
height: 80px;
border-radius: 4px;
object-fit: cover;
}
.upload-preview {
width: 100%;
height: 100%;
}
/* 上传区域 */
.upload-area {
width: 100px;
height: 80px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
overflow: hidden;
&:hover { border-color: #1890ff; }
}
.upload-area-wide {
width: 200px;
height: 120px;
border: 1px dashed #dcdfe6;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
overflow: hidden;
&:hover { border-color: #1890ff; }
}
.upload-placeholder {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.upload-icon { font-size: 20px; color: #c0c4cc; }
.upload-txt { font-size: 11px; color: #aaa; }
/* 证件图 */
.cert-img {
width: 200px;
height: 120px;
border-radius: 4px;
}
.view-mode { display: flex; flex-direction: row; align-items: center; }
/* Picker */
.picker-trigger {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 160px;
height: 36px;
border: 1px solid #dcdfe6;
border-radius: 4px;
padding: 0 12px;
font-size: 13px;
color: #333;
background: #fff;
}
.picker-arrow { font-size: 10px; color: #c0c4cc; }
/* 状态信息 */
.info-card { }
.info-row {
display: flex;
flex-direction: row;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #f9f9f9;
&:last-child { border-bottom: none; }
}
.info-label { width: 100px; font-size: 13px; color: #999; }
.info-val { font-size: 13px; color: #333; }
.status-normal { color: #52c41a; font-weight: 500; }
.status-off { color: #ff4d4f; font-weight: 500; }
</style>