feat:【ele】【mall】将 app-link-input 迁移到 mall/promotion/components 中,聚焦一点

This commit is contained in:
YunaiV
2025-10-25 15:53:51 +08:00
parent 457add90bd
commit 2909d1c4fa
17 changed files with 84 additions and 104 deletions

View File

@@ -0,0 +1,238 @@
<script lang="ts" setup>
import type { ButtonInstance, ScrollbarInstance } from 'element-plus';
import type { AppLink } from './data';
import { nextTick, ref } from 'vue';
import { getUrlNumberValue } from '@vben/utils';
import { ElScrollbar } from 'element-plus';
import ProductCategorySelect from '#/views/mall/product/category/components/product-category-select.vue';
import { APP_LINK_GROUP_LIST, APP_LINK_TYPE_ENUM } from './data';
/** APP 链接选择弹框 */
defineOptions({ name: 'AppLinkSelectDialog' });
const emit = defineEmits<{
appLinkChange: [appLink: AppLink];
change: [link: string];
}>();
const activeGroup = ref(APP_LINK_GROUP_LIST[0]?.name); // 选中的分组,默认选中第一个
const activeAppLink = ref({} as AppLink); // 选中的 APP 链接
const linkScrollbar = ref<ScrollbarInstance>(); // 右侧滚动条
const groupTitleRefs = ref<HTMLInputElement[]>([]); // 分组标题引用列表
const groupScrollbar = ref<ScrollbarInstance>(); // 分组滚动条
const groupBtnRefs = ref<ButtonInstance[]>([]); // 分组引用列表
const detailSelectDialog = ref<{
id?: number;
type?: APP_LINK_TYPE_ENUM;
visible: boolean;
}>({
visible: false,
id: undefined,
type: undefined,
}); // 详情选择对话框
/** 打开弹窗 */
const dialogVisible = ref(false);
const open = (link: string) => {
activeAppLink.value.path = link;
dialogVisible.value = true;
// 滚动到当前的链接
const group = APP_LINK_GROUP_LIST.find((group) =>
group.links.some((linkItem) => {
const sameLink = isSameLink(linkItem.path, link);
if (sameLink) {
activeAppLink.value = { ...linkItem, path: link };
}
return sameLink;
}),
);
if (group) {
// 使用 nextTick 的原因:可能 Dom 还没生成,导致滚动失败
nextTick(() => handleGroupSelected(group.name));
}
};
defineExpose({ open });
/** 处理 APP 链接选中 */
const handleAppLinkSelected = (appLink: AppLink) => {
if (!isSameLink(appLink.path, activeAppLink.value.path)) {
activeAppLink.value = appLink;
}
switch (appLink.type) {
case APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST: {
detailSelectDialog.value.visible = true;
detailSelectDialog.value.type = appLink.type;
// 返显
detailSelectDialog.value.id =
getUrlNumberValue(
'id',
`http://127.0.0.1${activeAppLink.value.path}`,
) || undefined;
break;
}
default: {
break;
}
}
};
function handleSubmit() {
dialogVisible.value = false;
emit('change', activeAppLink.value.path);
emit('appLinkChange', activeAppLink.value);
}
/**
* 处理右侧链接列表滚动
* @param {object} param0 滚动事件参数
* @param {number} param0.scrollTop 滚动条的位置
*/
function handleScroll({ scrollTop }: { scrollTop: number }) {
const titleEl = groupTitleRefs.value.find((titleEl: HTMLInputElement) => {
// 获取标题的位置信息
const { offsetHeight, offsetTop } = titleEl;
// 判断标题是否在可视范围内
return scrollTop >= offsetTop && scrollTop < offsetTop + offsetHeight;
});
// 只需处理一次
if (titleEl && activeGroup.value !== titleEl.textContent) {
activeGroup.value = titleEl.textContent || '';
// 同步左侧的滚动条位置
scrollToGroupBtn(activeGroup.value);
}
}
/** 处理分组选中 */
function handleGroupSelected(group: string) {
activeGroup.value = group;
const titleRef = groupTitleRefs.value.find(
(item: HTMLInputElement) => item.textContent === group,
);
if (titleRef) {
// 滚动分组标题
linkScrollbar.value?.setScrollTop(titleRef.offsetTop);
}
}
/** 自动滚动分组按钮,确保分组按钮保持在可视区域内 */
function scrollToGroupBtn(group: string) {
const groupBtn = groupBtnRefs.value
.map((btn: ButtonInstance) => btn.ref)
.find((ref: HTMLButtonElement | undefined) => ref?.textContent === group);
if (groupBtn) {
groupScrollbar.value?.setScrollTop(groupBtn.offsetTop);
}
}
/** 是否为相同的链接(不比较参数,只比较链接) */
function isSameLink(link1: string, link2: string) {
return link2 ? link1.split('?')[0] === link2.split('?')[0] : false;
}
/** 处理详情选择 */
function handleProductCategorySelected(id: number) {
// TODO @AI这里有点问题
const url = new URL(activeAppLink.value.path, 'http://127.0.0.1');
// 修改 id 参数
url.searchParams.set('id', `${id}`);
// 排除域名
activeAppLink.value.path = `${url.pathname}${url.search}`;
// 关闭对话框
detailSelectDialog.value.visible = false;
// 重置 id
detailSelectDialog.value.id = undefined;
}
</script>
<template>
<el-dialog v-model="dialogVisible" title="选择链接" width="65%">
<div class="flex h-[500px] gap-2">
<!-- 左侧分组列表 -->
<ElScrollbar
wrap-class="h-full"
ref="groupScrollbar"
view-class="flex flex-col"
>
<el-button
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
:key="groupIndex"
class="ml-0 mr-4 w-[90px] justify-start"
:class="[{ active: activeGroup === group.name }]"
ref="groupBtnRefs"
:text="activeGroup !== group.name"
:type="activeGroup === group.name ? 'primary' : 'default'"
@click="handleGroupSelected(group.name)"
>
{{ group.name }}
</el-button>
</ElScrollbar>
<!-- 右侧链接列表 -->
<ElScrollbar
class="h-full flex-1"
@scroll="handleScroll"
ref="linkScrollbar"
>
<div
v-for="(group, groupIndex) in APP_LINK_GROUP_LIST"
:key="groupIndex"
>
<!-- 分组标题 -->
<div class="font-bold" ref="groupTitleRefs">{{ group.name }}</div>
<!-- 链接列表 -->
<el-tooltip
v-for="(appLink, appLinkIndex) in group.links"
:key="appLinkIndex"
:content="appLink.path"
placement="bottom"
:show-after="300"
>
<el-button
class="mb-2 ml-0 mr-2"
:type="
isSameLink(appLink.path, activeAppLink.path)
? 'primary'
: 'default'
"
@click="handleAppLinkSelected(appLink)"
>
{{ appLink.name }}
</el-button>
</el-tooltip>
</div>
</ElScrollbar>
</div>
<!-- 底部对话框操作按钮 -->
<template #footer>
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="dialogVisible = false"> </el-button>
</template>
</el-dialog>
<el-dialog v-model="detailSelectDialog.visible" title="" width="50%">
<el-form class="min-h-[200px]">
<el-form-item
label="选择分类"
v-if="
detailSelectDialog.type === APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST
"
>
<ProductCategorySelect
v-model="detailSelectDialog.id"
:parent-id="0"
@update:model-value="handleProductCategorySelected"
/>
</el-form-item>
</el-form>
</el-dialog>
</template>
<style lang="scss" scoped>
:deep(.el-button + .el-button) {
margin-left: 0 !important;
}
</style>

