536 lines
27 KiB
HTML
536 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=1366" />
|
||
<title>Fonrey 首页设置 · 静态原型</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
primary: {
|
||
50: '#F0FDFA',
|
||
100: '#CCFBF1',
|
||
200: '#99F6E4',
|
||
500: '#14B8A6',
|
||
600: '#0F766E',
|
||
700: '#115E59',
|
||
800: '#134E4A'
|
||
},
|
||
neutral: {
|
||
50: '#F8FAFC',
|
||
100: '#F1F5F9',
|
||
200: '#E2E8F0',
|
||
300: '#CBD5E1',
|
||
400: '#94A3B8',
|
||
500: '#64748B',
|
||
600: '#475569',
|
||
700: '#334155',
|
||
800: '#1E293B',
|
||
900: '#0F172A'
|
||
},
|
||
success: { 50: '#F0FDF4', 600: '#16A34A' },
|
||
warning: { 50: '#FFFBEB', 600: '#D97706' },
|
||
danger: { 50: '#FEF2F2', 600: '#DC2626' },
|
||
info: { 50: '#EFF6FF', 600: '#2563EB' }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<style>
|
||
body { background: #F8FAFC; color: #0F172A; }
|
||
[x-cloak] { display: none !important; }
|
||
.role-tab { color: #64748B; border: 1px solid #E2E8F0; background: #fff; }
|
||
.role-tab.active { color: #115E59; border-color: #115E59; background: #F0FDFA; font-weight: 600; }
|
||
.switch {
|
||
position: relative;
|
||
display: inline-flex;
|
||
width: 38px;
|
||
height: 22px;
|
||
border-radius: 999px;
|
||
background: #CBD5E1;
|
||
transition: background .2s;
|
||
}
|
||
.switch::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 999px;
|
||
top: 2px;
|
||
left: 2px;
|
||
background: #fff;
|
||
transition: transform .2s;
|
||
box-shadow: 0 1px 3px rgba(0,0,0,.2);
|
||
}
|
||
.switch.on { background: #0F766E; }
|
||
.switch.on::after { transform: translateX(16px); }
|
||
</style>
|
||
</head>
|
||
<body class="text-sm antialiased" x-data="homeSettingPage()" x-init="init()">
|
||
<header class="fixed top-0 left-0 right-0 h-14 z-30 bg-primary-800 flex items-center justify-between">
|
||
<div class="flex items-center gap-2 px-4 w-60 shrink-0">
|
||
<div class="w-7 h-7 rounded-md bg-primary-500 flex items-center justify-center text-white text-sm font-semibold">F</div>
|
||
<span class="text-base font-semibold text-white">Fonrey</span>
|
||
</div>
|
||
<nav class="flex items-center gap-1 flex-1 px-2" aria-label="主导航">
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">主页</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">房源</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">客源</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">营销</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">交易</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">数据</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">人事</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md bg-primary-600 text-white font-medium">系统</a>
|
||
<a class="px-3 py-1.5 text-sm rounded-md text-primary-100 hover:bg-primary-700">三网</a>
|
||
</nav>
|
||
<div class="flex items-center gap-1 px-4 shrink-0">
|
||
<button class="p-1.5 text-primary-200 hover:bg-primary-700 rounded-md" aria-label="消息">
|
||
<svg class="w-5 h-5" fill="none" stroke="currentColor" stroke-width="1.8" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022 23.848 23.848 0 0 0 5.454 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"/></svg>
|
||
</button>
|
||
<div class="flex items-center gap-2 pl-3 ml-1 border-l border-primary-700">
|
||
<div class="w-8 h-8 rounded-full bg-primary-600 text-white flex items-center justify-center text-sm font-semibold">杜</div>
|
||
<span class="text-sm font-medium text-primary-100">杜利强</span>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<aside class="fixed left-0 top-14 h-[calc(100vh-56px)] w-60 z-20 border-r border-neutral-200 bg-white overflow-y-auto">
|
||
<nav class="p-3 space-y-0.5">
|
||
<div class="px-2 pt-2 pb-1 text-xs font-medium text-neutral-500 uppercase tracking-wide">设置</div>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md bg-primary-50 text-primary-700 font-medium">首页设置</a>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">房源设置</a>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">新房设置</a>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">客源设置</a>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">交易设置</a>
|
||
<a class="flex items-center gap-2 px-2 py-1.5 rounded-md text-neutral-700 hover:bg-neutral-100">人事OA设置</a>
|
||
</nav>
|
||
</aside>
|
||
|
||
<main class="ml-60 pt-[72px] min-h-screen px-6 py-5">
|
||
<div class="mx-auto max-w-[1760px] space-y-4">
|
||
<section class="bg-white border border-neutral-200 rounded-lg p-4">
|
||
<div class="flex items-start justify-between gap-4">
|
||
<div>
|
||
<nav class="flex items-center gap-1 text-xs text-neutral-500 mb-2" aria-label="面包屑">
|
||
<a href="#" class="hover:text-neutral-700">系统</a>
|
||
<span>/</span>
|
||
<a href="#" class="hover:text-neutral-700">设置</a>
|
||
<span>/</span>
|
||
<span class="text-neutral-900">首页设置</span>
|
||
</nav>
|
||
<h1 class="text-xl font-semibold text-neutral-900">系统配置-首页设置</h1>
|
||
<p class="text-xs text-neutral-500 mt-1">管理员可按角色配置首页展示卡片、排行榜及成交战报视图</p>
|
||
</div>
|
||
<div class="w-[320px]">
|
||
<input type="text" x-model.trim="searchKeyword" placeholder="请输入设置项名称" class="w-full px-3 py-2 rounded-md border border-neutral-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary-600/30" />
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="grid grid-cols-12 gap-4 items-start">
|
||
<section class="col-span-8 space-y-4">
|
||
<section class="bg-white border border-neutral-200 rounded-lg p-4 space-y-4">
|
||
<div class="flex items-center justify-between gap-3">
|
||
<div class="flex items-center gap-2" role="tablist" aria-label="角色视图切换">
|
||
<template x-for="role in roleOptions" :key="role.key">
|
||
<button
|
||
class="role-tab px-3 py-1.5 rounded-md"
|
||
:class="{ 'active': currentRole === role.key }"
|
||
@click="switchRole(role.key)"
|
||
x-text="role.label"
|
||
></button>
|
||
</template>
|
||
</div>
|
||
<div class="flex items-center gap-2">
|
||
<template x-if="!editMode">
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="startEdit">编辑</button>
|
||
</template>
|
||
<template x-if="editMode">
|
||
<div class="flex items-center gap-2">
|
||
<button class="px-3 py-1.5 rounded-md border border-neutral-300 hover:bg-neutral-50" @click="cancelEdit">取消</button>
|
||
<button class="px-3 py-1.5 rounded-md bg-primary-600 text-white hover:bg-primary-700" @click="saveConfig">保存配置</button>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<div x-show="formError" class="rounded-md border border-danger-600 bg-danger-50 text-danger-600 px-3 py-2 text-xs" x-text="formError"></div>
|
||
|
||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('员工信息')">
|
||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">员工信息模块</header>
|
||
<div class="p-4 flex items-center justify-between">
|
||
<div>
|
||
<p class="font-medium">是否展示员工司龄</p>
|
||
<p class="text-xs text-neutral-500 mt-1">若开启则首页展示员工司龄</p>
|
||
</div>
|
||
<button class="switch" :class="draftConfig.show_seniority ? 'on' : ''" @click="toggleBoolean('show_seniority')" :disabled="!editMode"></button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('行程')">
|
||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">行程模块显示指标</header>
|
||
<div class="p-4 space-y-2">
|
||
<label class="inline-flex items-center gap-2 mr-4">
|
||
<input type="checkbox" class="rounded border-neutral-300" :checked="draftConfig.itinerary_metrics.includes('sale')" @change="toggleMetric('sale')" :disabled="!editMode" />
|
||
<span>买卖</span>
|
||
</label>
|
||
<label class="inline-flex items-center gap-2">
|
||
<input type="checkbox" class="rounded border-neutral-300" :checked="draftConfig.itinerary_metrics.includes('rent')" @change="toggleMetric('rent')" :disabled="!editMode" />
|
||
<span>租赁</span>
|
||
</label>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('业绩')">
|
||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">首页业绩显示设置</header>
|
||
<div class="p-4 space-y-3">
|
||
<label class="flex items-start gap-2">
|
||
<input type="radio" name="kpi_mode" value="pending_and_approved" :checked="draftConfig.kpi_mode==='pending_and_approved'" @change="draftConfig.kpi_mode='pending_and_approved'" :disabled="!editMode" class="mt-0.5" />
|
||
<span class="text-xs leading-5">统计审批中和审批通过的转定业绩:录转定/认购后统计审批中、审批驳回和审批通过的业绩,若无转定/认购,直接签约则统计录签约后审批中、审批驳回和审批通过的分成后业绩</span>
|
||
</label>
|
||
<label class="flex items-start gap-2">
|
||
<input type="radio" name="kpi_mode" value="approved_only" :checked="draftConfig.kpi_mode==='approved_only'" @change="draftConfig.kpi_mode='approved_only'" :disabled="!editMode" class="mt-0.5" />
|
||
<span class="text-xs leading-5">仅统计审批通过业绩(严格口径)</span>
|
||
</label>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('统计卡片')">
|
||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">首页统计卡片配置</header>
|
||
<div class="p-4 space-y-2">
|
||
<template x-for="card in sortedCards" :key="card.key">
|
||
<div class="border border-neutral-200 rounded-md px-3 py-2 flex items-center justify-between gap-3 bg-white">
|
||
<div class="flex items-center gap-2">
|
||
<button class="switch" :class="card.enabled ? 'on' : ''" @click="toggleCard(card.key)" :disabled="!editMode"></button>
|
||
<div>
|
||
<p class="font-medium" x-text="card.label"></p>
|
||
<p class="text-xs text-neutral-500" x-text="card.desc"></p>
|
||
</div>
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<button class="px-2 py-1 rounded border border-neutral-300 text-xs" @click="moveCard(card.key,-1)" :disabled="!editMode">上移</button>
|
||
<button class="px-2 py-1 rounded border border-neutral-300 text-xs" @click="moveCard(card.key,1)" :disabled="!editMode">下移</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
<p class="text-xs text-neutral-500 mt-1">至少保留1个启用卡片;卡片顺序将影响首页展示顺序。</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('排行榜')">
|
||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">排行榜设置</header>
|
||
<div class="p-4 grid grid-cols-2 gap-4">
|
||
<div class="space-y-3">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="font-medium">是否显示业绩和单数</p>
|
||
<p class="text-xs text-neutral-500">若开启则排行榜中显示业绩和单数</p>
|
||
</div>
|
||
<button class="switch" :class="draftConfig.ranking.show_performance_and_deal ? 'on' : ''" @click="toggleNested('ranking','show_performance_and_deal')" :disabled="!editMode"></button>
|
||
</div>
|
||
|
||
<label class="block">
|
||
<span class="text-xs text-neutral-500">业绩计算方式</span>
|
||
<select class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300 bg-white" x-model="draftConfig.ranking.calc_mode" :disabled="!editMode">
|
||
<option value="turn_order">统计审批中和审批通过的转定单量和业绩</option>
|
||
<option value="approved_only">仅统计审批通过单量和业绩</option>
|
||
</select>
|
||
</label>
|
||
|
||
<label class="block">
|
||
<span class="text-xs text-neutral-500">默认按店或组排名</span>
|
||
<select class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300 bg-white" x-model="draftConfig.ranking.default_rank_level" :disabled="!editMode">
|
||
<option value="store">门店</option>
|
||
<option value="group">组别</option>
|
||
</select>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="space-y-3">
|
||
<div class="flex items-center justify-between">
|
||
<div>
|
||
<p class="font-medium">默认展示全公司前10排名数据</p>
|
||
<p class="text-xs text-neutral-500">权限为本人/无时仅展示默认排行</p>
|
||
</div>
|
||
<button class="switch" :class="draftConfig.ranking.show_company_top10 ? 'on' : ''" @click="toggleNested('ranking','show_company_top10')" :disabled="!editMode"></button>
|
||
</div>
|
||
|
||
<label class="block">
|
||
<span class="text-xs text-neutral-500">过滤账号</span>
|
||
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" placeholder="无限制" x-model="draftConfig.ranking.filter_accounts" :disabled="!editMode" />
|
||
</label>
|
||
|
||
<label class="block">
|
||
<span class="text-xs text-neutral-500">过滤部门</span>
|
||
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" placeholder="无限制" x-model="draftConfig.ranking.filter_departments" :disabled="!editMode" />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="rounded-lg border border-neutral-200" x-show="matchSection('成交战报')">
|
||
<header class="px-4 py-3 border-b border-neutral-200 bg-neutral-50 font-semibold">成交战报设置</header>
|
||
<div class="p-4 grid grid-cols-2 gap-4">
|
||
<div class="space-y-3">
|
||
<label class="block">
|
||
<span class="text-xs text-neutral-500">成交时间范围</span>
|
||
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" placeholder="不限制" x-model="draftConfig.battle_report.days_range" :disabled="!editMode" />
|
||
</label>
|
||
<label class="block">
|
||
<span class="text-xs text-neutral-500">成交业绩范围</span>
|
||
<input type="text" class="mt-1 w-full px-3 py-2 rounded-md border border-neutral-300" placeholder="不限制" x-model="draftConfig.battle_report.amount_range" :disabled="!editMode" />
|
||
</label>
|
||
</div>
|
||
<div class="space-y-3">
|
||
<div class="flex items-center justify-between">
|
||
<div><p class="font-medium">是否显示业绩</p><p class="text-xs text-neutral-500">若开启则成交战报中显示业绩</p></div>
|
||
<button class="switch" :class="draftConfig.battle_report.show_performance ? 'on' : ''" @click="toggleNested('battle_report','show_performance')" :disabled="!editMode"></button>
|
||
</div>
|
||
<div class="flex items-center justify-between">
|
||
<div><p class="font-medium">是否显示房源</p><p class="text-xs text-neutral-500">若开启则成交战报中显示房源名称</p></div>
|
||
<button class="switch" :class="draftConfig.battle_report.show_property_name ? 'on' : ''" @click="toggleNested('battle_report','show_property_name')" :disabled="!editMode"></button>
|
||
</div>
|
||
<div class="flex items-center justify-between">
|
||
<div><p class="font-medium">是否显示房源总价</p><p class="text-xs text-neutral-500">若开启则成交战报中显示房源总价</p></div>
|
||
<button class="switch" :class="draftConfig.battle_report.show_property_total_price ? 'on' : ''" @click="toggleNested('battle_report','show_property_total_price')" :disabled="!editMode"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
</section>
|
||
|
||
<aside class="col-span-4 space-y-4 sticky top-[84px]">
|
||
<section class="bg-white border border-neutral-200 rounded-lg p-4">
|
||
<h3 class="text-base font-semibold mb-3">首页预览(<span x-text="currentRoleLabel"></span>)</h3>
|
||
<div class="grid grid-cols-2 gap-2">
|
||
<template x-for="card in enabledCards" :key="card.key">
|
||
<div class="rounded-md border border-neutral-200 p-3 bg-neutral-50">
|
||
<p class="text-xs text-neutral-500" x-text="card.label"></p>
|
||
<p class="text-lg font-semibold mt-1" x-text="metricValue(card.key)"></p>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="bg-white border border-neutral-200 rounded-lg p-4 text-xs text-neutral-600 space-y-2">
|
||
<p><span class="font-medium text-neutral-800">员工司龄:</span><span x-text="draftConfig.show_seniority ? '显示' : '隐藏'"></span></p>
|
||
<p><span class="font-medium text-neutral-800">行程指标:</span><span x-text="draftConfig.itinerary_metrics.length ? draftConfig.itinerary_metrics.map(v => v==='sale'?'买卖':'租赁').join('、') : '无' "></span></p>
|
||
<p><span class="font-medium text-neutral-800">排行榜显示业绩/单数:</span><span x-text="draftConfig.ranking.show_performance_and_deal ? '开启' : '关闭'"></span></p>
|
||
<p><span class="font-medium text-neutral-800">成交战报展示房源:</span><span x-text="draftConfig.battle_report.show_property_name ? '开启' : '关闭'"></span></p>
|
||
</section>
|
||
</aside>
|
||
</section>
|
||
</div>
|
||
</main>
|
||
|
||
<div x-cloak x-show="toast.open" x-transition class="fixed right-6 bottom-6 px-4 py-2 rounded-md shadow-lg text-sm bg-neutral-900 text-white" x-text="toast.message"></div>
|
||
|
||
<script>
|
||
function homeSettingPage() {
|
||
return {
|
||
searchKeyword: '',
|
||
editMode: false,
|
||
formError: '',
|
||
currentRole: 'agent',
|
||
roleOptions: [
|
||
{ key: 'agent', label: '经纪人视图' },
|
||
{ key: 'manager', label: '店长视图' },
|
||
{ key: 'admin', label: '管理员视图' }
|
||
],
|
||
roleConfigs: {},
|
||
draftConfig: null,
|
||
backupConfig: null,
|
||
toast: { open: false, message: '' },
|
||
|
||
cardCatalog: {
|
||
new_property_today: { label: '今日新增房源', desc: '统计今日新增房源数量' },
|
||
new_client_today: { label: '今日新增客源', desc: '统计今日新增客源数量' },
|
||
new_showing_today: { label: '今日新增带看', desc: '统计今日新增带看数量' },
|
||
new_followup_today: { label: '今日新增跟进', desc: '统计今日新增跟进数量' },
|
||
signed_deals_today: { label: '今日签约单量', desc: '统计今日签约套数' },
|
||
deal_amount_today: { label: '今日成交业绩', desc: '统计今日成交业绩金额' }
|
||
},
|
||
|
||
metricValues: {
|
||
new_property_today: '18',
|
||
new_client_today: '12',
|
||
new_showing_today: '9',
|
||
new_followup_today: '36',
|
||
signed_deals_today: '4',
|
||
deal_amount_today: '126.8万'
|
||
},
|
||
|
||
init() {
|
||
this.roleConfigs = {
|
||
agent: this.defaultRoleConfig('agent'),
|
||
manager: this.defaultRoleConfig('manager'),
|
||
admin: this.defaultRoleConfig('admin')
|
||
};
|
||
this.loadRole('agent');
|
||
},
|
||
|
||
defaultRoleConfig(role) {
|
||
const baseCards = [
|
||
{ key: 'new_property_today', enabled: true, sort: 1 },
|
||
{ key: 'new_client_today', enabled: true, sort: 2 },
|
||
{ key: 'new_followup_today', enabled: true, sort: 3 },
|
||
{ key: 'new_showing_today', enabled: role !== 'agent', sort: 4 },
|
||
{ key: 'signed_deals_today', enabled: role === 'admin' || role === 'manager', sort: 5 },
|
||
{ key: 'deal_amount_today', enabled: role === 'admin', sort: 6 }
|
||
];
|
||
return {
|
||
show_seniority: role !== 'agent',
|
||
itinerary_metrics: ['sale', 'rent'],
|
||
kpi_mode: 'pending_and_approved',
|
||
home_cards: baseCards,
|
||
ranking: {
|
||
show_performance_and_deal: true,
|
||
calc_mode: 'turn_order',
|
||
default_rank_level: role === 'agent' ? 'group' : 'store',
|
||
show_company_top10: role === 'admin',
|
||
filter_accounts: '',
|
||
filter_departments: ''
|
||
},
|
||
battle_report: {
|
||
days_range: '',
|
||
amount_range: '',
|
||
show_performance: role === 'admin',
|
||
show_property_name: true,
|
||
show_property_total_price: role === 'admin'
|
||
}
|
||
};
|
||
},
|
||
|
||
get currentRoleLabel() {
|
||
return this.roleOptions.find(r => r.key === this.currentRole)?.label || '';
|
||
},
|
||
|
||
get sortedCards() {
|
||
if (!this.draftConfig) return [];
|
||
return this.draftConfig.home_cards
|
||
.slice()
|
||
.sort((a, b) => a.sort - b.sort)
|
||
.map(c => ({ ...c, label: this.cardCatalog[c.key].label, desc: this.cardCatalog[c.key].desc }));
|
||
},
|
||
|
||
get enabledCards() {
|
||
return this.sortedCards.filter(c => c.enabled);
|
||
},
|
||
|
||
metricValue(key) {
|
||
return this.metricValues[key] || '-';
|
||
},
|
||
|
||
matchSection(label) {
|
||
if (!this.searchKeyword) return true;
|
||
return label.includes(this.searchKeyword);
|
||
},
|
||
|
||
loadRole(role) {
|
||
this.currentRole = role;
|
||
this.draftConfig = this.deepClone(this.roleConfigs[role]);
|
||
this.backupConfig = this.deepClone(this.roleConfigs[role]);
|
||
this.formError = '';
|
||
this.editMode = false;
|
||
},
|
||
|
||
switchRole(role) {
|
||
if (this.editMode) {
|
||
this.formError = '请先保存或取消当前角色编辑内容';
|
||
return;
|
||
}
|
||
this.loadRole(role);
|
||
},
|
||
|
||
startEdit() {
|
||
this.editMode = true;
|
||
this.formError = '';
|
||
this.backupConfig = this.deepClone(this.draftConfig);
|
||
},
|
||
|
||
cancelEdit() {
|
||
this.draftConfig = this.deepClone(this.backupConfig);
|
||
this.editMode = false;
|
||
this.formError = '';
|
||
this.notify('已取消编辑,恢复到上次保存状态');
|
||
},
|
||
|
||
saveConfig() {
|
||
this.formError = '';
|
||
const enabledCount = this.draftConfig.home_cards.filter(c => c.enabled).length;
|
||
if (enabledCount < 1) {
|
||
this.formError = '至少保留 1 个首页统计卡片';
|
||
return;
|
||
}
|
||
if ((this.draftConfig.ranking.filter_accounts || '').length > 100) {
|
||
this.formError = '过滤账号输入过长,请控制在100字符以内';
|
||
return;
|
||
}
|
||
if ((this.draftConfig.ranking.filter_departments || '').length > 100) {
|
||
this.formError = '过滤部门输入过长,请控制在100字符以内';
|
||
return;
|
||
}
|
||
|
||
this.roleConfigs[this.currentRole] = this.deepClone(this.draftConfig);
|
||
this.backupConfig = this.deepClone(this.draftConfig);
|
||
this.editMode = false;
|
||
this.notify('首页设置已保存,当前角色视图即时生效');
|
||
},
|
||
|
||
toggleBoolean(field) {
|
||
if (!this.editMode) return;
|
||
this.draftConfig[field] = !this.draftConfig[field];
|
||
},
|
||
|
||
toggleNested(group, field) {
|
||
if (!this.editMode) return;
|
||
this.draftConfig[group][field] = !this.draftConfig[group][field];
|
||
},
|
||
|
||
toggleMetric(metric) {
|
||
if (!this.editMode) return;
|
||
const list = this.draftConfig.itinerary_metrics;
|
||
const idx = list.indexOf(metric);
|
||
if (idx >= 0) list.splice(idx, 1);
|
||
else list.push(metric);
|
||
},
|
||
|
||
toggleCard(key) {
|
||
if (!this.editMode) return;
|
||
const card = this.draftConfig.home_cards.find(c => c.key === key);
|
||
if (!card) return;
|
||
card.enabled = !card.enabled;
|
||
},
|
||
|
||
moveCard(key, delta) {
|
||
if (!this.editMode) return;
|
||
const cards = this.draftConfig.home_cards.slice().sort((a,b) => a.sort-b.sort);
|
||
const index = cards.findIndex(c => c.key === key);
|
||
if (index < 0) return;
|
||
const target = index + delta;
|
||
if (target < 0 || target >= cards.length) return;
|
||
const tmp = cards[index].sort;
|
||
cards[index].sort = cards[target].sort;
|
||
cards[target].sort = tmp;
|
||
},
|
||
|
||
notify(message) {
|
||
this.toast.message = message;
|
||
this.toast.open = true;
|
||
setTimeout(() => {
|
||
this.toast.open = false;
|
||
}, 1800);
|
||
},
|
||
|
||
deepClone(v) { return JSON.parse(JSON.stringify(v)); }
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html> |