ow
This commit is contained in:
1 parent
9344b56334
commit
441d1e821e
37 files changed
+3706
-23
No files matched your search
@@ -0,0 +1,432 @@
|
||||
<template>
|
||||
<dragModal wrap-class-name="help-tl-modal" :width="760" v-model:open="visible" @cancel="onCancel">
|
||||
<template #title>
|
||||
<div class="htl-title">
|
||||
<span class="htl-title-icon" :style="{ background: themeSoft, color: themeColor }">
|
||||
<cust-icon-font :type="themeIcon" :size="18" />
|
||||
</span>
|
||||
<span class="htl-title-text">{{ props.title || "使用帮助" }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="htl-body">
|
||||
<a-spin :spinning="showLoading">
|
||||
<!-- 功能简介 -->
|
||||
<p class="htl-intro">{{ doc.intro }}</p>
|
||||
|
||||
<!-- 未编写专属文档时的提示,避免用户以为看到了正确流程 -->
|
||||
<div v-if="!isSpecific" class="htl-notice">
|
||||
<span class="htl-notice-tag">通用指引</span>
|
||||
<span>该功能暂无专属帮助文档,下面是各功能页面的通用操作说明。</span>
|
||||
</div>
|
||||
|
||||
<!-- 操作流程:按先后顺序 -->
|
||||
<div class="htl-section-title">操作流程</div>
|
||||
<a-timeline class="htl-steps">
|
||||
<a-timeline-item v-for="(s, i) in doc.steps" :key="'step-' + i">
|
||||
<template #dot>
|
||||
<span class="htl-dot" :style="{ background: themeColor }">
|
||||
<cust-icon-font v-if="isFontIcon(s.icon)" :type="s.icon" :size="13" />
|
||||
<component v-else-if="s.icon" :is="antdIcon(s.icon)" />
|
||||
<template v-else>{{ i + 1 }}</template>
|
||||
</span>
|
||||
</template>
|
||||
<div class="htl-step">
|
||||
<div class="htl-step-head">
|
||||
<span class="htl-step-no">{{ i + 1 }}</span>
|
||||
<span class="htl-step-title">{{ s.title }}</span>
|
||||
</div>
|
||||
<div class="htl-step-desc">{{ s.desc }}</div>
|
||||
<div v-if="s.tips" class="htl-step-tips">
|
||||
<span class="htl-step-tips-label">提示</span>
|
||||
<span>{{ s.tips }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-timeline-item>
|
||||
</a-timeline>
|
||||
|
||||
<!-- 功能点 / 图标说明 -->
|
||||
<div class="htl-section-title">功能点与图标说明</div>
|
||||
<div class="htl-features">
|
||||
<div v-for="(f, i) in doc.features" :key="'feat-' + i" class="htl-feature">
|
||||
<span class="htl-feature-icon" :style="{ background: themeSoft, color: themeColor }">
|
||||
<cust-icon-font v-if="isFontIcon(f.icon)" :type="f.icon" :size="16" />
|
||||
<!-- 其余一律按 Ant Design 图标组件名解析,保证与页面上看到的完全一致 -->
|
||||
<component v-else :is="antdIcon(f.icon)" :style="{ color: f.color }" />
|
||||
</span>
|
||||
<div class="htl-feature-text">
|
||||
<div class="htl-feature-name">{{ f.name }}</div>
|
||||
<div class="htl-feature-desc">{{ f.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
|
||||
<!-- 只需要阅读,不需要确认按钮:传空 footer 占位隐藏默认按钮区 -->
|
||||
<template #footer>
|
||||
<span />
|
||||
</template>
|
||||
</dragModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import * as antdIcons from "@ant-design/icons-vue";
|
||||
import { QuestionCircleOutlined } from "@ant-design/icons-vue";
|
||||
import dragModal from "@/components/basic/DragModal.vue";
|
||||
import { assembleOrDefault } from "@/data/help/helpDocAssembler";
|
||||
import securityHelpFunctionService from "@/service/security/securityHelpFunctionService";
|
||||
import { buildSeedHelpDocData } from "@/data/help/helpDocSeed";
|
||||
import { SecurityHelpFunctionData } from "@/data/auto/securityHelpFunctionData";
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean;
|
||||
/** 目标功能的英文名,决定加载哪份帮助文档 */
|
||||
functionName?: string;
|
||||
/** 弹窗标题,一般为功能的本地化名称 */
|
||||
title?: string;
|
||||
/** 主题色,沿用所属分组的颜色保持视觉一致 */
|
||||
color?: string;
|
||||
soft?: string;
|
||||
icon?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "update:open", value: boolean): void;
|
||||
(e: "cancel"): void;
|
||||
}>();
|
||||
|
||||
/** 双向绑定弹窗显示状态 */
|
||||
const visible = computed({
|
||||
get: () => props.open,
|
||||
set: (v: boolean) => emit("update:open", v),
|
||||
});
|
||||
|
||||
/**
|
||||
* 接口还没就绪时就先用地上的种子数据(内容和建表数据一致),
|
||||
* 这样弹窗永远有东西可看,不会白屏。
|
||||
*/
|
||||
const seedData: SecurityHelpFunctionData = buildSeedHelpDocData();
|
||||
|
||||
/** 当前实际使用的数据:默认种子数据,接口取到后整体替换 */
|
||||
const rows = ref<SecurityHelpFunctionData>(seedData);
|
||||
const loading = ref(false);
|
||||
|
||||
/** 从表里取数据:接口优先,取不到就继续用种子数据 */
|
||||
const loadRows = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const data = await securityHelpFunctionService.FindAll();
|
||||
// 接口返回空说明表里还没灌数据,继续用种子数据
|
||||
if (data && data.SecurityHelpFunctions && data.SecurityHelpFunctions.length > 0) {
|
||||
rows.value = data;
|
||||
}
|
||||
} catch {
|
||||
rows.value = seedData;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadRows);
|
||||
|
||||
/** 把表行组合成页面要显示的文档;表里没有这个功能时退回通用兜底 */
|
||||
const doc = computed(() => assembleOrDefault(rows.value, props.functionName));
|
||||
|
||||
/**
|
||||
* false 表示表里没有这个功能的专属文档、拿到的是通用兜底,
|
||||
* 这种情况要在弹窗顶部提示,免得用户以为看到的就是本功能的流程。
|
||||
*/
|
||||
const isSpecific = computed(
|
||||
() =>
|
||||
!!props.functionName &&
|
||||
rows.value.SecurityHelpFunctions.some((f) => f.FunctionName === props.functionName)
|
||||
);
|
||||
|
||||
/** 只有真的还没有内容时才显示加载态,避免种子数据明明有内容还挡一层遮罩 */
|
||||
const showLoading = computed(() => loading.value && doc.value.steps.length === 0);
|
||||
|
||||
const themeColor = computed(() => props.color || "#1677ff");
|
||||
const themeSoft = computed(() => props.soft || "#e6f4ff");
|
||||
const themeIcon = computed(() => props.icon || "icon-help");
|
||||
|
||||
/**
|
||||
* 图标分两类,判别规则很直白:
|
||||
* 以 "icon-" 开头 → 项目自定义的 iconfont(如云盘的「预览」icon-qingbaojiankong)
|
||||
* 其余 → Ant Design 官方图标组件名(如 DeleteOutlined)
|
||||
* 这样帮助里出现的图标和用户在页面上真实看到的那个是同一个。
|
||||
*/
|
||||
const isFontIcon = (name?: string) => !!name && name.startsWith("icon-");
|
||||
|
||||
/**
|
||||
* 按名字取出 antd 图标组件。
|
||||
* 名字写错或组件库里没有时,退化成问号图标而不是渲染空白,避免出现一个空方块。
|
||||
*/
|
||||
const antdIcon = (name?: string) =>
|
||||
(name && (antdIcons as Record<string, unknown>)[name]) || QuestionCircleOutlined;
|
||||
|
||||
const onCancel = () => {
|
||||
emit("cancel");
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.htl-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.htl-title-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.htl-title-text {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.htl-body {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
.htl-intro {
|
||||
margin: 0 0 4px;
|
||||
color: #303133;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
/* 兜底提示:温和提醒,不与主内容抢视线 */
|
||||
.htl-notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
margin: 10px 0 14px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ffe7ba;
|
||||
border-radius: 8px;
|
||||
background: #fff7e6;
|
||||
color: #ad6800;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.htl-notice-tag {
|
||||
flex: none;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: #faad14;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 17px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.htl-section-title {
|
||||
position: relative;
|
||||
margin: 14px 0 12px;
|
||||
padding-left: 10px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
width: 3px;
|
||||
height: 13px;
|
||||
border-radius: 2px;
|
||||
background: #1677ff;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== 时间轴步骤 ==================== */
|
||||
.htl-steps {
|
||||
padding-top: 2px;
|
||||
margin-left:4px;
|
||||
}
|
||||
|
||||
/* antd 默认把内容顶得很开,这里收紧,让「操作流程」更像一套连贯的步骤 */
|
||||
.htl-steps :deep(.ant-timeline-item-content) {
|
||||
margin-left: 0;
|
||||
padding-left: 20px;
|
||||
top: -4px;
|
||||
}
|
||||
|
||||
.htl-steps :deep(.ant-timeline-item-tail) {
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
.htl-dot {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.06);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.htl-step {
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.htl-step-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.htl-step-no {
|
||||
flex: none;
|
||||
min-width: 17px;
|
||||
height: 17px;
|
||||
padding: 0 4px;
|
||||
border-radius: 9px;
|
||||
background: #f0f2f5;
|
||||
color: #8a919f;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 17px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.htl-step-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.htl-step-desc {
|
||||
color: #5c6470;
|
||||
font-size: 13px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.htl-step-tips {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
padding: 7px 10px;
|
||||
border-left: 3px solid #faad14;
|
||||
border-radius: 0 6px 6px 0;
|
||||
background: #fffbe6;
|
||||
color: #8a6116;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.htl-step-tips-label {
|
||||
flex: none;
|
||||
font-weight: 600;
|
||||
color: #ad6800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ==================== 功能点网格 ==================== */
|
||||
.htl-features {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 宽屏时双列,减少纵向滚动 */
|
||||
@media (min-width: 900px) {
|
||||
.htl-features {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.htl-feature {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 9px;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid #f0f1f3;
|
||||
border-radius: 9px;
|
||||
background: #fafbfc;
|
||||
transition: border-color 0.16s ease, background 0.16s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #d6e4ff;
|
||||
background: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.htl-feature-icon {
|
||||
flex: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.htl-feature-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.htl-feature-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1f2329;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.htl-feature-desc {
|
||||
color: #6b7280;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- 非 scoped:这部分要作用于 DragModal teleport 出去的 antd Modal 自身结构,scoped 打不到。
|
||||
全部写在 .help-tl-modal 这个 wrapClass 之下,只影响本组件,不会波及其它弹窗。 -->
|
||||
<style lang="less">
|
||||
.help-tl-modal {
|
||||
/* 隐藏空的底部按钮区(发了空 footer 插槽占位,只是为了让 antd 不渲染默认按钮) */
|
||||
.ant-modal-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 帮助文档通常比较长:限制高度并让内容区自己滚动 */
|
||||
.ant-modal-body {
|
||||
max-height: 68vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 窄按标题文字,留出右边三个操作图标的位置 */
|
||||
.ant-modal-title {
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.ant-modal {
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
max-height: 74vh;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -26,7 +26,7 @@
|
||||
type="text"
|
||||
class="nav-btn"
|
||||
:loading="loading"
|
||||
@click="onBreadcrumbClick(breadcrumbItems[0], 0)"
|
||||
@click="onRefresh"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</a-button>
|
||||
@@ -117,6 +117,16 @@ const onSearch = () => {
|
||||
const onBreadcrumbClick = (item: BreadcrumbItem, index: number) => {
|
||||
emit("breadcrumb-click", item, index);
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新:重新加载「当前所在目录」,也就是路径的最后一个节点。
|
||||
* 不能取第一个节点,否则会把路径截回到根目录,变成跳转而不是刷新。
|
||||
*/
|
||||
const onRefresh = () => {
|
||||
const lastIndex = breadcrumbItems.value.length - 1;
|
||||
if (lastIndex < 0) return;
|
||||
onBreadcrumbClick(breadcrumbItems.value[lastIndex], lastIndex);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
type="text"
|
||||
class="nav-btn"
|
||||
:loading="loading"
|
||||
@click="onBreadcrumbClick(breadcrumbItems[0], 0)"
|
||||
@click="onRefresh"
|
||||
>
|
||||
<ReloadOutlined />
|
||||
</a-button> </a-tooltip
|
||||
@@ -248,6 +248,16 @@ const onSearch = () => {
|
||||
const onBreadcrumbClick = (item: BreadcrumbItem, index: number) => {
|
||||
emit("breadcrumb-click", item, index);
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新:重新加载「当前所在目录」,也就是路径的最后一个节点。
|
||||
* 不能取第一个节点,否则会把路径截回到根目录,变成跳转而不是刷新。
|
||||
*/
|
||||
const onRefresh = () => {
|
||||
const lastIndex = breadcrumbItems.value.length - 1;
|
||||
if (lastIndex < 0) return;
|
||||
onBreadcrumbClick(breadcrumbItems.value[lastIndex], lastIndex);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
<span class="row-title">{{ it.FunctionLocalName }}</span>
|
||||
<span v-if="subtitleOf(it)" class="row-sub">{{ subtitleOf(it) }}</span>
|
||||
</span>
|
||||
<span class="row-glyph" :style="{ background: soft, color }">
|
||||
<cust-icon-font :type="icon" :size="18" />
|
||||
<span class="row-glyph" :style="{ background: soft, color }" title="使用帮助"
|
||||
@click.stop="onHelp(it)">
|
||||
<cust-icon-font type="icon-help" :size="18" />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -50,6 +51,8 @@ const props = defineProps<{
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "navigate", route: string): void;
|
||||
/** 点击行尾圆形图标:请求打开该功能的新手帮助 */
|
||||
(e: "help", item: GroupCardItem): void;
|
||||
}>();
|
||||
|
||||
const coverStyle = computed(() =>
|
||||
@@ -59,11 +62,14 @@ const coverStyle = computed(() =>
|
||||
);
|
||||
|
||||
/** 副标题优先取自定义 subtitle,其次回落英文名;都为空则不渲染该行 */
|
||||
const subtitleOf = (it: GroupCardItem) => (it.subtitle || it.FunctionEngName || "").trim();
|
||||
const subtitleOf = (it: GroupCardItem) => (it.subtitle || it.Remark || "").trim();
|
||||
|
||||
const onClick = (route?: string) => {
|
||||
if (route) emit("navigate", route);
|
||||
};
|
||||
|
||||
/** 点击行尾圆形图标:请求帮助(.stop 已阻止它冒泡到整行的路由跳转) */
|
||||
const onHelp = (it: GroupCardItem) => emit("help", it);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -196,6 +202,20 @@ const onClick = (route?: string) => {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition:
|
||||
transform 0.16s ease,
|
||||
box-shadow 0.16s ease;
|
||||
|
||||
/* 悬停 / 按下给出反馈,提示这里是打开「使用帮助」的入口 */
|
||||
&:hover {
|
||||
box-shadow: 0 0 0 4px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
Reference in new issue
Block a user