refactor(layout): 重构商家后台布局与导航结构

- 移除多标签页内嵌模式,详情页固定在新窗口打开
- 重构顶部栏与侧边栏布局,Logo 移至顶部栏区域
- 新增未读消息角标,与消息页面实时同步数量
- 新增面包屑路径栏替代静态页面标题展示
- 删除页面配置抽屉和菜单搜索组件
- 将发货与退货地址设置拆分为独立页面(店铺信息、地址管理)
- 增加 Logo 加载失败回退及 favicon 健壮性处理
- 注册店铺信息与地址管理页面的显式懒加载映射
This commit is contained in:
pikachu1995@126.com
2026-11-09 15:01:19 +08:00
parent fd241bc36a
commit 76b0d6efc8
19 changed files with 879 additions and 519 deletions

View File

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

View File

@@ -31,6 +31,6 @@ module.exports = {
*/ */
mainColor: "#ff5c58", //主题色 mainColor: "#ff5c58", //主题色
setting: { setting: {
isUseTabsRouter: false, // 多标签页内嵌模式默认关闭,用户可在页面配置中开启。 isUseTabsRouter: false, // 商家后台固定关闭多标签页内嵌模式,详情使用新页面展示。
}, },
}; };

View File

@@ -1,6 +1,6 @@
{ {
"setting": { "setting": {
"isUseTabsRouter": true, "isUseTabsRouter": false,
"showFooter": false "showFooter": false
} }
} }

View File