View File

@@ -0,0 +1,220 @@
/** APP 链接分组 */
export interface AppLinkGroup {
name: string; // 分组名称
links: AppLink[]; // 链接列表
}
/** APP 链接 */
export interface AppLink {
name: string; // 链接名称
path: string; // 链接地址
type?: APP_LINK_TYPE_ENUM; // 链接的类型
}
/** APP 链接类型(需要特殊处理,例如商品详情) */
export enum APP_LINK_TYPE_ENUM {
ACTIVITY_COMBINATION, // 拼团活动
ACTIVITY_POINT, // 积分商城活动
ACTIVITY_SECKILL, // 秒杀活动
ARTICLE_DETAIL, // 文章详情
COUPON_DETAIL, // 优惠券详情
DIY_PAGE_DETAIL, // 自定义页面详情
PRODUCT_CATEGORY_LIST, // 品类列表
PRODUCT_DETAIL_COMBINATION, // 拼团商品详情
PRODUCT_DETAIL_NORMAL, // 商品详情
PRODUCT_DETAIL_SECKILL, // 秒杀商品详情
PRODUCT_LIST, // 商品列表
}
/** APP 链接列表(做一下持久化?) */
export const APP_LINK_GROUP_LIST = [
{
name: '商城',
links: [
{
name: '首页',
path: '/pages/index/index',
},
{
name: '商品分类',
path: '/pages/index/category',
type: APP_LINK_TYPE_ENUM.PRODUCT_CATEGORY_LIST,
},
{
name: '购物车',
path: '/pages/index/cart',
},
{
name: '个人中心',
path: '/pages/index/user',
},
{
name: '商品搜索',
path: '/pages/index/search',
},
{
name: '自定义页面',
path: '/pages/index/page',
type: APP_LINK_TYPE_ENUM.DIY_PAGE_DETAIL,
},
{
name: '客服',
path: '/pages/chat/index',
},
{
name: '系统设置',
path: '/pages/public/setting',
},
{
name: '常见问题',
path: '/pages/public/faq',
},
],
},
{
name: '商品',
links: [
{
name: '商品列表',
path: '/pages/goods/list',
type: APP_LINK_TYPE_ENUM.PRODUCT_LIST,
},
{
name: '商品详情',
path: '/pages/goods/index',
type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_NORMAL,
},
{
name: '拼团商品详情',
path: '/pages/goods/groupon',
type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_COMBINATION,
},
{
name: '秒杀商品详情',
path: '/pages/goods/seckill',
type: APP_LINK_TYPE_ENUM.PRODUCT_DETAIL_SECKILL,
},
],
},
{
name: '营销活动',
links: [
{
name: '拼团订单',
path: '/pages/activity/groupon/order',
},
{
name: '营销商品',
path: '/pages/activity/index',
},
{
name: '拼团活动',
path: '/pages/activity/groupon/list',
type: APP_LINK_TYPE_ENUM.ACTIVITY_COMBINATION,
},
{
name: '秒杀活动',
path: '/pages/activity/seckill/list',
type: APP_LINK_TYPE_ENUM.ACTIVITY_SECKILL,
},
{
name: '积分商城活动',
path: '/pages/activity/point/list',
type: APP_LINK_TYPE_ENUM.ACTIVITY_POINT,
},
{
name: '签到中心',
path: '/pages/app/sign',
},
{
name: '优惠券中心',
path: '/pages/coupon/list',
},
{
name: '优惠券详情',
path: '/pages/coupon/detail',
type: APP_LINK_TYPE_ENUM.COUPON_DETAIL,
},
{
name: '文章详情',
path: '/pages/public/richtext',
type: APP_LINK_TYPE_ENUM.ARTICLE_DETAIL,
},
],
},
{
name: '分销商城',
links: [
{
name: '分销中心',
path: '/pages/commission/index',
},
{
name: '推广商品',
path: '/pages/commission/goods',
},
{
name: '分销订单',
path: '/pages/commission/order',
},
{
name: '我的团队',
path: '/pages/commission/team',
},
],
},
{
name: '支付',
links: [
{
name: '充值余额',
path: '/pages/pay/recharge',
},
{
name: '充值记录',
path: '/pages/pay/recharge-log',
},
],
},
{
name: '用户中心',
links: [
{
name: '用户信息',
path: '/pages/user/info',
},
{
name: '用户订单',
path: '/pages/order/list',
},
{
name: '售后订单',
path: '/pages/order/aftersale/list',
},
{
name: '商品收藏',
path: '/pages/user/goods-collect',
},
{
name: '浏览记录',
path: '/pages/user/goods-log',
},
{
name: '地址管理',
path: '/pages/user/address/list',
},
{
name: '用户佣金',
path: '/pages/user/wallet/commission',
},
{
name: '用户余额',
path: '/pages/user/wallet/money',
},
{
name: '用户积分',
path: '/pages/user/wallet/score',
},
],
},
] as AppLinkGroup[];

