296 Commits
v4.4 ... v5.0.1

Author SHA1 Message Date
pikachu1995@126.com
f21ce6e252 refactor(decoration): 重构店铺装修组件配置并规范化环境变量
- 重构装修组件配置,新增 icon 与 group 分类字段
- 将组件"图片"重命名为"热区","图片轮播"重命名为"轮播图"
- 新增搜索、公告、视频组件配置项
- 统一为各图片项添加 model 字段标识跳转模式
- 将装修页面组件间 pageGap 间距由 10 调整为 0
- 移除 im 模块中 VUE_APP_COMMON 变量,统一使用 VUE_APP_BUYER
- 为 im 环境变量与配置文件添加详细注释说明
2026-10-21 15:17:05 +08:00
pikachu1995@126.com
da95f17885 docs: 更新项目 README 与开源许可说明 2026-10-20 19:07:47 +08:00
pikachu1995@126.com
0f9593d5ed feat(page-decoration): 新增移动端头部综合组件并重构各楼层模板尺寸与排版 2026-10-20 19:07:43 +08:00
pikachu1995@126.com
088a2bb308 feat(oss): 优化素材库上传格式限制及组件模式下的 Tab 显示 2026-10-20 19:07:35 +08:00
pikachu1995@126.com
d29ad9c99b refactor(page-decoration): 重构装修页面组件与交互方式
- 统一 liliDialog ref 命名为 liliDialogRef,避免与组件 import 冲突
- 商品选择弹窗改为 el-table 表格形式展示
- 链接选择器从 el-dialog 改为 el-drawer 抽屉形式
- 为所有 img 标签添加 alt 属性提升可访问性
- 新增拼团、直播、秒杀、砍价、积分商城、优惠券等营销组件默认配置
- 删除商品分类组件,新增商品列表组件配置
- i18n 配置从 legacy 模式切换为 composition 模式
- 注册 PanelSearch、PanelNotice、PanelVideo 等新面板组件
- 优化画布预览区域样式并添加页面设置入口
2026-10-20 16:13:28 +08:00
pikachu1995@126.com
d87e0c5f6b fix(seller): 修正 serviceStaffList 中 getUserListData 的导入路径为 @/api/index 2026-10-20 15:48:39 +08:00
pikachu1995@126.com
5e74e356e0 refactor(manager): 重构商品/审核/品牌/分词/分销提现与等级等7个核心列表页为 useTable
- 重构商品管理(商品总览、商品审核)列表页为 useTable

- 重构商品维护(品牌管理、评价管理)列表页为 useTable

- 重构分销管理(分销提现、等级变动历史)与自定义分词列表页为 useTable

- 消除 150+ 行重复样板代码,统一竞态防护与分页查询规范
2026-10-20 14:56:06 +08:00
pikachu1995@126.com
cba624f480 refactor(seller): 重构预约服务、分销与商品库存等8个列表页为 useTable
- 重构预约服务模块(服务人员、系统表单、服务工单)列表页为 useTable

- 重构分销管理模块(分销商品、分销订单)列表页为 useTable

- 重构商品与库存模块(库存导出导入、库存预警管理、商品草稿模版)列表页为 useTable

- 消除 180+ 行模板重复代码,规范分页、防抖与请求竞态防护
2026-10-20 14:28:29 +08:00
pikachu1995@126.com
ba964ff478 refactor(manager): 重构统计报表、促销活动与工单审核等12个列表页为 useTable
- 重构统计报表(销售订单明细、商品销售汇总、店铺业绩)列表页为 useTable

- 重构营销活动(秒杀、第N件优惠、积分商品)列表页为 useTable

- 重构财务流水(钱包日志)、服务工单、分销导入、店铺审核、商品参数、订单投诉列表页

- 消除约 210+ 行分页/防抖/竞态重复样板代码,统一加载与查询状态规范
2026-10-20 14:20:36 +08:00
pikachu1995@126.com
45338c9d70 style(seller): 重构订单统计页转化漏斗与指标卡样式 2026-10-20 14:15:07 +08:00
pikachu1995@126.com
6b6dc2c4f3 refactor(manager): 引入 useTable 并重构采购/财务/会员等10个列表页
- 引入通用 useTable composable 实现分页与查询标准化
- 重构采购单/入库单/报损单/盘点单4个采购管理列表页
- 重构退款日志/预存款流水/会员充值/会员经验等财务与会员日志列表页
- 重构支付流水与退款流水列表页
- 统一竞态防护与 loading 状态管理,消除 150+ 行冗余样板代码
2026-10-20 14:02:03 +08:00
pikachu1995@126.com
8fd353a766 refactor(manager): 优化全局空状态、错误图片占位并升级遗留消息语法
- 增加表格与树组件全局空状态 (el-empty) 样式规范
- 补齐视图图片语义化 alt,并添加全局图片加载异常 SVG 占位兜底
- 将砍价/秒杀/自定义面板中的 $Message 兼容语法替换为 ElMessage
- 清理冗余依赖 vuex,将 sass-loader 调整至 devDependencies
2026-10-20 13:51:21 +08:00
pikachu1995@126.com
3cb60139d5 refactor(seller): 列表页抽离 useTable 重构与通用 UI 规范优化
- 抽离通用 useTable composable,并迁移 13 个典型列表页(采购/报损/订单/售后/统计/营销等)
- 增加表格与树组件全局空状态 (el-empty) 样式规范
- 补齐视图图片语义化 alt,并添加全局图片加载异常 SVG 占位兜底
- 替换遗留的 $Message 兼容语法为 ElMessage
- 将 WAP 店铺装修模板的 Options API 统一为 defineOptions
- 优化数据报表卡片视觉样式,并移除失效的品牌筛选选项
- 清理冗余依赖 vuex,将 sass-loader 调整至 devDependencies
2026-10-20 13:51:04 +08:00
pikachu1995@126.com
7d9bf776db feat(seller): 素材管理页重构布局并统一主色交互
- 双层卡片嵌套改为单面板,tabs 与操作按钮并入顶部工具栏
- 悬停/选中红色描边统一为 Element Plus 主色
- 复选框改为悬停/选中时显示,选中增加淡蓝遮罩
- 分组行编辑入口悬停显示,添加分组改为置顶虚线按钮
- 分页增加总数显示,空列表显示空状态
2026-10-20 13:42:46 +08:00
pikachu1995@126.com
0c814ec956 feat(seller): 系统消息页改为顶部 tab 筛选与卡片式列表
- 左侧菜单改为顶部 pill tab(未读带数量角标)
- 表格改为卡片列表:类型图标 + 标题加粗 + 内容摘要 + 时间操作
- 点击消息就地展开内容,去掉返回按钮
- 修复标为已读后错误刷新全量列表的问题
2026-10-20 13:42:37 +08:00
pikachu1995@126.com
322e357398 Merge branch 'feat/appointment-goods' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into feat/appointment-goods 2026-10-15 10:04:53 +08:00
lifenlong
cc4a94da40 fix(seller): shopSetting tab 事件改为 @tab-click
上一笔只把判断改成了 pane.paneName,但 @click 对未声明该 emit 的 el-tabs
会退化为原生 DOM 点击(handler 收到 MouseEvent),仍不生效。
@tab-click 才是 Element Plus 的自定义事件,handler 能拿到 pane 对象。
2026-10-14 21:22:05 +08:00
lifenlong
36648f065b fix(seller): 清理直播商品页的悬空模板引用
liveGoods 是从 addLive 复制来的纯只读详情页(所有表单项均 disabled),
但模板残留了三个不存在的引用:date-picker 的 @change="handleChangeTime"、
el-form 的 :rules="liveRulesForm"、"保存"按钮的 @click="createLives()"
(点击会抛 is not a function)。只读页无需保存入口,移除上述引用及该按钮。
2026-10-14 21:14:07 +08:00
lifenlong
9893494cf1 fix(seller): 修复店员管理页部门筛选失效
clerkManage 中 department-choose 监听器写的是 @change,而组件实际 emit 的是
on-change(与同目录 department-tree-choose 的约定一致),导致搜索区的部门筛选
永不触发。改为 @on-change 后恢复联动。
2026-10-14 21:09:49 +08:00
lifenlong
906cda0385 fix(seller): 修复三处历史遗留的恒假条件
- coupon-receive: 路由监听中 couponId 判断取错了来源(实际应判断 query.couponId),
  导致优惠券列表页切换时永不刷新;changeSelect 中 getType 判断改为读表单值 searchForm.getType
- addLive: 添加直播商品时 roomId 取 route.query.roomId 恒为 undefined,改为页面实际参数 route.query.id
- shopSetting: tab 切换回调 v == "INFO" 恒为 false(Element Plus 传 pane 对象),
  改为读 pane.paneName,基本信息/退货地址 tab 的数据重载恢复正常
2026-10-14 21:06:44 +08:00
lifenlong
92dd179525 chore(seller): 清理死代码与失效机制,生产构建剥离 console
- 删除零引用组件 main-components/message-tip.vue
- 清理 113 处 no-unused-vars(重点 goodsOperationSec 20+ 项、ossManage 25 项,含死代码链级联删除;renovation 的 ModelForm 为 vue-eslint-parser 误报,保留并核实)
- stores/app.js 移除只写不读的死字段(currNavTitle/menuTheme/themeColor/isFullScreen/messageCount/currentPageName/currentPath)及失效的 cachePage/dontCache 缓存页机制(无 keep-alive 消费方),同步清理 util.js/App.vue/Main.vue/shrinkable-menu/tags-page-opened 的调用点
- 删除 30 处 console.log 调试残留;生产构建 terser 配置 drop_console 兜底
2026-10-14 20:31:25 +08:00
pikachu1995@126.com
fefb748864 merge: 合并远端 script setup + pinia 迁移,保留装修编辑器 CRMEB 化功能 2026-10-13 11:28:23 +08:00
pikachu1995@126.com
0b644a1722 feat(manager): 移动端装修编辑器 CRMEB 化迭代
- 轮播图:双 tab 面板(内容/样式)、轮播风格(通栏/卡片)、指示器样式/位置/色调、渐变背景、边距圆角
- 图片魔方:9 种风格收进样式 tab,拖入默认左一右二,内容 tab 固定槽位配置,新增颜色/边距/圆角设置
- 宫格导航:双 tab 面板,菜单管理 + 颜色/边距/圆角设置
- 编辑器整体:灰底卡片模块化、顶栏页面名称编辑、画布操作条/名称标签外置、组件列表三列通高卡片
- 预览模板同步应用各组件样式字段
2026-10-13 11:07:30 +08:00
lifenlong
2a2c3f8174 perf(seller): Element Plus 按需引入 + 移除未使用的 vue-lazyload
- plugins/element.js 不再全量注册,组件由 unplugin-vue-components 按模板自动按需解析;
  v-loading 指令与全局配置(zhCn/size)单独保留
- chunk-framework 344KB → 279KB(gzip,-19%);样式仍走全量 CSS 保证覆盖顺序不变
- 修复 unplugin-vue-components 与 minimatch 的版本兼容(锁定 0.27.5 + resolutions 嵌套 v9)
- 移除全局注册但零使用的 vue-lazyload
2026-10-13 07:39:35 +08:00
lifenlong
88fc1b0b2a refactor: 四端完成 Options API → script setup + vuex → pinia 迁移
- buyer(95) / seller(169) / im(53) / manager(280) 共 597 个组件全部从 Options API 转换为 <script setup>
- 四端 vuex 全部移除,状态管理统一迁移至 pinia 4(stores/ 目录)
- util.js 等基础设施去除 vm 首参依赖;全局属性($Message/$filters 等)在 script 内改为具名导入
- 被外部 ref 访问的公共组件均已补齐 defineExpose(清单见各端 MIGRATION-VUE3.md)
- 各端 eslint 启用 vue/component-api-style 防止新代码回退 Options API
- 四端 yarn build 全部通过;迁移约定与注意事项已更新至各端 MIGRATION-VUE3.md
2026-10-13 07:06:59 +08:00
pikachu1995@126.com
b45910a35f style(manager): 左侧组件列表合并为一张通高卡片 2026-10-12 16:29:15 +08:00
pikachu1995@126.com
758f09a291 style(manager): 楼层操作条图标调大 2026-10-12 16:28:15 +08:00
pikachu1995@126.com
fce063642d style(manager): 左侧组件列表按分组模块化卡片展示 2026-10-12 16:27:03 +08:00
pikachu1995@126.com
c832350dd3 style(manager): 装修页整体间距梳理,三列与顶栏留 12px 间隔 2026-10-12 16:25:17 +08:00
pikachu1995@126.com
5169d4ac43 style(manager): 装修页顶栏改为卡片模块,页面换浅灰底 2026-10-12 16:21:29 +08:00
pikachu1995@126.com
d80ead4d94 style(manager): 热区面板标题行加白色卡片底色 2026-10-12 16:19:12 +08:00
pikachu1995@126.com
62700162d5 feat(manager): 装修页左上角支持设置页面名称 2026-10-12 16:17:57 +08:00
pikachu1995@126.com
a568131b7c style(manager): 热区设置面板按模块卡片形式展示 2026-10-12 16:14:40 +08:00
pikachu1995@126.com
27ca53068b style(manager): 左侧组件列表改为一行三个 2026-10-12 16:10:47 +08:00
pikachu1995@126.com
1d946a9616 style(manager): 组件分组标题与楼层名称标签字号调大 2026-10-12 16:09:45 +08:00
pikachu1995@126.com
f31669cba6 style(manager): 楼层名称标签与操作条移至预览区外两侧 2026-10-12 16:07:18 +08:00
pikachu1995@126.com
005006adf7 style(manager): 画布移除 iPhone 边框背景,改为简洁屏幕容器 2026-10-12 16:02:07 +08:00
pikachu1995@126.com
2f097ace49 feat(manager): 画布楼层加名称标签与悬浮操作条
- 每楼层左上角显示组件名标签
- 选中楼层右侧竖排操作条:上移/下移/复制/删除/隐藏,替代原右上角关闭图标
- 隐藏楼层变暗并显示已隐藏蒙层,hidden 随 pageData 保存
2026-10-12 15:59:58 +08:00
pikachu1995@126.com
654839b247 style(manager): 选择照片/绘制热区按钮改默认尺寸、自然宽度 2026-10-12 15:49:52 +08:00
pikachu1995@126.com
8ed67847dd style(manager): 滑块旁数字输入框放大(去 small 尺寸,宽 130px) 2026-10-12 15:48:30 +08:00
pikachu1995@126.com
47974dddab chore(manager): 设置热区按钮改名绘制热区 2026-10-12 15:47:03 +08:00
pikachu1995@126.com
76cc10c6af style(manager): 热区面板标题与 tab 同行展示
- 面板契约新增 hideDecorateTitle 标记,面板可自行接管标题行
- tab 分段控件改为自然宽度,右对齐
2026-10-12 15:46:02 +08:00
pikachu1995@126.com
ae08a568bd style(manager): 保存页面按钮右间距调整为 2% 2026-10-12 15:43:02 +08:00
pikachu1995@126.com
2bb3a71460 style(manager): 热区面板 tab 切换改为分段控件样式 2026-10-12 15:41:59 +08:00
pikachu1995@126.com
ce03b13718 feat(manager): 热区组件配置面板重做为内容/样式双 tab
- 新增 PanelFlexOne:内容 tab(选图+设置热区,复用 ossManage 与 hotzone 编辑器),样式 tab(背景颜色/上下左右边距/页面间距/背景圆角/内容圆角)
- 注册进 panelMap 替换 flexOne 的通用老面板
- 画布预览 tpl_flex_one 应用相同样式字段,缺字段兜底保持原样
2026-10-12 15:38:25 +08:00
pikachu1995@126.com
0ad79157d3 feat(manager): 图片组件更名为热区 2026-10-12 15:26:19 +08:00
pikachu1995@126.com
bdae07e585 feat(manager): 图片组件选择模式只保留热区
- 移除链接/热区单选,新增图片组件默认 hotzone 直接进绘制热区
- 链接区标题随模式显示热区/选择链接
- 存量 link 数据不受影响(uniapp 端热区由 tpl_hot_zone 渲染;原生小程序暂无热区渲染,待补)
2026-10-12 15:25:14 +08:00
pikachu1995@126.com
7fa6c2d84c style(manager): 保存页面按钮右间距调整为 5% 2026-10-12 15:22:19 +08:00
pikachu1995@126.com
59bd1b8c78 style(manager): 保存模板改名保存页面并往左收 15% 2026-10-12 15:17:58 +08:00
pikachu1995@126.com
18eb79054f feat(manager): 移动端装修编辑器改为全屏独立页
- /floorList/main 移出 Main 布局注册为顶层路由,不再显示左侧菜单/header/标签栏
- 编辑器容器铺满全屏,右侧面板高度同步调整
- 顶栏左侧新增返回按钮(goBack 复用已有方法)
- 路径/name/query 不变,列表页 4 处跳转零改动
2026-10-12 15:13:48 +08:00
pikachu1995@126.com
b5047a7a7a chore(manager): 移除装修页顶栏的页面切换按钮
watcher 会按 pagetype 重置为单 tab,切换入口实际不可用,且 main.vue 已按路由直接定页面
2026-10-12 15:05:13 +08:00
pikachu1995@126.com
45b83d0fe4 style(manager): 优化组件面板卡片质感
- 白底圆角卡片 + 淡蓝图标底块,hover 浮起、图标反白、投影
- 组标题加蓝色竖条点缀
2026-10-12 15:03:00 +08:00
pikachu1995@126.com
a804120fa2 feat(manager): 组件面板改为图标+文字双列布局
- config.js 21 个面板条目新增 icon 字段(element-plus 图标)
- 面板 item 渲染 el-icon + 名称,替换死的 img 占位
- 面板容器改 grid 两列,卡片式样式含 hover 高亮
2026-10-12 14:59:47 +08:00
pikachu1995@126.com
ebbf42cb76 chore(manager): 移除装修页顶栏的店铺装修标题 2026-10-12 14:51:45 +08:00
pikachu1995@126.com
1e7e78a841 fix(manager): 修复楼层装修审查发现的问题
- PanelArticle 文章分类字段名改为后端的 articleCategoryName
- blank/line/richText 预览组件补默认值兜底,避免拼出 undefinedpx
- 画布未知组件渲染可点击删除的占位条并 console.warn
2026-10-12 11:01:19 +08:00
pikachu1995@126.com
350e79895d feat(manager): 新增店铺列表装修组件 2026-10-12 10:23:00 +08:00
pikachu1995@126.com
2bee689198 feat(manager): 新增文章列表装修组件 2026-10-12 10:21:40 +08:00
pikachu1995@126.com
de333c2d34 feat(manager): 新增优惠券装修组件 2026-10-12 10:19:52 +08:00
pikachu1995@126.com
0c7ecbe039 feat(manager): 新增积分商城装修组件 2026-10-12 10:18:18 +08:00
pikachu1995@126.com
7a1ae5d5da feat(manager): 新增富文本装修组件 2026-10-12 10:16:45 +08:00
pikachu1995@126.com
2bbec5b9e5 feat(manager): 新增辅助线装修组件 2026-10-12 10:15:08 +08:00
pikachu1995@126.com
b4d67859da feat(manager): 新增辅助空白装修组件 2026-10-12 10:13:59 +08:00
pikachu1995@126.com
15c229fa62 feat(manager): 装修右栏按type分发配置面板机制 2026-10-12 10:12:11 +08:00
pikachu1995@126.com
3278726a25 feat(manager): 装修组件面板三类分组,移除促销活动组件,画布对未知类型增加守卫 2026-10-12 10:10:22 +08:00
pikachu1995@126.com
5df1ab6993 feat(manager): 装修编辑器共享控件库 ColorField/SliderField/SegmentField 2026-10-12 10:05:11 +08:00
pikachu1995@126.com
870bacdd8c refactor(seller): 重构店铺详情页面并优化表格列宽度
- 调整售后订单和订单列表中会员ID列宽度从120到185
- 将主题色管理状态从待办改为已完成
- 重构店铺详情页面UI组件结构和样式
- 更新店铺详情页面为卡片式布局并添加标签页功能
- 优化店铺详情信息展示为描述列表形式
- 修改订单类型选项将赠品订单和拼团订单替换为电子卡券订单和预约服务订单
- 重命名退款退货相关组件和变量为售后相关命名
- 添加店员列表标签页展示店铺员工信息
- 实现空白值过滤工具函数处理空数据展示
- 更新页面默认激活标签页为基本信息页
2026-10-09 17:06:59 +08:00
pikachu1995@126.com
bbe336d902 fix(theme): 优化主题色设置功能并修复令牌刷新逻辑
- 移除楼层列表中的主题色设置按钮及路由
- 重构主题色设置页面,新增系统预设色系和自定义模式
- 添加移动端真实页面预览功能,支持商品详情和订单页面预览
- 优化商品详情页优惠券展示样式和布局
- 修复买家端API刷新令牌请求的拦截器递归死锁问题
- 添加无refreshToken时的刷新请求保护机制
2026-10-09 15:50:56 +08:00
pikachu1995@126.com
1bd50cee45 fix(buyer): 修复商品列表路由变化时重复请求
- $route watcher 中 handleSearch 与末尾兜底调用重叠,带 keyword 时同一请求发两次,改为互斥分支
- 修复从关键词搜索切换到分类/促销路由时 params.keyword 残留旧值的问题
2026-10-09 09:50:38 +08:00
pikachu1995@126.com
b639bc36eb fix(buyer): 结算页异常兜底与白屏防御
- 1 处裸 await 包 try/catch,12 条 .then 链全部补 catch(原仅 2 条)
- 结算数据加载失败给用户可见提示并保持页面可操作,不再白屏;错误提示与 request 层拦截器去重
- 约 20 处深链式访问加可选链/默认值(含模板渲染层)
- 修复无效商品跳回购物车后缺少 return 导致的 TypeError
2026-10-09 09:46:06 +08:00
pikachu1995@126.com
89711af0e6 chore(buyer): 落地 ESLint 9 并清理调试代码
- 新增 eslint.config.js(flat config, vue3-essential),移除废弃的 .eslintrc.js/.eslintignore(引用已废弃的 babel-eslint),package.json 增加 lint/lint:fix 脚本
- 清理 17 处调试 console.log;修复 24 处空 catch(15 处补 console.warn 业务提示,9 处加注释说明可安全忽略)
- 修复存量 lint error:9 处 prop 默认值改为函数形式、3 处未使用组件、1 处 v-if/v-for 同级、1 处空 else
- vue.config.js 生产环境 terser 开启 drop_console 兜底
2026-10-09 09:45:59 +08:00
pikachu1995@126.com
3ec3f11d5b chore(seller): yarn.lock 入库,锁定依赖版本 2026-10-08 15:24:26 +08:00
pikachu1995@126.com
5fcc26a866 refactor(seller): 商家端系统性优化与 Vue3 迁移遗留问题修复
- 清理:删除 4 个未使用依赖、约 2700 行死代码(shop/ossManage 等)、tree-table 死组件、37 处未使用 import
- ESLint 9 flat config 升级(.eslintrc.js -> eslint.config.js),修复迁移遗留的 34 处真实 bug
- axios 重构:拦截器不再吞错、refreshToken 单例共享 Promise、超时收敛、form 序列化抽公共函数、vue-router 4 失效 API 修正
- 安全:4 处 v-html 增加 xss 过滤;logout 改为精确删除 localStorage 键
- API 重复定义收敛(delSpec/insertOrUpdateSpec、getDictData、ws)
- 首屏瘦身:tinymce 懒加载(约省 7.4MB)、core-js 按需 polyfill、vendor chunk 合并、dplayer/@antv/g2 动态 import、3 张大图压缩
- 页面修复:发布商品表单布局(iview 遗留 margin 移除)、beforeVideoUpload 补定义、el-tag type 空值、lili-dialog emits 声明、消息中心布局塌陷修复与样式重构、department-choose 空值、affix-time 不再发多余店铺请求、goods/distributionGoods 去掉品牌筛选
2026-10-08 15:22:56 +08:00
pikachu1995@126.com
8484936e3d refactor(axios): 优化HTTP请求拦截器和错误处理机制
- 移除调试用的console.log语句
- 调整表格列对齐方式,将操作列从居中改为右对齐
- 添加搜索表单功能,包括账单号、店铺名称、生成时间等筛选条件
- 重构axios拦截器逻辑,简化错误处理流程
- 实现统一的token刷新机制,使用单例模式避免重复请求
- 优化防抖函数实现,提高性能表现
- 添加401和403状态码的特殊处理逻辑
- 统一错误提示信息的显示方式
- 优化表格列宽度设置,调整金额相关列的显示效果
- 删除未使用的组件文件和样式代码
- 移除调试相关的代码和注释
- 优化图标组件的导入方式,使用markRaw包装
- 优化热词统计图表的数据显示逻辑,添加空数据提示
2026-10-08 14:39:12 +08:00
pikachu1995@126.com
1b275cf592 perf(image): 优化图片加载性能
- 为所有商品图片添加 loading="lazy" 属性实现懒加载
- 为所有图片元素添加 decoding="async" 属性实现异步解码
- 优化购物车页面的商品缩略图加载方式
- 改进订单中心各页面的图片加载策略
- 提升评价系统中图片的加载性能
- 优化会员中心收藏和足迹功能的图片加载
- 改善促销活动页面的图片加载体验
- 更新首页装饰组件中的图片加载属性
- 提高地址管理页面的界面展示效果
2026-10-08 14:36:22 +08:00
pikachu1995@126.com
4c232284f8 feat(goods, order): 完善预约与订单详情功能
- 预约规则区域的说明中补充可预约时段
- 价格区域的标题和说明改为根据商品类型动态展示
- 优化订单详情中配送信息的展示,仅在实物订单中显示
- 核销记录新增搜索功能,支持按商品名称和核销人筛选
- 精简订单列表搜索表单,移除不必要的字段,界面更简洁
2026-10-02 15:28:25 +08:00
pikachu1995@126.com
326ccea1b4 feat(router, appointment, procurement): 完善功能并优化界面
- 新增采购订单列表路由
- 预约工单列表新增状态筛选,搜索更方便
- 首页新增未完成预约工单和待审核采购的提醒
- 优化多个组件的样式与布局,提升用户体验和全站一致性
2026-10-02 15:27:52 +08:00
pikachu1995@126.com
8410179885 feat(buyer): 增强搜索功能并提升用户体验
- 新增 `getStoreById` 接口方法,用于获取包含评分在内的店铺信息
- `Search.vue` 新增搜索建议面板,展示搜索历史和热词,优化搜索体验
- 新增 `SearchSuggest.vue` 组件,用于展示搜索建议和搜索历史
- `App.vue` 根据路由元信息按条件设置页面标题
- 优化多个组件的布局与样式,让整体风格更统一
- 重构多个组件的跳转逻辑,改用路由跳转(`router.push`)替代新开窗口(`window.open`),提升用户体验
2026-10-02 15:26:58 +08:00
pikachu1995@126.com
4777eb2dd6 feat(buyer): 新增功能并优化样式,提升用户体验
- 新增 `getMemberMessage` 接口方法,用于获取会员消息
- 调整全局及商品列表样式,提升可读性与一致性
- 精简 `Search.vue`,移除不必要的店铺搜索切换
- 放宽 `addressManage` 中地址别名的输入长度
- 优化 `ShowGoods.vue` 和 `ShowGoodsDetail.vue` 的布局与样式
- 更新 `Cart.vue` 中的购物车步骤展示,让流程导航更清晰
- 调整多个组件,统一字号并优化界面元素
2026-09-29 18:11:31 +08:00
pikachu1995@126.com
867b03ebfc refactor(buyer): 优化组件结构并提升用户体验
- Main.vue 路由逻辑改为使用当前路由信息
- 简化 Header.vue 中的购物车展示,优化布局与可访问性
- GoodsClassNav.vue 改为按条件渲染分类信息
- 完善 MyOrder.vue 的订单展示与交互,优化样式与结构
- 调整 message.js,更稳妥地处理 Promise 拒绝

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 10:48:44 +08:00
pikachu1995@126.com
a6063874c7 Merge branch 'master' into feat/appointment-goods 2026-09-28 14:25:59 +08:00
pikachu1995@126.com
037fd0caa0 refactor(goods): 重构商品管理模块的API接口和页面结构
- 移除manager端的预约改期日志相关API接口
- 移除seller端的预约改期相关API接口
- 更新seller端API请求方法,将putRequest替换为putRequestWithNoForm
- 修改服务人员更新接口,使用POST方式并传入ID参数
- 重构seller端商品搜索表单,增加SKU名称、SKU货号、店铺分类等搜索条件
- 新增商品创建时间范围选择器和库存状态筛选功能
- 优化商品表格列结构,调整销售模式与商品类型显示逻辑
- 添加商品复制功能按钮
- 重构manager端商品详情页面,实现新的详情展示布局
- 新增商品审核弹窗和时段可约数量抽屉组件
- 优化商品详情页的商品参数展示逻辑
- 添加预约商品相关的特殊处理逻辑
2026-09-28 14:24:24 +08:00
田香琪
ea897b154b Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-09-24 17:49:09 +08:00
田香琪
54c72239d4 refactor(page-decoration): 用 beforeRelease 替换发布开关逻辑
- 楼层与移动端装修列表改用 before-change,避免开关被直接切换
- 唯一页面禁止关闭,已开启页面提示改开其他页
- 按接口成功/失败再决定是否切换开关
2026-09-24 17:45:46 +08:00
pikachu1995@126.com
1bad9a14db feat(appointment): 预约商品发布改为可视化配置,并接入表单设计器
商家端补齐预约规则、时段库存与系统表单设计;运营端增加预约工单与订单设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 17:06:16 +08:00
pikachu1995@126.com
50ebc7d6df refactor(buyer): 移除 PC 商城分销推荐入口与相关接口
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 09:55:20 +08:00
田香琪
ff38cf52b4 feat(distribution): 添加自动申请模式提示信息 2026-09-18 17:50:34 +08:00
Ryan Ran
2fdab71d14 fix(manager): 钉住 webpack 5.108.4,修复生产构建 $filters 全部报错
webpack 5.108.0 回归(webpack/webpack#21278):生产构建在 mangleExports 下遇到 namespace import 整体逃逸(app.config.globalProperties.$filters = filters)时,会把混淆后的导出名当属性路径渲染(u6 -> .u[6]),导致 $filters.unitPrice 等全部报 Cannot read properties of undefined (reading '6');dev server 不启用 mangleExports,本地无法复现。

- manager:webpack 固定 5.108.4(devDependencies + resolutions),pnpm-lock 同步更新
- buyer:纳入 pnpm-lock.yaml(webpack 5.111.0)

Refs: https://github.com/webpack/webpack/issues/21278
2026-09-18 11:29:18 +08:00
田香琪
b51148f520 Merge branch 'fix/distribution-statistics-v2-empty-date' 2026-09-15 10:43:26 +08:00
田香琪
87db9dbb12 Merge branch 'fix/featured-goods-display-count-limit' 2026-09-15 10:24:45 +08:00
田香琪
fb979ea514 feat(distribution): 增强主推商品选择功能与同步逻辑
- 添加 row-key 属性以优化表格行选择
- 引入 featuredPickerDraft 用于临时存储选择的商品
- 实现同步选择逻辑,确保用户在选择商品时状态一致
- 更新确认逻辑,确保用户选择的商品数量不超过限制
- 优化用户交互体验,提升选择反馈
2026-09-15 10:24:12 +08:00
田香琪
8d18f05085 feat(login): 增强扫码登录功能与会话管理
- 登录接口增加 loading 与超时配置,改善请求体验
- 根据服务端返回处理二维码过期,并动态设置有效时长
- 重构二维码刷新逻辑,改用 setTimeout 控制过期
- 合并会话管理与错误处理,简化扫码登录流程
- 移除多余 watch,精简组件行为
2026-09-15 09:46:45 +08:00
田香琪
dfb790e713 refactor(distribution): 移除不必要的时间参数构建逻辑
- 删除 buildQueryParams 方法及相关的 startTime 和 endTime 参数,简化数据查询逻辑
- 更新 loadOverviewData 和 loadBehavior 方法,直接调用 getDistributionOverviewV2,无需构建额外的查询参数
- 优化代码结构,提高可读性和维护性
2026-09-13 17:06:41 +08:00
田香琪
62d55e2b92 feat(distribution): 添加主推商品数量设置与超限提示
- 新增主推商品数量选择功能,用户可选择显示5个或10个商品
- 增加超限提示,确保用户在自定义商品选择时不超过限制
- 更新相关描述信息,提升用户体验与交互反馈
2026-09-13 10:06:33 +08:00
田香琪
f4a50cd854 refactor(distribution): 更新分销资格提示与申诉逻辑
- 修改分销资格被清退的提示信息,明确只可通过后台恢复,不再提供申诉功能
- 移除申诉相关的逻辑与按钮,简化用户交互
- 调整状态管理,去除不再使用的申诉状态
2026-09-09 15:21:07 +08:00
田香琪
47a2c40ffd feat(distribution): 添加百分比与分数转换功能
- 新增 toPercent 和 toFraction 函数,分别用于将小数转换为百分比和将百分比转换为小数
- 更新 distributionGrade.vue 中的佣金率处理逻辑,确保数据在保存和展示时正确转换
- 修改 distributionGradeHistory.vue 中的百分比显示逻辑,确保格式一致性
2026-09-07 11:12:55 +08:00
田香琪
fddb82e680 refactor(GoodsClassNav): 优化品牌ID处理与选择逻辑
- 重构品牌ID的获取方式,使用新方法 appendBrandIdsByNames 简化逻辑
- 更新多选确认逻辑,避免重复选择相同类型
- 调整标签内容显示逻辑,确保品牌标签在选择时正确显示
- 修复商品列表页的分页参数,确保从第一页开始加载数据
2026-09-03 17:17:11 +08:00
田香琪
2d80a795a7 feat(goodsOperation): 完善商品发布流程与分类选择界面
- 引入商品类型选择对话框,增强用户体验
- 重新组织商品发布步骤,优化步骤指示器
- 更新商品分类选择逻辑,确保用户选择到三级分类
- 调整数据传递与状态管理,支持商品编辑与复制功能
- 优化样式与布局,提高界面可用性
2026-08-27 17:48:16 +08:00
田香琪
ffdae4a332 refactor(renovation): 重构页面数据处理逻辑
- 将页面数据处理逻辑提取到 applyPageData 方法中,简化 onOk 和 getTemplateItem 方法
- 增强数据解析和错误处理,确保在无数据或解析失败时正确初始化 modelForm
- 优化对 topAdvert 类型的处理逻辑,提高代码可读性和维护性
2026-08-25 10:46:44 +08:00
田香琪
3ca9aad9be feat(editor): 增强 TinyMCE 编辑器功能与内容同步
- 提交前同步 PC 与移动端编辑器内容
- 增加编辑器未初始化时的待写入内容处理
- 优化内容读写方法,保证数据绑定准确
- 重构事件处理,避免初始化时覆盖 v-model 值
2026-08-24 12:46:39 +08:00
田香琪
085674dfa7 fix: 修复入驻误校验与商品品牌显示 0,并优化分类、运费模板交互
- 入驻第三步改为先回填再渲染,避免空字段进页即提示
- 入驻协议无内容时按空字符串处理,避免读取 null
- 商品品牌空值展示占位符,提交时再转为 0
- 店内分类为空时横向展示「暂无分类」
- 运费模板选择地址默认折叠,操作文案改为「选择地址」
2026-08-20 16:55:05 +08:00
田香琪
9540a1b98b refactor(goodsOperationSec): 优化批发规则表单与预览展示
- 调整批发规则表单的样式与结构,增强可读性
- 更新批发规则预览逻辑,确保信息准确展示
- 增加样式类以改善用户体验,确保输入框与按钮布局合理
2026-08-17 12:57:58 +08:00
田香琪
b9d48fab05 为保持开发环境一致性,将管理端与商家端的 API 基础地址均从 localhost 修改为生产环境地址。
说明:
fix(config):配置类修复,保留 git commit 前缀规范
manager:管理端
seller:商家端
localhost:本地地址
2026-08-17 11:18:08 +08:00
田香琪
6552767823 Merge branch 'salesman' 2026-08-17 11:05:22 +08:00
田香琪
f42f5903d3 fix(userManage): 提交前对密码进行 MD5 加密
- 用户管理表单在注册或更新用户时,先将密码 MD5 加密后再提交服务端,提升密码传输安全性
2026-08-13 19:20:08 +08:00
田香琪
0c94b67ad9 Merge branch 'feature/order-delivery-fulfillment' 2026-08-13 17:19:53 +08:00
田香琪
3b665f6d8c feat(orderFulfill): 增强订单发货逻辑与界面
- 新增无需配送场景及发货模式相关工具函数
- 更新订单详情与订单管理页,支持整单发货、分包裹发货、无需配送
- 优化物流信息与订单日志展示
- 重构订单发货 API 以适配新逻辑
- 增强订单列表与详情页的发货选项与操作展示
2026-08-13 17:19:06 +08:00
田香琪
420e8a84e5 fix(goodsOperationSec): 修复商品编辑页店内分类无法回填的问题
- 初始化店内分类 ID 列表时过滤空值
- 分类树数据加载完成后调用 setCheckedKeys 回填勾选状态
- 使用 const 声明 shopCategories 提升可读性
2026-08-13 16:37:52 +08:00
pikachu1995@126.com
05b14ada18 fix(promotion): 打开商品选择器前先初始化已选 SKU 列表
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 15:35:34 +08:00
pikachu1995@126.com
aed9d86ada Merge branch 'master' into salesman 2026-08-11 15:34:46 +08:00
pikachu1995@126.com
6933fb79ec feat(distribution): 优化分销提现页面功能
fix(setting): 更新海报和邀请卡背景图尺寸说明
style(table): 统一全局表格表头样式
refactor(home): 优化首页待办事项点击跳转
refactor(main): 优化主布局侧边栏收起动画
chore(menu): 优化菜单导航结构
docs(readme): 更新项目文档信息
feat(task): 新增定时任务管理页面
2026-08-11 15:16:32 +08:00
pikachu1995@126.com
4be36d61c7 增加二级分销相关设置以及页面 2026-08-05 18:03:45 +08:00
田香琪
d4cd6e0936 feat(buyer): 增强商品详情与支付逻辑
- 引入 buyerLogin 工具,优化商品详情页的链接跳转逻辑,确保未登录用户能正确重定向到登录页面
- 更新商品 SKU 详情 API,支持未登录状态下的访问
- 修改支付 API,新增 skipMessage 参数以控制支付提示信息
- 改进商品列表与支付页面的用户体验,确保更流畅的操作流程
- 规范化商品类型与库存提示,提升用户体验
2026-08-05 14:17:39 +08:00
田香琪
c0b00999c5 refactor(goodsType): 删除冗余的 promotionPriceMin 函数定义
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 16:13:01 +08:00
田香琪
1accdf5826 feat(buyer): 更新电子卡券(E_COUPON)相关逻辑与展示
- 优化购物车与支付页面,确保电子卡券只能通过立即购买结算
- 增强商品详情页,添加限时折扣与第N件优惠展示
- 更新促销逻辑,支持拼团与积分购买选项
- 规范化商品类型与库存提示,提升用户体验
2026-08-02 13:55:19 +08:00
田香琪
1ae7ab3036 feat(buyer): 添加电子卡券(E_COUPON)支持
- 新增 E_COUPON 商品类型,支持电子卡券的展示与购买逻辑
- 优化商品详情页,调整促销、库存和购买按钮逻辑以适应电子卡券
- 增加卡密信息展示与管理功能,支持商家端卡池管理
- 更新相关页面以处理电子卡券的特殊逻辑,如免地址、隐藏优惠券等
2026-07-31 13:44:56 +08:00
田香琪
36878e6f7f Merge branch 'fix/promotion-datetime-format' 2026-07-28 10:30:44 +08:00
田香琪
09b735e82a feat(buyer): 增加短信验证码发送状态管理
- 新增 codeSent 状态以跟踪短信验证码是否已发送
- 在登录流程中添加验证,确保用户完成安全验证和获取验证码
- 优化验证码发送逻辑,确保用户体验
2026-07-28 10:29:14 +08:00
pikachu1995@126.com
24f829033f Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-07-23 09:30:26 +08:00
pikachu1995@126.com
a72ff640cc feat(ossManage): 进行商家 素材管理 功能改造与界面优化 2026-07-23 09:29:47 +08:00
田香琪
68a791a439 Merge branch 'fix/im-vue3-migration' 2026-07-22 18:10:04 +08:00
田香琪
c0ca9672a1 fix(buyer): 未登录联系客服时提示登录
- 将登录提示由 Message 改为 Modal
- 新增 promptLogin 方法处理登录跳转
- IMService 增加 accessToken 校验,未登录时提示登录
- 改进用户信息获取的错误处理
- 重构 IM 链接 URL 拼接,提升可读性
2026-07-22 18:08:42 +08:00
pikachu1995@126.com
6ee49ce9ac feat:优化店铺管理相关功能并进行界面改造 2026-07-22 17:52:11 +08:00
pikachu1995@126.com
c83d715234 feat (router): 新增采购详情路由,优化预警数量管理功能
在管理员模块与商家模块中新增采购详情页面路由。
升级预警数量管理组件,优化交互体验与功能逻辑。
2026-07-22 15:52:15 +08:00
田香琪
4bcaf14bad fix(im): 迁移 Element UI 图标并修复 Vue 3 兼容问题
- 新增 migrate-legacy-icons 脚本,批量将旧图标替换为 legacy-el-icon 组件
- 多个组件中将 <i class="el-icon-*"> 统一改为 <legacy-el-icon name="el-icon-*">
- 将 size="mini"/"medium" 调整为 Element Plus 的 small/default
- 新增 legacy-el-icon 全局组件及图标映射逻辑
2026-07-22 13:36:44 +08:00
pikachu1995@126.com
e91d783e22 Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-07-21 18:46:39 +08:00
pikachu1995@126.com
4bb3d33f89 fix: 复选框的值双向绑定,并补充 tbody 标签,保证表格结构规范统一 2026-07-21 18:46:33 +08:00
田香琪
12291ad4d5 feat(invoiceModal): 发票弹窗组件新增 change 事件
- 在发票弹窗中增加 change 事件 emit,便于父组件监听数据变化
- 优化 coupon-publish 组件的 selectedMember、selectedList 绑定,完善会员选择功能
2026-07-21 17:01:38 +08:00
田香琪
829d3e65e7 Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-07-21 13:29:47 +08:00
pikachu1995@126.com
3be9aa5c14 feat(statistics): 添加店铺业绩、商品同比环比、销售订单明细及商品销售汇总报表视图和相关 API 接口 2026-07-20 15:31:41 +08:00
pikachu1995@126.com
6c9a3820e8 feat(statistics): 添加统计模块及相关视图 2026-07-20 09:10:56 +08:00
田香琪
d172a1c9b8 fix(cart): 调整错误提示展示并优化已下架商品遮罩样式
简化 Cart.vue 中错误文案展示,移除多余 margin
优化 error-goods 样式:使用 inset 定位,并通过 box-sizing 管理 padding,改善布局与响应表现
2026-07-17 17:35:42 +08:00
田香琪
59c98d1289 chore(config): 更新开发环境的 API 基础地址
将 manager 与 seller 配置中的 API 基础地址由本地开发地址改为生产地址,以统一各环境配置。
2026-07-17 11:46:56 +08:00
田香琪
b7235f5644 refactor(orderDetail): 迁移 UI 组件并优化订单信息展示
将 Card、Button 等组件替换为 el-card、el-button,与 Element Plus 保持一致
优化订单信息展示,补充备注缺省值,并改进发票信息处理
根据订单类型与可操作权限调整按钮显示条件
2026-07-17 11:11:23 +08:00
田香琪
9dc4829f1f fix(buyer): 解决 Profile.vue 合并冲突标记导致的编译错误
保留 getMemberMsg 安全加载用户信息逻辑,并统一使用 buyerUrl 上传接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 21:13:06 +08:00
田香琪
843134c82a Merge branch 'vue3' 2026-07-16 16:56:45 +08:00
田香琪
7e87214b6d refactor: 移除未使用的商品规格相关 API 接口与组件,优化用户管理与物流功能
- 删除了与商品规格相关的 API 接口和组件,简化代码结构。
- 更新用户管理功能,确保表单字段与验证规则一致。
- 优化物流添加与编辑功能,确保请求参数的正确性与一致性。
2026-07-16 16:52:55 +08:00
田香琪
192a9ca53f fix(manager): 解决 util.js 合并冲突标记导致的编译错误 2026-07-15 17:07:46 +08:00
田香琪
1df821aa0c Merge branch 'vue3' 2026-07-15 16:50:18 +08:00
田香琪
3e3644f4a4 Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-07-15 16:40:44 +08:00
田香琪
b1f62d599b feat(manager): 增强运营后台表单校验与页面体验
- 富文本编辑器支持 Vue3 modelValue 双向绑定
- 补充 lazyLoading 路径别名,跳过已弃用的分销等级菜单
- 完善文章、优惠券活动、砍价活动、商品参数等表单校验
- 修复文章管理布局遮挡、店铺选会员弹窗及操作列分隔符显示问题

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 16:39:10 +08:00
chc
4e0ee8e899 移除直播中控台消息发送按钮 2026-07-15 13:48:30 +08:00
田香琪
7804d4eba6 refactor: 移除已弃用的商品规格相关 API 接口与菜单路由检查 2026-07-15 11:03:45 +08:00
田香琪
1fa3a91979 解决 Profile.vue 合并冲突,同步master代码 2026-07-14 16:45:10 +08:00
pikachu1995@126.com
29d5ba98ff Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-07-13 17:38:20 +08:00
pikachu1995@126.com
c25c16adf3 替换登录背景图 2026-07-13 17:38:17 +08:00
chc
de9bf1902a Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-07-13 17:37:22 +08:00
chc
f2ec4530ed 直播功能完善提交 2026-07-13 17:37:10 +08:00
pikachu1995@126.com
7bbcab7679 feat: 更新 API 路径与文档 2026-07-13 17:36:11 +08:00
pikachu1995@126.com
f646022926 Merge branch 'vue3' 2026-07-13 16:56:36 +08:00
pikachu1995@126.com
f6037dc18b refactor: 更新 README 文档与后端服务配置 2026-07-13 16:49:49 +08:00
pikachu1995@126.com
29acc7f0ee Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-13 16:48:47 +08:00
chc
5ef5adf9be Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-07-13 15:28:32 +08:00
chc
9647e7614b 直播功能补充提交 2026-07-13 15:26:56 +08:00
pikachu1995@126.com
fb7c0c9ca2 refactor: 更新文件上传路径配置
- 将文件上传路径从 commonUrl 更新为 sellerUrl,确保 API 请求的正确性。
2026-07-13 14:32:34 +08:00
pikachu1995@126.com
0002a39fc4 Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-13 14:32:20 +08:00
pikachu1995@126.com
e67ea56651 refactor: 更新 API 请求路径与配置 2026-07-13 14:30:09 +08:00
田香琪
4cd96980ba Merge branch 'fix-vue3-left-bug' 2026-07-10 13:32:53 +08:00
田香琪
d648855533 feat: 增强用户登录体验与组件逻辑优化
- 在多个组件中添加用户登录状态检查,确保用户在进行特定操作前已登录。
- 更新购物车、消息中心等组件的点击事件,未登录时提示用户登录。
- 优化优惠券领取逻辑,未登录时引导用户登录。
- 调整样式以提升组件一致性,确保用户界面友好。
2026-07-10 13:31:14 +08:00
田香琪
9bed373ec1 Merge branch 'fix-vue3-left-bug' 2026-07-09 17:59:19 +08:00
田香琪
c355194aba feat: 优化商品详情与购物车功能
- 更新 ShowGoods.vue 和 ShowGoodsDetail.vue 中的按钮样式,统一使用 goods-action-btn 类。
- 在 Cart.vue 中实现商品收藏状态的同步与处理,优化收藏功能的用户体验。
- 调整 GoodsDetail.vue 中的店铺收藏按钮样式,提升视觉一致性。
- 在多个评价组件中引入主题色,增强评分组件的视觉效果。
2026-07-09 17:58:01 +08:00
田香琪
15fa1a5e52 Merge branch 'vue3' 2026-07-09 11:03:16 +08:00
田香琪
ecf11ae2d2 feat: 添加直播间推荐功能与视频上传优化
- 在 promotion.js 中新增 whetherStar 函数,用于更新直播间推荐状态。
- 在 goodsOperationSec.vue 中优化视频上传组件的样式与逻辑,增加视频删除功能。
- 在 live.vue 中集成 whetherStar 函数,优化直播间推荐状态切换逻辑。
2026-07-09 10:49:55 +08:00
田香琪
fb2a9e7a9f feat: 添加图标映射功能
- 新增 iconMap.js 文件,集中管理应用中的图标资源。
- 提供 getIconUrl 函数以便根据名称获取对应图标的 URL,提升代码可维护性。
2026-07-08 18:37:20 +08:00
田香琪
106e059d1a refactor: 统一组件导入与消息提示方式
- 将多个组件中的 require 替换为 import,提升代码一致性。
- 更新消息提示方式,使用统一的 Message 和 Modal 组件,增强用户体验。
- 调整部分组件的样式和逻辑,确保界面一致性与可读性。
2026-07-08 18:36:06 +08:00
田香琪
f6ddcf4f99 Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-08 16:23:15 +08:00
田香琪
a4659ed1a2 refactor: 统一组件导入方式与样式调整
- 将多个组件中的 require 替换为 import,提升代码一致性。
- 更新上传组件中的回调函数,简化图片选择逻辑。
- 调整样式以适配 Element Plus 组件,确保界面一致性。
- 删除不再使用的样式,优化项目结构。
2026-07-08 16:22:44 +08:00
pikachu1995@126.com
c90559981e Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-08 09:59:08 +08:00
pikachu1995@126.com
b299507df9 feat: 更新登录页面与底部信息展示 2026-07-08 09:56:25 +08:00
田香琪
cf92d9d939 Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-07 19:51:17 +08:00
田香琪
6b61b86585 feat: 优化组件导入与样式调整
- 在多个组件中将 require 替换为 import,提升代码可读性和一致性。
- 在 App.vue 中添加分类树的 API 请求,优化分类数据的加载逻辑。
- 更新样式以适配 Element Plus 组件,确保界面一致性。
- 删除不再使用的样式文件,简化项目结构。
2026-07-07 19:50:07 +08:00
Ryan Ran
ae896ff42c Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-07 16:40:19 +08:00
Ryan Ran
709d114986 feat: 添加直播聊天功能与相关配置
- 在 LiveChatPanel.vue 中实现直播间聊天功能,支持发送和接收消息。
- 新增 useChat.js 组合式 API,管理聊天逻辑与状态。
- 更新 Live.vue 控制面板,集成聊天组件并优化直播状态显示。
- 更新 package.json 和 pnpm-lock.yaml,添加 @tencentcloud/chat 和 hls.js 依赖。
2026-07-07 16:40:09 +08:00
pikachu1995@126.com
cc86430ea4 Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-07 10:56:17 +08:00
pikachu1995@126.com
295aaf1a01 feat: 添加主题色设置功能 2026-07-07 10:56:14 +08:00
田香琪
548335b658 feat: 优化售后服务页面与上传组件
- 在 AfterSaleDetail.vue 中添加样式以增强商品信息展示的可读性。
- 在 ApplyAfterSale.vue 中更新选择原因的下拉框,确保与服务类型关联。
- 在多个上传组件中添加图片选择与确认功能,提升用户体验。
- 在订单投诉与退款页面中调整表格列宽,确保信息展示的清晰性。
2026-07-07 10:38:59 +08:00
pikachu1995@126.com
df35a26f9e Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-07 09:40:52 +08:00
pikachu1995@126.com
c277cf1121 feat: 新增视频组件及配置功能
- 在 config.js 中添加视频类型配置,支持视频上传和样式设置。
- 创建 video-config.vue 组件,提供视频内容和样式的配置选项。
- 更新 decorate.vue 以支持视频类型的渲染和配置。
2026-07-07 09:39:00 +08:00
田香琪
297bdd95dd feat: 更新 API 请求参数与组件样式优化
- 在 account.js 和 member.js 中添加 loading 和 skipMessage 参数,提升 API 调用的用户体验。
- 在 ShowGoods.vue 中优化商品详情展示样式,增强可读性和交互性。
- 在 Cart.vue 中重构购物车商品列表结构,提升布局一致性。
- 在多个页面中修复了提示信息的内容,确保用户操作的清晰反馈。
2026-07-06 16:33:21 +08:00
pikachu1995@126.com
e13ca6281d feat: 更新登录页面与API逻辑 2026-07-06 15:59:14 +08:00
chc
235362d5b0 Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-07-06 14:48:47 +08:00
chc
fc5fb3832d 直播功能菜单调整 2026-07-06 14:48:40 +08:00
田香琪
bd9a5f75b0 fix: 增强错误处理与组件逻辑优化
- 在 App.vue 中添加 JSON 解析错误处理,确保站点信息获取的稳定性。
- 更新多个 API 请求,添加 loading 状态管理,提升用户体验。
- 在 Search.vue 中优化热词列表的解析逻辑,增强容错性。
- 改进购物车、地址管理等 API 请求参数,确保一致性与可用性。
- 新增用户中心布局组件,提升用户界面的一致性与可读性。
2026-07-03 18:29:41 +08:00
田香琪
99de0d4722 style: 更新短信设置页面样式以增强可读性和一致性
- 将 .card 类名更改为 .sms-setting-card,以更好地描述其功能。
- 移除不必要的样式定义,简化代码结构。
2026-07-01 17:30:33 +08:00
田香琪
726d563e1a style: 更新首页样式以增强布局和响应式表现
- 调整了 .flow-list、.flow-item 和 .today-item 的样式,优化了布局和间距。
- 增加了 .flow-box 和 .today-box 的 padding 和 flex 属性,提升了视觉一致性。
- 更新了 .today-list 的布局,确保更好的响应式设计。
- 修复了 .today-item 的样式,确保内容在不同屏幕尺寸下的可读性。
2026-07-01 17:00:30 +08:00
田香琪
7ce1ea8fa4 fix: 修复 formatPrice 对空值和非数字价格的处理
避免表格调用 unitPrice 时因 price 为 undefined/null/非法字符串导致报错。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 15:09:05 +08:00
田香琪
2e5be29a43 feat: 新增消息模板聚合接口及通知类站内信邮箱状态更新功能
- 在 setting.js 中添加了消息模板聚合分页接口和更新通知类站内信邮箱渠道开关的功能。
- 在 axios.js 中重构了 token 刷新和登录重定向逻辑,避免循环依赖。
- 新增 router-holder.js 文件以延迟持有 router 实例,优化依赖管理。
- 更新 util.js 中的动态路由管理逻辑,确保在无 router 实例时不执行相关操作。
2026-07-01 11:41:49 +08:00
chc
ac77acd888 快递鸟电子面单不进入打印页面 2026-07-01 10:39:54 +08:00
田香琪
f4b17254ec Merge branch 'vue3' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui into vue3 2026-06-30 21:10:06 +08:00
田香琪
28a857249a refactor: 重构组件逻辑与样式优化
- 在 App.vue 中重构站点信息的获取与应用逻辑,提升代码可读性与维护性。
- 更新 Search.vue 组件,优化搜索框的样式与结构,增强用户体验。
- 在 element.scss 中注释掉不再使用的样式,并新增确认收货按钮的样式。
- 改进多个页面的布局与样式,确保响应式设计的一致性与美观性。
- 更新订单相关组件的按钮样式,提升视觉一致性与可用性。
2026-06-30 21:09:28 +08:00
chc
22f7b1a343 腾讯云直播 2026-06-29 11:31:29 +08:00
田香琪
ec948b5496 feat: 增强组件功能与样式优化
- 在多个组件中添加了新的属性和方法以提升功能性,如在卡片组件中引入 _ActiveTab 属性以管理活动标签。
- 优化了商品详情和商户页面的收藏逻辑,确保在未登录状态下引导用户登录。
- 更新了轮播组件的路由解析逻辑,增强了用户体验。
- 改进了投诉列表和收藏列表的样式,提升了响应式布局和可读性。
- 在全局配置中添加了对开发环境的错误处理,提升了调试体验。
2026-06-25 16:52:46 +08:00
田香琪
1b2e980fb6 fix: 更新 API 请求参数格式及优化组件样式
- 将 addEvaluation API 的参数从 params 修改为 data,以符合标准请求格式
- 在多个组件中调整样式,提升响应式布局和用户体验
- 优化商品详情页的倒计时显示,增强视觉效果
- 更新购物车操作按钮样式,提升一致性和可用性
2026-06-23 14:13:25 +08:00
田香琪
8a60e23214 feat: 新增全局布局样式并提升组件响应式表现
- 在 global-layout.scss 中引入全局布局样式,统一页面宽度与对齐方式
- 更新多个组件与页面以优化响应式,包括宽度、间距及 flex 布局等调整
- 在 API 请求中增加 loading 状态管理,改善用户体验
- 优化领券中心与商品详情页,提升功能与 UI 一致性
2026-06-23 10:17:35 +08:00
Chopper711
ff55a0fade 完善首页 All-In-One 说明 2026-06-14 15:51:33 +08:00
lifenlong
c1447b4376 feat(促销管理): 添加限时直降和第N件优惠功能 2026-06-07 17:17:19 +08:00
lifenlong
734825ba9b 优化财务相关功能 2026-06-07 16:57:23 +08:00
lifenlong
0da6ef99af 升级Vue3,iView替换ElementPlus
- 删除babel配置、更新依赖与入口初始化
- 全量替换UI组件、样式适配,新增迁移文档与标签/过滤器自动化替换脚本
2026-06-05 17:50:17 +08:00
lifenlong
832fda813b 升级Vue3,iView替换ElementPlus
- 删除babel配置、更新依赖与入口初始化
- 全量替换UI组件、样式适配,新增迁移文档与标签/过滤器自动化替换脚本
2026-06-05 17:49:43 +08:00
pikachu1995@126.com
615ee91511 manager 升级到vue3 2026-05-25 10:49:09 +08:00
pikachu1995@126.com
e7350899bf Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2026-05-22 11:48:29 +08:00
pikachu1995@126.com
ad0522335e feat(礼品卡管理): 添加礼品卡选择、绑定、激活及列表功能
- 在购物车API中新增选择礼品卡的接口
- 在促销API中添加获取、绑定和激活礼品卡的接口
- 在订单详情和支付页面中展示礼品卡抵扣信息
- 新增“我的礼品卡”页面,支持礼品卡的查看、绑定和激活功能
- 更新路由配置以支持礼品卡相关页面
2026-05-22 11:48:19 +08:00
pikachu1995@126.com
38da0d9f3a fix(member/advance/withdrawApply): 完善提现申请审核提交逻辑
- 调整加载状态为submitLoading,通过finally确保加载状态正确关闭
- 优化成功判断逻辑,兼容布尔值和带success字段的响应结果
- 提交成功后重置更多表单状态并关闭相关弹窗
2026-05-20 11:26:52 +08:00
田香琪
020b413e20 feat(提现管理): 更新提现状态显示和逻辑
- 修改提现状态标签为“提现状态”
- 新增“用户确认”状态选项
- 更新状态显示逻辑以支持新状态
2026-05-19 20:35:05 +08:00
Chopper711
1750f04d0b update readme 2026-05-17 19:54:58 +08:00
Chopper711
14d53fa48d Merge branch 'master' of gitee.com:beijing_hongye_huicheng/lilishop-ui 2026-05-17 19:54:19 +08:00
pikachu1995@126.com
aa4bc81da4 feat(会员管理): 添加第三方账户绑定和会员等级功能
- 在登录API中新增获取第三方账户绑定列表、绑定和解绑功能
- 在会员API中新增获取当前会员等级、等级规则和等级列表的接口
- 在会员中心页面中添加第三方账户绑定的UI和逻辑
- 新增会员等级页面,展示当前等级、经验值和经验值记录
2026-05-13 18:53:33 +08:00
田香琪
fe4e0ce75c feat(商品评价管理): 添加商品名称搜索功能和评论置顶功能
- 在商品评价页面中新增商品名称搜索框
- 在评论列表中添加是否置顶的显示和操作功能
- 实现评论置顶状态的更新逻辑
2026-04-16 17:55:16 +08:00
田香琪
052c6e153d fix(会员积分管理): 更新分页设置,调整默认每页显示条目数为20 2026-04-15 16:27:37 +08:00
田香琪
60479b44b1 feat(会员积分管理): 添加会员积分分页功能和总数显示 2026-04-15 16:15:54 +08:00
田香琪
8e0d9148b1 运营后台,积分记录展示会员剩余积分列表 2026-04-15 15:06:33 +08:00
田香琪
fc471a17a0 Merge branch 'invoice_view' 2026-04-14 18:06:59 +08:00
田香琪
b6a2dbc23a feat(发票管理): 新增发票详情功能和优化发票信息展示
- 在会员API中添加获取发票详情的接口
- 更新发票模态框,支持电子普通发票和增值税专用发票的详细信息展示
- 在订单详情页和支付页面优化发票信息的显示逻辑
- 增加发票信息的校验和格式化处理
2026-04-13 18:52:32 +08:00
田香琪
b854f40cc7 feat(虚拟销量管理): 新增单个及批量设置商品规格虚拟销量的API接口 2026-04-01 19:00:58 +08:00
田香琪
a2cd9aebe8 Merge branch 'virtual_sales_volume' 2026-04-01 17:30:10 +08:00
田香琪
2e8d257140 refactor(router): remove unused message template route 2026-04-01 17:29:19 +08:00
田香琪
379fed15b3 feat(虚拟销量管理): 新增虚拟销量设置页面 2026-04-01 17:14:08 +08:00
pikachu1995@126.com
afa3290f11 refactor(router): 移除消息模版路由配置 2026-03-31 16:02:11 +08:00
田香琪
5593ebe574 feat(商品分组): 新增商品分组管理功能
- 添加商品分组相关API接口,包括分页获取、详情查询、增删改操作
- 在商品管理页面支持批量设置商品分组功能
- 新增商品分组管理页面,支持分组的增删改查
- 配置路由以支持商品分组页面访问
2026-03-27 17:40:36 +08:00
pikachu1995@126.com
b18999900d feat(会员): 新增会员分组功能
- 添加会员分组管理页面,支持增删改查
- 在会员列表页新增分组筛选和批量设置分组功能
- 扩展会员管理 API,添加分组相关接口
- 配置路由以支持分组页面访问
2026-03-24 16:24:50 +08:00
pikachu1995@126.com
03ccdf2d43 feat: 添加微信视频号管理模块和商品定时上下架功能
- 新增微信视频号管理页面,包含概况、订单、退单、商品、分类和设置六个标签页
- 实现微信视频号相关API接口,包括类目、商品、订单、退单和概况数据查询
- 在商品管理页面添加批量定时上下架功能,支持选择状态和触发时间
- 优化批量操作下拉菜单,整合上架、下架、定时上下架、物流模板设置和删除功能
- 改进设置页面样式,增强按钮布局的响应式设计
2026-03-15 17:34:50 +08:00
misworga831
40dc2b5d2f feat(商品参数): 优化销售模式处理及商品参数验证逻辑 2026-01-08 09:29:39 +09:00
misworga831
50960ed899 fix(客服跳转): 修复IM链接中ID的获取逻辑 2026-01-07 17:34:34 +09:00
misworga831
ede1de664a feat(商品详情): 增加商品参数初始化和详细信息获取的参数检查 2026-01-07 16:52:18 +09:00
pikachu1995@126.com
6884a89fe5 feat(会员积分): 添加会员积分统计功能
- 新增查询会员积分统计的API接口
- 在积分页面添加积分统计卡片展示总发放积分和未使用积分
- 调整表格列宽样式
2025-12-26 14:04:29 +08:00
pikachu1995@126.com
676410a108 feat(会员管理): 新增会员余额和积分管理功能
- 添加会员余额增加和积分修改的API接口
- 在会员列表页增加余额显示列
- 新增余额增加和积分修改的弹窗及操作逻辑
- 优化会员列表操作列,使用下拉菜单整合更多操作
2025-12-26 12:37:58 +08:00
pikachu1995@126.com
b37e12b7a7 feat(商品参数): 新增参数管理功能并优化相关页面
- 新增参数编辑页面,支持参数名称、必填、索引等属性配置
- 优化参数列表页面,增加搜索和分页功能
- 重构商品发布页面的参数展示逻辑
- 移除商品模板相关功能
- 优化品牌管理页面的分类关联功能
- 调整商品详情页参数展示样式
2025-12-22 14:38:52 +08:00
pikachu1995@126.com
8a0f5ff120 fix(goods): 移除下载结算单接口URL中的多余斜杠 2025-12-09 16:44:18 +08:00
chc
c5fecda212 先注释掉未实际使用功能,后续优化密码修改内容 2025-12-01 18:55:46 +08:00
pikachu1995@126.com
e5d98d022e feat(manager): 调整分页组件默认页面大小为20
- 将所有页面的默认页面大小从10调整为20
- 更新分页选项数组,将最小值从10改为20
- 统一所有分页相关组件的pageSize默认值为20
- 修改pageSizeOpts选项为[20, 50, 100]
- 确保所有涉及分页的业务逻辑使用新的默认值
2025-11-24 13:09:11 +08:00
pikachu1995@126.com
799184fd21 style(components): 将按钮组件替换为链接样式
- 统一将页面中的 Button 组件替换为 a 标签,保持样式一致
- 添加统一的链接样式类 ops-link 和分隔符样式类 ops-sep
- 更新操作列中按钮的样式,使用颜色、光标和文本装饰属性替代原有 props 配置
- 在多个文件中调整了操作项之间的分隔符显示逻辑
- 优化表格操作列渲染函数,去除冗余的 Button 属性配置
- 保持功能不变的前提下提升界面视觉一致性
2025-11-24 11:33:54 +08:00
pikachu1995@126.com
756cb28daa fix(api): 修正更新隐私协议的API路径格式
feat(ui): 在顶部导航栏添加客服按钮及样式
添加客服按钮功能,包括获取IM链接和用户信息验证

refactor(order): 重构分销订单状态显示逻辑
统一订单状态显示为"未完成"、"完成"和"退款",并调整对应颜色
2025-11-23 16:30:41 +08:00
lifenlong
5f6eb6a26d feat(积分商品分类): 添加分页功能并支持按排序值排序
在积分商品分类页面添加分页组件,支持分页查询和排序
新增排序值字段展示,并对分类列表按排序值进行递归排序
2025-11-20 15:32:46 +08:00
pikachu1995@126.com
4efbec5741 refactor(分页): 统一调整分页大小选项为[20, 50, 100]并更新默认pageSize
style(售后订单): 优化商品图片样式,添加圆角和固定宽高
docs(售后订单): 更新售后状态显示文本,使其更清晰易懂
2025-10-27 18:34:01 +08:00
Ryan Ran
86dbc0d86e docs(README): 更新安装指导,新增yarn安装和启动步骤,移除npm相关内容以简化用户体验 2025-10-22 15:01:21 +08:00
pikachu1995@126.com
c32d011851 docs(readme): 更新交流方式和联系方式- 移除了旧的QQ群交流信息
- 新增微信交流群二维码
- 添加在线客服链接
- 精简了联系方式展示方式
- 统一了交流渠道描述格式
2025-10-20 14:52:33 +08:00
pikachu1995@126.com
311ca35afc feat(商品设置): 添加ES商品索引管理功能
新增三个API接口用于管理ES商品索引:
1. 删除ES中下架的商品
2. 删除不存在的索引
3. 生成所有商品的缓存
同时在商品设置页面添加对应操作按钮
2025-10-20 13:43:34 +08:00
Ryan Ran
1a3fae6501 fix(README & package.json): 更新 README 文档以反映 Node.js 版本兼容性,移除 NODE_OPTIONS 环境变量的依赖,确保项目在 Node.js 16 及以上版本下正常运行,优化用户安装指导。 2025-10-10 15:23:27 +08:00
Ryan Ran
3432530de7 fix(README.md): 更新文档以兼容更高的 Node.js 版本(20、18、16),并提供安装指导,确保用户能够顺利拉取最新代码和处理依赖问题。 2025-09-30 14:03:43 +08:00
Ryan Ran
af85493863 fix(buyer端升级node版本): 添加 .npmrc 和 .yarnrc 文件以支持引擎配置,更新 package.json 中的 NODE_OPTIONS 环境变量,替换多个组件中的 /deep/ 选择器为 ::v-deep,优化依赖管理,确保兼容性,兼容node16版本 2025-09-30 13:33:22 +08:00
Ryan Ran
ae85f0d612 fix(manager端升级node版本): 更新多个组件的样式,替换 /deep/ 选择器为 ::v-deep,替换 node-sass 为 sass,优化依赖管理和兼容性,兼容node16版本 2025-09-30 11:38:38 +08:00
Ryan Ran
16c4a78e29 fix(兼容性): 更新 seller 项目的 package.json 和 README.md,添加 NODE_OPTIONS 环境变量以支持 Node.js 版本 20 和 18 的运行,详细说明各版本的兼容性和安装要求,优化文档以提升用户体验。 2025-09-30 11:04:12 +08:00
Ryan Ran
8fb87aa64d feat(seller端升级node版本): 添加 .npmrc 和 .yarnrc 文件以支持引擎配置,更新 package.json 中的引擎要求并替换 node-sass 为 sass,优化依赖管理和兼容性,兼容node16版本 2025-09-29 18:26:52 +08:00
Ryan Ran
c5d675b6d2 fix(商品规格): 优化规格值处理逻辑,确保在规格值为空时移除对应行并更新表格数据,增强用户体验 2025-09-23 19:17:24 +08:00
Ryan Ran
f4d06b2d7b fix(商品审核): 更新审核功能,添加请求头以支持表单数据提交,提升接口兼容性 2025-09-23 14:14:43 +08:00
Ryan Ran
bb610a7cd8 fix(商品审核): 优化审核功能,使用FormData处理请求数据,提升代码可读性和维护性 2025-09-23 14:01:53 +08:00
misworga831
bb864e72b3 fix(商品规格): 优化规格值编辑逻辑,避免空值提交并清理错误提示 2025-09-15 21:45:47 +09:00
misworga831
c6c4797d87 fix(商品规格): 修复规格值过滤逻辑,确保移除空值并更新表格数据 2025-09-15 21:03:33 +09:00
misworga831
25fed42395 fix(商品规格): 添加内联错误提示,优化规格值验证逻辑 2025-09-15 16:04:48 +09:00
misworga831
66c4676493 fix(商品规格): 更新规格值提示信息,增加用户操作指导 2025-09-12 22:35:03 +09:00
misworga831
f8747b5d5d fix(商品操作): 优化模板和方法中的代码格式,提升可读性 2025-09-12 22:19:38 +09:00
misworga831
0eb58d4b8a fix(商品规格): 修复规格值编辑逻辑,避免对未定义索引的访问 2025-09-11 19:52:35 +09:00
pikachu1995@126.com
7b30ea343f feat(商品管理): 添加商品复制功能并优化批量操作
- 在商品操作页面添加复制商品功能,通过copyId参数区分复制操作
- 复制商品时自动清除原商品ID确保提交为新商品
- 优化批量操作按钮功能,包括上架、下架、删除和设置物流模板
- 移除未使用的批量规格更新功能
- 调整商品列表操作按钮布局,将复制功能加入操作列
2025-09-09 17:04:58 +08:00
pikachu1995@126.com
7db8484a7c feat(售后订单): 添加售后数量统计功能并显示在标签页
- 在manager和seller端添加获取售后数量统计的API接口
- 修改售后订单页面,显示各状态对应的数量统计
- 新增计算属性serviceStatusWithCount动态生成带数量的标签页
- 在初始化、搜索和状态切换时调用统计接口更新数据
2025-08-29 11:36:45 +08:00
pikachu1995@126.com
78b058009b feat(订单/商品): 添加数量统计功能并优化列表展示
- 在订单和商品管理页面添加数量统计功能
- 优化订单和商品列表的筛选和展示样式
- 统一API接口参数传递方式
- 移除重复代码和无用代码
2025-08-29 09:40:36 +08:00
pikachu1995@126.com
05abad3905 feat(会员管理): 新增会员ID搜索和头像显示功能
refactor(订单管理): 移除表格边框并优化分页选项

feat(售后订单): 添加关键字搜索字段并移除二维码功能

feat(商品管理): 新增批量操作和审核功能,优化商品列表展示

style: 移除多余的margin-top样式
2025-08-27 16:15:11 +08:00
pikachu1995@126.com
ad178b1806 Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2025-08-27 10:12:05 +08:00
pikachu1995@126.com
075c20ae41 feat(订单列表): 新增搜索字段和支付方式筛选功能
- 添加关键字、商品名称、收货人和支付方式搜索字段
- 增加支付方式显示列和筛选功能
- 调整默认分页大小为20并增加100条选项
- 优化订单状态筛选逻辑
2025-08-27 10:11:40 +08:00
Ryan Ran
56420bd4d0 feat: 添加pageClientType属性以支持页面类型设置
在`renovation.vue`文件中,新增`pageClientType`属性并设置为'PC',以便于在页面数据中区分客户端类型,提升页面功能的灵活性。
2025-08-26 10:40:19 +08:00
pikachu1995@126.com
a18fc729c0 style(oss-manage): 调整表单元素宽度并优化布局结构
将日期选择器和输入框的宽度从200px统一调整为240px,提升表单元素的一致性
拆分操作区域到单独的Card组件中,改善页面布局结构
2025-08-21 16:41:46 +08:00
pikachu1995@126.com
099941a54e style: 统一输入框和选择器宽度为240px并优化卡片布局
- 将多处输入框和选择器的宽度从200px调整为240px
- 优化卡片布局结构,添加分割卡片提升可读性
- 移除部分冗余的刷新按钮和样式代码
- 调整日期选择器等表单元素的宽度为240px
2025-08-21 16:41:27 +08:00
Ryan Ran
3867d51d92 fix: 移除管理端会员列表,回收站username字段,优化成员列表组件的属性结构 2025-08-13 18:18:41 +08:00
Ryan Ran
f1be18b7e3 添加vue-virtual-scroller依赖并在消息视图中实现虚拟滚动功能,以优化对话列表的渲染性能。同时,为manager和seller项目添加minimatch依赖项的解析版本以解决潜在的版本冲突。 2025-07-15 16:16:42 +08:00
Ryan Ran
ef46380f9e 添加minimatch依赖项的解析版本以解决潜在的版本冲突 2025-07-04 17:16:47 +08:00
chc
d711273cf0 商品发布参数项校验无法some问题 2025-03-12 10:28:59 +08:00
chc
5e7cf2d679 Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2025-03-11 19:09:39 +08:00
chc
11319ba21f 分类参数必填校验 2025-03-11 19:09:05 +08:00
Chopper711
9c51523352 refactor(coupon-activity): 注释掉邀新赠券选项 (master)
注释掉了"邀新赠券"的Radio按钮,以简化界面。
2025-02-21 18:44:21 +08:00
Chopper711
b4a706820e ### feat: 优化优惠折扣输入组件 (master)
- 将`Input`组件替换为`InputNumber`组件,以确保输入的优惠折扣
  在0.1到9.9之间,并支持一位小数。
- 在`full-discount-add.vue`和`full-discount-detail.vue`文件中,
  更新了`FormItem`组件的`优惠折扣`输入逻辑。
- 在`coupon-publish.vue`文件中,更新了描述文本以强调输入
  范围和格式。
- 在`coupon.vue`文件中,修复了优惠券类型判断逻辑,增加了
  "DISCOUNT"类型和未知类型的处理。
2025-02-21 17:34:58 +08:00
Chopper711
376a3223dc fix: 优化积分商品添加页面和对账单详情页面的功能 (master)
- 移除积分商品添加页面的冗余标题
- 将兑换时间选择器替换为活动时间范围选择器
- 简化验证规则,使用rangeTime替代单独的开始和结束时间
- 调整批量删除按钮和其他按钮的样式格式
- 在对账单详情页面添加删除消息提示框功能
2025-02-21 16:09:14 +08:00
Chopper711
a85ec69549 fix: 消息提示框以及消息点击查看后需要标记已读按钮优化 2025-02-20 15:48:19 +08:00
Chopper711
ebf4d6d3eb Merge branch 'master' of gitee.com:beijing_hongye_huicheng/lilishop-ui 2025-02-20 14:57:21 +08:00
Chopper711
2ccd8265b9 fix: 消息提示框以及消息点击查看后需要标记已读按钮优化 2025-02-20 14:52:53 +08:00
chc
fb1671d062 Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2025-02-17 13:41:42 +08:00
chc
95e37df2ec 电子面单物流选中错误问题 2025-02-17 13:41:05 +08:00
pikachu1995@126.com
cfde72f1ab Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2025-02-11 15:43:20 +08:00
pikachu1995@126.com
efc778acbc 微信支付配置,添加配置使用公钥还是证书 2025-02-11 15:43:09 +08:00
misworga831
37384ebadd fix: 优化商品编辑 2025-01-21 13:53:07 +08:00
misworga831
9a7aee7e97 fix: 优化商品编辑 2025-01-20 18:43:21 +08:00
chc
1a56534d28 提现设置增加校验 2025-01-20 18:12:41 +08:00
chc
79e7e5f087 Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2025-01-17 17:42:58 +08:00
chc
aea03cf975 支付设置选中状态错误问题 2025-01-17 17:42:49 +08:00
pikachu1995@126.com
8ef6f944dc 微信支付配置,添加微信支付公钥 2025-01-08 16:14:14 +08:00
pikachu1995@126.com
e18d243961 优化微信支付配置,不读取密钥文件,改成读配置文件 2024-12-27 11:01:53 +08:00
chc
765b633cea Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2024-12-25 10:35:50 +08:00
chc
9c402a3878 1.查看物流信息
2.直播图片选择器修改
3.管理端隐藏会员详情
2024-12-25 10:35:40 +08:00
Chopper711
86426b087a feat: 使用InputNumber组件替换折扣输入框 (master)
在coupon-publish.vue中,将折扣输入框从Input组件更改为
InputNumber组件,以便更好地控制折扣值的输入范围。
2024-12-23 11:51:05 +08:00
chc
c5aaa2f3e2 Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2024-12-02 18:05:51 +08:00
chc
cc59bb6094 售后实际退款金额显示错误问题 2024-12-02 18:05:43 +08:00
Ryan Ran
5662da1210 fix: 修改商家端分销商品列表点击链接商品不展示问题 2024-11-25 10:43:49 +08:00
Ryan Ran
863725d3ba 优化管理端活动楼层装修 2024-11-11 15:23:32 +08:00
Yer11214
b1acfbdd4b !40 修复了行政地区切换验证状态没更新的问题
Merge pull request !40 from 孟锴/feature/mengkai-master-branch
2024-10-14 07:34:03 +00:00
mengkai
001f9c3f30 修复了行政地区切换验证状态没更新的问题 2024-10-14 11:26:26 +08:00
Yer11214
8d4df4b66e Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui 2024-10-07 22:26:15 +08:00
Yer11214
604447508a fix: oss选择图片不展示报错bug 2024-10-07 22:26:13 +08:00
misworga831
6f30949620 fix: 移动端楼层装修促销活动移除优惠券.pc端楼层装修快捷导航外部链接无法填写问题 2024-09-23 21:18:27 +08:00
chc
10e8e82fe4 增加部分发货状态 2024-09-18 15:25:04 +08:00
1046 changed files with 191194 additions and 102731 deletions

1
.gitignore vendored
View File

@@ -5,3 +5,4 @@
node_modules/
yarn.lock
package-lock.json
!seller/yarn.lock

682
LICENSE
View File

@@ -1,661 +1,21 @@
GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU Affero General Public License is a free, copyleft license for
software and other kinds of works, specifically designed to ensure
cooperation with the community in the case of network server software.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
our General Public Licenses are intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
Developers that use our General Public Licenses protect your rights
with two steps: (1) assert copyright on the software, and (2) offer
you this License which gives you legal permission to copy, distribute
and/or modify the software.
A secondary benefit of defending all users' freedom is that
improvements made in alternate versions of the program, if they
receive widespread use, become available for other developers to
incorporate. Many developers of free software are heartened and
encouraged by the resulting cooperation. However, in the case of
software used on network servers, this result may fail to come about.
The GNU General Public License permits making a modified version and
letting the public access it on a server without ever releasing its
source code to the public.
The GNU Affero General Public License is designed specifically to
ensure that, in such cases, the modified source code becomes available
to the community. It requires the operator of a network server to
provide the source code of the modified version running there to the
users of that server. Therefore, public use of a modified version, on
a publicly accessible server, gives the public access to the source
code of the modified version.
An older license, called the Affero General Public License and
published by Affero, was designed to accomplish similar goals. This is
a different license, not a version of the Affero GPL, but Affero has
released a new version of the Affero GPL which permits relicensing under
this license.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU Affero General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Remote Network Interaction; Use with the GNU General Public License.
Notwithstanding any other provision of this License, if you modify the
Program, your modified version must prominently offer all users
interacting with it remotely through a computer network (if your version
supports such interaction) an opportunity to receive the Corresponding
Source of your version by providing access to the Corresponding Source
from a network server at no charge, through some standard or customary
means of facilitating copying of software. This Corresponding Source
shall include the Corresponding Source for any work covered by version 3
of the GNU General Public License that is incorporated pursuant to the
following paragraph.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the work with which it is combined will remain governed by version
3 of the GNU General Public License.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU Affero General Public License from time to time. Such new versions
will be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU Affero General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU Affero General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU Affero General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
specific requirements.
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<http://www.gnu.org/licenses/>.
MIT License
Copyright (c) 2020-2026 Beijing Hongye Huicheng Technology Co., Ltd. (北京宏业汇成科技有限公司)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

201
MIGRATION-VUE3.md Normal file
View File

@@ -0,0 +1,201 @@
# Lilishop UI — Vue 3 迁移与上线前检查清单
四个子项目(buyer / seller / manager / im)已完成 **Vue 3 + Element Plus** 迁移,并完成依赖小版本对齐。本文档供上线前统一回归使用。
## 技术栈(四端一致)
| 类别 | 包 | 版本(package.json) |
|------|-----|---------------------|
| 框架 | vue | ^3.5.35 |
| 编译 | @vue/compiler-sfc | ^3.5.35 |
| 路由 | vue-router | ^4.6.4 |
| 状态 | pinia | ^4.0.3(四端已全部从 vuex 迁移完成) |
| 组件写法 | `<script setup>` | 四端 597 个组件已全部完成 Options API → 组合式 API 迁移;各端 eslint 已启用 `vue/component-api-style: ['warn', ['script-setup']]` 防止回退 |
| UI | element-plus | ^2.14.1 |
| 图标 | @element-plus/icons-vue | ^2.3.2 |
| 请求 | axios | ^1.7.9 |
| 兼容 | core-js | ^3.49.0 |
| 构建 | @vue/cli-service | ^5.0.9 |
> 使用 `^` 范围,实际安装版本以各目录 `yarn.lock` 为准。
> 要求 **Node >= 16**,包管理统一 **yarn**。
## 本地启动
```bash
# 买家端
cd buyer && yarn install && yarn dev # 端口 10000
# 商家端
cd seller && yarn install && yarn dev # 端口 10002
# 管理端
cd manager && yarn install && yarn dev # 端口 10003
# IM 客服
cd im && yarn install && yarn dev # 端口 8000
```
生产构建(上线前必跑):
```bash
cd buyer && yarn build
cd seller && yarn build
cd manager && yarn build
cd im && yarn build
```
---
## 一、通用检查(四端都要过)
### 1. 认证与请求(axios 1.x 重点)
各端请求封装位置:
| 端 | 文件 |
|----|------|
| buyer | `buyer/src/plugins/request.js` |
| seller | `seller/src/libs/axios.js` |
| manager | `manager/src/libs/axios.js` |
| im | `im/src/utils/request.js` |
- [ ] **登录**:账号密码登录成功,token 写入正常
- [ ] **登出**:清除 token,跳转登录页
- [ ] **401**:token 过期后跳转登录 / 刷新 token(buyer/manager/seller 有刷新逻辑)
- [ ] **403**:无权限时提示文案正确、不白屏
- [ ] **400 / 5xx**:接口报错能弹出 `Message` / `ElMessage` / `Notification`
- [ ] **网络超时**:断网或超时后有错误提示,页面不卡死
- [ ] **GET 带参**:列表筛选、分页参数序列化正常
- [ ] **POST JSON**:`Content-Type: application/json` 请求体正确
- [ ] **POST 表单**:`application/x-www-form-urlencoded` 提交正常(im 等)
### 2. Element Plus UI(2.6 → 2.14 重点)
- [ ] **表格** `el-table`:列展示、排序、空数据、加载态
- [ ] **分页** `el-pagination`:翻页、每页条数切换
- [ ] **表单** `el-form`:必填校验、错误提示位置
- [ ] **弹窗** `el-dialog` / `ElMessageBox`:打开、确认、取消、关闭
- [ ] **下拉** `el-select` / `el-dropdown`:选项、点击外部关闭
- [ ] **消息** `ElMessage` / `$Message` 兼容层:success / error / warning
- [ ] **加载** `ElLoading` / `v-loading`:请求期间遮罩正常消失
- [ ] **日期** `el-date-picker`:选择、清空、范围选择
- [ ] **上传** `el-upload`:选文件、进度、成功 / 失败回调
### 3. 路由与页面
- [ ] 浏览器 **刷新** 当前路由不 404(history 模式 + 服务端配置)
- [ ] **动态路由** 加载正常(seller / manager 菜单权限路由)
- [ ] 浏览器 **前进 / 后退** 正常
- [ ] 各端 **404 页** 可回到首页
### 4. 构建与部署
- [ ] 四端 `yarn build` 无 error(warning 可记录,不阻断)
- [ ] 生产环境 API 地址配置正确(`config` / 环境变量)
- [ ] 静态资源 `publicPath` 与 Nginx / CDN 路径一致
- [ ] gzip 压缩包(如有)可正常访问
---
## 二、分端专项检查
### Buyer 买家端(10000)
详见 [buyer/MIGRATION-VUE3.md](./buyer/MIGRATION-VUE3.md)
- [ ] 首页装修模块渲染(轮播、商品列表、秒杀区等)
- [ ] 商品详情:规格选择、加购、收藏、`el-image` 预览
- [ ] 购物车 → 结算 → 支付流程
- [ ] 用户中心:订单列表、地址、优惠券、售后
- [ ] 登录 / 注册 / 忘记密码
- [ ] 店铺入驻、商户页
- [ ] 地图(高德 `@amap/amap-jsapi-loader`)定位与选点
- [ ] 侧边栏 `el-menu`、头部 `el-dropdown`、动态图标 `iconMap.js`
### Seller 商家端(10002)
详见 [seller/MIGRATION-VUE3.md](./seller/MIGRATION-VUE3.md)
- [ ] 商家登录、店铺信息
- [ ] 商品发布三步流程(`goodsOperation` 步骤条)
- [ ] 订单管理:发货、退款、详情
- [ ] 促销 / 分销 / 统计图表(`@antv/g2`)
- [ ] 页面装修(wap / modelList,从 manager 同步部分)
- [ ] 图片懒加载 `vue-lazyload`
- [ ] Excel 导出 `vue-json-excel`
### Manager 管理端(10003)
详见 [manager/MIGRATION-VUE3.md](./manager/MIGRATION-VUE3.md)
- [ ] 管理员登录、权限菜单、动态路由
- [ ] 系统设置(`sys/setting-manage` 各 Tab)
- [ ] 菜单 / 部门管理
- [ ] 页面装修(PC + H5)
- [ ] 文章管理
- [ ] 商品 / 订单 / 会员 / 促销等核心列表 CRUD
- [ ] 富文本 TinyMCE 编辑、图片上传
- [ ] 多语言 `vue-i18n` 切换(如有启用)
### IM 客服端(8000)
详见 [im/MIGRATION-VUE3.md](./im/MIGRATION-VUE3.md)
- [ ] 带 token 参数进入会话页
- [ ] 对话列表加载、切换会话、未读数
- [ ] 发送文字、表情、图片消息
- [ ] 商品 / 订单卡片链接跳转(`linkToGoods` / `linkToOrders`)
- [ ] 右键菜单(复制、撤回、删除等)
- [ ] WebSocket 连接 / 断线重连 / 新消息通知
- [ ] 最近浏览、订单列表侧栏(商家场景)
- [ ] 用户名片弹层 `$user()`
---
## 三、已知可忽略项
| 项 | 说明 |
|----|------|
| `vue-awesome-swiper` peer warning | 历史依赖,Vue 3 下仍有 peer 告警,功能正常可暂忽略 |
| im 内 `<i class="el-icon-*">` | Element Plus 无字体图标,部分旧图标可能不显示,不影响核心功能 |
| `vue-prism-editor@2.0.0-alpha.2` | im 代码块编辑器刻意锁定 alpha 版 |
| 打包体积 warning | chunk-vendors 偏大,属 Element Plus 全量引入,可后续做按需优化 |
---
## 四、问题记录模板
上线测试时建议按端记录:
```markdown
### [端名] 问题标题
- 页面/路由:
- 操作步骤:
- 预期:
- 实际:
- 控制台报错:
- 优先级:P0 / P1 / P2
```
---
## 五、子项目文档索引
| 端 | 迁移说明 |
|----|----------|
| buyer | [buyer/MIGRATION-VUE3.md](./buyer/MIGRATION-VUE3.md) |
| seller | [seller/MIGRATION-VUE3.md](./seller/MIGRATION-VUE3.md) |
| manager | [manager/MIGRATION-VUE3.md](./manager/MIGRATION-VUE3.md) |
| im | [im/MIGRATION-VUE3.md](./im/MIGRATION-VUE3.md) |
---
## 六、建议上线顺序
1. **manager**(后台配置源头)
2. **seller**(商家经营)
3. **buyer**(C 端流量最大)
4. **im**(客服,依赖前后端 WebSocket)
每上一端,至少完成本文 **第一节通用检查** + 对应 **第二节分端检查** 后再切下一端。

144
README.en.md Normal file
View File

@@ -0,0 +1,144 @@
<div align="center">
<img src="./manager/src/assets/logo.png" width="180" alt="Lilishop Logo" />
<h1>Lilishop(PickMall) - Web Frontend (Vue 3)</h1>
<p>
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
</p>
<p>
<a href="https://pickmall.cn">🏠 Official Website</a> ·
<a href="https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home">📖 Product Documentation</a> ·
<a href="#2-online-demo">💻 Online Demo</a> ·
<a href="https://github.com/lilishop/lilishop">💻 GitHub</a>
</p>
<p>
<a href="./README.md">简体中文</a> | <b>English</b>
</p>
</div>
> ⭐ **If you like this project, please click `Star` in the top-right corner to support us! Your Star is our greatest motivation for continuous open-source iteration!**
**LILISHOP** is an open-source e-commerce system built with Spring Boot, Vue 3, and uni-app. It supports B2B2C multi-vendor marketplaces, WeChat Mini Programs, H5, Android/iOS Apps, live-streaming e-commerce, affiliate distribution, and flash sales.
### ✨ Why Choose Lilishop
- 🟢 **True MIT License**: Free for commercial use and private secondary development without licensing concerns.
- 🚀 **Modern Vue 3 Stack**: Built on `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus`, leaving legacy Vue 2 templates behind.
- 🧩 **4-in-1 Production Suite**: Covers **PC Buyer Mall (`buyer`)**, **Seller Portal (`seller`)**, **Platform Admin Panel (`manager`)**, and **IM Customer Service Web (`im`)**, complete with drag-and-drop visual storefront decoration and rich analytics.
---
## 1. Overview
This repository (`lilishop-ui`) is the **Web Frontend Suite** of Lilishop, containing four standalone **Vue 3** web applications: **PC Buyer Mall, Seller Panel, Platform Admin Panel, and IM Customer Service Web**.
- **Official Website**: <https://pickmall.cn>
- **Documentation**: [Product Documentation](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home)
- **Gitee Repository**: <https://gitee.com/beijing_hongye_huicheng>
- **GitHub Main Repository**: <https://github.com/lilishop/lilishop>
---
## 2. Online Demo
> **Note**: The verification code for the demo environment is `111111`.
| Portal | URL | Demo Credentials |
| :--- | :--- | :--- |
| **Platform Admin Panel (`manager`)** | <https://admin-b2b2c.pickmall.cn> | `admin` / `123456` |
| **Seller / Store Panel (`seller`)** | <https://store-b2b2c.pickmall.cn> | `13011111111` / `111111` |
| **PC Buyer Mall (`buyer`)** | <https://pc-b2b2c.pickmall.cn> | SMS code: `111111` |
| **Mobile H5 (`uni-app`)** | <https://m-b2b2c.pickmall.cn> | SMS code: `111111` |
| WeChat Mini Program | Mobile H5 ([Visit Directly](https://m-b2b2c.pickmall.cn)) |
| :---: | :---: |
| <img src="https://static.pickmall.cn/images/h5-qrcode.png" width="180" alt="WeChat Mini Program QR Code" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/h5-qrcode.png" width="180" alt="Mobile H5 QR Code" /> |
| Scan with WeChat | Scan or visit <https://m-b2b2c.pickmall.cn> |
---
## 3. Core Features & Tech Stack
### Core Features
- **Four-Portal Synergy**: Includes Platform Admin, Seller Panel, PC Buyer Mall, and IM Web Workbench with clear role boundaries.
- **Drag-and-Drop Visual Decoration**: Built-in visual floor editor (`vuedraggable`) for customizing both PC and mobile storefronts.
- **Multi-Vendor B2B2C Operations**: Merchant onboarding, product auditing, split-order fulfillment, promotions, and financial settlement ledgers.
- **Rich Business Analytics**: Powered by `@antv/g2` for sales overview, YoY/MoM product reports, member analytics, and distribution metrics.
- **IM Customer Service**: Integrated with Tencent Cloud IM / WebSocket customer service for real-time communication.
### Web Tech Stack
| Category | Technology | Description |
| :--- | :--- | :--- |
| Core Framework | `Vue 3.5` + `Vue Router 4` | Modern reactive frontend framework |
| State Management | `Pinia 4` | Official Vue 3 state management library |
| UI Library | `Element Plus 2.x` | Enterprise desktop UI component library |
| HTTP Client | `Axios 1.x` | Unified request/response interceptors & auth handling |
| Data Visualization | `@antv/g2` | Interactive charts and business analytics |
| Visual Decoration | `vuedraggable 4` | Drag-and-drop page floor builder |
| Messaging & IM | `@tencentcloud/chat` / `sockjs-client` | Real-time customer service messaging |
| Video Player | `dplayer` + `hls.js` | Product video and live streaming playback |
| Build & Styling | `Vue CLI 5` + `Webpack 5` + `Sass / Less` | Modular build pipeline and theme customization |
---
## 4. Directory Structure
```text
lilishop-ui
├── buyer/ # [PC Buyer Mall]
│ ├── public/ # Static assets & global config (config.js)
│ └── src/
│ ├── api/ # Buyer API modules (goods, cart, order, member, promotion)
│ ├── components/ # Shared components (floor renderer, navbar, goods card)
│ ├── pages/ # PC Mall pages (Home, Product Detail, Checkout, User Center)
│ ├── router/ # Route definitions & guards
│ ├── stores/ # Pinia stores
│ └── utils/ # Request interceptors & helpers
├── seller/ # [Seller / Store Management Portal]
│ ├── public/ # Static assets & global config
│ └── src/
│ ├── api/ # Seller API modules
│ ├── components/ # Seller shared & decoration components
│ ├── views/ # Seller views (goods, orders, after-sales, promotions, store settings)
│ ├── router/ # Dynamic permission routes
│ └── stores/ # Pinia stores
├── manager/ # [Platform Admin Portal]
│ ├── public/ # Static assets & global config
│ └── src/
│ ├── api/ # Admin API modules
│ ├── components/ # Admin components, rich-text editor, visual floor builder
│ ├── views/ # Admin views (member, goods, order, seller, marketing, finance, stats, system)
│ ├── router/ # Menu & permission routes
│ └── stores/ # Pinia stores
├── im/ # [IM Customer Service Web Client]
│ └── src/
│ ├── api/ # Chat session & message APIs
│ ├── components/ # Chat window, message list, product/order cards
│ ├── im-server/ # WebSocket / IM connection manager
│ └── views/ # Customer service workbench
├── build.sh # Unified build script
└── docker-build.sh # Docker image build script
```
---
## 5. Related Repositories
| Repository | Description | Links |
| :--- | :--- | :--- |
| **`lilishop`** | Backend Services (Spring Boot 4 / Java 21) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop) · [GitHub](https://github.com/lilishop/lilishop) |
| **`lilishop-ui`** | PC Mall / Seller / Admin / IM Web Frontend | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-ui) |
| **`lilishop-uniapp`** | Mobile Buyer Client (H5 / Mini Program / App) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp) |
| **`docker`** | Docker Compose, Images & SQL Init Scripts | [Gitee](https://gitee.com/beijing_hongye_huicheng/docker) · [MySQL Scripts](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql) |
---
## 6. License
Lilishop is licensed under the [MIT](./LICENSE) License. Copyright © 2020-2026 **Beijing Hongye Huicheng Technology Co., Ltd.**

354
README.md
View File

@@ -1,241 +1,181 @@
## Lilishop B2B2C商城系统
<div align="center">
<img src="./manager/src/assets/logo.png" width="180" alt="Lilishop Logo" />
<h1>Lilishop(PickMall) 开源商城系统 - Web 前端 (Vue 3)</h1>
#### 欢迎交流需求,交流业务,交流技术(基础问题自行解决,其他问题先看文档后提问)
<p>
<a href="https://github.com/lilishop/lilishop"><img src="https://img.shields.io/github/stars/lilishop/lilishop.svg?style=social&logo=github" alt="GitHub Stars" /></a>
<a href="https://gitee.com/beijing_hongye_huicheng/lilishop"><img src="https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark" alt="Gitee Stars" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" /></a>
<a href="https://vuejs.org/"><img src="https://img.shields.io/badge/Vue-3.5-brightgreen.svg" alt="Vue 3" /></a>
<a href="https://element-plus.org/"><img src="https://img.shields.io/badge/Element%20Plus-2.x-409EFF.svg" alt="Element Plus" /></a>
<a href="https://pinia.vuejs.org/"><img src="https://img.shields.io/badge/Pinia-4.x-yellow.svg" alt="Pinia" /></a>
</p>
#### 不用削尖脑袋往老群里加,老群活跃度较低,很多潜水党,新群相对而言活跃一些 :tw-1f606: :tw-1f606: :tw-1f606: :tw-1f606: :tw-1f606: :tw-1f606:
<p>
<a href="https://pickmall.cn">🏠 官方网站</a> ·
<a href="https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home">📖 产品文档</a> ·
<a href="#2-在线演示">💻 在线演示</a> ·
<a href="#7-社区与支持">💬 微信交流群</a>
</p>
#### PS: **演示站点所有环境均部署master分支。如果有演示站点问题,可以反馈,如果演示站点没问题本地运行有问题,需自行处理**
<p>
<b>简体中文</b> | <a href="./README.en.md">English</a>
</p>
</div>
##### 交流 qq 1群 961316482(已满)
##### 交流 qq 2群 875294241(已满)
##### 交流 qq 3群 263785057(已满)
##### 交流 qq 4群 674617534 (已满)
##### 交流 qq 5群 594675235
> ⭐ **如果觉得项目不错,请点击右上角 `Star` 收藏支持!您的 Star 是我们持续开源迭代的最大动力!**
##### 体验 公众号/小程序/APP 体验,扫描二维码
**LILISHOP** 是基于 Spring Boot / Vue 3 / uni-app 开发的开源商城系统,支持 B2B2C 多商户商城、微信小程序商城、H5 商城、APP 商城、直播电商、分销返佣、秒杀活动。
![image-20210511171611793](https://static.pickmall.cn/images/h5-qrcode.png)
### ✨ 为什么选择 Lilishop
- 🟢 **真 MIT 协议**:支持免费商用与私有化闭源二开,无版权后顾之忧。
- 🚀 **Vue 3 全新栈**:全面基于 `Vue 3.5 + Pinia 4 + Vue Router 4 + Element Plus` 构建,告别老旧 Vue 2 架构。
- 🧩 **四端合一生产级闭环**:完整覆盖 **PC 买家端 (`buyer`)**、**商家端 (`seller`)**、**平台运营管理端 (`manager`)** 与 **在线客服端 (`im`)**,支持拖拽式可视化楼层装修与多维数据统计报表。
[![star](https://gitee.com/beijing_hongye_huicheng/lilishop/badge/star.svg?theme=dark)](https://gitee.com/beijing_hongye_huicheng/lilishop/stargazers)
&nbsp;&nbsp;![github](https://img.shields.io/github/stars/hongyehuicheng/lilishop.svg?style=social&logo=#181717)
---
## 如何在本地环境运行lilishop-ui部署视频
https://www.bilibili.com/video/BV1B28EeJEnP/
## 1. 项目简介
## 如何在服务器上部署lilishop-ui
https://www.bilibili.com/video/BV1WD87eoE9F/
**Lilishop** 是一款功能完善的 B2B2C 多商户商城系统,采用前后端分离架构,全端代码开源。
后端基于 **Spring Boot 4**(Java 21)构建,具备高内聚、低耦合的特性,支持分布式部署;前端覆盖 PC、管理后台、H5、小程序和 APP,基于 **Vue 3** 和 **uni-app** 开发。
本仓库(`lilishop-ui`)为 **Web 前端源码仓库**,包含 **PC 商城端、商家后台、平台管理后台、IM 在线客服** 四个独立子工程。
## 开发项目
- **官方网站**: <https://pickmall.cn>
- **官方文档**: [产品文档](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home)
- **Gitee 仓库**: <https://gitee.com/beijing_hongye_huicheng>
- **GitHub 主仓库**: <https://github.com/lilishop/lilishop>
#### 安装Node.js
保证`node`版本`14`,推荐 14.17.0
---
可以使用 `yarn` 或者 `npm` 进行安装
## 2. 在线演示
#### yarn 安装/启动
```
// 如果没有 yarn 安装yarn
npm install yarn -g
> **注意**: 演示站手机验证码统一为 `111111`。演示环境部署于 `master` 分支。
// 切换源
yarn config set registry https://registry.npmmirror.com
| 演示站点 | 访问地址 | 演示账号 |
| :--- | :--- | :--- |
| **平台运营管理端 (`manager`)** | <https://admin-b2b2c.pickmall.cn> | 账号:`admin` / 密码:`123456` |
| **商家店铺管理端 (`seller`)** | <https://store-b2b2c.pickmall.cn> | 账号:`13011111111` / 密码:`111111` |
| **商城 PC 买家端 (`buyer`)** | <https://pc-b2b2c.pickmall.cn> | 可使用手机号验证码 `111111` 登录 |
| **移动端 H5 (`uni-app`)** | <https://m-b2b2c.pickmall.cn> | 可使用手机号验证码 `111111` 登录 |
// 以buyer项目为例
cd buyer
| 微信小程序 | 移动端 H5([点击直接访问](https://m-b2b2c.pickmall.cn)) |
| :---: | :---: |
| <img src="https://static.pickmall.cn/images/h5-qrcode.png" width="180" alt="微信小程序体验码" /> | <img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/h5-qrcode.png" width="180" alt="移动端 H5 体验二维码" /> |
| 微信扫码体验小程序 | 手机扫码或访问 <https://m-b2b2c.pickmall.cn> |
yarn install
---
yarn dev
## 3. 核心特性与技术栈
### 核心特性
- **四端协同**: 涵盖平台管理端、商家管理端、PC 买家商城端、IM 客服端,角色边界清晰,开箱即用。
- **可视化拖拽装修**: 管理端与商家端内置可视化楼层编辑器(基于 `vuedraggable`),可同步配置 PC 端与移动端首页/专题页。
- **多商户入驻与结算**: 完整的商家入驻审核、商品审核、拆单发货、促销报名、财务对账与结算台账体系。
- **多维经营分析**: 集成 `@antv/g2` 图表引擎,提供营业概况、商品同比环比、会员分析、营销与分销数据统计。
- **即时通讯客服**: 集成腾讯云 IM / WebSocket 在线客服系统,支持买家、商家与平台实时沟通。
### Web 前端技术栈
| 类别 | 技术选型 | 说明 |
| :--- | :--- | :--- |
| 核心框架 | `Vue 3.5` + `Vue Router 4` | 基于 Composition API 的现代响应式前端框架 |
| 状态管理 | `Pinia 4` | 轻量、类型友好的 Vue 3 官方状态管理库 |
| UI 组件库 | `Element Plus 2.x` | 企业级中后台与桌面端 UI 组件库 |
| 网络请求 | `Axios 1.x` | 统一请求/响应拦截、Token 刷新与异常提示 |
| 数据可视化 | `@antv/g2` | 运营端与商家端数据统计图表渲染 |
| 可视化拖拽 | `vuedraggable 4` | 商城首页及专题楼层可视化拖拽装修 |
| 即时通讯 | `@tencentcloud/chat` / `sockjs-client` | 在线客服消息收发与实时通知 |
| 音视频播放 | `dplayer` + `hls.js` | 商品视频与直播流播放支持 |
| 构建与样式 | `Vue CLI 5` + `Webpack 5` + `Sass / Less` | 模块化工程构建与全局主题样式定制 |
---
## 4. 目录结构
```text
lilishop-ui
├── buyer/ # 【PC 买家商城端】源码工程
│ ├── public/ # 静态资源与全局配置文件 (config.js)
│ └── src/
│ ├── api/ # 买家端接口定义(商品、购物车、订单、会员、促销等)
│ ├── components/ # 公共业务组件(楼层渲染、导航栏、商品卡片等)
│ ├── pages/ # PC 商城各业务页面(首页、商品详情、结算、个人中心等)
│ ├── router/ # 路由配置与守卫
│ ├── stores/ # Pinia 状态管理
│ └── utils/ # 请求封装与通用工具函数
├── seller/ # 【商家店铺管理端】源码工程
│ ├── public/ # 静态资源与全局配置
│ └── src/
│ ├── api/ # 商家端接口定义
│ ├── components/ # 商家端公共组件与装修组件
│ ├── views/ # 商家端业务视图(商品管理、订单发货、售后、促销、店铺设置、统计等)
│ ├── router/ # 动态权限路由
│ └── stores/ # Pinia 状态管理
├── manager/ # 【平台运营管理端】源码工程
│ ├── public/ # 静态资源与全局配置
│ └── src/
│ ├── api/ # 运营端接口定义
│ ├── components/ # 平台公共组件、富文本、可视化楼层装修编辑器
│ ├── views/ # 运营端业务视图(会员、商品、订单、商家、营销、分销、财务、统计、系统设置)
│ ├── router/ # 菜单与权限路由
│ └── stores/ # Pinia 状态管理
├── im/ # 【IM 在线客服 Web 端】源码工程
│ └── src/
│ ├── api/ # 客服会话与消息接口
│ ├── components/ # 聊天窗口、消息列表、商品/订单卡片组件
│ ├── im-server/ # WebSocket / IM 连接服务管理
│ └── views/ # 客服工作台页面
├── build.sh # 多端统一构建脚本
└── docker-build.sh # Docker 镜像构建脚本
```
#### npm 安装/启动
```
npm config set registry https://registry.npmmirror.com
---
// 以buyer项目为例
cd buyer
## 5. 配套源码仓库
npm run install
本仓库专注于 **PC / 商家端 / 运营端 / IM Web**(`lilishop-ui`)。完整系统由以下四个核心仓库组成:
npm run dev
```
| 仓库名称 | 职责说明 | 仓库地址 |
| :--- | :--- | :--- |
| **`lilishop`** | 后端服务源码(Spring Boot 4 / Java 21) | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop) · [GitHub](https://github.com/lilishop/lilishop) |
| **`lilishop-ui`** | PC 商城 / 商家端 / 运营管理端 / IM Web 前端 | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-ui) |
| **`lilishop-uniapp`** | 买家端 H5 / 微信小程序 / Android & iOS APP | [Gitee](https://gitee.com/beijing_hongye_huicheng/lilishop-uniapp) |
| **`docker`** | 容器化部署编排、镜像与数据库脚本 | [Gitee](https://gitee.com/beijing_hongye_huicheng/docker) · [MySQL 脚本](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql) |
#### FAQ
---
##### npmmirror镜像源报错 451 Unavailable For Legal Reasons
```
//切换其他的镜像源
## 6. 开源与授权
// npm
npm config set registry http://mirrors.cloud.tencent.com/npm/
本项目遵循 [MIT](./LICENSE) 开源协议,版权归 **北京宏业汇成科技有限公司** 所有。允许个人及企业免费用于学习、研究、修改和商业用途(需保留原始版权和许可声明)。
// yarn
yarn config set registry http://mirrors.cloud.tencent.com/npm/
```
---
##### 安装不上去
## 7. 社区与支持
可以按照这个思路排查一下
我们欢迎任何形式的交流与贡献。在提问前,请先查阅 [产品文档](https://bdwx2tfwwt.feishu.cn/wiki/space/7675179368632617952?ccm_open_type=lark_wiki_spaceLink&open_tab_from=wiki_home),并参考 [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md) 以便高效沟通。
```
1. 看下Node.js 是否是14.17.0
2. 项目目录下是否有 package-lock.json 或者 .npmrc 或者 .yarnrc 设置了镜像源, 如果有可以删除重新install
3. 切换当前 npm 或者 yarn 镜像源重试
```
- **[在线客服](https://work.weixin.qq.com/kfid/kfc4d8dc24a73c15f44)**
- **微信交流群**:
<img src="https://lilishop-wechat.oss-cn-beijing.aliyuncs.com/wechat.jpg" width="200" alt="微信群" />
---
### 商城介绍
**官网**:https://pickmall.cn
## 8. 升级计划
Lilishop商城系统支持商家入驻,后端基于SpringBoot 研发,前端使用 Vue、uniapp开发, **系统全端全部代码开源**
### ✅ 已完成特性
| 功能模块 | 功能模块 |
| :--- | :--- |
| ✅ 商品定时上下架 | ✅ 商品分组 / 会员分组 |
| ✅ 商品虚拟销量 | ✅ 专用发票(发票上传) |
| ✅ 置顶评论 | ✅ 客户等级体系 |
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
| ✅ 卡密商品 | ✅ 二级分销 |
| ✅ 定时任务管理 | ✅ 预约服务商品 |
| ✅ 主题色管理 | ✅ 商家发货改造(整单/拆单/无需配送) |
| ✅ 多维统计(营业/商品/会员/积分/储值/营销/分销) | ✅ 经营报表(店铺业绩/商品同比环比/订单明细/销售汇总) |
前后端分离,支持分布式部署,支持Docker,各个API独立,并且有独立的消费者。
### 商城 API/消费者 聚合版
api不需要单独部署,只需启动一个jar包就可以正常运转 如有需要,可以点击跳转
https://gitee.com/beijing_hongye_huicheng/lilishop-simplify
### 开发/使用/常见问题 帮助文档
https://docs.pickmall.cn
### 项目地址
gitee : https://gitee.com/beijing_hongye_huicheng
github 镜像: https://github.com/lilishop?tab=repositories
商城UI 项目下3个文件夹
buyer:买家PC端,seller:商家端,manager:后台管理端
### 演示地址
PS:手机验证码为 ‘111111’
**平台管理端**:https://admin-b2b2c.pickmall.cn 账号:admin/123456
**店铺管理端**:https://store-b2b2c.pickmall.cn 账号:13011111111/111111
**商城PC页面**:https://pc-b2b2c.pickmall.cn
**商城 小程序/公众号/APP**:扫描二维码
![image-20210511171611793](https://static.pickmall.cn/images/h5-qrcode.png)
### 快速本地部署
[点击跳转](https://docs.pickmall.cn/deploy/%E8%BF%90%E8%A1%8C%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87.html)
**商城数据库**
使用docker-compose部署数据库,自动初始化数据库,不需要手动下载等操作
如果手动部署,才需要获取sql [点击跳转](https://gitee.com/beijing_hongye_huicheng/docker/tree/master/init/mysql) (PS:这里有与tag版本一致的sql,如果是历史版本,则docker项目也切换至历史版本获取sql即可,历史版本升级则根据java相聚的根目录DB目录下的升级sql,按需执行)。
### 功能列表
#### 平台管理端功能
![平台管理端功能](https://static.pickmall.cn/images/other/managerList1.jpg)
#### 卖家功能
![商家端功能](https://static.pickmall.cn/images/other/storeList.jpg)
### 商城前端功能展示
#### 商城移动端
<img src="https://static.pickmall.cn/images/other/app.gif" alt="移动端功能展示" style="zoom:50%;" />
#### 平台管理端
![管理端功能展示](https://static.pickmall.cn/images/other/manager.gif)
### 技术选型
#### 架构图
![技术选型](https://lili-system.oss-cn-beijing.aliyuncs.com/docs/%E6%9E%B6%E6%9E%84.png)
##### 后台技术选型
| 说明 | 框架 | 说明 | |
| -------------- | --------------- | -------------- | ------------- |
| 基础框架 | Spring Boot | MVC框架 | Spring MVC |
| 持久框架 | Mybatis-Plus | 程序构建 | Maven |
| 关系型数据库 | MySQL | 消息中间件AMQP | RocketMQ |
| 缓存 | Redis +MongoDB | 搜索引擎 | Elasticsearch |
| 安全框架 | Spring Security | 数据库连接池 | Druid |
| 数据库分库分表 | sharding | 定时任务 | xxl-job |
| 负载均衡 | Nginx | 静态资源 | 阿里云OSS |
| 短信 | 阿里云短信 | 认证 | JWT |
| 日志处理 | Log4j | 接口规范 | RESTful |
##### 前端-运营后台、店铺后台
| 说明 | 框架 | 说明 | 框架 |
| ---------- | ---------- | ---------- | ------- |
| 构建工具 | webpack | JS版本 | ES6 |
| 基础JS框架 | Vue.js | 视频播放器 | Dplayer |
| 路由管理 | Vue Router | 状态管理 | Vuex |
| 基础UI库 | iView | UI界面基于 | iView |
| 网络请求 | axios | | |
##### 前端-移动端
| 说明 | 架构 | 说明 | 架构 |
| --------- | ------- | -------- | ------- |
| 基础UI库 | uViewui | 基础框架 | uni-app |
| CSS预处理 | scss | 地图引擎 | amap |
### 版本升级
```
系统后续会提供多场景解决方案。
更多架构:微服务、Saas、中台等,都会支持。 支持差价升级商业授权
```
### 商业授权
商业版本与开源版本代码一致,没有区分
商业使用需要授权,授权方式可选择联系官网客服,或者qq群联系群主。
商业授权模式为永久授权,支持永久升级。
商业案例由于涉及部分多层二开关系,如需了解可以咨询销售。
### 开源须知
1.仅允许用于个人学习研究使用.
2.禁止将本开源的代码和资源进行任何形式任何名义的出售.
3.软件受国家计算机软件著作权保护(登记号:2021SR0805085)。
4.限制商用,如果需要商业使用请联系我们。QQ3409056806.或者加入qq群联系群主。
### 交流群
##### 交流 qq 1群 961316482(已满)
##### 交流 qq 2群 875294241(已满)
##### 交流 qq 3群 263785057(已满)
##### 交流 qq 4群 674617534(已满)
##### 交流 qq 5群 594675235
### 附录
有人有自己的学习视频、学习记录文档、希望宣传关联开源项目等均可以私聊仓库所有者。
类似:
清晨敲代码同学的分析: https://blog.csdn.net/vaevaevae233/category_12103567.html
### 🔜 近期规划中
- 🔜 店铺结算汇总 & 结算台账
- 🔜 支付方式汇总 & 钱包流水 & 财务汇总
- 🔜 微信小店对接
- 🔜 抖店对接

View File

@@ -1,12 +0,0 @@
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"]
}

View File

@@ -1,4 +0,0 @@
/build/
/config/
/dist/
/*.js

View File

@@ -1,33 +0,0 @@
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
parser: 'babel-eslint'
},
env: {
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
],
// required to lint *.vue files
plugins: [
'vue'
],
// add your custom rules here
rules: {
"semi": [0, "never"],
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
"vue/no-parsing-error": [2, {
"x-invalid-end-tag": false
}]
}
}

1
buyer/.npmrc Normal file
View File

@@ -0,0 +1 @@
engine-strict=false

1
buyer/.yarnrc Normal file
View File

@@ -0,0 +1 @@
--ignore-engines true

55
buyer/MIGRATION-VUE3.md Normal file
View File

@@ -0,0 +1,55 @@
# Buyer 买家端 Vue 3 迁移说明
## 技术栈
- Vue 3 + Vue Router 4 + Pinia(vuex 已移除,见下文"组合式 API 迁移")
- Element Plus(替代 view-design / iView)
- 保留 `$Message` / `$Modal` / `$Notice` 兼容层(`src/utils/message.js`)
## 启动
```bash
cd buyer
yarn install
yarn dev # 默认端口 10000
yarn build
```
## 迁移要点
1. **入口** `src/main.js` 使用 `createApp`,全局过滤器改为 `$filters`
2. **路由** `src/router/index.js` 使用 `createRouter` + 动态 `import()`
3. **请求** `src/plugins/request.js` 使用 Element Plus `ElLoading`
4. **iView 组件已全部替换**:轮播 `el-carousel`、下拉 `el-dropdown`、侧栏 `el-menu`、头像 `el-avatar` 等
5. **图标映射** `src/utils/iconMap.js` — 动态图标(如侧边栏菜单)通过 `resolveIcon()` 使用
6. **自定义字体图标** `icomoon` 保留为 `<i class="icomoon icon-*">`(购物车步骤条、钱包等)
7. **首页装修** `indexDecorate/modelList` 保留买家端展示逻辑(非 manager 编辑版)
8. **特殊替换**
- `vue-piczoom` → `el-image` 预览
- `mv-count-down` → 内置倒计时
- 秒杀轮播 → 横向滚动列表
## 组合式 API 迁移(2026-10 完成)
全部 95 个 `.vue` 组件已从 Options API 转换为 `<script setup>`,同时完成 vuex → pinia:
- **状态管理**:`src/stores/app.js`(`useAppStore`:navList / cartNum / unreadMsg / logoImg / siteName / hotWordsList / category + 对应 setXxx actions)。原 `src/vuex/` 已删除,package.json 不再依赖 vuex。
- **路由守卫**:`beforeRouteEnter` 无组合式等价物,5 个页面(`GoodsList` / `GoodsDetail` / `PointGoodsDetail` / `Cart` / `ShoppingCart`)保留普通 `<script>` 块存放 `export default { name, beforeRouteEnter }`——这是官方支持的合并写法,统计 Options API 残留时需排除。
- **defineExpose**:以下组件的方法/状态被父组件通过模板 ref 调用,已显式暴露:`verify`(init / close)、`multiple-map`(open)、`addressManage`(show / hide)、`invoiceModal`(invoiceAvailable)。新增此类父子调用时必须加 `defineExpose`。
- **模板 ref 与响应式变量同名会互相覆盖**:同名时模板 ref 统一改名 `xxxRef`(如 `formRef` / `verifyRef`)。
- **mixin 已消除**:原 `components/mixes/talkIm.vue` 的 IM 客服逻辑已内联进 `Merchant.vue` 和 `goodsDetail/StoreSide.vue`,文件已删除。
- **全局属性照旧**:模板中的 `$filters.*` / `linkTo` / `connectCs` / `$inputMaxLength` 是 `main.js` 注册的全局属性,`<script setup>` 下模板可直接使用;script 内使用 `$filters` 函数需从 `@/plugins/filters.js` 具名导入,`this.Cookies` 等同 `storage`(`@/plugins/storage`)。
- **其余三端(seller / manager / im)仍为 Options API + vuex**,迁移时参照本端模式。
## 批量脚本
- `scripts/migrate-iview-to-element.js` — iView → Element 模板替换
- `scripts/fix-filters.js` — Vue 2 过滤器语法修复
- `scripts/replace-page.js` — `<Page>` → `el-pagination`
- `scripts/replace-icon.js` — 静态 `<Icon type>` → `el-icon`
- `scripts/fix-el-icon-click.js` — 修复 `@click` 绑定
- `scripts/fix-broken-tags.js` — 闭合标签修复
## 上线前检查
四端统一清单(认证、axios、Element Plus、分端专项):[../MIGRATION-VUE3.md](../MIGRATION-VUE3.md)

View File

@@ -1,19 +0,0 @@
# new
## Project setup
```
npm install
```
### Compiles and hot-reloads for development
```
npm run serve
```
### Compiles and minifies for production
```
npm run build
```
### Customize configuration
See [Configuration Reference](https://cli.vuejs.org/config/).

36
buyer/eslint.config.js Normal file
View File

@@ -0,0 +1,36 @@
const globals = require('globals')
const pluginVue = require('eslint-plugin-vue')
module.exports = [
{
ignores: [
'dist/**',
'node_modules/**',
'build/**',
'config/**',
'src/scripts/**', // 一次性迁移脚本,不参与 lint
],
},
...pluginVue.configs['flat/essential'],
{
files: ['src/**/*.{js,vue}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
globals: {
...globals.browser,
},
},
rules: {
'no-console': ['warn', { allow: ['warn', 'error'] }],
'no-empty': ['error', { allowEmptyCatch: false }],
'vue/no-parsing-error': ['error', { 'x-invalid-end-tag': false }],
// 页面组件通过路由按文件路径引用,组件名不参与解析,存量单词组件名不再强制多词
'vue/multi-word-component-names': 'off',
// 存量代码大量使用与 HTML 保留名同名的组件(element-plus 图标 Search/View/Camera/Link 及全局注册的 empty/drawer 等),重命名成本高,暂不启用
'vue/no-reserved-component-names': 'off',
// 全端已完成 Options API → <script setup> 迁移,防止新代码回退(路由守卫的普通 script 块等有意保留的除外,故用 warn)
'vue/component-api-style': ['warn', ['script-setup']],
},
},
]

View File

@@ -3,42 +3,54 @@
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "vue-cli-service serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
"build": "vue-cli-service build",
"dev": "vue-cli-service serve",
"lint": "eslint src",
"lint:fix": "eslint src --fix"
},
"engines": {
"node": ">=16"
},
"dependencies": {
"@amap/amap-jsapi-loader": "0.0.7",
"axios": "^0.19.2",
"@element-plus/icons-vue": "^2.3.2",
"axios": "^1.7.9",
"core-js": "^3.49.0",
"dplayer": "^1.27.1",
"element-plus": "^2.14.1",
"js-cookie": "^2.2.1",
"less": "^2.7.0",
"less-loader": "^5.0.0",
"mv-count-down": "^0.1.15",
"node-sass": "^4.14.1",
"postcss-loader": "^7.0.1",
"nprogress": "^0.2.0",
"pinia": "^4.0.3",
"psl": "^1.8.0",
"qs": "^6.9.4",
"sass-loader": "^7.3.1",
"sass": "^1.63.6",
"sass-loader": "^10.4.1",
"uuid": "^8.3.2",
"view-design": "^4.3.2",
"vue": "^2.6.11",
"vue-awesome-swiper": "^3.1.3",
"vue-piczoom": "^1.0.6",
"vue-qr": "^2.3.0",
"vue-router": "^3.0.1",
"vuex": "^3.0.1"
"vue": "^3.5.35",
"vue-qr": "^5.0.3",
"vue-router": "^4.6.4"
},
"devDependencies": {
"@vue/cli-service": "~4.5.0",
"compression-webpack-plugin": "^5.0.0",
"sass-loader": "^7.3.1",
"uglifyjs-webpack-plugin": "^2.2.0",
"vue-template-compiler": "^2.6.11"
"@vue/cli-plugin-babel": "^5.0.9",
"@vue/cli-service": "^5.0.9",
"@vue/compiler-sfc": "^3.5.35",
"compression-webpack-plugin": "^10.0.0",
"eslint": "^9.39.4",
"eslint-plugin-vue": "^10.11.1",
"globals": "^17.13.0",
"less": "^4.2.0",
"less-loader": "^11.1.4",
"vue-eslint-parser": "^10.4.1",
"webpack": "^5.95.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
],
"resolutions": {
"minimatch": "^3.1.2",
"@achrinza/node-ipc": "9.2.2"
}
}

8658
buyer/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,15 +3,9 @@ var BASE = {
* @description api请求基础路径
*/
API_DEV: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn"
},
API_PROD: {
common: "https://common-api.pickmall.cn",
buyer: "https://buyer-api.pickmall.cn",
seller: "https://store-api.pickmall.cn",
manager: "https://admin-api.pickmall.cn"
},
};

View File

@@ -7,20 +7,12 @@
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>logo.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
<% for(var css of htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%=css%>" />
<% } %>
</head>
<body>
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>"></script>
<% } %>
<script src="/config.js"></script>
<script src="<%= BASE_URL %>config.js"></script>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..");
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function fix(content) {
let c = content;
c = c.replace(/<\/Button>/g, "</el-button>");
c = c.replace(/<\/Button\b/g, "</el-button");
c = c.replace(/<\/Input>/g, "</el-input>");
c = c.replace(/<\/Input\b/g, "</el-input");
c = c.replace(/<\/Option>/g, "</el-option>");
c = c.replace(/<\/Option\b/g, "</el-option");
c = c.replace(/<\/Checkbox>/g, "</el-checkbox>");
c = c.replace(/<\/Checkbox\b/g, "</el-checkbox");
c = c.replace(/<\/Select>/g, "</el-select>");
c = c.replace(/<\/Select\b/g, "</el-select");
c = c.replace(/<\/Form>/g, "</el-form>");
c = c.replace(/<\/Modal>/g, "</el-dialog>");
c = c.replace(/@click\.native/g, "@click");
c = c.replace(/@on-search/g, "@keyup.enter");
c = c.replace(/\bsearch\b(?=\s+enter-button)/g, "");
c = c.replace(/\benter-button\b/g, "");
c = c.replace(/<BreadcrumbItem/g, "<el-breadcrumb-item");
c = c.replace(/<\/BreadcrumbItem>/g, "</el-breadcrumb-item>");
c = c.replace(/<Breadcrumb/g, "<el-breadcrumb");
c = c.replace(/<\/Breadcrumb>/g, "</el-breadcrumb>");
c = c.replace(/type="error"/g, 'type="danger"');
c = c.replace(/<i-col/g, "<div");
c = c.replace(/<\/i-col>/g, "</div>");
c = c.replace(/\{\{([^}|]+)\s*\|\s*secrecyMobile\s*\}\}/g, "{{ $filters.secrecyMobile($1) }}");
return c;
}
let count = 0;
for (const file of walkDir(path.join(ROOT, "src"))) {
const original = fs.readFileSync(file, "utf8");
const fixed = fix(original);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
}
}
console.log(`Fixed tags in ${count} files`);

View File

@@ -0,0 +1,52 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..");
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function fix(content) {
let c = content;
c = c.replace(/<p #header>/g, "<template #header><p>");
c = c.replace(
/(<template #header><p>[\s\S]*?<\/p>)(\s*<div)/g,
"$1</template>$2"
);
c = c.replace(
/<template #footer><div([^>]*)>([\s\S]*?)<\/div><\/template><\/el-dialog>/g,
"<template #footer><div$1>$2</div></template>\n </el-dialog>"
);
c = c.replace(
/<template #default\s*>([\s\S]*?)<\/template\s*>/g,
(match, inner) => {
if (match.includes("el-alert")) {
return `<template #title>${inner}</template>`;
}
return match;
}
);
return c;
}
let count = 0;
for (const file of walkDir(path.join(ROOT, "src"))) {
const original = fs.readFileSync(file, "utf8");
const fixed = fix(original);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
}
}
console.log(`Fixed dialog slots in ${count} files`);

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "src");
function walk(dir, files = []) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const f = path.join(dir, e.name);
if (e.isDirectory()) walk(f, files);
else if (e.name.endsWith(".vue")) files.push(f);
}
return files;
}
let n = 0;
for (const file of walk(ROOT)) {
let c = fs.readFileSync(file, "utf8");
const next = c.replace(/:size="(\d+)"\s+:size="\1"/g, ':size="$1"');
if (next !== c) {
fs.writeFileSync(file, next);
n++;
console.log(path.relative(ROOT, file));
}
}
console.log(`Fixed duplicate :size in ${n} files`);

View File

@@ -0,0 +1,28 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "src");
function walk(dir, files = []) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const f = path.join(dir, e.name);
if (e.isDirectory()) walk(f, files);
else if (e.name.endsWith(".vue")) files.push(f);
}
return files;
}
let n = 0;
for (const file of walk(ROOT)) {
let c = fs.readFileSync(file, "utf8");
const next = c
.replace(/<el-icon([^>]*?):click=/g, "<el-icon$1@click=")
.replace(/:class="refresh"/g, 'class="refresh"')
.replace(/:size="20" @click="show = false" :size="20"/g, ':size="20" @click="show = false"');
if (next !== c) {
fs.writeFileSync(file, next);
n++;
}
}
console.log(`Fixed el-icon click in ${n} files`);

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..");
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function fixFilters(content) {
let c = content;
// Fix broken: {{ expr {{ $filters.unitPrice(args) }}
c = c.replace(
/\{\{\s*([^}{]+?)\s*\{\{\s*\$filters\.unitPrice\(([^)]*)\)\s*\}\}/g,
"{{ $filters.unitPrice($1, $2) }}"
);
// Fix: {{ expr | unitPrice }} or {{ expr | unitPrice('¥') }}
c = c.replace(
/\{\{\s*([^}|]+?)\s*\|\s*unitPrice(?:\(([^)]*)\))?\s*\}\}/g,
(_, expr, args) => {
const e = expr.trim();
return args !== undefined
? `{{ $filters.unitPrice(${e}, ${args}) }}`
: `{{ $filters.unitPrice(${e}) }}`;
}
);
// Fix inline without braces: expr | unitPrice("¥")
c = c.replace(
/(?<!\{\{)\s*([a-zA-Z0-9_.?()[\]'"\s]+?)\s*\|\s*unitPrice\(([^)]*)\)/g,
(match, expr, args) => {
if (match.includes("{{")) return match;
return `{{ $filters.unitPrice(${expr.trim()}, ${args}) }}`;
}
);
// Fix inline without args: expr | unitPrice
c = c.replace(
/(?<!\{\{)\s*([a-zA-Z0-9_.?()[\]'"\s]+?)\s*\|\s*unitPrice(?!\()/g,
(match, expr) => {
if (match.includes("{{") || match.includes("$filters")) return match;
return `{{ $filters.unitPrice(${expr.trim()}) }}`;
}
);
// Fix unixToDate filter
c = c.replace(
/\{\{\s*([^}|]+?)\s*\|\s*unixToDate(?:\(([^)]*)\))?\s*\}\}/g,
(_, expr, args) => {
const e = expr.trim();
return args !== undefined
? `{{ $filters.unixToDate(${e}, ${args}) }}`
: `{{ $filters.unixToDate(${e}) }}`;
}
);
return c;
}
let count = 0;
for (const file of walkDir(path.join(ROOT, "src"))) {
const original = fs.readFileSync(file, "utf8");
const fixed = fixFilters(original);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
}
}
console.log(`Fixed filters in ${count} files`);

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..");
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function fix(content) {
let c = content;
c = c.replace(/<el-pagination\b/g, "<Page");
c = c.replace(/<\/el-pagination>/g, "</Page>");
c = c.replace(
/<Icon([^>]*)\s+#prepend\s*>\s*<\/Icon>/g,
"<template #prepend><Icon$1></Icon></template>"
);
c = c.replace(
/<Icon([^>]*)\s+#prepend\s*>\s*<\/Icon>/g,
"<template #prepend><Icon$1></Icon></template>"
);
c = c.replace(
/<template #prepend><Icon([^>]*)\s*\/>/g,
"<template #prepend><Icon$1></Icon></template>"
);
c = c.replace(/@keyup\.enter\.native/g, "@keyup.enter");
c = c.replace(/slot="append"/g, "#append");
c = c.replace(/(<div[^>]*)\s+#append/g, "<template #append>$1");
c = c.replace(/(<el-button[^>]*)\slong/g, '$1 style="width:100%"');
c = c.replace(/@on-select/g, "@select");
c = c.replace(/@on-click/g, "@click");
return c;
}
let count = 0;
for (const file of walkDir(path.join(ROOT, "src"))) {
const original = fs.readFileSync(file, "utf8");
const fixed = fix(original);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
}
}
console.log(`Runtime compat fixes in ${count} files`);

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..");
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function fix(content) {
let c = content;
c = c.replace(
/<template #default="(\{[^"]+\})" #([a-zA-Z0-9_-]+)>/g,
'<template #$2="$1">'
);
c = c.replace(
/<div([^>]*) #footer([^>]*)>/g,
'<template #footer><div$1$2>'
);
c = c.replace(/<\/div>\s*(?=<\/el-dialog>)/g, "</div></template>");
c = c.replace(/<span #append>/g, "<template #append><span>");
c = c.replace(/<\/span><\/el-input/g, "</span></template></el-input");
c = c.replace(/><span #append>/g, "><template #append><span>");
c = c.replace(/<el-button #append/g, "<template #append><el-button");
c = c.replace(
/<\/el-button>(\s*<\/el-input>)/g,
"</el-button></template>$1"
);
c = c.replace(/<template #desc\b/g, "<template #default");
return c;
}
let count = 0;
for (const file of walkDir(path.join(ROOT, "src"))) {
const original = fs.readFileSync(file, "utf8");
const fixed = fix(original);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
}
}
console.log(`Fixed slots in ${count} files`);

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..");
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function fix(content) {
let c = content;
c = c.replace(/<\/span\s*\n\s*>/g, "</span>");
c = c.replace(/<\/span\s+>/g, "</span>");
c = c.replace(/<i-input/g, "<el-input");
c = c.replace(/<\/i-input>/g, "</el-input>");
c = c.replace(
/<Icon([^>]*)\s+#prepend\s*\/>/g,
"<template #prepend><Icon$1 /></template>"
);
c = c.replace(
/<template #append><el-button([^>]*)>([\s\S]*?)<\/el-button>\s*<\/i-input>/g,
"<template #append><el-button$1>$2</el-button></template></el-input>"
);
c = c.replace(
/<template #append><el-button([^>]*)>([\s\S]*?)<\/el-button>\s*<\/el-input>/g,
"<template #append><el-button$1>$2</el-button></template></el-input>"
);
return c;
}
let count = 0;
for (const file of walkDir(path.join(ROOT, "src"))) {
const original = fs.readFileSync(file, "utf8");
const fixed = fix(original);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
}
}
console.log(`Fixed span/input tags in ${count} files`);

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..");
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function fix(content) {
let c = content;
c = c.replace(
/(<div[^>]*)\s+#content>/g,
"<template #content><div$1>"
);
c = c.replace(
/(<\/el-tooltip>)/g,
(match, _m, offset, str) => {
return match;
}
);
c = c.replace(
/(<div style="white-space:[^"]*"[^>]*>[\s\S]*?<\/div>)(\s*<\/el-tooltip>)/g,
"$1</template>$2"
);
c = c.replace(/<\/div><\/template><\/el-dialog>/g, (m, offset, str) => {
const before = str.slice(Math.max(0, offset - 200), offset);
if (before.includes("#footer")) return m;
return "</div></el-dialog>";
});
c = c.replace(/<i(\s[^>]*)\/>/g, "<i$1></i>");
return c;
}
let count = 0;
for (const file of walkDir(path.join(ROOT, "src"))) {
const original = fs.readFileSync(file, "utf8");
const fixed = fix(original);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
}
}
console.log(`Fixed stray templates in ${count} files`);

View File

@@ -0,0 +1,227 @@
#!/usr/bin/env node
/**
* Migrate buyer Vue files from iView to Element Plus.
* Usage: node scripts/migrate-iview-to-element.js
*/
const fs = require("fs");
const path = require("path");
const BUYER_ROOT = path.join(__dirname, "..");
const MANAGER_ROOT = path.join(__dirname, "../../manager");
const MODEL_LIST = [
["Carousel.vue", "carousel.vue"],
["Carousel1.vue", "carousel1.vue"],
["Carousel2.vue", "carousel2.vue"],
["FirstPageAdvert.vue", "firstPageAdvert.vue"],
["NewGoodsSort.vue", "newGoodsSort.vue"],
["NotEnough.vue", "notEnough.vue"],
["Recommend.vue", "recommend.vue"],
["Seckill.vue", "seckill.vue"],
["forYour.vue", "forYour.vue"],
["goodsAndType.vue", "goodsAndType.vue"],
["mixModel.vue", "mixModel.vue"],
["oneRowThreeColumns.vue", "oneRowThreeColumns.vue"],
["onlyGoodsModel.vue", "onlyGoodsModel.vue"],
["mixs/mix-brand.vue", "mixs/mix-brand.vue"],
["mixs/mix-goods.vue", "mixs/mix-goods.vue"],
];
const DECORATE_COPY_MAP = [
[
"src/components/indexDecorate/ModelForm.vue",
"src/views/page-decoration/modelForm.vue",
],
[
"src/components/indexDecorate/ModelFormItem.vue",
"src/views/page-decoration/modelFormItem.vue",
],
];
MODEL_LIST.forEach(([buyerName, managerName]) => {
DECORATE_COPY_MAP.push([
`src/components/indexDecorate/modelList/${buyerName}`,
`src/views/page-decoration/modelList/${managerName}`,
]);
});
const TARGET_DIRS = ["src/pages", "src/components"];
function adaptBuyerContent(content) {
return content
.replace(/@\/views\/page-decoration\//g, "@/components/indexDecorate/")
.replace(/admin-setting/g, "buyer-setting")
.replace(/adminPCPageCache/g, "buyerPCPageCache")
.replace(/getStore\('adminPCPageCache'\)/g, "getStore('buyerPCPageCache')")
.replace(/setStore\('adminPCPageCache'/g, "setStore('buyerPCPageCache'");
}
function applyScriptTransforms(content) {
let c = content;
c = c.replace(/\$options\.filters\./g, "$filters.");
c = c.replace(/\|\s*unitPrice\(/g, "{{ $filters.unitPrice(");
c = c.replace(/\|\s*unixToDate\(/g, "{{ $filters.unixToDate(");
c = c.replace(/\bbeforeDestroy\b/g, "beforeUnmount");
c = c.replace(/this\.\$set\(([^,]+),\s*([^,]+),\s*([^)]+)\)/g, "$1[$2] = $3");
c = c.replace(/::v-deep/g, ":deep");
c = c.replace(/\/deep\//g, ":deep");
c = c.replace(/@on-change/g, "@change");
c = c.replace(/@on-click/g, "@click");
c = c.replace(/@on-ok/g, "@ok");
c = c.replace(/@on-cancel/g, "@cancel");
c = c.replace(/@on-clear/g, "@clear");
c = c.replace(/@on-open-change/g, "@visible-change");
c = c.replace(/@on-page-size-change/g, "@size-change");
c = c.replace(/@on-select/g, "@select");
c = c.replace(/@keydown\.enter\.native/g, "@keyup.enter");
c = c.replace(/slot-scope="([^"]+)"/g, '#default="$1"');
c = c.replace(/slot="([^"]+)"/g, '#$1');
c = c.replace(/from ['"]view-design['"]/g, 'from "@/utils/message"');
c = c.replace(/import\s*\{([^}]*)\}\s*from\s*['"]view-design['"]/g, 'import {$1} from "@/utils/message"');
return c;
}
function applyTemplateTransforms(content) {
let c = content;
const replacements = [
[/<\/?Card>/g, (m) => m.replace("Card", "el-card")],
[/<Form-item/g, "<el-form-item"],
[/<\/Form-item>/g, "</el-form-item>"],
[/<FormItem/g, "<el-form-item"],
[/<\/FormItem>/g, "</el-form-item>"],
[/<Form(\s|>)/g, "<el-form$1"],
[/<\/Form>/g, "</el-form>"],
[/<InputNumber/g, "<el-input-number"],
[/<\/InputNumber>/g, "</el-input-number>"],
[/<Input(\s|>)/g, "<el-input$1"],
[/<\/Input>/g, "</el-input>"],
[/<Button(\s|>)/g, "<el-button$1"],
[/<\/Button>/g, "</el-button>"],
[/<Select(\s|>)/g, "<el-select$1"],
[/<\/Select>/g, "</el-select>"],
[/<Option(\s)/g, "<el-option "],
[/<\/Option>/g, "</el-option>"],
[/<DatePicker/g, "<el-date-picker"],
[/<\/DatePicker>/g, "</el-date-picker>"],
[/<TimePicker/g, "<el-time-picker"],
[/<\/TimePicker>/g, "</el-time-picker>"],
[/<Modal/g, "<el-dialog"],
[/<\/Modal>/g, "</el-dialog>"],
[/<Page(\s)/g, "<el-pagination "],
[/<\/Page>/g, "</el-pagination>"],
[/<Tabs(\s|>)/g, "<el-tabs$1"],
[/<\/Tabs>/g, "</el-tabs>"],
[/<TabPane/g, "<el-tab-pane"],
[/<\/TabPane>/g, "</el-tab-pane>"],
[/<Row(\s|>)/g, "<div$1"],
[/<\/Row>/g, "</div>"],
[/<Col(\s|>)/g, "<div$1"],
[/<\/Col>/g, "</div>"],
[/<Tag(\s|>)/g, "<el-tag$1"],
[/<\/Tag>/g, "</el-tag>"],
[/<Switch(\s|>)/g, "<el-switch$1"],
[/<\/Switch>/g, "</el-switch>"],
[/<RadioGroup/g, "<el-radio-group"],
[/<\/RadioGroup>/g, "</el-radio-group>"],
[/<Radio(\s)/g, "<el-radio "],
[/<\/Radio>/g, "</el-radio>"],
[/<CheckboxGroup/g, "<el-checkbox-group"],
[/<\/CheckboxGroup>/g, "</el-checkbox-group>"],
[/<Checkbox/g, "<el-checkbox"],
[/<\/Checkbox>/g, "</el-checkbox>"],
[/<Poptip/g, "<el-popover"],
[/<\/Poptip>/g, "</el-popover>"],
[/<Tooltip/g, "<el-tooltip"],
[/<\/Tooltip>/g, "</el-tooltip>"],
[/<Drawer/g, "<el-drawer"],
[/<\/Drawer>/g, "</el-drawer>"],
[/<Alert/g, "<el-alert"],
[/<\/Alert>/g, "</el-alert>"],
[/<Badge/g, "<el-badge"],
[/<\/Badge>/g, "</el-badge>"],
[/<Divider/g, "<el-divider"],
[/<\/Divider>/g, "</el-divider>"],
[/<Rate/g, "<el-rate"],
[/<\/Rate>/g, "</el-rate>"],
[/<Steps/g, "<el-steps"],
[/<\/Steps>/g, "</el-steps>"],
[/<Step(\s)/g, "<el-step "],
[/<\/Step>/g, "</el-step>"],
[/<Upload/g, "<el-upload"],
[/<\/Upload>/g, "</el-upload>"],
[/<Cascader/g, "<el-cascader"],
[/<\/Cascader>/g, "</el-cascader>"],
[/<Affix/g, "<el-affix"],
[/<\/Affix>/g, "</el-affix>"],
[/<Spin/g, "<el-skeleton"],
[/<\/Spin>/g, "</el-skeleton>"],
[/:label-width="(\d+)"/g, 'label-width="$1px"'],
[/:mask-closable="false"/g, ':close-on-click-modal="false"'],
[/:width="(\d+)"/g, 'width="$1"'],
[/type="primary"/g, 'type="primary"'],
];
for (const [pattern, replacement] of replacements) {
c = c.replace(pattern, replacement);
}
return c;
}
function walkDir(dir, files = []) {
if (!fs.existsSync(dir)) return files;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) {
walkDir(full, files);
} else if (entry.name.endsWith(".vue")) {
files.push(full);
}
}
return files;
}
const copied = [];
const transformed = [];
const skipped = [];
for (const [buyerRel, managerRel] of DECORATE_COPY_MAP) {
const managerPath = path.join(MANAGER_ROOT, managerRel);
const buyerPath = path.join(BUYER_ROOT, buyerRel);
if (!fs.existsSync(managerPath)) {
skipped.push({ file: buyerRel, reason: "manager counterpart missing" });
continue;
}
let content = fs.readFileSync(managerPath, "utf8");
content = adaptBuyerContent(content);
fs.mkdirSync(path.dirname(buyerPath), { recursive: true });
fs.writeFileSync(buyerPath, content);
copied.push(buyerRel);
}
const copiedSet = new Set(copied);
for (const dir of TARGET_DIRS) {
const absDir = path.join(BUYER_ROOT, dir);
for (const filePath of walkDir(absDir)) {
const rel = path.relative(BUYER_ROOT, filePath).replace(/\\/g, "/");
if (copiedSet.has(rel)) continue;
let content = fs.readFileSync(filePath, "utf8");
const hasIView =
/<(Card|Form|Form-item|FormItem|Table|Button|Modal|Page|Tabs|TabPane|Input|Select|Option|DatePicker|Row|Poptip|Drawer|Upload|Checkbox|Radio|Switch|Tag|Alert|Rate|Steps|Step|Spin|Affix|TimePicker|Cascader|Divider|Badge|InputNumber)\b/.test(
content
);
if (!hasIView) {
skipped.push({ file: rel, reason: "no iView components" });
continue;
}
content = applyTemplateTransforms(content);
content = applyScriptTransforms(content);
fs.writeFileSync(filePath, content);
transformed.push(rel);
}
}
console.log("=== Buyer Migration Summary ===");
console.log(`Copied from manager: ${copied.length}`);
console.log(`Script-transformed: ${transformed.length}`);
console.log(`Skipped: ${skipped.length}`);

View File

@@ -0,0 +1,149 @@
#!/usr/bin/env node
/**
* 将静态 <Icon type="..." /> 替换为 <el-icon><Xxx /></el-icon>
* 动态 :type / custom 属性保留,由人工或 resolveIcon 处理。
*/
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "src");
const TYPE_TO_COMPONENT = {
"md-checkmark": "Check",
"md-close": "Close",
"md-trash": "Delete",
"md-add": "Plus",
"md-arrow-dropdown": "ArrowDown",
"md-arrow-dropup": "ArrowUp",
"md-person": "User",
"md-lock": "Lock",
"md-phone-portrait": "Iphone",
"md-key": "Key",
"md-link": "Link",
"md-refresh": "Refresh",
"md-qr-scanner": "Iphone",
"ios-arrow-down": "ArrowDown",
"ios-arrow-up": "ArrowUp",
"ios-arrow-forward": "ArrowRight",
"ios-arrow-back": "ArrowLeft",
"ios-arrow-round-down": "ArrowDown",
"ios-arrow-round-forward": "ArrowRight",
"ios-cart-outline": "ShoppingCart",
"ios-camera": "Camera",
"ios-eye-outline": "View",
"ios-trash-outline": "Delete",
"ios-alert-outline": "Warning",
"ios-help-circle-outline": "Help",
"ios-add-circle-outline": "Plus",
"ios-information-circle": "InfoFilled",
"ios-search": "Search",
"ios-text-outline": "Document",
"ios-alarm-outline": "AlarmClock",
"ios-heart": "StarFilled",
"md-close-circle": "CircleClose",
edit: "Edit",
"trash-a": "Delete",
location: "Location",
};
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory() && entry.name !== "compat") walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function parseAttrs(attrStr) {
const attrs = {};
const re = /(?::([\w-]+)|@([\w-]+)|([\w-]+))=(?:"([^"]*)"|'([^']*)')|(?::([\w-]+)|@([\w-]+)|([\w-]+))(?=\s|\/|>)/g;
let m;
while ((m = re.exec(attrStr)) !== null) {
const name = m[1] || m[2] || m[3] || m[6] || m[7] || m[8];
const value = m[4] ?? m[5] ?? true;
attrs[name] = value;
}
return attrs;
}
function attrsToString(attrs, exclude = []) {
return Object.entries(attrs)
.filter(([k]) => !exclude.includes(k))
.map(([k, v]) => {
if (v === true) return k;
if (k.startsWith("@")) return `${k}="${v}"`;
return `:${k}="${v}"`;
})
.join(" ");
}
function transform(content, filePath) {
if (filePath.includes("/compat/")) return content;
let c = content;
const used = new Set();
c = c.replace(/<Icon\b([^>]*?)\/>/g, (match, attrStr) => {
if (/\bcustom=/.test(attrStr) || /:type=/.test(attrStr)) return match;
const attrs = parseAttrs(attrStr);
const type = attrs.type;
if (!type || !TYPE_TO_COMPONENT[type]) return match;
const comp = TYPE_TO_COMPONENT[type];
used.add(comp);
const rest = attrsToString(attrs, ["type"]);
const size = attrs.size ? ` :size="${attrs.size}"` : "";
const open = rest ? `<el-icon ${rest}${size}>` : `<el-icon${size}>`;
return `${open}<${comp} /></el-icon>`;
});
c = c.replace(/<Icon\b([^>]*?)><\/Icon>/g, (match, attrStr) => {
if (/\bcustom=/.test(attrStr) || /:type=/.test(attrStr)) return match;
const attrs = parseAttrs(attrStr);
const type = attrs.type;
if (!type || !TYPE_TO_COMPONENT[type]) return match;
const comp = TYPE_TO_COMPONENT[type];
used.add(comp);
const rest = attrsToString(attrs, ["type"]);
const size = attrs.size ? ` :size="${attrs.size}"` : "";
const open = rest ? `<el-icon ${rest}${size}>` : `<el-icon${size}>`;
return `${open}<${comp} /></el-icon>`;
});
if (used.size && !c.includes("@element-plus/icons-vue")) {
const imports = [...used].sort().join(", ");
c = c.replace(
/(<script[^>]*>\s*\n)/,
`$1import { ${imports} } from '@element-plus/icons-vue';\n`
);
c = c.replace(/export default \{/, (m) => {
if (/components:\s*\{/.test(c)) {
return m;
}
return `${m}\n components: { ${imports} },`;
});
if (/components:\s*\{/.test(c) && !c.includes(`${[...used][0]},`)) {
c = c.replace(/components:\s*\{([^}]*)\}/, (m, inner) => {
const existing = inner.trim();
const add = [...used].filter((n) => !inner.includes(n)).join(", ");
if (!add) return m;
const merged = existing ? `${existing}, ${add}` : add;
return `components: { ${merged} }`;
});
}
}
return c;
}
let count = 0;
for (const file of walkDir(ROOT)) {
const original = fs.readFileSync(file, "utf8");
if (!/<Icon\b/.test(original)) continue;
const fixed = transform(original, file);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
console.log(path.relative(ROOT, file));
}
}
console.log(`Icon replacements in ${count} files`);

View File

@@ -0,0 +1,63 @@
#!/usr/bin/env node
/**
* 将全局 <Page> 封装替换为原生 el-pagination
*/
const fs = require("fs");
const path = require("path");
const ROOT = path.join(__dirname, "..", "src");
function walkDir(dir, files = []) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walkDir(full, files);
else if (entry.name.endsWith(".vue")) files.push(full);
}
return files;
}
function transformPage(content) {
return content.replace(
/<Page\b([^>]*?)(\/>|>(\s*)<\/Page>)/gs,
(match, attrs, close) => {
let a = attrs;
const hasTotal = /\bshow-total\b/.test(a);
const hasSizer = /\bshow-sizer\b/.test(a);
const hasElevator = /\bshow-elevator\b/.test(a);
a = a.replace(/\s+show-total\b/g, "");
a = a.replace(/\s+show-sizer\b/g, "");
a = a.replace(/\s+show-elevator\b/g, "");
a = a.replace(/:current=/g, ":current-page=");
a = a.replace(/:page-size-opts=/g, ":page-sizes=");
a = a.replace(/@change=/g, "@current-change=");
a = a.replace(/\ssize="small"/g, " small");
const layoutParts = [];
if (hasTotal) layoutParts.push("total");
if (hasSizer) layoutParts.push("sizes");
layoutParts.push("prev", "pager", "next");
if (hasElevator) layoutParts.push("jumper");
const layoutAttr = ` layout="${layoutParts.join(", ")}"`;
if (close.startsWith("/>")) {
return `<el-pagination${a}${layoutAttr} />`;
}
return `<el-pagination${a}${layoutAttr}></el-pagination>`;
}
);
}
let count = 0;
for (const file of walkDir(ROOT)) {
const original = fs.readFileSync(file, "utf8");
if (!/<Page\b/.test(original)) continue;
const fixed = transformPage(original);
if (fixed !== original) {
fs.writeFileSync(file, fixed);
count++;
console.log(path.relative(ROOT, file));
}
}
console.log(`Page → el-pagination in ${count} files`);

View File

@@ -4,60 +4,78 @@
</div>
</template>
<script>
<script setup>
import { onMounted } from "vue";
import { useRoute } from "vue-router";
import storage from "@/plugins/storage";
import { getBaseSite } from "@/api/common.js";
export default {
name: "App",
mounted() {
this.init();
},
methods:{
init(){
if(!storage.getItem("siteName")||!storage.getItem("logoImg")||!storage.getItem("sitelogo_expiration_time")) {
this.getSite();
}else{
// 如果缓存过期,则获取最新的信息
if (new Date() > storage.getItem("sitelogo_expiration_time")) {
this.getSite();
return;
}else{
window.document.title = storage.getItem("siteName");
//动态获取icon
let link =document.querySelector("link[rel*='icon']") ||document.createElement("link");
link.type = "image/x-icon";
link.href = storage.getItem("siteIcon");
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
}
}
},
getSite(){
//获取基本站点信息
getBaseSite().then((res) => {
if (res.success && res.result.settingValue) {
let data = JSON.parse(res.result.settingValue);
// 过期时间
var expirationTime = new Date().setHours(new Date().getHours() + 1);
// 存放过期时间
storage.setItem("sitelogo_expiration_time", expirationTime);
// 存放信息
storage.setItem('siteName', data.siteName);
storage.setItem('logoImg', data.buyerSideLogo);
storage.setItem("siteIcon",data.buyerSideIcon);
window.document.title = data.siteName;
//动态获取icon
let link =document.querySelector("link[rel*='icon']") ||document.createElement("link");
link.type = "image/x-icon";
link.href = data.buyerSideIcon;
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
}
});
}
import { getBaseSite, getThemeSetting } from "@/api/common.js";
import { fetchAndApplyTheme } from "@/utils/theme";
import { useAppStore } from "@/stores/app";
defineOptions({ name: "App" });
const route = useRoute();
const appStore = useAppStore();
function applySiteInfo(data) {
const expirationTime = new Date().setHours(new Date().getHours() + 1);
storage.setItem("sitelogo_expiration_time", expirationTime);
storage.setItem("siteName", data.siteName);
storage.setItem("logoImg", data.buyerSideLogo);
storage.setItem("siteIcon", data.buyerSideIcon);
appStore.setSiteName(data.siteName);
appStore.setLogoImg(data.buyerSideLogo);
if (route.meta.title === undefined) {
window.document.title = data.siteName;
}
};
let link =
document.querySelector("link[rel*='icon']") ||
document.createElement("link");
link.type = "image/x-icon";
link.href = data.buyerSideIcon;
link.rel = "shortcut icon";
document.getElementsByTagName("head")[0].appendChild(link);
}
function init() {
const expirationTime = storage.getItem("sitelogo_expiration_time");
const cacheValid =
expirationTime && new Date() <= new Date(Number(expirationTime));
if (cacheValid) {
applySiteInfo({
siteName: storage.getItem("siteName"),
buyerSideLogo: storage.getItem("logoImg"),
buyerSideIcon: storage.getItem("siteIcon"),
});
}
getSite();
getTheme();
}
function getSite() {
getBaseSite().then((res) => {
if (res.success && res.result.settingValue) {
try {
const data = JSON.parse(res.result.settingValue);
applySiteInfo(data);
} catch {
// 站点配置解析失败时使用缓存
}
}
}).catch((err) => {
console.warn('获取站点配置失败', err);
});
}
function getTheme() {
fetchAndApplyTheme(getThemeSetting).catch((err) => {
console.warn('获取主题配置失败', err);
});
}
onMounted(() => {
init();
});
</script>
<style lang="scss">
#app {

View File

@@ -44,7 +44,8 @@ export function editMemberInfo (params) {
url: '/buyer/passport/member/editOwn',
method: Method.PUT,
needToken: true,
data: params
data: params,
loading: false
})
}
@@ -54,7 +55,9 @@ export function editPwd (params) {
url: `/buyer/passport/member/modifyPass`,
method: Method.PUT,
needToken: true,
data: params
data: params,
loading: false,
skipMessage: true
})
}

View File

@@ -1,5 +1,5 @@
import request, {
Method, commonUrl
Method, buyerUrl
} from '@/plugins/request.js';
// 会员收货地址列表
@@ -17,7 +17,8 @@ export function newMemberAddress (params) {
url: '/buyer/member/address',
needToken: true,
method: Method.POST,
data: params
data: params,
loading: false
});
}
@@ -27,7 +28,8 @@ export function editMemberAddress (params) {
url: '/buyer/member/address',
needToken: true,
method: Method.PUT,
params
params,
loading: false
});
}
@@ -36,7 +38,8 @@ export function delMemberAddress (id) {
return request({
url: `/buyer/member/address/delById/${id}`,
needToken: true,
method: Method.DELETE
method: Method.DELETE,
loading: false
});
}
@@ -52,7 +55,7 @@ export function getAddrDetail (id) {
// 传给后台citycode 获取城市街道等id
export function handleRegion (params) {
return request({
url: `${commonUrl}/common/common/region/region`,
url: `${buyerUrl}/buyer/common/region/region`,
needToken: true,
method: Method.GET,
params

View File

@@ -20,7 +20,8 @@ export function cartGoodsAll () {
return request({
url: '/buyer/trade/carts/all',
method: Method.GET,
needToken: true
needToken: true,
loading: false
});
}
@@ -64,7 +65,7 @@ export function addCartGoods (params) {
/**
* 创建交易
* @param client 客户端:H5/移动端 PC/PC端,WECHAT_MP/小程序端,APP/移动应用端
* @param way 购物车购买:CART/立即购买:BUY_NOW / 积分购买:POINT
* @param way 购物车:CART / 立即购买:BUY_NOW / 积分:POINTS / 拼团:PINTUAN / 砍价:KANJIA
* @param remark 备注 非必填
*/
export function createTrade (data) {
@@ -80,7 +81,7 @@ export function createTrade (data) {
/**
* 选择优惠券
* @param memberCouponId 优惠券id
* @param way 购物车购买:CART/立即购买:BUY_NOW/ 积分购买:POINT
* @param way 购物车:CART / 立即购买:BUY_NOW / 积分:POINTS
* @param used 使用true 弃用 false
*/
export function selectCoupon (params) {
@@ -92,6 +93,21 @@ export function selectCoupon (params) {
});
}
/**
* 选择礼品卡
* @param way 购物车:CART / 立即购买:BUY_NOW / 拼团:PINTUAN / 积分:POINTS / 砍价:KANJIA
* @param credentialId 礼品卡凭证 id;used=false 且不传则清空全部
* @param used 使用 true / 弃用 false
*/
export function selectGiftCard (params) {
return request({
url: '/buyer/trade/carts/select/giftCard',
method: Method.GET,
needToken: true,
params
});
}
/**
* 可用优惠券数量
*/

View File

@@ -1,11 +1,11 @@
import request, {Method, commonUrl} from '@/plugins/request.js';
import request, {Method, buyerUrl} from '@/plugins/request.js';
/**
* 获取拼图验证
*/
export function getVerifyImg (verificationEnums) {
return request({
url: `${commonUrl}/common/common/slider/${verificationEnums}`,
url: `${buyerUrl}/buyer/common/slider/${verificationEnums}`,
method: Method.GET,
needToken: false
});
@@ -15,7 +15,7 @@ export function getVerifyImg (verificationEnums) {
*/
export function postVerifyImg (params) {
return request({
url: `${commonUrl}/common/common/slider/${params.verificationEnums}`,
url: `${buyerUrl}/buyer/common/slider/${params.verificationEnums}`,
method: Method.POST,
needToken: false,
params
@@ -26,7 +26,7 @@ export function postVerifyImg (params) {
*/
export function sendSms (params) {
return request({
url: `${commonUrl}/common/common/sms/${params.verificationEnums}/${params.mobile}`,
url: `${buyerUrl}/buyer/common/sms/${params.verificationEnums}/${params.mobile}`,
method: Method.GET,
needToken: false,
params
@@ -36,9 +36,10 @@ export function sendSms (params) {
// 地区数据,用于三级联动
export function getRegion (id) {
return request({
url: `${commonUrl}/common/common/region/item/${id}`,
url: `${buyerUrl}/buyer/common/region/item/${id}`,
needToken: true,
method: Method.GET
method: Method.GET,
loading: false
});
}
@@ -79,7 +80,7 @@ export function articleDetail (id) {
// 获取IM接口前缀
export function getIMDetail () {
return request({
url: `${commonUrl}/common/common/IM`,
url: `${buyerUrl}/buyer/common/IM`,
method: Method.GET
});
}
@@ -88,8 +89,17 @@ export function getIMDetail () {
//获取图片logo
export function getBaseSite(){
return request ({
url:`${commonUrl}/common/common/site`,
url:`${buyerUrl}/buyer/common/site`,
method: Method.GET,
needToken: false
})
}
// 获取主题色配置
export function getThemeSetting() {
return request({
url: `${buyerUrl}/buyer/common/site/theme`,
method: Method.GET,
needToken: false,
});
}

View File

@@ -5,9 +5,10 @@ import request, {
// 商品列表
export function goodsList (params) {
return request({
url: '/buyer/goods/goods/es',
url: '/buyer/goods/goods/search',
method: Method.GET,
needToken: false,
optionalToken: true,
params
});
}
@@ -15,9 +16,10 @@ export function goodsList (params) {
// 商品分类 筛选条件 品牌, 尺寸等
export function filterList (params) {
return request({
url: '/buyer/goods/goods/es/related',
url: '/buyer/goods/goods/search/related',
method: Method.GET,
needToken: false,
optionalToken: true,
params
});
}
@@ -31,12 +33,13 @@ export function goodsDetail (goodsId) {
});
}
// id获取商品Sku详情
// id获取商品Sku详情(支持未登录浏览,登录后可选携带 token)
export function goodsSkuDetail (params) {
return request({
url: `/buyer/goods/goods/sku/${params.goodsId}/${params.skuId}`,
method: Method.GET,
needToken: true,
needToken: false,
optionalToken: true,
params
});
}

View File

@@ -63,6 +63,9 @@ export function handleRefreshToken(token) {
url: `/buyer/passport/member/refresh/${token}`,
method: Method.GET,
needToken: false,
// 刷新令牌请求自身不再进入 401/403 刷新分支,避免拦截器递归死锁;失败也不弹全局错误提示
skipAuthHandler: true,
skipMessage: true,
});
}

View File

@@ -69,6 +69,32 @@ export function loginCallback (uuid) {
});
}
export function getThirdAccountBindList () {
return request({
url: '/buyer/passport/connect/bind/list',
method: Method.GET,
needToken: true
});
}
export function bindThirdAccount (data) {
return request({
url: '/buyer/passport/connect/bind',
method: Method.POST,
needToken: true,
data
});
}
export function unbindThirdAccount (data) {
return request({
url: '/buyer/passport/connect/bind/unbind',
method: Method.POST,
needToken: true,
data
});
}
/**
* 忘记密码 验证手机验证码
*/
@@ -98,6 +124,7 @@ export function getSCLoginCode(params) {
url: `/buyer/passport/member/pc_session`,
method: Method.POST,
needToken: false,
loading: false,
params
});
}
@@ -106,6 +133,19 @@ export function sCLogin(token,params) {
url: `/buyer/passport/member/session_login/`+token,
method: Method.POST,
needToken: false,
loading: false,
skipMessage: true,
// 长轮询后端最长挂起 20s,需大于该值,避免 axios 10s 默认超时把二维码请求掐断
timeout: 25000,
params
});
}
export function getWechatH5QrCode (params) {
return request({
url: '/buyer/other/wechat/h5/qrcode',
method: Method.GET,
needToken: true,
params
})
}

View File

@@ -158,7 +158,7 @@ export function addEvaluation (params) {
url: `/buyer/member/evaluation`,
method: Method.POST,
needToken: true,
params
data: params
});
}
@@ -180,6 +180,15 @@ export function receiptList () {
});
}
// 发票详情
export function receiptDetail (id) {
return request({
url: `/buyer/trade/receipt/${id}`,
method: Method.GET,
needToken: true
});
}
// 保存发票信息
export function saveReceipt (params) {
return request({
@@ -196,6 +205,7 @@ export function couponList (params) {
url: `/buyer/promotion/coupon`,
method: Method.GET,
needToken: true,
loading: false,
params
});
}
@@ -288,7 +298,8 @@ export function cancelAfterSale (afterSaleSn) {
return request({
url: `/buyer/order/afterSale/cancel/${afterSaleSn}`,
method: Method.POST,
needToken: true
needToken: true,
loading: false
});
}
@@ -333,93 +344,6 @@ export function clearComplain (id) {
});
}
/**
* 获取当前会员分销信息
*/
export function distribution () {
return request({
url: `/buyer/distribution/distribution`,
method: Method.GET,
needToken: true
});
}
/**
* 申请成为分销商
* @param idNumber 身份证号
* @param name 名字
*/
export function applyDistribution (params) {
return request({
url: `/buyer/distribution/distribution`,
method: Method.POST,
needToken: true,
params
});
}
/**
* 获取分销商订单列表
*/
export function getDistOrderList (params) {
return request({
url: `/buyer/distribution/order`,
method: Method.GET,
needToken: true,
params
});
}
/**
* 获取分销商商品列表
*/
export function getDistGoodsList (params) {
return request({
url: `/buyer/distribution/goods`,
method: Method.GET,
needToken: true,
params
});
}
/**
* 绑定、解绑分销商品
* @param distributionGoodsId 分销商品id
* @param checked 分销商品id,true为绑定,false为解绑
*/
export function selectDistGoods (params) {
return request({
url: `/buyer/distribution/goods/checked/${params.distributionGoodsId}`,
method: Method.GET,
needToken: true,
params
});
}
/**
* 分销商提现历史
*/
export function distCashHistory (params) {
return request({
url: `/buyer/distribution/cash`,
method: Method.GET,
needToken: true,
params
});
}
/**
* 分销商提现
*/
export function distCash (params) {
return request({
url: `/buyer/distribution/cash`,
method: Method.POST,
needToken: true,
params
});
}
/**
* 我的足迹
* @param {Number} pageNumber 页码
@@ -480,6 +404,31 @@ export function memberPointHistory (params) {
params
});
}
export function memberGradeCurrent (params) {
return request({
url: `/buyer/member/memberGrade/current`,
method: Method.GET,
needToken: true,
params
})
}
export function memberGradeRules () {
return request({
url: `/buyer/member/memberGrade/rules`,
method: Method.GET,
needToken: true
})
}
export function memberGradeList () {
return request({
url: `/buyer/member/memberGrade/list`,
method: Method.GET,
needToken: true
})
}
/**
* 分页获取会员站内信
* @param {Object} params 请求参数,包括pageNumber、pageSize、status
@@ -497,6 +446,15 @@ export function memberMsgList (params) {
* @param {String} messageId 消息id
*/
export function getMemberMessage (id) {
return request({
url: `/buyer/message/member/${id}`,
method: Method.GET,
needToken: true,
skipMessage: true
});
}
export function readMemberMsg (id) {
return request({
url: `/buyer/message/member/${id}`,

View File

@@ -24,12 +24,13 @@ export function tradeDetail (params) {
* @param payClient 调起方式 可用值:APP,NATIVE,JSAPI,H5
* @param sn 订单编号
*/
export function pay (params) {
export function pay (params, config = {}) {
return request({
url: `/buyer/payment/cashier/pay/${params.paymentMethod}/${params.paymentClient}`,
needToken: true,
method: Method.GET,
params
params,
skipMessage: !!config.skipMessage,
});
}

View File

@@ -57,3 +57,50 @@ export function pointGoodsDetail (id) {
id
});
}
/**
* 分页查询当前客户礼品卡
*/
export function getGiftCardCashMemberCardPage (params) {
return request({
url: '/buyer/promotion/giftCardCash/memberCard',
method: Method.GET,
needToken: true,
params
});
}
/**
* 绑定礼品卡(卡号 + 兑换码/卡密)
*/
export function bindGiftCardCashMemberCard (data) {
return request({
url: '/buyer/promotion/giftCardCash/memberCard/bind',
method: Method.POST,
needToken: true,
headers: { 'Content-Type': 'application/json' },
data
});
}
/**
* 激活礼品卡
*/
export function activateGiftCardCashMemberCard (memberCardId) {
return request({
url: `/buyer/promotion/giftCardCash/memberCard/${memberCardId}/activate`,
method: Method.POST,
needToken: true
});
}
/**
* 下单页可用礼品卡列表
*/
export function getAvailableGiftCardCashMemberCards () {
return request({
url: '/buyer/promotion/giftCardCash/memberCard/available',
method: Method.GET,
needToken: true
});
}

View File

@@ -42,11 +42,21 @@ export function applyThird (params) {
})
}
// 店铺信息(含评分)
export function getStoreById (id) {
return request({
url: `/buyer/store/store/store/${id}`,
needToken: false,
method: Method.GET
})
}
// 店铺详情
export function getDetailById (id) {
return request({
url: `/buyer/store/store/get/detail/${id}`,
needToken: true,
needToken: false,
optionalToken: true,
method: Method.GET
})
}
@@ -72,7 +82,8 @@ export function applyStatus () {
return request({
url: `/buyer/store/store/apply`,
needToken: true,
method: Method.GET
method: Method.GET,
loading: false
})
}

View File

@@ -0,0 +1,29 @@
import back from "./back.png";
import carts from "./carts.png";
import collage from "./collage.png";
import feedback from "./feedback.png";
import notice from "./notice.png";
import notification from "./notification.png";
import shop from "./shop.png";
import story from "./story.png";
import support from "./support.png";
import user from "./user.png";
const iconMap = {
back,
carts,
collage,
feedback,
notice,
notification,
shop,
story,
support,
user,
};
export function getIconUrl(name) {
return iconMap[name] || "";
}
export default iconMap;

View File

@@ -0,0 +1,40 @@
:root {
--el-color-primary: var(--theme-color, #ff3c2a);
--el-color-danger: var(--theme-color, #ff3c2a);
--el-font-size-base: 14px;
}
.el-table--border {
.el-table__cell {
border-right: none !important;
}
}
.link-text {
color: var(--theme-color, #ff3c2a);
cursor: pointer;
text-decoration: none;
&:hover {
opacity: 0.85;
}
}
.op-split {
margin: 0 8px;
color: #dcdfe6;
}
/* 次要强调按钮,使用高亮主题色(如「立即购买」) */
.confirm-receipt-btn {
background-color: var(--theme-light, #ff6b35) !important;
border-color: var(--theme-light, #ff6b35) !important;
color: #fff !important;
&:hover,
&:focus {
background-color: color-mix(in srgb, var(--theme-light, #ff6b35) 88%, black) !important;
border-color: color-mix(in srgb, var(--theme-light, #ff6b35) 88%, black) !important;
color: #fff !important;
}
}

View File

@@ -0,0 +1,25 @@
// 全局布局工具类(非 scoped,避免子组件继承 class 时宽度失效)
.width_1200_auto {
width: 1200px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
box-sizing: border-box;
}
// 首页主体:分类导航 + 楼层装修共用同一 1200px 容器,保证列对齐
.index-main {
width: 1200px;
max-width: 1200px;
margin-left: auto;
margin-right: auto;
box-sizing: border-box;
position: relative;
}
// 与 CateNav 商品分类栏、Carousel 左占位一致的列宽
$index-main-width: 1200px;
$index-side-col-width: 263.2px;
$index-col-gap: 10px;
// 1200 - 263.2*2 - 10*2 = 653.6px
$index-center-col-width: $index-main-width - ($index-side-col-width * 2) - ($index-col-gap * 2);

View File

@@ -10,7 +10,7 @@ $warning_color: #ff9900;
$error_color: #ed3f14;
$handle-btn-color: #438cde;
$theme_color: #F31947;
$theme_color: var(--theme-color, #ff3c2a);
$border_color: #dddee1;
$title_color: #8c8c8c;
@@ -30,6 +30,7 @@ $dark_content_color: #d5d5d5;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
}
/***** 封装一些方法可用于 黑暗主题 ,明亮主题 *****/
@@ -305,9 +306,11 @@ li {
}
.width_1200_auto {
width: 1184px;
width: 1200px;
max-width: 1200px;
margin: 0 auto;
box-sizing: border-box;
}
@@ -325,18 +328,24 @@ li {
.goods-list {
display: flex;
flex-wrap: wrap;
width: 1200px;
width: 100%;
margin: 0 auto;
column-gap: 10px;
}
.goods-show-info {
width: 235px;
width: calc(20% - 8px);
padding: 6px;
margin: 10px 0px;
margin-left: 5px;
margin: 10px 0;
position: relative;
border: 1px solid #fff;
cursor: pointer;
background-color: #fff;
box-sizing: border-box;
}
.goods-show-img img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
}
.goods-show-info:hover {
border: 1px solid #ccc;
@@ -346,16 +355,17 @@ li {
margin-top: 6px;
}
.goods-show-detail {
font-size: 12px;
font-size: 14px;
line-height: 21px;
margin: 6px 0px;
}
.goods-show-num {
font-size: 12px;
font-size: 14px;
margin-bottom: 6px;
color: #666;
}
.goods-show-num {
font-size: 12px;
font-size: 14px;
margin-bottom: 6px;
color: #009688;
}
@@ -364,11 +374,16 @@ li {
font-weight: bold;
}
.goods-show-seller {
font-size: 12px;
font-size: 14px;
color: $theme_color;
}
.goods-page {
margin:10px auto ;
text-align: right;
width: 1200px;
margin: 10px 0;
width: 100%;
display: flex;
justify-content: flex-end;
:deep(.el-pagination) {
justify-content: flex-end;
}
}

View File

@@ -9,18 +9,24 @@
.goods-list {
display: flex;
flex-wrap: wrap;
width: 1200px;
width: 100%;
margin: 0 auto;
column-gap: 10px;
}
.goods-show-info {
width: 235px;
width: calc(20% - 8px);
padding: 6px;
margin: 10px 0px;
margin-left: 5px;
margin: 10px 0;
position: relative;
border: 1px solid #fff;
cursor: pointer;
background-color: #fff;
box-sizing: border-box;
}
.goods-show-img img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
}
.goods-show-info:hover {
border: 1px solid #ccc;
@@ -30,13 +36,15 @@
margin-top: 6px;
}
.goods-show-detail {
height: 36px;
font-size: 12px;
height: 42px;
font-size: 14px;
line-height: 21px;
margin: 6px 0px;
overflow:hidden;
text-overflow: ellipsis;
display:-webkit-box;
-webkit-line-clamp:2;
line-clamp: 2;
-webkit-box-orient:vertical;
color:#808080;
}
@@ -44,12 +52,12 @@
color:#E23A3A;
}
.goods-show-num {
font-size: 12px;
font-size: 14px;
margin-bottom: 6px;
color: #666;
}
.goods-show-num {
font-size: 12px;
font-size: 14px;
margin-bottom: 6px;
color: #009688;
}
@@ -58,11 +66,16 @@
font-weight: bold;
}
.goods-show-seller {
font-size: 12px;
font-size: 14px;
color: $theme_color;
}
.goods-page {
margin:10px auto ;
text-align: right;
width: 1200px;
margin: 10px 0;
width: 100%;
display: flex;
justify-content: flex-end;
:deep(.el-pagination) {
justify-content: flex-end;
}
}

View File

@@ -1,6 +1,4 @@
@import "~view-design/src/styles/index.less";
// iview 自定义样式
// Element Plus 主题变量(原 view-design less 已移除)
@primary-color: #ff5c58;
@info-color: #fa6419;
@success-color: #68cabe;
@@ -11,9 +9,7 @@
@table-td-highlight-bg: #ededed;
@font-size-base: 12px;
.ivu-drawer,
.drawer,
.ivu-drawer-wrap {
.el-drawer,
.drawer {
z-index: 2600 !important;
}

View File

@@ -1,217 +1,386 @@
<template>
<div class="navbar" :class="{'small-search-box':useClass == 'fixed-show'}">
<div class="container width_1200_auto flex flex-a-c">
<img
:src="$store.state.logoImg"
v-if="showLogo"
class="logo-img"
alt=""
@click="$router.push('/')"
/>
<div :class="{'small-search-box':useClass == 'fixed-show'}" class="search-box">
<i-input
<div class="navbar" ref="rootRef" :class="{'small-search-box': useClass == 'fixed-show'}">
<template v-if="useClass == 'fixed-show'">
<div class="search search-embedded">
<el-input
v-model="searchData"
size="large"
class="search "
class="search-input"
placeholder="输入你想查找的商品"
@keyup.enter.native="search"
>
<div class="search-icon" slot="append" @click="search">
<Icon type="ios-search" size="21"/>
</div>
</i-input>
<template v-if="showTag">
<div class="only-store" v-if="storeId" @click="research()">
切换为{{!onlyStore ? '店铺内' : '平台'}}搜索
</div>
<div v-if="promotionTags.length === 0"></div>
<div v-else class="history-list flex">
<div
v-for="(item, index) in promotionTags"
:key="index"
class="mr_10"
>
<span class="history-item" @click="selectTags(item)">{{ item }}</span>
@focus="openPanel"
@input="queueSuggest"
@keyup.enter="search"
/>
<div class="search-btn" @click="search">
<el-icon :size="21"><SearchIcon /></el-icon>
</div>
<div v-if="showPanel" class="suggest-panel" @mousedown.prevent>
<SearchSuggest
:keyword="trimmedKeyword"
:history="history"
:hot-words="promotionTags"
:suggestions="suggestions"
:suggesting="suggesting"
@pick="selectTags"
@clear="clearHistory"
/>
</div>
</div>
</template>
<template v-else>
<div class="container width_1200_auto flex flex-a-c">
<img
:src="logoImg"
v-if="showLogo"
class="logo-img"
alt=""
@click="$router.push('/')"
/>
<div class="search-box">
<div class="search">
<el-input
v-model="searchData"
size="large"
class="search-input"
placeholder="输入你想查找的商品"
@focus="openPanel"
@input="queueSuggest"
@keyup.enter="search"
/>
<div class="search-btn" @click="search">
<el-icon :size="21"><SearchIcon /></el-icon>
</div>
<div v-if="showPanel" class="suggest-panel" @mousedown.prevent>
<SearchSuggest
:keyword="trimmedKeyword"
:history="history"
:hot-words="promotionTags"
:suggestions="suggestions"
:suggesting="suggesting"
@pick="selectTags"
@clear="clearHistory"
/>
</div>
</div>
</template>
<template v-if="showTag">
<div v-if="promotionTags.length === 0"></div>
<div v-else class="history-list flex">
<div
v-for="(item, index) in promotionTags"
:key="index"
class="mr_10"
>
<span class="history-item" @click="selectTags(item)">{{ item }}</span>
</div>
</div>
</template>
</div>
</div>
</div>
</template>
</div>
</template>
<script>
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import storage from '@/plugins/storage.js'
import {hotWords} from '@/api/goods.js'
import { goodsList, hotWords } from '@/api/goods.js'
import { Search as SearchIcon } from '@element-plus/icons-vue'
import SearchSuggest from '@/components/SearchSuggest.vue'
import { useAppStore } from '@/stores/app'
export default {
name: 'search',
props: {
showTag: { // 是否展示搜索栏下方热门搜索
type: Boolean,
default: true
},
showLogo: { // 是否展示左侧logo
type: Boolean,
default: true
},
storeId: { // 是否为店铺页面
type: String,
default: ""
},
hover: {
type: Boolean,
default: false
},
useClass:{
type:null,
default:''
}
},
watch:{
storeId(val){
this.onlyStore = val ? true : false
}
},
data() {
return {
searchData: '', // 搜索内容
onlyStore:false,
};
},
methods: {
selectTags(item) { // 选择热门标签
this.searchData = item;
this.search();
},
research(){
this.onlyStore = !this.onlyStore
},
search () { // 全平台搜索商品
const url = this.$route.path;
if(url == '/goodsList'){
this.$emit('search', this.searchData)
}else{
const pushData = {
path:'/goodsList',
query: { keyword: this.searchData },
}
if(this.storeId && this.onlyStore) pushData.query.storeId = this.storeId
defineOptions({ name: 'search' })
this.$router.push(pushData);
}
},
searchStore() { // 店铺搜索商品
this.$emit('search', this.searchData)
}
const props = defineProps({
showTag: {
type: Boolean,
default: true
},
computed: {
promotionTags() {
if (this.$store.state.hotWordsList) {
return JSON.parse(this.$store.state.hotWordsList)
} else {
return []
}
}
showLogo: {
type: Boolean,
default: true
},
created() {
this.searchData = this.$route.query.keyword
if (!this.hover) { // 首页顶部固定搜索栏不调用热词接口
// 搜索热词每5分钟请求一次
const reloadTime = storage.getItem('hotWordsReloadTime')
const time = new Date().getTime() - 5 * 60 * 1000
if (!reloadTime) {
hotWords({count: 5}).then(res => {
if (res.success && res.result) storage.setItem('hotWordsList', res.result)
})
storage.setItem('hotWordsReloadTime', new Date().getTime())
} else if (reloadTime && time > reloadTime) {
hotWords({count: 5}).then(res => {
if (res.success && res.result) storage.setItem('hotWordsList', res.result)
})
storage.setItem('hotWordsReloadTime', new Date().getTime())
}
}
storeId: {
type: String,
default: ""
},
hover: {
type: Boolean,
default: false
},
useClass: {
type: null,
default: ''
}
};
})
const emit = defineEmits(['search'])
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const HISTORY_KEY = 'buyerSearchHistory'
const HISTORY_LIMIT = 10
function readHistory() {
try {
const raw = localStorage.getItem(HISTORY_KEY)
const list = raw ? JSON.parse(raw) : []
return Array.isArray(list) ? list.filter((item) => typeof item === 'string' && item.trim()) : []
} catch {
return []
}
}
function parseHotWords(raw) {
if (!raw) return []
if (Array.isArray(raw)) return raw.filter(Boolean)
if (typeof raw !== 'string') return []
try {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return parsed.filter(Boolean)
} catch {
// 旧数据可能是逗号拼接
}
return raw.split(',').map((item) => item.trim()).filter(Boolean)
}
const rootRef = ref(null)
const searchData = ref('')
const history = ref([])
const panelVisible = ref(false)
const suggestions = ref([])
const suggesting = ref(false)
let suggestTimer = null
let suggestSeq = 0
const logoImg = computed(() => appStore.logoImg)
const trimmedKeyword = computed(() => {
return (searchData.value || '').trim()
})
const promotionTags = computed(() => {
return parseHotWords(appStore.hotWordsList)
})
const showPanel = computed(() => {
if (!panelVisible.value) return false
if (trimmedKeyword.value) return true
return history.value.length > 0 || promotionTags.value.length > 0
})
watch(
() => route.query.keyword,
(value) => {
searchData.value = value || ''
}
)
function selectTags(item) {
searchData.value = item
search()
}
function remember(keyword) {
const next = [keyword, ...history.value.filter((item) => item !== keyword)].slice(0, HISTORY_LIMIT)
history.value = next
localStorage.setItem(HISTORY_KEY, JSON.stringify(next))
}
function clearHistory() {
history.value = []
localStorage.removeItem(HISTORY_KEY)
}
function openPanel() {
panelVisible.value = true
if (trimmedKeyword.value) queueSuggest()
}
function closePanel() {
panelVisible.value = false
}
function onDocumentClick(event) {
if (rootRef.value && !rootRef.value.contains(event.target)) closePanel()
}
function queueSuggest() {
panelVisible.value = true
clearTimeout(suggestTimer)
const keyword = trimmedKeyword.value
if (!keyword) {
suggestions.value = []
suggesting.value = false
return
}
suggesting.value = true
suggestTimer = setTimeout(() => fetchSuggest(keyword), 300)
}
function fetchSuggest(keyword) {
const seq = ++suggestSeq
const params = { keyword, pageNumber: 1, pageSize: 8 }
if (props.storeId) params.storeId = props.storeId
goodsList(params).then((res) => {
if (seq !== suggestSeq || keyword !== trimmedKeyword.value) return
const records = res && res.success ? (res.result.records || []) : []
const seen = new Set()
const names = []
promotionTags.value.forEach((word) => {
if (word && String(word).toLowerCase().includes(keyword.toLowerCase()) && !seen.has(word)) {
seen.add(word)
names.push(word)
}
})
records.forEach((item) => {
const name = item && item.goodsName
if (name && !seen.has(name)) {
seen.add(name)
names.push(name)
}
})
suggestions.value = names.slice(0, 8)
}).catch(() => {
if (seq === suggestSeq) suggestions.value = []
}).finally(() => {
if (seq === suggestSeq) suggesting.value = false
})
}
function search() {
const keyword = trimmedKeyword.value
searchData.value = keyword
closePanel()
if (keyword) remember(keyword)
const url = route.path
if (url == '/goodsList') {
emit('search', keyword)
} else {
const pushData = {
path: '/goodsList',
query: { keyword },
}
if (props.storeId) pushData.query.storeId = props.storeId
router.push(pushData)
}
}
function searchStore() {
emit('search', trimmedKeyword.value)
}
function applyHotWords(result) {
const list = parseHotWords(result)
appStore.setHotWords(list)
storage.setItem('hotWordsList', JSON.stringify(list))
}
function loadHotWords() {
hotWords({ count: 5 }).then((res) => {
if (res.success && res.result) applyHotWords(res.result)
}).catch(() => {
// 热词为可选数据,失败时沿用缓存或默认热词
})
storage.setItem('hotWordsReloadTime', new Date().getTime())
}
// 初始化(原 created 钩子逻辑)
searchData.value = route.query.keyword || ''
history.value = readHistory()
if (!props.hover) {
const reloadTime = storage.getItem('hotWordsReloadTime')
const time = new Date().getTime() - 5 * 60 * 1000
if (!reloadTime || time > reloadTime) loadHotWords()
}
onMounted(() => {
document.addEventListener('click', onDocumentClick)
})
onUnmounted(() => {
clearTimeout(suggestTimer)
document.removeEventListener('click', onDocumentClick)
})
</script>
<style scoped lang="scss">
.only-store{
text-align: right;
color:$theme_color;
cursor: pointer;
}
.navbar {
height: 113px;
background: #fff;
}
.search-icon{
width: 100%;
height: 100%;
}
.small-search-box{
height: 60px;
margin: 0 !important;
.navbar.small-search-box {
height: 60px;
width: 100%;
margin: 0;
display: flex;
align-items: center;
background: transparent;
}
.search-embedded {
width: 100%;
margin: 0;
}
.container {
position: relative;
height: 100%;
}
.search {
$btn-gap: 4px;
$btn-width: 60px;
$input-height: 37.8px;
position: relative;
width: 778.4px;
height: $input-height;
margin: 10px 0px 5px 0;
border-radius: 18.9px;
.search-input {
width: 100%;
/deep/ .ivu-input.ivu-input-large {
border: 1.4px solid $theme_color;
box-sizing: border-box;
border-radius: 19.6px;
position: relative;
padding-left: 26px;
font-size: 14px;
font-weight: normal;
height: 37.8px;
color: #999;
&:focus {
:deep(.el-input__wrapper) {
border: 1.4px solid $theme_color;
box-sizing: border-box;
border-radius: 19.6px;
padding-left: 26px;
padding-right: calc(#{$btn-gap * 2} + #{$btn-width});
font-size: 14px;
font-weight: normal;
height: $input-height;
color: #999;
box-shadow: none;
}
}
/deep/ .ivu-input-group-append {
border-radius: 19.6px !important;
.search-btn {
position: absolute;
right: $btn-gap;
top: $btn-gap;
bottom: $btn-gap;
width: $btn-width;
border-radius: calc((#{$input-height} - #{$btn-gap * 2}) / 2);
cursor: pointer;
box-sizing: border-box;
border: 1.4px solid $theme_color;
width: 67.2px;
height: 37.8px;
position: absolute;
right: 0;
display: flex;
align-items: center;
justify-content: center;
top: 0;
z-index: 99;
z-index: 1;
background-color: $theme_color;
color: #ffffff;
button {
font-size: 14px;
font-weight: 600;
line-height: 1;
}
}
}
.search-embedded.search {
width: 100%;
}
.search-box{
margin-left: 28px;
}
@@ -220,28 +389,7 @@ export default {
cursor: pointer;
}
.store-search {
width: 55.6px;
padding: 0 9px;
border-radius: 0;
border-radius: 3px;
&:nth-child(2) {
width: 55px;
margin-left: -2px;
border-radius: 3px;
}
}
.btn-div {
position: relative;
height: 0px;
top: -38px;
left: 352px;
}
.history-list {
margin-left: 28px;
}
@@ -254,4 +402,18 @@ export default {
color: #666666;
cursor: pointer;
}
.suggest-panel {
position: absolute;
left: 0;
right: 0;
top: calc(100% + 8px);
z-index: 30;
padding: 8px 0;
background: #fff;
border: 1px solid #eee;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<div>
<div v-if="keyword" class="suggest-section">
<div v-if="suggesting && !suggestions.length" class="suggest-empty">正在查找...</div>
<button
v-for="item in suggestions"
:key="item"
type="button"
class="suggest-item"
@click="$emit('pick', item)"
>
<span v-for="(part, index) in parts(item)" :key="index" :class="{ hit: part.hit }">{{ part.text }}</span>
</button>
<div v-if="!suggesting && !suggestions.length" class="suggest-empty">没有匹配的商品,回车查看全部结果</div>
</div>
<template v-else>
<div v-if="history.length" class="suggest-section">
<div class="suggest-head">
<span>搜索历史</span>
<button type="button" class="suggest-clear" @click="$emit('clear')">清空</button>
</div>
<div class="suggest-tags">
<button
v-for="item in history"
:key="item"
type="button"
class="suggest-tag"
@click="$emit('pick', item)"
>{{ item }}</button>
</div>
</div>
<div v-if="hotWords.length" class="suggest-section">
<div class="suggest-head"><span>热门搜索</span></div>
<div class="suggest-tags">
<button
v-for="item in hotWords"
:key="item"
type="button"
class="suggest-tag"
@click="$emit('pick', item)"
>{{ item }}</button>
</div>
</div>
</template>
</div>
</template>
<script setup>
defineOptions({ name: "SearchSuggest" });
const props = defineProps({
keyword: { type: String, default: "" },
history: { type: Array, default: () => [] },
hotWords: { type: Array, default: () => [] },
suggestions: { type: Array, default: () => [] },
suggesting: { type: Boolean, default: false },
});
defineEmits(["pick", "clear"]);
function parts(text) {
const value = String(text || "");
const key = props.keyword;
if (!key) return [{ text: value, hit: false }];
const index = value.toLowerCase().indexOf(key.toLowerCase());
if (index < 0) return [{ text: value, hit: false }];
return [
{ text: value.slice(0, index), hit: false },
{ text: value.slice(index, index + key.length), hit: true },
{ text: value.slice(index + key.length), hit: false },
].filter((part) => part.text);
}
</script>
<style scoped lang="scss">
.suggest-section + .suggest-section {
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid #f5f5f5;
}
.suggest-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 16px;
color: #999;
font-size: 13px;
}
.suggest-clear {
border: none;
background: transparent;
color: #999;
font-size: 13px;
cursor: pointer;
}
.suggest-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 0 16px 8px;
}
.suggest-tag {
max-width: 180px;
padding: 4px 10px;
border: none;
border-radius: 14px;
background: #f5f5f5;
color: #333;
font-size: 13px;
line-height: 20px;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.suggest-item {
display: block;
width: 100%;
padding: 8px 16px;
border: none;
background: transparent;
color: #333;
font-size: 14px;
line-height: 22px;
text-align: left;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:hover {
background: #fafafa;
}
.hit {
color: $theme_color;
}
}
.suggest-empty {
padding: 10px 16px;
color: #999;
font-size: 13px;
}
</style>

View File

@@ -1,172 +1,177 @@
<template>
<div>
<Modal v-model="showAddr" width="800" title="收件人地址">
<Form
<el-dialog v-model="showAddr" width="800" title="收件人地址">
<el-form
:model="formData"
ref="form"
ref="formRef"
label-position="left"
:label-width="100"
label-width="100px"
:rules="ruleInline"
>
<FormItem label="收件人" prop="name">
<i-input v-model="formData.name" style="width: 600px"></i-input>
</FormItem>
<FormItem label="收件地区" prop="address">
<el-form-item label="收件人" prop="name">
<el-input v-model="formData.name" style="width: 600px"></el-input>
</el-form-item>
<el-form-item label="收件地区" prop="address">
{{ formData.address || '暂无地址' }}
<Button type="primary" style="margin-left: 10px;" size="small" @click="$refs.map.open()">选择</Button>
</FormItem>
<FormItem label="详细地址" prop="detail">
<i-input v-model="formData.detail" style="width: 600px"></i-input>
</FormItem>
<FormItem label="手机号码" prop="mobile">
<i-input v-model="formData.mobile" style="width: 600px"></i-input>
</FormItem>
<FormItem label="地址别名">
<i-input
<el-button type="primary" style="margin-left: 10px;" size="small" @click="mapRef.open()">选择</el-button>
</el-form-item>
<el-form-item label="详细地址" prop="detail">
<el-input v-model="formData.detail" style="width: 600px"></el-input>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="formData.mobile" style="width: 600px"></el-input>
</el-form-item>
<el-form-item label="地址别名">
<el-input
v-model="formData.alias"
length
:maxlength="4"
:maxlength="5"
show-word-limit
placeholder="请输入地址别名,例如公司"
style="width: 600px"
></i-input>
</FormItem>
<FormItem label="默认地址">
<i-switch v-model="formData.isDefault" />
</FormItem>
</Form>
<div class="mt_20" slot="footer">
<Button @click="hide">取消</Button>
<Button type="primary" class="mr_10" :loading="loading" @click="save">保存收货地址</Button>
</div>
</Modal>
<multipleMap ref="map" @callback="getAddress"></multipleMap>
></el-input>
</el-form-item>
<el-form-item label="默认地址">
<el-switch v-model="formData.isDefault" />
</el-form-item>
</el-form>
<template #footer><div class="mt_20">
<el-button @click="hide">取消</el-button>
<el-button type="primary" class="mr_10" :loading="loading" @click="save">保存收货地址</el-button>
</div></template>
</el-dialog>
<multipleMap ref="mapRef" @callback="getAddress"></multipleMap>
</div>
</template>
<script>
<script setup>
import { ref, watch } from "vue";
import { Message } from "@/utils/message";
import multipleMap from "@/components/map/multiple-map";
import {
newMemberAddress,
editMemberAddress,
getAddrDetail
} from '@/api/address';
export default {
name: 'addressManage',
props: {
id: { // 传入的地址id
defalut: '',
type: String
}
},
data () {
return {
showAddr: false, // 控制模态框显隐
formData: { // 表单数据
isDefault: false
},
ruleInline: { // 验证规则
name: [{ required: true, message: '请输入收件人姓名', trigger: 'blur' }],
address: [{ required: true, message: '请输入地址', trigger: 'change' }],
detail: [
{ required: true, message: '请输入详细地址', trigger: 'blur' }
],
mobile: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{
type: 'string',
pattern: /^1[3|4|5|6|7|8][0-9]{9}$/,
message: '手机号格式出错',
trigger: 'blur'
}
]
},
loading: false, // 提交的加载状态
mapMsg: {} // 地图信息
};
},
methods: {
save () { // 保存地址
this.$refs.form.validate((valid) => {
if (valid) {
const params = JSON.parse(JSON.stringify(this.formData));
params.consigneeAddressPath = params.address.replace(/\s/g, ',');
delete params.address;
this.loading = true;
if (this.id) {
editMemberAddress(params).then((res) => {
this.loading = false;
if (res.success) {
this.$Message.success('编辑地址成功');
this.$emit('change', true);
this.hide();
}
}).catch(() => { this.loading = false; });
} else {
newMemberAddress(params).then((res) => {
this.loading = false;
if (res.success) {
this.$Message.success('新增地址成功');
this.$emit('change', true);
this.hide();
}
}).catch(() => { this.loading = false; });
}
}
});
},
getAddrById (id) {
// 获取地址详情
getAddrDetail(id).then((res) => {
if (res.success) {
console.log(res);
const data = res.result;
data.address = res.result.consigneeAddressPath.replace(/,/g, ' ');
this.formData = data;
}
});
},
getAddress (val) {
// 获取地图选择信息
if(val.type === 'select'){
const paths = val.data.map(item => item.name).join(',')
const ids = val.data.map(item => item.id).join(',')
this.$set(this.formData,'address',paths)
this.$set(this.formData,'consigneeAddressIdPath',ids)
const coord = val.data[val.data.length - 1].center.split(',')
this.formData.lat = coord[1]
this.formData.lon = coord[0]
}else{
this.$set(this.formData, "address", val.data.addr);
this.$set(this.formData, "consigneeAddressIdPath", val.data.addrId);
this.$set(this.formData, "detail", val.data.address);
this.formData.lat = val.data.position.lat;
this.formData.lon = val.data.position.lng;
}
},
show () { // 地址模态框显示
this.showAddr = true;
},
hide () { // 地址模态框隐藏
this.showAddr = false;
defineOptions({ name: 'addressManage' });
const props = defineProps({
id: { // 传入的地址id
defalut: '',
type: String
}
});
const emit = defineEmits(['change']);
const showAddr = ref(false); // 控制模态框显隐
const formData = ref({ // 表单数据
isDefault: false
});
const ruleInline = { // 验证规则
name: [{ required: true, message: '请输入收件人姓名', trigger: 'blur' }],
address: [{ required: true, message: '请输入地址', trigger: 'change' }],
detail: [
{ required: true, message: '请输入详细地址', trigger: 'blur' }
],
mobile: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{
type: 'string',
pattern: /^1[3|4|5|6|7|8][0-9]{9}$/,
message: '手机号格式出错',
trigger: 'blur'
}
},
watch: {
id: { // 传入的地址id
handler: function (v) {
if (v) {
this.getAddrById(v);
} else {
this.formData = {isDefault: false}
this.$refs.form.resetFields();
}
]
};
const loading = ref(false); // 提交的加载状态
const mapMsg = ref({}); // 地图信息
const formRef = ref(null);
const mapRef = ref(null);
function save () { // 保存地址
formRef.value.validate((valid) => {
if (valid) {
const params = JSON.parse(JSON.stringify(formData.value));
params.consigneeAddressPath = params.address.replace(/\s/g, ',');
delete params.address;
loading.value = true;
if (props.id) {
editMemberAddress(params).then((res) => {
loading.value = false;
if (res.success) {
Message.success('编辑地址成功');
emit('change', true);
hide();
}
}).catch(() => { loading.value = false; });
} else {
newMemberAddress(params).then((res) => {
loading.value = false;
if (res.success) {
Message.success('新增地址成功');
emit('change', true);
hide();
}
}).catch(() => { loading.value = false; });
}
}
},
components: {
multipleMap
});
}
function getAddrById (id) {
// 获取地址详情
getAddrDetail(id).then((res) => {
if (res.success) {
const data = res.result;
data.address = res.result.consigneeAddressPath.replace(/,/g, ' ');
formData.value = data;
}
});
}
function getAddress (val) {
// 获取地图选择信息
if(val.type === 'select'){
const paths = val.data.map(item => item.name).join(',')
const ids = val.data.map(item => item.id).join(',')
formData.value['address'] = paths
formData.value['consigneeAddressIdPath'] = ids
const coord = val.data[val.data.length - 1].center.split(',')
formData.value.lat = coord[1]
formData.value.lon = coord[0]
}else{
formData.value["address"] = val.data.addr;
formData.value["consigneeAddressIdPath"] = val.data.addrId;
formData.value["detail"] = val.data.address;
formData.value.lat = val.data.position.lat;
formData.value.lon = val.data.position.lng;
}
};
}
function show () { // 地址模态框显示
showAddr.value = true;
}
function hide () { // 地址模态框隐藏
showAddr.value = false;
}
defineExpose({ show, hide });
watch(
() => props.id, // 传入的地址id
(v) => {
if (v) {
getAddrById(v);
} else {
formData.value = {isDefault: false}
formRef.value.resetFields();
}
}
);
</script>
<style scoped lang="scss">

View File

@@ -1,24 +1,22 @@
<template>
<!-- 头部广告 -->
<div class="advertising" v-if="show" :style="{'background-color': data.bgColor}">
<div class="advertising" v-if="show && data" :style="{'background-color': data.bgColor}">
<img :src="data.img" class="hover-pointer" @click="linkTo(data.url)"/>
<Icon type="md-close-circle" size="20" @click="show = false" />
<el-icon :size="20" @click="show = false"><CircleClose /></el-icon>
</div>
</template>
<script>
export default {
props: {
data: { // 传入的广告信息
type: Object,
default: null
}
},
data () {
return {
show: true // 是否显示头部广告
}
<script setup>
import { ref } from 'vue';
import { CircleClose } from '@element-plus/icons-vue';
defineProps({
data: { // 传入的广告信息
type: Object,
default: null
}
};
});
const show = ref(true); // 是否显示头部广告
</script>
<style scoped lang="scss">
.advertising {

View File

@@ -1,148 +1,159 @@
<template>
<Card class="_Card" :bordered="false" :dis-hover="true">
<div slot="title" class="cardTitle">
<span :style="{fontSize:`${_Size}px`}">{{_Title}}</span>
<div v-if="_Tabs" class="cardTabs">
<div @click="tabsChange(index)" :class="{active:(isActive==index)}" class="cardTabsItem" :style="{fontSize:`${_Size-2}px`}" v-for="(item,index) in _Tabs"
:key="index">
{{item}}
<el-card class="_Card" shadow="never">
<template #header>
<div class="card-header">
<div class="cardTitle">
<span :style="{ fontSize: `${_Size}px` }">{{ _Title }}</span>
<div v-if="_Tabs" class="cardTabs">
<div
@click="tabsChange(index)"
:class="{ active: isActive == index }"
class="cardTabsItem"
:style="{ fontSize: `${_Size - 2}px` }"
v-for="(item, index) in _Tabs"
:key="index"
>
{{ item }}
</div>
</div>
</div>
<div class="cardExtra" v-if="_More" @click="callBack()">
{{ _More }}
</div>
</div>
</div>
<div slot="extra" class="cardExtra" v-if="_More" @click="callBack()">
{{_More}}
</div>
<div>
</div>
</Card>
</template>
<slot />
</el-card>
</template>
<script>
export default {
name: 'index',
props:
{
_Tabs: { // 可点击的tab栏
type: null,
default: ''
},
// 头部
_Title: { // 标题
type: null,
default: '卡片头部'
},
// 右侧更多
_More: {
type: null,
default: false
},
_Size: { // 文字大小
type: Number,
default: 16
},
// 点击更多触发跳转
_Src: {
type: null,
default: function (val) {
if (this._More) {
return val;
} else {
return false;
}
}
}
},
data () {
return {
isActive: 0 // 已激活tab栏下标
};
<script setup>
import { ref, watch } from "vue";
import { useRouter } from "vue-router";
defineOptions({ name: "index" });
const props = defineProps({
_Tabs: {
type: null,
default: "",
},
methods: {
// 点击右侧的回调
callBack () {
let _this = this;
if (this._Src !== '' || this._Src != null) {
this.$router.push({
path: _this._Src
});
}
},
// 点击tab的回调
tabsChange (index) {
// 处理并返回index
this.isActive = index;
this.$emit('_Change', index);
}
_Title: {
type: null,
default: "卡片头部",
},
_More: {
type: null,
default: false,
},
_Size: {
type: Number,
default: 16,
},
_Src: {
type: null,
default: null,
},
_ActiveTab: {
type: Number,
default: 0,
},
});
const emit = defineEmits(["_Change"]);
const router = useRouter();
const isActive = ref(props._ActiveTab);
watch(
() => props._ActiveTab,
(val) => {
isActive.value = val;
}
};
);
function callBack() {
if (props._Src) {
router.push({ path: props._Src });
}
}
function tabsChange(index) {
isActive.value = index;
emit("_Change", index);
}
</script>
<style scoped lang="scss">
.cardTitle {
display: flex;
cursor: pointer;
}
.active{
color: $theme_color;
position: relative;
&::before{
content: '';
position: absolute;
width: 100%;
height: 3px;
bottom: 0;
left: 0;
background: $theme_color;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.cardTitle {
display: flex;
cursor: pointer;
}
.active {
color: $theme_color;
position: relative;
&::before {
content: "";
position: absolute;
width: 100%;
height: 3px;
bottom: 0;
left: 0;
background: $theme_color;
}
}
.cardTabs {
display: flex;
.cardTabs {
display: flex;
padding: 0 12px;
> .cardTabsItem {
padding: 0 12px;
> .cardTabsItem {
padding: 0 12px;
}
> .cardTabsItem:hover {
color: $theme_color;
}
}
/deep/ .ivu-card, .ivu-card-head, ._Card {
margin-bottom: 20px;
@include white_background_color();
}
/deep/ .ivu-card-head {
position: relative;
padding: 0 14px;
height: 50px;
line-height: 50px;
&::before {
content: '';
width: 3px;
height: 50%;
top: 25%;
background: $theme_color;
position: absolute;
left: 0;
}
}
.cardExtra {
> .cardTabsItem:hover {
color: $theme_color;
cursor: pointer;
}
}
/deep/ .ivu-card-body {
padding: 0 !important;
display: none;
:deep(._Card) {
margin-bottom: 20px;
@include white_background_color();
}
:deep(._Card .el-card__header) {
position: relative;
padding: 0 14px;
height: 50px;
line-height: 50px;
&::before {
content: "";
width: 3px;
height: 50%;
top: 25%;
background: $theme_color;
position: absolute;
left: 0;
}
}
.cardExtra {
color: $theme_color;
cursor: pointer;
}
:deep(._Card .el-card__body) {
padding: 0 !important;
min-height: 0;
}
:deep(._Card .el-card__body:empty) {
display: none;
}
</style>

View File

@@ -4,16 +4,16 @@
<ul>
<li v-for="(goods,goodsIndex) in cartList" :key="goodsIndex">
<div>
<img :src="goods.goodsSku.thumbnail" width="90" height="90" alt="">
<img :src="goods.goodsSku.thumbnail" loading="lazy" decoding="async" width="90" height="90" alt="">
</div>
<div>
<p class="hover-color" @click="linkTo(`/goodsDetail?skuId=${goods.goodsSku.id}&goodsId=${goods.goodsSku.goodsId}`)">{{goods.goodsSku.goodsName}}</p>
<p class="price">{{goods.goodsSku.price | unitPrice('¥')}}<span>&nbsp; x{{goods.num}}</span></p>
<p class="price">{{ $filters.unitPrice(goods.goodsSku.price, '¥') }}<span>&nbsp; x{{goods.num}}</span></p>
</div>
<span class="del hover-color" @click="delGoods(goods.goodsSku.id)">删除</span>
</li>
</ul>
<Button size="large" class="mt_10" type="primary" @click="linkTo('/cart')" long>去购物车结算</Button>
<el-button size="large" class="mt_10" type="primary" @click="linkTo('/cart')" style="width:100%">去购物车结算</el-button>
</div>
<div v-else-if="title === '我的订单'" class="order-con">
@@ -21,15 +21,15 @@
<li v-for="(order,orderIndex) in orderList" :key="orderIndex">
<div class="order-status"><span>{{filterOrderStatus(order.orderStatus)}}</span><span>{{order.createTime}}</span></div>
<div class="goods-img">
<img :src="img.image"
<img :src="img.image" loading="lazy" decoding="async"
@click="linkTo(`/goodsDetail?skuId=${img.skuId}&goodsId=${img.goodsId}`)"
v-for="(img,imgIndex) in order.orderItems"
:key="imgIndex" width="40" height="40" alt="">
</div>
<div class="order-handle"><span>{{ order.flowPrice | unitPrice("¥") }}</span><span class="hover-color" @click="linkTo(`home/OrderDetail?sn=${order.sn}`)">查看订单</span></div>
<div class="order-handle"><span>{{ $filters.unitPrice(order.flowPrice, "¥") }}</span><span class="hover-color" @click="linkTo(`/home/OrderDetail?sn=${order.sn}`)">查看订单</span></div>
</li>
</ul>
<Button type="primary" @click="linkTo('/home/MyOrder')" long>查看全部订单</Button>
<el-button type="primary" @click="linkTo('/home/MyOrder')" style="width:100%">查看全部订单</el-button>
</div>
<div v-else-if="title === '优惠券'" class="coupon-con">
@@ -37,7 +37,7 @@
<li v-for="(coupon, index) in couponList" class="coupon-item" :key="index">
<div class="c-left">
<div>
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{coupon.price | unitPrice}}</span></span>
<span v-if="coupon.couponType === 'PRICE'" class="fontsize_12 global_color">¥<span class="price">{{ $filters.unitPrice(coupon.price) }}</span></span>
<span v-if="coupon.couponType === 'DISCOUNT'" class="fontsize_12 global_color"><span class="price">{{coupon.couponDiscount}}</span>折</span>
<span class="describe">满{{coupon.consumeThreshold}}元可用</span>
</div>
@@ -54,9 +54,9 @@
<div v-else-if="title === '我的足迹'" class="tracks-con">
<ul>
<li v-for="(track,trackIndex) in tracksList" :key="trackIndex">
<img :src="track.thumbnail" :alt="track.thumbnail" @click="linkTo(`/goodsDetail?skuId=${track.id}&goodsId=${track.goodsId}`)" width="100" height="100">
<img :src="track.thumbnail" loading="lazy" decoding="async" :alt="track.thumbnail" @click="linkTo(`/goodsDetail?skuId=${track.id}&goodsId=${track.goodsId}`)" width="100" height="100">
<div @click="addToCart(track.id)">加入购物车</div>
<p class="global_color">{{track.price | unitPrice('¥')}}</p>
<p class="global_color">{{ $filters.unitPrice(track.price, '¥') }}</p>
</li>
</ul>
<div class="hover-color" style="text-align:center;" @click="linkTo('/home/MyTracks')">查看更多>></div>
@@ -64,238 +64,242 @@
<div v-else-if="title === '我的收藏'" class="collect-con">
<ul>
<li v-for="(collect,collectIndex) in collectList" :key="collectIndex">
<img :src="collect.image" :alt="collect.image" @click="linkTo(`/goodsDetail?skuId=${collect.skuId}&goodsId=${collect.goodsId}`)" width="100" height="100">
<img :src="collect.image" loading="lazy" decoding="async" :alt="collect.image" @click="linkTo(`/goodsDetail?skuId=${collect.skuId}&goodsId=${collect.goodsId}`)" width="100" height="100">
<div @click="addToCart(collect.skuId)">加入购物车</div>
<span class="del-icon" @click.stop="cancelCollect(collect.skuId)">
<Icon type="md-trash" />
<el-icon><Delete /></el-icon>
</span>
<p class="global_color">{{collect.price | unitPrice('¥')}}</p>
<p class="global_color">{{ $filters.unitPrice(collect.price, '¥') }}</p>
</li>
</ul>
<div class="hover-color" style="text-align:center;" @click="linkTo('/home/Favorites')">查看更多>></div>
</div>
<Spin v-if="loading" fix></Spin>
<el-skeleton v-if="loading" fix></el-skeleton>
</div>
</template>
<script>
<script setup>
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { Message, Modal } from "@/utils/message";
import { Delete } from '@element-plus/icons-vue';
import {cartGoodsAll, delCartGoods, addCartGoods, cartCount} from '@/api/cart.js'
import { getOrderList } from '@/api/order';
import {couponList, receiveCoupon, tracksList, collectList, cancelCollect} from '@/api/member.js'
export default {
name: 'Drawer',
props: {
title: {
default: '',
type: String
}
},
watch: {
title (val) {
switch (val) {
case '购物车':
this.getCartList()
break;
case '我的订单':
this.getOrderList()
break;
case '我的足迹':
this.getTracksList()
break;
case '优惠券':
this.getCouponList()
break;
case '我的收藏':
this.getCollectList()
break;
}
}
},
data () {
return {
loading: false, // 控制spin显隐
cartList: [], // 购物车列表
couponList: [], // 优惠券列表
orderList: [], // 订单列表
collectList: [], // 收藏列表
tracksList: [], // 足迹列表
orderStatusList: [ // 订单状态
{
name: '未付款',
status: 'UNPAID'
},
{
name: '已付款',
status: 'PAID'
},
{
name: '待发货',
status: 'UNDELIVERED'
},
{
name: '已发货',
status: 'DELIVERED'
},
{
name: '已完成',
status: 'COMPLETED'
},
{
name: '待核验',
status: 'TAKE'
},
{
name: '已取消',
status: 'CANCELLED'
}
]
};
},
methods: {
getCartList () { // 获取购物车列表
this.loading = true
cartGoodsAll().then(res => {
this.loading = false
this.cartList = res.result.skuList
})
},
// 删除商品
delGoods (id) {
delCartGoods({ skuIds: id }).then((res) => {
if (res.success) {
this.$Message.success('删除成功');
this.getCartList();
cartCount().then(res => {
this.$store.commit('SET_CARTNUM', res.result)
this.Cookies.setItem('cartNum', res.result)
})
} else {
this.$Message.error(res.message);
}
});
},
filterOrderStatus (status) { // 获取订单状态中文
const ob = this.orderStatusList.filter(e => { return e.status === status });
return ob[0].name
},
receive (item) { // 领取优惠券
receiveCoupon(item.id).then(res => {
if (res.success) {
this.$Modal.confirm({
title: '领取优惠券',
content: '<p>优惠券领取成功,可到我的优惠券页面查看</p>',
okText: '我的优惠券',
cancelText: '立即使用',
onOk: () => {
this.$router.push('/home/Coupons')
},
onCancel: () => {
if (item.storeId !== '0') {
this.$router.push({path: '/merchant', query: {id: item.storeId}})
} else {
if (item.scopeType === 'PORTION_GOODS_CATEGORY') {
this.$router.push({path: '/goodsList', query: {categoryId: item.scopeId}})
} else {
this.$router.push({path: '/goodsList'})
}
}
}
});
}
})
},
useScope (type, storeName) { // 判断优惠券使用范围
let shop = '平台';
let goods = '全部商品'
if (storeName !== 'platform') shop = storeName
switch (type) {
case 'ALL':
goods = '全部商品'
break;
case 'PORTION_GOODS':
goods = '部分商品'
break;
case 'PORTION_GOODS_CATEGORY':
goods = '部分分类商品'
break;
}
return `${shop}${goods}可用`
},
addToCart (id) { // 添加商品到购物车
const params = {
num: 1,
skuId: id
}
this.loading = true;
addCartGoods(params).then(res => {
this.loading = false;
if (res.success) {
this.$Message.success('商品已成功添加到购物车')
} else {
this.$Message.warning(res.message);
}
}).catch(() => { this.loading = false });
},
getCouponList () { // 获取优惠券列表
// this.loading = true;
const params = {
pageNumber: 1,
pageSize: 10
}
couponList(params).then(res => {
this.loading = false
if (res.success) {
this.couponList = res.result.records
}
}).catch(() => { this.loading = false })
},
getOrderList () { // 获取订单列表
this.loading = true
const params = {
pageNumber: 1,
pageSize: 10,
tag: 'ALL'
}
getOrderList(params).then(res => {
this.loading = false
if (res.success) {
this.orderList = res.result.records;
}
});
},
getCollectList () { // 获取收藏列表
const params = {
pageNumber: 1,
pageSize: 10,
type: 'GOODS'
}
this.loading = true
collectList(params).then(res => {
this.loading = false
this.collectList = res.result.records
})
},
cancelCollect (id) { // 取消商品收藏
cancelCollect('GOODS', id).then(res => {
if (res.success) {
this.$Message.success('取消收藏成功')
this.getCollectList();
}
})
},
getTracksList () { // 获取足迹列表
const params = {
pageNumber: 1,
pageSize: 20
}
this.loading = true
tracksList(params).then(res => {
this.tracksList = res.result.records
this.loading = false
}).catch(() => { this.loading = false })
}
import { getOrderList as fetchOrderList } from '@/api/order';
import {couponList as fetchCouponList, receiveCoupon, tracksList as fetchTracksList, collectList as fetchCollectList, cancelCollect as apiCancelCollect} from '@/api/member.js'
import storage from '@/plugins/storage.js'
import { useAppStore } from '@/stores/app'
defineOptions({ name: 'Drawer' })
const props = defineProps({
title: {
default: '',
type: String
}
};
})
const router = useRouter()
const appStore = useAppStore()
const loading = ref(false) // 控制spin显隐
const cartList = ref([]) // 购物车列表
const couponList = ref([]) // 优惠券列表
const orderList = ref([]) // 订单列表
const collectList = ref([]) // 收藏列表
const tracksList = ref([]) // 足迹列表
const orderStatusList = ref([ // 订单状态
{
name: '未付款',
status: 'UNPAID'
},
{
name: '已付款',
status: 'PAID'
},
{
name: '待发货',
status: 'UNDELIVERED'
},
{
name: '已发货',
status: 'DELIVERED'
},
{
name: '已完成',
status: 'COMPLETED'
},
{
name: '待核验',
status: 'TAKE'
},
{
name: '已取消',
status: 'CANCELLED'
}
])
watch(() => props.title, (val) => {
switch (val) {
case '购物车':
getCartList()
break;
case '我的订单':
getOrderList()
break;
case '我的足迹':
getTracksList()
break;
case '优惠券':
getCouponList()
break;
case '我的收藏':
getCollectList()
break;
}
})
function getCartList () { // 获取购物车列表
loading.value = true
cartGoodsAll().then(res => {
loading.value = false
cartList.value = res.result.skuList
})
}
// 删除商品
function delGoods (id) {
delCartGoods({ skuIds: id }).then((res) => {
if (res.success) {
Message.success('删除成功');
getCartList();
cartCount().then(res => {
appStore.setCartNum(res.result)
storage.setItem('cartNum', res.result)
})
} else {
Message.error(res.message);
}
});
}
function filterOrderStatus (status) { // 获取订单状态中文
const ob = orderStatusList.value.filter(e => { return e.status === status });
return ob[0].name
}
function receive (item) { // 领取优惠券
receiveCoupon(item.id).then(res => {
if (res.success) {
Modal.confirm({
title: '领取优惠券',
content: '<p>优惠券领取成功,可到我的优惠券页面查看</p>',
okText: '我的优惠券',
cancelText: '立即使用',
onOk: () => {
router.push('/home/Coupons')
},
onCancel: () => {
if (item.storeId !== '0') {
router.push({path: '/merchant', query: {id: item.storeId}})
} else {
if (item.scopeType === 'PORTION_GOODS_CATEGORY') {
router.push({path: '/goodsList', query: {categoryId: item.scopeId}})
} else {
router.push({path: '/goodsList'})
}
}
}
});
}
})
}
function useScope (type, storeName) { // 判断优惠券使用范围
let shop = '平台';
let goods = '全部商品'
if (storeName !== 'platform') shop = storeName
switch (type) {
case 'ALL':
goods = '全部商品'
break;
case 'PORTION_GOODS':
goods = '部分商品'
break;
case 'PORTION_GOODS_CATEGORY':
goods = '部分分类商品'
break;
}
return `${shop}${goods}可用`
}
function addToCart (id) { // 添加商品到购物车
const params = {
num: 1,
skuId: id
}
loading.value = true;
addCartGoods(params).then(res => {
loading.value = false;
if (res.success) {
Message.success('商品已成功添加到购物车')
} else {
Message.warning(res.message);
}
}).catch(() => { loading.value = false });
}
function getCouponList () { // 获取优惠券列表
// loading.value = true;
const params = {
pageNumber: 1,
pageSize: 10
}
fetchCouponList(params).then(res => {
loading.value = false
if (res.success) {
couponList.value = res.result.records
}
}).catch(() => { loading.value = false })
}
function getOrderList () { // 获取订单列表
loading.value = true
const params = {
pageNumber: 1,
pageSize: 10,
tag: 'ALL'
}
fetchOrderList(params).then(res => {
loading.value = false
if (res.success) {
orderList.value = res.result.records;
}
});
}
function getCollectList () { // 获取收藏列表
const params = {
pageNumber: 1,
pageSize: 10,
type: 'GOODS'
}
loading.value = true
fetchCollectList(params).then(res => {
loading.value = false
collectList.value = res.result.records
})
}
function cancelCollect (id) { // 取消商品收藏
apiCancelCollect('GOODS', id).then(res => {
if (res.success) {
Message.success('取消收藏成功')
getCollectList();
}
})
}
function getTracksList () { // 获取足迹列表
const params = {
pageNumber: 1,
pageSize: 20
}
loading.value = true
fetchTracksList(params).then(res => {
tracksList.value = res.result.records
loading.value = false
}).catch(() => { loading.value = false })
}
</script>
<style scoped lang="scss">

View File

@@ -3,102 +3,99 @@
<div class="wrapper" :style="{right:handleDrawer ? '300px' : '0px'}">
<div class="barItem" @mouseenter="showCartNum(item)" @click="clickBar(item)" v-for="(item,index) in resetConfig.menuList" :key="index">
<Tooltip placement="left" :content="item.title">
<Icon size="20" :type="item.icon"/>
<el-tooltip placement="left" :content="item.title">
<el-icon :size="20"><component :is="resolveIcon(item.icon)" /></el-icon>
<p class="barTitle" v-if="item.titleShow"> {{item.title}}</p>
<div class="circle" v-if="item.title === '购物车'">
{{cartNum < 100 ? cartNum : 99}}
{{ Number(cartNum) > 99 ? '99+' : cartNum }}
</div>
</Tooltip>
</el-tooltip>
</div>
</div>
<Drawer width="300" class="popup" :title="drawerData.title" :mask="resetConfig.mask" :closable="resetConfig.closable"
<el-drawer width="300" class="popup" :title="drawerData.title" :modal="resetConfig.mask" :show-close="resetConfig.closable"
v-model="handleDrawer">
<drawerPage :title="drawerData.title" />
</Drawer>
</el-drawer>
</div>
</template>
<script>
import Storage from '@/plugins/storage.js';
<script setup>
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Modal } from "@/utils/message";
import storage from '@/plugins/storage.js';
import Configuration from './config';
import drawerPage from './Drawer'
import {cartCount} from '@/api/cart.js'
export default {
name: 'Main',
data () {
return {
resetConfig: Configuration, // 菜单项
handleDrawer: false, // 是否可展开
drawerData: '' // 菜单基础数据
}
},
components: {drawerPage},
computed: {
// 用户信息
userInfo () {
return Storage.getItem('userInfo');
},
// 购物车商品数量
cartNum () {
return this.$store.state.cartNum
}
},
methods: {
showCartNum (item) { // 获取购物车数量
if (this.userInfo && item.title === '购物车') {
this.getCartList()
}
},
clickBar (val) { // tabbar点击操作
if (!this.userInfo) {
this.$Modal.confirm({
title: '请登录',
content: '<p>请登录后执行此操作</p>',
okText: '立即登录',
cancelText: '继续浏览',
onOk: () => {
this.$router.push({
path: '/login',
query: {
rePath: this.$router.history.current.path,
query: JSON.stringify(this.$router.history.current.query)
}
});
}
import { resolveIcon } from '@/utils/iconMap'
import { useAppStore } from '@/stores/app'
defineOptions({ name: 'Main' })
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const resetConfig = ref(Configuration) // 菜单项
const handleDrawer = ref(false) // 是否可展开
const drawerData = ref('') // 菜单基础数据
// 用户信息
const userInfo = computed(() => {
return storage.getItem('userInfo');
})
// 购物车商品数量
const cartNum = computed(() => appStore.cartNum)
function showCartNum (item) { // 获取购物车数量
if (userInfo.value && item.title === '购物车') {
getCartList()
}
}
function clickBar (val) { // tabbar点击操作
if (!userInfo.value) {
Modal.confirm({
title: '请登录',
content: '请登录后执行此操作',
okText: '立即登录',
cancelText: '继续浏览',
onOk: () => {
router.push({
path: '/login',
query: {
rePath: route.path,
query: JSON.stringify(route.query || {}),
},
});
} else {
if (val.display) {
this.handleDrawer = true
this.drawerData = val
} else {
this.handleDrawer = false
switch (val.title) {
case '会员中心':
this.openBlank('/home')
break;
case '我的资产':
this.openBlank('/home/MoneyManagement')
break;
}
}
}
},
openBlank (path) { // 新页面打开地址
let routerUrl = this.$router.resolve({
path: path
})
window.open(routerUrl.href, '_blank')
},
getCartList () { // 获取购物车列表
cartCount().then(res => {
this.$store.commit('SET_CARTNUM', res.result)
this.Cookies.setItem('cartNum', res.result)
})
});
} else {
if (val.display) {
handleDrawer.value = true
drawerData.value = val
} else {
handleDrawer.value = false
switch (val.title) {
case '会员中心':
router.push('/home')
break;
case '我的资产':
router.push('/home/MoneyManagement')
break;
}
}
}
}
function getCartList () { // 获取购物车列表
cartCount().then(res => {
appStore.setCartNum(res.result)
storage.setItem('cartNum', res.result)
})
}
</script>
<style scoped lang="scss">
@@ -128,11 +125,15 @@ export default {
}
.circle {
width: 20px;
min-width: 20px;
height: 20px;
border-radius: 50%;
padding: 0 4px;
border-radius: 10px;
color: #fff;
background: $theme_color;
font-size: 12px;
line-height: 20px;
box-sizing: border-box;
}
.wrapper {
@@ -147,11 +148,11 @@ export default {
justify-content: center;
flex-direction: column;
}
/deep/.popup .ivu-drawer-body{
padding: 0!important;
:deep(.popup .el-drawer__body) {
padding: 0 !important;
background-color: #eee;
}
/deep/.popup .ivu-drawer-wrap{
:deep(.popup) {
z-index: 3001;
}
</style>

View File

@@ -1,42 +1,30 @@
# 右侧侧边栏组件
> 本组件依赖于iview的组件基础上进行封装
>项目结构
>* Main -- 组件用于挂在右侧的横栏
>* drawer -- 右侧横栏的内容
>* config -- 用于设置大小
基于 Element Plus `el-drawer` 封装的右侧悬浮栏。
#### config设置
```
## 项目结构
- `Main.vue` — 挂载右侧横栏的容器
- `Drawer.vue` — 横栏展开后的内容面板
- `config.js` — 尺寸与菜单配置
## config 设置
```js
/**
menuList // 组件的menu
display //是否显示此menu
badge //显示徽标数
titleShow //是否显示title
*/
//实例代码
width : 50, //bar的大小
menuList:[
{
icon
}
]
* menuList 菜单项列表
* display 是否显示该项
* badge 徽标数字
* titleShow 是否显示标题
*/
width: 50,
menuList: [{ icon: '...' }]
```
## 功能模块
账户信息、购物车、我的订单、优惠券、我的资产、我的足迹、我的收藏、邮箱订阅
> 账户信息
> 购物车
> 我的订单
> 优惠券
> 我的资产
> 我的足迹
> 我的收藏
> 邮箱订阅
## 使用
## 如何使用
1.
在页面中注册全局组件 `<drawer />` 即可(已在 `global.js` 中注册)。

View File

@@ -11,20 +11,19 @@
</div>
</template>
<script>
export default {
name: 'Main',
props: {
_Title: { // 描述内容
type: null,
default: '暂无更多'
},
_Size: { // 图片大小
type: Number,
default: 150
}
<script setup>
defineOptions({ name: 'Main' })
defineProps({
_Title: { // 描述内容
type: null,
default: '暂无更多'
},
_Size: { // 图片大小
type: Number,
default: 150
}
};
})
</script>
<style scoped lang="scss">

View File

@@ -1,81 +1,111 @@
<template>
<div class="shadow-box">
<div class="shadow-item flex flex-a-c flex-j-c" v-for="(item,index) in iconList" @click="handleClickIcon(item)" :key="index">
<img class="icon" :src="require(`@/assets/iconfont/${item.icon}.png`)">
<img class="icon" :src="getIconUrl(item.icon)">
<div class="shadow-label">{{item.label}}</div>
<div class="line" v-if="iconList.length-1 !=index"></div>
</div>
</div>
</template>
<script>
export default {
name: "fixed-index",
data(){
return{
iconList:[
{
icon:"user",
label:"会员中心",
path:"/home"
},
{
icon:"carts",
label:"购物车",
path:"/cart"
},
{
icon:"notification",
label:"消息",
path:"/home/MsgList"
},
{
icon:"collage",
label:"收藏",
path:"/home/Favorites"
},
{
icon:"back",
label:"顶部",
path:"back"
},
],
isScrolling:false,
timeoutId:"",
<script setup>
import { onMounted, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { getIconUrl } from "@/assets/iconfont/iconMap";
import storage from "@/plugins/storage.js";
defineOptions({ name: "fixed-index" });
const route = useRoute();
const router = useRouter();
const iconList = ref([
{
icon:"user",
label:"会员中心",
path:"/home",
requireLogin: true,
},
{
icon:"carts",
label:"购物车",
path:"/cart",
requireLogin: true,
},
{
icon:"notification",
label:"消息",
path:"/home/MsgList",
requireLogin: true,
},
{
icon:"collage",
label:"收藏",
path:"/home/Favorites",
requireLogin: true,
},
{
icon:"back",
label:"顶部",
path:"back"
},
]);
let isScrolling = false;
let timeoutId = "";
onMounted(() => {
// 添加滚轮事件监听器
window.addEventListener('wheel', function() {
if (isScrolling) {
// 如果页面正在滚动,则取消定时器并设置 isScrolling 为 false
window.cancelAnimationFrame(timeoutId);
isScrolling = false;
}
},
mounted() {
const vm = this
// 添加滚轮事件监听器
window.addEventListener('wheel', function() {
if (vm.isScrolling) {
// 如果页面正在滚动,则取消定时器并设置 isScrolling 为 false
window.cancelAnimationFrame(vm.timeoutId);
vm.isScrolling = false;
}
});
});
},
methods:{
scrollToTop() {
const c = document.documentElement.scrollTop || document.body.scrollTop;
if (c > 0) {
this.timeoutId = window.requestAnimationFrame(this.scrollToTop);
window.scrollTo(0, c - c / 4);
this.isScrolling = true;
}else {
this.isScrolling = false;
}
},
handleClickIcon(val){
if(val.path === 'back'){
this.scrollToTop()
}else{
this.$router.push(val.path)
}
});
function scrollToTop() {
const c = document.documentElement.scrollTop || document.body.scrollTop;
if (c > 0) {
timeoutId = window.requestAnimationFrame(scrollToTop);
window.scrollTo(0, c - c / 4);
isScrolling = true;
}else {
isScrolling = false;
}
}
function isLoggedIn () {
const rawUserInfo = storage.getItem('userInfo');
if (rawUserInfo) {
try {
return !!JSON.parse(rawUserInfo).username;
} catch {
return false;
}
}
return !!storage.getItem('accessToken');
}
function goLogin (rePath) {
router.push({
path: '/login',
query: {
rePath,
query: JSON.stringify(route.query || {}),
},
});
}
function handleClickIcon (val) {
if (val.path === 'back') {
scrollToTop();
return;
}
if (val.requireLogin && !isLoggedIn()) {
goLogin(val.path);
return;
}
router.push(val.path);
}
</script>

View File

@@ -68,34 +68,29 @@
</div>
</template>
<script>
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import config from '@/config'
export default {
name: "Footer",
data() {
return {
config:require('@/config'),
guideArr: [
// 导航链接
["购物指南", "购物流程", "会员介绍", "生活旅行", "常见问题"],
["配送方式", "上门自提", "配送查询", "收取标准", "物流规则"],
["支付方式", "在线支付", "公司转账", "余额支付", "积分支付"],
["售后服务", "售后政策", "退款说明", "返修/退货", "取消订单"],
],
moreLink: ["关于我们", "联系我们", "联系客服", "商家帮助", "隐私政策"], // 更多链接
year: new Date().getFullYear(), // 当前年份
};
},
methods: {
goArticle() {
// 跳转文章页
let routeUrl = this.$router.resolve({
path: "/article",
});
window.open(routeUrl.href, "_blank");
},
},
};
defineOptions({ name: 'Footer' })
const router = useRouter()
const guideArr = ref([
// 导航链接
["购物指南", "购物流程", "会员介绍", "生活旅行", "常见问题"],
["配送方式", "上门自提", "配送查询", "收取标准", "物流规则"],
["支付方式", "在线支付", "公司转账", "余额支付", "积分支付"],
["售后服务", "售后政策", "退款说明", "返修/退货", "取消订单"],
])
const moreLink = ref(["关于我们", "联系我们", "联系客服", "商家帮助", "隐私政策"]) // 更多链接
const year = ref(new Date().getFullYear()) // 当前年份
function goArticle() {
// 跳转文章页
router.push("/article")
}
</script>
<style scoped lang="scss">

View File

@@ -1,54 +1,21 @@
// 全局组件配置
import empty from "./empty/Main.vue";
import drawer from "./drawer/Main.vue";
import Header from "@/components/header/Header.vue";
import FixedTopPage from "@/components/advertising/FixedTop.vue";
import Footer from "@/components/footer/Footer.vue";
import Search from "@/components/Search.vue";
import card from "@/components/card/index.vue";
import cateNav from "@/components/nav/CateNav.vue";
import UserCenterLayout from "@/components/userCenter/Layout.vue";
import empty from './empty/Main' // 空状态组件
import drawer from './drawer/Main' // 右侧bar
import Header from '@/components/header/Header'; // 头部组件
import FixedTopPage from '@/components/advertising/FixedTop'; // 顶部广告
import Footer from '@/components/footer/Footer'; // 底部栏
import Search from '@/components/Search' // 搜索框
import card from '@/components/card' // 个人中心 卡片
import cateNav from '@/components/nav/CateNav' // 个人中心 卡片
empty.install = function (Vue) {
Vue.component('empty', empty);
};
drawer.install = function (Vue) {
Vue.component('drawer', drawer);
};
Header.install = function (Vue) {
Vue.component('BaseHeader', Header);
};
FixedTopPage.install = function (Vue) {
Vue.component('FixedTopPage', FixedTopPage);
};
Footer.install = function (Vue) {
Vue.component('BaseFooter', Footer);
};
Search.install = function (Vue) {
Vue.component('Search', Search);
};
card.install = function (Vue) {
Vue.component('card', card)
}
cateNav.install = function (Vue) {
Vue.component('cateNav', cateNav)
}
// 引用本js中所有的组件
export function InstallAll (Vue) {
Vue.use(empty)
Vue.use(drawer)
Vue.use(Header)
Vue.use(FixedTopPage)
Vue.use(Footer)
Vue.use(Search)
Vue.use(card)
Vue.use(cateNav)
export function registerGlobalComponents(app) {
app.component("empty", empty);
app.component("drawer", drawer);
app.component("BaseHeader", Header);
app.component("FixedTopPage", FixedTopPage);
app.component("BaseFooter", Footer);
app.component("Search", Search);
app.component("card", card);
app.component("UserCenterLayout", UserCenterLayout);
app.component("cateNav", cateNav);
}

View File

@@ -0,0 +1,122 @@
<template>
<div class="goods-thumb" ref="rootEl" :class="{ 'is-loaded': loaded, 'is-error': error }">
<img v-if="currentSrc" :src="currentSrc" :alt="alt" @load="loaded = true" @error="onError" />
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
defineOptions({ name: "GoodsThumb" });
const props = defineProps({
src: {
type: String,
default: "",
},
alt: {
type: String,
default: "",
},
});
const rootEl = ref(null);
const currentSrc = ref("");
const loaded = ref(false);
const error = ref(false);
let observer = null;
watch(
() => props.src,
() => {
observe();
}
);
function disconnect() {
if (observer) {
observer.disconnect();
observer = null;
}
}
function observe() {
disconnect();
currentSrc.value = "";
loaded.value = false;
error.value = false;
if (!props.src) {
error.value = true;
return;
}
if (typeof IntersectionObserver === "undefined") {
currentSrc.value = props.src;
return;
}
observer = new IntersectionObserver(
(entries) => {
if (!entries.some((entry) => entry.isIntersecting)) return;
currentSrc.value = props.src;
disconnect();
},
{ rootMargin: "160px 0px" }
);
observer.observe(rootEl.value);
}
function onError() {
error.value = true;
loaded.value = false;
currentSrc.value = "";
}
onMounted(() => {
observe();
});
onBeforeUnmount(() => {
disconnect();
});
</script>
<style scoped lang="scss">
.goods-thumb {
position: relative;
width: 100%;
aspect-ratio: 1;
overflow: hidden;
background: #f3f3f3;
}
.goods-thumb::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, #f3f3f3 25%, #e8e8e8 37%, #f3f3f3 63%);
background-size: 400% 100%;
animation: goods-thumb-loading 1.2s ease infinite;
}
.goods-thumb.is-loaded::before,
.goods-thumb.is-error::before {
display: none;
}
.goods-thumb img {
position: relative;
z-index: 1;
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
@keyframes goods-thumb-loading {
0% {
background-position: 100% 0;
}
100% {
background-position: 0 0;
}
}
</style>

View File

@@ -1,9 +1,14 @@
<template>
<div>
<div class="wrapper" v-if="type === 'goodsDetail'">
<div class="wr-l"><Icon size="23" type="ios-alarm-outline" /> 秒杀活动</div>
<div class="wr-l"><el-icon :size="23"><AlarmClock /></el-icon> 秒杀活动</div>
<div class="count-down" v-if="end === ''">
<p>倒计时:</p><span>{{ hours }}</span><span>{{ minutes }}</span><span>{{ seconds }}</span>
<span class="count-down-label">倒计时:</span>
<span class="count-down-num">{{ hours }}</span>
<span class="count-down-colon">:</span>
<span class="count-down-num">{{ minutes }}</span>
<span class="count-down-colon">:</span>
<span class="count-down-num">{{ seconds }}</span>
</div>
<div v-else>{{end}}</div>
</div>
@@ -13,58 +18,57 @@
</span>
</div>
</template>
<script>
export default {
props: {
time: { // 传入的初始时间
default: 1718977559428
},
type: { // 区分是在详情还是购物车调用
default: 'goodsDetail', // 设置两个值,goodsDetail和cart,样式不同
type: String
}
<script setup>
import { onMounted, ref } from 'vue';
import { AlarmClock } from '@element-plus/icons-vue';
const props = defineProps({
time: { // 传入的初始时间
default: 1718977559428
},
data () {
return {
end: '', // 结束状态
hours: '', // 小时
minutes: '', // 分钟
seconds: '', // 秒
interval: '' // 定时器
};
},
mounted () {
this.init()
},
methods: {
countDown (val) { // 倒计时方法
function addZero (i) {
return i < 10 ? '0' + i : i + '';
}
var nowtime = new Date();
var endtime = new Date(val);
var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000);
var h = parseInt((lefttime / (60 * 60)) % 24);
var m = parseInt((lefttime / 60) % 60);
var s = parseInt(lefttime % 60);
h = addZero(h);
m = addZero(m);
s = addZero(s);
this.hours = h;
this.minutes = m
this.seconds = s;
if (lefttime <= 0) {
this.end = `活动已结束`;
clearInterval(this.interval)
}
},
init () { // 初始化
this.interval = setInterval(() => {
this.countDown(this.time);
}, 1000);
}
type: { // 区分是在详情还是购物车调用
default: 'goodsDetail', // 设置两个值,goodsDetail和cart,样式不同
type: String
}
};
});
const end = ref(''); // 结束状态
const hours = ref(''); // 小时
const minutes = ref(''); // 分钟
const seconds = ref(''); // 秒
let interval = null; // 定时器
function countDown (val) { // 倒计时方法
function addZero (i) {
return i < 10 ? '0' + i : i + '';
}
var nowtime = new Date();
var endtime = new Date(val);
var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000);
var h = parseInt((lefttime / (60 * 60)) % 24);
var m = parseInt((lefttime / 60) % 60);
var s = parseInt(lefttime % 60);
h = addZero(h);
m = addZero(m);
s = addZero(s);
hours.value = h;
minutes.value = m
seconds.value = s;
if (lefttime <= 0) {
end.value = `活动已结束`;
clearInterval(interval)
}
}
function init () { // 初始化
interval = setInterval(() => {
countDown(props.time);
}, 1000);
}
onMounted(() => {
init()
});
</script>
<style scoped lang="scss">
.cart-promotion{
@@ -87,36 +91,35 @@ export default {
font-size: 13px;
}
.count-down {
margin-right: -20px;
p{
float: left;
line-height: 20px;
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
line-height: 1;
.count-down-label {
flex-shrink: 0;
white-space: nowrap;
}
> span {
position: relative;
float: left;
width: 20px;
.count-down-num {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
text-align: center;
padding: 0 2px;
background-color: #2f3430;
margin-right: 20px;
color: white;
color: #fff;
font-size: 14px;
line-height: 20px;
&::after {
content: ":";
display: block;
position: absolute;
right: -20px;
font-weight: bolder;
font-size: 14px;
width: 20px;
height: 100%;
top: 0;
}
line-height: 1;
border-radius: 2px;
}
> span:last-child::after {
content: "";
.count-down-colon {
font-weight: bold;
font-size: 14px;
line-height: 1;
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,495 @@
<template>
<aside class="store-side" v-if="storeId">
<div class="store-card">
<div class="store-head" @click="goStore">
<img
v-if="store.storeLogo && !logoError"
class="store-logo"
:src="store.storeLogo"
loading="lazy"
decoding="async"
:alt="storeName"
@error="logoError = true"
/>
<span v-else class="store-logo store-logo-fallback">{{ storeInitial }}</span>
<div class="store-head-text">
<p class="store-name">
<span v-if="isSelfOperated" class="store-self">自营</span>
<span class="store-name-text">{{ storeName }}</span>
</p>
</div>
</div>
<div class="store-scores">
<div class="score-item">
<span class="score-label">描述</span>
<span class="score-value">{{ formatScore(store.descriptionScore) }}</span>
</div>
<div class="score-item">
<span class="score-label">服务</span>
<span class="score-value">{{ formatScore(store.serviceScore) }}</span>
</div>
<div class="score-item">
<span class="score-label">物流</span>
<span class="score-value">{{ formatScore(store.deliveryScore) }}</span>
</div>
</div>
<div class="store-actions">
<button type="button" class="store-btn" @click="contact">联系客服</button>
<button type="button" class="store-btn" :class="{ 'is-on': collected }" @click="toggleCollect">
{{ collected ? "已收藏" : "收藏店铺" }}
</button>
</div>
<button type="button" class="enter-store" @click="goStore">进店逛逛</button>
</div>
<div class="recommend-card" v-if="recommends.length">
<p class="recommend-title">店铺推荐</p>
<div
class="recommend-item"
v-for="item in recommends"
:key="item.id"
@click="goGoods(item)"
>
<img class="recommend-img" :src="item.thumbnail || item.small" loading="lazy" decoding="async" :alt="item.goodsName" />
<div class="recommend-info">
<p class="recommend-name">{{ item.goodsName }}</p>
<div class="recommend-meta">
<p class="recommend-price">{{ $filters.unitPrice(item.price, "¥") }}</p>
<span class="recommend-sales">销量 {{ item.buyCount || 0 }}</span>
</div>
</div>
</div>
</div>
</aside>
</template>
<script setup>
import { computed, ref, watch } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Message, Modal } from "@/utils/message";
import { goodsList } from "@/api/goods";
import { getStoreById } from "@/api/shopentry";
import { cancelStoreCollect, collectStore, isStoreCollection } from "@/api/member";
import { getIMDetail } from "@/api/common";
import { getMemberMsg } from "@/api/login";
import storage from "@/plugins/storage.js";
defineOptions({ name: "StoreSide" });
const props = defineProps({
storeId: {
type: [String, Number],
default: "",
},
goodsId: {
type: [String, Number],
default: "",
},
skuId: {
type: [String, Number],
default: "",
},
storeNameFallback: {
type: String,
default: "",
},
});
const route = useRoute();
const router = useRouter();
const store = ref({});
const logoError = ref(false);
const collected = ref(false);
const recommends = ref([]);
// 原 talkIm mixin 的 IM 链接
const IMLink = ref("");
const storeName = computed(() => {
return store.value.storeName || props.storeNameFallback || "店铺";
});
const storeInitial = computed(() => {
return storeName.value.slice(0, 1);
});
// 自营标记后端可能是 true、1 或 "1"
const isSelfOperated = computed(() => {
const flag = store.value.selfOperated;
return flag === true || flag === 1 || flag === "1";
});
watch(
() => props.storeId,
(id) => {
if (!id) return;
logoError.value = false;
loadStore(id);
loadRecommend(id);
loadCollected(id);
},
{ immediate: true }
);
// 没有评分或小于等于 0 时按满分展示
function formatScore(value) {
const score = Number(value);
if (!Number.isFinite(score) || score <= 0) return "5.00";
return score.toFixed(2);
}
function loadStore(id) {
getStoreById(id).then((res) => {
if (res.success && res.result) store.value = res.result;
}).catch((err) => {
console.warn('获取店铺信息失败', err);
});
}
// 按销量取店内商品,去掉当前商品,最多 4 个
function loadRecommend(id) {
goodsList({
storeId: id,
pageNumber: 1,
pageSize: 8,
sort: "buyCount",
order: "desc",
}).then((res) => {
const records = (res.success && res.result && res.result.records) || [];
recommends.value = records
.filter((item) => String(item.goodsId) !== String(props.goodsId))
.slice(0, 4);
}).catch(() => {
// 推荐商品为可选数据,加载失败时静默
});
}
// 同时有 token 和用户信息才查收藏,避免残留 userInfo 触发登录跳转
function loadCollected(id) {
if (!storage.getItem("accessToken") || !storage.getItem("userInfo")) return;
isStoreCollection("STORE", id).then((res) => {
if (res.success && res.result) collected.value = true;
}).catch(() => {
// 收藏状态为可选数据,失败时静默
});
}
function goStore() {
router.push({ path: "/merchant", query: { id: props.storeId } });
}
// 推荐商品新开详情页,店铺页仍在当前页打开
function goGoods(item) {
const href = router.resolve({
path: "/goodsDetail",
query: { skuId: item.id, goodsId: item.goodsId },
}).href;
window.open(href, "_blank");
}
// 原 talkIm mixin 的登录提示
function promptLogin() {
Modal.confirm({
title: "温馨提示",
content: "请登录后执行此操作",
okText: "立即登录",
cancelText: "取消",
onOk: () => {
router.push({
path: "/login",
query: {
rePath: route.path,
query: JSON.stringify(route.query || {}),
},
});
},
});
}
// 获取im信息(原 talkIm mixin)
async function getIMDetailMethods() {
let res = await getIMDetail();
if (res.success) {
IMLink.value = res.result;
}
}
// 跳转im客服(原 talkIm mixin;本组件无 storeMsg,mixin 的 storeMsg?.storeId 兜底恒为 undefined,等价于直接用传入的 id)
async function IMService(id, goodsId, skuId) {
if (!storage.getItem("accessToken")) {
promptLogin();
return;
}
try {
await getIMDetailMethods();
const userInfo = await getMemberMsg();
if (!userInfo.success) {
promptLogin();
return;
}
const accessToken = storage.getItem("accessToken");
const storeId = id;
const baseUrl = `${IMLink.value}?token=${accessToken}&id=${storeId}`;
if (goodsId && skuId) {
window.open(`${baseUrl}&goodsId=${goodsId}&skuId=${skuId}`);
} else {
window.open(baseUrl);
}
} catch {
// token 过期等场景由 request 拦截器处理;未登录时避免未捕获异常
if (!storage.getItem("accessToken")) {
promptLogin();
}
}
}
function contact() {
IMService(props.storeId, props.goodsId, props.skuId);
}
// 未登录先去登录,并带回当前商品页
async function toggleCollect() {
if (!storage.getItem("accessToken") || !storage.getItem("userInfo")) {
router.push({
path: "/login",
query: {
rePath: route.path,
query: JSON.stringify(route.query || {}),
},
});
return;
}
if (collected.value) {
const cancel = await cancelStoreCollect("STORE", props.storeId);
if (cancel.success) {
collected.value = false;
Message.success("已取消收藏");
}
return;
}
const collect = await collectStore("STORE", props.storeId);
if (collect.code === 200) {
collected.value = true;
Message.success("收藏店铺成功,可以前往个人中心我的收藏查看");
}
}
</script>
<style scoped lang="scss">
.store-side {
width: 260px;
flex-shrink: 0;
position: sticky;
top: 16px;
}
.store-card,
.recommend-card {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 4px;
}
.store-card {
padding: 16px;
}
.store-head {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
cursor: pointer;
}
.store-logo {
width: 64px;
height: 64px;
border-radius: 4px;
object-fit: cover;
border: 1px solid #f0f0f0;
flex-shrink: 0;
background: #fafafa;
}
.store-logo-fallback {
display: flex;
align-items: center;
justify-content: center;
color: #fff;
background: $theme_color;
font-size: 18px;
font-weight: 600;
}
.store-head-text {
width: 100%;
text-align: center;
}
.store-name {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin: 0;
color: #171A26;
font-size: 15px;
font-weight: 600;
line-height: 22px;
}
.store-name-text {
min-width: 0;
}
.store-self {
flex-shrink: 0;
padding: 0 6px;
border-radius: 2px;
background: $theme_color;
color: #fff;
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
.store-scores {
display: flex;
margin-top: 14px;
padding: 10px 0;
border-top: 1px solid #f5f5f5;
border-bottom: 1px solid #f5f5f5;
}
.score-item {
flex: 1;
text-align: center;
}
.score-label {
display: block;
color: #999;
font-size: 12px;
line-height: 18px;
}
.score-value {
display: block;
margin-top: 2px;
color: $theme_color;
font-size: 14px;
font-weight: 600;
line-height: 20px;
}
.store-actions {
display: flex;
gap: 8px;
margin-top: 14px;
}
.store-btn,
.enter-store {
border: 1px solid #eee;
background: #fff;
color: #171A26;
font-size: 13px;
line-height: 32px;
border-radius: 4px;
cursor: pointer;
&:hover {
color: $theme_color;
border-color: $theme_color;
}
}
.store-btn {
flex: 1;
}
.store-btn.is-on {
color: $theme_color;
border-color: $theme_color;
background: #fff5f4;
}
.enter-store {
width: 100%;
margin-top: 8px;
}
.recommend-card {
margin-top: 12px;
padding: 12px 12px 4px;
}
.recommend-title {
margin: 0 0 8px;
color: #171A26;
font-size: 18px;
font-weight: 600;
line-height: 26px;
}
.recommend-item {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0 12px;
cursor: pointer;
& + & {
border-top: 1px solid #f5f5f5;
}
}
.recommend-img {
width: 100%;
aspect-ratio: 1;
object-fit: cover;
border-radius: 4px;
background: #fafafa;
}
.recommend-info {
min-width: 0;
}
.recommend-name {
margin: 0;
color: #333;
font-size: 16px;
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.recommend-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 4px;
}
.recommend-price {
margin: 0;
color: $theme_color;
font-size: 18px;
font-weight: 600;
line-height: 26px;
}
.recommend-sales {
flex-shrink: 0;
color: #999;
font-size: 15px;
line-height: 22px;
}
</style>

View File

@@ -23,12 +23,13 @@
<li v-show="!!userInfo.username">
<div class="username-p">
<div>
<Avatar class="person-icon" :src="userInfo.face" icon="person" size="small"/>
<span class="username">{{
userInfo.nickName ? userInfo.nickName : userInfo.username | secrecyMobile
}}</span>
<el-avatar class="person-icon" :src="userInfo.face" :size="24" />
<span class="username">{{ $filters.secrecyMobile(
userInfo.nickName ? userInfo.nickName : userInfo.username ) }}</span>
</div>
<transition name='fade'>
<!-- 下拉菜单通过父级 hover 的 CSS 控制显隐,transition 内无需 v-if/v-show -->
<!-- eslint-disable-next-line vue/require-toggle-inside-transition -->
<ul class="drop-items">
<li @click="goUserCenter('/home')">我的主页</li>
<li @click="goUserCenter('/home/Coupons')">优惠券</li>
@@ -40,46 +41,45 @@
</li>
<li @click="goUserCenter('/home/MyOrder')"><span class="nav-item">我的订单</span></li>
<li @click="goUserCenter('/home/MyTracks')"><span class="nav-item">我的足迹</span></li>
<li @click="goUserCenter('/home/MsgList')"><span class="nav-item">我的消息</span></li>
<li v-if="$route.name !== 'Cart'" style="position:relative;">
<Dropdown placement="bottom-start">
<router-link to="/cart" target="_blank">
<span class="nav-item" @mouseenter="getCartList">
<Icon size="18" type="ios-cart-outline"></Icon>
购物车({{ cartNum < 100 ? cartNum : '99' }})
</span>
</router-link>
<DropdownMenu slot="list">
<div class="shopping-cart-null" style="width:200px" v-show="shoppingCart.length <= 0">
<Icon type="ios-cart-outline" class="cart-null-icon"></Icon>
<li @click="goUserCenter('/home/MsgList')">
<span class="nav-item">
我的消息
<i v-if="unreadMsg > 0" class="msg-badge">{{ unreadLabel }}</i>
</span>
</li>
<li v-if="$route.name !== 'Cart'" class="cart-nav-item">
<el-dropdown
class="cart-nav-dropdown"
placement="bottom-start"
trigger="hover"
@visible-change="handleCartDropdownVisible"
>
<span class="nav-item" @click="goToPay">
<el-icon :size="18"><ShoppingCart /></el-icon>
购物车({{ Number(cartNum) > 99 ? '99+' : cartNum }})
</span>
<template #dropdown>
<div class="shopping-cart-null" v-show="shoppingCart.length <= 0">
<el-icon class="cart-null-icon"><ShoppingCart /></el-icon>
<span>你的购物车没有宝贝哦</span>
<span>赶快去添加商品吧~</span>
</div>
<div class="shopping-cart-list" v-show="shoppingCart.length > 0">
<div class="shopping-cart-box" v-for="(item, index) in shoppingCart" @click="goToPay" :key="index">
<div class="shopping-cart-img">
<img :src="item.goodsSku.thumbnail" class="hover-pointer"/>
<img :src="item.goodsSku.thumbnail" alt="" />
</div>
<div class="shopping-cart-info">
<div class="shopping-cart-title ">
<p class="hover-pointer goods-title ellipsis">{{ item.goodsSku.goodsName }}</p>
</div>
<p class="goods-title">{{ item.goodsSku.goodsName }}</p>
<div class="shopping-cart-detail">
<div>
数量:
<span class="shopping-cart-text">{{ item.num }}</span>
</div>
<div class="shopping-price">
{{ item.purchasePrice | unitPrice('¥') }}
</div>
<span>数量 {{ item.num }}</span>
<span class="shopping-price">{{ $filters.unitPrice(item.purchasePrice, '¥') }}</span>
</div>
</div>
</div>
</div>
</DropdownMenu>
</Dropdown>
</template>
</el-dropdown>
</li>
<li>
<span class="nav-item" @click="shopEntry">店铺入驻</span>
@@ -90,145 +90,243 @@
</div>
</template>
<script>
<script setup>
import { ref, computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Modal } from "@/utils/message";
import { ShoppingCart } from '@element-plus/icons-vue';
import storage from '@/plugins/storage.js';
import {cartGoodsAll} from '@/api/cart.js';
import {logout} from '@/api/account.js';
import config from '@/config';
import { refreshUnreadMessage } from '@/utils/unreadMessage';
import { useAppStore } from '@/stores/app';
defineOptions({ name: 'M-Header' });
export default {
name: 'M-Header',
created() {
if (storage.getItem('userInfo')) {
this.userInfo = JSON.parse(storage.getItem('userInfo'));
}
},
const route = useRoute();
const router = useRouter();
const appStore = useAppStore();
data() {
return {
config: require('@/config'),
userInfo: {}, // 用户信息
shoppingCart: [] // 购物车
};
},
computed: {
// 购物车商品数量
cartNum() {
return this.$store.state.cartNum;
}
},
methods: {
goToPay() { // 跳转购物车
let url = this.$router.resolve({
path: '/cart'
});
window.open(url.href, '_blank');
},
myInfo() { // 跳转会员中心
let url = this.$router.resolve({
path: '/home'
});
window.open(url.href, '_blank');
},
signOutFun() { // 退出登录
logout().then(res => {
storage.removeItem('accessToken');
storage.removeItem('refreshToken');
storage.removeItem('userInfo');
storage.removeItem('cartNum');
this.$store.commit('SET_CARTNUM', 0)
this.$router.push('/login');
});
},
goUserCenter(path) {
// 跳转我的订单,我的足迹、收藏等
if (this.userInfo.username) {
this.$router.push({path: path});
} else {
this.$Modal.confirm({
title: '请登录',
content: '<p>请登录后执行此操作</p>',
okText: '立即登录',
cancelText: '继续浏览',
onOk: () => {
this.$router.push({
path: '/login',
query: {
rePath: this.$router.history.current.path,
query: JSON.stringify(this.$router.history.current.query)
}
});
}
const userInfo = ref({}); // 用户信息
const shoppingCart = ref([]); // 购物车
const cartLoading = ref(false);
const cartLoadedAt = ref(0);
const cartCacheTime = ref(5000);
// 购物车商品数量
const cartNum = computed(() => appStore.cartNum);
const unreadMsg = computed(() => appStore.unreadMsg);
const unreadLabel = computed(() => {
return unreadMsg.value > 99 ? '99+' : String(unreadMsg.value);
});
if (storage.getItem('userInfo')) {
userInfo.value = JSON.parse(storage.getItem('userInfo'));
refreshUnreadMessage();
}
function goToPay() {
if (!userInfo.value.username) {
Modal.confirm({
title: '温馨提示',
content: '请登录后执行此操作',
okText: '立即登录',
cancelText: '取消',
onOk: () => {
router.push({
path: '/login',
query: {
rePath: '/cart',
query: JSON.stringify(route.query || {}),
},
});
}
},
shopEntry() {
// 店铺入驻
if (storage.getItem('accessToken')) {
let routeUrl = this.$router.resolve({
path: '/shopEntry',
query: {id: 1}
});
window.open(routeUrl.href, '_blank');
} else {
this.$router.push('login');
}
},
getCartList() {
// 获取购物车列表
if (this.userInfo.username) {
cartGoodsAll().then((res) => {
this.shoppingCart = res.result.skuList;
this.$store.commit('SET_CARTNUM', this.shoppingCart.length);
this.Cookies.setItem('cartNum', this.shoppingCart.length);
});
}
}
},
});
return;
}
};
router.push('/cart');
}
function myInfo() { // 跳转会员中心
router.push('/home');
}
function signOutFun() { // 退出登录
logout().then(res => {
storage.removeItem('accessToken');
storage.removeItem('refreshToken');
storage.removeItem('userInfo');
storage.removeItem('cartNum');
appStore.setCartNum(0)
appStore.setUnreadMsg(0)
router.push('/login');
});
}
function goUserCenter(path) {
// 跳转我的订单,我的足迹、收藏等
if (userInfo.value.username) {
router.push({path: path});
} else {
Modal.confirm({
title: '请登录',
content: '请登录后执行此操作',
okText: '立即登录',
cancelText: '继续浏览',
onOk: () => {
router.push({
path: '/login',
query: {
rePath: route.path,
query: JSON.stringify(route.query || {}),
},
});
}
});
}
}
function shopEntry() {
if (storage.getItem('accessToken')) {
router.push({
path: '/shopEntry',
query: { id: 1 },
});
return;
}
Modal.confirm({
title: '温馨提示',
content: '请登录后执行此操作',
okText: '立即登录',
cancelText: '取消',
onOk: () => {
router.push({
path: '/login',
query: {
rePath: '/shopEntry',
query: JSON.stringify({ id: 1 }),
},
});
},
});
}
function handleCartDropdownVisible(visible) {
if (visible) {
getCartList();
}
}
function getCartList() {
// 获取购物车列表
if (!userInfo.value.username || cartLoading.value) {
return;
}
const now = Date.now();
if (cartLoadedAt.value && now - cartLoadedAt.value < cartCacheTime.value) {
return;
}
cartLoadedAt.value = now;
cartLoading.value = true;
cartGoodsAll().then((res) => {
if (res.success) {
shoppingCart.value = (res.result && res.result.skuList) || [];
appStore.setCartNum(shoppingCart.value.length);
storage.setItem('cartNum', shoppingCart.value.length);
}
}).finally(() => {
cartLoading.value = false;
});
}
</script>
<style scoped lang="scss">
.box {
width: 100%;
font-size: 12px !important;
height: 36px;
font-size: 14px;
height: 40px;
background: #333;
color: #fff;
display: flex;
align-items: center;
}
.nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
height: 100%;
}
.nav ul {
.nav > ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
align-items: center;
}
.nav li {
.nav > ul > li,
.location > ul.flex > li {
cursor: pointer;
float: left;
line-height: 36px;
float: none;
line-height: 40px;
margin-right: 15px;
display: flex;
align-items: center;
}
.nav a,
.nav-item {
font-size: 13px;
font-size: 14px;
font-weight: normal;
text-decoration: none;
cursor: pointer;
color: #fff;
display: inline-flex;
align-items: center;
line-height: 1;
}
.nav-item {
padding-left: 20px;
gap: 4px;
}
.msg-badge {
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 8px;
background: $theme_color;
color: #fff;
font-size: 12px;
font-style: normal;
line-height: 16px;
text-align: center;
}
.cart-nav-item {
:deep(.cart-nav-dropdown),
:deep(.el-dropdown) {
display: inline-flex;
align-items: center;
margin: 0;
padding: 0;
vertical-align: middle;
line-height: 40px;
}
:deep(.el-tooltip__trigger),
:deep(.el-dropdown-selfdefine),
:deep([aria-expanded="true"]) {
outline: none !important;
border: none !important;
box-shadow: none !important;
}
}
.location a {
@@ -261,30 +359,10 @@ export default {
}
.shopping-cart-img {
margin-right: 15px;
width: 40px;
height: 40px;
}
.shopping-cart-img img {
width: 100%;
}
.shopping-cart-info {
display: flex;
flex-direction: column;
justify-content: space-between;
align-content: space-between;
width: 200px;
overflow: hidden;
font-size: 12px;
line-height: 20px;
}
.shopping-cart-null {
padding: 15px;
width: 320px;
padding: 28px 16px;
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
@@ -292,20 +370,102 @@ export default {
}
.cart-null-icon {
font-size: 38px;
margin-bottom: 15px;
font-size: 36px;
margin-bottom: 12px;
color: #ccc;
}
.shopping-cart-null span {
font-size: 12px;
line-height: 16px;
font-size: 13px;
line-height: 1.6;
color: #999;
}
.shopping-cart-list {
width: 320px;
max-height: 320px;
overflow-y: auto;
padding: 4px 0;
box-sizing: border-box;
color: #333;
}
.shopping-cart-box {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
cursor: pointer;
&:hover {
background: #fafafa;
}
& + .shopping-cart-box {
border-top: 1px solid #f2f2f2;
}
}
.shopping-cart-img {
width: 64px;
height: 64px;
flex-shrink: 0;
border-radius: 6px;
overflow: hidden;
background: #f5f5f5;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.shopping-cart-info {
flex: 1;
min-width: 0;
min-height: 64px;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.shopping-cart-info .goods-title {
margin: 0;
font-size: 13px;
line-height: 1.4;
color: #333;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
white-space: normal;
}
.shopping-cart-detail {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
color: #999;
font-size: 14px;
}
.shopping-price {
color: $theme_color;
font-size: 14px;
font-weight: 600;
}
.username-p {
position: relative;
z-index: 100;
div {
cursor: pointer;
display: flex;
align-items: center;
> span {
margin-left: 5px;
@@ -315,59 +475,71 @@ export default {
.drop-items {
position: absolute;
display: none;
top: 45px;
left: 0;
right: 0;
margin: 0 auto;
padding: 5px 10px;
z-index: 20;
height: 150px;
flex-direction: column;
top: calc(100% + 10px);
left: 50%;
transform: translateX(-50%);
margin: 0;
padding: 4px 0;
z-index: 1000;
min-width: 100px;
width: max-content;
background-color: #fff;
width: 80px;
border: 1px solid #eee;
box-shadow: 2px 2px 7px #999;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
list-style: none;
li {
color: rgb(107, 106, 106);
display: block;
float: none;
width: 100%;
border-bottom: 1px solid rgb(207, 206, 206);
margin: 0;
padding: 8px 16px;
line-height: 1.4;
color: #666;
font-size: 14px;
font-weight: normal;
text-align: center;
white-space: nowrap;
border-bottom: 1px solid #f0f0f0;
box-sizing: border-box;
&:last-child {
border: none;
border-bottom: none;
}
&:hover {
cursor: pointer;
color: $theme_color;
background-color: #fff7f7;
}
}
&::before {
position: absolute;
top: -20px;
left: 30px;
top: -6px;
left: 50%;
transform: translateX(-50%);
content: '';
width: 0;
height: 0;
border: 10px solid #999;
border-color: transparent transparent #fff transparent;
border: 6px solid transparent;
border-bottom-color: #fff;
}
&::after {
content: '';
position: absolute;
width: 80px;
height: 20px;
top: -20px;
width: 100%;
height: 10px;
top: -10px;
left: 0;
}
}
&:hover {
.drop-items {
display: block;
display: flex;
}
}
}
@@ -404,7 +576,7 @@ export default {
}
.my-info p {
font-size: 12px;
font-size: 14px;
}
.sign-out {
@@ -424,7 +596,7 @@ export default {
}
.sign-out p {
font-size: 12px;
font-size: 14px;
}
.goods-title:hover {
@@ -446,34 +618,4 @@ export default {
z-index: 3;
top: 3px;
}
.shopping-cart-box {
padding: 8px 0;
cursor: pointer;
display: flex;
align-items: center;
border-bottom: 1px #ccc dotted;
}
.shopping-cart-list {
padding: 10px 10px;
box-sizing: border-box;
max-height: 300px;
overflow-y: scroll;
color: #333;
}
.shopping-cart-detail {
color: #999;
font-size: 12px;
}
.shopping-price {
color: $theme_color;
font-size: 14px;
font-weight: bold;
}
</style>

View File

@@ -1,8 +1,12 @@
<template>
<div class="scroll-show">
<div class="content clearfix">
<cateNav class="cate" :hover="true" :showNavBar="false"
useClass="fixed-show"></cateNav>
<div class="content">
<cateNav
class="cate"
:hover="true"
:showNavBar="false"
useClass="fixed-show"
/>
<Search
class="search-con"
:hover="true"
@@ -10,108 +14,118 @@
:showLogo="false"
:showTag="false"
useClass="fixed-show"
></Search>
<div class="flex flex-a-c cart">
<Icon
type="ios-cart-outline"
@click="goCartList"
class="cart-icon"
@mouseenter.native="getCartList"
/>
<i class="cart-badge">{{ cartNum < 100 ? cartNum : "99" }}</i>
/>
<div class="cart">
<span @mouseenter="getCartList">
<el-icon class="cart-icon" :size="20" @click="goCartList"><ShoppingCart /></el-icon>
</span>
<i class="cart-badge">{{ Number(cartNum) > 99 ? "99+" : cartNum }}</i>
</div>
</div>
</div>
</template>
<script>
import { cartCount } from "@/api/cart.js";
import storage from "@/plugins/storage.js";
export default {
data() {
return {
userInfo: {}, // 用户信息
};
},
computed: {
cartNum() {
// 购物车商品数量
return this.$store.state.cartNum;
},
},
methods: {
goCartList() {
// 跳转购物车页面
let routerUrl = this.$router.resolve({
path: "/cart",
});
window.open(routerUrl.href, "_blank");
},
getCartList() {
// 获取购物车列表
if (storage.getItem("userInfo")) {
cartCount().then((res) => {
this.$store.commit("SET_CARTNUM", res.result);
this.Cookies.setItem("cartNum", res.result);
});
}
},
},
mounted() {
<script setup>
import { computed, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import Search from '@/components/Search.vue'
import cateNav from '@/components/nav/CateNav.vue'
import { cartCount } from '@/api/cart.js'
import storage from '@/plugins/storage.js'
import { ShoppingCart } from '@element-plus/icons-vue'
import { useAppStore } from '@/stores/app'
if (storage.getItem("userInfo")) {
this.userInfo = JSON.parse(storage.getItem("userInfo"));
}
},
};
const router = useRouter()
const appStore = useAppStore()
const userInfo = ref({})
const cartNum = computed(() => appStore.cartNum)
function goCartList() {
router.push('/cart')
}
function getCartList() {
if (storage.getItem('userInfo')) {
cartCount().then((res) => {
appStore.setCartNum(res.result)
storage.setItem('cartNum', res.result)
})
}
}
onMounted(() => {
if (storage.getItem('userInfo')) {
userInfo.value = JSON.parse(storage.getItem('userInfo'))
}
})
</script>
<style lang="scss" scoped>
$side-col-width: 263.2px;
$col-gap: 10px;
$cart-col-width: 40px;
.scroll-show {
height: 60px;
overflow: visible;
}
.content {
width: 1200px;
height: 40px;
margin: 0 auto;
position: relative;
}
.cate {
float: left;
width: 200px !important;
}
.search-con {
float: left;
width: 800px;
overflow: hidden;
margin-top: -27px;
}
.cart{
height: 60px;
margin: 0 auto;
display: grid;
grid-template-columns: $side-col-width 1fr $cart-col-width;
column-gap: $col-gap;
align-items: center;
}
.cart-icon {
width: 30px;
float: left;
font-size: 25px;
color: $theme_color;
z-index: 1;
position: relative;
&:hover {
cursor: pointer;
.cate {
min-width: 0;
:deep(.cate-nav) {
width: 100%;
margin: 0;
}
:deep(.nav-con) {
height: 60px;
}
}
.search-con {
min-width: 0;
width: 100%;
}
.cart {
display: flex;
align-items: center;
justify-content: center;
height: 60px;
position: relative;
}
.cart-icon {
font-size: 25px;
color: $theme_color;
cursor: pointer;
}
.cart-badge {
position: absolute;
font-style: normal;
right: 165px;
top: 12px;
right: -2px;
display: block;
background-color: $theme_color;
color: #fff;
font-size: 12px;
width: 17px;
min-width: 17px;
height: 17px;
padding: 0 4px;
border-radius: 10px;
line-height: 17px;
text-align: center;
z-index: 5;
top: 10px;
}
</style>

View File

@@ -1,31 +1,47 @@
<template>
<div class="model-form">
<div class="model-content">
<template v-for="(element, index) in data.list">
<model-form-item
v-if="element && element.key"
:key="element.key"
:element="element"
:index="index"
:data="data"
></model-form-item>
</template>
<model-form-item
v-for="(element, index) in renderList"
:key="element.key"
:element="element"
:index="index"
:data="data"
></model-form-item>
</div>
</div>
</template>
<script>
<script setup>
import { computed } from 'vue';
import ModelFormItem from './ModelFormItem.vue';
export default {
name: 'modelForm',
components: {
ModelFormItem
},
props: ['data']
};
defineOptions({ name: 'modelForm' });
const props = defineProps(['data']);
const renderList = computed(() => {
const list = props.data?.list || [];
return list.filter(
(el) => el && el.key && !isBuiltinModule(el.type)
);
});
function isBuiltinModule(type) {
// 顶部广告、快捷导航由 Index 页单独渲染
return type === 'topAdvert' || type === 'navBar';
}
</script>
<style lang="scss" scoped>
.model-form {
position: relative;
z-index: 0;
width: 100%;
}
.model-content {
width: 100%;
min-height: 1200px;
box-sizing: border-box;
/* 勿设 overflow-x: hidden,会裁切 discountAdvert 的 margin-left: -100px 左溢背景 */
overflow-x: visible;
}
</style>

View File

@@ -2,10 +2,9 @@
<div class="model-item" v-if="element && element.key">
<!-- 轮播图模块,包括个人信息,快捷导航模块 -->
<template v-if="element.type == 'carousel'">
<model-carousel
:data="element"
class="mb_20 width_1200_auto"
></model-carousel>
<div class="carousel-wrap mb_20">
<model-carousel :key="element.key" :data="element"></model-carousel>
</div>
</template>
<template v-if="element.type == 'carousel1'">
<model-carousel1 :data="element" class="mb_20"></model-carousel1>
@@ -18,62 +17,66 @@
</template>
<!-- 热门广告 -->
<template v-if="element.type == 'hotAdvert'">
<div class="mb_20 width_1200_auto">
<img
style="display: block"
class="hover-pointer"
:src="element.options.list[0].img"
@click="linkTo(element.options.list[0].url)"
width="1200"
alt=""
/>
</div>
<ul class="advert-list width_1200_auto">
<template v-for="(item, index) in element.options.list">
<li
v-if="index !== 0"
@click="linkTo(item.url)"
<div class="hot-advert mb_20 width_1200_auto">
<div class="hot-advert-banner">
<img
class="hover-pointer"
:key="index"
>
<img :src="item.img" width="230" height="190" alt="" />
</li>
</template>
</ul>
:src="element.options.list[0].img"
@click="linkTo(element.options.list[0].url)"
alt=""
/>
</div>
<ul class="advert-list">
<template v-for="(item, index) in element.options.list" :key="index">
<li
v-if="index !== 0"
@click="linkTo(item.url)"
class="hover-pointer"
>
<img :src="item.img" alt="" />
</li>
</template>
</ul>
</div>
</template>
<!-- 限时秒杀 待完善 -->
<template v-if="element.type == 'seckill' && element.options.list.length">
<template v-if="element.type == 'seckill' && element.options?.list?.length">
<seckill :data="element" class="mb_20 width_1200_auto"></seckill>
</template>
<!-- 折扣广告 -->
<template v-if="element.type == 'discountAdvert'">
<div
class="discountAdvert"
:style="{
backgroundImage:
'url(' + require('@/assets/images/decorate.png') + ')',
}"
>
<img
@click="linkTo(item.url)"
class="hover-pointer"
v-for="(item, index) in element.options.classification"
:key="index"
:src="item.img"
width="190"
height="210"
alt=""
/>
<img
@click="linkTo(item.url)"
class="hover-pointer"
v-for="(item, index) in element.options.brandList"
:key="'discount' + index"
:src="item.img"
width="240"
height="105"
alt=""
/>
<div class="discountAdvert-wrap width_1200_auto mb_20">
<div
class="discountAdvert"
:style="{
backgroundImage: 'url(' + decorateImg + ')',
}"
>
<div>
<img
@click="linkTo(item.url)"
class="hover-pointer"
v-for="(item, index) in element.options.classification"
:key="index"
:src="item.img"
width="190"
height="210"
alt=""
/>
</div>
<div>
<img
@click="linkTo(item.url)"
class="hover-pointer"
v-for="(item, index) in element.options.brandList"
:key="'discount' + index"
:src="item.img"
width="240"
height="105"
alt=""
/>
</div>
</div>
</div>
</template>
@@ -117,15 +120,12 @@
></first-page-advert>
</template>
<!-- 横幅广告 -->
<template v-if="element.type == 'bannerAdvert'">
<div style="width: 100%; text-align: center">
<img
width="1200"
class="hover-pointer mb_20 bannerAd"
@click="linkTo(element.options.url)"
:src="element.options.img"
alt=""
/>
<template v-if="element.type == 'bannerAdvert' && element.options.img">
<div
class="horizontal-advert width_1200_auto mb_20 hover-pointer"
@click="linkTo(element.options.url)"
>
<img class="bannerAd" :src="element.options.img" alt="" />
</div>
</template>
<template v-if="element.type == 'notEnough'"
@@ -138,7 +138,8 @@
</div>
</template>
<script>
<script setup>
import { ref } from "vue";
import ModelCarousel from "./modelList/Carousel.vue";
import ModelCarousel1 from "./modelList/Carousel1.vue";
import ModelCarousel2 from "./modelList/Carousel2.vue";
@@ -152,53 +153,79 @@ import goodsAndType from "./modelList/goodsAndType.vue";
import onlyGoodsModel from "./modelList/onlyGoodsModel.vue";
import mixModel from "./modelList/mixModel.vue";
import forYour from "./modelList/forYour.vue";
import decorateImgUrl from "@/assets/images/decorate.png";
defineOptions({ name: "modelFormItem" });
export default {
name: "modelFormItem",
props: ["element", "select", "index", "data"],
components: {
ModelCarousel,
ModelCarousel1,
ModelCarousel2,
Recommend,
NewGoodsSort,
FirstPageAdvert,
NotEnough,
Seckill,
oneRowThreeColumns,
goodsAndType,
onlyGoodsModel,
mixModel,
forYour
},
data() {
return {
showModal: false, // 控制模态框显隐
selected: {}, // 已选数据
};
},
};
defineProps(["element", "select", "index", "data"]);
const decorateImg = ref(decorateImgUrl);
const showModal = ref(false); // 控制模态框显隐
const selected = ref({}); // 已选数据
</script>
<style lang="scss" scoped>
.model-item {
position: relative;
margin-bottom: 10px;
width: 100%;
box-sizing: border-box;
}
.bannerAd{
width: 1183px;
height: 166.6px;
.carousel-wrap {
width: 100%;
max-width: 1200px;
margin: 0 auto;
box-sizing: border-box;
overflow: hidden;
}
/** 横幅广告 */
.horizontal-advert {
width: 100%;
max-width: 1200px;
overflow: hidden;
border-radius: 10px;
line-height: 0;
}
.bannerAd {
display: block;
width: 100%;
height: auto;
max-height: 166px;
object-fit: cover;
}
/** 热门广告 */
.advert-list {
background: $theme_color;
height: 200px;
display: flex;
justify-content: space-around;
padding: 3px 10px;
> li {
.hot-advert {
width: 1200px;
max-width: 100%;
overflow: hidden;
.hot-advert-banner {
line-height: 0;
img {
display: block;
width: 100%;
height: auto;
}
}
}
.advert-list {
width: 100%;
margin: 0;
padding: 10px;
list-style: none;
box-sizing: border-box;
background: $theme_color;
height: 210px;
display: flex;
gap: 10px;
align-items: center;
> li {
flex: 1 1 0;
min-width: 0;
img {
display: block;
width: 100%;
height: 190px;
cursor: pointer;
border-radius: 10px;
transition: all 150ms ease-in-out;
@@ -211,25 +238,43 @@ export default {
}
/** 折扣广告 */
.discountAdvert-wrap {
width: 1200px;
position: relative;
overflow: visible;
}
.discountAdvert {
width: 1300px;
height: 566px;
margin: 0 auto;
margin-bottom: 20px;
height: 585px;
margin-left: -100px;
box-sizing: border-box;
background-repeat: no-repeat;
background-size: 1300px 585px;
background-position: left top;
position: relative;
left: -47px;
padding-left: 295px;
display: flex;
flex-wrap: wrap;
align-items: flex-start;
img {
margin-top: 10px;
margin-right: 10px;
transition: all 150ms ease-in-out;
&:hover {
box-shadow: 0 5px 12px 0 rgba(0, 0, 0, 0.4);
transform: translateY(-2px);
> div {
padding-left: 295px;
display: flex;
flex-wrap: wrap;
box-sizing: border-box;
&:nth-child(1) img {
margin: 10px 10px 0 0;
transition: all 150ms ease-in-out;
&:hover {
box-shadow: 0 5px 12px 0 rgba(0, 0, 0, 0.4);
transform: translateY(-2px);
}
}
&:nth-child(2) {
margin-top: 10px;
img {
margin: 0 10px 0 0;
transition: all 150ms ease-in-out;
&:hover {
box-shadow: 0 5px 12px 0 rgba(0, 0, 0, 0.4);
transform: translateY(-2px);
}
}
}
}
}

View File

@@ -1,22 +1,30 @@
<template>
<div class="model-carousel">
<div class="nav-body clearfix">
<!-- 侧边导航占位 -->
<div class="nav-side"></div>
<div class="nav-body">
<!-- 侧边导航占位(与 CateNav 分类栏同宽,勿与 CateNav .nav-side 混用类名) -->
<div class="carousel-side-spacer"></div>
<div class="nav-content">
<!-- 轮播图 -->
<!-- autoplay autoplay-speed="10000"-->
<Carousel :height="333.9" autoplay :autoplay-speed="5000">
<CarouselItem v-for="(item, index) in data.options.list" :key="index">
<div class="swiper-img" >
<!-- 轮播图:无有效图片时不挂载 el-carousel;勿对 el-carousel 使用随图片变化的 key,否则 Vue3 patch 会报 emitsOptions 错误 -->
<el-carousel
v-if="displaySlides.length"
height="334px"
:interval="5000"
:loop="displaySlides.length > 1"
>
<el-carousel-item
v-for="(item, index) in displaySlides"
:key="carouselItemKey(item, index)"
>
<div class="swiper-img">
<img
:src="item.img"
@click="linkTo(item.url)"
class="hover-pointer"
/>
</div>
</CarouselItem>
</Carousel>
</el-carousel-item>
</el-carousel>
<div v-else class="swiper-img carousel-placeholder"></div>
</div>
<div class="nav-right">
<div class="person-msg">
@@ -24,7 +32,7 @@
<img class="user-face" :src="userInfo.face || defaultAvatar" alt />
<div class="welcome">
Hi, {{
userInfo.nickName || `欢迎来到${config.title}` | secrecyMobile
$filters.secrecyMobile(userInfo.nickName || `欢迎来到${config.title}`)
}}
</div>
</div>
@@ -43,18 +51,14 @@
</div> -->
</div>
<div v-else class="flex flex-a-c ">
<div class="btns" @click="$router.push('login')" shape="circle"
>登录</div
>
<div class="btns sign-up" @click="$router.push('signUp')" shape="circle"
>注册</div
>
<div class="btns" @click="goLogin">登录</div>
<div class="btns sign-up" @click="$router.push('signUp')">注册</div>
</div>
<div class="gray-line"></div>
<div class="icon-list flex flex-j-sb" >
<div class="icon-item" @click="entryControl(item)" :key="index" v-for="(item, index) in entranceList">
<img class="icon" :src="require(`@/assets/iconfont/${item.icon}.png`)">
<img class="icon" :src="getIconUrl(item.icon)">
<div>
{{ item.label }}
</div>
@@ -62,7 +66,7 @@
</div>
<div class="icon-list flex flex-j-sb" >
<div class="icon-item" :key="index" @click="entryControl(item)" v-for="(item, index) in appendList">
<img class="icon" :src="require(`@/assets/iconfont/${item.icon}.png`)">
<img class="icon" :src="getIconUrl(item.icon)">
<div>
{{ item.label }}
</div>
@@ -78,95 +82,153 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { Modal } from "@/utils/message";
import storage from "@/plugins/storage";
export default {
name: "modelCarousel",
props: ["data"],
data() {
return {
config: require("@/config"),
defaultAvatar: require("@/assets/images/default.png"),
userInfo: {}, // 用户信息
entranceList: [
{
icon: "collage",
label: "宝贝收藏",
path: "/home/Favorites"
},
{
icon: "shop",
label: "收藏店铺",
path: "/home/Favorites?type=STORE"
},
{
icon: "carts",
label: "购物车",
path: "/cart"
},
{
icon: "story",
label: "我的足迹",
path: "/home/MyTracks"
},
],
appendList: [
{
icon: "support",
label: "帮助中心",
path: "/article"
},
{
icon: "feedback",
label: "我的评论",
path: "/home/CommentList"
},
{
icon: "notice",
label: "收货地址",
path: "/home/MyAddress"
},
{
icon: "notification",
label: "我的消息",
path: "/home/MsgList"
},
],
recentList: [
{
value: "0",
label: "购物车"
},
{
value: "0",
label: "待收货"
},
{
value: "0",
label: "待发货"
},
{
value: "0",
label: "代付款"
},
],
};
},
methods: {
// 快捷跳转中心
entryControl(val) {
console.log("val",val)
let url = this.$router.resolve(val.path);
window.open(url.href, '_blank');
},
},
mounted() {
if (storage.getItem("userInfo"))
this.userInfo = JSON.parse(storage.getItem("userInfo"));
import config from "@/config";
import defaultAvatar from "@/assets/images/default.png";
import { getIconUrl } from "@/assets/iconfont/iconMap";
defineOptions({ name: "modelCarousel" });
const props = defineProps(["data"]);
const router = useRouter();
const carouselList = computed(() => {
return props.data?.options?.list || [];
});
const displaySlides = computed(() => {
return carouselList.value.filter((item) => item && item.img);
});
const userInfo = ref({}); // 用户信息
const entranceList = ref([
{
icon: "collage",
label: "宝贝收藏",
path: "/home/Favorites"
},
};
{
icon: "shop",
label: "收藏店铺",
path: "/home/Favorites?type=STORE"
},
{
icon: "carts",
label: "购物车",
path: "/cart"
},
{
icon: "story",
label: "我的足迹",
path: "/home/MyTracks"
},
]);
const appendList = ref([
{
icon: "support",
label: "帮助中心",
path: "/article"
},
{
icon: "feedback",
label: "我的评论",
path: "/home/CommentList"
},
{
icon: "notice",
label: "收货地址",
path: "/home/MyAddress"
},
{
icon: "notification",
label: "我的消息",
path: "/home/MsgList"
},
]);
const recentList = ref([
{
value: "0",
label: "购物车"
},
{
value: "0",
label: "待收货"
},
{
value: "0",
label: "待发货"
},
{
value: "0",
label: "待付款"
},
]);
function carouselItemKey(item, index) {
return item?.img || item?.url || `slide-${index}`;
}
function resolveRoute(pathStr) {
if (!pathStr) return { path: "/" };
const [path, search] = pathStr.split("?");
if (!search) return { path };
return {
path,
query: Object.fromEntries(new URLSearchParams(search)),
};
}
function isMemberRoute(location) {
const path = location.path && location.path.charAt(0) === "/" ? location.path : `/${location.path || ""}`;
return path === "/home" || path.startsWith("/home/") || path === "/cart";
}
function normalizeLocation(location) {
if (!location.path || location.path.charAt(0) === "/") return location;
return { ...location, path: `/${location.path}` };
}
function promptLoginThenNavigate(location) {
Modal.confirm({
title: "温馨提示",
content: "请登录后执行此操作",
okText: "立即登录",
cancelText: "取消",
onOk: () => {
router.push({
path: "/login",
query: {
rePath: location.path,
query: JSON.stringify(location.query || {}),
},
});
},
});
}
function goLogin() {
router.push("/login");
}
// 快捷跳转中心
function entryControl(val) {
const location = normalizeLocation(resolveRoute(val.path));
if (isMemberRoute(location) && !storage.getItem("userInfo")) {
promptLoginThenNavigate(location);
return;
}
router.push(location);
}
onMounted(() => {
if (storage.getItem("userInfo"))
userInfo.value = JSON.parse(storage.getItem("userInfo"));
});
</script>
<style scoped lang="scss">
@@ -182,10 +244,15 @@ export default {
.swiper-img {
overflow: hidden;
width: 637px;
width: 100%;
height: 329.9px;
}
.carousel-placeholder {
border-radius: 10px;
background: #ececec;
}
.icon-list {
width: 216px
}
@@ -265,21 +332,21 @@ export default {
}
.model-carousel {
width: 1200px;
width: 100%;
max-width: 1200px;
height: 340px;
margin: 0 auto;
overflow: hidden;
box-sizing: border-box;
position: relative;
z-index: 0;
}
.hover-pointer {
//display: block;
//width: 100%;
//height: 100%;
width: 637px;
width: 100%;
height: 329.9px;
object-fit: cover;
border-radius: 10px;
}
.welcome {
@@ -334,44 +401,62 @@ export default {
}
/* 导航主体 */
/* 导航主体:263.2 + 10 + 1fr + 10 + 263.2 = 1200px,与下方楼层右缘对齐 */
.nav-body {
width: 1200px;
width: 100%;
max-width: 1200px;
height: 340px;
margin: 0px auto;
margin: 0 auto;
display: grid;
grid-template-columns: 263.2px minmax(0, 1fr) 263.2px;
column-gap: 10px;
align-items: start;
box-sizing: border-box;
}
.nav-side {
height: 334px;
.carousel-side-spacer {
grid-column: 1;
width: 263.2px;
height: 334px;
flex-shrink: 0;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
float: left;
padding: 0px;
color: #fff;
// background-color: #6e6568;
pointer-events: none;
}
/*导航内容*/
.nav-content {
width: 637px;
margin-left: 10.8px;
grid-column: 2;
width: 100%;
min-width: 0;
margin-top: 10px;
height: 333.9px;
float: left;
position: relative;
overflow: hidden;
:deep(.el-carousel) {
width: 100%;
height: 100%;
border-radius: 10px;
overflow: hidden;
}
:deep(.el-carousel__container) {
height: 334px;
}
:deep(.el-carousel__item) {
width: 100%;
}
}
.nav-right {
float: left;
margin-top: 10px;
grid-column: 3;
width: 263.2px;
margin-left: 10px;
min-width: 263.2px;
max-width: 263.2px;
margin-top: 10px;
border-radius: 10px;
background: #FFFFFF;

View File

@@ -5,40 +5,38 @@
<div class="nav-side"></div>
<div class="nav-content">
<!-- 轮播图 -->
<Carousel autoplay @on-change="autoChange">
<CarouselItem v-for="(item, index) in data.options.list" :key="index" >
<el-carousel height="470px" :interval="5000" @change="autoChange">
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
<div style="overflow: hidden">
<img :src="item.img" width="1200" height="470" />
</div>
</CarouselItem>
</Carousel>
</el-carousel-item>
</el-carousel>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'modelCarousel1',
props: ['data'],
data () {
return {
showModal: false, // modal显隐
selected: null, // 已选数据
picModelFlag: false, // 选择图片modal
bgColor: '#fff' // 轮播背景色
};
},
mounted () {
this.bgColor = this.data.options.list[0].bgColor
},
methods: {
// 自动切换时改变背景色
autoChange (oVal, val) {
this.bgColor = this.data.options.list[val].bgColor
}
}
};
<script setup>
import { onMounted, ref } from 'vue'
defineOptions({ name: 'modelCarousel1' })
const props = defineProps(['data'])
const showModal = ref(false) // modal显隐
const selected = ref(null) // 已选数据
const picModelFlag = ref(false) // 选择图片modal
const bgColor = ref('#fff') // 轮播背景色
onMounted(() => {
bgColor.value = props.data.options.list[0].bgColor
})
// 自动切换时改变背景色
function autoChange(index) {
bgColor.value = props.data.options.list[index].bgColor
}
</script>
<style scoped lang="scss">

View File

@@ -5,18 +5,17 @@
<div class="nav-side"></div>
<div class="nav-content">
<!-- 轮播图 -->
<Carousel autoplay>
<CarouselItem v-for="(item, index) in data.options.list" :key="index">
<el-carousel height="470px" :interval="5000">
<el-carousel-item v-for="(item, index) in data.options.list" :key="index">
<div style="overflow: hidden">
<img :src="item.img" width="590" height="470" />
</div>
</CarouselItem>
</Carousel>
</el-carousel-item>
</el-carousel>
</div>
<div class="nav-content1">
<!-- 轮播图 -->
<Carousel autoplay :autoplay-speed="5000">
<CarouselItem v-for="(item, index) in data.options.listRight" :key="index">
<el-carousel height="470px" :interval="5000">
<el-carousel-item v-for="(item, index) in data.options.listRight" :key="index">
<div class="mb_10">
<img :src="item[0].img" width="190" height="150" />
</div>
@@ -26,19 +25,19 @@
<div>
<img :src="item[2].img" width="190" height="150" />
</div>
</CarouselItem>
</Carousel>
</el-carousel-item>
</el-carousel>
</div>
<div class="nav-right">
<div class="person-msg">
<img :src="userInfo.face" v-if="userInfo.face" alt />
<Avatar icon="ios-person" class="mb_10" v-else size="80" />
<div>Hi,{{ userInfo.nickName || `欢迎来到${config.title}` | secrecyMobile }}</div>
<el-avatar v-else :size="80" class="mb_10"><el-icon><User /></el-icon></el-avatar>
<div>Hi,{{ $filters.secrecyMobile(userInfo.nickName || `欢迎来到${config.title}`) }}</div>
<div v-if="userInfo.id">
<Button type="error" shape="circle">会员中心</Button>
<el-button type="danger">会员中心</el-button>
</div>
<div v-else>
<Button type="error" shape="circle">请登录</Button>
<el-button type="danger">请登录</el-button>
</div>
</div>
<div class="shop-msg">
@@ -56,47 +55,48 @@
</div>
</template>
<script>
import {articleList} from '@/api/common.js'
<script setup>
import { onMounted, reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { articleList as getArticleListApi } from '@/api/common.js'
import storage from '@/plugins/storage';
export default {
name: 'modelCarousel2',
props: ['data'],
data () {
return {
config:require('@/config'),
userInfo: {}, // 用户信息
articleList: [], // 常见问题
params: { // 请求常见问题参数
pageNumber: 1,
pageSize: 10,
type: 'ANNOUNCEMENT',
sort: 'sort'
}
};
},
methods: {
getArticleList () { // 获取常见问题列表
articleList(this.params).then(res => {
if (res.success) {
this.articleList = res.result.records
}
})
},
goArticle (id) { // 跳转文章详情
let routeUrl = this.$router.resolve({
path: '/article',
query: {id}
});
window.open(routeUrl.href, '_blank');
import { User } from '@element-plus/icons-vue';
import config from '@/config';
defineOptions({ name: 'modelCarousel2' })
defineProps(['data'])
const router = useRouter()
const userInfo = ref({}) // 用户信息
const articleList = ref([]) // 常见问题
const params = reactive({ // 请求常见问题参数
pageNumber: 1,
pageSize: 10,
type: 'ANNOUNCEMENT',
sort: 'sort'
})
function getArticleList () { // 获取常见问题列表
getArticleListApi(params).then(res => {
if (res.success) {
articleList.value = res.result.records
}
},
mounted () {
if (storage.getItem('userInfo')) this.userInfo = JSON.parse(storage.getItem('userInfo'));
this.getArticleList()
}
};
})
}
function goArticle (id) { // 跳转文章详情
router.push({
path: '/article',
query: {id}
});
}
onMounted(() => {
if (storage.getItem('userInfo')) userInfo.value = JSON.parse(storage.getItem('userInfo'));
getArticleList()
})
</script>
<style scoped lang="scss">
@@ -163,7 +163,7 @@ export default {
margin-top: 10px;
}
.ivu-btn-default {
.el-button--default {
color: $theme_color;
border-color: $theme_color;
}

View File

@@ -15,24 +15,21 @@
<span class="line btm-line"></span>
<p>{{ item.describe }}</p>
</div>
<img :src="item.img" width="170" height="170" alt="" />
<img :src="item.img" loading="lazy" decoding="async" width="170" height="170" alt="" />
</div>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object,
default: null
}
},
data () {
return {
options: this.data.options // 装修数据
};
<script setup>
import { ref } from 'vue';
const props = defineProps({
data: {
type: Object,
default: null
}
};
});
const options = ref(props.data.options); // 装修数据
</script>
<style lang="scss" scoped>
.first-page-advert {

View File

@@ -4,8 +4,7 @@
<div class="top-header" :style="{ background: options.left.bgColor }">
<span>{{ options.left.title }}</span>
<span @click="linkTo(options.left.url)" class="hover-pointer"
>{{ options.left.secondTitle }} &gt;</span
>
>{{ options.left.secondTitle }} &gt;</span>
</div>
<div class="content">
<div
@@ -18,7 +17,7 @@
<p>{{ item.name }}</p>
<p class="describe">{{ item.describe }}</p>
</div>
<img :src="item.img" alt="" />
<img :src="item.img" loading="lazy" decoding="async" alt="" />
</div>
</div>
</div>
@@ -27,8 +26,7 @@
<div class="top-header" :style="{ background: options.middle.bgColor }">
<span>{{ options.middle.title }}</span>
<span class="hover-pointer" @click="linkTo(options.middle.url)"
>{{ options.middle.secondTitle }} &gt;</span
>
>{{ options.middle.secondTitle }} &gt;</span>
</div>
<div class="content">
<div
@@ -41,7 +39,7 @@
<p>{{ item.name }}</p>
<p class="describe">{{ item.describe }}</p>
</div>
<img :src="item.img" alt="" />
<img :src="item.img" loading="lazy" decoding="async" alt="" />
</div>
</div>
</div>
@@ -50,8 +48,7 @@
<div class="top-header" :style="{ background: options.right.bgColor }">
<span>{{ options.right.title }}</span>
<span @click="linkTo(options.right.url)" class="hover-pointer"
>{{ options.right.secondTitle }} &gt;</span
>
>{{ options.right.secondTitle }} &gt;</span>
</div>
<div class="content">
<div
@@ -60,134 +57,236 @@
class="hover-pointer"
@click="linkTo(item.url)"
>
<img :src="item.img" alt="" />
<p>{{ item.name }}</p>
<p>{{ item.price | unitPrice("¥") }}</p>
<div class="jiaobiao" :class="'jiaobiao' + (index + 1)">
{{ index + 1 }}
<div class="img-wrap">
<img :src="item.img" loading="lazy" decoding="async" alt="" />
<div class="jiaobiao" :class="'jiaobiao' + (index + 1)">
{{ index + 1 }}
</div>
</div>
<p>{{ item.name }}</p>
<p>{{ $filters.unitPrice(item.price, "¥") }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object,
default: null
}
},
data () {
return {
options: this.data.options // 装修数据
};
},
methods: {}
};
<script setup>
import { ref } from 'vue'
const props = defineProps({
data: {
type: Object,
default: null
}
})
const options = ref(props.data.options) // 装修数据
</script>
<style lang="scss" scoped>
.new-goods {
display: flex;
justify-content: space-between;
align-items: stretch;
> div {
width: 393px;
height: 440px;
overflow: hidden;
}
.left > .content {
> div:nth-child(1) {
height: 240px;
flex-direction: column;
border: 1px solid #eee;
border-top: none;
border-left: none;
justify-content: space-between;
img {
width: 160px;
height: 160px;
}
.describe {
margin-top: 10px;
}
}
> div:nth-child(2) {
border-right: 1px solid #eee;
}
> div:nth-child(3),
> div:nth-child(4) {
border-bottom: 1px solid #eee;
}
}
.middle > .content {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
flex-direction: unset;
overflow: hidden;
> div {
width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
box-sizing: border-box;
display: flex;
padding: 6px 4px 4px 6px;
border-style: solid;
border-color: #eee;
border-width: 0;
border-bottom-width: 1px;
border-right-width: 1px;
img {
width: 64px;
height: 64px;
margin-top: 2px;
flex-shrink: 0;
object-fit: cover;
}
> div {
flex: 1;
min-width: 0;
p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
line-height: 1.3;
}
.describe {
margin-top: 4px;
font-size: 11px;
}
}
}
> div:nth-child(1) {
grid-row: span 2;
flex-direction: column;
justify-content: space-between;
align-items: center;
text-align: center;
padding: 8px 6px;
img {
width: 120px;
height: 120px;
margin-top: 6px;
}
> div {
flex: unset;
width: 100%;
p {
white-space: normal;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.describe {
margin-top: 6px;
white-space: normal;
}
}
}
> div:nth-child(1),
> div:nth-child(2),
> div:nth-child(3) {
> div:nth-child(3),
> div:nth-child(5) {
border-right-width: 0;
}
> div:nth-child(4),
> div:nth-child(5) {
border-bottom-width: 0;
}
}
.middle > .content {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(3, 1fr);
flex-direction: unset;
> div {
width: 100%;
height: 100%;
min-width: 0;
overflow: hidden;
box-sizing: border-box;
border-style: solid;
border-color: #eee;
border-width: 0;
border-bottom-width: 1px;
border-right-width: 1px;
}
> div:nth-child(6),
> div:nth-child(3) {
> div:nth-child(2n) {
border-right-width: 0;
}
> div:nth-child(n + 5) {
border-bottom-width: 0;
}
}
.right > .content {
display: flex;
flex-wrap: wrap;
flex-direction: row;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
flex-direction: unset;
flex-wrap: unset;
font-size: 12px;
overflow: hidden;
> div {
position: relative;
width: 120px;
padding: 5px 10px 0 10px;
img {
width: 100px;
height: 100px;
}
width: 100%;
height: 100%;
min-height: 0;
box-sizing: border-box;
padding: 4px 4px 2px;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
border-bottom: 1px solid #eee;
:nth-child(2) {
height: 38px;
border-right: 1px solid #eee;
.img-wrap {
position: relative;
width: 80px;
height: 80px;
flex-shrink: 0;
img {
width: 100%;
height: 100%;
display: block;
object-fit: cover;
}
.jiaobiao {
position: absolute;
width: 23px;
height: 23px;
top: 0;
right: 0;
margin: 0;
padding: 0;
max-height: none;
overflow: visible;
display: block;
background: url(../../../assets/images/festival_icon.png) no-repeat;
color: #fff;
text-align: center;
line-height: 23px;
font-size: 12px;
-webkit-line-clamp: unset;
-webkit-box-orient: unset;
}
.jiaobiao1,
.jiaobiao4 {
background-position: -2px -30px;
}
.jiaobiao2,
.jiaobiao5 {
background-position: -31px -30px;
}
.jiaobiao3,
.jiaobiao6 {
background-position: -60px -30px;
}
}
> p:nth-child(2) {
width: 100%;
height: auto;
max-height: 32px;
line-height: 16px;
margin-top: 4px;
overflow: hidden;
}
:nth-child(3) {
color: $theme_color;
margin-top: 5px;
}
.jiaobiao {
position: absolute;
width: 23px;
height: 23px;
top: 10px;
right: 16px;
background: url(../../../assets/images/festival_icon.png);
color: #fff;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-align: center;
}
.jiaobiao1,
.jiaobiao4 {
background-position: -2px -30px;
}
.jiaobiao2,
.jiaobiao5 {
background-position: -31px -30px;
}
.jiaobiao3,
.jiaobiao6 {
background-position: -60px -30px;
> p:nth-child(3) {
color: $theme_color;
margin-top: 2px;
line-height: 1.2;
flex-shrink: 0;
}
}
> div:nth-child(4),
> div:nth-child(5),
> div:nth-child(6) {
border: none;
> div:nth-child(3n) {
border-right: none;
}
> div:nth-child(n + 4) {
border-bottom: none;
}
}
@@ -212,6 +311,8 @@ export default {
flex-wrap: wrap;
flex-direction: column;
height: 370px;
box-sizing: border-box;
overflow: hidden;
}
.con-item {
width: 185px;
@@ -225,6 +326,42 @@ export default {
margin-top: 10px;
}
}
.middle > .content .con-item {
width: 100%;
max-width: 100%;
height: 100%;
min-width: 0;
min-height: 0;
overflow: hidden;
box-sizing: border-box;
padding: 6px 4px 4px 6px;
img {
width: 64px;
height: 64px;
margin-top: 2px;
flex-shrink: 0;
}
> div {
flex: 1;
min-width: 0;
p {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 12px;
line-height: 1.3;
}
.describe {
margin-top: 4px;
font-size: 11px;
}
}
}
.left > .content .con-item {
width: 100%;
max-width: 100%;
height: 100%;
}
.describe {
color: #999;
font-size: 12px;

View File

@@ -1,18 +1,20 @@
<template>
<div class="not-enough" ref="obtain" id="demo">
<Affix :offset-top="62" @on-change="change">
<ul class="nav-bar" v-show="topSearchShow">
<li
v-for="(item, index) in conData.options.navList"
:class="currentIndex === index ? 'curr' : ''"
@click="changeCurr(index)"
:key="index"
>
<p @click="gotoDemo">{{ item.title }}</p>
<p @click="gotoDemo">{{ item.desc }}</p>
</li>
</ul>
</Affix>
<el-affix :offset-top="62" @change="change" class="nav-affix">
<div class="nav-bar" v-show="topSearchShow">
<ul class="nav-list">
<li
v-for="(item, index) in conData.options.navList"
:class="currentIndex === index ? 'curr' : ''"
@click="changeCurr(index)"
:key="index"
>
<p @click="gotoDemo">{{ item.title }}</p>
<p @click="gotoDemo">{{ item.desc }}</p>
</li>
</ul>
</div>
</el-affix>
<div class="content" v-if="showContent">
<div
v-for="(item, index) in conData.options.list[currentIndex]"
@@ -20,93 +22,119 @@
class="hover-pointer"
@click="linkTo(item.url)"
>
<img :src="item.img" width="210" height="210" :alt="item.name" />
<img :src="item.img" loading="lazy" decoding="async" width="210" height="210" :alt="item.name" />
<p>{{ item.name }}</p>
<p>
<span>{{ Number(item.price) | unitPrice("¥") }}</span>
<span>{{ $filters.unitPrice(Number(item.price), "¥") }}</span>
</p>
</div>
</div>
</div>
</template>
<script>
export default {
mounted() {
window.addEventListener('scroll',this.handleScrollx,true)
<script setup>
import { onMounted, ref, watch } from 'vue';
import { useRouter } from 'vue-router';
const props = defineProps({
data: {
type: Object,
default: null,
},
props: {
data: {
type: Object,
default: null,
},
},
data() {
return {
screenHeight:document.body.clientHeight,
scrollHieght:0,
topSearchShow: true, //展示 导航条
topIndex: 0, // 当前滚动条位置 (取整)
scrollTops: 0,
open:'',
currentIndex: 0, // 当前分类下标
conData: this.data, // 装修数据
showContent: true, // 是否展示内容
};
},
watch: {
data: function (val) {
this.conData = val;
},
conData: function (val) {
this.$emit("content", val);
},
},
methods: {
handleScrollx(){
// console.log('滚动高度',window.pageYOffset) // 获取滚动条的高度
// console.log(this.$refs.obtain.getBoundingClientRect().top) //获取到距离顶部的距离
// this.scrollHieght = Number(window.pageYOffset);//获取到距离顶部的距离
// this.scrollTops = Number(this.$refs.obtain.getBoundingClientRect().top); // 获取到距离顶部的距离
// this.topSearchShow = true; // 展示图钉
// if(this.scrollTops < -660){ // 超过隐藏
// this.topSearchShow = false;
// }
},
toguid(path,id){
var path =path;
var Id = id;
localStorage.setItem('toId',Id);
this.$router.push(path);
},
change(status){ //获取是否获取到图钉
this.open = status
},
gotoDemo(){ // 跳转到demo的位置
if(this.open){ // 获取到图钉之后在跳转当前位置
document.querySelector("#demo").scrollIntoView(true);
}
//scrollIntoView()可以在所有的HTML元素上调用,通过滚动浏览器窗口或某个容器元素
},
changeCurr(index) {
// 选择分类
this.currentIndex = index;
},
},
};
});
const emit = defineEmits(['content']);
const router = useRouter();
const obtain = ref(null);
const screenHeight = ref(document.body.clientHeight);
const scrollHieght = ref(0);
const topSearchShow = ref(true); //展示 导航条
const topIndex = ref(0); // 当前滚动条位置 (取整)
const scrollTops = ref(0);
const open = ref('');
const currentIndex = ref(0); // 当前分类下标
const conData = ref(props.data); // 装修数据
const showContent = ref(true); // 是否展示内容
watch(
() => props.data,
(val) => {
conData.value = val;
}
);
watch(conData, (val) => {
emit('content', val);
});
function handleScrollx() {
// console.log('滚动高度',window.pageYOffset) // 获取滚动条的高度
// console.log(obtain.value.getBoundingClientRect().top) //获取到距离顶部的距离
// scrollHieght.value = Number(window.pageYOffset);//获取到距离顶部的距离
// scrollTops.value = Number(obtain.value.getBoundingClientRect().top); // 获取到距离顶部的距离
// topSearchShow.value = true; // 展示图钉
// if(scrollTops.value < -660){ // 超过隐藏
// topSearchShow.value = false;
// }
}
function toguid(path, id) {
var path = path;
var Id = id;
localStorage.setItem('toId', Id);
router.push(path);
}
function change(status) { //获取是否获取到图钉
open.value = status
}
function gotoDemo() { // 跳转到demo的位置
if (open.value) { // 获取到图钉之后在跳转当前位置
document.querySelector("#demo").scrollIntoView(true);
}
//scrollIntoView()可以在所有的HTML元素上调用,通过滚动浏览器窗口或某个容器元素
}
function changeCurr(index) {
// 选择分类
currentIndex.value = index;
}
onMounted(() => {
window.addEventListener('scroll', handleScrollx, true)
});
</script>
<style lang="scss" scoped>
.not-enough {
width: 100%;
box-sizing: border-box;
}
:deep(.nav-affix.el-affix) {
width: 100%;
}
.nav-bar {
display: flex;
justify-content: center;
width: 100%;
margin-bottom: 10px;
background-color: #f8f8f8;
height: 60px;
box-sizing: border-box;
}
.nav-list {
display: flex;
justify-content: center;
align-items: center;
position: relative;
position: sticky;
position: -webkit-sticky;
top: 0;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
list-style: none;
box-sizing: border-box;
li {
padding: 0 30px;
text-align: center;
@@ -114,11 +142,15 @@ export default {
font-size: 16px;
border-radius: 50px;
padding: 0 7px;
margin: 0;
line-height: 1.2;
}
p:nth-child(2) {
font-size: 14px;
color: #999;
margin: 2px 0 0;
line-height: 1.2;
}
&:hover {
@@ -129,6 +161,7 @@ export default {
}
border-right: 1px solid #eee;
}
li:last-of-type {
border: none;
}
@@ -136,7 +169,6 @@ export default {
.curr {
p:nth-child(1) {
background-color: $theme_color;
color: #fff;
}
p:nth-child(2) {
@@ -146,6 +178,8 @@ export default {
}
.content {
width: 100%;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
justify-content: space-between;

View File

@@ -7,21 +7,21 @@
</div>
<div class="content-left">
<div>
<img class="hover-pointer" @click="linkTo(msgLeft.list[0].url)" :src="msgLeft.list[0].img" width="160" height="160" alt="" />
<img class="hover-pointer" @click="linkTo(msgLeft.list[0].url)" :src="msgLeft.list[0].img" loading="lazy" decoding="async" width="160" height="160" alt="" />
<div class="margin-left">{{ msgLeft.list[0].name }}</div>
<div class="margin-left">{{ msgLeft.list[0].describe }}</div>
<Button
<el-button
size="small"
:style="{ background: msgLeft.bgColor }"
@click="linkTo(msgLeft.list[0].url)"
class="fz_12 view-btn"
>点击查看</Button
>点击查看</el-button
>
</div>
<div>
<template v-for="(item, index) in msgLeft.list">
<div v-if="index != 0" :key="index" @click="linkTo(item.url)" class="hover-pointer">
<img :src="item.img" width="80" height="80" alt="" />
<img :src="item.img" loading="lazy" decoding="async" width="80" height="80" alt="" />
<div>
<div>{{ item.name }}</div>
<div>{{ item.describe }}</div>
@@ -36,8 +36,7 @@
<div class="head-recommend" :style="{ background: msgRight.bgColor }">
<span>{{ msgRight.title }}</span>
<span @click="linkTo(msgRight.url)" class="hover-pointer"
>{{ msgRight.secondTitle }}&gt;</span
>
>{{ msgRight.secondTitle }}&gt;</span>
</div>
<div class="content-right">
<div
@@ -45,13 +44,13 @@
:key="index"
@click="linkTo(item.url)" class="hover-pointer"
>
<div class="right-item" :style="{'border': index===2 || index===3 ?'none': ''}">
<div class="right-item">
<div>
<span :style="{ background: msgRight.bgColor }">{{item.name}}</span>
<span>{{ item.describe }}</span>
</div>
<div class="right-img">
<img :src="item.img" alt="" />
<img :src="item.img" loading="lazy" decoding="async" alt="" />
</div>
</div>
</div>
@@ -59,22 +58,18 @@
</div>
</div>
</template>
<script>
export default {
props: {
data: {
type: Object,
default: {}
}
},
data () {
return {
msgLeft: this.data.options.contentLeft, // 左侧数据
msgRight: this.data.options.contentRight // 右侧数据
};
},
methods: {}
};
<script setup>
import { ref } from 'vue';
const props = defineProps({
data: {
type: Object,
default: () => ({})
}
});
const msgLeft = ref(props.data.options.contentLeft); // 左侧数据
const msgRight = ref(props.data.options.contentRight); // 右侧数据
</script>
<style lang="scss" scoped>
.recommend {
@@ -84,12 +79,17 @@ export default {
width: 595px;
.content-left {
display: flex;
align-items: stretch;
padding-top: 10px;
font-size: 12px;
box-sizing: content-box;
> div:nth-child(1) {
width: 189px;
border-right: 1px solid #eee;
height: 360px;
display: flex;
flex-direction: column;
box-sizing: border-box;
img {
margin: 40px 0 0 15px;
}
@@ -107,15 +107,20 @@ export default {
color: #999;
}
.view-btn {
margin-left: 15px;
margin-top: 10px;
align-self: center;
width: 145px;
margin-top: auto;
margin-bottom: 15px;
color: #fff;
}
}
> div:nth-child(2) {
width: 405px;
height: 360px;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
box-sizing: border-box;
> div {
display: flex;
align-items: center;
@@ -136,65 +141,85 @@ export default {
.recommend-right {
width: 595px;
height: 360px;
.head-recommend {
background: #a25684;
}
.content-right {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-rows: repeat(2, 1fr);
height: 360px;
padding-top: 10px;
box-sizing: content-box;
> div {
width: 50%;
width: 100%;
height: 100%;
text-align: center;
height: 180px;
padding-top: 10px;
padding: 0;
border-right: 1px solid #eee;
border-bottom: 1px solid #eee;
box-sizing: border-box;
&:nth-child(2n) {
border-right: none;
}
&:nth-child(n + 3) {
border-bottom: none;
}
.right-item {
border-bottom: 1px solid #eee;
border-bottom: none;
display: flex;
margin-top: 30px;
margin-left: 5px;
margin-right: 5px;
height: 150px;
padding: 0 10px;
align-items: center;
justify-content: space-between;
height: 100%;
margin: 0;
padding: 10px 15px;
font-size: 12px;
>div:nth-child(1) {
box-sizing: border-box;
margin-top: 30px;
span:nth-child(1){
color: #fff;
border-radius: 10px;
padding: 0 5px;
background-color: #a25684;
display: block;
> div:nth-child(1) {
flex: 1;
min-width: 0;
margin-top: 0;
text-align: left;
overflow: hidden;
white-space: nowrap;
margin: 0 10px 10px 0;
span:nth-child(1) {
color: #fff;
border-radius: 10px;
padding: 0 5px;
background-color: #a25684;
display: block;
max-width: 120px;
overflow: hidden;
white-space: nowrap;
margin: 0 0 10px;
}
span:nth-child(2) {
font-size: 12px;
color: #666;
display: block;
font-size: 12px;
color: #666;
display: block;
}
}
.right-img {
width: 100;
width: 100px;
height: 100px;
flex-shrink: 0;
text-align: center;
margin: 0 auto;
img{
max-height: 100px;
max-width: 100px;
margin: 0;
img {
max-height: 100px;
max-width: 100px;
}
}
}
}
> div:nth-child(n + 1) {
border-right: 1px solid #eee;
}
}
}

View File

@@ -1,5 +1,5 @@
<template>
<div class="seckill" v-if="goodsList.length">
<div class="seckill" v-if="goodsList && goodsList.length">
<div class="aside hover-pointer" @click="goToSeckill">
<div class="title">{{ actName }}</div>
<div class="hour">
@@ -12,156 +12,114 @@
</div>
<div class="act-status" v-else>未开始</div>
</div>
<swiper :options="swiperOption" ref="mySwiper">
<swiper-slide v-for="(item,index) in goodsList" :key="index">
<div class="content hover-pointer" @click.stop="goToSeckill">
<img :src="item.goodsImage" width="140" height="140" :alt="item.goodsName">
<div class="ellipsis">{{item.goodsName}}</div>
<div>
<span>{{ item.price | unitPrice('¥') }}</span>
<span>{{ item.originalPrice | unitPrice('¥') }}</span>
</div>
<div class="goods-scroll">
<div
v-for="(item, index) in goodsList"
:key="index"
class="content hover-pointer"
@click.stop="goToSeckill"
>
<img :src="item.goodsImage" loading="lazy" decoding="async" width="140" height="140" :alt="item.goodsName" />
<div class="ellipsis">{{ item.goodsName }}</div>
<div>
<span>{{ $filters.unitPrice(item.price, "¥") }}</span>
<span>{{ $filters.unitPrice(item.originalPrice, "¥") }}</span>
</div>
</swiper-slide>
<div class="swiper-button-prev" slot="button-prev">
<Icon type="ios-arrow-back" />
</div>
<div class="swiper-button-next" slot="button-next">
<Icon type="ios-arrow-forward" />
</div>
</swiper>
</div>
</div>
</template>
<script>
// 引入插件
import { swiper, swiperSlide } from 'vue-awesome-swiper';
import 'swiper/dist/css/swiper.css';
export default {
components: {
swiper,
swiperSlide
},
props: {
data: Object
},
data () {
return {
list: [], // 秒杀时段列表
goodsList: [], // 商品列表
actStatus: 0, // 0 未开始 1 进行中
actName: '限时秒杀', // 活动名称
currIndex: 0, // 当前时间段的下标
currHour: '00', // 当前秒杀场
diffSeconds: 0, // 倒计时秒数
hours: 0, // 小时
minutes: 0, // 分钟
seconds: 0, // 秒
interval: null, // 定时器
swiperOption: { // 轮播图参数
<script setup>
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
slidesPerView: 5,
// 设置点击箭头
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
}
};
},
watch: {
currIndex (val) {
clearInterval(this.interval)
this.interval = null
this.countDown(val)
this.goodsList = this.list[val].seckillGoodsList
},
diffSeconds (val) {
const hours = Math.floor(val / 3600);
// 当前秒数 / 60,向下取整
// 获取到所有分钟数 3600 / 60 = 60分钟
// 对60取模,超过小时数的分钟数
const minutes = Math.floor(val / 60) % 60;
// 当前的秒数 % 60,获取到 超过小时数、分钟数的秒数(秒数)
const seconds = val % 60;
this.hours = filteTime(hours)
this.minutes = filteTime(minutes)
this.seconds = filteTime(seconds)
if (val <= 0) {
clearInterval(this.interval)
this.interval = null
}
function filteTime (time) {
if (time < 10) {
return '0' + time
} else {
return time
}
}
}
},
computed: {
swiper () { // 轮播组件
return this.$refs.mySwiper.swiper;
}
},
beforeDestroy () {
// 销毁前清除定时器
clearInterval(this.interval);
},
mounted () {
this.getListByDay()
},
methods: {
goToSeckill () { // 跳转秒杀页面
let routeUrl = this.$router.resolve({
path: '/seckill'
});
window.open(routeUrl.href, '_blank');
},
countDown (currIndex) { // 倒计时
// 0点时间戳
let zeroTime = new Date(new Date().toLocaleDateString()).getTime();
let currTime = new Date().getTime()
let actTime = 0;
let nowHour = new Date().getHours(); // 当前小时数
if (this.list[currIndex].timeLine > nowHour) { // 活动未开始
this.actStatus = 0;
actTime = zeroTime + this.list[currIndex].timeLine * 3600 * 1000
} else if (this.list[currIndex].timeLine <= nowHour) { // 活动进行中
this.actStatus = 1;
if (currIndex === this.list.length - 1) { // 如果是最后一个活动,直到24点结束
actTime = zeroTime + 24 * 3600 * 1000
} else {
actTime = zeroTime + this.list[currIndex + 1].timeLine * 3600 * 1000
}
}
this.currHour = this.list[this.currIndex].timeLine
this.diffSeconds = Math.floor((actTime - currTime) / 1000)
this.interval = setInterval(() => {
this.diffSeconds--
}, 1000)
},
getListByDay () { // 当天秒杀活动
// const list = [
// {
// timeLine: 18,
// seckillGoodsList: [
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12},
// {goodsImage: 'https://lilishop-oss.oss-cn-beijing.aliyuncs.com/a9593607de404546953055f279fd5d54.png', goodsName: 'dfsdgsdf', originalPrice: 39, price: 12}
// ]
// }
// ]
this.list = this.data.options.list
this.goodsList = this.list[0].seckillGoodsList
console.log( this.goodsList)
this.countDown(this.currIndex)
const props = defineProps({
data: Object,
});
const router = useRouter();
const list = ref([]);
const goodsList = ref([]);
const actStatus = ref(0);
const actName = ref("限时秒杀");
const currIndex = ref(0);
const currHour = ref("00");
const diffSeconds = ref(0);
const hours = ref(0);
const minutes = ref(0);
const seconds = ref(0);
const interval = ref(null);
watch(currIndex, (val) => {
clearInterval(interval.value);
interval.value = null;
countDown(val);
goodsList.value = list.value[val]?.seckillGoodsList || [];
});
watch(diffSeconds, (val) => {
const h = Math.floor(val / 3600);
const m = Math.floor(val / 60) % 60;
const s = val % 60;
hours.value = filteTime(h);
minutes.value = filteTime(m);
seconds.value = filteTime(s);
if (val <= 0) {
clearInterval(interval.value);
interval.value = null;
}
function filteTime(time) {
return time < 10 ? "0" + time : time;
}
});
onBeforeUnmount(() => {
clearInterval(interval.value);
});
onMounted(() => {
getListByDay();
});
function goToSeckill() {
router.push("/seckill");
}
function countDown(index) {
const zeroTime = new Date(new Date().toLocaleDateString()).getTime();
const currTime = new Date().getTime();
let actTime = 0;
const nowHour = new Date().getHours();
if (list.value[index].timeLine > nowHour) {
actStatus.value = 0;
actTime = zeroTime + list.value[index].timeLine * 3600 * 1000;
} else if (list.value[index].timeLine <= nowHour) {
actStatus.value = 1;
if (index === list.value.length - 1) {
actTime = zeroTime + 24 * 3600 * 1000;
} else {
actTime = zeroTime + list.value[index + 1].timeLine * 3600 * 1000;
}
}
};
currHour.value = list.value[currIndex.value].timeLine;
diffSeconds.value = Math.floor((actTime - currTime) / 1000);
interval.value = setInterval(() => {
diffSeconds.value--;
}, 1000);
}
function getListByDay() {
list.value = props.data?.options?.list || [];
if (!list.value.length) {
goodsList.value = [];
return;
}
goodsList.value = list.value[0]?.seckillGoodsList || [];
if (goodsList.value.length) {
countDown(currIndex.value);
}
}
</script>
<style lang="scss" scoped>
.seckill {
@@ -174,15 +132,13 @@ export default {
width: 190px;
height: 100%;
color: #fff;
background-image: url("../../../assets/images/seckillBg.png");
background: linear-gradient(180deg, var(--theme-color, #F31947) 0%, var(--theme-light, #ff6b35) 100%);
.title {
width: 100%;
text-align: center;
font-size: 28px;
margin-top: 31px;
}
.hour {
margin-top: 90px;
text-align: center;
@@ -190,7 +146,6 @@ export default {
font-size: 18px;
}
}
.count-down {
margin: 10px 0 0 30px;
> span {
@@ -219,15 +174,21 @@ export default {
content: "";
}
}
.act-status {
margin: 10px 0 0 65px;
font-size: 20px;
}
}
.goods-scroll {
height: 260px;
flex: 1;
margin-left: 10px;
background-color: #fff;
display: flex;
overflow-x: auto;
}
.content {
width: 200px;
min-width: 200px;
display: flex;
justify-content: center;
align-items: center;
@@ -262,26 +223,12 @@ export default {
line-height: 24px;
display: flex;
text-align: center;
span:nth-child(1) {
color: #fff;
font-size: 16px;
width: 92px;
background-color: $theme_color;
position: relative;
&::before {
content: " ";
width: 0;
height: 0;
border-color: transparent white transparent transparent;
border-style: solid;
border-width: 24px 8px 0 0;
position: absolute;
top: 0;
left: 84px;
}
}
span:nth-child(2) {
color: #999;
width: 66px;
@@ -290,35 +237,4 @@ export default {
}
}
}
.swiper-container {
height: 260px;
width: 1000px;
margin-left: 10px;
background-color: #fff;
}
.swiper-button-prev, .swiper-button-next {
background: #ccc;
width: 25px;
height: 35px;
font-size: 16px;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.swiper-button-prev:hover, .swiper-button-next:hover {
background: #aaa;
}
.swiper-button-prev {
left: 0;
border-bottom-right-radius: 18px;
border-top-right-radius: 18px;
padding-right: 5px;
}
.swiper-button-next {
right: 0;
border-top-left-radius: 18px;
border-bottom-left-radius: 18px;
padding-left: 5px;
}
</style>

View File

@@ -2,14 +2,14 @@
<div class="model" v-if="data">
<div class="for-your">{{ data.options.title }}</div>
<div class="flex card">
<div class="left">
<img :src="data.options.data.image.src" alt="" />
<div class="left hover-pointer" @click="linkTo(data.options.data.image.url)">
<img :src="data.options.data.image.src" loading="lazy" decoding="async" alt="" />
</div>
<div class="right flex">
<!-- 商品区 -->
<div class="goods-list">
<div class="for-your-goods-list">
<div
class="goods-item"
class="for-your-goods-item"
:key="index"
v-for="(item, index) in data.options.data.list"
@click="linkTo(item.url)"
@@ -18,7 +18,7 @@
<div class="goods-name">{{ item.title }}</div>
<div class="goods-desc">{{ item.desc }}</div>
</div>
<img class="goods-img" :src="item.img" alt="" />
<img class="goods-img" :src="item.img" loading="lazy" decoding="async" alt="" />
</div>
</div>
<!-- 热卖区 -->
@@ -30,11 +30,11 @@
:key="index"
@click="linkTo(item.url)"
>
<img :src="item.img" alt="" />
<img :src="item.img" loading="lazy" decoding="async" alt="" />
<div class="hot-goods">
<div class="hot-goods-title">{{ item.title }}</div>
<div>
<div class="hot-price">¥{{ item.price | unitPrice }}</div>
<div class="hot-price">¥{{ $filters.unitPrice(item.price) }}</div>
</div>
</div>
</div>
@@ -44,35 +44,34 @@
</div>
</template>
<script>
export default {
name: "forYour",
data() {
return {
current: "",
};
},
props: {
data: {
type: Object,
default: {},
},
},
<script setup>
import { ref } from 'vue'
mounted() {},
methods: {},
};
defineOptions({ name: 'forYour' })
defineProps({
data: {
type: Object,
default: () => ({}),
},
})
const current = ref('')
</script>
<style scoped lang="scss">
.goods-list {
.for-your-goods-list {
display: flex;
flex: 0 0 558px;
width: 558px;
flex-wrap: wrap;
column-gap: 0;
}
.goods-item {
.for-your-goods-item {
flex: 0 0 278px;
width: 278px;
height: 277px;
min-width: 0;
text-align: center;
box-sizing: border-box;
cursor: pointer;
@@ -84,10 +83,8 @@ export default {
border-top: 1.4px solid #e2e2e2;
}
&:hover {
/deep/ .goods-name {
color: $theme_color;
}
/deep/ .goods-desc {
.goods-name,
.goods-desc {
color: $theme_color;
}
}
@@ -105,10 +102,12 @@ export default {
color: #333333;
}
.goods-img {
display: block;
width: 190px;
height: 156px;
margin-top: 19px;
margin: 19px auto 0;
border-radius: 10px;
object-fit: contain;
}
.goods-desc {
font-size: 14px;
@@ -137,6 +136,8 @@ export default {
background: #ffffff;
border-radius: 10px;
position: relative;
overflow: hidden;
flex-shrink: 0;
box-shadow: 0px 1px 13px 0px #E5E5E5;
&:hover {
@@ -159,20 +160,27 @@ export default {
}
}
.left {
flex: 0 0 346px;
width: 346px;
height: 554px;
border-radius: 9.8px 0px 0px 9.8px;
overflow: hidden;
> img {
max-width: 100%;
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
}
.right {
flex: 0 0 839px;
width: 839px;
height: 554px;
border-radius: 0px 9.8px 9.8px 0px;
overflow: hidden;
}
.hot-list {
flex: 0 0 279px;
width: 279px;
}
.hot-title {
@@ -201,8 +209,11 @@ export default {
}
> img {
flex-shrink: 0;
display: block;
width: 76.3px;
height: 77.7px;
object-fit: contain;
}
}
.hot-goods {

View File

@@ -15,9 +15,8 @@
</div>
</div>
<!-- 商品部分 -->
<div class="only-goods-list flex-j-sb">
<div class="only-goods-list flex-j-sb" v-if="paramData.options">
<div
v-if="paramData.options"
class="only-goods-list-item "
v-for="(item, index) in paramData.options.list.filter((client) => {
return client.___index == current;
@@ -26,14 +25,14 @@
@click="linkTo(item.url)"
>
<div class="goods-img">
<img :src="item.img" />
<img :src="item.img" loading="lazy" decoding="async" />
</div>
<div>
<div class="goods-name wes-2">{{ item.title }}</div>
<div class="goods-desc">{{ item.desc }}</div>
</div>
<div class="goods-price">
{{ item.price | unitPrice("¥") }}
{{ $filters.unitPrice(item.price, "¥") }}
</div>
<!-- 查看 -->
<div class="white-shadow">
@@ -44,31 +43,28 @@
</div>
</template>
<script>
export default {
name: "goods",
props: {
paramData: {
type: Object,
default: {},
},
<script setup>
import { onMounted, ref } from "vue";
defineOptions({ name: "goods" });
const props = defineProps({
paramData: {
type: Object,
default: () => ({}),
},
data() {
return {
current: "",
};
},
watch: {},
mounted() {
this.current = this.paramData.options.labels[0].___index;
},
methods: {
// 筛选出当前分类下的商品
handleClickLabel(val, index) {
this.current = index;
},
},
};
});
const current = ref("");
onMounted(() => {
current.value = props.paramData.options.labels[0].___index;
});
// 筛选出当前分类下的商品
function handleClickLabel(val, index) {
current.value = index;
}
</script>
<style scoped lang="scss">
@@ -173,7 +169,7 @@ export default {
text-align: center;
letter-spacing: 0px;
color: #f31947;
color: $theme_color;
-webkit-text-stroke: #979797 0.7px; /* 浏览器可能不支持 */
}
.goods-type-line {
@@ -190,7 +186,7 @@ export default {
color: #333333;
}
.active {
color: #f31947;
color: $theme_color;
}
.goods-type-labels {
cursor: pointer;

View File

@@ -11,29 +11,18 @@
</div>
</template>
<script>
<script setup>
import left from './mixs/mix-goods'
import right from './mixs/mix-brand'
export default {
name: "mixModel",
data() {
return {}
},
props: {
data: {
type: Object,
default: {}
}
},
components: {
left,
right
},
mounted() {
},
methods: {}
}
defineOptions({ name: "mixModel" });
defineProps({
data: {
type: Object,
default: () => ({})
}
})
</script>
<style scoped lang="scss">
@@ -49,5 +38,6 @@ export default {
background: #FFFFFF;
box-shadow: 0px 1px 13px 0px #E5E5E5;
position: relative;
overflow: hidden;
}
</style>

View File

@@ -1,20 +1,23 @@
<template>
<div class="flex" v-if="data.options.right.model == 'brand'">
<div class="left-side" @click="linkTo(data.options.right.data.image.url)">
<img :src="data.options.right.data.image.src" alt="">
<div class="mix-panel flex" v-if="data.options.right.model == 'brand'">
<div class="left-side hover-pointer" @click="linkTo(data.options.right.data.image.url)">
<img :src="data.options.right.data.image.src" loading="lazy" decoding="async" alt="" />
</div>
<div class="right-side">
<div class="badge-box flex">
<div class="round">
<Icon type="ios-arrow-forward" />
<el-icon><ArrowRight /></el-icon>
</div>
</div>
<div class="flex goods-list">
<div class="goods-item flex" @click="linkTo(item.url)" :key="index" v-for="(item,index) in data.options.right.data.list">
<div
class="goods-item flex hover-pointer"
@click="linkTo(item.url)"
:key="index"
v-for="(item, index) in data.options.right.data.list"
>
<div class="goods-thumbnail">
<img :src="item.img" alt="">
<img :src="item.img" loading="lazy" decoding="async" alt="" />
</div>
</div>
</div>
@@ -22,63 +25,59 @@
</div>
</template>
<script>
export default {
name: "mix-goods",
data() {
return {}
},
props: {
data: {
type: Object,
default: {}
}
},
components: {},
mounted() {
<script setup>
import { ArrowRight } from "@element-plus/icons-vue";
defineOptions({ name: "mix-brand" });
defineProps({
data: {
type: Object,
default: () => ({}),
},
methods: {}
}
});
</script>
<style scoped lang="scss">
.right-side{
width: 330px;
.mix-panel {
height: 100%;
align-items: stretch;
overflow: hidden;
}
.goods-detail-title{
font-size: 13px;
font-weight: normal;
line-height: 16px;
text-align: center;
letter-spacing: 0px;
color: #333333;
}
.goods-detail-price{
font-size: 14px;
font-weight: normal;
line-height: 17px;
letter-spacing: 0px;
color: $theme_color;
margin-top: 8px;
margin-bottom:10px;
}
.goods-thumbnail{
margin-left: 7px;
display: flex;
align-items: center;
>img{
width:90px;
height: 90px;
.left-side {
flex-shrink: 0;
width: 254px;
height: 344px;
> img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 9.8px 0 0 9.8px;
}
}
.badge-box{
margin-top: 33px;
justify-content:right;
>.round{
.right-side {
flex: 1;
min-width: 0;
height: 344px;
display: flex;
flex-direction: column;
box-sizing: border-box;
overflow: hidden;
}
.badge-box {
margin-top: 24px;
margin-bottom: 8px;
justify-content: flex-end;
flex-shrink: 0;
> .round {
width: 17.5px;
height: 17.5px;
opacity: 1;
border-radius: 50%;
text-align: center;
line-height: 17.5px;
@@ -86,47 +85,48 @@ export default {
margin-right: 17.5px;
}
}
.goods-list{
width: 330px;
.goods-list {
width: 100%;
box-sizing: border-box;
flex-wrap: wrap;
justify-content:space-between;
padding: 0 16px;
justify-content: space-between;
align-content: flex-start;
padding: 0 16px 12px;
gap: 8px 0;
flex: 1;
min-height: 0;
overflow: hidden;
}
.goods-item{
.goods-item {
display: flex;
align-items: center;
justify-content: center;
width:50%;
height: 85px;
flex: 0 0 calc(50% - 4px);
width: calc(50% - 4px);
height: 82px;
border-radius: 9.8px;
opacity: 1;
transition: .35s;
background: #FFFFFF;
margin-bottom:9px;
box-sizing: border-box;
overflow: hidden;
background: #ffffff;
transition: 0.35s;
cursor: pointer;
}
.left-side {
>img{
width: 254px;
height: 344px;
border-radius: 9.8px 0px 0px 9.8px;
opacity: 1;
.goods-thumbnail {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
> img {
max-width: 80px;
max-height: 76px;
width: auto;
height: auto;
object-fit: contain;
}
}
.badge {
width: 50px;
height: 27px;
line-height:27px;
border-radius: 13.3px 0px 0px 13.3px;
opacity: 1;
background: #F31947;
font-size: 12.6px;
font-weight: normal;
text-align: center;
letter-spacing: 0px;
color: #FFFFFF;
margin-top: 26px;
margin-bottom:17px;
}
</style>

View File

@@ -1,127 +1,148 @@
<template>
<div class="flex" v-if="data.options.left.model == 'goods'">
<div class="left-side" @click="linkTo(data.options.right.data.image.url)">
<img :src="data.options.left.data.image.src" alt="">
<div class="mix-panel flex" v-if="data.options.left.model == 'goods'">
<div class="left-side hover-pointer" @click="linkTo(data.options.left.data.image.url)">
<img :src="data.options.left.data.image.src" loading="lazy" decoding="async" alt="" />
</div>
<div class="right-side">
<div class="badge-box flex">
<div class="badge hover-pointer" @click="linkTo(data.options.left.data.badge.url)">
{{ data.options.left.data.badge.label }}
</div>
</div>
<div class="right-side">
<div class="badge-box flex">
<div class="badge" @click="linkTo(data.options.right.data.image.url)">
{{data.options.left.data.badge.label}}
</div>
<div class="flex goods-list">
<div
class="goods-item flex hover-pointer"
@click="linkTo(item.url)"
:key="index"
v-for="(item, index) in data.options.left.data.list"
>
<div class="goods-thumbnail">
<img :src="item.img" loading="lazy" decoding="async" alt="" />
</div>
<div class="flex goods-list">
<div class="goods-item flex" @click="linkTo(item.url)" :key="index" v-for="(item,index) in data.options.left.data.list">
<div class="goods-thumbnail">
<img :src="item.img" alt="">
</div>
<div class="goods-detail">
<div class="goods-detail-title">{{item.title}}</div>
<div class="goods-detail-price">{{item.price | unitPrice('¥')}}</div>
</div>
</div>
<div class="goods-detail">
<div class="goods-detail-title">{{ item.title }}</div>
<div class="goods-detail-price">{{ $filters.unitPrice(item.price, '¥') }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "mix-goods",
data() {
return {}
},
props: {
data: {
type: Object,
default: {}
}
},
components: {},
mounted() {
<script setup>
defineOptions({ name: "mix-goods" });
defineProps({
data: {
type: Object,
default: () => ({}),
},
methods: {}
}
});
</script>
<style scoped lang="scss">
.right-side{
width: 387px;
.mix-panel {
height: 100%;
align-items: stretch;
overflow: hidden;
}
.goods-detail-title{
.left-side {
flex-shrink: 0;
width: 196.7px;
height: 344px;
> img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 10px 0 0 10px;
}
}
.right-side {
flex: 1;
min-width: 0;
height: 344px;
display: flex;
flex-direction: column;
justify-content: center;
box-sizing: border-box;
overflow: hidden;
}
.goods-detail-title {
font-size: 13px;
font-weight: normal;
line-height: 16px;
text-align: center;
letter-spacing: 0px;
color: #333333;
}
.goods-detail-price{
.goods-detail-price {
font-size: 14px;
font-weight: normal;
line-height: 17px;
letter-spacing: 0px;
color: #F31947;
color: $theme_color;
margin-top: 8px;
margin-bottom:10px;
}
.goods-thumbnail{
.goods-thumbnail {
margin-left: 7px;
display: flex;
align-items: center;
>img{
width:90px;
flex-shrink: 0;
> img {
width: 90px;
height: 90px;
object-fit: contain;
}
}
.badge-box{
justify-content:right;
.badge-box {
justify-content: flex-end;
flex-shrink: 0;
}
.goods-list{
width: 387px;
.goods-list {
width: 100%;
box-sizing: border-box;
flex-wrap: wrap;
justify-content:space-between;
justify-content: space-between;
padding: 0 16px;
gap: 9px 0;
}
.goods-item{
.goods-item {
cursor: pointer;
display: flex;
align-items: center;
width: 173.6px;
flex: 0 0 calc(50% - 4px);
width: calc(50% - 4px);
height: 119px;
border-radius: 9.8px;
opacity: 1;
transition: .35s;
background: #FFFFFF;
margin-bottom:9px;
&:hover{
box-shadow: 0px 1px 6px 0px #E5E5E5;
}
box-sizing: border-box;
overflow: hidden;
background: #ffffff;
transition: 0.35s;
}
.left-side {
>img{
border-radius: 10px 0px 0px 10px;
display: block;
width: 196.7px;
height: 343.7px;
&:hover {
box-shadow: 0 1px 6px 0 #e5e5e5;
}
}
.badge {
width: 50px;
height: 27px;
line-height:27px;
border-radius: 13.3px 0px 0px 13.3px;
opacity: 1;
background: #F31947;
line-height: 27px;
border-radius: 13.3px 0 0 13.3px;
background: $theme_color;
font-size: 12.6px;
font-weight: normal;
text-align: center;
letter-spacing: 0px;
color: #FFFFFF;
color: #ffffff;
margin-top: 26px;
margin-bottom:17px;
margin-bottom: 17px;
}
</style>

View File

@@ -1,36 +1,40 @@
<template>
<div class="line flex flex-a-c flex-j-sb">
<div class="column" v-for="(item,index) in data.options.list" :key="index">
<img :src="item.img" class="three-column-img">
<div
class="column hover-pointer"
v-for="(item, index) in data.options.list"
:key="index"
@click="linkTo(item.url)"
>
<img :src="item.img" loading="lazy" decoding="async" class="three-column-img" alt="" />
</div>
</div>
</template>
<script>
export default {
name: "oneRowThreeColumns",
props: {
data: {
type: Object,
default: {}
}
},
data () {
return {
<script setup>
defineOptions({ name: "oneRowThreeColumns" });
};
},
methods: {}
}
defineProps({
data: {
type: Object,
default: () => ({})
}
});
</script>
<style scoped lang="scss">
.column{
.line {
width: 100%;
}
.column {
width: 385px;
height: 165px;
>img{
cursor: pointer;
> img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
}
</style>

View File

@@ -13,7 +13,7 @@
<div class="goods-desc">{{ item.desc }}</div>
</div>
<div class="goods-img">
<img :src="item.img" />
<img :src="item.img" loading="lazy" decoding="async" />
</div>
</div>
</div>
@@ -21,35 +21,32 @@
</div>
</template>
<script>
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import { openLink } from "@/utils/buyerLogin";
export default {
name: "onlyGoodsList",
props: {
data: {
type: Object,
default: {},
},
},
data() {
return {
flag:false,
tabIndex:0,
current: 0,
showModal: false,
selected: {}, // 已选数据
picModelFlag: false,
};
},
mounted(){
defineOptions({ name: "onlyGoodsList" });
defineProps({
data: {
type: Object,
default: () => ({}),
},
methods: {
handleClick(val){
this.linkTo(val.url)
},
},
};
});
const router = useRouter();
const flag = ref(false);
const tabIndex = ref(0);
const current = ref(0);
const showModal = ref(false);
const selected = ref({}); // 已选数据
const picModelFlag = ref(false);
function handleClick(val) {
openLink(val.url, router);
}
</script>
<style scoped lang="scss">

View File

@@ -0,0 +1,62 @@
.setup-content{
position: relative;
&:hover{
.setup-box{
display: block;
}
}
}
.setup-box{
display: none;
position: absolute;
top: 0;
left: 0;
background: rgba(0, 0, 0, .2);
z-index: 99;
width: 100%;
height: 100%;
.el-button{
float: right;
margin-right: 5px;
margin-top: 5px;
}
}
.modal-tab-bar{
margin-left: 20px;
table{
margin-top: 10px;
display:inline-block;
border: 1px solid #ddd;
border-radius: 5px;
max-height: 400px !important;
overflow: hidden auto;
}
thead{
background-color: #eee;
th{
padding: 5px 0;
}
}
td{
padding: 10px;
text-align: center;
}
tbody>tr:hover{
background-color: aliceblue;
}
}
.modal-top-advert{
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
>*{
margin-bottom: 10px;
}
.el-input{
width: 200px;
}
}

View File

@@ -1,150 +1,296 @@
<template>
<div class="invoice-modal">
<Modal v-model="invoiceAvailable" width="600" footer-hide>
<p slot="header">
<el-dialog v-model="invoiceAvailable" width="640">
<template #header><p>
<span>发票信息</span>
</p>
<!-- 普通发票 -->
</p></template>
<div class="nav-content">
<Form :model="invoiceForm" ref="form" label-position="left" :rules="ruleInline" :label-width="110">
<FormItem label="发票类型">
<RadioGroup v-model="invoice" type="button" button-style="solid">
<Radio @on-change="changeInvoice" :label="1">电子普通发票</Radio>
<Radio :label="2" :disabled="true">增值税专用发票</Radio>
</RadioGroup>
</FormItem>
<FormItem label="发票抬头">
<RadioGroup v-model="type" @on-change="changeInvoice" type="button" button-style="solid">
<Radio :label="1">个人</Radio>
<Radio :label="2">单位</Radio>
</RadioGroup>
</FormItem>
<FormItem label="个人名称" v-if="type === 1" prop="receiptTitle">
<i-input v-model="invoiceForm.receiptTitle"></i-input>
</FormItem>
<FormItem label="单位名称" v-if="type === 2" prop="receiptTitle">
<i-input v-model="invoiceForm.receiptTitle"></i-input>
</FormItem>
<FormItem label="纳税人识别号" v-if="type === 2" prop="taxpayerId">
<i-input v-model="invoiceForm.taxpayerId"></i-input>
</FormItem>
<FormItem label="发票内容">
<RadioGroup v-model="invoiceForm.receiptContent" type="button" button-style="solid">
<Radio label="商品明细">商品明细</Radio>
<Radio label="商品类别">商品类别</Radio>
</RadioGroup>
</FormItem>
</Form>
<el-form :model="invoiceForm" ref="formRef" label-position="left" :rules="formRules" label-width="118px">
<el-form-item label="发票类型">
<el-radio-group v-model="invoice" type="button" button-style="solid" @change="onInvoiceKindChange">
<el-radio :label="1">电子普通发票</el-radio>
<el-radio :label="2">增值税专用发票</el-radio>
</el-radio-group>
</el-form-item>
<!-- 电子普通发票 -->
<div v-if="invoice === 1" key="invoice-normal">
<el-form-item label="发票抬头">
<el-radio-group v-model="type" type="button" button-style="solid" @change="onHeaderTypeChange">
<el-radio :label="1">个人</el-radio>
<el-radio :label="2">单位</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="type === 1 ? '个人名称' : '单位名称'" :prop="type === 1 ? 'personalName' : 'companyName'">
<el-input v-if="type === 1" v-model="invoiceForm.personalName"></el-input>
<el-input v-else v-model="invoiceForm.companyName"></el-input>
</el-form-item>
<el-form-item label="纳税人识别号" v-if="type === 2" prop="taxpayerId">
<el-input v-model="invoiceForm.taxpayerId"></el-input>
</el-form-item>
<el-form-item label="发票内容">
<el-radio-group v-model="invoiceForm.receiptContent" type="button" button-style="solid">
<el-radio label="商品明细">商品明细</el-radio>
<el-radio label="商品类别">商品类别</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="收票人手机" prop="receiptPhone">
<el-input v-model="invoiceForm.receiptPhone"></el-input>
</el-form-item>
<el-form-item label="收票人邮箱" prop="receiptEmail">
<el-input v-model="invoiceForm.receiptEmail" placeholder="可选"></el-input>
</el-form-item>
</div>
<!-- 增值税专用发票(固定为单位) -->
<div v-else key="invoice-vat">
<el-form-item label="发票抬头">
<span class="inv-title-fixed">单位</span>
</el-form-item>
<el-form-item label="单位名称" prop="companyName">
<el-input v-model="invoiceForm.companyName" placeholder="与营业执照一致"></el-input>
</el-form-item>
<el-form-item label="纳税人识别号" prop="taxpayerId">
<el-input v-model="invoiceForm.taxpayerId"></el-input>
</el-form-item>
<el-form-item label="单位地址" prop="companyAddress">
<el-input v-model="invoiceForm.companyAddress" placeholder="注册地址"></el-input>
</el-form-item>
<el-form-item label="单位电话" prop="companyPhone">
<el-input v-model="invoiceForm.companyPhone" placeholder="固话或手机"></el-input>
</el-form-item>
<el-form-item label="开户银行" prop="bankName">
<el-input v-model="invoiceForm.bankName"></el-input>
</el-form-item>
<el-form-item label="银行账号" prop="bankAccount">
<el-input v-model="invoiceForm.bankAccount"></el-input>
</el-form-item>
<el-form-item label="发票内容">
<el-radio-group v-model="invoiceForm.receiptContent" type="button" button-style="solid">
<el-radio label="商品明细">商品明细</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="收票人手机" prop="receiptPhone">
<el-input v-model="invoiceForm.receiptPhone"></el-input>
</el-form-item>
<el-form-item label="收票人邮箱" prop="receiptEmail">
<el-input v-model="invoiceForm.receiptEmail" placeholder="可选"></el-input>
</el-form-item>
</div>
</el-form>
<div style="text-align: center">
<Button type="primary" :loading="loading" @click="submit">保存发票信息</Button>
<Button type="default" @click="invoiceAvailable = false">取消</Button>
<el-button type="primary" :loading="loading" @click="submit">保存发票信息</el-button>
<el-button type="default" @click="invoiceAvailable = false">取消</el-button>
</div>
</div>
</Modal>
</div></el-dialog>
</div>
</template>
<script>
<script setup>
import { computed, nextTick, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { receiptSelect } from '@/api/cart.js';
import { TINumber } from '@/plugins/RegExp.js';
export default {
name: 'invoiceModal',
data () {
import { mobile, email } from '@/plugins/RegExp.js';
defineOptions({ name: 'invoiceModal' });
const props = defineProps(['invoiceData']);
const emit = defineEmits(['change']);
const route = useRoute();
const invoice = ref(1); // 1 电子普通发票 2 增值税专用发票
const invoiceAvailable = ref(false);
const loading = ref(false);
const invoiceForm = ref({
receiptTitle: '',
personalName: '',
companyName: '',
taxpayerId: '',
receiptContent: '商品明细',
receiptPhone: '',
receiptEmail: '',
companyAddress: '',
companyPhone: '',
bankName: '',
bankAccount: ''
});
const type = ref(1);
const formRef = ref(null);
const formRules = computed(() => {
const receiptPhoneRules = [
{ required: true, message: '请填写收票人手机', trigger: 'blur' },
{ pattern: mobile, message: '请填写正确的手机号码', trigger: 'blur' }
];
// 收票人邮箱可选:仅校验格式;提示文案须与手机区分,避免误用「请填写收票人手机」
const receiptEmailRules = [
{
validator (rule, val, cb) {
const s = val != null ? String(val).trim() : '';
if (!s) {
cb();
return;
}
if (!email.test(s)) {
cb(new Error('请填写正确的收票人邮箱'));
return;
}
cb();
},
trigger: 'blur'
}
];
if (invoice.value === 2) {
return {
invoice: 1, // 发票类型
invoiceAvailable: false, // 模态框显隐
loading: false, // 提交状态
invoiceForm: {
// 普票表单
receiptTitle: '', // 发票抬头
taxpayerId: '', // 纳税人识别号
receiptContent: '商品明细' // 发票内容
},
type: 1, // 1 个人 2 单位
ruleInline: {
taxpayerId: [
{ required: true, message: '请填写纳税人识别号' },
{ pattern: TINumber, message: '请填写正确的纳税人识别号' }
]
}
companyName: [{ required: true, message: '请填写单位名称', trigger: 'blur' }],
taxpayerId: [{ required: true, message: '请填写纳税人识别号', trigger: 'blur' }],
companyAddress: [{ required: true, message: '请填写单位地址', trigger: 'blur' }],
companyPhone: [{ required: true, message: '请填写单位电话', trigger: 'blur' }],
bankName: [{ required: true, message: '请填写开户银行', trigger: 'blur' }],
bankAccount: [{ required: true, message: '请填写银行账号', trigger: 'blur' }],
receiptPhone: receiptPhoneRules,
receiptEmail: receiptEmailRules
};
},
props: ['invoiceData'],
watch: {
// 回显的发票信息
invoiceData: {
handler (val) {
this.invoiceForm = { ...val };
if (val.taxpayerId) {
this.type = 2;
} else {
this.type = 1;
}
},
deep: true,
immeadite: true
}
},
methods: {
/**
* 选择发票抬头
*/
changeInvoice (val) {
this.$nextTick(() => {
this.type = val;
});
},
/**
* 保存判断
*/
save () {
let flage = true;
// 保存分为两种类型,个人以及企业
const { receiptTitle } = JSON.parse(
JSON.stringify(this.invoiceForm)
);
// 判断是否填写发票抬头
if (!receiptTitle) {
this.$Message.error('请填写发票抬头!');
flage = false;
return false;
}
if (this.type === 2) {
this.$refs.form.validate((valid) => {
if (!valid) {
flage = false;
}
});
} else {
delete this.invoiceForm.taxpayerId;
}
return flage;
},
// 保存发票信息
async submit () {
if (this.save()) {
this.loading = true;
let submit = {
way: this.$route.query.way,
...this.invoiceForm
};
let receipt = await receiptSelect(submit);
if (receipt.success) {
this.$emit('change', true);
}
this.loading = false;
}
}
}
};
const rules = {
receiptPhone: receiptPhoneRules,
receiptEmail: receiptEmailRules
};
if (type.value === 1) {
rules.personalName = [{ required: true, message: '请填写个人名称', trigger: 'blur' }];
} else {
rules.companyName = [{ required: true, message: '请填写单位名称', trigger: 'blur' }];
rules.taxpayerId = [{ required: true, message: '请填写纳税人识别号', trigger: 'blur' }];
}
return rules;
});
watch(() => props.invoiceData, (val) => {
if (!val) return;
invoiceForm.value = { ...invoiceForm.value, ...val };
if (!invoiceForm.value.receiptPhone) invoiceForm.value.receiptPhone = '';
if (!invoiceForm.value.receiptEmail) invoiceForm.value.receiptEmail = '';
if (!invoiceForm.value.receiptContent) invoiceForm.value.receiptContent = '商品明细';
if (!invoiceForm.value.companyAddress) invoiceForm.value.companyAddress = '';
if (!invoiceForm.value.companyPhone) invoiceForm.value.companyPhone = '';
if (!invoiceForm.value.bankName) invoiceForm.value.bankName = '';
if (!invoiceForm.value.bankAccount) invoiceForm.value.bankAccount = '';
if (!invoiceForm.value.personalName) invoiceForm.value.personalName = '';
if (!invoiceForm.value.companyName) invoiceForm.value.companyName = '';
const rt = val.receiptType;
if (rt === 2 || rt === '2' || val.invoiceKind === 'VAT_SPECIAL') {
invoice.value = 2;
type.value = 2;
} else {
invoice.value = 1;
if (val.taxpayerId) type.value = 2;
else type.value = 1;
}
// 兼容后端 receiptTitle 存「个人/单位」,实际名称在 personalName/companyName;旧数据名称可能只在 receiptTitle
const titleAsName = (t) => (t && t !== '个人' && t !== '单位' ? t : '');
if (invoice.value === 2) {
invoiceForm.value.companyName = val.companyName || titleAsName(val.receiptTitle) || '';
} else if (type.value === 2) {
invoiceForm.value.companyName = val.companyName || titleAsName(val.receiptTitle) || '';
invoiceForm.value.personalName = val.personalName || '';
} else {
invoiceForm.value.personalName = val.personalName || titleAsName(val.receiptTitle) || '';
invoiceForm.value.companyName = val.companyName || '';
}
if (invoice.value === 1) {
invoiceForm.value.companyAddress = '';
invoiceForm.value.companyPhone = '';
invoiceForm.value.bankName = '';
invoiceForm.value.bankAccount = '';
}
if (invoice.value === 2) {
invoiceForm.value.personalName = '';
}
}, { deep: true, immediate: true });
function onHeaderTypeChange (val) {
if (invoice.value === 1 && val === 1 && invoiceForm.value.personalName === '个人') {
invoiceForm.value.personalName = '';
}
nextTick(() => {
if (formRef.value) formRef.value.clearValidate();
});
}
function onInvoiceKindChange (val) {
type.value = val === 2 ? 2 : 1;
invoiceForm.value = {
receiptTitle: '',
personalName: '',
companyName: '',
taxpayerId: '',
receiptContent: '商品明细',
receiptPhone: '',
receiptEmail: '',
companyAddress: '',
companyPhone: '',
bankName: '',
bankAccount: ''
};
nextTick(() => {
if (formRef.value) formRef.value.clearValidate();
});
}
async function save () {
if (invoice.value === 2) {
invoiceForm.value.receiptContent = '商品明细';
}
return await new Promise(resolve => {
formRef.value.validate(valid => resolve(valid));
});
}
async function submit () {
if (!await save()) return;
loading.value = true;
const raw = JSON.parse(JSON.stringify(invoiceForm.value));
if (invoice.value === 1) {
delete raw.companyAddress;
delete raw.companyPhone;
delete raw.bankName;
delete raw.bankAccount;
delete raw.invoiceAddress;
if (type.value === 1) {
delete raw.taxpayerId;
}
} else {
delete raw.personalName;
}
const isCompanyTitle = invoice.value === 2 || type.value === 2;
const titleName = isCompanyTitle
? (raw.companyName != null ? String(raw.companyName).trim() : '')
: (raw.personalName != null ? String(raw.personalName).trim() : '');
const submit = {
way: route.query.way,
...raw,
receiptType: invoice.value,
receiptTitle: isCompanyTitle ? '单位' : '个人',
personalName: isCompanyTitle ? '' : titleName,
companyName: isCompanyTitle ? titleName : ''
};
let receipt = await receiptSelect(submit);
if (receipt.success) {
emit('change', true);
}
loading.value = false;
}
defineExpose({ invoiceAvailable });
</script>
<style lang="scss" scoped>
/** 普票 */
.inv-type {
text-align: center;
}
@@ -158,7 +304,12 @@ export default {
}
.nav-content {
width: 500px;
width: 520px;
margin: 10px auto;
}
.inv-title-fixed {
line-height: 32px;
color: #515a6e;
}
</style>

View File

@@ -5,7 +5,7 @@
<div id="map-container"></div>
<div class="search-con">
<Input placeholder="输入关键字搜索" id="input-map" v-model="mapSearch" />
<el-input placeholder="输入关键字搜索" id="input-map" v-model="mapSearch" />
<ul>
<li v-for="(tip, index) in tips" :key="index" @click="selectAddr(tip.location)">
<p>{{ tip.name }}</p>
@@ -13,139 +13,137 @@
</li>
</ul>
</div>
<div slot="footer" class="footer">
<Button type="primary" :loading="loading" @click="ok">确定</Button>
<div class="footer">
<el-button type="primary" :loading="loading" @click="ok">确定</el-button>
</div>
</div>
</template>
<script>
<script setup>
import { onMounted, ref, watch } from "vue";
import { Message } from "@/utils/message";
import AMapLoader from "@amap/amap-jsapi-loader";
import { handleRegion } from "@/api/address.js";
import config from "@/config/index";
const config = require('@/config/index')
export default {
name: "map",
data() {
return {
config,
showMap: false, // 地图显隐
mapSearch: "", // 地图搜索
map: null, // 初始化地图
autoComplete: null, // 初始化搜索方法
geocoder: null, // 初始化地理、坐标转化
positionPicker: null, // 地图拖拽选点
tips: [], //搜索关键字列表
addrContent: {}, // 回显地址信息
loading: false, // 加载状态
defineOptions({ name: "map" });
const emit = defineEmits(["getAddress"]);
const showMap = ref(false); // 地图显隐
const mapSearch = ref(""); // 地图搜索
let map = null; // 初始化地图
let autoComplete = null; // 初始化搜索方法
let geocoder = null; // 初始化地理、坐标转化
let positionPicker = null; // 地图拖拽选点
const tips = ref([]); //搜索关键字列表
const addrContent = ref({}); // 回显地址信息
const loading = ref(false); // 加载状态
watch(mapSearch, function (val) {
searchOfMap(val);
});
function ok() {
if (addrContent.value && addrContent.value.regeocode) {
const params = {
cityCode: addrContent.value.regeocode.addressComponent.citycode,
townName: addrContent.value.regeocode.addressComponent.township,
};
},
watch: {
mapSearch: function (val) {
this.searchOfMap(val);
},
},
methods: {
ok() {
handleRegion(params).then((res) => {
if (res.success) {
addrContent.value.addr = res.result.name.replace(/,/g, " ");
addrContent.value.addrId = res.result.id;
loading.value = false;
if (this.addrContent && this.addrContent.regeocode) {
const params = {
cityCode: this.addrContent.regeocode.addressComponent.citycode,
townName: this.addrContent.regeocode.addressComponent.township,
};
handleRegion(params).then((res) => {
if (res.success) {
this.addrContent.addr = res.result.name.replace(/,/g, " ");
this.addrContent.addrId = res.result.id;
this.loading = false;
this.$emit("getAddress", this.addrContent);
}
});
} else {
this.$Message.error('未获取到坐标信息!请查看高德API配置是否正确')
emit("getAddress", addrContent.value);
}
});
} else {
Message.error('未获取到坐标信息!请查看高德API配置是否正确')
}
}
function init() {
AMapLoader.load({
key: config.aMapKey, // 申请好的Web端开发者Key,首次调用 load 时必填
version: "", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Autocomplete",
"AMap.PlaceSearch",
"AMap.Geolocation",
"AMap.Geocoder",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
AMapUI: {
// 是否加载 AMapUI,缺省不加载
version: "1.1", // AMapUI 缺省 1.1
plugins: ["misc/PositionPicker"], // 需要加载的 AMapUI ui插件
},
init() {
AMapLoader.load({
key: this.config.aMapKey, // 申请好的Web端开发者Key,首次调用 load 时必填
version: "", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15
plugins: [
"AMap.ToolBar",
"AMap.Autocomplete",
"AMap.PlaceSearch",
"AMap.Geolocation",
"AMap.Geocoder",
], // 需要使用的的插件列表,如比例尺'AMap.Scale'等
AMapUI: {
// 是否加载 AMapUI,缺省不加载
version: "1.1", // AMapUI 缺省 1.1
plugins: ["misc/PositionPicker"], // 需要加载的 AMapUI ui插件
},
})
.then((AMap) => {
let that = this;
this.map = new AMap.Map("map-container", {
zoom: 12,
});
that.map.addControl(new AMap.ToolBar());
that.map.addControl(new AMap.Autocomplete());
that.map.addControl(new AMap.PlaceSearch());
that.map.addControl(new AMap.Geocoder());
// 实例化Autocomplete
let autoOptions = {
city: "全国",
};
that.autoComplete = new AMap.Autocomplete(autoOptions); // 搜索
that.geocoder = new AMap.Geocoder(autoOptions);
that.positionPicker = new AMapUI.PositionPicker({
// 拖拽选点
mode: "dragMap",
map: that.map,
});
that.positionPicker.start();
/**
*
* 所有回显数据,都在positionResult里面
* 需要字段可以查找
*
*/
that.positionPicker.on("success", function (positionResult) {
that.addrContent = positionResult;
});
})
.catch((e) => { });
},
searchOfMap(val) {
// 地图搜索
let that = this;
this.autoComplete.search(val, function (status, result) {
// 搜索成功时,result即是对应的匹配数据
if (status == "complete" && result.info == "OK") {
that.tips = result.tips;
} else {
that.tips = [];
}
})
.then((AMap) => {
map = new AMap.Map("map-container", {
zoom: 12,
});
},
selectAddr(location) {
// 选择坐标
if (!location) {
this.$Message.warning("请选择正确点位");
return false;
}
const lnglat = [location.lng, location.lat];
this.positionPicker.start(lnglat);
},
},
mounted() {
this.init();
},
};
map.addControl(new AMap.ToolBar());
map.addControl(new AMap.Autocomplete());
map.addControl(new AMap.PlaceSearch());
map.addControl(new AMap.Geocoder());
// 实例化Autocomplete
let autoOptions = {
city: "全国",
};
autoComplete = new AMap.Autocomplete(autoOptions); // 搜索
geocoder = new AMap.Geocoder(autoOptions);
positionPicker = new AMapUI.PositionPicker({
// 拖拽选点
mode: "dragMap",
map: map,
});
positionPicker.start();
/**
*
* 所有回显数据,都在positionResult里面
* 需要字段可以查找
*
*/
positionPicker.on("success", function (positionResult) {
addrContent.value = positionResult;
});
})
.catch((e) => {
console.warn('地图组件加载失败', e);
});
}
function searchOfMap(val) {
// 地图搜索
autoComplete.search(val, function (status, result) {
// 搜索成功时,result即是对应的匹配数据
if (status == "complete" && result.info == "OK") {
tips.value = result.tips;
} else {
tips.value = [];
}
});
}
function selectAddr(location) {
// 选择坐标
if (!location) {
Message.warning("请选择正确点位");
return false;
}
const lnglat = [location.lng, location.lat];
positionPicker.start(lnglat);
}
onMounted(() => {
init();
});
</script>
<style lang="scss" scoped>
#map-container {

View File

@@ -1,9 +1,9 @@
<template>
<Modal width="800" footer-hide v-model="enableMap">
<RadioGroup @on-change="changeMap" v-model="mapDefault" type="button">
<Radio label="select">级联选择</Radio>
<Radio label="map" v-if="aMapSwitch">高德地图</Radio>
</RadioGroup>
<el-dialog width="800" v-model="enableMap">
<el-radio-group v-if="aMapSwitch" @change="changeMap" v-model="mapDefault">
<el-radio label="select">地区选择</el-radio>
<el-radio label="map">高德地图</el-radio>
</el-radio-group>
<div>
<div v-if="mapDefault === 'select'">
<div class="selector">
@@ -18,103 +18,98 @@
</div>
<div class="footer">
<Button type="primary" @click="finished">确定</Button>
<el-button type="primary" @click="finished">确定</el-button>
</div>
</div>
<mapping v-if="mapDefault === 'map'" ref="map" @getAddress="getAddress" />
</div>
</Modal>
</div></el-dialog>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { Message } from "@/utils/message";
import { aMapSwitch } from '@/config/index'
import mapping from "@/components/map/index.vue";
import * as API_Setup from "@/api/common.js";
export default {
components: { mapping },
data() {
return {
aMapSwitch,
enableMap: false,
mapDefault: "select",
data: {
province: [], //省
city: [], //市
area: [], //区
street: [], //街道
},
chiosend: [],
};
},
mounted() {
const emit = defineEmits(['callback'])
this.chiosend = new Array(4).fill("");
const enableMap = ref(false)
const mapDefault = ref("select")
const data = reactive({
province: [], //省
city: [], //市
area: [], //区
street: [], //街道
})
const chiosend = ref([])
},
methods: {
open() {
this.enableMap = true
this.init({ id: 0 }, 'province');
},
changeMap(val) {
this.mapDefault = val
onMounted(() => {
chiosend.value = new Array(4).fill("");
})
function open() {
enableMap.value = true
init({ id: 0 }, 'province');
}
},
init(val, level = 'province', index) {
if (level == 0) {
// 说明选择到了街道,将街道id存入数组
this.chiosend.splice(3, 1, val);
defineExpose({ open })
function changeMap(val) {
mapDefault.value = val
}
function init(val, level = 'province', index) {
if (level == 0) {
// 说明选择到了街道,将街道id存入数组
chiosend.value.splice(3, 1, val);
}
else {
API_Setup.getRegion(val.id).then((res) => {
if (res.result.length && val.id !== 0) {
chiosend.value[index] = val
}
else {
API_Setup.getRegion(val.id).then((res) => {
if (res.result.length && val.id !== 0) {
this.chiosend[index] = val
}
else if(!res.result.length){
this.chiosend[index] = val
}
this.data[level] = res.result;
if (level == 'city') {
this.data.area = []
this.data.street = []
this.chiosend.splice(1, 3, "","","");
}
if (level == 'area') {
this.data.street = []
this.chiosend.splice(2, 2, "","");
}
if (level == 'street') {
this.chiosend.splice(3, 1, "");
}
});
else if (!res.result.length) {
chiosend.value[index] = val
}
},
getAddress(center) {
this.$emit('callback', {
type: this.mapDefault,
data: center
})
this.enableMap = false;
},
// 选择完成
finished() {
if(!this.chiosend[0]){
this.$Message.error("请选择地址")
return
data[level] = res.result;
if (level == 'city') {
data.area = []
data.street = []
chiosend.value.splice(1, 3, "", "", "");
}
const params = this.chiosend.filter((item) => item!=="" && item.value !== "");
this.enableMap = false;
this.$emit('callback', {
type: this.mapDefault,
data: params
})
},
},
if (level == 'area') {
data.street = []
chiosend.value.splice(2, 2, "", "");
}
if (level == 'street') {
chiosend.value.splice(3, 1, "");
}
});
}
}
function getAddress(center) {
emit('callback', {
type: mapDefault.value,
data: center
})
enableMap.value = false;
}
// 选择完成
function finished() {
if (!chiosend.value[0]) {
Message.error("请选择地址")
return
}
const params = chiosend.value.filter((item) => item !== "" && item.value !== "");
enableMap.value = false;
emit('callback', {
type: mapDefault.value,
data: params
})
}
</script>

View File

@@ -1,151 +1,44 @@
<template>
<div>
<Cascader
<el-cascader
:data="data"
:load-data="loadData"
v-model="addr"
@on-change="change"
@change="change"
style="width: 300px"
></Cascader>
></el-cascader>
</div>
</template>
<script>
import {getRegion} from '@/api/common.js';
export default {
data () {
return {
data: [], // 地区数据
addr: [] // 已选数据
};
},
props: ['addressId'],
mounted () {},
methods: {
change (val, selectedData) { // 选择地区
/**
* @returns [regionId,region]
*/
this.$emit('selected', [
val,
selectedData[selectedData.length - 1].__label.split('/')
]);
},
loadData (item, callback) { // 加载数据
item.loading = true;
getRegion(item.value).then((res) => {
if (res.result.length <= 0) {
item.loading = false;
} else {
res.result.forEach((child) => {
item.loading = false;
<script setup>
import { ref, watch } from 'vue';
import { getRegion } from '@/api/common.js';
let data = {
value: child.id,
label: child.name,
loading: false,
children: []
};
const props = defineProps(['addressId']);
const emit = defineEmits(['selected']);
if (child.level === 'street' || item.label === '香港特别行政区') {
item.children.push({
value: child.id,
label: child.name
});
} else {
item.children.push(data);
}
});
callback();
}
});
},
async init () { // 初始化地图数据
let data = await getRegion(0);
let arr = [];
data.result.forEach((item) => {
let obj;
// 台湾省做处理
if (item.name === '台湾省') {
obj = {
value: item.id,
label: item.name
};
} else {
obj = {
value: item.id,
label: item.name,
loading: false,
children: []
};
}
arr.push(obj);
});
this.data = arr;
console.warn('init');
},
async reviewData () {
// 数据回显
let addr = JSON.parse(JSON.stringify(this.addressId.split(',')));
let length = addr.length;
let data = await getRegion(0);
let arr0 = [];
let arr1 = [];
let arr2 = [];
// 第一级数据
data.result.forEach((item) => {
let obj;
// 台湾省做处理
if (item.name === '台湾省') {
obj = {
value: item.id,
label: item.name
};
} else {
obj = {
value: item.id,
label: item.name,
loading: false,
children: []
};
}
arr0.push(obj);
});
// 根据选择的数据来加载数据列表
if (length > 0) {
let children = await getRegion(addr[0]);
children = this.handleData(children.result);
arr0.forEach((e) => {
if (e.value === addr[0]) {
e.children = arr1 = children;
}
});
}
if (length > 1) {
let children = await getRegion(addr[1]);
children = this.handleData(children.result);
arr1.forEach((e) => {
if (e.value === addr[1]) {
e.children = arr2 = children;
}
});
}
if (length > 2) {
let children = await getRegion(addr[2]);
children = this.handleData(children.result);
arr2.forEach((e) => {
if (e.value === addr[2]) {
e.children = children;
}
});
}
this.data = arr0;
this.addr = addr;
},
handleData (data) {
// 处理接口数据
let item = [];
data.forEach((child) => {
let obj = {
const data = ref([]); // 地区数据
const addr = ref([]); // 已选数据
function change (val, selectedData) { // 选择地区
/**
* @returns [regionId,region]
*/
emit('selected', [
val,
selectedData[selectedData.length - 1].__label.split('/')
]);
}
function loadData (item, callback) { // 加载数据
item.loading = true;
getRegion(item.value).then((res) => {
if (res.result.length <= 0) {
item.loading = false;
} else {
res.result.forEach((child) => {
item.loading = false;
let data = {
value: child.id,
label: child.name,
loading: false,
@@ -153,30 +46,133 @@ export default {
};
if (child.level === 'street' || item.label === '香港特别行政区') {
item.push({
item.children.push({
value: child.id,
label: child.name
});
} else {
item.push(obj);
item.children.push(data);
}
});
return item;
callback();
}
},
watch: {
addressId: {
handler: function (v) {
if (v) {
this.reviewData();
} else {
this.init();
}
},
immediate: true
});
}
async function init () { // 初始化地图数据
let res = await getRegion(0);
let arr = [];
res.result.forEach((item) => {
let obj;
// 台湾省做处理
if (item.name === '台湾省') {
obj = {
value: item.id,
label: item.name
};
} else {
obj = {
value: item.id,
label: item.name,
loading: false,
children: []
};
}
arr.push(obj);
});
data.value = arr;
console.warn('init');
}
async function reviewData () {
// 数据回显
let addrArr = JSON.parse(JSON.stringify(props.addressId.split(',')));
let length = addrArr.length;
let res = await getRegion(0);
let arr0 = [];
let arr1 = [];
let arr2 = [];
// 第一级数据
res.result.forEach((item) => {
let obj;
// 台湾省做处理
if (item.name === '台湾省') {
obj = {
value: item.id,
label: item.name
};
} else {
obj = {
value: item.id,
label: item.name,
loading: false,
children: []
};
}
arr0.push(obj);
});
// 根据选择的数据来加载数据列表
if (length > 0) {
let children = await getRegion(addrArr[0]);
children = handleData(children.result);
arr0.forEach((e) => {
if (e.value === addrArr[0]) {
e.children = arr1 = children;
}
});
}
};
if (length > 1) {
let children = await getRegion(addrArr[1]);
children = handleData(children.result);
arr1.forEach((e) => {
if (e.value === addrArr[1]) {
e.children = arr2 = children;
}
});
}
if (length > 2) {
let children = await getRegion(addrArr[2]);
children = handleData(children.result);
arr2.forEach((e) => {
if (e.value === addrArr[2]) {
e.children = children;
}
});
}
data.value = arr0;
addr.value = addrArr;
}
function handleData (data) {
// 处理接口数据
let item = [];
data.forEach((child) => {
let obj = {
value: child.id,
label: child.name,
loading: false,
children: []
};
if (child.level === 'street' || item.label === '香港特别行政区') {
item.push({
value: child.id,
label: child.name
});
} else {
item.push(obj);
}
});
return item;
}
watch(() => props.addressId, (v) => {
if (v) {
reviewData();
} else {
init();
}
}, { immediate: true });
</script>
<style scoped lang="scss">
</style>

View File

@@ -1,52 +0,0 @@
<template>
<div></div>
</template>
<script>
import { getIMDetail } from "@/api/common";
import Storage from "@/plugins/storage";
import { getMemberMsg } from "@/api/login";
export default {
data() {
return {
Storage,
IMLink: "",
};
},
methods: {
// 跳转im客服
async IMService(id, goodsId, skuId) {
// 获取访问Token
let accessToken = Storage.getItem("accessToken");
await this.getIMDetailMethods();
const userInfo = await getMemberMsg();
if (userInfo.success) {
//携带商品Id,在IM可以发送商品信息
if(goodsId && skuId){
window.open(
this.IMLink + "?token=" + accessToken + "&id=" + id || this.storeMsg.storeId + "&goodsId=" + goodsId + "&skuId=" + skuId
);
}else{
window.open(
this.IMLink + "?token=" + accessToken + "&id=" + id || this.storeMsg.storeId
);
}
} else {
this.$Message.error("请登录后再联系客服");
return;
}
},
// 获取im信息
async getIMDetailMethods() {
let res = await getIMDetail();
if (res.success) {
this.IMLink = res.result;
}
},
},
};
</script>
<style lang="scss" scoped></style>

View File

@@ -1,5 +1,5 @@
<template>
<div class="cate-nav width_1200_auto" :class="{'fixed-show':useClass == 'fixed-show'}">
<div class="cate-nav" :class="{'fixed-show':useClass == 'fixed-show'}">
<div class="nav-con" :class="{'background-white':useClass == 'background-white'}">
<div
class="all-categories hover-pointer"
@@ -26,32 +26,22 @@
:style="{'top':!showNavBar ?'60px':'46px'}"
v-show="showAlways || showFirstList"
@mouseenter="showFirstList = true"
@mouseleave="showFirstList = false"
@mouseleave="closePanel"
>
<!-- 第一级分类 -->
<div
class="nav-side"
:class="{ 'large-nav': large, 'opacity-nav': opacity }"
@mouseleave="panel = false"
>
<ul>
<li
v-for="(item, index) in cateList"
:key="index"
:class="{ 'is-active': panel && panelIndex === index }"
@mouseenter="showDetail(index)"
>
<span class="nav-side-item" @click="goGoodsList(item.id)">{{
item.name
}}</span>
<span v-for="(second, secIndex) in item.children" :key="secIndex">
<span v-if="secIndex < 2"> / </span>
<span
@click="goGoodsList(second.id, second.parentId)"
class="nav-side-item"
v-if="secIndex < 2"
>{{ second.name }}</span
>
</span>
<span class="nav-side-item" @click="goGoodsList(item.id)">{{ item.name }}</span>
<el-icon><ArrowRight /></el-icon>
</li>
</ul>
</div>
@@ -69,8 +59,7 @@
@click="goGoodsList(item.id,item.parentId)"
v-if="index < 8"
:key="index"
>{{ item.name }}<Icon type="ios-arrow-forward"
/></span>
>{{ item.name }}<el-icon><ArrowRight /></el-icon></span>
</template>
</div>
<ul>
@@ -83,7 +72,7 @@
class="detail-item-title"
@click="goGoodsList(items.id, items.parentId)"
>
{{ items.name }} <Icon type="ios-arrow-forward" />
{{ items.name }} <el-icon><ArrowRight /></el-icon>
<span class="glyphicon glyphicon-menu-right"></span>
</span>
<div>
@@ -92,8 +81,7 @@
@click="goGoodsList(item.id, items.id, items.parentId)"
:key="subIndex"
class="detail-item"
>{{ item.name }}</span
>
>{{ item.name }}</span>
</div>
</li>
</ul>
@@ -102,129 +90,143 @@
</div>
</template>
<script>
<script setup>
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import { getCategory } from "@/api/goods";
import storage from "@/plugins/storage.js";
export default {
name: "GoodsListNav",
props: {
showAlways: {
// 总是显示下拉分类
default: false,
type: Boolean,
},
showNavBar: {
// 显示全部商品分类右侧导航条
default: true,
type: Boolean,
},
hover: {
default: false,
type: Boolean,
},
large: {
// 是否更高的高度
default: false,
type: Boolean,
},
opacity: {
// 是否背景透明
default: false,
type: Boolean,
},
useClass:{
type:null,
default:''
import { ArrowRight } from "@element-plus/icons-vue";
import { useAppStore } from "@/stores/app";
defineOptions({ name: "GoodsListNav" });
const props = defineProps({
showAlways: {
// 总是显示下拉分类
default: false,
type: Boolean,
},
showNavBar: {
// 显示全部商品分类右侧导航条
default: true,
type: Boolean,
},
hover: {
default: false,
type: Boolean,
},
large: {
// 是否更高的高度
default: false,
type: Boolean,
},
opacity: {
// 是否背景透明
default: false,
type: Boolean,
},
useClass: {
type: null,
default: '',
},
});
const router = useRouter();
const appStore = useAppStore();
const panel = ref(false); // 二级分类展示
const panelIndex = ref(-1);
const panelData = ref([]); // 二级分类数据
const showFirstList = ref(false); // 始终展示一级列表
const cateList = ref([]); // 商品分类
// 导航列表
const navList = computed(() => {
if (storage.getItem("navList")) {
return JSON.parse(storage.getItem("navList"));
}
return { list: [] };
});
function showFirstLists() {
showFirstList.value = true;
if (
localStorage.getItem("category") &&
localStorage.getItem("category_expiration_time")
) {
// this.getCate();
// 如果缓存过期,则获取最新的信息
if (new Date() > localStorage.getItem("category_expiration_time")) {
getCate();
return;
}
},
data() {
return {
panel: false, // 二级分类展示
panelData: [], // 二级分类数据
showFirstList: false, // 始终展示一级列表
cateList: [], // 商品分类
};
},
computed: {
navList() {
// 导航列表
if (storage.getItem("navList")) {
return JSON.parse(storage.getItem("navList"));
} else {
return [];
}
},
},
methods: {
showFirstLists(){
this.showFirstList = true;
if(
localStorage.getItem("category") &&
localStorage.getItem("category_expiration_time")
) {
// this.getCate();
// 如果缓存过期,则获取最新的信息
if (new Date() > localStorage.getItem("category_expiration_time")) {
this.getCate();
return;
}
this.cateList = JSON.parse(localStorage.getItem("category"));
// this.$Message.info(cateList)
}
},
getCate() {
// 获取分类数据
if (this.hover) return false;
getCategory(0).then((res) => {
if (res.success) {
this.cateList = res.result;
this.$store.commit("SET_CATEGORY", res.result);
// 过期时间
var expirationTime = new Date().setHours(new Date().getHours() + 1);
// 存放过期时间
localStorage.setItem("category_expiration_time", expirationTime);
// 存放分类信息
localStorage.setItem("category", JSON.stringify(res.result));
}
});
},
showDetail(index) {
// 展示全部分类
this.panel = true;
this.panelData = this.cateList[index].children;
},
goGoodsList(id, secondId, firstId) {
// 分类共有三级,传全部分类过去
const arr = [firstId, secondId, id];
if (!arr[1]) {
arr.splice(0, 2);
}
if (!arr[0]) {
arr.shift();
}
let routerUrl = this.$router.resolve({
path: "/goodsList",
query: { categoryId: arr.toString() },
});
window.open(routerUrl.href, "_blank");
},
},
mounted() {
if (
localStorage.getItem("category") &&
localStorage.getItem("category_expiration_time")
) {
// 如果缓存过期,则获取最新的信息
if (new Date() > localStorage.getItem("category_expiration_time")) {
this.getCate();
return;
}
this.cateList = JSON.parse(localStorage.getItem("category"));
} else {
this.getCate();
cateList.value = JSON.parse(localStorage.getItem("category"));
// Message.info(cateList)
}
}
function getCate() {
// 获取分类数据
if (props.hover) return false;
getCategory(0).then((res) => {
if (res.success) {
cateList.value = res.result;
appStore.setCategory(res.result);
// 过期时间
var expirationTime = new Date().setHours(new Date().getHours() + 1);
// 存放过期时间
localStorage.setItem("category_expiration_time", expirationTime);
// 存放分类信息
localStorage.setItem("category", JSON.stringify(res.result));
}
},
};
}).catch((err) => {
console.warn('获取分类失败', err);
});
}
function showDetail(index) {
// 展示全部分类
panel.value = true;
panelIndex.value = index;
panelData.value = cateList.value[index].children;
}
function closePanel() {
showFirstList.value = false;
panel.value = false;
panelIndex.value = -1;
}
function goGoodsList(id, secondId, firstId) {
// 分类共有三级,传全部分类过去
const arr = [firstId, secondId, id];
if (!arr[1]) {
arr.splice(0, 2);
}
if (!arr[0]) {
arr.shift();
}
router.push({
path: "/goodsList",
query: { categoryId: arr.toString() },
});
}
onMounted(() => {
if (
localStorage.getItem("category") &&
localStorage.getItem("category_expiration_time")
) {
// 如果缓存过期,则获取最新的信息
if (new Date() > localStorage.getItem("category_expiration_time")) {
getCate();
return;
}
cateList.value = JSON.parse(localStorage.getItem("category"));
} else {
getCate();
}
});
</script>
<style scoped lang="scss">
@@ -243,8 +245,10 @@ export default {
}
.fixed-show{
margin-top: 0 !important;
width: 100%;
>.nav-con{
>.all-categories{
width: 100%;
align-items: center !important;
height: 60px;
display: inherit;
@@ -262,43 +266,50 @@ export default {
}
.cate-nav {
position: relative;
margin: 14px auto 0 auto;
z-index: 5;
width: 100%;
margin: 14px 0 0;
box-sizing: border-box;
}
/** 商品分类 */
.nav-con {
height: 46px;
margin: 0 auto;
display: flex;
align-items: stretch;
.all-categories {
border-top-left-radius: 10px;
border-top-right-radius: 10px;
padding-left: 37.4px;
flex-shrink: 0;
display: flex;
align-items: center;
height: 46px;
border-top-left-radius: 4px;
border-top-right-radius: 4px;
padding-left: 16px;
background: #FFFFFF;
width: 263.2px;
padding-top: 15.4px;
line-height: 20px;
border-bottom: none;
font-size: 16.8px;
font-weight: normal;
color: #333333;
letter-spacing: 0px;
font-size: 15px;
font-weight: 600;
color: #171A26;
box-sizing: border-box;
}
.nav-item {
width: 914px;
flex: 1;
min-width: 0;
height: 46px;
border-radius: 10px;
background: #FFFFFF;
margin-left: 10px;
margin: 0 0 0 10px;
padding: 0;
line-height: 46px;
overflow: hidden;
list-style: none;
// background-color: #eee;
display: flex;
li {
float: left;
font-size: 17px;
font-weight: normal;
color: #333333;
font-size: 15px;
font-weight: 400;
color: #171A26;
&:hover {
@@ -307,24 +318,23 @@ export default {
}
}
}
// 分类列表
// 分类列表(浮层叠在轮播左列之上)
.cate-list {
margin: 0 auto;
position: absolute;
left: 0;
top: 46px;
z-index: 1000;
}
.nav-side {
width: 263.2px;
color: #666;
color: #171A26;
float: left;
overflow:hidden;
border-bottom-left-radius: 10px;
border-bottom-right-radius: 10px;
overflow: hidden;
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
background-color: #fff;
height: 340px;
height: 344px;
////自动移滚动条样式
//&::-webkit-scrollbar {
// width: 1px;
@@ -341,21 +351,53 @@ export default {
}
.large-nav {
height: 470px;
ul > li {
line-height: 20px;
}
}
.opacity-nav {
background-color: rgba(0, 0, 0, 0.5);
}
.nav-side ul {
width: 100%;
height: 100%;
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
}
.nav-side li {
padding: 0 0 16.2px 37.4px;
font-size: 13px;
line-height: 18px;
display: flex;
align-items: center;
justify-content: space-between;
flex: 1 1 0;
min-height: 0;
height: auto;
padding: 0 12px 0 16px;
font-size: 14px;
line-height: 20px;
box-sizing: border-box;
cursor: pointer;
.el-icon {
flex-shrink: 0;
color: #ccc;
font-size: 12px;
}
&:hover,
&.is-active {
background: #fff5f4;
color: $theme_color;
.el-icon {
color: $theme_color;
}
}
}
.nav-side-item {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.nav-side-item:hover {
cursor: pointer;
@@ -364,63 +406,102 @@ export default {
/*显示商品详细信息*/
.detail-item-panel {
width: 1000px;
width: 936px;
min-height: 340px;
background-color: #fff;
box-shadow: 0px 0px 15px #ccc;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
border-radius: 0 4px 4px 0;
position: absolute;
top: 0;
left: 200px;
left: 263.2px;
z-index: 1000;
padding: 15px;
padding: 8px 20px 16px;
box-sizing: border-box;
}
.nav-detail-item {
margin-top: 5px;
margin-bottom: 15px;
display: flex;
flex-wrap: wrap;
gap: 4px 20px;
margin: 8px 0 4px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
color: #eee;
}
.nav-detail-item span {
padding: 6px;
padding-left: 12px;
margin-right: 15px;
font-size: 12px;
color: #333;
display: inline-flex;
align-items: center;
gap: 2px;
padding: 4px 0;
font-size: 14px;
font-weight: 600;
color: #171A26;
.el-icon {
font-size: 12px;
color: #ccc;
}
}
.nav-detail-item span:hover {
background-color: $theme_color;
color: #fff;
color: $theme_color;
.el-icon {
color: $theme_color;
}
}
.detail-item-panel li {
line-height: 30px;
line-height: 28px;
}
.detail-item-title {
font-weight: bold;
font-size: 12px;
display: inline-flex;
align-items: center;
gap: 2px;
flex-shrink: 0;
width: 108px;
font-weight: 600;
font-size: 14px;
line-height: 28px;
cursor: pointer;
color: #555555;
padding-right: 10px;
width: 81px;
text-align: right;
color: #171A26;
text-align: left;
.glyphicon {
display: none;
}
.el-icon {
font-size: 12px;
color: #ccc;
}
}
.detail-item-title:hover {
color: $theme_color;
.el-icon {
color: $theme_color;
}
}
.detail-item-row {
display: flex;
align-items: flex-start;
padding: 6px 0;
> div {
flex: 1;
min-width: 0;
display: flex;
flex-wrap: wrap;
}
}
.detail-item {
font-size: 12px;
padding-left: 8px;
padding-right: 8px;
font-size: 14px;
line-height: 28px;
padding: 0 12px;
color: #666;
cursor: pointer;
border-left: 1px solid #ccc;
border: none;
&:first-child {
border: none;
padding-left: 0;
padding-left: 12px;
}
}
.detail-item:hover {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
<template>
<div class="user-center-page">
<div v-if="title" class="user-center-title">{{ title }}</div>
<div class="user-center-panel">
<div v-if="showToolbar" class="user-center-toolbar" :class="{ 'is-actions-only': !tabs.length }">
<div class="user-center-tabs">
<div
v-for="(item, index) in tabs"
:key="index"
class="user-center-tab"
:class="{ active: activeTab === index }"
@click="$emit('tab-change', index)"
>
{{ item }}
</div>
</div>
<div class="user-center-extra">
<slot name="extra">
<el-button v-if="moreText" type="primary" @click="handleMore">{{ moreText }}</el-button>
</slot>
</div>
</div>
<div class="user-center-content">
<slot />
</div>
</div>
</div>
</template>
<script setup>
import { computed, useSlots } from "vue";
import { useRouter } from "vue-router";
defineOptions({ name: "UserCenterLayout" });
const props = defineProps({
title: {
type: String,
default: "",
},
tabs: {
type: Array,
default: () => [],
},
activeTab: {
type: Number,
default: 0,
},
moreText: {
type: String,
default: "",
},
moreTo: {
type: [String, Object],
default: "",
},
});
const emit = defineEmits(["tab-change", "more"]);
const router = useRouter();
const slots = useSlots();
const showToolbar = computed(() => {
return props.tabs.length || props.moreText || slots.extra;
});
function handleMore() {
emit("more");
if (!props.moreTo) return;
if (typeof props.moreTo === "string") {
router.push(props.moreTo);
} else {
router.push(props.moreTo);
}
}
</script>
<style scoped lang="scss">
.user-center-page {
margin-bottom: 40px;
}
.user-center-title {
height: 54px;
line-height: 54px;
padding: 0 20px;
margin-bottom: 12px;
background: #fff;
border: 1px solid #eee;
color: #333;
font-size: 16px;
font-weight: 600;
}
.user-center-panel {
padding: 0 16px 16px;
background: #fff;
border: 1px solid #eee;
}
.user-center-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 54px;
border-bottom: 1px solid #eee;
gap: 16px;
}
.user-center-toolbar.is-actions-only {
justify-content: flex-start;
.user-center-tabs {
display: none;
}
}
.user-center-tabs {
display: flex;
align-items: center;
height: 54px;
flex: 1;
min-width: 0;
overflow: hidden;
}
.user-center-tab {
position: relative;
flex-shrink: 0;
height: 54px;
line-height: 54px;
margin-right: 28px;
color: #333;
cursor: pointer;
white-space: nowrap;
&:hover,
&.active {
color: $theme_color;
}
&.active::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 2px;
background: $theme_color;
}
}
.user-center-extra {
display: flex;
align-items: center;
flex-shrink: 0;
}
.user-center-content {
padding-top: 14px;
}
</style>

View File

@@ -3,122 +3,123 @@
<div class="imgBox" :style="{width:data.originalWidth+'px',height:data.originalHeight + 'px'}">
<img :src="data.backImage" style="width:100%;height:100%" alt="">
<img class="slider" :src="data.slidingImage" :style="{left:distance+'px',top:data.randomY+'px'}" :width="data.sliderWidth" :height="data.sliderHeight" alt="">
<Icon type="md-refresh" class="refresh" @click="init" />
<el-icon class="refresh" @click="init"><Refresh /></el-icon>
</div>
<div class="handle" :style="{width:data.originalWidth+'px'}">
<span class="bgcolor" :style="{width:distance + 'px',background:bgColor}"></span>
<span class="swiper" :style="{left:distance + 'px'}" @mousedown="mouseDown">
<Icon type="md-arrow-round-forward" />
<el-icon><ArrowRight /></el-icon>
</span>
<span class="text">{{verifyText}}</span>
</div>
</div>
</template>
<script>
<script setup>
import { ref, watch } from 'vue';
import { Message } from "@/utils/message";
import { Refresh, ArrowRight } from '@element-plus/icons-vue';
import { getVerifyImg, postVerifyImg } from './verify.js';
export default {
props: {
// 传入数据,判断是登录、注册、修改密码
verifyType: {
defalut: 'LOGIN',
type: String
}
},
data () {
return {
show: false, // 验证码显隐
type: 'LOGIN', // 请求类型
data: { // 验证码数据
backImage: '',
slidingImage: '',
originalHeight: 150,
originalWidth: 300,
sliderWidth: 60,
sliderHeight: 60
},
distance: 0, // 拼图移动距离
flag: false, // 判断滑块是否按下
downX: 0, // 鼠标按下位置
bgColor: '#04ad11', // 滑动背景颜色
verifyText: '拖动滑块解锁' // 文字提示
};
},
methods: {
// 鼠标按下事件,开始拖动滑块
mouseDown (e) {
this.downX = e.clientX;
this.flag = true;
},
// 鼠标移动事件,计算距离
mouseMove (e) {
if (this.flag) {
let offset = e.clientX - this.downX;
if (offset > this.data.originalWidth - 43) {
this.distance = this.data.originalWidth - 43;
} else if (offset < 0) {
this.distance = 0;
} else {
this.distance = offset;
}
}
},
// 鼠标抬起事件,验证是否正确
mouseUp () {
if (!this.flag) return false;
this.flag = false;
let params = {
verificationEnums: this.type,
xPos: this.distance
};
postVerifyImg(params).then(res => {
if (res.success) {
if (res.result) {
this.bgColor = 'green';
this.verifyText = '解锁成功';
this.$emit('change', { status: true, distance: this.distance });
} else {
this.bgColor = 'red';
this.verifyText = '解锁失败';
let that = this;
setTimeout(() => {
that.init();
}, 1000);
this.$emit('change', { status: false, distance: this.distance });
}
} else {
this.init()
}
}).catch(()=>{
this.init()
});
},
init () { // 初始化数据
this.flag = false;
this.downX = 0;
this.distance = 0;
this.bgColor = '#04ad11';
this.verifyText = '拖动滑块解锁';
getVerifyImg(this.type).then(res => {
if (res.result) {
this.data = res.result;
this.show = true;
} else {
this.$Message.warning('请求失败请重试!')
}
});
}
},
watch: {
verifyType: {
immediate: true,
handler: function (v) {
this.type = v;
}
const props = defineProps({
// 传入数据,判断是登录、注册、修改密码
verifyType: {
defalut: 'LOGIN',
type: String
}
});
const emit = defineEmits(['change']);
const show = ref(false); // 验证码显隐
const type = ref('LOGIN'); // 请求类型
const data = ref({ // 验证码数据
backImage: '',
slidingImage: '',
originalHeight: 150,
originalWidth: 300,
sliderWidth: 60,
sliderHeight: 60
});
const distance = ref(0); // 拼图移动距离
const flag = ref(false); // 判断滑块是否按下
const downX = ref(0); // 鼠标按下位置
const bgColor = ref('#04ad11'); // 滑动背景颜色
const verifyText = ref('拖动滑块解锁'); // 文字提示
// 鼠标按下事件,开始拖动滑块
function mouseDown (e) {
downX.value = e.clientX;
flag.value = true;
}
// 鼠标移动事件,计算距离
function mouseMove (e) {
if (flag.value) {
let offset = e.clientX - downX.value;
if (offset > data.value.originalWidth - 43) {
distance.value = data.value.originalWidth - 43;
} else if (offset < 0) {
distance.value = 0;
} else {
distance.value = offset;
}
}
};
}
// 鼠标抬起事件,验证是否正确
function mouseUp () {
if (!flag.value) return false;
flag.value = false;
let params = {
verificationEnums: type.value,
xPos: distance.value
};
postVerifyImg(params).then(res => {
if (res.success) {
if (res.result) {
bgColor.value = 'green';
verifyText.value = '解锁成功';
emit('change', { status: true, distance: distance.value });
} else {
bgColor.value = 'red';
verifyText.value = '解锁失败';
setTimeout(() => {
init();
}, 1000);
emit('change', { status: false, distance: distance.value });
}
} else {
init()
}
}).catch(()=>{
init()
});
}
function close () {
show.value = false;
}
function init () { // 初始化数据
flag.value = false;
downX.value = 0;
distance.value = 0;
bgColor.value = '#04ad11';
verifyText.value = '拖动滑块解锁';
getVerifyImg(type.value).then(res => {
if (res.result) {
data.value = res.result;
show.value = true;
} else {
Message.warning('请求失败请重试!')
}
});
}
// 父组件通过 ref 调用 init/close
defineExpose({ init, close });
watch(() => props.verifyType, (v) => {
type.value = v;
}, { immediate: true });
</script>
<style lang="scss" scoped>
.verify-content{
@@ -174,7 +175,7 @@ export default {
display: flex;
align-items: center;
justify-content: center;
.ivu-icon {
.el-icon {
font-size: 20px;
}
}

View File

@@ -1,5 +1,5 @@
import request, {Method, commonUrl} from '@/plugins/request.js';
import request, {Method, buyerUrl} from '@/plugins/request.js';
import storage from '@/plugins/storage.js';
/**
@@ -7,7 +7,7 @@ import storage from '@/plugins/storage.js';
*/
export function getVerifyImg (verificationEnums) {
return request({
url: `${commonUrl}/common/common/slider/${verificationEnums}`,
url: `${buyerUrl}/buyer/common/slider/${verificationEnums}`,
method: Method.GET,
needToken: false,
headers: {uuid: storage.getItem('uuid')}
@@ -19,7 +19,7 @@ export function getVerifyImg (verificationEnums) {
*/
export function postVerifyImg (params) {
return request({
url: `${commonUrl}/common/common/slider/${params.verificationEnums}`,
url: `${buyerUrl}/buyer/common/slider/${params.verificationEnums}`,
method: Method.POST,
needToken: false,
params,

View File

@@ -0,0 +1,208 @@
/**
* 卡密商品(E_COUPON / 电子卡券)— 买家端公共常量与工具
*
* 与 VIRTUAL_GOODS(核销型虚拟)区分:E_COUPON 常规促销走 BUY_NOW,
* 特殊活动走 PINTUAN / POINTS / KANJIA;禁止 CART / VIRTUAL。
* 需求:lilishop/docs/requirements/card-key-goods-v4.md · M-02
*
* @author Mike
* @date 2026-07-31
*/
export const E_COUPON_GOODS_TYPE = "E_COUPON";
export const E_COUPON_ORDER_TYPE = "E_COUPON";
/** 单次购买数量上限(O-09 默认:min(quantity, 999)) */
export const E_COUPON_MAX_BUY_NUM = 999;
/** 常规立即购买(秒杀/满减/券/限时/第N件等) */
export const E_COUPON_CART_BUY_NOW = "BUY_NOW";
export const E_COUPON_CART_PINTUAN = "PINTUAN";
export const E_COUPON_CART_POINTS = "POINTS";
export const E_COUPON_CART_KANJIA = "KANJIA";
export function isECoupon(goodsType) {
return goodsType === E_COUPON_GOODS_TYPE;
}
export function isECouponOrder(orderType) {
return orderType === E_COUPON_ORDER_TYPE;
}
/** 详情页提示:卡密交付后不支持买家售后(FR-B-04) */
export const E_COUPON_AFTER_SALE_HINT =
"卡密交付后不支持买家售后,请确认商品信息后再购买。";
export function eCouponAfterSaleHint() {
return E_COUPON_AFTER_SALE_HINT;
}
/** 可售库存:E_COUPON 读 SKU quantity(由卡池 syncSkuStock 同步) */
export function getECouponStock(sku) {
if (!sku) return 0;
return Number(sku.quantity) || 0;
}
export function getECouponMaxBuyNum(sku) {
const stock = getECouponStock(sku);
if (stock <= 0) return 1;
return Math.min(stock, E_COUPON_MAX_BUY_NUM);
}
/**
* 解析 E_COUPON 结算 cartType / way
* @param {object} opts
* @param {string} [opts.routeWay] 路由 query.way
* @param {'BUY_NOW'|'PINTUAN'|'POINTS'|'KANJIA'} [opts.action] 用户点击的购买入口
*/
export function resolveECouponCartType({ routeWay, action } = {}) {
if (action === E_COUPON_CART_PINTUAN) return E_COUPON_CART_PINTUAN;
if (action === E_COUPON_CART_POINTS) return E_COUPON_CART_POINTS;
if (action === E_COUPON_CART_KANJIA) return E_COUPON_CART_KANJIA;
if (action === E_COUPON_CART_BUY_NOW) return E_COUPON_CART_BUY_NOW;
const way = routeWay ? String(routeWay).toUpperCase() : "";
if (way === "KANJIA") return E_COUPON_CART_KANJIA;
if (way === "PINTUAN") return E_COUPON_CART_PINTUAN;
if (way === "POINT" || way === "POINTS") return E_COUPON_CART_POINTS;
return E_COUPON_CART_BUY_NOW;
}
/** 详情页 promotionMap 初始结构(切换 SKU 时完整重置) */
export function createEmptyPromotionMap() {
return {
SECKILL: null,
FULL_DISCOUNT: null,
COUPON: [],
DISCOUNT: null,
NTH: null,
PINTUAN: null,
KANJIA: null,
};
}
/** 卡密履约状态(与后端 CardKeyFulfillStatusEnum 一致) */
export const CARD_KEY_FULFILL_STATUS = {
PENDING: "PENDING",
DELIVERED: "DELIVERED",
FAILED: "FAILED",
NOT_APPLICABLE: "NOT_APPLICABLE",
};
export const CARD_KEY_FULFILL_STATUS_TEXT = {
PENDING: "待发放",
DELIVERED: "已发放",
FAILED: "发放失败",
NOT_APPLICABLE: "不适用",
};
export function cardKeyFulfillAlertType(status) {
const map = {
PENDING: "info",
DELIVERED: "success",
FAILED: "warning",
};
return map[status] || "info";
}
export function formatCardKeyFulfillStatus(status) {
return CARD_KEY_FULFILL_STATUS_TEXT[status] || status || "—";
}
export function resolveCardKeyFulfillMessage(line) {
if (line && line.message) return line.message;
return formatCardKeyFulfillStatus(line?.status);
}
export function isCardKeyFulfillApplicable(item) {
if (!item) return false;
const status = item.cardKeyFulfillStatus;
if (status) return status !== CARD_KEY_FULFILL_STATUS.NOT_APPLICABLE;
if (item.cardKeyDelivered != null || (item.cardKeys && item.cardKeys.length)) return true;
return item.goodsType === E_COUPON_GOODS_TYPE;
}
function normalizeCardKeyFulfillLine(source) {
const status =
source.cardKeyFulfillStatus ||
(source.cardKeyDelivered
? CARD_KEY_FULFILL_STATUS.DELIVERED
: CARD_KEY_FULFILL_STATUS.PENDING);
const cardKeys =
status === CARD_KEY_FULFILL_STATUS.DELIVERED && source.cardKeyDelivered
? source.cardKeys || []
: [];
return {
key: source.sn || source.orderSn || source.id || source.skuId,
goodsName: source.goodsName,
status,
message: source.cardKeyFulfillMessage,
cardKeys,
isGift: !!source.isGift,
};
}
/** 汇总当前订单 orderItems 中的卡密履约行(不含满赠子单;赠品卡密在赠品子单详情单独查看) */
export function collectCardKeyFulfillLines(orderItems = []) {
const lines = [];
(orderItems || []).forEach((item) => {
if (!isCardKeyFulfillApplicable(item)) return;
lines.push(normalizeCardKeyFulfillLine(item));
});
return lines;
}
export function orderHasCardKeySection({ orderItems, isECouponOrder } = {}) {
if (isECouponOrder) return true;
if (collectCardKeyFulfillLines(orderItems).length > 0) return true;
return (orderItems || []).some(
(item) =>
item.goodsType === E_COUPON_GOODS_TYPE ||
(Array.isArray(item.cardKeys) && item.cardKeys.length > 0)
);
}
export function flattenCardKeyFulfillLines(lines, withGoodsName = false) {
const rows = [];
(lines || []).forEach((line) => {
if (line.status !== CARD_KEY_FULFILL_STATUS.DELIVERED) return;
(line.cardKeys || []).forEach((ck) => {
rows.push(withGoodsName ? { ...ck, goodsName: line.goodsName } : { ...ck });
});
});
return rows;
}
/** 当前订单是否含已交付卡密(仅 orderItems,不含满赠子单) */
export function orderHasCardKeyContent(orderItems) {
return flattenCardKeyFulfillLines(collectCardKeyFulfillLines(orderItems)).length > 0;
}
/** 扁平化当前订单 orderItems 卡密 */
export function flattenOrderCardKeys(orderItems, withGoodsName = false) {
const lines = collectCardKeyFulfillLines(orderItems);
if (lines.some((line) => line.status)) {
return flattenCardKeyFulfillLines(lines, withGoodsName);
}
const rows = [];
(orderItems || []).forEach((item) => {
if (!item.cardKeyDelivered || !item.cardKeys || !item.cardKeys.length) return;
item.cardKeys.forEach((ck) => {
rows.push(withGoodsName ? { ...ck, goodsName: item.goodsName } : { ...ck });
});
});
return rows;
}
/** createTrade 后是否无需进入收银台(积分单 / 0 元 / 服务端已标记) */
export function shouldSkipPaymentPage(tradeResult, way) {
if (!tradeResult) return false;
if (way === E_COUPON_CART_POINTS) return true;
if (tradeResult.needPay === false || tradeResult.paid === true) return true;
const price =
tradeResult.price != null
? Number(tradeResult.price)
: tradeResult.flowPrice != null
? Number(tradeResult.flowPrice)
: null;
if (price != null && !Number.isNaN(price) && price <= 0) return true;
return false;
}

View File

@@ -1,70 +1,54 @@
import Vue from "vue";
import App from "./App";
import router from "./router";
import ViewUI from "view-design";
import { createApp } from "vue";
import { createPinia } from "pinia";
import "core-js/stable";
import "./assets/styles/global-layout.scss";
import "./assets/styles/theme.less";
// import './assets/iconfont/iconfont.css';
import App from "./App.vue";
import { router } from "./router/index";
import { setupElementPlus } from "@/plugins/element";
import { setupLegacyMessage } from "@/utils/message";
import { registerGlobalComponents } from "@/components/global.js";
import * as filters from "./plugins/filters";
import store from "@/vuex/store";
import storage from "@/plugins/storage";
// 全局引入封装组件
import {InstallAll} from "@/components/global.js";
import { fetchAndApplyTheme } from "@/utils/theme";
import { getThemeSetting } from "@/api/common.js";
import config from "@/config";
import { openLink } from "@/utils/buyerLogin";
const {aMapSecurityJsCode, title, inputMaxLength} = require("@/config");
const { aMapSecurityJsCode, inputMaxLength } = config;
const copyViewUi = {...ViewUI}
copyViewUi.Input.props.maxlength.default = inputMaxLength // 挂载最大输入值
Vue.use(copyViewUi);
Vue.use(InstallAll);
Vue.config.productionTip = false;
Object.keys(filters).forEach((key) => {
Vue.filter(key, filters[key]);
});
// 高德安全密钥
if (aMapSecurityJsCode) {
window._AMapSecurityConfig = {
securityJsCode: aMapSecurityJsCode,
};
}
router.beforeEach((to, from, next) => {
ViewUI.LoadingBar.start();
window.document.title = to.meta.title === undefined ? title : to.meta.title;
next();
});
const app = createApp(App);
router.afterEach((route) => {
ViewUI.LoadingBar.finish();
});
setupElementPlus(app);
setupLegacyMessage(app);
registerGlobalComponents(app);
Vue.prototype.linkTo = function (url) {
if (url.substr(0, 1) === "/") {
// 非外部链接,没有origin,只有路由地址
if (router.mode === "hash") {
window.open(location.origin + "/#" + url, "_blank");
} else {
window.open(location.origin + url, "_blank");
}
} else {
// 外部链接,完整的url地址
window.open(url, "_blank");
}
app.use(router);
app.use(createPinia());
app.config.globalProperties.$filters = filters;
app.config.globalProperties.Cookies = storage;
app.config.globalProperties.$inputMaxLength = inputMaxLength;
app.config.globalProperties.linkTo = function (url) {
openLink(url, router);
};
// 联系客服
Vue.prototype.connectCs = function (
app.config.globalProperties.connectCs = function (
sign = "37ef9b97807d03c6741298ed4eb5b536d2d238e08a3c00fb01fe48f03a569974c99ad767e72c04b3165ef29aca2c488b505fe4ca"
) {
const url = "https://yzf.qq.com/xv/web/static/chat/index.html?sign=" + sign;
const url =
"https://yzf.qq.com/xv/web/static/chat/index.html?sign=" + sign;
window.open(url, "_blank");
};
Vue.prototype.Cookies = storage;
/* eslint-disable no-new */
new Vue({
router,
store,
render: (h) => h(App),
}).$mount("#app");
router.isReady().then(async () => {
await fetchAndApplyTheme(getThemeSetting);
app.mount("#app");
});

File diff suppressed because it is too large Load Diff

View File

@@ -2,200 +2,320 @@
<div class="coupon-center">
<BaseHeader></BaseHeader>
<div class="content">
<div>
<div class="coupon-title">
<router-link to="/">
<img src="../assets/images/logo.png" width="120" alt="" />
</router-link>
<p>领券中心</p>
<Input
search
style="width: 400px"
@on-search="search"
enter-button="搜索"
placeholder="搜索优惠券"
/>
</div>
<h2 class="coupon-page-title">领券中心</h2>
<div class="fontsize_18 recommend">推荐好券</div>
<empty v-if="list.length === 0" />
<ul class="coupon-list" v-else>
<li v-for="(item, index) in list" class="coupon-item" :key="index">
<div class="c-left">
<div>
<span
v-if="item.couponType === 'PRICE'"
class="fontsize_12 global_color"
>¥<span class="price">{{
item.price | unitPrice
}}</span></span
>
<span
v-if="item.couponType === 'DISCOUNT'"
class="fontsize_12 global_color"
><span class="price">{{ item.couponDiscount }}</span
>折</span
>
<span class="describe"
>满{{ item.consumeThreshold }}元可用</span
>
</div>
<p>使用范围:{{ useScope(item.scopeType, item.storeName) }}</p>
<p>有效期:{{ item.endTime }}</p>
<empty v-if="!loading && list.length === 0" />
<ul class="coupon-list" v-else-if="list.length > 0">
<li v-for="item in list" class="coupon-item" :key="item.id">
<div class="c-left">
<div class="coupon-amount">
<span v-if="item.couponType === 'PRICE'" class="global_color">
¥<span class="price">{{ $filters.unitPrice(item.price) }}</span>
</span>
<span v-if="item.couponType === 'DISCOUNT'" class="global_color">
<span class="price">{{ item.couponDiscount || item.discount }}</span>折
</span>
<span class="describe">{{ thresholdText(item) }}</span>
</div>
<b></b>
<a class="c-right" @click="receive(item)">立即领取</a>
<i class="circle-top"></i>
<i class="circle-bottom"></i>
</li>
</ul>
<Page
:total="total"
@on-change="changePageNum"
class="pageration"
@on-page-size-change="changePageSize"
:page-size="params.pageSize"
show-total
show-sizer
>
</Page>
</div>
<p class="coupon-name">
<span class="scope-tag">{{ scopeTag(item.scopeType, item.storeName) }}</span>
<span>{{ item.couponName }}</span>
</p>
<p class="coupon-time">有效期:{{ item.endTime }}</p>
</div>
<a
class="c-right"
:class="{ 'is-finished': isFinished(item) }"
@click="receive(item)"
>{{ isFinished(item) ? "已领完" : "立即领取" }}</a>
</li>
</ul>
<el-pagination
v-model:current-page="params.pageNumber"
v-model:page-size="params.pageSize"
:total="total"
@current-change="changePageNum"
class="pageration"
@size-change="changePageSize"
layout="total, sizes, prev, pager, next"
></el-pagination>
</div>
<BaseFooter></BaseFooter>
</div>
</template>
<script>
<script setup>
import { onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Modal } from "@/utils/message";
import storage from "@/plugins/storage";
import { couponList, receiveCoupon } from "@/api/member.js";
export default {
data() {
return {
list: [], // 优惠券列表
total: 0, // 优惠券总数
params: {
// 请求参数
getType: "FREE",
pageNumber: 1,
pageSize: 20,
},
};
},
methods: {
// 搜索优惠券
search(item) {
this.params.couponName = item;
this.params.pageNumber = 1;
this.getList();
},
// 获取优惠券列表
getList() {
this.$Spin.show();
couponList(this.params)
.then((res) => {
this.$Spin.hide();
this.loading = false;
if (res.success) {
this.list = res.result.records;
this.total = res.result.total;
}
})
.catch(() => {
this.$Spin.hide();
});
},
// 分页 改变页码
changePageNum(val) {
this.params.pageNumber = val;
this.getList();
},
// 分页 改变每页数
changePageSize(val) {
this.params.pageNumber = 1;
this.params.pageSize = val;
this.getList();
},
// 领取优惠券
receive(item) {
receiveCoupon(item.id).then((res) => {
if (res.success) {
console.log(item);
this.$Modal.confirm({
title: "领取优惠券",
content: "<p>优惠券领取成功,可到我的优惠券页面查看</p>",
okText: "我的优惠券",
cancelText: "立即使用",
closable: true,
onOk: () => {
this.$router.push("/home/Coupons");
},
onCancel: () => {
this.$router.push({
path: "/goodsList",
query: { promotionsId: item.id, promotionType: "COUPON" },
});
},
});
}
});
},
// 优惠券可用范围
useScope(type, storeName) {
let shop = "平台";
let goods = "全部商品";
if (storeName !== "platform") shop = storeName;
switch (type) {
case "ALL":
goods = "全部商品";
break;
case "PORTION_GOODS":
goods = "部分商品";
break;
case "PORTION_GOODS_CATEGORY":
goods = "部分分类商品";
break;
const route = useRoute();
const router = useRouter();
const list = ref([]); // 优惠券列表
const total = ref(0); // 优惠券总数
const loading = ref(false);
const params = reactive({
// 请求参数
getType: "FREE",
pageNumber: 1,
pageSize: 20,
});
function getList() {
if (loading.value) return;
loading.value = true;
couponList(params)
.then((res) => {
if (res && res.success && res.result) {
list.value = res.result.records || [];
total.value = res.result.total || 0;
} else {
list.value = [];
total.value = 0;
}
return `${shop}${goods}可用`;
},
},
mounted() {
this.getList();
},
};
})
.catch(() => {
list.value = [];
total.value = 0;
})
.finally(() => {
loading.value = false;
});
}
// 分页 改变页码
function changePageNum(val) {
params.pageNumber = val;
getList();
}
// 分页 改变每页数
function changePageSize(val) {
params.pageNumber = 1;
params.pageSize = val;
getList();
}
// 领取优惠券
function receive(item) {
if (isFinished(item)) return;
const userInfo = storage.getItem("userInfo");
const isLogin = userInfo && JSON.parse(userInfo).username;
if (!isLogin) {
Modal.confirm({
title: "温馨提示",
content: "请登录后执行此操作",
okText: "立即登录",
cancelText: "取消",
onOk: () => {
router.push({
path: "/login",
query: {
rePath: route.path,
query: JSON.stringify(route.query),
},
});
},
});
return;
}
receiveCoupon(item.id)
.then((res) => {
if (!res || !res.success) return;
Modal.confirm({
title: "领取优惠券",
content: "优惠券领取成功,可到我的优惠券页面查看",
okText: "我的优惠券",
cancelText: "立即使用",
closable: true,
onOk: () => {
router.push("/home/Coupons");
},
onCancel: () => {
router.push({
path: "/goodsList",
query: { promotionsId: item.id, promotionType: "COUPON" },
});
},
});
})
.catch((err) => {
console.warn('领取优惠券失败', err);
});
}
// 发行量已领完
function isFinished(item) {
return Number(item.publishNum) > 0 && Number(item.receivedNum) >= Number(item.publishNum);
}
// 使用门槛文案
function thresholdText(item) {
const threshold = Number(item.consumeThreshold);
if (!threshold) return "无使用门槛";
return `满${item.consumeThreshold}元可用`;
}
// 券范围。店铺名为 platform 时是平台通用券
function scopeTag(type, storeName) {
if (type === "PORTION_GOODS") return "商品券";
if (type === "PORTION_GOODS_CATEGORY") return "分类券";
if (storeName && storeName !== "platform") return "店铺券";
return "通用券";
}
onMounted(() => {
getList();
});
</script>
<style lang="scss" scoped>
@import "../assets/styles/coupon.scss";
.coupon-center {
background: #f5f5f5;
}
.content {
width: 100%;
background-color: #fff;
> div {
margin: 10px auto;
width: 1200px;
}
width: 1200px;
margin: 0 auto 48px;
background: transparent;
}
.coupon-title {
display: flex;
align-items: center;
p {
font-size: 18px;
margin-right: 500px;
}
border-bottom: 2px solid $theme_color;
}
.recommend {
margin: 20px auto;
font-weight: bold;
width: 200px;
.coupon-page-title {
margin: 28px 0 20px;
color: #171A26;
font-size: 28px;
font-weight: 600;
text-align: center;
}
.coupon-list {
gap: 16px;
margin: 0;
padding: 0;
list-style: none;
}
.coupon-item {
b {
background: url("../assets/images/small-circle.png") top left repeat-y;
width: calc((100% - 32px) / 3);
height: auto;
min-height: 132px;
margin: 0;
display: flex;
align-items: stretch;
overflow: hidden;
border: none;
border-radius: 8px;
background: #fff;
b,
i.circle-top,
i.circle-bottom {
display: none;
}
.c-left {
flex: 1;
width: auto;
min-width: 0;
padding: 16px 16px 14px;
box-sizing: border-box;
.coupon-amount {
display: flex;
align-items: baseline;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.price {
font-size: 28px;
font-weight: 600;
line-height: 1;
}
.describe {
margin-left: 0;
padding: 0;
background: transparent;
font-size: 13px;
line-height: 20px;
}
.coupon-name {
display: flex;
align-items: center;
gap: 6px;
margin: 0 0 8px;
color: #171A26;
font-size: 14px;
font-weight: 400;
line-height: 20px;
}
.scope-tag {
flex-shrink: 0;
padding: 0 6px;
border-radius: 2px;
background: #fff5f4;
color: $theme_color;
font-size: 12px;
line-height: 20px;
}
.coupon-name span:last-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.coupon-time {
margin: 0;
color: #999;
font-size: 12px;
font-weight: 400;
line-height: 18px;
}
}
.c-right {
position: relative;
right: auto;
top: auto;
flex-shrink: 0;
box-sizing: border-box;
width: 48px;
height: auto;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
writing-mode: vertical-rl;
letter-spacing: 4px;
white-space: nowrap;
font-size: 14px;
font-weight: 600;
line-height: 1;
cursor: pointer;
text-decoration: none;
user-select: none;
&:hover {
opacity: 0.92;
}
&.is-finished {
background: #c8c8c8;
cursor: default;
&:hover {
opacity: 1;
}
}
}
}
.pageration {
text-align: right;
padding-bottom: 10px;
display: flex;
justify-content: flex-end;
margin-top: 20px;
padding-bottom: 0;
}
</style>

View File

@@ -1,89 +1,85 @@
<template>
<div class="forget-password" @click='$refs.verify.show = false'>
<div class="forget-password" @click="hideVerify">
<div style="height:50px;"></div>
<!-- 顶部logo -->
<div class="logo-box">
<img
:src="$store.state.logoImg" width='150'
:src="logoImg" width='150'
@click="$router.push('/')"
/>
<div>修改密码</div>
</div>
<div class="login-container">
<!-- 验证手机号 -->
<Form
ref="formFirst"
<el-form
ref="formFirstRef"
:model="formFirst"
:rules="ruleInline"
style="width:300px;"
v-show="step === 0"
>
<FormItem prop="mobile">
<i-input
<el-form-item prop="mobile">
<el-input
type="text"
v-model="formFirst.mobile"
clearable
placeholder="手机号"
>
<Icon type="md-phone-portrait" slot="prepend"></Icon>
</i-input>
</FormItem>
<FormItem prop="code">
<i-input
<template #prepend><el-icon><Iphone /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item prop="code">
<el-input
type="text"
v-model="formFirst.code"
clearable
placeholder="手机验证码"
>
<Icon
type="ios-text-outline"
style="font-weight: bold"
slot="prepend"
/>
<Button slot="append" @click="sendCode">{{ codeMsg }}</Button>
</i-input>
</FormItem>
<FormItem>
<Button @click="verifyBtnClick" long :type="verifyStatus?'success':'default'">{{verifyStatus?'验证通过':'点击完成安全验证'}}</Button>
</FormItem>
<FormItem>
<Button type="error" @click="next" :loading="loading" long>下一步</Button>
</FormItem>
</Form>
<Form
ref="form"
<template #prepend><el-icon style="font-weight: bold"><Document /></el-icon></template>
<template #append><el-button @click="sendCode">{{ codeMsg }}</el-button></template>
</el-input>
</el-form-item>
<el-form-item>
<el-button @click="verifyBtnClick" style="width:100%" :type="verifyStatus?'success':'default'">{{verifyStatus?'验证通过':'点击完成安全验证'}}</el-button>
</el-form-item>
<el-form-item>
<el-button type="danger" @click="next" :loading="loading" style="width:100%">下一步</el-button>
</el-form-item>
</el-form>
<el-form
ref="formRef"
:model="form"
:rules="ruleInline"
style="width:300px;"
v-show="step === 1"
>
<FormItem prop="password">
<i-input
<el-form-item prop="password">
<el-input
type="password"
v-model="form.password"
clearable
placeholder="请输入至少六位密码"
>
<Icon type="md-lock" slot="prepend"></Icon>
</i-input>
</FormItem>
<FormItem prop="password">
<i-input
<template #prepend><el-icon><Lock /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
type="password"
v-model="form.oncePasd"
clearable
placeholder="请再次输入密码"
>
<Icon type="md-lock" slot="prepend"></Icon>
</i-input>
</FormItem>
<FormItem>
<Button type="error" size="large" @click="handleSubmit" :loading="loading1" long>提交</Button>
</FormItem>
</Form>
<template #prepend><el-icon><Lock /></el-icon></template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="danger" size="large" @click="handleSubmit" :loading="loading1" style="width:100%">提交</el-button>
</el-form-item>
</el-form>
<!-- 拼图验证码 -->
<Verify
ref="verify"
ref="verifyRef"
class="verify-con"
:verifyType="verifyType"
@change="verifyChange"
@@ -91,164 +87,175 @@
<div class="login-btn"><a @click="$router.push('login')">前往登录</a></div>
</div>
<div class="foot">
<Row type="flex" justify="space-around" class="help">
<div type="flex" justify="space-around" class="help">
<a class="item" href="https://pickmall.cn/" target="_blank">帮助</a>
<a class="item" href="https://pickmall.cn/" target="_blank">隐私</a>
<a class="item" href="https://pickmall.cn/" target="_blank">条款</a>
</Row>
<Row type="flex" justify="center" class="copyright">
</div>
<div type="flex" justify="center" class="copyright">
Copyright © {{year}} - Present
<a href="https://pickmall.cn/" target="_blank" style="margin: 0 5px"
>{{config.title}}</a
>
版权所有
</Row>
</div>
</div>
</div>
</template>
<script>
<script setup>
import { computed, onMounted, reactive, ref } from "vue";
import { useRouter } from "vue-router";
import { Message } from "@/utils/message";
import * as RegExp from '@/plugins/RegExp.js';
import { md5 } from '@/plugins/md5.js';
import * as apiLogin from '@/api/login.js';
import { sendSms } from '@/api/common.js';
import Verify from '@/components/verify';
export default {
name: 'ForgetPassword',
components: { Verify },
data () {
return {
config:require('@/config'),
loading: false, // 加载状态
loading1: false, // 第二步加载状态
formFirst: { // 手机验证码表单
// 注册表单
mobile: '',
code: ''
},
form: { // 密码
password: '',
oncePasd: ''
},
year: new Date().getFullYear(), // 当前年份
step: 0, // 步骤
ruleInline: {
// 验证规则
mobile: [
{ required: true, message: '请输入手机号码' },
{
pattern: RegExp.mobile,
trigger: 'blur',
message: '请输入正确的手机号'
}
],
code: [{ required: true, message: '请输入手机验证码' }],
password: [{required: true, message: '密码不能为空'}, {pattern: RegExp.password, message: '密码不能少于6位'}]
},
verifyStatus: false, // 图片验证状态
verifyType: 'FIND_USER', // 图片验证类型
codeMsg: '发送验证码', // 验证码文字
interval: '', // 定时器
time: 60 // 倒计时时间
};
},
methods: {
// 提交短信验证码,修改密码
next () {
this.$refs.formFirst.validate((valid) => {
if (valid) {
this.loading = true;
let data = JSON.parse(JSON.stringify(this.formFirst));
apiLogin.validateCode(data).then((res) => {
this.loading = false;
if (res.success) {
// this.$Message.success('');
this.step = 1;
} else {
this.$Message.warning(res.message);
}
}).catch(() => { this.loading = false; });
} else {}
});
},
handleSubmit () { // 提交密码
this.$refs.form.validate(valid => {
if (valid) {
let params = JSON.parse(JSON.stringify(this.form));
if (params.password !== params.oncePasd) {
this.$Message.warning('两次输入密码不一致');
return;
};
params.mobile = this.formFirst.mobile;
params.password = md5(params.password);
delete params.oncePasd;
this.loading1 = true;
import { Lock, Iphone, Document } from '@element-plus/icons-vue';
import config from '@/config';
import { useAppStore } from '@/stores/app';
apiLogin.resetPassword(params).then(res => {
this.loading1 = false;
console.log(res);
if (res.success) {
this.$Message.success('修改密码成功');
this.$router.push('login');
}
}).catch(() => { this.loading = false; });
};
});
},
sendCode () { // 发送验证码
if (this.time === 60) {
if (this.formFirst.mobile === '') {
this.$Message.warning('请先填写手机号');
return;
}
if (!this.verifyStatus) {
this.$Message.warning('请先完成安全验证');
return;
}
let params = {
mobile: this.formFirst.mobile,
verificationEnums: 'FIND_USER'
};
sendSms(params).then(res => {
if (res.success) {
this.$Message.success('验证码发送成功');
let that = this;
this.interval = setInterval(() => {
that.time--;
if (that.time === 0) {
that.time = 60;
that.codeMsg = '重新发送';
that.verifyStatus = false;
clearInterval(that.interval);
} else {
that.codeMsg = that.time;
}
}, 1000);
} else {
this.$Message.warning(res.message);
}
});
}
},
verifyChange (con) { // 验证通过
if (!con.status) return;
this.$refs.verify.show = false;
this.verifyStatus = true;
},
verifyBtnClick () {
if (!this.verifyStatus) {
this.$refs.verify.init();
}
defineOptions({ name: 'ForgetPassword' });
const router = useRouter();
const appStore = useAppStore();
const logoImg = computed(() => appStore.logoImg);
const loading = ref(false); // 加载状态
const loading1 = ref(false); // 第二步加载状态
const formFirst = reactive({ // 手机验证码表单
// 注册表单
mobile: '',
code: ''
});
const form = reactive({ // 密码
password: '',
oncePasd: ''
});
const year = new Date().getFullYear(); // 当前年份
const step = ref(0); // 步骤
const ruleInline = {
// 验证规则
mobile: [
{ required: true, message: '请输入手机号码' },
{
pattern: RegExp.mobile,
trigger: 'blur',
message: '请输入正确的手机号'
}
},
mounted () {
document.querySelector('.forget-password').style.height = window.innerHeight + 'px'
this.$refs.formFirst.resetFields();
},
watch: {
}
],
code: [{ required: true, message: '请输入手机验证码' }],
password: [{required: true, message: '密码不能为空'}, {pattern: RegExp.password, message: '密码不能少于6位'}]
};
const verifyStatus = ref(false); // 图片验证状态
const verifyType = ref('FIND_USER'); // 图片验证类型
const codeMsg = ref('发送验证码'); // 验证码文字
const interval = ref(''); // 定时器
const time = ref(60); // 倒计时时间
const formFirstRef = ref(null);
const formRef = ref(null);
const verifyRef = ref(null);
function hideVerify() {
verifyRef.value?.close?.();
}
// 提交短信验证码,修改密码
function next () {
formFirstRef.value.validate((valid) => {
if (valid) {
loading.value = true;
let data = JSON.parse(JSON.stringify(formFirst));
apiLogin.validateCode(data).then((res) => {
loading.value = false;
if (res.success) {
// Message.success('');
step.value = 1;
} else {
Message.warning(res.message);
}
}).catch(() => { loading.value = false; });
}
});
}
function handleSubmit () { // 提交密码
formRef.value.validate(valid => {
if (valid) {
let params = JSON.parse(JSON.stringify(form));
if (params.password !== params.oncePasd) {
Message.warning('两次输入密码不一致');
return;
};
params.mobile = formFirst.mobile;
params.password = md5(params.password);
delete params.oncePasd;
loading1.value = true;
apiLogin.resetPassword(params).then(res => {
loading1.value = false;
if (res.success) {
Message.success('修改密码成功');
router.push('login');
}
}).catch(() => { loading.value = false; });
};
});
}
function sendCode () { // 发送验证码
if (time.value === 60) {
if (formFirst.mobile === '') {
Message.warning('请先填写手机号');
return;
}
if (!verifyStatus.value) {
Message.warning('请先完成安全验证');
return;
}
let params = {
mobile: formFirst.mobile,
verificationEnums: 'FIND_USER'
};
sendSms(params).then(res => {
if (res.success) {
Message.success('验证码发送成功');
interval.value = setInterval(() => {
time.value--;
if (time.value === 0) {
time.value = 60;
codeMsg.value = '重新发送';
verifyStatus.value = false;
clearInterval(interval.value);
} else {
codeMsg.value = time.value;
}
}, 1000);
} else {
Message.warning(res.message);
}
});
}
}
function verifyChange (con) { // 验证通过
if (!con.status) return;
hideVerify();
verifyStatus.value = true;
}
function verifyBtnClick () {
if (!verifyStatus.value) {
verifyRef.value?.init();
}
}
onMounted(() => {
document.querySelector('.forget-password').style.height = window.innerHeight + 'px'
formFirstRef.value.resetFields();
});
</script>
<style scoped lang="scss">
@@ -294,7 +301,7 @@ export default {
.other-login {
margin: 0 auto;
.ivu-icon {
.el-icon {
font-size: 24px;
}
}

View File

@@ -1,27 +1,15 @@
<template>
<div style="background: #fff">
<div class="goods-detail-page">
<BaseHeader></BaseHeader>
<Search></Search>
<div class="shop-item-path">
<div class="shop-nav-container">
<Breadcrumb>
<BreadcrumbItem to="/">首页</BreadcrumbItem>
<BreadcrumbItem v-for="(item, index) in categoryBar" :to="goGoodsList(index)" target="_blank" :key="index">
<el-breadcrumb>
<el-breadcrumb-item to="/">首页</el-breadcrumb-item>
<el-breadcrumb-item v-for="(item, index) in categoryBar" :to="goGoodsList(index)" :key="index">
{{ item.name }}
</BreadcrumbItem>
</Breadcrumb>
<div class="store-collect" v-if="!takeDownSale">
<span class="mr_10" v-if="goodsMsg.data">
<router-link :to="'Merchant?id=' + goodsMsg.data.storeId">{{
goodsMsg.data.storeName
}}</router-link>
</span>
<span @click="collect">
<Icon type="ios-heart" :color="storeCollected ? '#ed3f14' : '#666'" />
{{ storeCollected? "已收藏店铺": "收藏店铺" }}
</span>
<span class="ml_10" @click="IMService(goodsMsg.data.storeId,goodsMsg.data.goodsId,goodsMsg.data.id)">联系客服</span>
</div>
</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
@@ -32,202 +20,164 @@
<empty _Title='当前商品已下架' v-if="takeDownSale">
<div class="sale-btn">
<Button size="small" class="mr_10" @click="target('/')">返回首页</Button>
<Button size="small" @click="target('goodsList')">返回商品列表</Button>
<el-button size="small" class="mr_10" @click="target('/')">返回首页</el-button>
<el-button size="small" @click="target('goodsList')">返回商品列表</el-button>
</div>
</empty>
<Spin size="large" fix v-if="isLoading"></Spin>
<el-skeleton size="large" fix v-if="isLoading"></el-skeleton>
<BaseFooter></BaseFooter>
</div>
</template>
<script>
import Search from "@/components/Search";
import ShowGoods from "@/components/goodsDetail/ShowGoods";
import empty from "@/components/empty/Main";
import ShowGoodsDetail from "@/components/goodsDetail/ShowGoodsDetail";
import { goodsSkuDetail } from "@/api/goods";
import {
cancelStoreCollect,
collectStore,
isStoreCollection,
getGoodsDistribution,
} from "@/api/member";
import { getDetailById } from "@/api/shopentry";
import imTalk from '@/components/mixes/talkIm'
export default {
name: "GoodsDetail",
beforeRouteEnter (to, from, next) {
window.scrollTo(0, 0);
next();
},
created () {
this.getGoodsDetail();
},
mixins: [imTalk],
data () {
return {
goodsMsg: {}, // 商品信息
isLoading: false, // 加载状态
categoryBar: [], // 分类
storeCollected: false, // 商品收藏
storeMsg: {}, // 店铺信息
takeDownSale:false, // 是否下架
};
},
methods: {
// 跳转首页或商品页面
target(url){
this.$router.push({path: url})
},
// 点击规格
targetClickSku (val) {
this.getGoodsDetail(val);
},
// 获取商品详情
getGoodsDetail (val) {
this.isLoading = true;
const params = val || this.$route.query;
// 分销员id
let distributionId =
params && params.distributionId
? params.distributionId
: this.Cookies.getItem("distributionId");
// 如果有分销信息
if (distributionId) {
console.log(distributionId);
// 先存储
this.Cookies.setItem("distributionId", params.distributionId);
let _this = this;
// 绑定关系
getGoodsDistribution(params.distributionId).then((res) => {
// 绑定成功,则清除关系
if (res.success) {
_this.Cookies.removeItem("distributionId");
}
});
}
goodsSkuDetail(params)
.then((res) => {
this.isLoading = false;
if (res.success) {
const result = res.result;
const cateName = res.result.categoryName;
const cateId = result.data.categoryPath.split(",");
const cateArr = [];
cateId.forEach((e, index) => {
// 插入分类id和name
cateArr.push({
id: e,
name: cateName ? cateName[index] : "",
});
});
this.categoryBar = cateArr;
this.$set(this, "goodsMsg", res.result);
if (!this.goodsMsg.data.intro) {
this.goodsMsg.data.intro = ''
}
// 判断是否收藏
if (this.Cookies.getItem("userInfo")) {
isStoreCollection("STORE", this.goodsMsg.data.storeId).then((res) => {
if (res.success && res.result) {
this.storeCollected = true;
}
});
}
if (!this.storeMsg) {
// 获取店铺信息
getDetailById(this.goodsMsg.data.storeId).then((res) => {
if (res.success) {
this.storeMsg = res.result;
}
});
}
} else {
this.$Message.error(res.message);
this.isLoading = false
}
})
.catch((e) => {
this.isLoading = false
if(e.code === 11001){
this.takeDownSale = true
}
});
},
goGoodsList (currIndex) {
// 跳转商品列表
const arr = [];
this.categoryBar.forEach((e, index) => {
if (index <= currIndex) {
arr.push(e.id);
}
});
return location.origin + "/goodsList?categoryId=" + arr.toString();
},
async collect () {
// 收藏店铺
if (this.storeCollected) {
let cancel = await cancelStoreCollect("STORE", this.goodsMsg.data.storeId);
if (cancel.success) {
this.$Message.success("已取消收藏");
this.storeCollected = false;
}
} else {
let collect = await collectStore("STORE", this.goodsMsg.data.storeId);
if (collect.code === 200) {
this.storeCollected = true;
this.$Message.success("收藏店铺成功,可以前往个人中心我的收藏查看");
}
}
},
},
watch: {},
components: {
Search,
ShowGoods,
ShowGoodsDetail,
empty
},
};
</script>
<script setup>
import { ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { Message } from "@/utils/message";
import storage from "@/plugins/storage.js";
import Search from "@/components/Search";
import ShowGoods from "@/components/goodsDetail/ShowGoods";
import empty from "@/components/empty/Main";
import ShowGoodsDetail from "@/components/goodsDetail/ShowGoodsDetail";
import { goodsSkuDetail } from "@/api/goods";
import { getGoodsDistribution } from "@/api/member";
const route = useRoute();
const router = useRouter();
const goodsMsg = ref({}); // 商品信息
const isLoading = ref(false); // 加载状态
const categoryBar = ref([]); // 分类
const takeDownSale = ref(false); // 是否下架
// 跳转首页或商品页面
function target(url){
router.push({path: url})
}
// 点击规格
function targetClickSku (val) {
getGoodsDetail(val);
}
// 获取商品详情
function getGoodsDetail (val) {
isLoading.value = true;
const params = val || route.query;
// 分销员id
let distributionId =
params && params.distributionId
? params.distributionId
: storage.getItem("distributionId");
// 如果有分销信息
if (distributionId) {
// 先存储
storage.setItem("distributionId", params.distributionId);
// 绑定关系
getGoodsDistribution(params.distributionId).then((res) => {
// 绑定成功,则清除关系
if (res.success) {
storage.removeItem("distributionId");
}
});
}
goodsSkuDetail(params)
.then((res) => {
isLoading.value = false;
if (res.success) {
const result = res.result;
const cateName = res.result.categoryName;
const cateId = result.data.categoryPath.split(",");
const cateArr = [];
cateId.forEach((e, index) => {
// 插入分类id和name
cateArr.push({
id: e,
name: cateName ? cateName[index] : "",
});
});
categoryBar.value = cateArr;
goodsMsg.value = res.result;
if (!goodsMsg.value.data.intro) {
goodsMsg.value.data.intro = ''
}
} else {
Message.error(res.message);
isLoading.value = false
}
})
.catch((e) => {
isLoading.value = false
if(e.code === 11001){
takeDownSale.value = true
}
});
}
function goGoodsList (currIndex) {
// 跳转商品列表
const arr = [];
categoryBar.value.forEach((e, index) => {
if (index <= currIndex) {
arr.push(e.id);
}
});
return {
path: "/goodsList",
query: { categoryId: arr.toString() },
};
}
// 初始化(原 created 钩子逻辑)
getGoodsDetail();
</script>
<style scoped lang="scss">
.goods-detail-page {
background: #f5f5f5;
}
.shop-item-path {
height: 38px;
@include background_color($light_background_color);
line-height: 38px;
color: #2c2c2c;
height: 44px;
background: transparent;
color: #666;
}
.shop-nav-container {
width: 1200px;
height: 100%;
margin: 0 auto;
position: relative;
display: flex;
align-items: center;
.store-collect {
position: absolute;
right: 20px;
top: 0;
color: #999;
:deep(.el-breadcrumb) {
line-height: 1;
font-size: 14px;
}
span {
&:hover {
cursor: pointer;
color: $theme_color;
}
}
:deep(.el-breadcrumb__inner) {
color: #666;
font-weight: 400;
}
:deep(.el-breadcrumb__item:last-child .el-breadcrumb__inner) {
color: #171A26;
}
}
.sale-btn{
margin:10px 0
}
</style>

Some files were not shown because too many files have changed in this diff Show More