ow
This commit is contained in:
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>
|
||||
Reference in new issue
Block a user