1
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import request from './request.js'
|
||||
|
||||
export function getSettings() {
|
||||
return request.get('/system-config/settings')
|
||||
}
|
||||
|
||||
export function saveSettings(data) {
|
||||
return request.post('/system-config/save-settings', data)
|
||||
}
|
||||
@@ -12,8 +12,10 @@
|
||||
<el-tag v-else size="small" type="info">未设置</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="分块" width="140">
|
||||
<template #default="{ row }">{{ STRATEGY_NAMES[row.chunk_strategy || 'title'] }} {{ row.chunk_size || 800 }}{{ row.chunk_strategy === 'semantic' ? '' : ' / ' + (row.chunk_overlap ?? 150) }}</template>
|
||||
<el-table-column label="分块" width="210">
|
||||
<template #default="{ row }">
|
||||
{{ row.chunk_size || 800 }} / {{ row.chunk_overlap ?? 150 }}<template v-if="row.unit_pattern">(自动识别:{{ humanizePattern(row.unit_pattern) }})</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="190" fixed="right">
|
||||
@@ -38,21 +40,13 @@
|
||||
<el-option v-for="m in embedders" :key="m.id" :label="m.name" :value="m.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分块策略">
|
||||
<el-select v-model="form.chunk_strategy" style="width: 100%">
|
||||
<el-option label="标题分块(推荐)" value="title" />
|
||||
<el-option label="递归字符分块" value="recursive" />
|
||||
<el-option label="语义分块" value="semantic" />
|
||||
</el-select>
|
||||
<div class="ds-tip">标题分块保留标题与段落结构;递归字符分块按分隔符切分,通用文本;语义分块按语义相似度切分,质量更高但解析更慢,且需绑定向量模型</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="form.chunk_strategy === 'semantic' ? '上限大小' : '分块大小'">
|
||||
<el-form-item label="分块大小">
|
||||
<el-input-number v-model="form.chunk_size" :min="50" :max="5000" :step="50" style="width: 100%" />
|
||||
<div class="ds-tip">{{ form.chunk_strategy === 'semantic' ? '语义分块按句子相似度切分,此值为安全上限(max_chunk_size),超过上限的块会再切分;语义分块不支持重叠' : '每块最大字数,超长段落自动按句号/换行切分' }}</div>
|
||||
<div class="ds-tip">每块最大字数。系统自动组合分块策略:优先识别文档结构(条文/章节/编号等)按结构切分,无结构时依次按标题感知、语义、递归切分</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="form.chunk_strategy !== 'semantic'" label="重叠字数">
|
||||
<el-form-item label="重叠字数">
|
||||
<el-input-number v-model="form.chunk_overlap" :min="0" :max="500" :step="10" style="width: 100%" />
|
||||
<div class="ds-tip">相邻分块间的重叠字数,用于保持上下文连贯</div>
|
||||
<div class="ds-tip">相邻分块间的重叠字数,用于保持上下文连贯(语义切分路径自动忽略)</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -68,21 +62,41 @@ import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listDatasets, saveDataset, deleteDataset } from '../api/dataset.js'
|
||||
import { listModelConfigs } from '../api/model_config.js'
|
||||
import { getSettings } from '../api/settings.js'
|
||||
|
||||
const STRATEGY_NAMES = { title: '标题', recursive: '递归', semantic: '语义' }
|
||||
// 把识别出的结构正则转成可读名称(第[一二三四五六七八九十百千]+条 → 条文)
|
||||
function humanizePattern(p) {
|
||||
if (!p) return ''
|
||||
if (p.includes('条')) return '条文'
|
||||
if (p.includes('回')) return '章回'
|
||||
if (p.includes('节')) return '小节'
|
||||
if (p.includes('章')) return '章节'
|
||||
if (p.includes('篇') || p.includes('部')) return '篇章'
|
||||
if (p.includes('(') || p.includes('(')) return '序号'
|
||||
if (/\d/.test(p)) return '编号'
|
||||
return '结构单元'
|
||||
}
|
||||
|
||||
const datasets = ref([])
|
||||
const embedders = ref([])
|
||||
const defaults = ref({ chunk_size: 800, chunk_overlap: 150 })
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref(false)
|
||||
let editingRowCfgId = 0
|
||||
let editingRowChunk = { chunk_size: 0, chunk_overlap: 0, chunk_strategy: '' }
|
||||
const form = ref({ id: 0, name: '', description: '', embedding_cfg_id: 0, chunk_size: 800, chunk_overlap: 150, chunk_strategy: 'title' })
|
||||
let editingRowChunk = { chunk_size: 0, chunk_overlap: 0 }
|
||||
const form = ref({ id: 0, name: '', description: '', embedding_cfg_id: 0, chunk_size: 800, chunk_overlap: 150 })
|
||||
|
||||
onMounted(async () => {
|
||||
await load()
|
||||
try {
|
||||
const s = await getSettings()
|
||||
if (s) {
|
||||
defaults.value.chunk_size = s.chunk_size || 800
|
||||
defaults.value.chunk_overlap = s.chunk_overlap ?? 150
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
try {
|
||||
const m = await listModelConfigs('embedding')
|
||||
if (m && m.list) embedders.value = m.list
|
||||
@@ -107,20 +121,20 @@ function embeddingName(id) {
|
||||
function openCreate() {
|
||||
editing.value = false
|
||||
editingRowCfgId = 0
|
||||
editingRowChunk = { chunk_size: 0, chunk_overlap: 0, chunk_strategy: '' }
|
||||
// 默认选中默认向量模型,其次第一个
|
||||
editingRowChunk = { chunk_size: 0, chunk_overlap: 0 }
|
||||
// 默认选中默认向量模型,其次第一个;分块大小/重叠带出全局设置值
|
||||
const def = embedders.value.find(x => x.is_default === 1) || embedders.value[0]
|
||||
form.value = { id: 0, name: '', description: '', embedding_cfg_id: def?.id || 0, chunk_size: 800, chunk_overlap: 150, chunk_strategy: 'title' }
|
||||
form.value = { id: 0, name: '', description: '', embedding_cfg_id: def?.id || 0, chunk_size: defaults.value.chunk_size, chunk_overlap: defaults.value.chunk_overlap }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row) {
|
||||
editing.value = true
|
||||
editingRowCfgId = row.embedding_cfg_id
|
||||
editingRowChunk = { chunk_size: row.chunk_size || 800, chunk_overlap: row.chunk_overlap ?? 150, chunk_strategy: row.chunk_strategy || 'title' }
|
||||
editingRowChunk = { chunk_size: row.chunk_size || 800, chunk_overlap: row.chunk_overlap ?? 150 }
|
||||
form.value = {
|
||||
id: row.id, name: row.name, description: row.description, embedding_cfg_id: row.embedding_cfg_id,
|
||||
chunk_size: editingRowChunk.chunk_size, chunk_overlap: editingRowChunk.chunk_overlap, chunk_strategy: editingRowChunk.chunk_strategy,
|
||||
chunk_size: editingRowChunk.chunk_size, chunk_overlap: editingRowChunk.chunk_overlap,
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
@@ -135,11 +149,11 @@ async function save() {
|
||||
return
|
||||
}
|
||||
const modelChanged = editing.value && form.value.embedding_cfg_id !== editingRowCfgId
|
||||
const chunkChanged = editing.value && (form.value.chunk_size !== editingRowChunk.chunk_size || form.value.chunk_overlap !== editingRowChunk.chunk_overlap || form.value.chunk_strategy !== editingRowChunk.chunk_strategy)
|
||||
const chunkChanged = editing.value && (form.value.chunk_size !== editingRowChunk.chunk_size || form.value.chunk_overlap !== editingRowChunk.chunk_overlap)
|
||||
if (modelChanged || chunkChanged) {
|
||||
const changed = []
|
||||
if (modelChanged) changed.push('向量模型')
|
||||
if (chunkChanged) changed.push('分块策略')
|
||||
if (chunkChanged) changed.push('分块配置')
|
||||
try {
|
||||
await ElMessageBox.confirm(`${changed.join('与')}已变更,保存后将重新处理该数据集下所有文档(异步进行,可在文档列表查看进度)。是否继续?`, '重新处理文档', { type: 'warning' })
|
||||
} catch {
|
||||
|
||||
+162
-22
@@ -4,33 +4,44 @@
|
||||
<el-select v-model="datasetId" placeholder="选择知识库" style="width: 260px" @change="load">
|
||||
<el-option v-for="d in datasets" :key="d.id" :label="d.name" :value="d.id" />
|
||||
</el-select>
|
||||
<span class="kg-tip">实体与关系由解析流水线中的 LLM 抽取生成,问答时命中实体自动注入一跳邻居</span>
|
||||
<span class="kg-tip">实体与关系由解析流水线中的 LLM 抽取生成,问答时命中实体自动注入一跳邻居;点击节点高亮其关联实体</span>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="10">
|
||||
<el-col :span="15">
|
||||
<el-card shadow="never" class="kg-card">
|
||||
<template #header>实体({{ entityTotal }})</template>
|
||||
<el-table :data="entities" size="small" v-loading="loading">
|
||||
<el-table-column prop="name" label="实体" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="entity_type" label="类型" width="110" />
|
||||
<el-table-column prop="chunk_id" label="来源分块" width="100" />
|
||||
</el-table>
|
||||
<el-pagination class="kg-page-bar" small layout="prev, pager, next" :total="entityTotal"
|
||||
:page-size="entityPageSize" v-model:current-page="entityPage" @current-change="loadEntities" />
|
||||
<template #header>
|
||||
<div class="kg-graph-head">
|
||||
<span>关系图谱({{ graphCountText }})</span>
|
||||
<el-tag v-if="truncated" size="small" type="warning">实体过多,仅展示度数最高的前 {{ MAX_NODES }} 个</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div ref="graphEl" class="kg-graph" v-loading="loading"></div>
|
||||
<el-empty v-if="!loading && graphNodes.length === 0" description="暂无图谱数据,上传文档并完成解析后自动生成" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<el-col :span="9">
|
||||
<el-card shadow="never" class="kg-card">
|
||||
<template #header>关系({{ relationTotal }})</template>
|
||||
<el-table :data="relations" size="small" v-loading="loading">
|
||||
<el-table-column prop="head" label="主体" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="relation" label="关系" min-width="100" show-overflow-tooltip />
|
||||
<el-table-column prop="tail" label="客体" min-width="140" show-overflow-tooltip />
|
||||
<el-table-column prop="chunk_id" label="来源分块" width="100" />
|
||||
</el-table>
|
||||
<el-pagination class="kg-page-bar" small layout="prev, pager, next" :total="relationTotal"
|
||||
:page-size="relationPageSize" v-model:current-page="relationPage" @current-change="loadRelations" />
|
||||
<el-tabs v-model="sideTab">
|
||||
<el-tab-pane :label="`实体(${entityTotal})`" name="entity">
|
||||
<el-table :data="entities" size="small" height="440" v-loading="loading">
|
||||
<el-table-column prop="name" label="实体" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="entity_type" label="类型" width="100" />
|
||||
<el-table-column prop="chunk_id" label="来源分块" width="90" />
|
||||
</el-table>
|
||||
<el-pagination class="kg-page-bar" small layout="prev, pager, next" :total="entityTotal"
|
||||
:page-size="entityPageSize" v-model:current-page="entityPage" @current-change="loadEntities" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="`关系(${relationTotal})`" name="relation">
|
||||
<el-table :data="relations" size="small" height="440" v-loading="loading">
|
||||
<el-table-column prop="head" label="主体" min-width="110" show-overflow-tooltip />
|
||||
<el-table-column prop="relation" label="关系" min-width="90" show-overflow-tooltip />
|
||||
<el-table-column prop="tail" label="客体" min-width="110" show-overflow-tooltip />
|
||||
</el-table>
|
||||
<el-pagination class="kg-page-bar" small layout="prev, pager, next" :total="relationTotal"
|
||||
:page-size="relationPageSize" v-model:current-page="relationPage" @current-change="loadRelations" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
@@ -38,13 +49,23 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import * as echarts from 'echarts/core'
|
||||
import { GraphChart } from 'echarts/charts'
|
||||
import { TooltipComponent, LegendComponent } from 'echarts/components'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { listDatasets } from '../api/dataset.js'
|
||||
import { listEntities, listRelations } from '../api/kg.js'
|
||||
|
||||
echarts.use([GraphChart, TooltipComponent, LegendComponent, CanvasRenderer])
|
||||
|
||||
const MAX_NODES = 400 // 力导向图节点上限,超过取度数最高者,避免布局卡顿
|
||||
|
||||
const datasets = ref([])
|
||||
const datasetId = ref(null)
|
||||
const loading = ref(false)
|
||||
const graphEl = ref(null)
|
||||
const truncated = ref(false)
|
||||
|
||||
const entities = ref([])
|
||||
const entityTotal = ref(0)
|
||||
@@ -56,6 +77,14 @@ const relationTotal = ref(0)
|
||||
const relationPage = ref(1)
|
||||
const relationPageSize = 20
|
||||
|
||||
const sideTab = ref('entity')
|
||||
let chart = null
|
||||
let resizeObserver = null
|
||||
|
||||
const graphCountText = computed(() => `${truncated.value ? '≈' : ''}${graphNodes.value.length} 实体 / ${graphLinks.value.length} 关系`)
|
||||
const graphNodes = ref([])
|
||||
const graphLinks = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const d = await listDatasets()
|
||||
@@ -65,10 +94,107 @@ onMounted(async () => {
|
||||
await load()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
resizeObserver?.disconnect()
|
||||
chart?.dispose()
|
||||
})
|
||||
|
||||
async function load() {
|
||||
entityPage.value = 1
|
||||
relationPage.value = 1
|
||||
await Promise.all([loadEntities(), loadRelations()])
|
||||
loading.value = true
|
||||
try {
|
||||
const [allEntities, allRelations] = await Promise.all([
|
||||
listEntities({ dataset_id: datasetId.value, page: 1, page_size: 100000 }),
|
||||
listRelations({ dataset_id: datasetId.value, page: 1, page_size: 100000 }),
|
||||
])
|
||||
buildGraph(allEntities?.list || [], allRelations?.list || [])
|
||||
await Promise.all([loadEntities(), loadRelations()])
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 实体按名字去重合并(同一实体可能出现在多个分块),按出入度决定节点大小
|
||||
function buildGraph(entitiesList, relationsList) {
|
||||
const byName = new Map()
|
||||
for (const e of entitiesList) {
|
||||
const prev = byName.get(e.name)
|
||||
if (prev) {
|
||||
prev.chunks = [...new Set([...prev.chunks, e.chunk_id])]
|
||||
} else {
|
||||
byName.set(e.name, { name: e.name, entity_type: e.entity_type, chunks: [e.chunk_id] })
|
||||
}
|
||||
}
|
||||
const degree = new Map()
|
||||
const edges = []
|
||||
const seenEdges = new Set()
|
||||
for (const r of relationsList) {
|
||||
if (!byName.has(r.head) || !byName.has(r.tail)) continue
|
||||
const key = `${r.head}|${r.relation}|${r.tail}`
|
||||
if (seenEdges.has(key)) continue
|
||||
seenEdges.add(key)
|
||||
edges.push({ source: r.head, target: r.tail, relation: r.relation })
|
||||
degree.set(r.head, (degree.get(r.head) || 0) + 1)
|
||||
degree.set(r.tail, (degree.get(r.tail) || 0) + 1)
|
||||
}
|
||||
let nodes = [...byName.values()]
|
||||
truncated.value = nodes.length > MAX_NODES
|
||||
if (truncated.value) {
|
||||
nodes.sort((a, b) => (degree.get(b.name) || 0) - (degree.get(a.name) || 0))
|
||||
const keep = new Set(nodes.slice(0, MAX_NODES).map(n => n.name))
|
||||
nodes = nodes.filter(n => keep.has(n.name))
|
||||
for (const e of [...edges]) {
|
||||
if (!keep.has(e.source) || !keep.has(e.target)) edges.splice(edges.indexOf(e), 1)
|
||||
}
|
||||
}
|
||||
const minD = Math.min(...nodes.map(n => degree.get(n.name) || 0))
|
||||
const maxD = Math.max(...nodes.map(n => degree.get(n.name) || 0))
|
||||
const sizeFor = d => (maxD === minD ? 30 : 20 + ((d - minD) / (maxD - minD)) * 36)
|
||||
graphNodes.value = nodes.map(n => ({
|
||||
id: n.name,
|
||||
name: n.name,
|
||||
category: n.entity_type || '未分类',
|
||||
symbolSize: sizeFor(degree.get(n.name) || 0),
|
||||
}))
|
||||
graphLinks.value = edges
|
||||
renderGraph()
|
||||
}
|
||||
|
||||
function renderGraph() {
|
||||
if (!graphEl.value) return
|
||||
chart ??= echarts.init(graphEl.value)
|
||||
const types = [...new Set(graphNodes.value.map(n => n.category))]
|
||||
chart.setOption({
|
||||
legend: { bottom: 0, type: 'scroll', data: types, textStyle: { fontSize: 11 } },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: p => {
|
||||
if (p.dataType === 'edge') {
|
||||
const e = graphLinks.value[p.dataIndex]
|
||||
return `${e.source} → ${e.target}<br/>关系:${e.relation}`
|
||||
}
|
||||
return `${p.name}<br/>类型:${p.data.category}`
|
||||
},
|
||||
},
|
||||
series: [{
|
||||
type: 'graph',
|
||||
layout: 'force',
|
||||
roam: true,
|
||||
draggable: true,
|
||||
data: graphNodes.value,
|
||||
links: graphLinks.value,
|
||||
categories: types.map(t => ({ name: t })),
|
||||
force: { repulsion: 260, edgeLength: [40, 110], gravity: 0.08 },
|
||||
label: { show: true, position: 'right', fontSize: 10, color: '#606266' },
|
||||
edgeLabel: { show: false },
|
||||
emphasis: {
|
||||
focus: 'adjacency', // 点击节点自动高亮一跳邻居并淡化其余
|
||||
label: { fontSize: 12, fontWeight: 600 },
|
||||
lineStyle: { width: 2 },
|
||||
},
|
||||
}],
|
||||
}, true)
|
||||
}
|
||||
|
||||
async function loadEntities() {
|
||||
@@ -94,6 +220,12 @@ async function loadRelations() {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 布局稳定后渲染一次(图表容器尺寸依赖 el-row 布局),并监听容器尺寸变化
|
||||
resizeObserver = new ResizeObserver(() => nextTick(renderGraph))
|
||||
resizeObserver.observe(graphEl.value)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -107,6 +239,14 @@ async function loadRelations() {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
.kg-graph {
|
||||
height: 620px;
|
||||
}
|
||||
.kg-graph-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.kg-page-bar {
|
||||
margin-top: 10px;
|
||||
justify-content: flex-end;
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
<template>
|
||||
<div class="settings-page">
|
||||
<div class="block-card">
|
||||
<div class="block-title">分块默认值</div>
|
||||
<div class="block-tip">新建数据集时自动带出,单个数据集仍可在表单中修改</div>
|
||||
<div class="block-row">
|
||||
<span class="field-label">分块大小</span>
|
||||
<el-input-number v-model="chunkForm.chunk_size" :min="50" :max="5000" :step="50" />
|
||||
<span class="field-label">重叠字数</span>
|
||||
<el-input-number v-model="chunkForm.chunk_overlap" :min="0" :max="500" :step="10" />
|
||||
<el-button type="primary" :loading="chunkSaving" @click="saveChunkDefaults">保存</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-head">
|
||||
<el-radio-group v-model="modelType" @change="loadModels">
|
||||
<el-radio-button value="chat">对话模型</el-radio-button>
|
||||
@@ -65,8 +77,32 @@
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listModelConfigs, saveModelConfig, deleteModelConfig, testModelConfig, setDefaultModelConfig } from '../api/model_config.js'
|
||||
import { getSettings, saveSettings } from '../api/settings.js'
|
||||
|
||||
const modelType = ref('chat')
|
||||
|
||||
const chunkForm = ref({ chunk_size: 800, chunk_overlap: 150 })
|
||||
const chunkSaving = ref(false)
|
||||
|
||||
async function loadChunkDefaults() {
|
||||
try {
|
||||
const s = await getSettings()
|
||||
if (s) {
|
||||
chunkForm.value.chunk_size = s.chunk_size || 800
|
||||
chunkForm.value.chunk_overlap = s.chunk_overlap ?? 150
|
||||
}
|
||||
} catch { /* 忽略 */ }
|
||||
}
|
||||
|
||||
async function saveChunkDefaults() {
|
||||
chunkSaving.value = true
|
||||
try {
|
||||
await saveSettings(chunkForm.value)
|
||||
ElMessage.success('分块默认值已保存')
|
||||
} finally {
|
||||
chunkSaving.value = false
|
||||
}
|
||||
}
|
||||
const models = ref([])
|
||||
const allModels = ref([])
|
||||
const modelLoading = ref(false)
|
||||
@@ -78,6 +114,7 @@ const modelForm = ref({ id: 0, name: '', model_type: 'chat', model_name: '', end
|
||||
|
||||
onMounted(async () => {
|
||||
await loadModels()
|
||||
await loadChunkDefaults()
|
||||
try { const all = await listModelConfigs(''); if (all && all.list) allModels.value = all.list } catch { /* 忽略 */ }
|
||||
})
|
||||
|
||||
@@ -180,4 +217,28 @@ async function test(row) {
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.block-card {
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.block-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.block-tip {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.block-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.field-label {
|
||||
color: #606266;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user