fix: adjust knowledge operations overview layout and add rank badges

- Change ops-grid from 3 columns to 2 for better metric card sizing
- Add rank badges (hot/warm/normal) to liked and disliked answer lists
- Unify row heights (min-height: 32px) across hot-list and feedback-list
This commit is contained in:
2026-04-30 22:11:57 +08:00
parent 789f90dc1f
commit 64537119e0

View File

@@ -1,31 +1,916 @@
<template>
<section class="view single">
<article class="panel">
<PanelHead eyebrow="Policy automation" title="规则运行状态" note="把关键政策、阈值和命中表现集中维护。" />
<div class="list">
<InfoRow
v-for="policy in policies"
:key="policy.code"
:rank="policy.code"
:title="policy.title"
:note="policy.note"
:badge="policy.badge"
:tone="policy.tone"
/>
</div>
</article>
<section class="knowledge-page">
<div class="knowledge-grid">
<section class="knowledge-main">
<div class="knowledge-metrics panel">
<article v-for="item in metrics" :key="item.label" class="metric-card" :style="{ '--accent': item.accent }">
<span class="metric-icon"><i :class="item.icon"></i></span>
<div>
<p>{{ item.label }} <i v-if="item.help" class="pi pi-question-circle"></i></p>
<strong>{{ item.value }}</strong>
<small>{{ item.meta }}</small>
</div>
</article>
</div>
<article class="library-panel panel">
<header class="panel-title">
<h2>文档库 / 文件夹</h2>
</header>
<div class="library-body">
<aside class="folder-rail">
<label class="folder-search">
<i class="pi pi-search"></i>
<input type="search" placeholder="搜索文件夹" />
<button type="button" aria-label="新增文件夹"><i class="pi pi-plus"></i></button>
</label>
<nav class="folder-tree" aria-label="知识库文件夹">
<button v-for="folder in folders" :key="folder.name" type="button" :class="{ active: folder.active }">
<i :class="folder.icon"></i>
<span>{{ folder.name }}</span>
<b>{{ folder.count }}</b>
</button>
</nav>
<button class="new-folder-btn" type="button">
<i class="pi pi-plus"></i>
<span>新建文件夹</span>
</button>
</aside>
<section class="document-area">
<div class="upload-zone">
<i class="pi pi-cloud-upload"></i>
<strong>拖拽文档到此处或点击上传</strong>
<span>支持 PDF / Word / Excel / PPT 文档单个文件不超过 100MB</span>
</div>
<div class="doc-table-wrap">
<table>
<thead>
<tr>
<th>文件名称</th>
<th>标签</th>
<th>上传时间 <i class="pi pi-arrow-down"></i></th>
<th>版本</th>
<th>状态</th>
<th>上传人</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="doc in documents" :key="doc.name">
<td>
<span class="file-name">
<i :class="doc.icon"></i>
{{ doc.name }}
</span>
</td>
<td>
<span class="doc-tag">{{ doc.tag }}</span>
</td>
<td>{{ doc.time }}</td>
<td>{{ doc.version }}</td>
<td><span class="state-tag" :class="doc.stateTone">{{ doc.state }}</span></td>
<td>{{ doc.owner }}</td>
<td><button class="more-btn" type="button" aria-label="更多操作"><i class="pi pi-ellipsis-h"></i></button></td>
</tr>
</tbody>
</table>
</div>
<footer class="list-foot">
<span> 18 </span>
<button type="button">10/ <i class="pi pi-angle-down"></i></button>
<div class="pager" aria-label="分页">
<button type="button" aria-label="上一页"><i class="pi pi-angle-left"></i></button>
<button class="active" type="button" aria-current="page">1</button>
<button type="button">2</button>
<button type="button" aria-label="下一页"><i class="pi pi-angle-right"></i></button>
</div>
<label>前往 <input value="1" aria-label="页码" /> </label>
</footer>
</section>
</div>
</article>
</section>
<aside class="analytics-column">
<article class="ops-card panel">
<header class="card-head">
<h2>知识运营概览 <i class="pi pi-question-circle"></i></h2>
</header>
<div class="ops-grid">
<div v-for="item in opsMetrics" :key="item.label" class="ops-item" :style="{ '--accent': item.accent }">
<span><i :class="item.icon"></i></span>
<div>
<strong>{{ item.value }}</strong>
<p>{{ item.label }}</p>
<small>{{ item.meta }}</small>
</div>
</div>
</div>
</article>
<article class="top-card panel">
<header class="card-head">
<h2>热门问题 TOP5 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
</header>
<ol class="hot-list">
<li v-for="item in hotQuestions" :key="item.title">
<span :class="item.tone">{{ item.rank }}</span>
<strong>{{ item.title }}</strong>
<b>{{ item.count }}</b>
</li>
</ol>
</article>
<article class="feedback-card panel">
<header class="card-head">
<h2>用户点赞最多 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
</header>
<ul class="feedback-list positive">
<li v-for="(item, idx) in likedAnswers" :key="item.title">
<span class="rank-badge" :class="idx === 0 ? 'hot' : idx < 3 ? 'warm' : 'normal'">{{ idx + 1 }}</span>
<span>{{ item.title }}</span>
<b><i class="pi pi-thumbs-up-fill"></i>{{ item.count }}</b>
</li>
</ul>
</article>
<article class="feedback-card panel">
<header class="card-head">
<h2>用户点踩较多 / 待优化 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
</header>
<ul class="feedback-list negative">
<li v-for="(item, idx) in dislikedAnswers" :key="item.title">
<span class="rank-badge" :class="idx === 0 ? 'hot' : idx < 3 ? 'warm' : 'normal'">{{ idx + 1 }}</span>
<span>{{ item.title }}</span>
<b><i class="pi pi-thumbs-down-fill"></i>{{ item.count }}</b>
<em>待优化</em>
</li>
</ul>
</article>
<article class="trend-card panel">
<header class="card-head">
<h2>近7天提问趋势 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
</header>
<svg class="trend-chart" viewBox="0 0 420 190" role="img" aria-label="近7天提问趋势">
<line v-for="tick in chartTicks" :key="tick" x1="38" :y1="tick.y" x2="398" :y2="tick.y" />
<text v-for="tick in chartTicks" :key="tick.label" x="8" :y="tick.y + 4">{{ tick.label }}</text>
<polyline :points="trendPoints" />
<circle v-for="point in trendData" :key="point.label" :cx="point.x" :cy="point.y" r="4" />
<text v-for="point in trendData" :key="point.label + '-label'" :x="point.x - 14" y="180">{{ point.label }}</text>
</svg>
</article>
<article class="recent-card panel">
<header class="card-head">
<h2>最近更新知识 <i class="pi pi-question-circle"></i></h2>
<button type="button">更多 <i class="pi pi-angle-right"></i></button>
</header>
<ul class="recent-list">
<li v-for="item in recentKnowledge" :key="item.name">
<i :class="item.icon"></i>
<strong>{{ item.name }}</strong>
<time>{{ item.time }}</time>
</li>
</ul>
</article>
</aside>
</div>
</section>
</template>
<script setup>
import PanelHead from '../components/shared/PanelHead.vue'
import InfoRow from '../components/shared/InfoRow.vue'
import { policies } from '../data/policies.js'
import { computed } from 'vue'
const metrics = [
{ label: '文档总数', value: '1,248', meta: '较上周 +68', icon: 'pi pi-file', accent: '#10b981', help: true },
{ label: '文件夹总数', value: '36', meta: '较上周 +2', icon: 'pi pi-folder', accent: '#3b82f6' },
{ label: '问答总量', value: '8,562', meta: '较上周 +321', icon: 'pi pi-comments', accent: '#8b5cf6' },
{ label: '知识命中率', value: '87.3%', meta: '较上周 +1.2%', icon: 'pi pi-bullseye', accent: '#f59e0b' }
]
const folders = [
{ name: '财务知识库', count: 36, icon: 'pi pi-folder', active: false },
{ name: '制度政策', count: 8, icon: 'pi pi-folder', active: false },
{ name: '报销制度', count: 12, icon: 'pi pi-folder-open', active: false },
{ name: '差旅规范', count: 18, icon: 'pi pi-folder', active: true },
{ name: '发票管理', count: 14, icon: 'pi pi-folder', active: false },
{ name: '税务合规', count: 16, icon: 'pi pi-folder', active: false },
{ name: '预算管理', count: 9, icon: 'pi pi-folder', active: false },
{ name: '财务共享', count: 7, icon: 'pi pi-folder', active: false },
{ name: '培训资料', count: 6, icon: 'pi pi-folder', active: false },
{ name: '常见问答', count: 11, icon: 'pi pi-folder', active: false }
]
const documents = [
{ name: '差旅报销管理办法2024版', tag: '差旅 / 制度', time: '2024-05-12 14:35', version: 'v3.2', state: '已生效', stateTone: 'success', owner: '张明', icon: 'pi pi-file-pdf pdf' },
{ name: '发票查验规范及操作指引', tag: '发票 / 操作', time: '2024-05-10 10:22', version: 'v1.5', state: '已生效', stateTone: 'success', owner: '李娜', icon: 'pi pi-file-word word' },
{ name: '费用报销标准细则2024', tag: '报销 / 标准', time: '2024-05-08 09:16', version: 'v2.1', state: '已生效', stateTone: 'success', owner: '王磊', icon: 'pi pi-file-pdf pdf' },
{ name: '差旅费用标准对照表(国内)', tag: '差旅 / 标准', time: '2024-05-05 08:20', version: 'v1.3', state: '审批中', stateTone: 'warning', owner: '陈杰', icon: 'pi pi-file-excel excel' },
{ name: '借款管理办法及流程', tag: '借款 / 流程', time: '2024-05-03 11:05', version: 'v1.0', state: '已生效', stateTone: 'success', owner: '刘洋', icon: 'pi pi-file-pdf pdf' }
]
const opsMetrics = [
{ label: '本周新增问题', value: '328', meta: '较上周 +46', icon: 'pi pi-question-circle', accent: '#3b82f6' },
{ label: '已解决问题', value: '286', meta: '解决率 87.2%', icon: 'pi pi-check', accent: '#10b981' },
{ label: '平均响应时长', value: '2.4h', meta: '较上周 -0.6h', icon: 'pi pi-clock', accent: '#3b82f6' },
{ label: '用户满意度', value: '91.2%', meta: '较上周 +2.1%', icon: 'pi pi-face-smile', accent: '#10b981' },
{ label: '收藏次数', value: '1,236', meta: '较上周 +128', icon: 'pi pi-star', accent: '#f59e0b' },
{ label: '点赞总数', value: '3,582', meta: '较上周 +312', icon: 'pi pi-thumbs-up', accent: '#f59e0b' }
]
const hotQuestions = [
{ rank: 1, title: '酒店住宿超标怎么办?', count: '1,256', tone: 'hot' },
{ rank: 2, title: '发票抬头不一致如何处理?', count: '1,023', tone: 'warm' },
{ rank: 3, title: '差旅补贴标准是多少?', count: '856', tone: 'warm' },
{ rank: 4, title: '借款申请后如何冲销?', count: '622', tone: 'normal' },
{ rank: 5, title: '预算占用失败怎么处理?', count: '485', tone: 'normal' }
]
const likedAnswers = [
{ title: '差旅报销标准及注意事项汇总', count: 326 },
{ title: '发票查验失败常见原因及处理', count: 285 },
{ title: '报销单据填写规范与示例', count: 241 },
{ title: '借款冲销操作流程详解', count: 198 },
{ title: '预算占用与释放操作指南', count: 176 }
]
const dislikedAnswers = [
{ title: '补贴发放延迟如何处理?', count: 68 },
{ title: '发票跨月入账如何处理?', count: 57 },
{ title: '预填预算申请流程复杂?', count: 43 },
{ title: '费用分类规则不清晰?', count: 38 },
{ title: '固定资产报废流程说明', count: 31 }
]
const trendData = [
{ label: '05-06', value: 220 },
{ label: '05-07', value: 270 },
{ label: '05-08', value: 200 },
{ label: '05-09', value: 250 },
{ label: '05-10', value: 150 },
{ label: '05-11', value: 210 }
].map((item, index) => ({
...item,
x: 48 + index * 66,
y: 164 - (item.value / 320) * 128
}))
const chartTicks = [
{ label: '320', y: 36 },
{ label: '240', y: 68 },
{ label: '160', y: 100 },
{ label: '80', y: 132 },
{ label: '0', y: 164 }
]
const trendPoints = computed(() => trendData.map((point) => `${point.x},${point.y}`).join(' '))
const recentKnowledge = [
{ name: '差旅报销管理办法2024版v3.2', time: '05-12 14:35', icon: 'pi pi-file-pdf pdf' },
{ name: '发票查验规范及操作指引 v1.5', time: '05-10 10:22', icon: 'pi pi-file-word word' },
{ name: '费用报销标准细则2024v2.1', time: '05-08 09:16', icon: 'pi pi-file-pdf pdf' }
]
</script>
<style scoped>
.view { display: grid; gap: 22px; animation: fadeUp 220ms var(--ease) both; }
.view.single { max-width: 1120px; }
.panel { padding: 20px; }
.list { display: grid; gap: 12px; }
.knowledge-page {
height: 100%;
min-height: 0;
overflow: hidden;
animation: fadeUp 220ms var(--ease) both;
}
.knowledge-grid {
height: 100%;
min-height: 0;
display: grid;
grid-template-columns: minmax(620px, 1.08fr) minmax(560px, .92fr);
gap: 16px;
}
.knowledge-main,
.analytics-column {
min-width: 0;
min-height: 0;
display: grid;
gap: 16px;
}
.knowledge-main {
grid-template-rows: auto minmax(0, 1fr);
}
.analytics-column {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-auto-rows: minmax(0, auto);
overflow-y: auto;
}
.knowledge-metrics {
min-height: 96px;
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
padding: 0;
overflow: hidden;
}
.metric-card {
min-height: 96px;
display: grid;
grid-template-columns: 54px minmax(0, 1fr);
align-items: center;
gap: 14px;
padding: 16px 20px;
border-right: 1px solid #edf2f7;
}
.metric-card:last-child {
border-right: 0;
}
.metric-icon {
width: 48px;
height: 48px;
display: grid;
place-items: center;
border-radius: 999px;
background: color-mix(in srgb, var(--accent) 14%, white);
color: var(--accent);
font-size: 22px;
}
.metric-card p {
color: #334155;
font-size: 14px;
font-weight: 650;
}
.metric-card p i,
.card-head h2 i {
color: #94a3b8;
font-size: 12px;
}
.metric-card strong {
display: block;
margin-top: 5px;
color: #0f172a;
font-size: 24px;
font-weight: 850;
line-height: 1;
}
.metric-card small {
display: block;
margin-top: 7px;
color: #64748b;
font-size: 13px;
}
.library-panel {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr);
padding: 16px 18px;
overflow: hidden;
}
.panel-title h2,
.card-head h2 {
color: #0f172a;
font-size: 19px;
font-weight: 850;
}
.library-body {
min-height: 0;
display: grid;
grid-template-columns: 180px minmax(0, 1fr);
gap: 14px;
margin-top: 16px;
}
.folder-rail {
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 12px;
border-right: 1px solid #edf2f7;
padding-right: 12px;
}
.folder-search {
height: 36px;
display: grid;
grid-template-columns: 18px minmax(0, 1fr) 24px;
align-items: center;
gap: 6px;
padding: 0 8px;
border: 1px solid #d7e0ea;
border-radius: 8px;
color: #64748b;
}
.folder-search input {
min-width: 0;
border: 0;
color: #0f172a;
font-size: 13px;
}
.folder-search input:focus {
outline: none;
}
.folder-search button {
border: 0;
background: transparent;
color: #64748b;
}
.folder-tree {
min-height: 0;
display: grid;
align-content: start;
gap: 6px;
overflow-y: auto;
}
.folder-tree button {
min-height: 34px;
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
padding: 0 9px;
border: 0;
border-radius: 7px;
background: transparent;
color: #334155;
font-size: 13px;
text-align: left;
}
.folder-tree button.active {
background: #dcfce7;
color: #059669;
font-weight: 850;
}
.folder-tree b {
min-width: 24px;
height: 20px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 999px;
background: #f1f5f9;
color: #64748b;
font-size: 11px;
}
.new-folder-btn {
min-height: 36px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 1px solid rgba(16, 185, 129, .28);
border-radius: 8px;
background: #f0fdf4;
color: #059669;
font-size: 13px;
font-weight: 850;
}
.new-folder-btn:hover {
background: #dcfce7;
}
.document-area {
min-width: 0;
min-height: 0;
display: grid;
grid-template-rows: auto minmax(0, 1fr) auto;
gap: 12px;
}
.upload-zone {
min-height: 112px;
display: grid;
place-items: center;
align-content: center;
gap: 8px;
border: 1px dashed #93c5fd;
border-radius: 10px;
background: #f8fbff;
color: #334155;
text-align: center;
}
.upload-zone i {
color: #2563eb;
font-size: 31px;
}
.upload-zone strong {
font-size: 13px;
font-weight: 850;
}
.upload-zone span {
color: #64748b;
font-size: 12px;
}
.doc-table-wrap {
min-height: 0;
overflow: auto;
}
table {
width: 100%;
min-width: 690px;
border-collapse: collapse;
}
th,
td {
padding: 12px 10px;
border-bottom: 1px solid #edf2f7;
color: #24324a;
font-size: 12px;
line-height: 1.35;
text-align: left;
vertical-align: middle;
}
th {
background: #f7fafc;
color: #64748b;
font-weight: 800;
}
.file-name {
display: inline-flex;
align-items: center;
gap: 7px;
font-weight: 750;
white-space: nowrap;
}
.file-name .pdf,
.recent-list .pdf { color: #ef4444; }
.file-name .word,
.recent-list .word { color: #2563eb; }
.file-name .excel { color: #10b981; }
.doc-tag {
display: inline-flex;
align-items: center;
min-height: 22px;
padding: 0 7px;
border-radius: 6px;
background: #f1f5f9;
color: #64748b;
font-size: 11px;
font-weight: 750;
}
.state-tag {
min-height: 22px;
display: inline-flex;
align-items: center;
padding: 0 8px;
border-radius: 6px;
font-size: 11px;
font-weight: 800;
white-space: nowrap;
}
.state-tag.success {
background: #dcfce7;
color: #059669;
}
.state-tag.warning {
background: #ffedd5;
color: #f97316;
}
.more-btn {
border: 0;
background: transparent;
color: #2563eb;
}
.list-foot {
display: grid;
grid-template-columns: auto auto 1fr auto;
align-items: center;
gap: 10px;
color: #64748b;
font-size: 13px;
}
.list-foot button {
min-height: 32px;
border: 1px solid #d7e0ea;
border-radius: 8px;
background: #fff;
color: #334155;
font-weight: 750;
}
.pager {
display: inline-flex;
gap: 6px;
}
.pager button {
width: 32px;
padding: 0;
}
.pager button.active {
border-color: #059669;
background: #059669;
color: #fff;
}
.list-foot input {
width: 42px;
height: 30px;
border: 1px solid #d7e0ea;
border-radius: 7px;
text-align: center;
}
.panel {
min-width: 0;
}
.card-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.card-head button {
border: 0;
background: transparent;
color: #64748b;
font-size: 12px;
font-weight: 850;
}
.ops-card {
grid-column: span 1;
padding: 16px 18px;
}
.ops-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px 12px;
margin-top: 18px;
}
.ops-item {
min-width: 0;
display: grid;
grid-template-columns: 28px minmax(0, 1fr);
gap: 9px;
align-items: start;
}
.ops-item > span {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border-radius: 999px;
background: color-mix(in srgb, var(--accent) 12%, white);
color: var(--accent);
font-size: 16px;
}
.ops-item strong {
color: #0f172a;
font-size: 18px;
font-weight: 900;
}
.ops-item p {
margin-top: 3px;
color: #334155;
font-size: 12px;
font-weight: 750;
}
.ops-item small {
display: block;
margin-top: 8px;
color: #64748b;
font-size: 11px;
}
.top-card,
.feedback-card,
.trend-card,
.recent-card {
padding: 16px 18px;
}
.hot-list,
.feedback-list,
.recent-list {
display: grid;
gap: 13px;
margin: 16px 0 0;
padding: 0;
list-style: none;
}
.hot-list li {
min-height: 32px;
display: grid;
grid-template-columns: 24px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
}
.hot-list span {
width: 20px;
height: 20px;
display: grid;
place-items: center;
border-radius: 5px;
color: #fff;
font-size: 12px;
font-weight: 900;
}
.hot-list .hot { background: #ef4444; }
.hot-list .warm { background: #f59e0b; }
.hot-list .normal {
background: #f1f5f9;
color: #64748b;
}
.hot-list strong,
.feedback-list span {
min-width: 0;
color: #334155;
font-size: 13px;
font-weight: 800;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hot-list b {
color: #334155;
font-size: 13px;
font-weight: 850;
}
.feedback-list li {
min-height: 32px;
display: grid;
grid-template-columns: 24px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
}
.feedback-list.negative li {
grid-template-columns: 24px minmax(0, 1fr) auto auto;
}
.feedback-list b {
display: inline-flex;
align-items: center;
gap: 6px;
color: #059669;
font-size: 13px;
font-weight: 900;
}
.feedback-list .rank-badge {
width: 20px;
height: 20px;
display: grid;
place-items: center;
border-radius: 5px;
color: #fff;
font-size: 12px;
font-weight: 900;
}
.feedback-list .rank-badge.hot { background: #ef4444; }
.feedback-list .rank-badge.warm { background: #f59e0b; }
.feedback-list .rank-badge.normal { background: #f1f5f9; color: #64748b; }
.feedback-list.negative b {
color: #ef4444;
}
.feedback-list em {
padding: 4px 8px;
border-radius: 7px;
background: #fee2e2;
color: #ef4444;
font-size: 12px;
font-style: normal;
font-weight: 850;
}
.trend-chart {
width: 100%;
height: 220px;
margin-top: 12px;
}
.trend-chart line {
stroke: #edf2f7;
}
.trend-chart text {
fill: #64748b;
font-size: 11px;
}
.trend-chart polyline {
fill: none;
stroke: #10b981;
stroke-width: 3;
}
.trend-chart circle {
fill: #fff;
stroke: #10b981;
stroke-width: 3;
}
.recent-list li {
display: grid;
grid-template-columns: 18px minmax(0, 1fr) auto;
align-items: center;
gap: 8px;
}
.recent-list strong {
color: #334155;
font-size: 12px;
font-weight: 850;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.recent-list time {
color: #64748b;
font-size: 12px;
font-weight: 750;
}
@media (max-width: 1380px) {
.knowledge-grid {
grid-template-columns: 1fr;
overflow-y: auto;
}
}
@media (max-width: 980px) {
.knowledge-metrics,
.analytics-column {
grid-template-columns: 1fr;
}
.library-body {
grid-template-columns: 1fr;
}
.folder-rail {
border-right: 0;
border-bottom: 1px solid #edf2f7;
padding: 0 0 12px;
}
.ops-grid {
grid-template-columns: 1fr;
}
}
</style>