This commit is contained in:
owenchen committed 2026-10-15 19:26:18 +08:00
1 parent 23d14ba54c
commit dcc112c076
17 files changed
+636 -31

No files matched your search

@@ -0,0 +1,22 @@
/**
* 帮助文档预览的渲染视图模型:help-doc-preview 组件与调用它的页面共用。
*
* 单独放一个 .ts 是因为 <script setup> 里不能写 ES module export,
* 类型只能外提到这里,组件和页面各自 import。
*/
/** 一条操作步骤 */
export interface HelpStepVM {
title: string;
desc: string;
tips?: string;
icon?: string;
}
/** 一个功能点 / 图标说明 */
export interface HelpFeatureVM {
name: string;
desc: string;
icon?: string;
color?: string;
}
@@ -57,6 +57,11 @@ function actionVisible(action: AdminFormAction): boolean {
const visibleActions = computed(() => props.actions.filter(actionVisible));
/** 导航栏返回:u-navbar 没有 left-click 事件,只能通过 custom-back 回调接接;抛给页面 @back(内部走带兜底的 smartBack) */
function onNavBack(): void {
emit("back");
}
function rawValue(field: AdminFormField): unknown {
return props.model?.[field.prop];
}
@@ -118,7 +123,7 @@ function onTapAction(action: AdminFormAction): void {
<template>
<view class="admin-form">
<u-navbar v-if="navTitle" :title="navTitle" :is-back="showNavBack" :is-fixed="false" :immersive="false"
back-icon-name="arrow-leftward" @left-click="emit('back')" />
back-icon-name="arrow-leftward" :custom-back="onNavBack" />
<scroll-view class="form-body" scroll-y :show-scrollbar="false">
<view class="hero">
@@ -30,6 +30,7 @@ import {
restColumns,
rowKeyOf,
} from "@/admin/admin-column";
import { smartBack } from "@/router/routes";
/** 列表取数:只给页码与每页条数,条件由上层闭包注入(见 useAdminList.query) */
type AdminListLoader = (page: number, pageSize: number) => Promise<AdminLoadResult>;
@@ -200,6 +201,11 @@ function refresh(): void {
paging.value?.reload();
}
/** 导航栏返回:走带兜底的 smartBack,无栈时回「我的」页(security 域统一落地),避免 H5 刷新后点了没反应 */
function onNavBack(): void {
smartBack("mineTab");
}
defineExpose({ refresh, clearSelection, selectedRows });
</script>
@@ -209,7 +215,7 @@ defineExpose({ refresh, clearSelection, selectedRows });
:loading-more-enabled="true" :empty-view-text="t('AdminPage.emptyText')" @query="onQuery">
<template #top>
<u-navbar v-if="navTitle" :title="navTitle" :is-back="showNavBack" :is-fixed="false" :immersive="false"
back-icon-name="arrow-leftward" />
back-icon-name="arrow-leftward" :custom-back="onNavBack" />
<view class="toolbar">
<view class="toolbar-search">
<u-search v-model="keywordModel" :placeholder="t('AdminPage.searchPlaceholder')"
@@ -0,0 +1,485 @@
<script setup lang="ts">
/**
* 帮助文档预览弹窗:旧 WebVue components/basic/HelpTimelineModal.vue 的移动端等价物。
*
* 用途:在「帮助功能详情」里点「预览」,把当前正在编辑/查看的这份帮助数据
* 按最终用户看到的样子渲染出来——上方时间轴讲「操作流程」,下方网格讲「功能点与图标说明」。
*
* 与旧弹窗一致的结构:
* - 头部:圆形图标徽标 + 标题(+ u-popup 自带右上角关闭);
* - 功能简介 intro;
* - 操作流程:竖排时间轴,每步一个圆形徽标(有图标显图标、否则显序号)+ 序号胶囊 + 标题 + 说明 + 可选「提示」黄条;
* - 功能点与图标说明:图标徽标 + 名称 + 作用说明。
*
* 主题色沿用本项目青绿主色(旧弹窗的紫色是它所属分组的主题色,由调用方传入;
* 这里是通用预览,统一用 $u-type-primary 与全站一致)。
*
* 图标:帮助数据里存的是旧项目的图标名(Ant Design 组件名如 DeleteOutlined,或 iconfont 名 icon-xxx),
* 移动端渲染不了这些,逐个翻译到 uview-pro 内置图标名,认不出的兜底成 question-circle
* (与旧弹窗取不到图标时退化成问号的行为一致)。
*/
import type { PropType } from "vue";
import type { HelpFeatureVM, HelpStepVM } from "@/admin/help-preview-types";
import { useLocale } from "uview-pro";
import { computed } from "vue";
const props = defineProps({
visible: { type: Boolean, default: false },
/** 弹窗标题:一般是功能的本地化名称,缺省显示「使用帮助」 */
title: { type: String, default: "" },
intro: { type: String, default: "" },
steps: { type: Array as PropType<HelpStepVM[]>, default: () => [] },
features: { type: Array as PropType<HelpFeatureVM[]>, default: () => [] },
});
const emit = defineEmits<{
(e: "close"): void;
}>();
const { t } = useLocale();
/** 旧项目图标名 -> uview-pro 内置图标名(只认这张表里的名字,否则渲染空白) */
const ICON_MAP: Record<string, string> = {
FileSearchOutlined: "search",
SearchOutlined: "search",
FilterOutlined: "search",
ClearOutlined: "close-circle",
FileAddOutlined: "plus-circle",
PlusOutlined: "plus",
PlusCircleOutlined: "plus-circle",
EditOutlined: "edit-pen",
FormOutlined: "edit-pen",
SaveOutlined: "checkmark-circle",
CheckOutlined: "checkmark",
CheckCircleOutlined: "checkmark-circle",
UndoOutlined: "reload",
ReloadOutlined: "reload",
SyncOutlined: "reload",
DeleteOutlined: "trash",
UpSquareTwoTone: "arrow-up",
ArrowUpOutlined: "arrow-up",
DownSquareTwoTone: "arrow-down",
ArrowDownOutlined: "arrow-down",
LeftCircleOutlined: "arrow-left",
ArrowLeftOutlined: "arrow-left",
RightCircleOutlined: "arrow-right",
ArrowRightOutlined: "arrow-right",
HomeOutlined: "home",
AppstoreOutlined: "grid",
LaptopOutlined: "grid",
FolderOutlined: "grid",
PictureOutlined: "photo",
FileImageOutlined: "photo",
FileTextOutlined: "file-text",
ProfileOutlined: "file-text",
EyeOutlined: "eye",
DownloadOutlined: "download",
CloudDownloadOutlined: "download",
ShareAltOutlined: "share",
StarOutlined: "star",
HeartOutlined: "heart",
UserOutlined: "account",
TeamOutlined: "account",
SettingOutlined: "setting",
ClockCircleOutlined: "clock",
InfoCircleOutlined: "info-circle",
CloseCircleOutlined: "close-circle",
QuestionCircleOutlined: "question-circle",
CameraOutlined: "camera",
PlayCircleOutlined: "play-circle",
VideoCameraOutlined: "camera",
LockOutlined: "lock",
MailOutlined: "email",
PhoneOutlined: "phone",
CalendarOutlined: "calendar",
MapOutlined: "map",
};
/** 兜底图标:与旧弹窗取不到图标时退化成一问号的行为一致 */
const FALLBACK_ICON = "question-circle";
/** 把存储的图标名解析成 u-icon 认得的内置名;icon-* 是旧 iconfont,移动端没有,一律兜底 */
function resolveIcon(name?: string): string {
const key = String(name ?? "").trim();
if (!key || key.startsWith("icon-")) {
return FALLBACK_ICON;
}
return ICON_MAP[key] ?? FALLBACK_ICON;
}
/** 步骤徽标:有图标才显图标,否则显序号(对齐旧 htl-dot 的 icon-else-number) */
function stepHasIcon(step: HelpStepVM): boolean {
return String(step.icon ?? "").trim().length > 0;
}
/** 功能点图标色:数据自带 color 优先,否则用主色 */
function featureColor(color?: string): string {
return String(color ?? "").trim() || "#059669";
}
const hasContent = computed(
() =>
Boolean(String(props.intro ?? "").trim())
|| props.steps.length > 0
|| props.features.length > 0,
);
</script>
<template>
<u-popup :model-value="visible" mode="bottom" round="24" :closeable="true" close-icon-pos="top-right"
:safe-area-inset-bottom="true" @close="emit('close')">
<view class="help-preview">
<scroll-view class="hp-body" scroll-y :show-scrollbar="false">
<!-- 头部:圆形图标徽标 + 标题 -->
<view class="hp-head">
<view class="hp-head-icon">
<u-icon name="question-circle" size="30" color="#ffffff" />
</view>
<text class="hp-head-title">
{{ title || t("HelpPreview.title") }}
</text>
</view>
<!-- 空态:这份帮助还没内容 -->
<view v-if="!hasContent" class="hp-empty">
<text>
{{ t("HelpPreview.empty") }}
</text>
</view>
<view v-else>
<!-- 功能简介 -->
<text v-if="intro" class="hp-intro">
{{ intro }}
</text>
<!-- 操作流程 -->
<template v-if="steps.length">
<view class="hp-section">
<text class="hp-section-title">
{{ t("HelpPreview.flow") }}
</text>
</view>
<view class="hp-steps">
<view v-for="(step, i) in steps" :key="`step-${i}`" class="hp-step">
<view class="hp-rail">
<view class="hp-dot">
<u-icon v-if="stepHasIcon(step)" :name="resolveIcon(step.icon)" size="22"
color="#ffffff" />
<text v-else class="hp-dot-no">
{{ i + 1 }}
</text>
</view>
<view v-if="i !== steps.length - 1" class="hp-line" />
</view>
<view class="hp-step-main">
<view class="hp-step-head">
<text class="hp-step-no">
{{ i + 1 }}
</text>
<text class="hp-step-title">
{{ step.title }}
</text>
</view>
<text v-if="step.desc" class="hp-step-desc">
{{ step.desc }}
</text>
<view v-if="step.tips" class="hp-step-tips">
<text class="hp-tips-label">
{{ t("HelpPreview.tip") }}
</text>
<text class="hp-tips-text">
{{ step.tips }}
</text>
</view>
</view>
</view>
</view>
</template>
<!-- 功能点与图标说明 -->
<template v-if="features.length">
<view class="hp-section">
<text class="hp-section-title">
{{ t("HelpPreview.features") }}
</text>
</view>
<view class="hp-features">
<view v-for="(feat, i) in features" :key="`feat-${i}`" class="hp-feature">
<view class="hp-feature-icon">
<u-icon :name="resolveIcon(feat.icon)" size="28"
:color="featureColor(feat.color)" />
</view>
<view class="hp-feature-text">
<text class="hp-feature-name">
{{ feat.name }}
</text>
<text v-if="feat.desc" class="hp-feature-desc">
{{ feat.desc }}
</text>
</view>
</view>
</view>
</template>
</view>
</scroll-view>
</view>
</u-popup>
</template>
<style lang="scss" scoped>
.help-preview {
display: flex;
flex-direction: column;
max-height: 82vh;
padding: 8rpx 32rpx 24rpx;
background-color: #ffffff;
border-radius: 24rpx;
}
.hp-body {
flex: 1;
min-height: 0;
max-height: calc(82vh - 32rpx);
}
/* 头部 */
.hp-head {
display: flex;
flex-direction: row;
align-items: center;
padding: 24rpx 4rpx 8rpx;
}
.hp-head-icon {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 52rpx;
height: 52rpx;
margin-right: 16rpx;
background: linear-gradient(135deg, $u-type-primary-dark, $u-type-primary);
border-radius: 50%;
}
.hp-head-title {
flex: 1;
min-width: 0;
font-size: 32rpx;
font-weight: 700;
color: $u-main-color;
}
/* 空态 */
.hp-empty {
padding: 80rpx 20rpx;
font-size: 26rpx;
color: $u-tips-color;
text-align: center;
}
/* 简介 */
.hp-intro {
display: block;
padding: 8rpx 4rpx 4rpx;
font-size: 27rpx;
line-height: 1.8;
color: $u-content-color;
}
/* 分节标题:左侧竖条 */
.hp-section {
display: flex;
flex-direction: row;
align-items: center;
margin: 32rpx 0 20rpx;
}
.hp-section-title {
position: relative;
padding-left: 18rpx;
font-size: 28rpx;
font-weight: 700;
color: $u-main-color;
}
.hp-section-title::before {
position: absolute;
top: 50%;
left: 0;
width: 6rpx;
height: 26rpx;
content: "";
background-color: $u-type-primary;
border-radius: 3rpx;
transform: translateY(-50%);
}
/* 时间轴步骤 */
.hp-steps {
padding-left: 4rpx;
}
.hp-step {
display: flex;
flex-direction: row;
align-items: stretch;
}
.hp-rail {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 48rpx;
}
.hp-dot {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
background: linear-gradient(135deg, $u-type-primary-dark, $u-type-primary);
border-radius: 50%;
box-shadow: 0 0 0 4rpx rgba(5, 150, 105, 0.12);
}
.hp-dot-no {
font-size: 24rpx;
font-weight: 700;
color: #ffffff;
}
.hp-line {
flex: 1;
min-height: 20rpx;
width: 2rpx;
margin: 6rpx 0;
background-color: $u-border-color;
}
.hp-step-main {
flex: 1;
min-width: 0;
padding: 0 0 28rpx 20rpx;
}
.hp-step-head {
display: flex;
flex-direction: row;
align-items: center;
}
.hp-step-no {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
min-width: 34rpx;
height: 34rpx;
padding: 0 10rpx;
margin-right: 12rpx;
font-size: 22rpx;
font-weight: 700;
color: $u-tips-color;
background-color: #f0f2f5;
border-radius: 17rpx;
}
.hp-step-title {
flex: 1;
min-width: 0;
font-size: 29rpx;
font-weight: 700;
color: $u-main-color;
}
.hp-step-desc {
display: block;
margin-top: 10rpx;
font-size: 26rpx;
line-height: 1.8;
color: $u-content-color;
}
/* 提示黄条 */
.hp-step-tips {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 14rpx 18rpx;
margin-top: 14rpx;
background-color: #fffbe6;
border-left: 6rpx solid #faad14;
border-radius: 0 10rpx 10rpx 0;
}
.hp-tips-label {
flex-shrink: 0;
margin-right: 10rpx;
font-size: 24rpx;
font-weight: 700;
color: #ad6800;
}
.hp-tips-text {
flex: 1;
min-width: 0;
font-size: 24rpx;
line-height: 1.7;
color: #8a6116;
}
/* 功能点网格 */
.hp-features {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.hp-feature {
display: flex;
flex-direction: row;
align-items: flex-start;
padding: 20rpx 22rpx;
background-color: #fafbfc;
border: 1rpx solid #f0f1f3;
border-radius: 16rpx;
}
.hp-feature-icon {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 52rpx;
height: 52rpx;
margin-right: 18rpx;
background-color: rgba(5, 150, 105, 0.1);
border-radius: 12rpx;
}
.hp-feature-text {
flex: 1;
min-width: 0;
}
.hp-feature-name {
display: block;
font-size: 27rpx;
font-weight: 700;
color: $u-main-color;
}
.hp-feature-desc {
display: block;
margin-top: 6rpx;
font-size: 24rpx;
line-height: 1.7;
color: #6b7280;
}
</style>
+9 -1
View File
@@ -208,7 +208,15 @@
"stepIconPlaceholder": "Icon name on the page",
"featureTitle": "Referenced Feature Groups",
"addFeatureBut": "Reference Feature Group",
"featureEmpty": "No feature group referenced"
"featureEmpty": "No feature group referenced",
"previewBut": "Preview"
},
"HelpPreview": {
"title": "Help",
"flow": "Operation Steps",
"features": "Features & Icons",
"tip": "Tip",
"empty": "Nothing to preview yet. Fill in the introduction, steps, or reference a feature group first."
},
"HelpFeaturePage": {
"title": "Help Features",
+9 -1
View File
@@ -208,7 +208,15 @@
"stepIconPlaceholder": "页面上的图标名",
"featureTitle": "引用的功能点组",
"addFeatureBut": "引用功能点组",
"featureEmpty": "暂未引用功能点组"
"featureEmpty": "暂未引用功能点组",
"previewBut": "预览"
},
"HelpPreview": {
"title": "使用帮助",
"flow": "操作流程",
"features": "功能点与图标说明",
"tip": "提示",
"empty": "还没有可预览的帮助内容,请先填写功能说明、操作步骤或引用功能点组。"
},
"HelpFeaturePage": {
"title": "帮助功能点查询",
@@ -21,6 +21,7 @@ import { SecurityActorData } from "@/data/auto/securityActorData";
import { SecurityActorGroupViewEntity } from "@/data/auto/securityActorGroupViewEntity";
import { SecurityUserActorViewEntity } from "@/data/auto/securityUserActorViewEntity";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityActorService from "@/service/security/securityActorService";
const { t } = useLocale();
@@ -135,7 +136,7 @@ async function onSave(): Promise<void> {
async function onDelete(): Promise<void> {
await securityActorService.Delete(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => uni.navigateBack(), 600);
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
@@ -151,7 +152,7 @@ function onAction(action: AdminFormAction): void {
break;
case "cancel":
if (isNew.value || !routeId.value) {
uni.navigateBack();
smartBack("mineTab");
return;
}
readonly.value = true;
@@ -163,7 +164,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
// 组别选择:跳 group-enquiry(多选),回传后按 GroupName 去重合并
@@ -20,6 +20,7 @@ import { useAdminPick, useAuthGuard } from "@/composables";
import { SecurityFunctionData } from "@/data/auto/securityFunctionData";
import { SecurityFunctionMapViewEntity } from "@/data/auto/securityFunctionMapViewEntity";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityFunctionService from "@/service/security/securityFunctionService";
const { t } = useLocale();
@@ -148,7 +149,7 @@ async function onSave(): Promise<void> {
async function onDelete(): Promise<void> {
await securityFunctionService.Delete(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => uni.navigateBack(), 600);
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
@@ -164,7 +165,7 @@ function onAction(action: AdminFormAction): void {
break;
case "cancel":
if (isNew.value || !routeId.value) {
uni.navigateBack();
smartBack("mineTab");
return;
}
readonly.value = true;
@@ -176,7 +177,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
// 权限名称:单选 permission-enquiry,回传写进 FunctionName 之外不影响
@@ -16,6 +16,7 @@ import { computed, ref } from "vue";
import { useAuthGuard } from "@/composables";
import { SecurityGeneratorNumberEntity } from "@/data/auto/securityGeneratorNumberEntity";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityGeneratorNumberService from "@/service/security/securityGeneratorNumberService";
const { t } = useLocale();
@@ -120,7 +121,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
onLoad((options) => {
@@ -22,6 +22,7 @@ import { SecurityGroupData } from "@/data/auto/securityGroupData";
import { SecurityGroupPermissionDependentViewDisplayData } from "@/data/auto/securityGroupPermissionDependentViewDisplayData";
import { SecurityGroupPermissionViewDisplayData } from "@/data/auto/securityGroupPermissionViewDisplayData";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityGroupService from "@/service/security/securityGroupService";
const { t } = useLocale();
@@ -130,7 +131,7 @@ async function onSave(): Promise<void> {
async function onDelete(): Promise<void> {
await securityGroupService.Delete(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => uni.navigateBack(), 600);
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
@@ -146,7 +147,7 @@ function onAction(action: AdminFormAction): void {
break;
case "cancel":
if (isNew.value || !routeId.value) {
uni.navigateBack();
smartBack("mineTab");
return;
}
readonly.value = true;
@@ -158,7 +159,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
// 权限勾选:四个 boolean 直接落在 DisplayData 上(与旧 GroupPermissionActions 一致)
@@ -17,6 +17,7 @@ import { useAuthGuard } from "@/composables";
import { SecurityHelpFeatureData } from "@/data/auto/securityHelpFeatureData";
import { SecurityHelpFeatureEntity } from "@/data/auto/securityHelpFeatureEntity";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityHelpFunctionService from "@/service/security/securityHelpFunctionService";
const { t } = useLocale();
@@ -123,7 +124,7 @@ async function onSave(): Promise<void> {
async function onDelete(): Promise<void> {
await securityHelpFunctionService.DeleteByGroupCode(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => uni.navigateBack(), 600);
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
@@ -139,7 +140,7 @@ function onAction(action: AdminFormAction): void {
break;
case "cancel":
if (isNew.value || !routeId.value) {
uni.navigateBack();
smartBack("mineTab");
return;
}
readonly.value = true;
@@ -151,7 +152,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
function addFeature(): void {
@@ -13,6 +13,8 @@
* 删除走 DeleteByFunctionName;新建默认 EnabledIndc='Y'。
*/
import type { AdminFormAction, AdminFormField, AdminFormSection } from "@/admin/admin-types";
import type { HelpFeatureVM, HelpStepVM } from "@/admin/help-preview-types";
import type { SecurityHelpFeatureEntity } from "@/data/auto/securityHelpFeatureEntity";
import type { SecurityHelpFeatureGroupEntity } from "@/data/auto/securityHelpFeatureGroupEntity";
import { onLoad } from "@dcloudio/uni-app";
import { useLocale } from "uview-pro";
@@ -22,6 +24,7 @@ import { SecurityHelpFunctionData } from "@/data/auto/securityHelpFunctionData";
import { SecurityHelpFunctionFeatureEntity } from "@/data/auto/securityHelpFunctionFeatureEntity";
import { SecurityHelpStepEntity } from "@/data/auto/securityHelpStepEntity";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityHelpFunctionService from "@/service/security/securityHelpFunctionService";
const { t } = useLocale();
@@ -37,6 +40,14 @@ const isNew = computed(() => !String(helpFunction.value.CreatedBy ?? "").trim())
/** 组表:把功能点上的「组主键」翻译成「组编码」 */
const groups = ref<SecurityHelpFeatureGroupEntity[]>([]);
/** 全部功能点明细(FindAll 带回),预览时按组主键展开成「功能点与图标说明」 */
const allFeatures = ref<SecurityHelpFeatureEntity[]>([]);
/** 预览弹窗状态:openPreview 时按当前界面数据现拼,不额外发请求 */
const previewVisible = ref(false);
const previewSteps = ref<HelpStepVM[]>([]);
const previewFeatures = ref<HelpFeatureVM[]>([]);
const heroTitle = computed(() => String(helpFunction.value.FunctionName || ""));
const heroChips = computed(() => {
const chips: string[] = [];
@@ -69,7 +80,8 @@ const sections = computed<AdminFormSection[]>(() => {
});
const actions = computed<AdminFormAction[]>(() => {
const list: AdminFormAction[] = [];
// 预览:不分查看/编辑态都可点,让管理员随时看这份帮助最终长什么样
const list: AdminFormAction[] = [{ key: "preview", label: t("HelpFunctionPage.previewBut") }];
if (perm.canAmend.value) {
list.push({ key: "amend", label: t("AdminPage.amendBut"), when: "view" });
}
@@ -100,6 +112,7 @@ async function loadGroups(): Promise<void> {
try {
const all = await securityHelpFunctionService.FindAll();
groups.value = all?.SecurityHelpFeatureGroups ?? [];
allFeatures.value = all?.SecurityHelpFeatures ?? [];
}
catch (error) {
console.warn(error);
@@ -166,11 +179,14 @@ async function onSave(): Promise<void> {
async function onDelete(): Promise<void> {
await securityHelpFunctionService.DeleteByFunctionName(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => uni.navigateBack(), 600);
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
switch (action.key) {
case "preview":
openPreview();
break;
case "amend":
readonly.value = false;
break;
@@ -182,7 +198,7 @@ function onAction(action: AdminFormAction): void {
break;
case "cancel":
if (isNew.value || !routeId.value) {
uni.navigateBack();
smartBack("mineTab");
return;
}
readonly.value = true;
@@ -194,7 +210,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
// 功能名称:新建时单选 function-enquiry 回填(旧页用 FindFunctionNameOptions 下拉,移动端退化成选择器)
@@ -278,6 +294,37 @@ function downStep(index: number): void {
swap(data.value.SecurityHelpSteps, index, 1);
}
/** 预览:把当前界面的步骤与引用组功能点拼成最终用户看到的帮助文档,弹窗展示 */
function openPreview(): void {
ensureLists();
// 步骤按界面顺序(数组序即显示序,StepNo 要到保存时才重排)
previewSteps.value = data.value.SecurityHelpSteps.map(step => ({
title: String(step.Title ?? ""),
desc: String(step.Description ?? ""),
tips: String(step.Tips ?? ""),
icon: String(step.Icon ?? ""),
}));
// 功能点:每个引用组按组主键展开成组内明细,组内按 SortNo 升序
const items: HelpFeatureVM[] = [];
for (const link of data.value.SecurityHelpFunctionFeatures) {
const groupId = groups.value.find(g => g.GroupCode === link.GroupCode)?.HelpFeatureGroupId;
if (!groupId) {
continue;
}
allFeatures.value
.filter(feature => feature.HelpFeatureGroupId === groupId)
.sort((a, b) => (a.SortNo ?? 0) - (b.SortNo ?? 0))
.forEach(feature => items.push({
name: String(feature.Name ?? ""),
desc: String(feature.Description ?? ""),
icon: String(feature.Icon ?? ""),
color: String(feature.Color ?? ""),
}));
}
previewFeatures.value = items;
previewVisible.value = true;
}
onLoad((options) => {
routeId.value = String(options?.id ?? "");
const mode = String(options?.mode ?? "view");
@@ -436,6 +483,9 @@ useAuthGuard(() => {
</view>
</template>
</admin-form>
<help-doc-preview :visible="previewVisible" :title="heroTitle" :intro="helpFunction.Intro" :steps="previewSteps"
:features="previewFeatures" @close="previewVisible = false" />
</template>
<style lang="scss" scoped>
@@ -16,6 +16,7 @@ import { computed, ref } from "vue";
import { useAuthGuard } from "@/composables";
import { SecurityMasterEntity } from "@/data/auto/securityMasterEntity";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityMasterService from "@/service/security/securityMasterService";
const { t } = useLocale();
@@ -146,7 +147,7 @@ async function onSave(): Promise<void> {
async function onDelete(): Promise<void> {
await securityMasterService.Delete(dataKey.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => uni.navigateBack(), 600);
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
@@ -162,7 +163,7 @@ function onAction(action: AdminFormAction): void {
break;
case "cancel":
if (isNew.value || !isValidKey()) {
uni.navigateBack();
smartBack("mineTab");
return;
}
readonly.value = true;
@@ -174,7 +175,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
onLoad((options) => {
@@ -17,6 +17,7 @@ import { useAuthGuard } from "@/composables";
import { SecurityPermissionData } from "@/data/auto/securityPermissionData";
import { SecurityPermissionDependentEntity } from "@/data/auto/securityPermissionDependentEntity";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityPermissionService from "@/service/security/securityPermissionService";
const { t } = useLocale();
@@ -140,7 +141,7 @@ async function onSave(): Promise<void> {
async function onDelete(): Promise<void> {
await securityPermissionService.Delete(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => uni.navigateBack(), 600);
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
@@ -156,7 +157,7 @@ function onAction(action: AdminFormAction): void {
break;
case "cancel":
if (isNew.value || !routeId.value) {
uni.navigateBack();
smartBack("mineTab");
return;
}
readonly.value = true;
@@ -168,7 +169,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
function addDependent(): void {
@@ -179,7 +179,7 @@ useAuthGuard(() => {
<template>
<view class="page">
<admin-list ref="listRef" v-model:keyword="keyword" :nav-title="t('UserPage.title')" :show-nav-back="isPick"
<admin-list ref="listRef" v-model:keyword="keyword" :nav-title="t('UserPage.title')" :show-nav-back="!isPick"
:columns="columns" :loader="state.query" :row-key="SecurityUserEntity.USER_NAME" title-key="UserLocalName"
:selectable="isPick ? pickTarget.multiple.value : Boolean(perm.canDelete.value)" :filter-count="filterCount"
:filter-visible="filterVisible" :filter-items="draft" :search-token="searchToken"
@@ -24,6 +24,7 @@ import { useAdminPick, useAuthGuard } from "@/composables";
import { SecurityUserActorViewEntity } from "@/data/auto/securityUserActorViewEntity";
import { SecurityUserData } from "@/data/auto/securityUserData";
import { usePagePermission } from "@/permission/permission-context";
import { smartBack } from "@/router/routes";
import securityUserService from "@/service/security/securityUserService";
const { t } = useLocale();
@@ -142,7 +143,7 @@ async function onSave(): Promise<void> {
async function onDelete(): Promise<void> {
await securityUserService.Delete(routeId.value);
uni.showToast({ title: t("AdminPage.deletedTip"), icon: "none" });
setTimeout(() => uni.navigateBack(), 600);
setTimeout(() => smartBack("mineTab"), 600);
}
function onAction(action: AdminFormAction): void {
@@ -158,7 +159,7 @@ function onAction(action: AdminFormAction): void {
break;
case "cancel":
if (isNew.value || !routeId.value) {
uni.navigateBack();
smartBack("mineTab");
return;
}
readonly.value = true;
@@ -170,7 +171,7 @@ function onAction(action: AdminFormAction): void {
}
function onBack(): void {
uni.navigateBack();
smartBack("mineTab");
}
// 角色选择:跳 actor-enquiry(可多选),回传后按 ActorName 去重合并
+13
View File
@@ -250,3 +250,16 @@ export function decodeQueryPath(value: unknown): string {
export function isAuthFree(name: PageName): boolean {
return AUTH_FREE_PAGES.includes(name);
}
/**
* 带兜底的返回:优先 navigateBack;H5 整页刷新(F5)或深链直达会把页面栈清空,
* 此时无处可退,退回到落地页(默认首页 dashboard),避免“返回”箭头点了没反应。
* 与 pages/tab/file.vue、transport.vue、trash.vue 既有的“无栈就回首页”兜底策略一致。
*/
export function smartBack(fallback: PageName = "dashboard"): void {
if (getCurrentPages().length > 1) {
uni.navigateBack();
return;
}
uni.reLaunch({ url: pageUrl(fallback) });
}