@@ -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`));

View File

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

View File

@@ -27,8 +27,13 @@ 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() { resetNavigationRoutes() {
this.routers = [otherRouter]; this.routers = [otherRouter];

View File

@@ -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 };
}, },
}, },
}); });

View File

@@ -30,6 +30,7 @@ export const useUserStore = defineStore("user", {
appStore.updateMenulist([]); appStore.updateMenulist([]);
appStore.setCurrNav(""); appStore.setCurrNav("");
appStore.setAdded(false); appStore.setAdded(false);
appStore.setUnreadMessageCount(0);
appStore.resetNavigationRoutes(); appStore.resetNavigationRoutes();
appStore.storeOpenedList = [{ title: "首页", path: "", name: "home_index" }]; appStore.storeOpenedList = [{ title: "首页", path: "", name: "home_index" }];
}, },

View File

@@ -51,21 +51,12 @@ 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);
window.open(url.href, '_blank');
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');
}
}
} }

View File

@@ -5,29 +5,44 @@
<template> <template>
<div class="main"> <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" />
<span class="brand-caption">商家工作台</span>
</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 class="header-page-info">
<span class="workspace-label">商家后台</span>
<span class="header-divider">/</span>
<strong>{{ currentPageTitle }}</strong>
</div>
<div class="user-dropdown-menu-con"> <div class="user-dropdown-menu-con">
<el-row <el-row
justify="end" justify="end"
align="middle" align="middle"
class="user-dropdown-innercon" class="user-dropdown-innercon"
> >
<menu-search />
<store-qrcode /> <store-qrcode />
<ul class="nav-list"> <ul class="nav-list">
<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"> <li class="nav-item">
<el-tooltip content="联系客服" placement="bottom"> <el-tooltip content="联系客服" placement="bottom">
<el-button type="warning" size="small" :loading="load" @click="im"> <el-button type="warning" size="small" :loading="load" @click="im">
@@ -36,13 +51,6 @@
</el-button> </el-button>
</el-tooltip> </el-tooltip>
</li> </li>
<li class="nav-item">
<el-tooltip content="设置" placement="bottom">
<button type="button" class="header-icon-button" aria-label="页面设置" @click="handleClickSetting">
<el-icon :size="17"><Setting /></el-icon>
</button>
</el-tooltip>
</li>
</ul> </ul>
<el-dropdown trigger="hover" @command="handleClickUserDropdown"> <el-dropdown trigger="hover" @command="handleClickUserDropdown">
<div class="dropList"> <div class="dropList">
@@ -66,17 +74,17 @@
</div> </div>
</div> </div>
</div> </div>
<div class="tags-con" v-if="setting.isUseTabsRouter">
<tags-page-opened :pageTagsList="pageTagsList" />
</div>
</div> </div>
<div <div class="single-page-con">
class="single-page-con" <div class="page-path-bar">
:style="{ <div class="header-page-info">
top: setting.isUseTabsRouter ? '106px' : '60px', <template v-for="(part, index) in currentPathParts" :key="index">
height: setting.isUseTabsRouter ? 'calc(100% - 116px)' : 'calc(100% - 70px)', <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 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" />
@@ -84,52 +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 menuSearch from "@/views/main-components/menu-search.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 currentPageTitle = computed(() => route.meta.title || pageTagsList.value.find((item) => item.name === route.name)?.title || '工作台'); 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() {
@@ -152,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");
storeSideLogo.value =
cachedLogo && cachedLogo !== "undefined" && cachedLogo !== "null"
? cachedLogo
: defaultLogo;
util.title(currentPageTitle.value); util.title(currentPageTitle.value);
const link = applyFavicon(localStorage.getItem("sellerIconImg"));
document.querySelector("link[rel*='icon']") ||
document.createElement("link"); loadSiteSettings()
link.type = "image/x-icon"; .then((data) => {
link.href = localStorage.getItem("sellerIconImg"); if (data) {
link.rel = "shortcut icon"; const logo = data.storeSideLogo || data.domainLogo || "";
document.getElementsByTagName("head")[0].appendChild(link); 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) {
@@ -214,6 +291,7 @@ watch(
} }
checkTag(name); checkTag(name);
util.title(currentPageTitle.value); util.title(currentPageTitle.value);
loadUnreadMessageCount();
} }
); );

View File

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

View File

@@ -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(() => {});

View File

@@ -1,92 +0,0 @@
<!--
商家功能搜索:仅检索当前账号已授权且已注册的菜单,支持键盘选择。
@author Bulbasaur
@since 2026-11-08
-->
<template>
<button type="button" class="menu-search-trigger" aria-label="搜索功能菜单" @click="openSearch">
<el-icon :size="17"><Search /></el-icon><span>搜索功能菜单</span><kbd>/</kbd>
</button>
<el-dialog v-model="visible" class="seller-menu-search" title="搜索功能" width="560px" align-center
@opened="focusInput" @closed="keyword = ''">
<el-input ref="inputRef" v-model="keyword" placeholder="搜索菜单,例如:商品、订单、优惠券" size="large" clearable
aria-label="搜索菜单" @keydown.down.prevent="moveSelection(1)" @keydown.up.prevent="moveSelection(-1)"
@keydown.enter.prevent="navigate(results[selected])">
<template #prefix><el-icon><Search /></el-icon></template>
</el-input>
<div class="search-summary">{{ keyword.trim() ? `找到 ${results.length} 个功能` : '全部可用功能' }}</div>
<div ref="resultsRef" class="search-results">
<button v-for="(item, index) in results" :key="`${item.topName}-${item.name}`" type="button"
class="search-result" :class="{ selected: selected === index }" @click="navigate(item)" @mouseenter="selected = index">
<div><strong>{{ item.title }}</strong><span>{{ item.topTitle }} / {{ item.groupTitle }}</span></div>
<el-icon><ArrowRight /></el-icon>
</button>
<el-empty v-if="!results.length" :image-size="72" description="没有匹配的功能,试试其他关键词" />
</div>
<template #footer><div class="search-help"><span>↑ ↓ 选择</span><span>Enter 打开</span><span>Esc 关闭</span></div></template>
</el-dialog>
</template>
<script setup>
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { useRouter } from "vue-router";
import { Search, ArrowRight } from "@element-plus/icons-vue";
import { useAppStore } from "@/stores/app";
import { flattenNavigation, searchNavigation } from "@/libs/seller-navigation";
const router = useRouter();
const appStore = useAppStore();
const visible = ref(false);
const keyword = ref("");
const selected = ref(0);
const inputRef = ref(null);
const resultsRef = ref(null);
const items = computed(() => flattenNavigation(appStore.navigationMenus).filter((item) => router.hasRoute(item.name)));
const results = computed(() => searchNavigation(items.value, keyword.value));
/** 每次打开清空上次关键词,避免残留搜索状态。 */
function openSearch() { keyword.value = ""; selected.value = 0; visible.value = true; }
/** 等弹层完成动画后聚焦输入框。 */
function focusInput() { inputRef.value?.focus(); }
/** 键盘移动候选项并滚动到可见区域;空列表不产生无效索引。 */
async function moveSelection(step) {
if (!results.value.length) return;
selected.value = (selected.value + step + results.value.length) % results.value.length;
await nextTick();
resultsRef.value?.children[selected.value]?.scrollIntoView({ block: "nearest" });
}
/** 只允许打开当前权限搜索结果;清空关闭的弹层避免遮挡业务页面。 */
function navigate(item) {
if (!item || !items.value.some((entry) => entry.name === item.name)) return;
visible.value = false;
router.push({ name: item.name });
}
/** 仅在非编辑区域按 / 唤起搜索,不截获输入框、富文本和其他弹窗内的按键。 */
function handleShortcut(event) {
if (event.key !== "/" || event.ctrlKey || event.metaKey || event.altKey || visible.value ||
event.target.closest?.('input, textarea, select, [contenteditable="true"], [role="dialog"]')) return;
event.preventDefault();
openSearch();
}
watch(results, () => { selected.value = 0; });
watch(() => appStore.navigationMenus, () => { visible.value = false; keyword.value = ""; });
onMounted(() => window.addEventListener("keydown", handleShortcut));
onBeforeUnmount(() => window.removeEventListener("keydown", handleShortcut));
</script>
<style lang="scss" scoped>
.menu-search-trigger { width: 240px; height: 34px; display: flex; align-items: center; gap: 9px; padding: 0 12px; border: 1px solid #eceef3; border-radius: 7px; background: #f7f8fb; color: #969cab; font: inherit; font-size: 12px; cursor: pointer;
&:hover { border-color: #c5d2ff; color: #315efb; }
&:focus-visible { outline: 2px solid #315efb; }
kbd { margin-left: auto; padding: 0 5px; border: 1px solid #e0e4ed; border-radius: 3px; background: #fff; font-family: inherit; }
}
.search-summary { padding: 18px 2px 8px; font-size: 12px; color: #969cab; }
.search-results { max-height: 380px; overflow-y: auto; }
.search-result { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 12px; margin: 3px 0; border: 0; border-radius: 7px; background: #fff; text-align: left; cursor: pointer; color: #858c9c;
strong { display: block; font-size: 14px; font-weight: 500; color: #303749; }
span { display: block; margin-top: 5px; font-size: 12px; }
&.selected, &:focus-visible { background: #eef2ff; strong { color: #315efb; } }
}
.search-help { display: flex; gap: 20px; color: #969cab; font-size: 12px; }
@media (max-width: 1100px) { .menu-search-trigger { width: 180px; } }
</style>

View File

@@ -78,7 +78,7 @@ watch([() => route.fullPath, () => appStore.navigationMenus], syncRouteMenu, { i
<style lang="scss" scoped> <style lang="scss" scoped>
// 隐藏滚动条但保留滚轮、触控和键盘滚动,确保下方栏目仍可访问。 // 隐藏滚动条但保留滚轮、触控和键盘滚动,确保下方栏目仍可访问。
.seller-navigation { height: calc(100% - 64px); box-sizing: border-box; padding: 18px 14px 32px; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; background: #fff; .seller-navigation { height: 100%; box-sizing: border-box; padding: 18px 14px 32px; overflow-y: auto; overflow-x: hidden; scrollbar-width: none; background: #fff;
&::-webkit-scrollbar { display: none; width: 0; height: 0; } &::-webkit-scrollbar { display: none; width: 0; height: 0; }
} }
.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; .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;

View File

@@ -24,15 +24,15 @@
} }
.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: 300px; width: 300px;
transition: width 0.3s; // background: rgb(73, 80, 96) transition: width 0.3s; // background: rgb(73, 80, 96)
background: #fff; background: #fff;
border-right: 1px solid #e9ecf2; border-right: 1px solid #e4e7ed;
box-shadow: none; box-shadow: none;
} }
@@ -53,12 +53,15 @@
&-header-con { &-header-con {
box-sizing: border-box; box-sizing: border-box;
position: fixed; position: fixed;
display: block; display: flex;
padding-left: 300px; align-items: stretch;
top: 0;
left: 0;
background: #fff;
width: 100%; width: 100%;
height: 106px; height: 60px;
z-index: 20; z-index: 22;
border-bottom: 1px solid #e9ecf2; border-bottom: 1px solid #e4e7ed;
box-shadow: none; box-shadow: none;
transition: padding 0.3s; transition: padding 0.3s;
} }
@@ -125,10 +128,10 @@
} }
&-header { &-header {
flex: 1;
min-width: 0; min-width: 0;
height: 60px; height: 100%;
background: #fff; background: transparent;
border-bottom: 1px solid #e9ecf2;
box-shadow: none; box-shadow: none;
position: relative; position: relative;
z-index: 11; z-index: 11;
@@ -158,7 +161,7 @@
padding: 0 24px; padding: 0 24px;
gap: 20px; gap: 20px;
justify-content: space-between; justify-content: flex-end;
height: 100%; height: 100%;
@@ -233,7 +236,7 @@
position: absolute; position: absolute;
left: 300px; left: 300px;
right: 0; right: 0;
top: 106px; top: 60px;
bottom: 0; bottom: 0;
width: auto; width: auto;
min-width: 0; min-width: 0;
@@ -262,19 +265,21 @@
transition: transform 0.3s; transition: transform 0.3s;
} }
.logo-con { .main .logo-con {
width: 100%; flex: 0 0 300px;
height: 64px; height: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 12px 16px; padding: 8px 16px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; justify-content: flex-start;
overflow: hidden; overflow: hidden;
border-bottom: 1px solid #e9ecf2;
img { img {
max-height: 36px; display: block;
max-width: 48px; max-height: 44px;
max-width: 100%;
width: auto;
height: auto;
object-fit: contain; object-fit: contain;
} }
} }
@@ -296,6 +301,7 @@
background: #fff; background: #fff;
} }
.nav-item { .nav-item {
font-size: 14px; font-size: 14px;
} }
@@ -334,8 +340,19 @@
// 导航与顶部栏的样式局限于商家外层布局,不覆盖业务页面的组件主题。 // 导航与顶部栏的样式局限于商家外层布局,不覆盖业务页面的组件主题。
.main { .main {
.brand-caption { color: #252b3a; font-size: 15px; font-weight: 600; white-space: nowrap; } .page-path-bar {
.header-page-info { display: flex; align-items: center; gap: 12px; min-width: 0; font-size: 13px; 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; } strong { color: #303749; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
} }
.workspace-label { color: #969cab; white-space: nowrap; } .workspace-label { color: #969cab; white-space: nowrap; }
@@ -347,13 +364,20 @@
&:hover { background: #f5f7fb; color: #315efb; } &:hover { background: #f5f7fb; color: #315efb; }
&:focus-visible { outline: 2px solid #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; } .single-page-con .single-page { margin: 16px; }
} }
@media (max-width: 1200px) { @media (max-width: 1200px) {
.main { .main {
.header-avator-con { padding: 0 16px; gap: 12px; } .header-avator-con { padding: 0 16px; gap: 12px; }
.page-path-bar { padding: 8px 14px; }
.user-dropdown-innercon { gap: 10px; } .user-dropdown-innercon { gap: 10px; }
.workspace-label, .header-divider, .header-store-name { display: none; } .header-store-name { display: none; }
.single-page-con .single-page { margin: 12px; } .single-page-con .single-page { margin: 12px; }
} }
} }

View File

@@ -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;
} }
}); });

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

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

View File

@@ -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,53 +47,23 @@ 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";
defineOptions({ name: "shopSetting" }); defineOptions({ name: "shopSetting" });
const currentKey = ref(""); //存储当前点击地址的key const currentKey = ref(""); // 存储当前点击地址的key
const type = ref("INFO"); 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: "", // 经度
latitude: "", //纬度 latitude: "", // 纬度
storeAddressDetail: "", //详细地址 storeAddressDetail: "", // 详细地址
storeAddressIdPath: "", //地址 storeAddressIdPath: "", // 地址
storeDesc: "", // 店铺描述 storeDesc: "", // 店铺描述
pageShow: false, pageShow: false,
selfPickFlag: false, selfPickFlag: false,
@@ -220,134 +111,69 @@ 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) {
if (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 changeSelfPickFlag(item) {
if (item) {
form.value.selfPickFlag = item;
}
}
// 提交保存
function handleSubmit() { function handleSubmit() {
formRef.value.validate((valid) => { formRef.value.validate((valid) => {
if (valid) { if (valid) {
@@ -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>