Files
medical-mall/pages/mall/admin/shop/manage.uvue
2026-03-13 16:32:37 +08:00

861 lines
26 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="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>