@@ -0,0 +1,390 @@
< template >
< div class = "okr-templates-page" >
< div class = "page-header" >
< h3 > OKR模板库 < / h3 >
< p class = "page-desc" > 从模板库中选择O + KR模板 , 快速应用到战略地图画布 < / p >
< / div >
<!-- 搜索与筛选 -- >
< div class = "filter-bar" >
< el-input
v - model = "searchText"
placeholder = "搜索O名称..."
clearable
prefix - icon = "Search"
style = "width:260px"
@ input = "onSearch"
/ >
< el-select v-model = "filterIndustry" placeholder="行业" clearable style="width:150px" @change="loadTemplates" >
< el-option label = "全部行业" value = "" / >
< el-option label = "贸易经销" value = "trading" / >
< el-option label = "IT服务" value = "it_service" / >
< / el-select >
< el-select v-model = "filterDimension" placeholder="维度" clearable style="width:150px" @change="loadTemplates" >
< el-option label = "全部维度" value = "" / >
< el-option label = "财务层" value = "finance" / >
< el-option label = "客户层" value = "customer" / >
< el-option label = "流程层" value = "process" / >
< el-option label = "学习成长层" value = "learning" / >
< / el-select >
< el-button type = "primary" :icon = "Search" @click ="loadTemplates" > 搜索 < / el -button >
< / div >
<!-- 统计 -- >
< div class = "stat-bar" >
< el-tag type = "" > { { systemCount } } 个系统模板 < / el-tag >
< el-tag type = "warning" > { { industryCount } } 个行业包 < / el-tag >
< el-tag type = "success" > { { userCount } } 个用户自定义 < / el-tag >
< / div >
<!-- 加载中 -- >
< div v-if = "loading" style="text-align:center;padding:40px"><el-icon class="is-loading" :size="24" > < Loading / > < / el-icon > 加载中 ... < / div >
<!-- 系统模板分区 -- >
< div v-if = "!loading" >
< div class = "section-title" >
< el-icon color = "#409EFF" > < Document / > < / el-icon > 系统模板 < span class = "section-badge" > ( { { systemTemplates . length } } ) < / span >
< / div >
< div v-if = "systemTemplates.length === 0" class="empty-section" > 暂无匹配的系统模板 < / div >
< div v-else class = "template-grid" >
< div v-for = "t in systemTemplates" :key="t.id" class="template-card" >
< div class = "card-icon" : style = "{ background: dimColor(t.dimension) + '20', color: dimColor(t.dimension) }" >
{ { dimIcon ( t . dimension ) } }
< / div >
< div class = "card-body" >
< div class = "card-name" > { { t . name } } < / div >
< div class = "card-dim tag" > { { dimLabel ( t . dimension ) } } < / div >
< div class = "card-desc" > { { t . description } } < / div >
< div class = "card-footer" >
< span class = "use-count" > 已使用 { { t . use _count || 0 } } 次 < / span >
< el-button size = "small" type = "primary" @click ="applyTemplate(t)" > 应用 < / el -button >
< / div >
< / div >
< / div >
< / div >
<!-- 行业包分区 -- >
< div class = "section-title" style = "margin-top:28px;" >
< el-icon color = "#E6A23C" > < Files / > < / el-icon > 行业扩展包 < span class = "section-badge" > ( { { industryTemplates . length } } ) < / span >
< / div >
< div v-if = "industryTemplates.length === 0" class="empty-section" > 暂无匹配的行业包模板 < / div >
< div v-else class = "template-grid" >
< div v-for = "t in industryTemplates" :key="t.id" class="template-card card-industry" >
< div v-if = "t.industry_tag === 'trading'" class="industry-badge trading" > 贸易经销 < / div >
< div v-if = "t.industry_tag === 'it_service'" class="industry-badge it" > IT服务 < / div >
< div class = "card-icon" : style = "{ background: dimColor(t.dimension) + '20', color: dimColor(t.dimension) }" >
{ { dimIcon ( t . dimension ) } }
< / div >
< div class = "card-body" >
< div class = "card-name" > { { t . name } } < / div >
< div class = "card-dim tag" > { { dimLabel ( t . dimension ) } } < / div >
< div class = "card-desc" > { { t . description } } < / div >
< div class = "card-footer" >
< span class = "use-count" > 已使用 { { t . use _count || 0 } } 次 < / span >
< el-button size = "small" type = "warning" @click ="applyTemplate(t)" > 应用 < / el -button >
< / div >
< / div >
< / div >
< / div >
<!-- 用户自定义分区 -- >
< div class = "section-title" style = "margin-top:28px;" >
< el-icon color = "#67C23A" > < User / > < / el-icon > 用户自定义 < span class = "section-badge" > ( { { userTemplates . length } } ) < / span >
< / div >
< div v-if = "userTemplates.length === 0" class="empty-section" > 暂无用户自定义模板 — 在画布中保存O + KR后将自动生成 < / div >
< div v-else class = "template-grid" >
< div v-for = "t in userTemplates" :key="t.id" class="template-card card-user" >
< div class = "card-icon" : style = "{ background: dimColor(t.dimension) + '20', color: dimColor(t.dimension) }" >
{ { dimIcon ( t . dimension ) } }
< / div >
< div class = "card-body" >
< div class = "card-name" > { { t . name } } < / div >
< div class = "card-dim tag" > { { dimLabel ( t . dimension ) } } < / div >
< div class = "card-desc" > { { t . description } } < / div >
< div class = "card-footer" >
< span class = "use-count" > 已使用 { { t . use _count || 0 } } 次 < / span >
< el-button size = "small" type = "success" @click ="applyTemplate(t)" > 应用 < / el -button >
< / div >
< / div >
< / div >
< / div >
< / div >
<!-- 应用弹窗 — 选择目标地图 -- >
< el-dialog v-model = "showApplyDialog" title="应用模板" width="480px" destroy -on -close >
< div v-if = "applyingTemplate" >
< p style = "margin-bottom:12px;font-weight:500;" > O : { { applyingTemplate . name } } < / p >
< p style = "margin-bottom:12px;color:#909399;font-size:13px;" >
维度 : { { dimLabel ( applyingTemplate . dimension ) } } |
KR数量 : { { applyingTemplate . preset _krs ? . length || 0 } }
< / p >
< el-divider / >
< el-form label -width = " 100px " >
< el-form-item label = "地图名称" >
< el-input v-model = "applyMapTitle" placeholder="输入新地图名称" / >
< / el-form-item >
< / el-form >
< div style = "font-size:12px;color:#909399;margin-top:8px;" >
⚡ 将创建一张新战略地图并自动填入该O + KR , 之后可在画布中继续编辑
< / div >
< / div >
< template # footer >
< el-button size = "small" @click ="showApplyDialog = false" > 取消 < / el -button >
< el-button size = "small" type = "primary" :loading = "applying" @click ="confirmApply" > 确认应用 < / el -button >
< / template >
< / el-dialog >
< / div >
< / template >
< script setup lang = "ts" >
import { ref , computed , onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Search , Document , Files , User , Loading } from '@element-plus/icons-vue'
import { okrTemplateApi } from '../api/index'
const router = useRouter ( )
const loading = ref ( true )
const allTemplates = ref < any [ ] > ( [ ] )
const searchText = ref ( '' )
const filterIndustry = ref ( '' )
const filterDimension = ref ( '' )
const showApplyDialog = ref ( false )
const applyingTemplate = ref < any > ( null )
const applyMapTitle = ref ( '' )
const applying = ref ( false )
/ / 维 度 映 射
const DIM _LABELS : Record < string , string > = {
finance : '财务层' ,
customer : '客户层' ,
process : '流程层' ,
learning : '学习成长层' ,
}
const DIM _ICONS : Record < string , string > = {
finance : '💰' ,
customer : '👥' ,
process : '⚙️' ,
learning : '📚' ,
}
const DIM _COLORS : Record < string , string > = {
finance : '#F56C6C' ,
customer : '#409EFF' ,
process : '#67C23A' ,
learning : '#E6A23C' ,
}
function dimLabel ( d : string ) { return DIM _LABELS [ d ] || d }
function dimIcon ( d : string ) { return DIM _ICONS [ d ] || '📌' }
function dimColor ( d : string ) { return DIM _COLORS [ d ] || '#909399' }
const debounceTimer = ref < ReturnType < typeof setTimeout > | null > ( null )
function onSearch ( ) {
if ( debounceTimer . value ) clearTimeout ( debounceTimer . value )
debounceTimer . value = setTimeout ( ( ) => loadTemplates ( ) , 300 )
}
async function loadTemplates ( ) {
loading . value = true
try {
const params : Record < string , any > = { }
if ( filterIndustry . value ) params . industry _tag = filterIndustry . value
if ( filterDimension . value ) params . dimension = filterDimension . value
if ( searchText . value . trim ( ) ) params . search = searchText . value . trim ( )
const res : any = await okrTemplateApi . list ( params )
allTemplates . value = res ? . items || [ ]
} catch ( e : any ) {
ElMessage . error ( '加载模板失败: ' + ( e ? . message || '' ) )
allTemplates . value = [ ]
} finally {
loading . value = false
}
}
const systemTemplates = computed ( ( ) =>
allTemplates . value . filter ( t => t . source === 'system' )
)
const industryTemplates = computed ( ( ) =>
allTemplates . value . filter ( t => t . source === 'industry_pack' )
)
const userTemplates = computed ( ( ) =>
allTemplates . value . filter ( t => t . source === 'user' )
)
const systemCount = computed ( ( ) => systemTemplates . value . length )
const industryCount = computed ( ( ) => industryTemplates . value . length )
const userCount = computed ( ( ) => userTemplates . value . length )
function applyTemplate ( t : any ) {
applyingTemplate . value = t
applyMapTitle . value = t . name + ' — 战略地图'
showApplyDialog . value = true
}
async function confirmApply ( ) {
if ( ! applyMapTitle . value . trim ( ) ) {
ElMessage . warning ( '请输入地图名称' )
return
}
applying . value = true
try {
const res : any = await okrTemplateApi . applyTemplate ( applyingTemplate . value . id , {
title : applyMapTitle . value . trim ( ) ,
} )
ElMessage . success ( '模板已应用,正在跳转到画布...' )
showApplyDialog . value = false
/ / 刷 新 列 表 以 更 新 u s e _ c o u n t
loadTemplates ( )
/ / 跳 转 到 画 布
if ( res ? . map _id ) {
router . push ( '/maps/canvas/' + res . map _id )
}
} catch ( e : any ) {
ElMessage . error ( '应用失败: ' + ( e ? . response ? . data ? . detail || e ? . message || '' ) )
} finally {
applying . value = false
}
}
onMounted ( ( ) => {
loadTemplates ( )
} )
< / script >
< style scoped >
. okr - templates - page {
padding : 16 px 20 px ;
}
. page - header h3 {
margin : 0 0 4 px ;
font - size : 18 px ;
}
. page - desc {
margin : 0 0 16 px ;
color : # 909399 ;
font - size : 13 px ;
}
. filter - bar {
display : flex ;
align - items : center ;
gap : 10 px ;
margin - bottom : 14 px ;
flex - wrap : wrap ;
}
. stat - bar {
display : flex ;
gap : 8 px ;
margin - bottom : 20 px ;
}
. section - title {
font - size : 15 px ;
font - weight : 600 ;
display : flex ;
align - items : center ;
gap : 6 px ;
margin - bottom : 12 px ;
}
. section - badge {
font - weight : 400 ;
color : # 909399 ;
font - size : 13 px ;
}
. empty - section {
color : # C0C4CC ;
font - size : 13 px ;
padding : 20 px 0 ;
text - align : center ;
}
. template - grid {
display : grid ;
grid - template - columns : repeat ( auto - fill , minmax ( 280 px , 1 fr ) ) ;
gap : 14 px ;
}
. template - card {
position : relative ;
background : # fff ;
border : 1 px solid # EBEEF5 ;
border - radius : 8 px ;
padding : 16 px ;
display : flex ;
gap : 14 px ;
transition : box - shadow 0.2 s ;
}
. template - card : hover {
box - shadow : 0 2 px 12 px rgba ( 0 , 0 , 0 , 0.06 ) ;
}
. card - industry {
border - left : 3 px solid # E6A23C ;
}
. card - user {
border - left : 3 px solid # 67 C23A ;
}
. industry - badge {
position : absolute ;
top : 8 px ;
right : 8 px ;
font - size : 11 px ;
padding : 1 px 8 px ;
border - radius : 10 px ;
font - weight : 500 ;
}
. industry - badge . trading {
background : # FDF6EC ;
color : # E6A23C ;
border : 1 px solid # E6A23C ;
}
. industry - badge . it {
background : # ECF5FF ;
color : # 409 EFF ;
border : 1 px solid # 409 EFF ;
}
. card - icon {
width : 44 px ;
height : 44 px ;
border - radius : 10 px ;
display : flex ;
align - items : center ;
justify - content : center ;
font - size : 22 px ;
flex - shrink : 0 ;
}
. card - body {
flex : 1 ;
min - width : 0 ;
}
. card - name {
font - weight : 600 ;
font - size : 14 px ;
margin - bottom : 4 px ;
}
. card - dim . tag {
display : inline - block ;
font - size : 11 px ;
padding : 0 6 px ;
border - radius : 4 px ;
background : # F5F7FA ;
color : # 606266 ;
margin - bottom : 6 px ;
}
. card - desc {
font - size : 12 px ;
color : # 909399 ;
line - height : 1.5 ;
margin - bottom : 8 px ;
display : - webkit - box ;
- webkit - line - clamp : 2 ;
- webkit - box - orient : vertical ;
overflow : hidden ;
}
. card - footer {
display : flex ;
align - items : center ;
justify - content : space - between ;
}
. use - count {
font - size : 12 px ;
color : # C0C4CC ;
}
< / style >