feat(web): add full dialog layout simulation (issue/create/detail/confirm/revoke) with Figma tokens

This commit is contained in:
2026-05-18 23:55:57 +08:00
parent 9f3da47574
commit 51c2598fb7
@@ -1,322 +1,267 @@
<template>
<div style="height:100vh;overflow:hidden;font-family:-apple-system,BlinkMacSystemFont,sans-serif;font-size:13px;display:flex;flex-direction:column">
<!-- ========== HEADER 60px ========== -->
<!-- ====== HEADER 60px ====== -->
<header style="height:60px;background:#fff;border-bottom:1px solid #E8ECF1;display:flex;align-items:center;padding:0 20px;flex-shrink:0;z-index:10">
<div style="display:flex;align-items:center;gap:32px">
<div style="display:flex;align-items:center;gap:8px">
<div style="width:28px;height:28px;background:linear-gradient(135deg,#2C3E6B,#3D5A99);border-radius:6px"></div>
<span style="font-weight:700;font-size:16px;color:#2C3E6B;letter-spacing:.5px">CraftLabs</span>
</div>
<nav style="display:flex;gap:0">
<div v-for="item in topNav" :key="item" :class="['nav-item', { active: item===activeTopNav }]" @click="activeTopNav=item" style="padding:0 16px;height:60px;line-height:60px;cursor:pointer;color:#606266;font-size:14px;position:relative;transition:all .2s"
:style="item===activeTopNav?'color:#2C3E6B;font-weight:600':''">
{{ item }}
<div v-if="item===activeTopNav" style="position:absolute;bottom:0;left:16px;right:16px;height:2px;background:#2C3E6B;border-radius:1px 1px 0 0"></div>
</div>
</nav>
<nav style="display:flex;gap:0"><div v-for="n in ['授权平台','运营分析','系统设置']" :key="n" :style="{padding:'0 16px',height:'60px',lineHeight:'60px',cursor:'pointer',color:n===activeTopNav?'#2C3E6B':'#606266',fontSize:'14px',fontWeight:n===activeTopNav?600:400,borderBottom:n===activeTopNav?'2px solid #2C3E6B':'none'}" @click="activeTopNav=n">{{ n }}</div></nav>
</div>
<div style="margin-left:auto;display:flex;align-items:center;gap:12px">
<div style="display:flex;align-items:center;border:1px solid #E0E3E8;border-radius:6px;padding:4px 10px;gap:6px;width:220px;background:#F8F9FB">
<span style="color:#C0C4CC;font-size:14px">🔍</span>
<input v-model="globalSearch" placeholder="搜索许可证 / 客户 / 合同..." style="border:none;outline:none;flex:1;font-size:13px;background:transparent;color:#303133">
</div>
<div v-for="icon in ['🌐','🔔','⚙️']" :key="icon" style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:6px;cursor:pointer;font-size:14px;position:relative"
@mouseenter="hoverIcon=icon" @mouseleave="hoverIcon=''" :style="{background:hoverIcon===icon?'#F2F5FC':''}">
{{ icon }}
<span v-if="icon==='🔔'" style="position:absolute;top:4px;right:6px;width:16px;height:16px;background:#D54941;border-radius:50%;font-size:10px;color:#fff;line-height:16px;text-align:center">3</span>
</div>
<div style="display:flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;cursor:pointer" @mouseenter="hoverUser=true" @mouseleave="hoverUser=false" :style="{background:hoverUser?'#F2F5FC':''}">
<div style="width:28px;height:28px;border-radius:50%;background:#2C3E6B;color:#fff;text-align:center;line-height:28px;font-size:12px;font-weight:600"></div>
<span style="color:#303133;font-weight:500">huangping</span>
</div>
<div style="display:flex;align-items:center;border:1px solid #E0E3E8;border-radius:6px;padding:4px 10px;gap:6px;width:220px;background:#F8F9FB"><span style="color:#C0C4CC;font-size:14px">🔍</span><input placeholder="搜索..." style="border:none;outline:none;flex:1;font-size:13px;background:transparent;color:#303133"></div>
<div v-for="icon in ['🌐','🔔','⚙️']" :key="icon" style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;border-radius:6px;cursor:pointer;position:relative"><span style="font-size:14px">{{ icon }}</span><span v-if="icon==='🔔'" style="position:absolute;top:4px;right:6px;width:16px;height:16px;background:#D54941;border-radius:50%;font-size:10px;color:#fff;line-height:16px;text-align:center">3</span></div>
<div style="display:flex;align-items:center;gap:6px;padding:4px 10px;border-radius:6px;cursor:pointer"><div style="width:28px;height:28px;border-radius:50%;background:#2C3E6B;color:#fff;text-align:center;line-height:28px;font-size:12px;font-weight:600"></div><span style="color:#303133;font-weight:500">huangping</span></div>
</div>
</header>
<!-- ========== BODY ========== -->
<div style="flex:1;display:flex;overflow:hidden;background:#EAEFFA">
<!-- SIDEBAR 232px -->
<!-- ====== SIDEBAR 232px ====== -->
<aside style="width:232px;background:#fff;border-right:1px solid #E8ECF1;overflow-y:auto;flex-shrink:0;display:flex;flex-direction:column;padding-top:8px">
<div style="padding:4px 20px 12px;font-size:12px;color:#C0C4CC;text-transform:uppercase;letter-spacing:1px;font-weight:600">授权运营</div>
<div v-for="group in sidebarGroups" :key="group.label" style="margin-bottom:4px">
<div style="padding:4px 20px;font-size:11px;color:#C0C4CC;text-transform:uppercase;font-weight:600" v-if="group.label">{{ group.label }}</div>
<div v-for="item in group.items" :key="item.key" :class="['menu-item', { active: item.key===activeModule }]" @click="activeModule=item.key"
:style="{display:'flex',alignItems:'center',gap:10,padding:'8px 20px',cursor:'pointer',fontSize:'14px',transition:'all .15s',
color:item.key===activeModule?'#2C3E6B':'#606266',
background:item.key===activeModule?'#F2F5FC':'',
borderRight:item.key===activeModule?'3px solid #2C3E6B':'3px solid transparent',
fontWeight:item.key===activeModule?600:400}">
<span style="font-size:14px;width:20px;text-align:center">{{ item.icon }}</span>
<span>{{ item.name }}</span>
<div v-for="g in sidebarGroups" :key="g.label" style="margin-bottom:4px">
<div v-if="g.label" style="padding:4px 20px;font-size:11px;color:#C0C4CC;text-transform:uppercase;font-weight:600">{{ g.label }}</div>
<div v-for="item in g.items" :key="item.key" @click="activeModule=item.key;closeAllDialogs()" :style="{display:'flex',alignItems:'center',gap:10,padding:'8px 20px',cursor:'pointer',fontSize:'14px',color:item.key===activeModule?'#2C3E6B':'#606266',background:item.key===activeModule?'#F2F5FC':'',borderRight:item.key===activeModule?'3px solid #2C3E6B':'3px solid transparent',fontWeight:item.key===activeModule?600:400}">
<span style="width:20px;text-align:center">{{ item.icon }}</span><span>{{ item.name }}</span>
<span v-if="item.badge" style="margin-left:auto;background:#D54941;color:#fff;font-size:10px;padding:1px 6px;border-radius:10px;font-weight:600">{{ item.badge }}</span>
</div>
</div>
<div style="margin-top:auto;padding:12px 20px;border-top:1px solid #F2F5FC;font-size:11px;color:#C0C4CC">CraftLabs Platform v0.1.0</div>
<div style="margin-top:auto;padding:12px 20px;border-top:1px solid #F2F5FC;font-size:11px;color:#C0C4CC">CraftLabs v0.1.0</div>
</aside>
<!-- CONTENT -->
<div style="flex:1;overflow:hidden;display:flex;flex-direction:column">
<!-- Breadcrumb 46px -->
<div style="height:46px;background:#fff;border-bottom:1px solid #E8ECF1;display:flex;align-items:center;padding:0 20px;gap:6px;font-size:13px;flex-shrink:0">
<span style="color:#909399">授权运营</span>
<span style="color:#C0C4CC"></span>
<span style="color:#2C3E6B;font-weight:600">{{ activePageName }}</span>
<span style="color:#909399">授权运营</span><span style="color:#C0C4CC"></span><span style="color:#2C3E6B;font-weight:600">{{ pageNames[activeModule] }}</span>
</div>
<!-- Main area with optional tree -->
<div style="flex:1;display:flex;overflow:hidden">
<!-- Tree Panel 280px (shown for modules with tree) -->
<div v-if="showTree" style="width:280px;background:#fff;border-right:1px solid #E8ECF1;overflow-y:auto;flex-shrink:0;padding:12px 0">
<div style="padding:0 16px 10px">
<div style="display:flex;align-items:center;border:1px solid #E0E3E8;border-radius:6px;padding:6px 10px;gap:6px;background:#F8F9FB">
<span style="color:#C0C4CC;font-size:12px">🔍</span>
<input placeholder="搜索..." style="border:none;outline:none;flex:1;font-size:12px;background:transparent">
<div style="padding:0 16px 10px"><div style="display:flex;align-items:center;border:1px solid #E0E3E8;border-radius:6px;padding:6px 10px;gap:6px;background:#F8F9FB"><span style="color:#C0C4CC;font-size:12px">🔍</span><input placeholder="搜索..." style="border:none;outline:none;flex:1;font-size:12px;background:transparent"></div></div>
<div v-for="t in treeNodes" :key="t.label" @click="treeNodes.forEach(n=>n.active=false);t.active=true;t.expanded=!t.expanded" :style="{padding:'5px 16px 5px 20px',cursor:'pointer',fontSize:'13px',color:t.active?'#2C3E6B':'#606266',fontWeight:t.active?600:400,background:t.active?'#F2F5FC':'',borderRight:t.active?'3px solid #2C3E6B':'none',display:'flex',alignItems:'center',gap:6}">
<span style="font-size:12px;width:16px;text-align:center">{{ t.expanded?'▾':'▸' }}</span><span>{{ t.icon }}</span><span>{{ t.label }}</span><span v-if="t.count" style="margin-left:auto;font-size:11px;color:#C0C4CC">{{ t.count }}</span>
</div>
</div>
<div v-for="t in treeNodes" :key="t.label" style="padding:5px 16px 5px 20px;cursor:pointer;font-size:13px;color:#606266;display:flex;align-items:center;gap:6px;transition:all .1s"
:style="t.active?{color:'#2C3E6B',fontWeight:600,background:'#F2F5FC',borderRight:'3px solid #2C3E6B'}:{}"
@click="treeNodes.forEach(n=>n.active=false);t.active=true"
@mouseenter="t.hover=true" @mouseleave="t.hover=false"
:class="{'tree-hover': t.hover && !t.active}">
<span style="font-size:12px;width:16px;text-align:center">{{ t.expanded ? '▾' : '▸' }}</span>
<span>{{ t.icon }}</span>
<span>{{ t.label }}</span>
<span v-if="t.count" style="margin-left:auto;font-size:11px;color:#C0C4CC">{{ t.count }}</span>
</div>
</div>
<!-- Main content panel -->
<div style="flex:1;overflow-y:auto;padding:16px 20px">
<!-- Module: Dashboard -->
<!-- ====== DASHBOARD ====== -->
<template v-if="activeModule==='dashboard'">
<div style="display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-bottom:16px">
<div v-for="s in dashboardStats" :key="s.label" class="stat-card" style="background:#fff;border-radius:6px;padding:18px;border:1px solid #E8ECF1;box-shadow:0 1px 2px rgba(0,0,0,.03)">
<div style="font-size:12px;color:#909399;margin-bottom:6px">{{ s.label }}</div>
<div style="font-size:28px;font-weight:700;color:#303133;margin-bottom:4px">{{ s.value }}</div>
<div :style="{fontSize:'12px',color:s.trend>0?'#67C23A':'#F56C6C'}">{{ s.trend > 0 ? '↑' : '↓' }} {{ Math.abs(s.trend) }}% 较上月</div>
</div>
<div v-for="s in dashboardStats" :key="s.label" class="card card-hover" style="padding:18px"><div style="font-size:12px;color:#909399;margin-bottom:6px">{{ s.label }}</div><div style="font-size:28px;font-weight:700;color:#303133;margin-bottom:4px">{{ s.value }}</div><div :style="{fontSize:'12px',color:s.trend>0?'#67C23A':'#F56C6C'}">{{ s.trend>0?'↑':'↓' }} {{ Math.abs(s.trend) }}%</div></div>
</div>
<div style="display:grid;grid-template-columns:2fr 1fr;gap:14px">
<div class="card" style="background:#fff;border-radius:6px;padding:16px;border:1px solid #E8ECF1">
<div style="font-weight:600;color:#303133;margin-bottom:12px">📊 许可证签发趋势</div>
<div style="height:200px;display:flex;align-items:flex-end;gap:12px;padding:0 8px">
<div v-for="(v,i) in [45,52,38,60,55,70,65,80]" :key="i" style="flex:1;background:linear-gradient(180deg,#2C3E6B,#3D5A99);border-radius:4px 4px 0 0" :style="{height:v+'px'}"></div>
</div>
</div>
<div class="card" style="background:#fff;border-radius:6px;padding:16px;border:1px solid #E8ECF1">
<div style="font-weight:600;color:#303133;margin-bottom:12px"> 待处理事项</div>
<div v-for="a in alerts" :key="a.text" style="padding:8px 0;border-bottom:1px solid #F2F5FC;display:flex;align-items:center;gap:8px">
<span :style="{width:6,height:6,borderRadius:'50%',background:a.color}"></span>
<span style="flex:1;color:#606266">{{ a.text }}</span>
<span style="font-size:11px;color:#C0C4CC">{{ a.time }}</span>
</div>
</div>
<div class="card" style="padding:16px"><div style="font-weight:600;color:#303133;margin-bottom:12px">📊 许可证签发趋势</div><div style="height:200px;display:flex;align-items:flex-end;gap:12px;padding:0 8px"><div v-for="(v,i) in [45,52,38,60,55,70,65,80]" :key="i" style="flex:1;background:linear-gradient(180deg,#2C3E6B,#3D5A99);border-radius:4px 4px 0 0" :style="{height:v+'px'}"></div></div></div>
<div class="card" style="padding:16px"><div style="font-weight:600;color:#303133;margin-bottom:12px"> 待处理</div><div v-for="a in alerts" :key="a.text" style="padding:8px 0;border-bottom:1px solid #F2F5FC;display:flex;align-items:center;gap:8px"><span :style="{width:6,height:6,borderRadius:'50%',background:a.color}"></span><span style="flex:1;color:#606266;font-size:12px">{{ a.text }}</span><span style="font-size:11px;color:#C0C4CC">{{ a.time }}</span></div></div>
</div>
</template>
<!-- Module: List + Table (used for most CRUD pages) -->
<template v-else-if="activeModule==='customers'||activeModule==='contracts'||activeModule==='deliveries'||activeModule==='license-sns'">
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;margin-bottom:12px">
<div style="padding:12px 16px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px">
<!-- ====== LIST PAGES (customers/contracts/deliveries/license-sns) ====== -->
<template v-if="showListPage">
<div class="card" style="padding:12px 16px;margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<div style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;display:flex;align-items:center;gap:6px;background:#F8F9FB;width:200px">
<span style="color:#C0C4CC;font-size:12px">🔍</span>
<input :placeholder="'搜索'+activePageName+'...'" style="border:none;outline:none;flex:1;font-size:13px;background:transparent">
<div style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;display:flex;align-items:center;gap:6px;background:#F8F9FB;width:200px"><span style="color:#C0C4CC;font-size:12px">🔍</span><input placeholder="搜索..." style="border:none;outline:none;flex:1;font-size:13px;background:transparent"></div>
<select style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff"><option>全部状态</option></select>
<button class="btn-primary" style="padding:6px 14px;font-size:13px" @click="openViewList">查询</button>
</div>
<select style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff">
<option>全部状态</option><option>活跃</option><option>已吊销</option>
</select>
<button style="border:none;background:#2C3E6B;color:#fff;padding:6px 14px;border-radius:4px;font-size:13px;cursor:pointer;font-weight:500">查询</button>
<button class="btn-primary" style="padding:7px 16px;font-size:13px;box-shadow:0 2px 6px rgba(44,62,107,.2)" @click="openDialog('create')"> 新建{{ pageNames[activeModule] }}</button>
</div>
<button style="border:none;background:#2C3E6B;color:#fff;padding:7px 16px;border-radius:4px;font-size:13px;cursor:pointer;font-weight:500;box-shadow:0 2px 6px rgba(44,62,107,.2)"> 新建{{ activePageName }}</button>
</div>
</div>
<!-- Table -->
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;overflow:hidden">
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead><tr style="background:#F2F5FC;color:#2C3E6B;font-weight:600"><th style="padding:10px 14px;text-align:left;font-size:12px">ID/编号</th><th style="padding:10px 14px;text-align:left;font-size:12px">名称/标题</th><th style="padding:10px 14px;text-align:left;font-size:12px">关联</th><th style="padding:10px 14px;text-align:left;font-size:12px">状态</th><th style="padding:10px 14px;text-align:left;font-size:12px">更新时间</th><th style="padding:10px 14px;text-align:left;font-size:12px;width:120px">操作</th></tr></thead>
<tbody>
<tr v-for="i in 5" :key="i" style="border-bottom:1px solid #F2F5FC" @mouseenter="rowHover=i" @mouseleave="rowHover=0" :style="{background:rowHover===i?'#FAFBFC':''}">
<td style="padding:10px 14px;font-family:monospace;color:#303133;font-size:12px">{{ 'CT-2026-0'+((i-1)*2+1).toString().padStart(3,'0') }}</td>
<td style="padding:10px 14px;color:#303133;font-weight:500">{{ mockNames[i-1] }}</td>
<td style="padding:10px 14px;color:#606266">{{ mockRefs[i-1] }}</td>
<td style="padding:10px 14px"><span :style="{display:'inline-block',padding:'2px 8px',borderRadius:'3px',fontSize:'11px',fontWeight:500,background:i===3?'#fef0f0':'#E6F7EE',color:i===3?'#F56C6C':'#1A7A3A',border:'1px solid '+(i===3?'#fbc4c4':'#A8E6C1')}">{{ i===3?'已吊销':'活跃' }}</span></td>
<td style="padding:10px 14px;color:#909399;font-size:12px">2026-05-{{ (15-i*2).toString().padStart(2,'0') }}</td>
<td style="padding:10px 14px"><span style="color:#2C3E6B;cursor:pointer;margin-right:12px">详情</span><span style="color:#F56C6C;cursor:pointer">吊销</span></td>
</tr>
</tbody>
<div class="card" style="overflow:hidden">
<table class="dt"><thead><tr><th>ID/编号</th><th>名称</th><th>关联</th><th>状态</th><th>时间</th><th style="width:120px">操作</th></tr></thead>
<tbody><tr v-for="i in 5" :key="i" style="border-bottom:1px solid #F2F5FC"><td style="font-family:monospace;font-size:12px;color:#303133">{{ 'CT-2026-0'+((i-1)*2+1).toString().padStart(3,'0') }}</td><td style="font-weight:500;color:#303133">{{ names[i-1] }}</td><td style="color:#606266">{{ refs[i-1] }}</td><td><span class="tag" :style="tagStyle(i===3?'revoked':'active')">{{ i===3?'已吊销':'活跃' }}</span></td><td style="color:#909399;font-size:12px">2026-05-{{ (15-i*2).toString().padStart(2,'0') }}</td><td><span class="lnk" @click="openDialog('detail',names[i-1])">详情</span><span v-if="i!==3" class="lnk" style="color:#F56C6C" @click="openDialog('confirm',names[i-1])">删除</span></td></tr></tbody>
</table>
<div style="padding:10px 16px;display:flex;justify-content:flex-end;align-items:center;gap:8px;font-size:12px;color:#909399;border-top:1px solid #F2F5FC">
<span> 128 </span><span style="cursor:pointer;color:#606266;font-weight:600"></span><span style="color:#2C3E6B;font-weight:600">1</span><span style="cursor:pointer;color:#606266">2</span><span></span><span style="cursor:pointer;color:#606266">13</span><span style="cursor:pointer;color:#606266;font-weight:600"></span>
</div>
<div style="padding:10px 16px;display:flex;justify-content:flex-end;gap:8px;font-size:12px;color:#909399;border-top:1px solid #F2F5FC"><span> 128 </span><span class="pc"></span><span class="pc" style="color:#2C3E6B;font-weight:600">1</span><span class="pc">2</span><span></span><span class="pc">13</span><span class="pc"></span></div>
</div>
</template>
<!-- Module: License Management (NEW - selfhosted key page) -->
<template v-else-if="activeModule==='licenses'">
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;margin-bottom:12px">
<div style="padding:12px 16px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px">
<!-- ====== LICENSE MANAGEMENT ====== -->
<template v-if="activeModule==='licenses'">
<div class="card" style="padding:12px 16px;margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:10px">
<div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<div style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;display:flex;align-items:center;gap:6px;background:#F8F9FB;width:200px">
<span style="color:#C0C4CC;font-size:12px">🔍</span><input placeholder="搜索许可证ID..." style="border:none;outline:none;flex:1;font-size:13px;background:transparent">
<div style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;display:flex;align-items:center;gap:6px;background:#F8F9FB;width:200px"><span style="color:#C0C4CC;font-size:12px">🔍</span><input placeholder="搜索许可证ID..." style="border:none;outline:none;flex:1;font-size:13px;background:transparent"></div>
<select style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff"><option>全部类型</option><option>订阅</option><option>永久</option><option>试用</option></select>
<select style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff"><option>全部状态</option></select>
<button class="btn-primary" style="padding:6px 14px;font-size:13px">查询</button>
</div>
<select style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff"><option>全部授权类型</option><option>订阅</option><option>永久</option><option>试用</option></select>
<select style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff"><option>全部状态</option><option>活跃</option><option>已吊销</option><option>已过期</option></select>
<button style="border:none;background:#2C3E6B;color:#fff;padding:6px 14px;border-radius:4px;font-size:13px;cursor:pointer;font-weight:500">查询</button>
<button class="btn-primary" style="padding:7px 16px;font-size:13px;box-shadow:0 2px 6px rgba(44,62,107,.2)" @click="openDialog('issue')"> 签发许可证</button>
</div>
<button style="border:none;background:#2C3E6B;color:#fff;padding:7px 16px;border-radius:4px;font-size:13px;cursor:pointer;font-weight:500;box-shadow:0 2px 6px rgba(44,62,107,.2)"> 签发许可证</button>
</div>
</div>
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;overflow:hidden">
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead><tr style="background:#F2F5FC;color:#2C3E6B;font-weight:600">
<th style="padding:10px 14px;text-align:left;font-size:12px">许可证 ID</th><th style="padding:10px 14px;text-align:left;font-size:12px">租户</th><th style="padding:10px 14px;text-align:left;font-size:12px">产品</th><th style="padding:10px 14px;text-align:left;font-size:12px">类型</th><th style="padding:10px 14px;text-align:left;font-size:12px">终端</th><th style="padding:10px 14px;text-align:left;font-size:12px">宽限</th><th style="padding:10px 14px;text-align:left;font-size:12px">状态</th><th style="padding:10px 14px;text-align:left;font-size:12px">签发时间</th><th style="padding:10px 14px;text-align:left;font-size:12px;width:120px">操作</th>
</tr></thead>
<tbody>
<tr v-for="l in licenseData" :key="l.id" style="border-bottom:1px solid #F2F5FC">
<td style="padding:10px 14px;font-family:monospace;color:#303133;font-size:12px">{{ l.id }}</td>
<td style="padding:10px 14px;color:#303133">{{ l.tenant }}</td>
<td style="padding:10px 14px;color:#606266">{{ l.product }}</td>
<td style="padding:10px 14px">{{ l.type }}</td>
<td style="padding:10px 14px;color:#606266;text-align:center">{{ l.devices }}</td>
<td style="padding:10px 14px;color:#606266;text-align:center">{{ l.grace }}</td>
<td style="padding:10px 14px"><span :style="{display:'inline-block',padding:'2px 8px',borderRadius:'3px',fontSize:'11px',fontWeight:500,background:l.status==='active'?'#E6F7EE':l.status==='revoked'?'#fef0f0':'#f4f4f5',color:l.status==='active'?'#1A7A3A':l.status==='revoked'?'#F56C6C':'#909399',border:'1px solid '+(l.status==='active'?'#A8E6C1':l.status==='revoked'?'#fbc4c4':'#e9e9eb')}">{{ {active:'活跃',revoked:'已吊销',expired:'已过期'}[l.status] }}</span></td>
<td style="padding:10px 14px;color:#909399;font-size:12px">{{ l.issued }}</td>
<td style="padding:10px 14px"><span style="color:#2C3E6B;cursor:pointer;margin-right:12px">详情</span><span v-if="l.status==='active'" style="color:#F56C6C;cursor:pointer">吊销</span></td>
</tr>
</tbody>
<div class="card" style="overflow:hidden">
<table class="dt"><thead><tr><th>许可证 ID</th><th>租户</th><th>产品</th><th>类型</th><th>终端</th><th>宽限</th><th>状态</th><th>签发时间</th><th style="width:120px">操作</th></tr></thead>
<tbody><tr v-for="l in licenseData" :key="l.id" style="border-bottom:1px solid #F2F5FC"><td style="font-family:monospace;font-size:12px;color:#303133">{{ l.id }}</td><td style="color:#303133">{{ l.tenant }}</td><td style="color:#606266">{{ l.product }}</td><td>{{ l.type }}</td><td style="color:#606266;text-align:center">{{ l.devices }}</td><td style="color:#606266;text-align:center">{{ l.grace }}天</td><td><span :class="'tag tag-'+l.status">{{ statusLabels[l.status] }}</span></td><td style="color:#909399;font-size:12px">{{ l.issued }}</td><td><span class="lnk" @click="openDialog('detail-license',l)">详情</span><span v-if="l.status==='active'" class="lnk" style="color:#F56C6C" @click="openDialog('revoke',l)">吊销</span></td></tr></tbody>
</table>
<div style="padding:10px 16px;display:flex;justify-content:flex-end;align-items:center;gap:8px;font-size:12px;color:#909399;border-top:1px solid #F2F5FC">
<span> 47 </span><span style="cursor:pointer;color:#606266;font-weight:600"></span><span style="color:#2C3E6B;font-weight:600">1</span><span style="cursor:pointer;color:#606266">2</span><span></span><span style="cursor:pointer;color:#606266">5</span><span style="cursor:pointer;color:#606266;font-weight:600"></span>
</div>
<div style="padding:10px 16px;display:flex;justify-content:flex-end;gap:8px;font-size:12px;color:#909399;border-top:1px solid #F2F5FC"><span> 47 </span><span class="pc"></span><span class="pc" style="color:#2C3E6B;font-weight:600">1</span><span class="pc">2</span><span></span><span class="pc">5</span><span class="pc"></span></div>
</div>
</template>
<!-- Module: Callback Inbox -->
<template v-else-if="activeModule==='callbacks'">
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;margin-bottom:12px">
<div style="padding:12px 16px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<select v-for="f in callbackFilters" :key="f.label" style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff"><option>{{ f.label }}</option></select>
<button style="border:none;background:#2C3E6B;color:#fff;padding:6px 14px;border-radius:4px;font-size:13px;cursor:pointer;font-weight:500">查询</button>
<!-- ====== CALLBACKS ====== -->
<template v-if="activeModule==='callbacks'">
<div class="card" style="padding:12px 16px;margin-bottom:12px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<select style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff"><option>全部状态</option></select>
<select style="border:1px solid #E0E3E8;border-radius:4px;padding:5px 10px;font-size:13px;color:#606266;background:#fff"><option>全部事件</option></select>
<button class="btn-primary" style="padding:6px 14px;font-size:13px">查询</button>
</div>
</div>
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;overflow:hidden">
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead><tr style="background:#F2F5FC;color:#2C3E6B;font-weight:600"><th style="padding:10px 14px;text-align:left;font-size:12px">来源</th><th style="padding:10px 14px;text-align:left;font-size:12px">事件类型</th><th style="padding:10px 14px;text-align:left;font-size:12px">SN</th><th style="padding:10px 14px;text-align:left;font-size:12px">状态</th><th style="padding:10px 14px;text-align:left;font-size:12px">收件时间</th><th style="padding:10px 14px;text-align:left;font-size:12px;width:120px">操作</th></tr></thead>
<tbody>
<tr v-for="c in callbackData" :key="c.id" style="border-bottom:1px solid #F2F5FC">
<td style="padding:10px 14px;color:#303133">{{ c.source }}</td><td style="padding:10px 14px;font-family:monospace;font-size:12px;color:#606266">{{ c.event }}</td>
<td style="padding:10px 14px;font-family:monospace;font-size:12px;color:#303133">{{ c.sn }}</td>
<td style="padding:10px 14px"><span :style="{display:'inline-block',padding:'2px 8px',borderRadius:'3px',fontSize:'11px',fontWeight:500,background:c.status==='PENDING'?'#FDF6EC':c.status==='PROCESSED'?'#E6F7EE':'#f4f4f5',color:c.status==='PENDING'?'#E6A23C':c.status==='PROCESSED'?'#1A7A3A':'#909399'}">{{ {PENDING:'待处理',PROCESSED:'已处理',IGNORED:'忽略'}[c.status] }}</span></td>
<td style="padding:10px 14px;color:#909399;font-size:12px">{{ c.time }}</td>
<td style="padding:10px 14px"><span style="color:#2C3E6B;cursor:pointer">详情</span></td>
</tr>
</tbody>
<div class="card" style="overflow:hidden">
<table class="dt"><thead><tr><th>来源</th><th>事件类型</th><th>SN</th><th>状态</th><th>收件时间</th><th style="width:100px">操作</th></tr></thead>
<tbody><tr v-for="c in callbackData" :key="c.id" style="border-bottom:1px solid #F2F5FC"><td style="color:#303133">{{ c.source }}</td><td style="font-family:monospace;font-size:12px;color:#606266">{{ c.event }}</td><td style="font-family:monospace;font-size:12px;color:#303133">{{ c.sn }}</td><td><span :class="'tag tag-'+c.status.toLowerCase()">{{ c.status==='PENDING'?'待处理':c.status==='PROCESSED'?'已处理':'忽略' }}</span></td><td style="color:#909399;font-size:12px">{{ c.time }}</td><td><span class="lnk" @click="openDialog('detail-callback',c)">详情</span></td></tr></tbody>
</table>
</div>
</template>
<!-- Module: Integration -->
<template v-else-if="activeModule==='integration-envs'||activeModule==='integration-plines'">
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;overflow:hidden">
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead><tr style="background:#F2F5FC;color:#2C3E6B;font-weight:600"><th style="padding:10px 14px;text-align:left;font-size:12px">名称</th><th style="padding:10px 14px;text-align:left;font-size:12px">标识</th><th style="padding:10px 14px;text-align:left;font-size:12px">说明</th><th style="padding:10px 14px;text-align:left;font-size:12px">状态</th></tr></thead>
<tbody>
<tr v-for="i in 4" :key="i" style="border-bottom:1px solid #F2F5FC"><td style="padding:10px 14px;color:#303133;font-weight:500">{{ ['生产环境','预发布','测试环境','开发环境'][i-1] }}</td><td style="padding:10px 14px;font-family:monospace;font-size:12px;color:#606266">{{ ['prod','staging','test','dev'][i-1] }}</td><td style="padding:10px 14px;color:#909399">—</td><td style="padding:10px 14px"><span :style="{display:'inline-block',padding:'2px 8px',borderRadius:'3px',fontSize:'11px',fontWeight:500,background:i===1?'#E6F7EE':'#f4f4f5',color:i===1?'#1A7A3A':'#909399'}">{{ i===1?'启用':'维护中' }}</span></td></tr>
</tbody>
</table>
</div>
<!-- ====== INTEGRATION ====== -->
<template v-if="activeModule==='integration-envs'||activeModule==='integration-plines'">
<div class="card" style="overflow:hidden"><table class="dt"><thead><tr><th>名称</th><th>标识</th><th>说明</th><th>状态</th></tr></thead>
<tbody><tr v-for="i in 4" :key="i" style="border-bottom:1px solid #F2F5FC"><td style="font-weight:500;color:#303133">{{ ['生产环境','预发布','测试环境','开发环境'][i-1] }}</td><td style="font-family:monospace;font-size:12px;color:#606266">{{ ['prod','staging','test','dev'][i-1] }}</td><td style="color:#909399">—</td><td><span :class="'tag tag-'+(i===1?'active':'inactive')">{{ i===1?'启用':'维护中' }}</span></td></tr></tbody></table></div>
</template>
<!-- Module: Settings -->
<template v-else-if="activeModule==='settings-keys'">
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;margin-bottom:12px;display:flex;align-items:center;justify-content:space-between;padding:12px 16px">
<span style="font-weight:600;color:#303133">RSA 密钥对管理</span>
<button style="border:none;background:#2C3E6B;color:#fff;padding:7px 16px;border-radius:4px;font-size:13px;cursor:pointer;font-weight:500"> 生成密钥对</button>
</div>
<div class="card" style="background:#fff;border-radius:6px;border:1px solid #E8ECF1;overflow:hidden">
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead><tr style="background:#F2F5FC;color:#2C3E6B;font-weight:600"><th style="padding:10px 14px;text-align:left;font-size:12px">Key ID</th><th style="padding:10px 14px;text-align:left;font-size:12px">算法</th><th style="padding:10px 14px;text-align:left;font-size:12px">状态</th><th style="padding:10px 14px;text-align:left;font-size:12px">创建时间</th><th style="padding:10px 14px;text-align:left;font-size:12px;width:120px">操作</th></tr></thead>
<tbody>
<tr v-for="k in keyData" :key="k.id" style="border-bottom:1px solid #F2F5FC"><td style="padding:10px 14px;font-family:monospace;font-size:12px;color:#303133">{{ k.id }}</td><td style="padding:10px 14px;color:#606266">{{ k.algo }}</td><td style="padding:10px 14px"><span :style="{display:'inline-block',padding:'2px 8px',borderRadius:'3px',fontSize:'11px',fontWeight:500,background:k.status==='active'?'#E6F7EE':'#f4f4f5',color:k.status==='active'?'#1A7A3A':'#909399'}">{{ k.status==='active'?'活跃':'已轮换' }}</span></td><td style="padding:10px 14px;color:#909399;font-size:12px">{{ k.time }}</td><td style="padding:10px 14px"><span style="color:#2C3E6B;cursor:pointer;margin-right:12px">查看公钥</span><span v-if="k.status==='active'" style="color:#E6A23C;cursor:pointer">轮换</span></td></tr>
</tbody>
</table>
</div>
<!-- ====== SETTINGS - KEYS ====== -->
<template v-if="activeModule==='settings-keys'">
<div class="card" style="padding:12px 16px;margin-bottom:12px;display:flex;justify-content:space-between;align-items:center"><span style="font-weight:600;color:#303133">RSA 密钥对管理</span><button class="btn-primary" style="padding:7px 16px;font-size:13px"> 生成密钥对</button></div>
<div class="card" style="overflow:hidden"><table class="dt"><thead><tr><th>Key ID</th><th>算法</th><th>状态</th><th>创建时间</th><th style="width:120px">操作</th></tr></thead>
<tbody><tr v-for="k in keyData" :key="k.id" style="border-bottom:1px solid #F2F5FC"><td style="font-family:monospace;font-size:12px;color:#303133">{{ k.id }}</td><td style="color:#606266">{{ k.algo }}</td><td><span :class="'tag tag-'+(k.status==='active'?'active':'inactive')">{{ k.status==='active'?'活跃':'已轮换' }}</span></td><td style="color:#909399;font-size:12px">{{ k.time }}</td><td><span class="lnk">查看公钥</span><span v-if="k.status==='active'" class="lnk" style="color:#E6A23C">轮换</span></td></tr></tbody></table></div>
</template>
</div>
</div>
</div>
</div>
<!-- ====== DIALOG OVERLAY ====== -->
<div v-if="dialogType" style="position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:center;justify-content:center" @click.self="closeAllDialogs()">
<!-- ISSUE LICENSE DIALOG 520px -->
<div v-if="dialogType==='issue'" class="dlg" style="width:560px;max-height:85vh;overflow-y:auto">
<div class="dlg-header"><span style="font-size:16px;font-weight:600;color:#303133">签发许可证</span><span class="dlg-close" @click="closeAllDialogs()"></span></div>
<div class="dlg-body">
<div class="form-g"><label class="form-l">租户ID <span style="color:#F56C6C">*</span></label><input class="form-i" placeholder="如 craftlabs-wharf-prod"></div>
<div class="form-g"><label class="form-l">产品名称</label><input class="form-i" placeholder="如 wharf-inspection-v2"></div>
<div class="form-g" style="display:flex;gap:12px">
<div style="flex:1"><label class="form-l" style="display:block;margin-bottom:4px">授权类型</label>
<select class="form-i" style="width:100%"><option>订阅 (subscription)</option><option>永久 (perpetual)</option><option>试用 (trial)</option></select>
</div>
<div style="flex:1"><label class="form-l" style="display:block;margin-bottom:4px">有效期()</label><input class="form-i" type="number" value="365" style="width:100%"></div>
</div>
<div class="form-g" style="display:flex;gap:12px">
<div style="flex:1"><label class="form-l" style="display:block;margin-bottom:4px">最大终端数</label><input class="form-i" type="number" value="5" style="width:100%"></div>
<div style="flex:1"><label class="form-l" style="display:block;margin-bottom:4px">离线宽限期()</label><input class="form-i" type="number" value="7" style="width:100%"></div>
</div>
<div class="form-g"><label class="form-l" style="display:block;margin-bottom:6px">特性开关</label>
<div style="display:flex;flex-wrap:wrap;gap:10px 20px">
<label v-for="f in ['advanced_analytics','real_time_monitor','api_export','multi_tenant']" :key="f" style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px;color:#606266"><input type="checkbox" checked style="accent-color:#2C3E6B">{{ featureNames[f] }}</label>
</div>
</div>
<div style="background:#F8F9FB;border:1px solid #E8ECF1;border-radius:6px;padding:12px;margin-top:12px">
<div style="font-size:12px;color:#909399;margin-bottom:6px">📐 布局说明</div>
<div style="font-size:12px;color:#606266;line-height:1.8">
<div> 弹窗宽度: <b>560px</b>表单类弹窗标准宽度</div>
<div> 表单项间距: <b>16px</b></div>
<div> 标签宽度: <b>自适应</b>左侧标签 + 右侧控件</div>
<div> Footer: <b>右对齐</b>取消在左/确认在右</div>
</div>
</div>
</div>
<div class="dlg-footer"><button class="btn-cancel" @click="closeAllDialogs()">取消</button><button class="btn-primary" style="padding:8px 20px;font-size:14px" @click="closeAllDialogs()">签发</button></div>
</div>
<!-- CREATE/EDIT DIALOG 480px -->
<div v-if="dialogType==='create'" class="dlg" style="width:480px">
<div class="dlg-header"><span style="font-size:16px;font-weight:600;color:#303133">新建{{ pageNames[activeModule] }}</span><span class="dlg-close" @click="closeAllDialogs()"></span></div>
<div class="dlg-body">
<div class="form-g"><label class="form-l">名称 <span style="color:#F56C6C">*</span></label><input class="form-i" placeholder="请输入名称"></div>
<div class="form-g"><label class="form-l">备注</label><input class="form-i" placeholder="选填"></div>
<div style="background:#F8F9FB;border:1px solid #E8ECF1;border-radius:6px;padding:12px;margin-top:12px">
<div style="font-size:12px;color:#909399;margin-bottom:6px">📐 布局说明</div>
<div style="font-size:12px;color:#606266;line-height:1.8"><div> 弹窗宽度: <b>480px</b>简单表单</div><div> 与签发弹窗同体系字段少时缩小宽度</div></div>
</div>
</div>
<div class="dlg-footer"><button class="btn-cancel" @click="closeAllDialogs()">取消</button><button class="btn-primary" style="padding:8px 20px;font-size:14px" @click="closeAllDialogs()">保存</button></div>
</div>
<!-- DETAIL DIALOG 480px -->
<div v-if="dialogType==='detail'" class="dlg" style="width:480px">
<div class="dlg-header"><span style="font-size:16px;font-weight:600;color:#303133">详情 - {{ selectedItem }}</span><span class="dlg-close" @click="closeAllDialogs()"></span></div>
<div class="dlg-body">
<div class="detail-row" v-for="r in ['ID/编号','名称','状态','创建时间','关联项目','负责人']" :key="r"><span class="detail-label">{{ r }}</span><span class="detail-value">{{ r==='状态'?'活跃':r==='创建时间'?'2026-05-10':r==='关联项目'?'码头南沙二期':r==='负责人'?'huangping':'' }}</span></div>
<div style="background:#F8F9FB;border:1px solid #E8ECF1;border-radius:6px;padding:12px;margin-top:12px">
<div style="font-size:12px;color:#909399;margin-bottom:6px">📐 布局说明</div>
<div style="font-size:12px;color:#606266;line-height:1.8"><div> 弹窗宽度: <b>480px</b>只读详情</div><div> 布局: <b>标签- 双列</b>标签 100px 宽右对齐</div><div> 行间距: <b>12px</b></div></div>
</div>
</div>
<div class="dlg-footer"><button class="btn-primary" style="padding:8px 20px;font-size:14px" @click="closeAllDialogs()">关闭</button></div>
</div>
<!-- DETAIL-LICENSE DIALOG 520px -->
<div v-if="dialogType==='detail-license'" class="dlg" style="width:520px">
<div class="dlg-header"><span style="font-size:16px;font-weight:600;color:#303133">许可证详情</span><span class="dlg-close" @click="closeAllDialogs()"></span></div>
<div class="dlg-body">
<div class="detail-row" v-for="r in licenseDetailRows" :key="r.label"><span class="detail-label">{{ r.label }}</span><span class="detail-value" :style="r.code?{fontFamily:'monospace',fontSize:'12px'}:{}">{{ r.value }}</span></div>
<div style="background:#F2F5FC;border:1px dashed #D6DFF0;border-radius:6px;padding:12px;margin-top:12px">
<div style="font-size:12px;color:#2C3E6B;font-weight:600;margin-bottom:6px">📐 许可证详情布局</div>
<div style="font-size:12px;color:#606266;line-height:1.8"><div> 弹窗宽度: <b>520px</b>含长许可证ID</div><div> 关键字段用 <b>monospace</b> 字体展示许可证ID</div><div> 特性区用 <b>蓝色虚线框</b> 区分于基础信息</div></div>
</div>
</div>
<div class="dlg-footer"><button class="btn-cancel" @click="closeAllDialogs()">关闭</button><button v-if="selectedLicense&&selectedLicense.status==='active'" class="btn-primary" style="padding:8px 20px;font-size:14px;background:#F56C6C" @click="openDialog('revoke',selectedLicense)">吊销</button></div>
</div>
<!-- CONFIRM / REVOKE DIALOG 420px -->
<div v-if="dialogType==='confirm'" class="dlg" style="width:420px;text-align:center">
<div class="dlg-body" style="padding:32px 24px 20px">
<div style="font-size:48px;margin-bottom:16px"></div>
<div style="font-size:16px;font-weight:600;color:#303133;margin-bottom:8px">确认删除</div>
<div style="font-size:13px;color:#909399">确定要删除 <b style="color:#303133">{{ selectedItem }}</b>此操作不可撤销</div>
<div style="background:#F8F9FB;border:1px solid #E8ECF1;border-radius:6px;padding:12px;margin-top:16px;text-align:left">
<div style="font-size:12px;color:#909399;margin-bottom:6px">📐 布局说明</div>
<div style="font-size:12px;color:#606266;line-height:1.8"><div> 弹窗宽度: <b>420px</b>确认类最窄</div><div> 内容: <b>居中</b>图标+标题+说明</div><div> 按钮: 取消在左 / 确认在右</div></div>
</div>
</div>
<div class="dlg-footer" style="justify-content:center;gap:12px"><button class="btn-cancel" @click="closeAllDialogs()">取消</button><button style="border:none;background:#F56C6C;color:#fff;padding:8px 20px;border-radius:4px;font-size:14px;cursor:pointer;font-weight:500" @click="closeAllDialogs()">确认删除</button></div>
</div>
<!-- REVOKE LICENSE DIALOG -->
<div v-if="dialogType==='revoke'" class="dlg" style="width:420px;text-align:center">
<div class="dlg-body" style="padding:32px 24px 20px">
<div style="font-size:48px;margin-bottom:16px">🛑</div>
<div style="font-size:16px;font-weight:600;color:#303133;margin-bottom:8px">确认吊销许可证</div>
<div style="font-size:13px;color:#909399">确定要吊销许可证<br><b style="color:#303133;font-family:monospace">{{ selectedLicense?.id }}</b><br>吊销后该许可证立即失效</div>
<div style="background:#FEF0F0;border:1px solid #FBC4C4;border-radius:6px;padding:12px;margin-top:16px;text-align:left">
<div style="font-size:12px;color:#F56C6C;font-weight:600;margin-bottom:6px">📐 危险操作布局</div>
<div style="font-size:12px;color:#606266;line-height:1.8"><div> 背景: <b>淡红色 #FEF0F0</b> 提示危险</div><div> 确认按钮: <b>红色 #F56C6C</b></div><div> 与普通确认弹窗区分视觉权重</div></div>
</div>
</div>
<div class="dlg-footer" style="justify-content:center;gap:12px"><button class="btn-cancel" @click="closeAllDialogs()">取消</button><button style="border:none;background:#F56C6C;color:#fff;padding:8px 20px;border-radius:4px;font-size:14px;cursor:pointer;font-weight:500" @click="closeAllDialogs()">确认吊销</button></div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import { ref, computed, reactive } from 'vue'
const activeModule = ref('dashboard')
const activeModule = ref('licenses')
const activeTopNav = ref('授权平台')
const globalSearch = ref('')
const hoverIcon = ref('')
const hoverUser = ref(false)
const rowHover = ref(0)
const dialogType = ref('')
const selectedItem = ref('')
const selectedLicense = ref(null)
const topNav = ['授权平台', '运营分析', '系统设置']
const sidebarGroups = [
{ label:'', items:[
{ key:'dashboard', icon:'📊', name:'工作台概览' },
]},
{ label:'业务管理', items:[
{ key:'customers', icon:'👥', name:'客户管理' },
{ key:'contracts', icon:'📋', name:'合同管理' },
{ key:'deliveries', icon:'📦', name:'交付管理' },
{ key:'license-sns', icon:'🔑', name:'许可 SN' },
]},
{ label:'授权运营', items:[
{ key:'licenses', icon:'🛡️', name:'许可证管理', badge:'NEW' },
{ key:'callbacks', icon:'📨', name:'Callback 收件箱', badge:'3' },
]},
{ label:'集成配置', items:[
{ key:'integration-envs', icon:'🌐', name:'集成环境' },
{ key:'integration-plines', icon:'📱', name:'产品线' },
]},
{ label:'系统运维', items:[
{ key:'settings-keys', icon:'🔐', name:'密钥管理' },
]},
]
const modulePageNames = {
dashboard:'工作台概览', customers:'客户管理', contracts:'合同管理',
deliveries:'交付管理', 'license-sns':'许可 SN', licenses:'许可证管理',
callbacks:'Callback 收件箱', 'integration-envs':'集成环境',
'integration-plines':'产品线', 'settings-keys':'密钥管理'
}
const activePageName = computed(() => modulePageNames[activeModule.value] || '—')
const pageNames = { dashboard:'工作台概览', customers:'客户管理', contracts:'合同管理', deliveries:'交付管理', 'license-sns':'许可 SN', licenses:'许可证管理', callbacks:'Callback 收件箱', 'integration-envs':'集成环境', 'integration-plines':'产品线', 'settings-keys':'密钥管理' }
const showListPage = computed(() => ['customers','contracts','deliveries','license-sns'].includes(activeModule.value))
const showTree = computed(() => ['licenses','customers','contracts'].includes(activeModule.value))
const treeNodes = ref([
{ icon:'🏢', label:'创飞码头项目', expanded:true, active:true, count:12 },
{ icon:'🏫', label:'学校合作项目', expanded:false, active:false, count:8 },
{ icon:'🔬', label:'实验室项目', expanded:false, active:false, count:5 },
{ icon:'📐', label:'内部测试', expanded:false, active:false, count:3 },
])
const dashboardStats = [
{ label:'总许可证', value:47, trend:12 },
{ label:'活跃终端', value:183, trend:8 },
{ label:'待处理 Callback', value:3, trend:-25 },
{ label:'本月签发', value:15, trend:20 },
const sidebarGroups = [
{ label:'', items:[{ key:'dashboard', icon:'📊', name:'工作台概览' }] },
{ label:'业务管理', items:[{ key:'customers', icon:'👥', name:'客户管理' },{ key:'contracts', icon:'📋', name:'合同管理' },{ key:'deliveries', icon:'📦', name:'交付管理' },{ key:'license-sns', icon:'🔑', name:'许可 SN' }] },
{ label:'授权运营', items:[{ key:'licenses', icon:'🛡️', name:'许可证管理', badge:'NEW' },{ key:'callbacks', icon:'📨', name:'Callback 收件箱', badge:'3' }] },
{ label:'集成配置', items:[{ key:'integration-envs', icon:'🌐', name:'集成环境' },{ key:'integration-plines', icon:'📱', name:'产品线' }] },
{ label:'系统运维', items:[{ key:'settings-keys', icon:'🔐', name:'密钥管理' }] },
]
const alerts = [
{ text:'许可证 CT-2026-003 即将到期', time:'2h前', color:'#E6A23C' },
{ text:'Callback sn:post_activate 待处理', time:'1h前', color:'#F56C6C' },
{ text:'终端限制已达上限 (码头项目)', time:'3h前', color:'#F56C6C' },
{ text:'新版本 SDK v0.2.0 已发布', time:'6h前', color:'#409EFF' },
]
const opens = (t, item) => {
if (activeModule.value !== 'licenses' && t === 'issue') return
dialogType.value = t
selectedItem.value = typeof item === 'string' ? item : ''
if (t === 'revoke' || t === 'detail-license') selectedLicense.value = item
}
const mockNames = ['广州创飞 · 码头检测合同', '深圳教育局 · 学校合同', '实验室设备授权 v2', '内部测试许可', '流动人口项目 042']
const mockRefs = ['码头南沙二期', '深圳南山校区', '实验室 A 区', '内部', '项目 042']
const treeNodes = ref([{ icon:'🏢', label:'创飞码头项目', expanded:true, active:true, count:12 },{ icon:'🏫', label:'学校合作项目', expanded:false, active:false, count:8 },{ icon:'🔬', label:'实验室项目', expanded:false, active:false, count:5 },{ icon:'📐', label:'内部测试', expanded:false, active:false, count:3 }])
const dashboardStats = [{ label:'总许可证', value:47, trend:12 },{ label:'活跃终端', value:183, trend:8 },{ label:'待处理 Callback', value:3, trend:-25 },{ label:'本月签发', value:15, trend:20 }]
const alerts = [{ text:'许可证 CT-2026-003 即将到期', time:'2h前', color:'#E6A23C' },{ text:'Callback sn:post_activate 待处理', time:'1h前', color:'#F56C6C' },{ text:'终端限制已达上限 (码头项目)', time:'3h前', color:'#F56C6C' },{ text:'新版本 SDK v0.2.0 已发布', time:'6h前', color:'#409EFF' }]
const names = ['广州创飞 · 码头检测合同','深圳教育局 · 学校合同','实验室设备授权 v2','内部测试许可','流动人口项目 042']
const refs = ['码头南沙二期','深圳南山校区','实验室 A 区','内部','项目 042']
const licenseData = [
{ id:'01JQNX...a1b2', tenant:'craftlabs-wharf-prod', product:'wharf-inspection-v2', type:'永久', devices:5, grace:7, status:'active', issued:'2026-05-10' },
@@ -326,21 +271,39 @@ const licenseData = [
{ id:'01JQPB...i9j0', tenant:'internal-test', product:'test-license', type:'永久', devices:1, grace:0, status:'active', issued:'2026-05-15' },
]
const licenseDetailRows = computed(() => {
const l = selectedLicense.value || licenseData[0]
return [
{ label:'许可证 ID', value:l.id, code:true },
{ label:'租户', value:l.tenant },
{ label:'产品', value:l.product },
{ label:'授权类型', value:l.type },
{ label:'最大终端数', value:String(l.devices) },
{ label:'离线宽限期', value:l.grace+'天' },
{ label:'心跳间隔', value:'24小时' },
{ label:'状态', value:l.status==='active'?'活跃':l.status==='revoked'?'已吊销':'已过期' },
{ label:'签发时间', value:l.issued },
]
})
const callbackData = [
{ source:'BitAnswer', event:'sn:post_activate', sn:'SN-2026-A1B2C', status:'PENDING', time:'2026-05-18 14:30' },
{ source:'BitAnswer', event:'device:pre_activate', sn:'SN-2026-D3E4F', status:'PROCESSED', time:'2026-05-18 12:15' },
{ source:'SelfHosted', event:'license:heartbeat', sn:'01JQNX...a1b2', status:'PROCESSED', time:'2026-05-18 10:00' },
{ source:'BitAnswer', event:'sn:post_activate', sn:'SN-2026-G5H6I', status:'IGNORED', time:'2026-05-17 16:45' },
]
const callbackFilters = [
{ label:'全部状态' }, { label:'全部事件类型' }, { label:'全部来源' }
]
const keyData = [{ id:'kp_2026_q2', algo:'RS256', status:'active', time:'2026-04-01' },{ id:'kp_2026_q1', algo:'RS256', status:'rotated', time:'2026-01-05' }]
const keyData = [
{ id:'kp_2026_q2', algo:'RS256', status:'active', time:'2026-04-01' },
{ id:'kp_2026_q1', algo:'RS256', status:'rotated', time:'2026-01-05' },
]
const statusLabels = { active:'活跃', revoked:'已吊销', expired:'已过期' }
const featureNames = { advanced_analytics:'高级分析', real_time_monitor:'实时监控', api_export:'API导出', multi_tenant:'多租户' }
function closeAllDialogs() { dialogType.value=''; selectedItem.value=''; selectedLicense.value=null }
function openDialog(t, item) { opens(t, item) }
function openViewList() {}
function tagStyle(s) {
if (s==='revoked') return { background:'#fef0f0', color:'#F56C6C', border:'1px solid #fbc4c4' }
return { background:'#E6F7EE', color:'#1A7A3A', border:'1px solid #A8E6C1' }
}
</script>
<style>
@@ -348,12 +311,36 @@ const keyData = [
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;overflow:hidden}
input{font-family:inherit}select{font-family:inherit}button{font-family:inherit}
</style>
<style scoped>
.nav-item:hover{color:#2C3E6B}
.menu-item:hover{background:#F2F5FC}
.tree-hover{background:#F2F5FC}
.stat-card{transition:transform .15s,box-shadow .15s}
.stat-card:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.06)}
input::placeholder{color:#C0C4CC}
.card{background:#fff;border-radius:6px;border:1px solid #E8ECF1;box-shadow:0 1px 2px rgba(0,0,0,.03)}
.card-hover{transition:transform .15s,box-shadow .15s}
.card-hover:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,.06)}
.dt{width:100%;border-collapse:collapse;font-size:13px}
.dt th{padding:9px 12px;text-align:left;font-weight:600;font-size:12px;color:#2C3E6B;background:#F2F5FC;border-bottom:1px solid #E8ECF1}
.dt td{padding:9px 12px}
.lnk{color:#2C3E6B;cursor:pointer;margin-right:12px;font-size:13px}
.pc{cursor:pointer;color:#606266}
.tag{display:inline-block;padding:2px 8px;border-radius:3px;font-size:11px;font-weight:500}
.tag-active{background:#E6F7EE;color:#1A7A3A;border:1px solid #A8E6C1}
.tag-revoked,.tag-expired{background:#f4f4f5;color:#909399;border:1px solid #e9e9eb}
.tag-pending{background:#FDF6EC;color:#E6A23C;border:1px solid #F5DAB1}
.tag-processed{background:#E6F7EE;color:#1A7A3A;border:1px solid #A8E6C1}
.tag-inactive{background:#f4f4f5;color:#909399;border:1px solid #e9e9eb}
.btn-primary{border:none;background:#2C3E6B;color:#fff;border-radius:4px;cursor:pointer;font-weight:500}
.btn-cancel{border:1px solid #E0E3E8;background:#fff;color:#606266;padding:8px 20px;border-radius:4px;font-size:14px;cursor:pointer;font-weight:500}
/* Dialog styles */
.dlg{background:#fff;border-radius:8px;box-shadow:0 8px 40px rgba(0,0,0,.15);animation:dlgIn .2s ease-out}
.dlg-header{padding:16px 20px;border-bottom:1px solid #F2F5FC;display:flex;align-items:center;justify-content:space-between}
.dlg-close{width:28px;height:28px;display:flex;align-items:center;justify-content:center;border-radius:4px;cursor:pointer;color:#909399;font-size:16px;transition:all .15s}
.dlg-close:hover{background:#F2F5FC;color:#303133}
.dlg-body{padding:20px}
.dlg-footer{padding:12px 20px 16px;display:flex;justify-content:flex-end;gap:10px;border-top:1px solid #F2F5FC}
.form-g{margin-bottom:16px}
.form-l{display:block;font-size:13px;color:#606266;margin-bottom:4px;font-weight:500}
.form-i{width:100%;border:1px solid #E0E3E8;border-radius:4px;padding:7px 12px;font-size:13px;color:#303133;outline:none;transition:border-color .15s}
.form-i:focus{border-color:#2C3E6B}
.detail-row{display:flex;padding:8px 0;border-bottom:1px solid #F2F5FC}
.detail-label{width:100px;text-align:right;padding-right:16px;font-size:13px;color:#909399;flex-shrink:0}
.detail-value{flex:1;font-size:13px;color:#303133}
@keyframes dlgIn{from{opacity:0;transform:scale(.96) translateY(-10px)}to{opacity:1;transform:scale(1) translateY(0)}}
</style>