mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-10 02:49:03 +08:00
Compare commits
4 Commits
feature/pi
...
v5.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76b0d6efc8 | ||
|
|
fd241bc36a | ||
|
|
d799b37f0c | ||
|
|
0232ddeb27 |
@@ -1,201 +0,0 @@
|
|||||||
# 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)
|
|
||||||
|
|
||||||
每上一端,至少完成本文 **第一节通用检查** + 对应 **第二节分端检查** 后再切下一端。
|
|
||||||
@@ -45,32 +45,36 @@ export function getRegion (id) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 分页获取文章列表
|
* 分页获取文章列表
|
||||||
* @param cateId 文章分类id
|
* @param params 分页及分类查询条件
|
||||||
|
* @param options 请求选项,skipMessage 为 true 时由页面展示错误
|
||||||
*/
|
*/
|
||||||
export function articleList (params) {
|
export function articleList (params, options = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/other/article`,
|
url: `/buyer/other/article`,
|
||||||
method: Method.GET,
|
method: Method.GET,
|
||||||
params
|
params,
|
||||||
|
skipMessage: options.skipMessage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取帮助中心文章分类列表
|
* 获取帮助中心文章分类列表
|
||||||
* @param cateId 文章分类id
|
* @param options 请求选项,skipMessage 为 true 时由页面展示错误
|
||||||
*/
|
*/
|
||||||
export function articleCateList () {
|
export function articleCateList (options = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/other/article/articleCategory/list`,
|
url: `/buyer/other/article/articleCategory/list`,
|
||||||
method: Method.GET
|
method: Method.GET,
|
||||||
|
skipMessage: options.skipMessage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通过id获取文章
|
/** 通过文章 ID 获取正文;可由页面接管错误提示。 */
|
||||||
export function articleDetail (id) {
|
export function articleDetail (id, options = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: `/buyer/other/article/get/${id}`,
|
url: `/buyer/other/article/get/${id}`,
|
||||||
method: Method.GET
|
method: Method.GET,
|
||||||
|
skipMessage: options.skipMessage
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ import request, { Method } from '@/plugins/request.js';
|
|||||||
/** 查询当前会员的卡规则及候选 SKU。 */
|
/** 查询当前会员的卡规则及候选 SKU。 */
|
||||||
export const getPickupCard = id => request({ url: `/buyer/promotion/pickupCard/${encodeURIComponent(id)}`, method: Method.GET, needToken: true });
|
export const getPickupCard = id => request({ url: `/buyer/promotion/pickupCard/${encodeURIComponent(id)}`, method: Method.GET, needToken: true });
|
||||||
/** 服务端校验商品与地址,返回零现金试算。 */
|
/** 服务端校验商品与地址,返回零现金试算。 */
|
||||||
export const previewPickupCard = data => request({ url: '/buyer/promotion/pickupCard/preview', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data });
|
export const previewPickupCard = data => request({ url: '/buyer/promotion/pickupCard/preview', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data: { ...data, client: 'PC' } });
|
||||||
/** 一次兑换;重试返回同一卡已创建的交易。 */
|
/** 一次兑换;重试返回同一卡已创建的交易。 */
|
||||||
export const redeemPickupCard = data => request({ url: '/buyer/promotion/pickupCard/redeem', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data });
|
export const redeemPickupCard = data => request({ url: '/buyer/promotion/pickupCard/redeem', method: Method.POST, needToken: true, headers: { 'Content-Type': 'application/json' }, data: { ...data, client: 'PC' } });
|
||||||
|
|||||||
@@ -50,10 +50,10 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultMenus = [
|
const defaultMenus = [
|
||||||
{ title: '基本信息', url: '/home/UserInfo' },
|
{ title: '基本信息', url: '/home/Profile' },
|
||||||
{ title: '我的订单', url: '/home/MyOrder' },
|
{ title: '我的订单', url: '/home/MyOrder' },
|
||||||
{ title: '账户安全', url: '/home/AccountSafe' },
|
{ title: '账户安全', url: '/home/AccountSafe' },
|
||||||
{ title: '消息', url: '/home/Msg' }
|
{ title: '消息', url: '/home/MsgList' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const username = computed(() => {
|
const username = computed(() => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- 1. 用户头像与信息区 -->
|
<!-- 1. 用户头像与信息区 -->
|
||||||
<div class="user-main-info">
|
<div class="user-main-info">
|
||||||
<div class="avatar-box">
|
<div class="avatar-box">
|
||||||
<img v-if="options.avatar" :src="options.avatar" alt="avatar" />
|
<img v-if="displayAvatar" :src="displayAvatar" alt="用户头像" />
|
||||||
<div v-else class="avatar-placeholder">
|
<div v-else class="avatar-placeholder">
|
||||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="#ffffff">
|
<svg viewBox="0 0 24 24" width="22" height="22" fill="#ffffff">
|
||||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z" />
|
||||||
@@ -12,18 +12,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-greeting">
|
<div class="user-greeting">
|
||||||
<div class="badge-tag">{{ options.badgeText || '请登录' }}</div>
|
<div class="badge-tag">{{ isLoggedIn ? '已登录' : (options.badgeText || '请登录') }}</div>
|
||||||
<div class="welcome-text" :title="options.welcomeText || '欢迎来到商城'">
|
<div class="welcome-text" :title="welcomeText">
|
||||||
{{ options.welcomeText || '欢迎来到商城' }}
|
{{ welcomeText }}
|
||||||
</div>
|
</div>
|
||||||
<div class="sub-text" :title="options.subtitle || '登录后查看专属权益'">
|
<div class="sub-text" :title="subtitle">
|
||||||
{{ options.subtitle || '登录后查看专属权益' }}
|
{{ subtitle }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 2. 登录 / 注册按钮组 -->
|
<!-- 已登录时提供会员入口,装修中的登录与注册文案仅用于游客。 -->
|
||||||
<div class="action-btn-row">
|
<div v-if="isLoggedIn" class="action-btn-row">
|
||||||
|
<router-link to="/home" class="action-btn login-btn">会员中心</router-link>
|
||||||
|
<router-link to="/home/MyOrder" class="action-btn register-btn">我的订单</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-else class="action-btn-row">
|
||||||
<a href="javascript:;" class="action-btn login-btn" @click="handleNavigate(options.loginUrl || '/login')">
|
<a href="javascript:;" class="action-btn login-btn" @click="handleNavigate(options.loginUrl || '/login')">
|
||||||
{{ options.loginText || '登录' }}
|
{{ options.loginText || '登录' }}
|
||||||
</a>
|
</a>
|
||||||
@@ -35,19 +39,23 @@
|
|||||||
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
|
<!-- 3. 指标数据卡 (购物车 / 优惠券 / 订单) -->
|
||||||
<div class="metrics-cards-row">
|
<div class="metrics-cards-row">
|
||||||
<a href="javascript:;" class="metric-card" v-if="options.showCart !== false" @click="handleNavigate(options.cartUrl || '/cart')">
|
<a href="javascript:;" class="metric-card" v-if="options.showCart !== false" @click="handleNavigate(options.cartUrl || '/cart')">
|
||||||
<div class="metric-val">0</div>
|
<div class="metric-val">{{ metricText('cart') }}</div>
|
||||||
<div class="metric-label">{{ options.cartText || '购物车' }}</div>
|
<div class="metric-label">{{ options.cartText || '购物车' }}</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:;" class="metric-card" v-if="options.showCoupons !== false" @click="handleNavigate(options.couponUrl || '/member/coupon')">
|
<a href="javascript:;" class="metric-card" v-if="options.showCoupons !== false" @click="handleNavigate(metricUrl('coupons'))">
|
||||||
<div class="metric-val">0</div>
|
<div class="metric-val">{{ metricText('coupons') }}</div>
|
||||||
<div class="metric-label">{{ options.couponText || '优惠券' }}</div>
|
<div class="metric-label">{{ options.couponText || '优惠券' }}</div>
|
||||||
</a>
|
</a>
|
||||||
<a href="javascript:;" class="metric-card" v-if="options.showOrders !== false" @click="handleNavigate(options.orderUrl || '/member/order')">
|
<a href="javascript:;" class="metric-card" v-if="options.showOrders !== false" @click="handleNavigate(metricUrl('orders'))">
|
||||||
<div class="metric-val">0</div>
|
<div class="metric-val">{{ metricText('orders') }}</div>
|
||||||
<div class="metric-label">{{ options.orderText || '订单' }}</div>
|
<div class="metric-label">{{ options.orderText || '订单' }}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button v-if="hasMetricError" type="button" class="metrics-retry" @click="loadMetrics">
|
||||||
|
数量加载失败,点击重试
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- 4. 快捷便民服务入口(底部图标排) -->
|
<!-- 4. 快捷便民服务入口(底部图标排) -->
|
||||||
<div class="quick-service-row">
|
<div class="quick-service-row">
|
||||||
<a
|
<a
|
||||||
@@ -65,11 +73,17 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, onActivated, onBeforeUnmount, onDeactivated, onMounted, reactive, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { Star, Clock, Present, Service } from '@element-plus/icons-vue';
|
import { Star, Clock, Present, Service } from '@element-plus/icons-vue';
|
||||||
|
import storage from '@/plugins/storage';
|
||||||
|
import { cartCount } from '@/api/cart';
|
||||||
|
import { memberCouponList } from '@/api/member';
|
||||||
|
import { getOrderList } from '@/api/order';
|
||||||
|
import { useAppStore } from '@/stores/app';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
options: {
|
options: {
|
||||||
@@ -86,6 +100,113 @@ const props = defineProps({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const userInfo = ref({});
|
||||||
|
const isLoggedIn = ref(false);
|
||||||
|
|
||||||
|
/** 读取现有登录缓存;缺少令牌时按游客展示,损坏的资料缓存不阻断首页。 */
|
||||||
|
function syncLoginState() {
|
||||||
|
isLoggedIn.value = Boolean(storage.getItem('accessToken'));
|
||||||
|
userInfo.value = {};
|
||||||
|
if (!isLoggedIn.value) return;
|
||||||
|
try {
|
||||||
|
const cached = JSON.parse(storage.getItem('userInfo') || '{}');
|
||||||
|
userInfo.value = cached && typeof cached === 'object' ? cached : {};
|
||||||
|
} catch {
|
||||||
|
userInfo.value = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
syncLoginState();
|
||||||
|
|
||||||
|
// 购物车沿用顶部购物车的有效 SKU 种数;优惠券统计未使用张数,订单统计全部订单。
|
||||||
|
const metrics = reactive({
|
||||||
|
cart: { status: 'idle', value: 0 },
|
||||||
|
coupons: { status: 'idle', value: 0 },
|
||||||
|
orders: { status: 'idle', value: 0 }
|
||||||
|
});
|
||||||
|
let metricRequestId = 0;
|
||||||
|
let metricsLoading = false;
|
||||||
|
const hasMetricError = computed(() => Object.values(metrics).some(item => item.status === 'error'));
|
||||||
|
|
||||||
|
/** 游客显示 0;已登录时区分加载、失败和真实的零值,购物车随公共状态更新。 */
|
||||||
|
function metricText(key) {
|
||||||
|
if (!isLoggedIn.value) return 0;
|
||||||
|
const metric = metrics[key];
|
||||||
|
if (metric.status === 'loading' || metric.status === 'idle') return '…';
|
||||||
|
if (metric.status === 'error') return '—';
|
||||||
|
return key === 'cart' ? appStore.cartNum : metric.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 将装修默认的历史地址映射到现有列表,保留商家配置的其他自定义链接。 */
|
||||||
|
function metricUrl(key) {
|
||||||
|
const isCoupon = key === 'coupons';
|
||||||
|
const configured = isCoupon ? props.options.couponUrl : props.options.orderUrl;
|
||||||
|
const legacy = isCoupon ? '/member/coupon' : '/member/order';
|
||||||
|
return !configured || configured === legacy
|
||||||
|
? (isCoupon ? '/home/Coupons' : '/home/MyOrder')
|
||||||
|
: configured;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 并行读取汇总;失败独立展示,离开页面后忽略旧请求,重入首页时刷新数量。 */
|
||||||
|
async function loadMetrics() {
|
||||||
|
if (metricsLoading) return;
|
||||||
|
syncLoginState();
|
||||||
|
const requestId = ++metricRequestId;
|
||||||
|
if (!isLoggedIn.value) {
|
||||||
|
Object.values(metrics).forEach(item => { item.status = 'idle'; item.value = 0; });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
metricsLoading = true;
|
||||||
|
const page = { pageNumber: 1, pageSize: 1 };
|
||||||
|
const queries = [
|
||||||
|
['cart', props.options.showCart !== false, () => cartCount()],
|
||||||
|
['coupons', props.options.showCoupons !== false, () => memberCouponList({ ...page, memberCouponStatus: 'NEW' })],
|
||||||
|
['orders', props.options.showOrders !== false, () => getOrderList({ ...page, tag: 'ALL' })]
|
||||||
|
];
|
||||||
|
await Promise.all(queries.filter(([, visible]) => visible).map(async ([key, , fetchCount]) => {
|
||||||
|
metrics[key].status = 'loading';
|
||||||
|
try {
|
||||||
|
const response = await fetchCount();
|
||||||
|
const raw = key === 'cart' ? response?.result : response?.result?.total;
|
||||||
|
const count = Number(raw);
|
||||||
|
if (!response?.success || raw == null || !Number.isSafeInteger(count) || count < 0) {
|
||||||
|
throw new Error('无效的数量响应');
|
||||||
|
}
|
||||||
|
if (requestId !== metricRequestId) return;
|
||||||
|
metrics[key].value = count;
|
||||||
|
metrics[key].status = 'ready';
|
||||||
|
if (key === 'cart') {
|
||||||
|
appStore.setCartNum(count);
|
||||||
|
storage.setItem('cartNum', count);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (requestId === metricRequestId) metrics[key].status = 'error';
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
if (requestId === metricRequestId) metricsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 使离开首页前的异步响应失效,避免覆盖下一次进入后的数据。 */
|
||||||
|
function invalidateMetrics() {
|
||||||
|
metricRequestId += 1;
|
||||||
|
metricsLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadMetrics);
|
||||||
|
onActivated(loadMetrics);
|
||||||
|
onDeactivated(invalidateMetrics);
|
||||||
|
onBeforeUnmount(invalidateMetrics);
|
||||||
|
|
||||||
|
const displayAvatar = computed(() => isLoggedIn.value
|
||||||
|
? userInfo.value.face
|
||||||
|
: props.options.avatar);
|
||||||
|
const welcomeText = computed(() => isLoggedIn.value
|
||||||
|
? `你好,${userInfo.value.nickName || userInfo.value.username || '会员'}`
|
||||||
|
: (props.options.welcomeText || '欢迎来到商城'));
|
||||||
|
const subtitle = computed(() => isLoggedIn.value
|
||||||
|
? '查看我的订单与专属权益'
|
||||||
|
: (props.options.subtitle || '登录后查看专属权益'));
|
||||||
|
|
||||||
function handleNavigate(url) {
|
function handleNavigate(url) {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
if (/^https?:\/\//i.test(url)) {
|
if (/^https?:\/\//i.test(url)) {
|
||||||
@@ -96,10 +217,10 @@ function handleNavigate(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultServices = [
|
const defaultServices = [
|
||||||
{ name: '收藏', url: '/member/favorites', icon: 'Star' },
|
{ name: '收藏', url: '/home/Favorites', icon: 'Star' },
|
||||||
{ name: '足迹', url: '/member/footprint', icon: 'Clock' },
|
{ name: '足迹', url: '/home/MyTracks', icon: 'Clock' },
|
||||||
{ name: '红包', url: '/couponCenter', icon: 'Present' },
|
{ name: '领券', url: '/coupon', icon: 'Present' },
|
||||||
{ name: '客服', url: '/article', icon: 'Service' }
|
{ name: '帮助', url: '/article', icon: 'Service' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
@@ -114,12 +235,26 @@ function getServiceIcon(iconName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const displayServices = computed(() => {
|
const displayServices = computed(() => {
|
||||||
if (Array.isArray(props.options?.services) && props.options.services.length > 0) {
|
const services = Array.isArray(props.options?.services) && props.options.services.length > 0
|
||||||
return props.options.services;
|
? props.options.services
|
||||||
}
|
: defaultServices;
|
||||||
return defaultServices;
|
return services.map(normalizeService);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** 兼容装修中保存的旧默认入口,仅修正已知地址,不覆盖其他自定义链接。 */
|
||||||
|
function normalizeService(service) {
|
||||||
|
const legacyRoutes = {
|
||||||
|
'/member/favorites': '/home/Favorites',
|
||||||
|
'/member/footprint': '/home/MyTracks',
|
||||||
|
'/couponCenter': '/coupon'
|
||||||
|
};
|
||||||
|
const url = legacyRoutes[service.url] || service.url;
|
||||||
|
let name = service.name;
|
||||||
|
if (name === '红包' && url === '/coupon') name = '领券';
|
||||||
|
if (name === '客服' && url === '/article') name = '帮助';
|
||||||
|
return { ...service, url, name };
|
||||||
|
}
|
||||||
|
|
||||||
const customStyle = computed(() => {
|
const customStyle = computed(() => {
|
||||||
const s = props.componentStyle || {};
|
const s = props.componentStyle || {};
|
||||||
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
const hasRowHeight = Boolean(props.rowStyle?.height && Number(props.rowStyle.height) > 0);
|
||||||
@@ -135,6 +270,14 @@ const customStyle = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
.metrics-retry {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
.decor-user-benefits {
|
.decor-user-benefits {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
<div class="layoutAll">
|
<div class="layoutAll">
|
||||||
<el-aside class="side-bar" width="220px">
|
<el-aside class="side-bar" width="220px">
|
||||||
<div class="article-cate">文章分类</div>
|
<div class="article-cate">文章分类</div>
|
||||||
|
<p v-if="categoryError" role="alert" class="load-message">
|
||||||
|
分类加载失败,可先浏览全部文章。
|
||||||
|
<el-button link type="primary" :loading="categoryLoading" @click="getCateList">重试分类</el-button>
|
||||||
|
</p>
|
||||||
|
<el-button link type="primary" @click="onSelect('')">全部文章</el-button>
|
||||||
<el-menu
|
<el-menu
|
||||||
class="side-menu"
|
class="side-menu"
|
||||||
:key="menuKey"
|
:key="menuKey"
|
||||||
@@ -31,14 +36,22 @@
|
|||||||
</el-menu>
|
</el-menu>
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-main class="content">
|
<el-main class="content">
|
||||||
<ul class="article-list" v-show="showList">
|
<div v-if="contentError" role="alert" class="load-message">
|
||||||
|
{{ contentError }}
|
||||||
|
<el-button link type="primary" @click="retryContent">重新加载</el-button>
|
||||||
|
</div>
|
||||||
|
<el-skeleton v-if="contentLoading" :rows="5" animated />
|
||||||
|
<ul class="article-list" v-show="showList && !contentLoading && !contentError">
|
||||||
<li v-if="!articleList.length" class="article-empty">暂无文章</li>
|
<li v-if="!articleList.length" class="article-empty">暂无文章</li>
|
||||||
<li v-for="(article, index) in articleList" :key="article.id || index" @click="getDetail(article.id)">
|
<li v-for="(article, index) in articleList" :key="article.id || index" @click="getDetail(article.id)">
|
||||||
{{ article.title }}
|
{{ article.title }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div v-show="!showList" class="article-detail">
|
<el-pagination v-if="showList && !contentLoading && !contentError && total > params.pageSize"
|
||||||
<button type="button" class="back-btn" @click="showList = true">返回列表</button>
|
:current-page="params.pageNumber" :page-size="params.pageSize" :total="total"
|
||||||
|
layout="prev, pager, next" @current-change="changePage" />
|
||||||
|
<div v-show="!showList && !contentLoading && !contentError" class="article-detail">
|
||||||
|
<button type="button" class="back-btn" @click="getList()">返回列表</button>
|
||||||
<h2>{{ detail.title }}</h2>
|
<h2>{{ detail.title }}</h2>
|
||||||
<div class="article-html" v-html="detail.content"></div>
|
<div class="article-html" v-html="detail.content"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,82 +73,111 @@ defineOptions({ name: 'ArticleIndex' })
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const list = ref([])
|
const list = ref([])
|
||||||
const detail = ref('')
|
const detail = ref({})
|
||||||
const articleList = ref([])
|
const articleList = ref([])
|
||||||
const activeName = ref('1347456734864367616')
|
const activeName = ref('')
|
||||||
const openName = ref([])
|
const openName = ref([])
|
||||||
const menuKey = ref(0)
|
const menuKey = ref(0)
|
||||||
const params = reactive({
|
const params = reactive({ pageNumber: 1, pageSize: 20, categoryId: '', sort: 'sort' })
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 100,
|
|
||||||
categoryId: '',
|
|
||||||
sort: 'sort'
|
|
||||||
})
|
|
||||||
const showList = ref(true)
|
const showList = ref(true)
|
||||||
|
const total = ref(0)
|
||||||
|
const categoryError = ref(false)
|
||||||
|
const categoryLoading = ref(false)
|
||||||
|
const contentError = ref('')
|
||||||
|
const contentLoading = ref(false)
|
||||||
|
const detailId = ref('')
|
||||||
|
let contentRequestId = 0
|
||||||
|
|
||||||
|
/** 切换分类时重置分页,并使之前的列表或正文请求失效。 */
|
||||||
function onSelect(id) {
|
function onSelect(id) {
|
||||||
getList(id)
|
activeName.value = id
|
||||||
detail.value = ''
|
params.pageNumber = 1
|
||||||
showList.value = true
|
return getList(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCateList () {
|
/** 独立加载分类;分类服务不可用时保留全部文章入口,不阻断正文和列表。 */
|
||||||
articleCateList().then(res => {
|
async function getCateList() {
|
||||||
if (res.success) {
|
if (categoryLoading.value) return
|
||||||
list.value = res.result || []
|
categoryLoading.value = true
|
||||||
openName.value = []
|
categoryError.value = false
|
||||||
if (route.query.id && detail.value && detail.value.categoryId) {
|
try {
|
||||||
activeName.value = detail.value.categoryId
|
const res = await articleCateList({ skipMessage: true })
|
||||||
list.value.forEach(e => {
|
if (!res.success || !Array.isArray(res.result)) throw new Error('分类响应无效')
|
||||||
const children = e.children || []
|
list.value = res.result
|
||||||
const hitParent = String(e.id) === String(activeName.value)
|
openName.value = list.value.filter(item => Array.isArray(item.children) && item.children.length)
|
||||||
const hitChild = children.some(i => i && String(i.id) === String(activeName.value))
|
.map(item => String(item.id))
|
||||||
if ((hitParent || hitChild) && children.length) {
|
|
||||||
openName.value.push(String(e.id))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else if (list.value[0] && list.value[0].id) {
|
|
||||||
activeName.value = list.value[0].id
|
|
||||||
if (list.value[0].children && list.value[0].children.length) {
|
|
||||||
openName.value.push(String(list.value[0].id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menuKey.value++
|
menuKey.value++
|
||||||
if (activeName.value) {
|
} catch {
|
||||||
getList(activeName.value)
|
categoryError.value = true
|
||||||
|
} finally {
|
||||||
|
categoryLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function getList (id) {
|
/** 按分类分页查询公开文章;只接收最新一次请求结果,失败不冒充空列表。 */
|
||||||
params.categoryId = id
|
async function getList(id = activeName.value) {
|
||||||
fetchArticleList(params).then(res => {
|
const requestId = ++contentRequestId
|
||||||
if (res.success) {
|
showList.value = true
|
||||||
|
contentLoading.value = true
|
||||||
|
contentError.value = ''
|
||||||
|
params.categoryId = id || ''
|
||||||
|
try {
|
||||||
|
const res = await fetchArticleList({ ...params }, { skipMessage: true })
|
||||||
|
if (!res.success || !Array.isArray(res.result?.records)) throw new Error('列表响应无效')
|
||||||
|
if (requestId !== contentRequestId) return
|
||||||
articleList.value = res.result.records
|
articleList.value = res.result.records
|
||||||
|
total.value = Number(res.result.total) || 0
|
||||||
|
} catch {
|
||||||
|
if (requestId === contentRequestId) contentError.value = '文章加载失败,请稍后重试。'
|
||||||
|
} finally {
|
||||||
|
if (requestId === contentRequestId) contentLoading.value = false
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 加载指定文章;失败可原地重试,过时的正文响应不得覆盖用户新选择。 */
|
||||||
async function getDetail(id) {
|
async function getDetail(id) {
|
||||||
await articleDetail(id).then(res => {
|
const requestId = ++contentRequestId
|
||||||
if (res.success) {
|
detailId.value = id
|
||||||
detail.value = res.result
|
|
||||||
showList.value = false
|
showList.value = false
|
||||||
|
contentLoading.value = true
|
||||||
|
contentError.value = ''
|
||||||
|
try {
|
||||||
|
const res = await articleDetail(id, { skipMessage: true })
|
||||||
|
if (!res.success || !res.result) throw new Error('文章不存在')
|
||||||
|
if (requestId !== contentRequestId) return
|
||||||
|
detail.value = res.result
|
||||||
|
} catch {
|
||||||
|
if (requestId === contentRequestId) contentError.value = '文章详情加载失败或已不存在,请稍后重试。'
|
||||||
|
} finally {
|
||||||
|
if (requestId === contentRequestId) contentLoading.value = false
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
/** 重试当前列表或正文,保留分类和分页条件。 */
|
||||||
const articleId = route.query.id
|
function retryContent() {
|
||||||
if (articleId) {
|
return showList.value ? getList() : getDetail(detailId.value)
|
||||||
await getDetail(articleId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 切换文章列表页码,沿用当前分类。 */
|
||||||
|
function changePage(page) {
|
||||||
|
params.pageNumber = page
|
||||||
|
return getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
getCateList()
|
getCateList()
|
||||||
|
if (route.query.id) getDetail(route.query.id)
|
||||||
|
else getList()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
.load-message {
|
||||||
|
padding: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px 0 40px;
|
padding: 20px 0 40px;
|
||||||
|
|||||||
@@ -279,6 +279,10 @@ export const router = createRouter({
|
|||||||
component: MemberGrade,
|
component: MemberGrade,
|
||||||
meta: { title: "我的等级" },
|
meta: { title: "我的等级" },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "UserInfo",
|
||||||
|
redirect: { name: "Profile" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "Profile",
|
path: "Profile",
|
||||||
name: "Profile",
|
name: "Profile",
|
||||||
@@ -307,6 +311,11 @@ export const router = createRouter({
|
|||||||
name: "AddAddress",
|
name: "AddAddress",
|
||||||
component: AddAddress,
|
component: AddAddress,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// 兼容已保存的装修顶部菜单中的旧消息地址,统一进入现有消息列表。
|
||||||
|
path: "Msg",
|
||||||
|
redirect: { name: "MsgList" },
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "MsgList",
|
path: "MsgList",
|
||||||
name: "MsgList",
|
name: "MsgList",
|
||||||
|
|||||||
@@ -320,10 +320,10 @@ if (props.options.couponUrl === undefined) props.options.couponUrl = '/member/co
|
|||||||
if (props.options.orderUrl === undefined) props.options.orderUrl = '/member/order';
|
if (props.options.orderUrl === undefined) props.options.orderUrl = '/member/order';
|
||||||
|
|
||||||
const defaultServices = [
|
const defaultServices = [
|
||||||
{ name: '收藏', url: '/member/favorites', icon: 'Star' },
|
{ name: '收藏', url: '/home/Favorites', icon: 'Star' },
|
||||||
{ name: '足迹', url: '/member/footprint', icon: 'Clock' },
|
{ name: '足迹', url: '/home/MyTracks', icon: 'Clock' },
|
||||||
{ name: '红包', url: '/couponCenter', icon: 'Present' },
|
{ name: '领券', url: '/coupon', icon: 'Present' },
|
||||||
{ name: '客服', url: '/article', icon: 'Service' }
|
{ name: '帮助', url: '/article', icon: 'Service' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const ensureServices = computed(() => {
|
const ensureServices = computed(() => {
|
||||||
|
|||||||
@@ -78,10 +78,10 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const defaultServices = [
|
const defaultServices = [
|
||||||
{ name: '收藏', url: '/member/favorites', icon: 'Star' },
|
{ name: '收藏', url: '/home/Favorites', icon: 'Star' },
|
||||||
{ name: '足迹', url: '/member/footprint', icon: 'Clock' },
|
{ name: '足迹', url: '/home/MyTracks', icon: 'Clock' },
|
||||||
{ name: '红包', url: '/couponCenter', icon: 'Present' },
|
{ name: '领券', url: '/coupon', icon: 'Present' },
|
||||||
{ name: '客服', url: '/article', icon: 'Service' }
|
{ name: '帮助', url: '/article', icon: 'Service' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const iconMap = {
|
const iconMap = {
|
||||||
|
|||||||
47
seller/scripts/test-seller-navigation.mjs
Normal file
47
seller/scripts/test-seller-navigation.mjs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* 商家导航回归:覆盖权限裁剪、详情归属和跨分组检索,使用 Node 内置测试运行器。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-08
|
||||||
|
*/
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { flattenNavigation, findNavigationItem, searchNavigation } from '../src/libs/seller-navigation.js';
|
||||||
|
|
||||||
|
const component = () => {};
|
||||||
|
const menus = [
|
||||||
|
{ name: 'goods', title: '商品', children: [{ title: '商品管理', children: [
|
||||||
|
{ name: 'goods', title: '商品列表', component },
|
||||||
|
{ name: 'disabled', title: '不可用页面', component: null },
|
||||||
|
] }] },
|
||||||
|
{ name: 'order', title: '订单', children: [{ title: '订单管理', children: [
|
||||||
|
{ name: 'orderList', title: '商品订单', component },
|
||||||
|
] }] },
|
||||||
|
];
|
||||||
|
const items = flattenNavigation(menus);
|
||||||
|
|
||||||
|
test('搜索不补出未授权入口,也不展示无法加载的菜单', () => {
|
||||||
|
assert.equal(searchNavigation(items, '优惠券').length, 0);
|
||||||
|
assert.equal(searchNavigation(items, '不可用页面').length, 0);
|
||||||
|
});
|
||||||
|
test('详情页刷新可定位已授权业务入口', () => {
|
||||||
|
assert.equal(findNavigationItem(items, 'goods-operation-edit')?.topName, 'goods');
|
||||||
|
assert.equal(findNavigationItem(items, 'order-detail')?.name, 'orderList');
|
||||||
|
});
|
||||||
|
test('详情页对应入口被撤权后不能沿用旧账号菜单', () => {
|
||||||
|
const restricted = flattenNavigation(menus.slice(0, 1));
|
||||||
|
assert.equal(findNavigationItem(restricted, 'order-detail'), null);
|
||||||
|
assert.equal(findNavigationItem([], 'goods-operation-edit'), null);
|
||||||
|
});
|
||||||
|
test('详情本身是授权菜单时优先使用精确入口', () => {
|
||||||
|
const exact = { name: 'order-detail', topName: 'custom' };
|
||||||
|
assert.equal(findNavigationItem([...items, exact], 'order-detail'), exact);
|
||||||
|
});
|
||||||
|
test('关键词同时匹配栏目及名称,避免同名商品功能混淆', () => {
|
||||||
|
assert.deepEqual(searchNavigation(items, ' 订单 商品 ').map(item => item.name), ['orderList']);
|
||||||
|
assert.deepEqual(searchNavigation(items, '商品 列表').map(item => item.name), ['goods']);
|
||||||
|
});
|
||||||
|
test('搜索清空与空权限账号正确处理', () => {
|
||||||
|
assert.equal(searchNavigation(items, ' ').length, 2);
|
||||||
|
assert.deepEqual(searchNavigation(flattenNavigation([]), ''), []);
|
||||||
|
assert.equal(findNavigationItem(items, 'unknown-detail'), null);
|
||||||
|
});
|
||||||
@@ -87,6 +87,11 @@ export const getShopInfo = () => {
|
|||||||
return getRequest(`/settings/storeSettings`)
|
return getRequest(`/settings/storeSettings`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取商家入驻详细信息
|
||||||
|
export const getShopEntryDetail = () => {
|
||||||
|
return getRequest(`/settings/storeSettings/storeDetail`)
|
||||||
|
}
|
||||||
|
|
||||||
// 保存商家详细信息
|
// 保存商家详细信息
|
||||||
export const saveShopInfo = (params) => {
|
export const saveShopInfo = (params) => {
|
||||||
return putRequest(`/settings/storeSettings`, params)
|
return putRequest(`/settings/storeSettings`, params)
|
||||||
@@ -127,5 +132,3 @@ export const editShipTemplate = (id, params, headers) => {
|
|||||||
export const editChecked = (logisticsId,params) => {
|
export const editChecked = (logisticsId,params) => {
|
||||||
return putRequest(`/other/logistics/${logisticsId}/updateStoreLogistics`,params)
|
return putRequest(`/other/logistics/${logisticsId}/updateStoreLogistics`,params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,6 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
mainColor: "#ff5c58", //主题色
|
mainColor: "#ff5c58", //主题色
|
||||||
setting: {
|
setting: {
|
||||||
isUseTabsRouter: true, //多标签Tab页模式
|
isUseTabsRouter: false, // 商家后台固定关闭多标签页内嵌模式,详情使用新页面展示。
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"setting": {
|
"setting": {
|
||||||
"isUseTabsRouter": true,
|
"isUseTabsRouter": false,
|
||||||
"showFooter": false
|
"showFooter": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,6 @@
|
|||||||
export default (url) =>()=>import(`@/views/${url}.vue`)
|
const explicitViews = {
|
||||||
|
"shop/shopInfo": () => import("@/views/shop/shopInfo.vue"),
|
||||||
|
"shop/addressManage": () => import("@/views/shop/addressManage.vue"),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (url) => explicitViews[url] || (() => import(`@/views/${url}.vue`));
|
||||||
|
|||||||
@@ -489,6 +489,24 @@ export const result = [{
|
|||||||
component: "shop/shopSetting",
|
component: "shop/shopSetting",
|
||||||
children: null,
|
children: null,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "shopInfo",
|
||||||
|
level: 2,
|
||||||
|
type: 0,
|
||||||
|
title: "店铺信息",
|
||||||
|
path: "shopInfo",
|
||||||
|
component: "shop/shopInfo",
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "addressManage",
|
||||||
|
level: 2,
|
||||||
|
type: 0,
|
||||||
|
title: "地址管理",
|
||||||
|
path: "addressManage",
|
||||||
|
component: "shop/addressManage",
|
||||||
|
children: null,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "wapList",
|
name: "wapList",
|
||||||
level: 2,
|
level: 2,
|
||||||
|
|||||||
55
seller/src/libs/seller-navigation.js
Normal file
55
seller/src/libs/seller-navigation.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 商家导航的权限菜单检索及详情页归属;仅在已授权菜单中定位,不增加访问权限。
|
||||||
|
* @author Bulbasaur
|
||||||
|
* @since 2026-11-08
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 非菜单页面回到对应业务入口;多个候选项按实际已授权入口匹配。
|
||||||
|
const DETAIL_MENUS = {
|
||||||
|
"goods-operation-edit": ["goods"],
|
||||||
|
"goods-template-operation-edit": ["template-goods"],
|
||||||
|
"goods-draft-operation-edit": ["draftGoods", "draft-goods", "goods"],
|
||||||
|
"card-key-pool": ["goods"],
|
||||||
|
"add-coupon": ["coupon"],
|
||||||
|
"coupon-receive": ["coupon"],
|
||||||
|
"add-live": ["live"],
|
||||||
|
"bill-detail": ["storeBill", "accountStatementBill"],
|
||||||
|
"order-detail": ["orderList", "virtualOrderList"],
|
||||||
|
"export-order-deliver": ["orderList"],
|
||||||
|
"order-complaint-detail": ["orderComplaint"],
|
||||||
|
"return-goods-order-detail": ["returnGoodsOrder", "returnMoneyOrder"],
|
||||||
|
"full-discount-detail": ["full-discount", "full-cut"],
|
||||||
|
"seckill-goods": ["seckill"],
|
||||||
|
"pintuan-goods": ["pintuan"],
|
||||||
|
"pintuan-edit": ["pintuan"],
|
||||||
|
main: ["wapList"],
|
||||||
|
renovation: ["pcList"],
|
||||||
|
"appointment-system-form-designer": ["appointment-system-forms"],
|
||||||
|
"procurement-purchase-order-operation": ["procurementPurchaseOrderList"],
|
||||||
|
"procurement-inbound-operation": ["procurementInboundList"],
|
||||||
|
"procurement-inventory-count-detail": ["procurementInventoryCountList"],
|
||||||
|
"procurement-damage-report-operation": ["procurementDamageReportList"],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 将权限树展开为带栏目与分组信息的可跳转菜单,用于搜索和路由定位。 */
|
||||||
|
export function flattenNavigation(menus) {
|
||||||
|
return menus.flatMap((top) => (top.children || []).flatMap((group) =>
|
||||||
|
(group.children || []).filter((leaf) => leaf.name && typeof leaf.component === "function")
|
||||||
|
.map((leaf) => ({ ...leaf, topName: top.name, topTitle: top.title, groupTitle: group.title }))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 优先精确匹配菜单,再匹配详情页业务入口;无授权入口时返回空,不沿用旧账号栏目。 */
|
||||||
|
export function findNavigationItem(items, routeName) {
|
||||||
|
return items.find((item) => item.name === routeName) ||
|
||||||
|
(DETAIL_MENUS[routeName] || []).map((name) => items.find((item) => item.name === name)).find(Boolean) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按菜单、分组和栏目检索,空格分隔的关键词须全部匹配。 */
|
||||||
|
export function searchNavigation(items, keyword) {
|
||||||
|
const terms = keyword.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
||||||
|
return items.filter((item) => {
|
||||||
|
const text = `${item.topTitle} ${item.groupTitle} ${item.title}`.toLowerCase();
|
||||||
|
return terms.every((term) => text.includes(term));
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -450,47 +450,29 @@ util.initAllMenuData = function (constRoutes, data) {
|
|||||||
|
|
||||||
util.initMenuData = function (data) {
|
util.initMenuData = function (data) {
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const menuRoutes = [];
|
// 全量导航与搜索只使用当前账号权限接口返回的菜单,沿用已下线菜单过滤规则。
|
||||||
let menuData = data;
|
appStore.navigationMenus = data.map((top) => {
|
||||||
|
const children = [];
|
||||||
|
util.initRouterNode(children, top.children || []);
|
||||||
|
return { ...top, children: children.filter((group) => group.children?.length) };
|
||||||
|
}).filter((top) => top.children.length);
|
||||||
let navList = [HOME_NAV_ITEM];
|
let navList = [HOME_NAV_ITEM];
|
||||||
menuData.forEach((e) => {
|
appStore.navigationMenus.forEach((e) => {
|
||||||
navList.push({ name: e.name, title: e.title });
|
navList.push({ name: e.name, title: e.title });
|
||||||
});
|
});
|
||||||
if (navList.length < 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
appStore.setNavList(navList);
|
appStore.setNavList(navList);
|
||||||
let currNav = window.localStorage.getItem("currNav");
|
let currNav = window.localStorage.getItem("currNav");
|
||||||
if (!currNav) {
|
if (!navList.some((item) => item.name === currNav)) {
|
||||||
currNav = HOME_NAV_NAME;
|
currNav = HOME_NAV_NAME;
|
||||||
}
|
}
|
||||||
appStore.setCurrNav(currNav);
|
appStore.setCurrNav(currNav);
|
||||||
|
|
||||||
if (currNav === HOME_NAV_NAME) {
|
// 首页也必须准备全部标签入口;切换栏目只更新展示,不能依赖再次初始化路由补齐标签。
|
||||||
appStore.updateMenulist([]);
|
appStore.tagsList = appStore.routers.flatMap((item) => item.children || []);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let item of menuData) {
|
|
||||||
if (item.name == currNav) {
|
|
||||||
menuData = item.children;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
util.initRouterNode(menuRoutes, menuData);
|
|
||||||
appStore.updateMenulist(
|
appStore.updateMenulist(
|
||||||
menuRoutes.filter((item) => item.children.length > 0)
|
appStore.navigationMenus.find((item) => item.name === currNav)?.children || []
|
||||||
);
|
);
|
||||||
|
|
||||||
let tagsList = [];
|
|
||||||
appStore.routers.map((item) => {
|
|
||||||
if (item.children.length <= 1) {
|
|
||||||
tagsList.push(item.children[0]);
|
|
||||||
} else {
|
|
||||||
tagsList.push(...item.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
appStore.setTagsList(tagsList);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
util.initRouterNode = function (routers, data) {
|
util.initRouterNode = function (routers, data) {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const useAppStore = defineStore("app", {
|
|||||||
loading: false,
|
loading: false,
|
||||||
added: false,
|
added: false,
|
||||||
navList: [],
|
navList: [],
|
||||||
|
navigationMenus: [],
|
||||||
currNav: "",
|
currNav: "",
|
||||||
lang: "",
|
lang: "",
|
||||||
openedSubmenuArr: [],
|
openedSubmenuArr: [],
|
||||||
@@ -26,8 +27,18 @@ export const useAppStore = defineStore("app", {
|
|||||||
routers: [otherRouter],
|
routers: [otherRouter],
|
||||||
tagsList: [...otherRouter.children],
|
tagsList: [...otherRouter.children],
|
||||||
refMenu: "",
|
refMenu: "",
|
||||||
|
unreadMessageCount: 0,
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
/** 更新顶部未读消息数量。 */
|
||||||
|
setUnreadMessageCount(count) {
|
||||||
|
this.unreadMessageCount = Math.max(0, Number(count) || 0);
|
||||||
|
},
|
||||||
|
/** 清理账号绑定的路由与标签目录,保留静态页面,防止切换账号后复用旧菜单。 */
|
||||||
|
resetNavigationRoutes() {
|
||||||
|
this.routers = [otherRouter];
|
||||||
|
this.tagsList = [...otherRouter.children];
|
||||||
|
},
|
||||||
childrenMenu(v) {
|
childrenMenu(v) {
|
||||||
this.refMenu = v;
|
this.refMenu = v;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ const localSetting = window.localStorage.getItem("setting");
|
|||||||
|
|
||||||
export const useSettingStore = defineStore("setting", {
|
export const useSettingStore = defineStore("setting", {
|
||||||
state: () => ({
|
state: () => ({
|
||||||
setting: localSetting ? JSON.parse(localSetting) : setting,
|
// 兼容已保存的其他偏好,旧浏览器中的多标签开启状态统一归为关闭。
|
||||||
|
setting: { ...setting, ...(localSetting ? JSON.parse(localSetting) : {}), isUseTabsRouter: false },
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
/** 更新其他页面偏好时,仍保持商家多标签模式关闭。 */
|
||||||
updateSetting(data) {
|
updateSetting(data) {
|
||||||
this.setting = data;
|
this.setting = { ...data, isUseTabsRouter: false };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { removeStore } from "@/libs/storage";
|
import { removeStore } from "@/libs/storage";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
// 退出登录时需要清除的会话/店铺数据
|
// 退出登录时需要清除的会话/店铺数据
|
||||||
// 保留 uuid(设备标识)、setting(主题)、currNav、category(缓存)、saveLogin(记住登录状态)等无关键
|
// 保留 uuid(设备标识)、setting(主题)、category(缓存)、saveLogin(记住登录状态)等无关键
|
||||||
const LOGOUT_KEYS = [
|
const LOGOUT_KEYS = [
|
||||||
"accessToken",
|
"accessToken",
|
||||||
"refreshToken",
|
"refreshToken",
|
||||||
"menuData",
|
"menuData",
|
||||||
|
"currNav",
|
||||||
|
"storeOpenedList",
|
||||||
"seller_expiration_time",
|
"seller_expiration_time",
|
||||||
"sellersiteName",
|
"sellersiteName",
|
||||||
"sellerlogoImg",
|
"sellerlogoImg",
|
||||||
@@ -20,6 +23,16 @@ export const useUserStore = defineStore("user", {
|
|||||||
Cookies.remove("userInfoSeller");
|
Cookies.remove("userInfoSeller");
|
||||||
// 精确清除会话相关键,不再 localStorage.clear() 误删无关数据
|
// 精确清除会话相关键,不再 localStorage.clear() 误删无关数据
|
||||||
LOGOUT_KEYS.forEach((key) => removeStore(key));
|
LOGOUT_KEYS.forEach((key) => removeStore(key));
|
||||||
|
// 导航和已打开标签属于当前账号,退出时同步清理内存,避免下一账号复用旧权限菜单。
|
||||||
|
const appStore = useAppStore();
|
||||||
|
appStore.navigationMenus = [];
|
||||||
|
appStore.setNavList([]);
|
||||||
|
appStore.updateMenulist([]);
|
||||||
|
appStore.setCurrNav("");
|
||||||
|
appStore.setAdded(false);
|
||||||
|
appStore.setUnreadMessageCount(0);
|
||||||
|
appStore.resetNavigationRoutes();
|
||||||
|
appStore.storeOpenedList = [{ title: "首页", path: "", name: "home_index" }];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -51,22 +51,13 @@ export function enCode(v1) {
|
|||||||
|
|
||||||
import {router} from "@/router/index";
|
import {router} from "@/router/index";
|
||||||
/**
|
/**
|
||||||
* 自定义跳转
|
* 打开详情页面;商家后台固定关闭多标签页内嵌模式,不再读取旧浏览器偏好。
|
||||||
|
* @param push Vue Router 路由位置
|
||||||
*/
|
*/
|
||||||
export function customRouterPush(push){
|
export function customRouterPush(push){
|
||||||
const setting = window.localStorage.getItem('setting') ? JSON.parse(window.localStorage.getItem('setting')) : {};
|
const url = router.resolve(push);
|
||||||
|
|
||||||
if(setting.isUseTabsRouter){
|
|
||||||
router.push(push)
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
if(Object.keys(setting).length == 0){router.push(push)}
|
|
||||||
else{
|
|
||||||
let url = router.resolve(push);
|
|
||||||
window.open(url.href, '_blank');
|
window.open(url.href, '_blank');
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,17 +3,16 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="main" :class="{ 'menu-collapsed': !showSubMenu }">
|
<div class="main">
|
||||||
<div class="sidebar-menu-con menu-bar">
|
<div class="sidebar-menu-con menu-bar">
|
||||||
<div class="logo-con">
|
|
||||||
<img alt="店铺 Logo" :src="storeSideLogo" key="max-logo" />
|
|
||||||
</div>
|
|
||||||
<shrinkable-menu />
|
<shrinkable-menu />
|
||||||
</div>
|
</div>
|
||||||
<div class="main-header-con" :style="{ height: setting.isUseTabsRouter ? '106px' : '60px' }">
|
<div class="main-header-con">
|
||||||
|
<div class="logo-con">
|
||||||
|
<img alt="平台 Logo" :src="storeSideLogo" key="max-logo" @error="onLogoError" />
|
||||||
|
</div>
|
||||||
<div class="main-header">
|
<div class="main-header">
|
||||||
<div class="header-avator-con">
|
<div class="header-avator-con">
|
||||||
<div></div>
|
|
||||||
<div class="user-dropdown-menu-con">
|
<div class="user-dropdown-menu-con">
|
||||||
<el-row
|
<el-row
|
||||||
justify="end"
|
justify="end"
|
||||||
@@ -22,19 +21,36 @@
|
|||||||
>
|
>
|
||||||
<store-qrcode />
|
<store-qrcode />
|
||||||
<ul class="nav-list">
|
<ul class="nav-list">
|
||||||
<li class="nav-item" @click="im">
|
<li class="nav-item">
|
||||||
|
<el-tooltip
|
||||||
|
:content="unreadMessageCount > 0 ? `有 ${unreadMessageCount} 条未读消息` : '暂无未读消息'"
|
||||||
|
placement="bottom"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="header-icon-button header-message-btn"
|
||||||
|
:aria-label="unreadMessageCount > 0 ? `有 ${unreadMessageCount} 条未读消息` : '消息中心'"
|
||||||
|
@click="openMessagePage"
|
||||||
|
>
|
||||||
|
<el-badge
|
||||||
|
:value="unreadMessageCount"
|
||||||
|
:max="99"
|
||||||
|
:hidden="unreadMessageCount <= 0"
|
||||||
|
class="header-message-badge"
|
||||||
|
>
|
||||||
|
<el-icon :size="18"><Bell /></el-icon>
|
||||||
|
</el-badge>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
<el-tooltip content="联系客服" placement="bottom">
|
<el-tooltip content="联系客服" placement="bottom">
|
||||||
<el-button type="warning" size="small" :loading="load">
|
<el-button type="warning" size="small" :loading="load" @click="im">
|
||||||
<el-icon><ChatDotRound /></el-icon>
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
客服
|
客服
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" @click="handleClickSetting">
|
|
||||||
<el-tooltip content="设置" placement="bottom">
|
|
||||||
<el-icon :size="16"><Setting /></el-icon>
|
|
||||||
</el-tooltip>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
<el-dropdown trigger="hover" @command="handleClickUserDropdown">
|
<el-dropdown trigger="hover" @command="handleClickUserDropdown">
|
||||||
<div class="dropList">
|
<div class="dropList">
|
||||||
@@ -45,6 +61,7 @@
|
|||||||
>
|
>
|
||||||
<el-icon><UserFilled /></el-icon>
|
<el-icon><UserFilled /></el-icon>
|
||||||
</el-avatar>
|
</el-avatar>
|
||||||
|
<span class="header-store-name">{{ userInfo.storeName || '我的店铺' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
@@ -57,17 +74,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tags-con" v-if="setting.isUseTabsRouter">
|
</div>
|
||||||
<tags-page-opened :pageTagsList="pageTagsList" />
|
<div class="single-page-con">
|
||||||
|
<div class="page-path-bar">
|
||||||
|
<div class="header-page-info">
|
||||||
|
<template v-for="(part, index) in currentPathParts" :key="index">
|
||||||
|
<span v-if="index > 0" class="header-divider">/</span>
|
||||||
|
<strong v-if="index === currentPathParts.length - 1">{{ part }}</strong>
|
||||||
|
<span v-else class="workspace-label">{{ part }}</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
class="single-page-con"
|
|
||||||
:style="{
|
|
||||||
top: setting.isUseTabsRouter ? '106px' : '60px',
|
|
||||||
height: setting.isUseTabsRouter ? 'calc(100% - 116px)' : 'calc(100% - 70px)',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<div class="single-page">
|
<div class="single-page">
|
||||||
<router-view v-slot="{ Component }">
|
<router-view v-slot="{ Component }">
|
||||||
<component :is="Component" v-if="Component" :key="$route.fullPath" />
|
<component :is="Component" v-if="Component" :key="$route.fullPath" />
|
||||||
@@ -75,51 +92,77 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<circleLoading class="loading-position" v-show="loading" />
|
<circleLoading class="loading-position" v-show="loading" />
|
||||||
<configDrawer ref="configRef" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { ChatDotRound, Setting, UserFilled } from "@element-plus/icons-vue";
|
import { Bell, ChatDotRound, UserFilled } from "@element-plus/icons-vue";
|
||||||
import shrinkableMenu from "./main-components/shrinkable-menu/shrinkable-menu.vue";
|
import shrinkableMenu from "./main-components/shrinkable-menu/shrinkable-menu.vue";
|
||||||
import tagsPageOpened from "./main-components/tags-page-opened.vue";
|
|
||||||
import circleLoading from "@/views/my-components/lili/circle-loading.vue";
|
import circleLoading from "@/views/my-components/lili/circle-loading.vue";
|
||||||
import configDrawer from "@/views/main-components/config-drawer.vue";
|
|
||||||
import storeQrcode from "@/views/main-components/store-qrcode.vue";
|
import storeQrcode from "@/views/main-components/store-qrcode.vue";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import util from "@/libs/util.js";
|
import util from "@/libs/util.js";
|
||||||
import { logout } from "@/api/index";
|
import { flattenNavigation, findNavigationItem } from "@/libs/seller-navigation";
|
||||||
|
import defaultLogo from "@/assets/logo.png";
|
||||||
|
import { loadSiteSettings } from "@/composables/useSiteSettings";
|
||||||
|
import { getMessageSendData, logout, userMsg } from "@/api/index";
|
||||||
import { getIMDetail } from "@/api/common";
|
import { getIMDetail } from "@/api/common";
|
||||||
import { userMsg } from "@/api/index";
|
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
import { getStore, setStore } from "@/libs/storage";
|
import { getStore, setStore } from "@/libs/storage";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import { useSettingStore } from "@/stores/setting";
|
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
const configRef = ref(null);
|
|
||||||
const sliceNum = ref(5);
|
const sliceNum = ref(5);
|
||||||
const userInfo = ref({});
|
const userInfo = ref({});
|
||||||
const storeSideLogo = ref("");
|
const storeSideLogo = ref(defaultLogo);
|
||||||
const IMLink = ref("");
|
const IMLink = ref("");
|
||||||
const load = ref(false);
|
const load = ref(false);
|
||||||
|
|
||||||
const setting = computed(() => settingStore.setting);
|
|
||||||
const loading = computed(() => appStore.loading);
|
const loading = computed(() => appStore.loading);
|
||||||
|
const unreadMessageCount = computed(() => appStore.unreadMessageCount);
|
||||||
const pageTagsList = computed(() => appStore.storeOpenedList);
|
const pageTagsList = computed(() => appStore.storeOpenedList);
|
||||||
const showSubMenu = computed(() => appStore.menuList.length > 0);
|
const currentPageTitle = computed(() => route.meta.title || pageTagsList.value.find((item) => item.name === route.name)?.title || '工作台');
|
||||||
|
const navigationItems = computed(() => flattenNavigation(appStore.navigationMenus || []));
|
||||||
|
const currentPathParts = computed(() => {
|
||||||
|
const matched = findNavigationItem(navigationItems.value, route.name);
|
||||||
|
const rawParts = matched
|
||||||
|
? [
|
||||||
|
matched.topTitle,
|
||||||
|
matched.groupTitle,
|
||||||
|
matched.title,
|
||||||
|
route.name !== matched.name ? currentPageTitle.value : "",
|
||||||
|
]
|
||||||
|
: ["商家后台", currentPageTitle.value];
|
||||||
|
return rawParts.filter((part, idx, arr) => part && part !== arr[idx - 1]);
|
||||||
|
});
|
||||||
const lang = computed(() => appStore.lang);
|
const lang = computed(() => appStore.lang);
|
||||||
|
|
||||||
function handleClickSetting() {
|
/**
|
||||||
configRef.value.open();
|
* 跳转至系统消息页面。
|
||||||
|
*/
|
||||||
|
function openMessagePage() {
|
||||||
|
util.openNewPage("message_index");
|
||||||
|
router.push({ name: "message_index" });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取当前店铺未读消息总数,更新右上角消息角标。
|
||||||
|
*/
|
||||||
|
function loadUnreadMessageCount() {
|
||||||
|
getMessageSendData({ status: "UN_READY", pageNumber: 1, pageSize: 1 })
|
||||||
|
.then((res) => {
|
||||||
|
if (res && res.success && res.result) {
|
||||||
|
appStore.setUnreadMessageCount(res.result.total);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function im() {
|
async function im() {
|
||||||
@@ -142,23 +185,67 @@ async function getIMDetailMethods() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新页面站点图标。
|
||||||
|
* @param {string} href 图标地址
|
||||||
|
*/
|
||||||
|
function applyFavicon(href) {
|
||||||
|
if (!href || href === "undefined" || href === "null") return;
|
||||||
|
const link =
|
||||||
|
document.querySelector("link[rel*='icon']") ||
|
||||||
|
document.createElement("link");
|
||||||
|
link.type = "image/x-icon";
|
||||||
|
link.href = href;
|
||||||
|
link.rel = "shortcut icon";
|
||||||
|
document.getElementsByTagName("head")[0].appendChild(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台 Logo 加载失败时回退为默认 Logo。
|
||||||
|
* @param {Event} e 图片加载错误事件
|
||||||
|
*/
|
||||||
|
function onLogoError(e) {
|
||||||
|
if (e?.target && e.target.src !== defaultLogo) {
|
||||||
|
e.target.src = defaultLogo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
const pathArr = util.setCurrentPath(route.name);
|
const pathArr = util.setCurrentPath(route.name);
|
||||||
if (pathArr.length >= 2) {
|
if (pathArr.length >= 2) {
|
||||||
appStore.addOpenSubmenu(pathArr[1].name);
|
appStore.addOpenSubmenu(pathArr[1].name);
|
||||||
}
|
}
|
||||||
storeSideLogo.value = localStorage.getItem("sellerlogoImg");
|
const cachedLogo = localStorage.getItem("sellerlogoImg");
|
||||||
window.document.title = localStorage.getItem("sellersiteName");
|
storeSideLogo.value =
|
||||||
const link =
|
cachedLogo && cachedLogo !== "undefined" && cachedLogo !== "null"
|
||||||
document.querySelector("link[rel*='icon']") ||
|
? cachedLogo
|
||||||
document.createElement("link");
|
: defaultLogo;
|
||||||
link.type = "image/x-icon";
|
util.title(currentPageTitle.value);
|
||||||
link.href = localStorage.getItem("sellerIconImg");
|
applyFavicon(localStorage.getItem("sellerIconImg"));
|
||||||
link.rel = "shortcut icon";
|
|
||||||
document.getElementsByTagName("head")[0].appendChild(link);
|
loadSiteSettings()
|
||||||
|
.then((data) => {
|
||||||
|
if (data) {
|
||||||
|
const logo = data.storeSideLogo || data.domainLogo || "";
|
||||||
|
const icon = data.storeSideIcon || data.domainIcon || "";
|
||||||
|
if (logo) {
|
||||||
|
localStorage.setItem("sellerlogoImg", logo);
|
||||||
|
storeSideLogo.value = logo;
|
||||||
|
}
|
||||||
|
if (icon) {
|
||||||
|
localStorage.setItem("sellerIconImg", icon);
|
||||||
|
applyFavicon(icon);
|
||||||
|
}
|
||||||
|
if (data.siteName) {
|
||||||
|
localStorage.setItem("sellersiteName", data.siteName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {});
|
||||||
|
|
||||||
userInfo.value = JSON.parse(Cookies.get("userInfoSeller"));
|
userInfo.value = JSON.parse(Cookies.get("userInfoSeller"));
|
||||||
checkTag(route.name);
|
checkTag(route.name);
|
||||||
|
loadUnreadMessageCount();
|
||||||
|
|
||||||
const currWidth = document.body.clientWidth;
|
const currWidth = document.body.clientWidth;
|
||||||
if (currWidth <= 1200) {
|
if (currWidth <= 1200) {
|
||||||
@@ -203,6 +290,8 @@ watch(
|
|||||||
appStore.addOpenSubmenu(pathArr[1].name);
|
appStore.addOpenSubmenu(pathArr[1].name);
|
||||||
}
|
}
|
||||||
checkTag(name);
|
checkTag(name);
|
||||||
|
util.title(currentPageTitle.value);
|
||||||
|
loadUnreadMessageCount();
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -210,6 +299,12 @@ watch(lang, () => {
|
|||||||
util.setCurrentPath(route.name);
|
util.setCurrentPath(route.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 刷新时权限菜单晚于布局挂载;目录到达后补齐当前标签和标题,无需再切换一次栏目。
|
||||||
|
watch(() => appStore.tagsList, () => {
|
||||||
|
checkTag(route.name);
|
||||||
|
util.title(currentPageTitle.value);
|
||||||
|
});
|
||||||
|
|
||||||
appStore.setOpenedList();
|
appStore.setOpenedList();
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
|||||||
@@ -150,9 +150,9 @@
|
|||||||
row-key="id"
|
row-key="id"
|
||||||
@selection-change="onSelectionChange"
|
@selection-change="onSelectionChange"
|
||||||
>
|
>
|
||||||
<el-table-column v-if="!scheduledOnly" type="selection" width="100" align="center" />
|
<el-table-column v-if="!scheduledOnly" type="selection" width="48" align="center" />
|
||||||
<el-table-column prop="id" label="SPU编码" width="200" show-overflow-tooltip />
|
<el-table-column prop="id" label="SPU编码" width="180" show-overflow-tooltip />
|
||||||
<el-table-column label="商品图片" width="100" align="center">
|
<el-table-column label="商品图片" width="80" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<img alt="商品图片"
|
<img alt="商品图片"
|
||||||
v-if="row && row.original"
|
v-if="row && row.original"
|
||||||
@@ -172,60 +172,57 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="currentStatus === 'SCHEDULED_UPPER'" prop="scheduledUpperTime" label="计划上架时间" width="190" />
|
<el-table-column v-if="currentStatus === 'SCHEDULED_UPPER'" prop="scheduledUpperTime" label="计划上架时间" width="190" />
|
||||||
<el-table-column v-if="currentStatus === 'SCHEDULED_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
|
<el-table-column v-if="currentStatus === 'SCHEDULED_DOWN'" prop="scheduledDownTime" label="计划下架时间" width="190" />
|
||||||
<el-table-column label="价格" width="120">
|
<el-table-column label="价格" width="110">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span>
|
<span :style="{ color: $mainColor }">{{ $filters.unitPrice(row.price, '¥') }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column prop="buyCount" label="销量" width="90" />
|
<el-table-column prop="buyCount" label="销量" width="70" />
|
||||||
<el-table-column prop="quantity" label="库存" width="90" />
|
<el-table-column prop="quantity" label="库存" width="70" />
|
||||||
<el-table-column label="商品类型" width="150">
|
<el-table-column label="商品类型" width="100">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
|
<span v-if="row">{{ goodsTypeText(row.goodsType) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="商品分类" min-width="200" show-overflow-tooltip>
|
<el-table-column label="商品分类" min-width="240" class-name="goods-category-column">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
{{ platformCategoryText(row?.categoryPath) }}
|
{{ platformCategoryText(row?.categoryPath) }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="!scheduledOnly" label="状态" width="150">
|
<el-table-column v-if="!scheduledOnly" label="状态" width="80">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
<span v-if="row">{{ marketEnableText(row.marketEnable) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="审核状态" width="150">
|
<el-table-column label="审核状态" width="90">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span v-if="row">{{ authFlagText(row.authFlag) }}</span>
|
<span v-if="row">{{ authFlagText(row.authFlag) }}</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column v-if="!scheduledOnly" prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
|
<el-table-column v-if="!scheduledOnly" prop="createTime" label="创建时间" width="170" show-overflow-tooltip />
|
||||||
<el-table-column label="操作" :width="scheduledOnly ? 150 : 320" align="right" fixed="right">
|
<el-table-column label="操作" :width="scheduledOnly ? 100 : 190" align="right" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
|
<div class="goods-actions">
|
||||||
<template v-if="scheduledOnly">
|
<template v-if="scheduledOnly">
|
||||||
<a class="link-text" @click="emit('edit-schedule', row)">编辑</a>
|
<a class="link-text" @click="emit('edit-schedule', row)">编辑</a>
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
|
<a class="link-text" @click="emit('delete-schedule', row)">删除</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<template v-if="row.marketEnable === 'DOWN'">
|
<template v-if="row.marketEnable === 'DOWN'">
|
||||||
<a class="link-text" @click="upper(row)">上架</a>
|
<a class="link-text" @click="upper(row)">上架</a>
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="editGoods(row)">编辑</a>
|
<a class="link-text" @click="editGoods(row)">编辑</a>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<a class="link-text" @click="edit(row)">下架</a>
|
<a class="link-text" @click="edit(row)">下架</a>
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="editGoods(row)">编辑</a>
|
<a class="link-text" @click="editGoods(row)">编辑</a>
|
||||||
</template>
|
</template>
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="copyGoods(row)">复制商品</a>
|
<a class="link-text" @click="copyGoods(row)">复制商品</a>
|
||||||
<!-- E_COUPON 专属:卡池管理入口(原型 P-03) -->
|
<!-- E_COUPON 专属:卡池管理入口(原型 P-03) -->
|
||||||
<template v-if="row.goodsType === 'E_COUPON'">
|
<template v-if="row.goodsType === 'E_COUPON'">
|
||||||
<span class="op-split">|</span>
|
|
||||||
<a class="link-text" @click="goCardKeyPool(row)">卡池管理</a>
|
<a class="link-text" @click="goCardKeyPool(row)">卡池管理</a>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
@@ -753,6 +750,19 @@ onMounted(() => {
|
|||||||
white-space: normal;
|
white-space: normal;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
// 分类完整换行,不使用省略号;操作按钮保持可读,额外入口允许换行。
|
||||||
|
:deep(.goods-category-column .cell) {
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.goods-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 12px;
|
||||||
|
.link-text { white-space: nowrap; }
|
||||||
|
}
|
||||||
.link-text {
|
.link-text {
|
||||||
color: #409eff;
|
color: #409eff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -1,67 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<el-drawer v-model="drawer" title="页面配置" size="300px">
|
|
||||||
<h3>内容设置</h3>
|
|
||||||
<div class="config-item flex flex-a-c flex-j-sb">
|
|
||||||
<div>
|
|
||||||
<el-tooltip
|
|
||||||
placement="bottom-end"
|
|
||||||
content="关闭之后部分页面点击'查看''详情'等按钮将跳到新页面展示"
|
|
||||||
>
|
|
||||||
<div>多标签Tab页内嵌模式</div>
|
|
||||||
</el-tooltip>
|
|
||||||
</div>
|
|
||||||
<el-switch v-model="setting.isUseTabsRouter" />
|
|
||||||
</div>
|
|
||||||
</el-drawer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, ref, watch } from "vue";
|
|
||||||
import { useSettingStore } from "@/stores/setting";
|
|
||||||
import { setStore } from "@/libs/storage";
|
|
||||||
|
|
||||||
defineOptions({ name: "configDrawer" });
|
|
||||||
|
|
||||||
const settingStore = useSettingStore();
|
|
||||||
|
|
||||||
const drawer = ref(false);
|
|
||||||
|
|
||||||
const setting = computed(() => settingStore.setting);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
setting,
|
|
||||||
(val) => {
|
|
||||||
setStore("setting", val);
|
|
||||||
settingStore.updateSetting(val);
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
function open() {
|
|
||||||
drawer.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function close() {
|
|
||||||
drawer.value = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
defineExpose({ open, close });
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
* {
|
|
||||||
color: #333 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin: 10px 0 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-item {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<el-row class="header">
|
<el-row class="header">
|
||||||
<img alt="店铺 Logo" class="logo" :src="storeSideLogo" />
|
<img alt="平台 Logo" class="logo" :src="storeSideLogo" />
|
||||||
</el-row>
|
</el-row>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -16,7 +16,7 @@ function init() {
|
|||||||
getSite();
|
getSite();
|
||||||
}
|
}
|
||||||
function applyFavicon(href) {
|
function applyFavicon(href) {
|
||||||
if (!href) return;
|
if (!href || href === "undefined" || href === "null") return;
|
||||||
const link =
|
const link =
|
||||||
document.querySelector("link[rel*='icon']") ||
|
document.querySelector("link[rel*='icon']") ||
|
||||||
document.createElement("link");
|
document.createElement("link");
|
||||||
@@ -29,16 +29,18 @@ function getSite() {
|
|||||||
loadSiteSettings()
|
loadSiteSettings()
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data) {
|
if (data) {
|
||||||
|
const logo = data.storeSideLogo || data.domainLogo || "";
|
||||||
|
const icon = data.storeSideIcon || data.domainIcon || "";
|
||||||
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||||
localStorage.setItem("seller_expiration_time", expirationTime);
|
localStorage.setItem("seller_expiration_time", expirationTime);
|
||||||
localStorage.setItem("sellersiteName", data.siteName);
|
localStorage.setItem("sellersiteName", data.siteName || "");
|
||||||
localStorage.setItem("sellerlogoImg", data.storeSideLogo);
|
if (logo) localStorage.setItem("sellerlogoImg", logo);
|
||||||
localStorage.setItem("sellerIconImg", data.storeSideIcon);
|
if (icon) localStorage.setItem("sellerIconImg", icon);
|
||||||
localStorage.setItem("sellerCompanyName", data.companyName || "");
|
localStorage.setItem("sellerCompanyName", data.companyName || "");
|
||||||
localStorage.setItem("sellerCompanyUrl", data.companyUrl || "");
|
localStorage.setItem("sellerCompanyUrl", data.companyUrl || "");
|
||||||
storeSideLogo.value = data.storeSideLogo || storeSideLogo.value;
|
storeSideLogo.value = logo || storeSideLogo.value;
|
||||||
if (data.siteName) window.document.title = data.siteName;
|
if (data.siteName) window.document.title = data.siteName;
|
||||||
applyFavicon(data.storeSideIcon);
|
applyFavicon(icon);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
|
|||||||
@@ -1,166 +1,99 @@
|
|||||||
|
<!--
|
||||||
|
商家单侧栏导航:栏目纵向展开,功能入口按两列排列,沿用当前账号权限与路由定位。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-08
|
||||||
|
-->
|
||||||
<template>
|
<template>
|
||||||
<div class="shrinkable-menu" :class="{ 'no-sub-menu': !menuList.length }">
|
<nav class="seller-navigation" aria-label="商家功能菜单">
|
||||||
<el-menu
|
<button type="button" class="nav-heading home-link" :class="{ 'is-active': route.name === 'home_index' }"
|
||||||
ref="sideMenu"
|
:aria-current="route.name === 'home_index' ? 'page' : undefined" @click="router.push({ name: 'home_index' })">
|
||||||
class="nav-menu-dark"
|
<el-icon :size="20"><House /></el-icon><span>首页</span>
|
||||||
:default-active="currNav"
|
</button>
|
||||||
@select="selectNav"
|
<section v-for="section in sections" :key="section.name" class="nav-section">
|
||||||
>
|
<button type="button" class="nav-heading" :class="{ 'is-current': activeItem?.topName === section.name }"
|
||||||
<el-menu-item v-for="(item, i) in navList" :key="i" :index="item.name">
|
:aria-label="section.title" :aria-expanded="!closedSections.includes(section.name)"
|
||||||
{{ item.title }}
|
:aria-controls="`seller-nav-${section.name}`" @click="toggleSection(section.name)">
|
||||||
</el-menu-item>
|
<el-icon :size="19"><component :is="navIcon(section)" /></el-icon>
|
||||||
</el-menu>
|
<span>{{ section.title }}</span>
|
||||||
<el-menu
|
<el-icon class="section-arrow" :class="{ 'is-open': !closedSections.includes(section.name) }" :size="14"><ArrowDown /></el-icon>
|
||||||
v-if="menuList.length"
|
</button>
|
||||||
ref="childrenMenu"
|
<div v-show="!closedSections.includes(section.name)" :id="`seller-nav-${section.name}`" class="nav-grid">
|
||||||
:key="currNav"
|
<button v-for="menu in section.items" :key="menu.name" type="button" class="nav-link"
|
||||||
class="sub-menu"
|
:class="{ 'is-active': activeItem?.name === menu.name }"
|
||||||
:default-active="$route.name"
|
:aria-current="activeItem?.name === menu.name ? 'page' : undefined"
|
||||||
@select="changeMenu"
|
:title="`${menu.groupTitle} / ${menu.title}`" @click="router.push({ name: menu.name })">
|
||||||
>
|
|
||||||
<template v-for="item in menuList" :key="item.id">
|
|
||||||
<el-menu-item-group :title="item.title">
|
|
||||||
<el-menu-item
|
|
||||||
v-for="menu in item.children"
|
|
||||||
:key="menu.name"
|
|
||||||
:index="menu.name"
|
|
||||||
>
|
|
||||||
{{ menu.title }}
|
{{ menu.title }}
|
||||||
</el-menu-item>
|
</button>
|
||||||
</el-menu-item-group>
|
|
||||||
</template>
|
|
||||||
</el-menu>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
</nav>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch, onMounted } from "vue";
|
import { computed, nextTick, ref, watch } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import util, { HOME_NAV_NAME } from "@/libs/util.js";
|
import { House, Goods, Tickets, Wallet, Present, TrendCharts, Shop, Bell, Grid, ArrowDown, User, Box } from "@element-plus/icons-vue";
|
||||||
|
import { HOME_NAV_NAME } from "@/libs/util.js";
|
||||||
|
import { flattenNavigation, findNavigationItem } from "@/libs/seller-navigation";
|
||||||
import { setStore } from "@/libs/storage";
|
import { setStore } from "@/libs/storage";
|
||||||
import { useAppStore } from "@/stores/app";
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
defineOptions({ name: "shrinkableMenu" });
|
defineOptions({ name: "shrinkableMenu" });
|
||||||
|
|
||||||
const appStore = useAppStore();
|
const appStore = useAppStore();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const closedSections = ref([]);
|
||||||
|
const allItems = computed(() => flattenNavigation(appStore.navigationMenus).filter((item) => router.hasRoute(item.name)));
|
||||||
|
const activeItem = computed(() => findNavigationItem(allItems.value, route.name));
|
||||||
|
const sections = computed(() => appStore.navigationMenus.map((top) => ({
|
||||||
|
...top, items: allItems.value.filter((item) => item.topName === top.name),
|
||||||
|
})).filter((top) => top.items.length));
|
||||||
|
|
||||||
const childrenMenu = ref(null);
|
/** 使用业务栏目图标,新增的未知栏目显示通用图标。 */
|
||||||
|
function navIcon(item) {
|
||||||
const menuList = computed(() => appStore.menuList);
|
return ({ 商品: Goods, 订单: Tickets, 财务: Wallet, 营销: Present, 统计: TrendCharts,
|
||||||
const navList = computed(() => appStore.navList);
|
数据: TrendCharts, 设置: Shop, 店铺: Shop, 消息: Bell, 会员: User, 采购: Box })[item.title] || Grid;
|
||||||
const currNav = computed(() => appStore.currNav);
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.name,
|
|
||||||
() => {
|
|
||||||
syncRouteMenu(route);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function syncRouteMenu(route) {
|
|
||||||
if (!route?.name) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (route.name === "home_index") {
|
|
||||||
activateHomeNav();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const firstRouterName =
|
|
||||||
route.meta?.firstRouterName || util.getFirstRouterName(route.name);
|
|
||||||
if (firstRouterName && firstRouterName !== currNav.value) {
|
|
||||||
selectNav(firstRouterName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateHomeNav() {
|
/** 各栏目独立展开收起,可同时查看多个栏目的功能,不改变当前业务页面。 */
|
||||||
appStore.setCurrNav(HOME_NAV_NAME);
|
function toggleSection(name) {
|
||||||
setStore("currNav", HOME_NAV_NAME);
|
closedSections.value = closedSections.value.includes(name)
|
||||||
appStore.updateMenulist([]);
|
? closedSections.value.filter((item) => item !== name) : [...closedSections.value, name];
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeMenu(name) {
|
/** 刷新、搜索和详情跳转后展开所属栏目;仅定位授权入口,不改变菜单顺序和权限。 */
|
||||||
if (!name) return;
|
async function syncRouteMenu() {
|
||||||
router.push({ name });
|
const name = activeItem.value?.topName || HOME_NAV_NAME;
|
||||||
}
|
|
||||||
|
|
||||||
function selectNav(name) {
|
|
||||||
appStore.childrenMenu(childrenMenu.value);
|
|
||||||
appStore.setCurrNav(name);
|
appStore.setCurrNav(name);
|
||||||
|
appStore.updateMenulist(appStore.navigationMenus.find((top) => top.name === name)?.children || []);
|
||||||
setStore("currNav", name);
|
setStore("currNav", name);
|
||||||
if (name === HOME_NAV_NAME) {
|
closedSections.value = closedSections.value.filter((item) => item !== name);
|
||||||
appStore.updateMenulist([]);
|
await nextTick();
|
||||||
if (route.name !== "home_index") {
|
document.querySelector('.seller-navigation .nav-link.is-active')?.scrollIntoView({ block: "nearest" });
|
||||||
router.push({ name: "home_index" });
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
util.initRouter();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
watch(() => appStore.navigationMenus, () => { closedSections.value = []; });
|
||||||
syncRouteMenu(route);
|
watch([() => route.fullPath, () => appStore.navigationMenus], syncRouteMenu, { immediate: true });
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.shrinkable-menu {
|
// 隐藏滚动条但保留滚轮、触控和键盘滚动,确保下方栏目仍可访问。
|
||||||
height: calc(100% - 60px);
|
.seller-navigation { height: 100%; box-sizing: border-box; padding: 18px 14px 32px; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; background: #fff;
|
||||||
width: 180px;
|
&::-webkit-scrollbar { display: none; width: 0; height: 0; }
|
||||||
display: flex;
|
|
||||||
transition: width 0.3s;
|
|
||||||
|
|
||||||
&.no-sub-menu {
|
|
||||||
width: 80px;
|
|
||||||
}
|
}
|
||||||
|
.nav-heading { width: 100%; display: flex; align-items: center; gap: 13px; min-height: 46px; padding: 11px 14px; border: 0; border-radius: 6px; background: transparent; color: #242832; text-align: left; font: inherit; font-size: 15px; font-weight: 600; cursor: pointer;
|
||||||
|
&:hover { background: #f7f8fc; }
|
||||||
|
&:focus-visible { outline: 2px solid #315efb; outline-offset: -2px; }
|
||||||
|
&.is-active { color: #315efb; background: #f0f4ff; }
|
||||||
}
|
}
|
||||||
|
.home-link { margin-bottom: 16px; }
|
||||||
.nav-menu-dark {
|
.nav-section { margin-bottom: 22px; }
|
||||||
width: 80px;
|
.section-arrow { margin-left: auto; color: #b8bdc7; transition: transform .15s; &.is-open { transform: rotate(180deg); } }
|
||||||
background-color: #191a23;
|
.nav-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px 8px; padding: 2px 5px 0 43px; }
|
||||||
border-right: none;
|
.nav-link { min-width: 0; min-height: 36px; padding: 7px 5px; border: 0; border-radius: 5px; background: transparent; color: #62666f; text-align: left; font: inherit; font-size: 13px; line-height: 22px; overflow-wrap: anywhere; cursor: pointer;
|
||||||
overflow-y: auto;
|
&:hover { background: #f5f7fc; color: #315efb; }
|
||||||
|
&:focus-visible { outline: 2px solid #315efb; outline-offset: -2px; }
|
||||||
:deep(.el-menu-item) {
|
&.is-active { color: #315efb; background: #eef3ff; font-weight: 600; }
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 8px !important;
|
|
||||||
text-align: center;
|
|
||||||
line-height: 1.3;
|
|
||||||
height: auto;
|
|
||||||
min-height: 56px;
|
|
||||||
white-space: normal;
|
|
||||||
transition: background-color 0.2s ease, color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item:not(.is-active):hover),
|
|
||||||
:deep(.el-menu-item:not(.is-active):focus) {
|
|
||||||
background-color: #43444d !important;
|
|
||||||
color: #fff !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item.is-active) {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-menu-item.is-active:hover),
|
|
||||||
:deep(.el-menu-item.is-active:focus) {
|
|
||||||
background-color: #fff !important;
|
|
||||||
color: var(--el-color-primary) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.sub-menu {
|
|
||||||
width: 100px;
|
|
||||||
overflow-y: auto;
|
|
||||||
border-right: none;
|
|
||||||
|
|
||||||
:deep(.el-menu-item-group__title) {
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
padding-left: 20px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) { .section-arrow { transition: none; } }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -24,15 +24,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-menu-con {
|
.sidebar-menu-con {
|
||||||
height: 100%;
|
height: calc(100% - 60px);
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 60px;
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 21;
|
z-index: 21;
|
||||||
width: 180px;
|
width: 300px;
|
||||||
transition: width 0.3s; // background: rgb(73, 80, 96)
|
transition: width 0.3s; // background: rgb(73, 80, 96)
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: rgba(0, 21, 41, 0.35) 2px 0px 6px;
|
border-right: 1px solid #e4e7ed;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout-text {
|
.layout-text {
|
||||||
@@ -52,12 +53,16 @@
|
|||||||
&-header-con {
|
&-header-con {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: block;
|
display: flex;
|
||||||
padding-left: 180px;
|
align-items: stretch;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #fff;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 106px;
|
height: 60px;
|
||||||
z-index: 20;
|
z-index: 22;
|
||||||
box-shadow: 0 2px 1px 1px rgba(100, 100, 100, 0.1);
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
box-shadow: none;
|
||||||
transition: padding 0.3s;
|
transition: padding 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +89,7 @@
|
|||||||
.tags-con {
|
.tags-con {
|
||||||
height: 46px;
|
height: 46px;
|
||||||
z-index: -1;
|
z-index: -1;
|
||||||
background: #f0f0f0;
|
background: #f7f8fa;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
@@ -123,10 +128,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-header {
|
&-header {
|
||||||
min-width: 740px;
|
flex: 1;
|
||||||
height: 60px;
|
min-width: 0;
|
||||||
background: #fff;
|
height: 100%;
|
||||||
box-shadow: 0 2px 1px 1px rgba(100, 100, 100, 0.1);
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 11;
|
z-index: 11;
|
||||||
|
|
||||||
@@ -153,8 +159,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
padding: 0 30px;
|
padding: 0 24px;
|
||||||
justify-content: space-between;
|
gap: 20px;
|
||||||
|
justify-content: flex-end;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
|
|
||||||
@@ -227,15 +234,15 @@
|
|||||||
|
|
||||||
.single-page-con {
|
.single-page-con {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 180px;
|
left: 300px;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 106px;
|
top: 60px;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: auto;
|
width: auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background-color: #f0f0f0;
|
background-color: #f7f8fa;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
transition: left 0.3s;
|
transition: left 0.3s;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -247,22 +254,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.menu-collapsed {
|
|
||||||
.sidebar-menu-con {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-header-con {
|
|
||||||
padding-left: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-page-con {
|
|
||||||
left: 80px;
|
|
||||||
right: 0;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&-copy {
|
&-copy {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 10px 0 20px;
|
padding: 10px 0 20px;
|
||||||
@@ -274,21 +265,27 @@
|
|||||||
transition: transform 0.3s;
|
transition: transform 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-con {
|
.main .logo-con {
|
||||||
width: 100%;
|
flex: 0 0 300px;
|
||||||
height: 60px;
|
height: 100%;
|
||||||
padding: 8px;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
padding: 8px 16px;
|
||||||
border-bottom: 1px solid #eee;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
overflow: hidden;
|
||||||
img {
|
img {
|
||||||
height: 44px;
|
display: block;
|
||||||
|
max-height: 44px;
|
||||||
|
max-width: 100%;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-bar {
|
.menu-bar {
|
||||||
overflow: auto;
|
overflow: visible;
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-bar::-webkit-scrollbar {
|
.menu-bar::-webkit-scrollbar {
|
||||||
@@ -304,6 +301,7 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.nav-item {
|
.nav-item {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
@@ -339,3 +337,50 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导航与顶部栏的样式局限于商家外层布局,不覆盖业务页面的组件主题。
|
||||||
|
.main {
|
||||||
|
.page-path-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 8px 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #e4e7ed;
|
||||||
|
}
|
||||||
|
.header-page-info { display: flex; align-items: center; gap: 8px; min-width: 0; font-size: 13px;
|
||||||
|
strong { color: #303749; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
}
|
||||||
|
.workspace-label { color: #969cab; white-space: nowrap; }
|
||||||
|
.header-divider { color: #d9dde6; }
|
||||||
|
.user-dropdown-innercon { flex-wrap: nowrap; gap: 16px; }
|
||||||
|
.header-store-name { margin-left: 9px; max-width: 110px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; color: #525a6b; }
|
||||||
|
.header-avator-con .nav-item { margin: 0; width: auto; height: auto; border: 0; border-radius: 0; }
|
||||||
|
.header-icon-button { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border: 1px solid #eceef3; border-radius: 6px; background: #fff; color: #626879; cursor: pointer;
|
||||||
|
&:hover { background: #f5f7fb; color: #315efb; }
|
||||||
|
&:focus-visible { outline: 2px solid #315efb; }
|
||||||
|
}
|
||||||
|
.header-message-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.single-page-con .single-page { margin: 16px; }
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.main {
|
||||||
|
.header-avator-con { padding: 0 16px; gap: 12px; }
|
||||||
|
.page-path-bar { padding: 8px 14px; }
|
||||||
|
.user-dropdown-innercon { gap: 10px; }
|
||||||
|
.header-store-name { display: none; }
|
||||||
|
.single-page-con .single-page { margin: 12px; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.main .sidebar-menu-con, .main-header-con, .main .single-page-con { transition: none; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -84,10 +84,12 @@ import { computed, onMounted, reactive, ref, watch } from "vue";
|
|||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import { Bell, ChatDotRound, Clock, Delete } from "@element-plus/icons-vue";
|
import { Bell, ChatDotRound, Clock, Delete } from "@element-plus/icons-vue";
|
||||||
import * as API_Index from "@/api/index";
|
import * as API_Index from "@/api/index";
|
||||||
|
import { useAppStore } from "@/stores/app";
|
||||||
|
|
||||||
defineOptions({ name: "message_index" });
|
defineOptions({ name: "message_index" });
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const appStore = useAppStore();
|
||||||
|
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const mesTypeList = ref([
|
const mesTypeList = ref([
|
||||||
@@ -153,7 +155,10 @@ function refreshMessage() {
|
|||||||
}
|
}
|
||||||
function refreshUnreadCount() {
|
function refreshUnreadCount() {
|
||||||
API_Index.getAllMessage(params).then((res) => {
|
API_Index.getAllMessage(params).then((res) => {
|
||||||
if (res.success) unReadCount.value = res.result.UN_READY.total;
|
if (res.success) {
|
||||||
|
unReadCount.value = res.result.UN_READY.total;
|
||||||
|
appStore.setUnreadMessageCount(unReadCount.value);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function getAll() {
|
function getAll() {
|
||||||
@@ -161,6 +166,7 @@ function getAll() {
|
|||||||
loading.value = false;
|
loading.value = false;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
unReadCount.value = res.result.UN_READY.total;
|
unReadCount.value = res.result.UN_READY.total;
|
||||||
|
appStore.setUnreadMessageCount(unReadCount.value);
|
||||||
currentMesList.value = res.result.UN_READY.records;
|
currentMesList.value = res.result.UN_READY.records;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
312
seller/src/views/shop/addressManage.vue
Normal file
312
seller/src/views/shop/addressManage.vue
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
<!--
|
||||||
|
商家端地址管理页面:集中维护店铺发货地址与退货(售后)地址。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-09
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<el-card>
|
||||||
|
<div class="setting-page">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
label-width="150px"
|
||||||
|
label-position="right"
|
||||||
|
:model="form"
|
||||||
|
:rules="formRules"
|
||||||
|
>
|
||||||
|
<div class="setting-section">
|
||||||
|
<div class="section-title">发货地址</div>
|
||||||
|
<el-form-item label="发货人姓名" prop="salesConsignorName">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsignorName"
|
||||||
|
maxlength="11"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入发货人姓名"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="发货人手机号" prop="salesConsignorMobile">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsignorMobile"
|
||||||
|
maxlength="11"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入发货人手机号"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="地址" prop="salesConsignorAddressId">
|
||||||
|
<span v-if="form.salesConsignorAddressPath" class="address-text">
|
||||||
|
{{ form.salesConsignorAddressPath }}
|
||||||
|
</span>
|
||||||
|
<el-button @click="handleChangeAddress('addressGoods')">选择</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细地址" prop="salesConsignorDetail">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsignorDetail"
|
||||||
|
clearable
|
||||||
|
maxlength="50"
|
||||||
|
placeholder="请输入详细地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="setting-section">
|
||||||
|
<div class="section-title">退货地址</div>
|
||||||
|
<el-form-item label="收货人" prop="salesConsigneeName">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsigneeName"
|
||||||
|
maxlength="11"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入收货人姓名"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="收货人电话" prop="salesConsigneeMobile">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsigneeMobile"
|
||||||
|
maxlength="11"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入收货人电话"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="售后地址" prop="salesConsigneeAddressId">
|
||||||
|
<span v-if="form.salesConsigneeAddressPath" class="address-text">
|
||||||
|
{{ form.salesConsigneeAddressPath }}
|
||||||
|
</span>
|
||||||
|
<el-button @click="handleChangeAddress('salesConsigneeAddressPath')">选择</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="详细地址" prop="salesConsigneeDetail">
|
||||||
|
<el-input
|
||||||
|
v-model="form.salesConsigneeDetail"
|
||||||
|
clearable
|
||||||
|
maxlength="50"
|
||||||
|
placeholder="请输入详细地址"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="submit-bar">
|
||||||
|
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<multipleMap ref="mapRef" @callback="callbackAddress" />
|
||||||
|
</el-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from "vue";
|
||||||
|
import * as API_Shop from "@/api/shops";
|
||||||
|
import multipleMap from "@/views/my-components/map/multiple-map";
|
||||||
|
import * as RegExp from "@/libs/RegExp.js";
|
||||||
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
|
defineOptions({ name: "addressManage" });
|
||||||
|
|
||||||
|
const currentKey = ref("");
|
||||||
|
const submitLoading = ref(false);
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
salesConsignorName: "",
|
||||||
|
salesConsignorMobile: "",
|
||||||
|
salesConsignorAddressId: "",
|
||||||
|
salesConsignorAddressPath: "",
|
||||||
|
salesConsignorDetail: "",
|
||||||
|
salesConsigneeName: "",
|
||||||
|
salesConsigneeMobile: "",
|
||||||
|
salesConsigneeAddressId: "",
|
||||||
|
salesConsigneeAddressPath: "",
|
||||||
|
salesConsigneeDetail: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const formRules = {
|
||||||
|
salesConsignorName: [
|
||||||
|
{ required: true, message: "请输入发货人姓名", trigger: "blur" },
|
||||||
|
],
|
||||||
|
salesConsignorMobile: [
|
||||||
|
{ required: true, message: "手机号不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: RegExp.mobile,
|
||||||
|
trigger: "blur",
|
||||||
|
message: "请输入正确的手机号",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
salesConsignorDetail: [
|
||||||
|
{ required: true, message: "请输入详细地址", trigger: "blur" },
|
||||||
|
],
|
||||||
|
salesConsigneeName: [
|
||||||
|
{ required: true, message: "请输入收货人", trigger: "blur" },
|
||||||
|
],
|
||||||
|
salesConsigneeMobile: [
|
||||||
|
{ required: true, message: "手机号不能为空", trigger: "blur" },
|
||||||
|
{
|
||||||
|
pattern: RegExp.mobile,
|
||||||
|
trigger: "blur",
|
||||||
|
message: "请输入正确的手机号",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
salesConsigneeDetail: [
|
||||||
|
{ required: true, message: "请输入详细地址", trigger: "blur" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
const mapRef = ref(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 接收地图/行政区选择器回传的地址数据。
|
||||||
|
* @param {object} val 地址选择结果
|
||||||
|
*/
|
||||||
|
function callbackAddress(val) {
|
||||||
|
if (val.type === "select") {
|
||||||
|
const paths = val.data.map((item) => item.name).join(",");
|
||||||
|
const ids = val.data.map((item) => item.id).join(",");
|
||||||
|
|
||||||
|
if (currentKey.value === "addressGoods") {
|
||||||
|
form.value.salesConsignorAddressPath = paths;
|
||||||
|
form.value.salesConsignorAddressId = ids;
|
||||||
|
}
|
||||||
|
if (currentKey.value === "salesConsigneeAddressPath") {
|
||||||
|
form.value.salesConsigneeAddressPath = paths;
|
||||||
|
form.value.salesConsigneeAddressId = ids;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (currentKey.value === "addressGoods") {
|
||||||
|
form.value.salesConsignorAddressPath = val.data.addr;
|
||||||
|
form.value.salesConsignorAddressId = val.data.addrId;
|
||||||
|
}
|
||||||
|
if (currentKey.value === "salesConsigneeAddressPath") {
|
||||||
|
form.value.salesConsigneeAddressPath = val.data.addr;
|
||||||
|
form.value.salesConsigneeAddressId = val.data.addrId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打开地址选择弹窗。
|
||||||
|
* @param {string} key 当前编辑的地址类型标识
|
||||||
|
*/
|
||||||
|
function handleChangeAddress(key) {
|
||||||
|
currentKey.value = key;
|
||||||
|
mapRef.value.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取店铺发货地址配置。
|
||||||
|
*/
|
||||||
|
function getDeliverAddress() {
|
||||||
|
API_Shop.getDeliverAddress().then((res) => {
|
||||||
|
if (res.success && res.result) {
|
||||||
|
form.value.salesConsignorName = res.result.salesConsignorName || "";
|
||||||
|
form.value.salesConsignorMobile = res.result.salesConsignorMobile || "";
|
||||||
|
form.value.salesConsignorAddressId = res.result.salesConsignorAddressId || "";
|
||||||
|
form.value.salesConsignorAddressPath = res.result.salesConsignorAddressPath || "";
|
||||||
|
form.value.salesConsignorDetail = res.result.salesConsignorDetail || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拉取商家退货地址配置。
|
||||||
|
*/
|
||||||
|
function getRefundGoodsAddress() {
|
||||||
|
API_Shop.getRefundGoodsAddress().then((res) => {
|
||||||
|
if (res.success && res.result) {
|
||||||
|
form.value.salesConsigneeName = res.result.salesConsigneeName || "";
|
||||||
|
form.value.salesConsigneeMobile = res.result.salesConsigneeMobile || "";
|
||||||
|
form.value.salesConsigneeAddressId = res.result.salesConsigneeAddressId || "";
|
||||||
|
form.value.salesConsigneeAddressPath = res.result.salesConsigneeAddressPath || "";
|
||||||
|
form.value.salesConsigneeDetail = res.result.salesConsigneeDetail || "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一校验并保存发货地址与退货地址配置。
|
||||||
|
*/
|
||||||
|
function handleSubmit() {
|
||||||
|
formRef.value.validate(async (valid) => {
|
||||||
|
if (!valid) return;
|
||||||
|
submitLoading.value = true;
|
||||||
|
try {
|
||||||
|
const deliverParams = {
|
||||||
|
salesConsignorName: form.value.salesConsignorName,
|
||||||
|
salesConsignorMobile: form.value.salesConsignorMobile,
|
||||||
|
salesConsignorAddressId: form.value.salesConsignorAddressId,
|
||||||
|
salesConsignorAddressPath: form.value.salesConsignorAddressPath,
|
||||||
|
salesConsignorDetail: form.value.salesConsignorDetail,
|
||||||
|
};
|
||||||
|
const refundParams = {
|
||||||
|
salesConsigneeName: form.value.salesConsigneeName,
|
||||||
|
salesConsigneeMobile: form.value.salesConsigneeMobile,
|
||||||
|
salesConsigneeAddressId: form.value.salesConsigneeAddressId,
|
||||||
|
salesConsigneeAddressPath: form.value.salesConsigneeAddressPath,
|
||||||
|
salesConsigneeDetail: form.value.salesConsigneeDetail,
|
||||||
|
};
|
||||||
|
const [deliverRes, refundRes] = await Promise.all([
|
||||||
|
API_Shop.editDeliverAddress(deliverParams),
|
||||||
|
API_Shop.saveRefundGoodsAddress(refundParams),
|
||||||
|
]);
|
||||||
|
if (deliverRes?.success && refundRes?.success) {
|
||||||
|
Message.success("保存成功");
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
submitLoading.value = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
getDeliverAddress();
|
||||||
|
getRefundGoodsAddress();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
init();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.setting-page {
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-section {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 20px 24px 4px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-form-item__content) {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-input) {
|
||||||
|
width: 400px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-bar {
|
||||||
|
text-align: center;
|
||||||
|
margin: 24px 0 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
294
seller/src/views/shop/shopInfo.vue
Normal file
294
seller/src/views/shop/shopInfo.vue
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
<!--
|
||||||
|
店铺信息页面:展示商家入驻时填写的企业及联系人信息、营业执照与法人信息、财务结算信息、店铺及经营信息。
|
||||||
|
@author Bulbasaur
|
||||||
|
@since 2026-11-09
|
||||||
|
-->
|
||||||
|
<template>
|
||||||
|
<div class="shop-info-page">
|
||||||
|
<el-card v-loading="loading" shadow="never" class="info-card">
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="section-title">店铺及经营信息</div>
|
||||||
|
<el-descriptions :column="2" border class="shop-desc">
|
||||||
|
<el-descriptions-item label="店铺名称">
|
||||||
|
{{ blankFilter(storeInfo.storeName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="商家账号">
|
||||||
|
{{ blankFilter(storeInfo.memberName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺类型">
|
||||||
|
{{ storeInfo.selfOperated == 1 || storeInfo.selfOperated === true ? "自营" : "非自营" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺状态">
|
||||||
|
{{ storeStatusText(storeInfo.storeDisable) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开店时间">
|
||||||
|
{{ blankFilter(storeInfo.createTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺定位">
|
||||||
|
{{ blankFilter(storeInfo.storeCenter) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺所在地">
|
||||||
|
{{ blankFilter(storeInfo.storeAddressPath) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺详细地址">
|
||||||
|
{{ blankFilter(storeInfo.storeAddressDetail) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺LOGO">
|
||||||
|
<el-image
|
||||||
|
v-if="isValid(storeInfo.storeLogo)"
|
||||||
|
:src="storeInfo.storeLogo"
|
||||||
|
:preview-src-list="[storeInfo.storeLogo]"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
class="cert-img"
|
||||||
|
/>
|
||||||
|
<span v-else>暂未上传</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺经营类目">
|
||||||
|
{{ categories.length ? categories.map((item) => item.name).join("、") : "暂未配置" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="店铺简介" :span="2">
|
||||||
|
{{ blankFilter(storeInfo.storeDesc) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="section-title">企业及联系人信息</div>
|
||||||
|
<el-descriptions :column="2" border class="shop-desc">
|
||||||
|
<el-descriptions-item label="公司名称">
|
||||||
|
{{ blankFilter(storeInfo.companyName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司电话">
|
||||||
|
{{ blankFilter(storeInfo.companyPhone) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司所在地">
|
||||||
|
{{ blankFilter(storeInfo.companyAddressPath) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="公司详细地址">
|
||||||
|
{{ blankFilter(storeInfo.companyAddress) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="员工总数">
|
||||||
|
{{ isValid(storeInfo.employeeNum) ? `${storeInfo.employeeNum} 人` : "-" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="注册资金">
|
||||||
|
{{ isValid(storeInfo.registeredCapital) ? `${storeInfo.registeredCapital} 万元` : "-" }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系人姓名">
|
||||||
|
{{ blankFilter(storeInfo.linkName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="联系人电话">
|
||||||
|
{{ blankFilter(storeInfo.linkPhone) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="电子邮箱" :span="2">
|
||||||
|
{{ blankFilter(storeInfo.companyEmail) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="section-title">营业执照与法人信息</div>
|
||||||
|
<el-descriptions :column="2" border class="shop-desc">
|
||||||
|
<el-descriptions-item label="营业执照号">
|
||||||
|
{{ blankFilter(storeInfo.licenseNum) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人姓名">
|
||||||
|
{{ blankFilter(storeInfo.legalName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人证件号" :span="2">
|
||||||
|
{{ blankFilter(storeInfo.legalId) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法定经营范围" :span="2">
|
||||||
|
{{ blankFilter(storeInfo.scope) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="营业执照电子版">
|
||||||
|
<el-image
|
||||||
|
v-if="isValid(storeInfo.licencePhoto)"
|
||||||
|
:src="storeInfo.licencePhoto"
|
||||||
|
:preview-src-list="[storeInfo.licencePhoto]"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
class="cert-img"
|
||||||
|
/>
|
||||||
|
<span v-else>暂未上传</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="法人证件电子版">
|
||||||
|
<template v-if="legalPhotoList.length">
|
||||||
|
<el-image
|
||||||
|
v-for="(img, idx) in legalPhotoList"
|
||||||
|
:key="idx"
|
||||||
|
:src="img"
|
||||||
|
:preview-src-list="legalPhotoList"
|
||||||
|
:initial-index="idx"
|
||||||
|
fit="cover"
|
||||||
|
preview-teleported
|
||||||
|
class="cert-img"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span v-else>暂未上传</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section-block">
|
||||||
|
<div class="section-title">财务结算信息</div>
|
||||||
|
<el-descriptions :column="2" border class="shop-desc">
|
||||||
|
<el-descriptions-item label="银行开户名">
|
||||||
|
{{ blankFilter(storeInfo.settlementBankAccountName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="银行账号">
|
||||||
|
{{ blankFilter(storeInfo.settlementBankAccountNum) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="开户银行支行名称">
|
||||||
|
{{ blankFilter(storeInfo.settlementBankBranchName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="支行联行号">
|
||||||
|
{{ blankFilter(storeInfo.settlementBankJointName) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="结算日" :span="2">
|
||||||
|
<span v-if="settlementCycles.length">每月 {{ settlementCycles.join("、") }} 日</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from "vue";
|
||||||
|
import * as API_Shop from "@/api/shops";
|
||||||
|
import { getGoodsCategoryAll } from "@/api/goods";
|
||||||
|
|
||||||
|
defineOptions({ name: "shopInfo" });
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const storeInfo = ref({});
|
||||||
|
const categories = ref([]);
|
||||||
|
|
||||||
|
const isValid = (v) => v !== null && v !== undefined && v !== "" && v !== "null";
|
||||||
|
|
||||||
|
function blankFilter(v) {
|
||||||
|
return isValid(v) ? v : "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
function storeStatusText(status) {
|
||||||
|
const map = {
|
||||||
|
OPEN: "营业中",
|
||||||
|
CLOSED: "已关闭",
|
||||||
|
APPLY: "申请开店中",
|
||||||
|
APPLYING: "待平台审核",
|
||||||
|
REFUSED: "审核未通过",
|
||||||
|
};
|
||||||
|
return map[status] || status || "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
const legalPhotoList = computed(() => {
|
||||||
|
if (!isValid(storeInfo.value.legalPhoto)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (Array.isArray(storeInfo.value.legalPhoto)) {
|
||||||
|
return storeInfo.value.legalPhoto.filter(isValid);
|
||||||
|
}
|
||||||
|
return String(storeInfo.value.legalPhoto)
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(isValid);
|
||||||
|
});
|
||||||
|
|
||||||
|
const settlementCycles = computed(() => {
|
||||||
|
if (!isValid(storeInfo.value.settlementCycle)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return String(storeInfo.value.settlementCycle)
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const [baseRes, detailRes, catRes] = await Promise.allSettled([
|
||||||
|
API_Shop.getShopInfo(),
|
||||||
|
API_Shop.getShopEntryDetail(),
|
||||||
|
getGoodsCategoryAll(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const merged = {};
|
||||||
|
if (baseRes.status === "fulfilled" && baseRes.value?.success && baseRes.value.result) {
|
||||||
|
Object.assign(merged, baseRes.value.result);
|
||||||
|
}
|
||||||
|
if (detailRes.status === "fulfilled" && detailRes.value?.success && detailRes.value.result) {
|
||||||
|
Object.assign(merged, detailRes.value.result);
|
||||||
|
}
|
||||||
|
storeInfo.value = merged;
|
||||||
|
|
||||||
|
if (catRes.status === "fulfilled" && catRes.value?.success && Array.isArray(catRes.value.result)) {
|
||||||
|
categories.value = catRes.value.result;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.shop-info-page {
|
||||||
|
padding-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-block + .section-block {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 4px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--el-color-primary);
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.shop-desc {
|
||||||
|
:deep(.el-descriptions__label) {
|
||||||
|
width: 160px;
|
||||||
|
color: #606266;
|
||||||
|
background: #fafbfc;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__content) {
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__cell) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.cert-img {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
margin: 0 10px 4px 0;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
cursor: pointer;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="search">
|
<div class="search">
|
||||||
<el-card style="margin-left: 10px">
|
<el-card style="margin-left: 10px">
|
||||||
<el-tabs v-model="type" @tab-click="handleClickType">
|
<el-tabs v-model="type">
|
||||||
<el-tab-pane label="基本信息" name="INFO">
|
<el-tab-pane label="基本信息" name="INFO">
|
||||||
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
<el-form ref="formRef" :model="form" label-width="100px" :rules="formValidate">
|
||||||
<el-form-item label="店铺名称">
|
<el-form-item label="店铺名称">
|
||||||
@@ -34,85 +34,6 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="发货地址">
|
|
||||||
<el-form ref="addressGoodsRef" :model="addressGoods" label-width="120px" :rules="addressGoodsValidate" class="shop-setting-form">
|
|
||||||
<el-form-item label="发货人姓名" prop="salesConsignorName">
|
|
||||||
<el-input v-model="addressGoods.salesConsignorName" maxlength="11" clearable style="width: 20%">
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="发货人手机号" prop="salesConsignorMobile">
|
|
||||||
<el-input v-model="addressGoods.salesConsignorMobile" maxlength="11" clearable style="width: 20%">
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="地址" prop="salesConsignorAddressId">
|
|
||||||
<span>{{ addressGoods.salesConsignorAddressPath }}</span>
|
|
||||||
<el-button style="margin-left: 10px;" @click="handleChangeAddress('addressGoods')">选择</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
<!-- <el-form-item label="地址名称" prop="salesConsignorAddressPath">
|
|
||||||
<el-input
|
|
||||||
v-model="addressGoods.salesConsignorAddressPath"
|
|
||||||
clearable
|
|
||||||
style="width: 20%"
|
|
||||||
>
|
|
||||||
</el-input>
|
|
||||||
</el-form-item> -->
|
|
||||||
<el-form-item label="详细地址" prop="salesConsignorDetail">
|
|
||||||
<el-input v-model="addressGoods.salesConsignorDetail" clearable style="width: 20%">
|
|
||||||
</el-input>
|
|
||||||
</el-form-item>
|
|
||||||
<el-button @click="setAddressGoods" type="primary" style="margin-left: 8px">确认
|
|
||||||
</el-button>
|
|
||||||
</el-form>
|
|
||||||
</el-tab-pane>
|
|
||||||
<el-tab-pane label="退货地址" name="REFUND_GOODS_ADDRESS">
|
|
||||||
<el-form ref="addressFormRef" :model="addressForm" label-width="100px" :rules="afterFormValidate">
|
|
||||||
<el-form-item label="收货人" prop="salesConsigneeName">
|
|
||||||
<el-input v-model="addressForm.salesConsigneeName" maxlength="11" clearable style="width: 20%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="收货人电话" prop="salesConsigneeMobile">
|
|
||||||
<el-input v-model="addressForm.salesConsigneeMobile" maxlength="11" style="width: 20%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="售后地址">
|
|
||||||
<span>{{ addressForm.salesConsigneeAddressPath }}</span>
|
|
||||||
<el-button style="margin-left: 10px;" @click="handleChangeAddress('salesConsigneeAddressPath')">选择</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="详细地址" prop="salesConsigneeDetail">
|
|
||||||
<el-input v-model="addressForm.salesConsigneeDetail" clearable style="width: 20%" maxlength="50" />
|
|
||||||
</el-form-item>
|
|
||||||
|
|
||||||
<el-form-item>
|
|
||||||
<el-button @click="afterHandleSubmit" :loading="submitLoading" type="primary" style="margin-right: 5px">修改
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-tab-pane>
|
|
||||||
<!-- Udesk 坐席功能 后续维护 -->
|
|
||||||
<!-- <el-tab-pane label="客服设置" name="UDESK">
|
|
||||||
<el-form
|
|
||||||
ref="udeskForm"
|
|
||||||
:model="udeskForm"
|
|
||||||
label-width="100px"
|
|
||||||
:rules="udeskFormValidate"
|
|
||||||
>
|
|
||||||
<el-form-item label="坐席id" prop="merchantEuid">
|
|
||||||
<el-input
|
|
||||||
v-model="udeskForm.merchantEuid"
|
|
||||||
maxlength="30"
|
|
||||||
clearable
|
|
||||||
style="width: 20%"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item>
|
|
||||||
<el-button
|
|
||||||
@click="merchantSubmit"
|
|
||||||
:loading="submitLoading"
|
|
||||||
type="primary"
|
|
||||||
style="margin-right: 5px"
|
|
||||||
>修改
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
</el-tab-pane> -->
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@@ -126,7 +47,6 @@ import * as API_Shop from "@/api/shops";
|
|||||||
import { validateMobile } from "@/libs/validate";
|
import { validateMobile } from "@/libs/validate";
|
||||||
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
|
import uploadPicThumb from "@/views/my-components/lili/upload-pic-thumb";
|
||||||
import multipleMap from "@/views/my-components/map/multiple-map";
|
import multipleMap from "@/views/my-components/map/multiple-map";
|
||||||
import * as RegExp from "@/libs/RegExp.js";
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import { Message } from "@/utils/message";
|
import { Message } from "@/utils/message";
|
||||||
|
|
||||||
@@ -137,36 +57,7 @@ const type = ref("INFO");
|
|||||||
|
|
||||||
const storeName = ref(""); // 店铺名称
|
const storeName = ref(""); // 店铺名称
|
||||||
|
|
||||||
const addressForm = ref({
|
|
||||||
// 退货地址
|
|
||||||
salesConsigneeName: "", // 收货人姓名
|
|
||||||
salesConsigneeMobile: "", // 收货人电话
|
|
||||||
salesConsigneeAddressId: "", // 售后地址id,逗号分割
|
|
||||||
salesConsigneeAddressPath: "", // 售后地址,逗号分割
|
|
||||||
salesConsigneeDetail: "", // 详细地址
|
|
||||||
});
|
|
||||||
//im form
|
|
||||||
const udeskForm = ref({
|
|
||||||
merchantEuid: "",
|
|
||||||
});
|
|
||||||
const afterFormValidate = {
|
|
||||||
salesConsigneeMobile: [
|
|
||||||
{ required: true, message: "手机号不能为空", trigger: "blur" },
|
|
||||||
{
|
|
||||||
pattern: RegExp.mobile,
|
|
||||||
trigger: "blur",
|
|
||||||
message: "请输入正确的手机号",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
salesConsigneeName: [
|
|
||||||
{ required: true, message: "请输入收货人", trigger: "blur" },
|
|
||||||
],
|
|
||||||
salesConsigneeDetail: [
|
|
||||||
{ required: true, message: "请输入详细地址", trigger: "blur" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const form = ref({
|
const form = ref({
|
||||||
// 添加或编辑表单对象初始化数据
|
|
||||||
storeAddressPath: "", // 店铺地址中文
|
storeAddressPath: "", // 店铺地址中文
|
||||||
storeCenter: "", // 经度 + 纬度
|
storeCenter: "", // 经度 + 纬度
|
||||||
longitude: "", // 经度
|
longitude: "", // 经度
|
||||||
@@ -220,132 +111,67 @@ const formValidate = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
const submitLoading = ref(false); // 添加或编辑提交状态
|
const submitLoading = ref(false);
|
||||||
//发货地址
|
|
||||||
const addressGoods = ref({
|
|
||||||
salesConsignorName: " ",
|
|
||||||
salesConsignorMobile: " ",
|
|
||||||
salesConsignorAddressId: " ",
|
|
||||||
salesConsignorAddressPath: " ",
|
|
||||||
salesConsignorDetail: " ",
|
|
||||||
});
|
|
||||||
|
|
||||||
const addressGoodsValidate = {
|
|
||||||
salesConsignorName: [
|
|
||||||
{ required: true, message: "请输入发货人姓名", trigger: "blur" },
|
|
||||||
],
|
|
||||||
salesConsignorMobile: [
|
|
||||||
{ required: true, message: "手机号不能为空", trigger: "blur" },
|
|
||||||
{
|
|
||||||
pattern: RegExp.mobile,
|
|
||||||
trigger: "blur",
|
|
||||||
message: "请输入正确的手机号",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
salesConsignorDetail: [
|
|
||||||
{ required: true, message: "请输入详细地址", trigger: "blur" },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const formRef = ref(null);
|
const formRef = ref(null);
|
||||||
const addressGoodsRef = ref(null);
|
|
||||||
const addressFormRef = ref(null);
|
|
||||||
const mapRef = ref(null);
|
const mapRef = ref(null);
|
||||||
|
|
||||||
// 回调地址
|
// 回调地址
|
||||||
function callbackAddress(val) {
|
function callbackAddress(val) {
|
||||||
if (val.type === 'select') {
|
if (val.type === "select") {
|
||||||
const paths = val.data.map(item => item.name).join(',')
|
const paths = val.data.map((item) => item.name).join(",");
|
||||||
|
const ids = val.data.map((item) => item.id).join(",");
|
||||||
|
|
||||||
const ids = val.data.map(item => item.id).join(',')
|
if (currentKey.value === "storeAddress") {
|
||||||
|
form.value.storeAddressPath = paths;
|
||||||
if (currentKey.value === 'storeAddress') {
|
form.value.storeAddressIdPath = ids;
|
||||||
form.value.storeAddressPath = paths
|
form.value.storeCenter = val.data[val.data.length - 1].center;
|
||||||
form.value.storeAddressIdPath = ids
|
|
||||||
form.value.storeCenter = val.data[val.data.length - 1].center
|
|
||||||
}
|
}
|
||||||
if (currentKey.value === 'addressGoods') {
|
} else {
|
||||||
addressGoods.value.salesConsignorAddressPath = paths
|
if (
|
||||||
addressGoods.value.salesConsignorAddressId = ids
|
currentKey.value === "storeAddress" ||
|
||||||
}
|
currentKey.value === "storeAddressstoreAddress"
|
||||||
if (currentKey.value === 'salesConsigneeAddressPath') {
|
) {
|
||||||
addressForm.value.salesConsigneeAddressPath = paths
|
|
||||||
addressForm.value.salesConsigneeAddressId = ids
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (currentKey.value === 'storeAddressstoreAddress') {
|
|
||||||
form.value.storeAddressPath = val.data.addr;
|
form.value.storeAddressPath = val.data.addr;
|
||||||
form.value.storeAddressIdPath = val.data.addrId;
|
form.value.storeAddressIdPath = val.data.addrId;
|
||||||
form.value.storeCenter = val.data.position.lng + "," + val.data.position.lat;
|
form.value.storeCenter =
|
||||||
}
|
val.data.position.lng + "," + val.data.position.lat;
|
||||||
if (currentKey.value === 'addressGoods') {
|
|
||||||
addressGoods.value.salesConsignorAddressPath = val.data.addr;
|
|
||||||
addressGoods.value.salesConsignorAddressId = val.data.addrId;
|
|
||||||
}
|
|
||||||
if (currentKey.value === 'salesConsigneeAddressPath') {
|
|
||||||
addressForm.value.salesConsigneeAddressPath = val.data.addr;
|
|
||||||
addressForm.value.salesConsigneeAddressId = val.data.addrId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 修改地址
|
// 修改地址
|
||||||
function handleChangeAddress(key) {
|
function handleChangeAddress(key) {
|
||||||
currentKey.value = key
|
currentKey.value = key;
|
||||||
mapRef.value.open();
|
mapRef.value.open();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化数据
|
// 初始化数据
|
||||||
function init() {
|
function init() {
|
||||||
getShopInfo();
|
getShopInfo();
|
||||||
getDeliverAddress()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function setAddressGoods() {
|
|
||||||
addressGoodsRef.value.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
API_Shop.editDeliverAddress(addressGoods.value).then(res => {
|
|
||||||
if (res.success) {
|
|
||||||
Message.success("修改成功")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// 获取店铺信息
|
// 获取店铺信息
|
||||||
function getShopInfo() {
|
function getShopInfo() {
|
||||||
API_Shop.getShopInfo().then((res) => {
|
API_Shop.getShopInfo().then((res) => {
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
form.value = res.result;
|
form.value = res.result;
|
||||||
|
|
||||||
storeName.value = res.result.storeName;
|
storeName.value = res.result.storeName;
|
||||||
form.value.storeCenter = res.result.storeCenter;
|
form.value.storeCenter = res.result.storeCenter;
|
||||||
Cookies.set("userInfoSeller", JSON.stringify(res.result));
|
Cookies.set("userInfoSeller", JSON.stringify(res.result));
|
||||||
if (res.result.merchantEuid) {
|
|
||||||
//赋予坐席id
|
|
||||||
udeskForm.value.merchantEuid = res.result.merchantEuid;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function pageShow(type) {
|
|
||||||
form.value.pageShow = type
|
function pageShow(val) {
|
||||||
|
form.value.pageShow = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
function changeSelfPickFlag(item) {
|
function changeSelfPickFlag(item) {
|
||||||
if (item) {
|
if (item) {
|
||||||
form.value.selfPickFlag = item
|
form.value.selfPickFlag = item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function getDeliverAddress() {
|
|
||||||
API_Shop.getDeliverAddress().then(res => {
|
|
||||||
if (res.success) {
|
|
||||||
if (res.result != '' && res.result != null) {
|
|
||||||
addressGoods.value = res.result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交保存
|
// 提交保存
|
||||||
function handleSubmit() {
|
function handleSubmit() {
|
||||||
@@ -363,42 +189,6 @@ function handleSubmit() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//tab切换
|
|
||||||
function handleClickType(pane) {
|
|
||||||
const name = pane.paneName;
|
|
||||||
//退款
|
|
||||||
if (name === "INFO") {
|
|
||||||
getShopInfo();
|
|
||||||
}
|
|
||||||
//退货
|
|
||||||
if (name === "REFUND_GOODS_ADDRESS") {
|
|
||||||
getRefundGoodsAddress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//获取商家退货地址
|
|
||||||
function getRefundGoodsAddress() {
|
|
||||||
API_Shop.getRefundGoodsAddress().then((res) => {
|
|
||||||
if (res.result != null) {
|
|
||||||
addressForm.value = res.result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//提交保存
|
|
||||||
function afterHandleSubmit() {
|
|
||||||
addressFormRef.value.validate((valid) => {
|
|
||||||
if (valid) {
|
|
||||||
submitLoading.value = true;
|
|
||||||
API_Shop.saveRefundGoodsAddress(addressForm.value).then((res) => {
|
|
||||||
submitLoading.value = false;
|
|
||||||
if (res.success) {
|
|
||||||
Message.success("修改成功");
|
|
||||||
getRefundGoodsAddress();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
@@ -408,15 +198,4 @@ onMounted(() => {
|
|||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shop-setting-form {
|
|
||||||
:deep(.el-form-item) {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-form-item__label) {
|
|
||||||
white-space: nowrap;
|
|
||||||
line-height: 32px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user