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

@@ -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>