View File

@@ -0,0 +1,46 @@
<script lang="ts" setup>
import { ref, watch } from 'vue';
import AppLinkSelectDialog from './app-link-select-dialog.vue';
/** APP 链接输入框 */
defineOptions({ name: 'AppLinkInput' });
// 定义属性
const props = defineProps({
modelValue: {
type: String,
default: '',
}, // 当前选中的链接
});
const emit = defineEmits<{
'update:modelValue': [link: string];
}>();
const dialogRef = ref(); // 选择对话框
const appLink = ref(''); // 当前的链接
const handleOpenDialog = () => dialogRef.value?.open(appLink.value); // 处理打开对话框
const handleLinkSelected = (link: string) => (appLink.value = link); // 处理 APP 链接选中
watch(
() => props.modelValue,
() => (appLink.value = props.modelValue),
{ immediate: true },
);
watch(
() => appLink.value,
() => emit('update:modelValue', appLink.value),
);
</script>
<template>
<el-input v-model="appLink" placeholder="输入或选择链接">
<template #append>
<el-button @click="handleOpenDialog">选择</el-button>
</template>
</el-input>
<AppLinkSelectDialog ref="dialogRef" @change="handleLinkSelected" />
</template>

View File

@@ -0,0 +1,2 @@
export { default as VerticalButtonGroup } from './vertical-button-group/index.vue';
export { default as AppLinkInput } from './app-link-input/index.vue';