mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-11-06 17:09:08 +08:00
refactor(site-settings): 统一站点配置管理并清理废弃页面
- 新增 useSiteSettings composable 替代各端硬编码的配置引用 - 移除 memberRecycle 和 fictitiousOrderList 废弃页面并增加缓存过滤 - 版权信息改为通过接口动态获取,后台新增版权归属配置字段 - 全局禁止 textarea 拖拽缩放并统一三端样式规则 - 会员列表增加状态 tab 筛选及启用/禁用双向切换功能 - 删除未使用的 getSpecificationList API 方法
This commit is contained in:
18
README.md
18
README.md
@@ -162,6 +162,8 @@ lilishop-ui
|
||||
|
||||
## 8. 升级计划
|
||||
|
||||
> 更新日期:2026-11-03,依据各端近期提交及当前代码实现同步。
|
||||
|
||||
### ✅ 已完成特性
|
||||
| 功能模块 | 功能模块 |
|
||||
| :--- | :--- |
|
||||
@@ -170,17 +172,21 @@ lilishop-ui
|
||||
| ✅ 置顶评论 | ✅ 客户等级体系 |
|
||||
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
|
||||
| ✅ 卡密商品 | ✅ 二级分销 |
|
||||
| ✅ 定时任务管理 | ✅ 预约服务商品 |
|
||||
| ✅ 定时任务管理(数据库配置驱动) | ✅ 预约服务商品 |
|
||||
| ✅ 主题色管理 | ✅ 新增无需配送发货方式 |
|
||||
| ✅ 多维统计(营业/商品/会员/积分/储值/营销/分销) | ✅ 经营报表(店铺业绩/商品同比环比/订单明细/销售汇总) |
|
||||
| ✅ 分类页多模板设置 | ✅ 移动端楼层装修组件库 |
|
||||
| ✅ PC端楼层装修重构 | ✅ 全站统一顶栏 |
|
||||
| ✅ PC端店铺独立列表 | ✅ 敏感词场景化过滤 |
|
||||
| ✅ PC端楼层装修重构(自适应高度 / 可视化热区绘制) | ✅ 全站统一顶栏 / 商品与店铺双模式搜索 |
|
||||
| ✅ PC端店铺独立列表 | ✅ 敏感词场景化过滤与分组开关 |
|
||||
| ✅ 店铺结算汇总 / 结算台账 | ✅ 支付方式汇总 / 钱包流水 |
|
||||
| ✅ 平台经营报表 / 商家周期财务汇总 | ✅ 财务报表与流水 Excel 导出 / 结算单批量下载 |
|
||||
| ✅ 会员消息四渠道异步通知(站内信 / 短信 / 公众号 / 小程序) | ✅ 消息模板多渠道矩阵管理(模板配置 / 渠道开关) |
|
||||
| ✅ 平台订单详情优化 / 标准 A4 出库发货单打印 | ✅ 售后详情展示优化 / 移动端售后入口整合至订单列表 |
|
||||
| ✅ 商品详情视频内嵌相册 | ✅ 评价管理增强(回复状态 / 图片筛选 / 评价分级) |
|
||||
| ✅ PC会员中心订单快捷入口 / 资产卡片 | ✅ 平台表单抽屉化 / 文章独立编辑页 |
|
||||
|
||||
### 🔜 近期规划中
|
||||
- 🔜 店铺结算汇总 & 结算台账
|
||||
- 🔜 支付方式汇总 & 钱包流水 & 财务汇总
|
||||
- 🔜 微信小店对接
|
||||
- 🔜 抖店对接
|
||||
- 🔜 微信小程序原生重构与体验对齐
|
||||
- 🔜 微信小程序原生重构与体验对齐(持续完善;已接入预约交易、分类多模板与首页楼层装修)
|
||||
- 🔜 OpenAPI 开放平台与开发者中心
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
|
||||
.el-textarea__inner {
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
:root {
|
||||
--el-color-primary: var(--theme-color, #ff3c2a);
|
||||
--el-color-danger: var(--theme-color, #ff3c2a);
|
||||
|
||||
@@ -43,25 +43,20 @@
|
||||
</div>
|
||||
<div class="information footer-bottmom">
|
||||
<div class="icp">
|
||||
<li v-if="config.icpCard">
|
||||
<a :href='"https://tsm.miit.gov.cn/dxxzsp/xkz/xkzgl/resource/qiyesearch.jsp?num="+config.icpCard+"&type=xuke"' target="_blank">
|
||||
{{config.icpCard}}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="config.icpMessage" class="footer-bottmom">
|
||||
<li v-if="icp" class="footer-bottmom">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank">
|
||||
{{config.icpMessage}}
|
||||
{{icp}}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<a class="flex " :href="config.company.href">
|
||||
<img class="zhizhao" src="@/assets/images/zhizhao.jpg" mode="" />{{config.company.name}}
|
||||
<a v-if="companyName" class="flex " :href="companyUrl || undefined">
|
||||
<img class="zhizhao" src="@/assets/images/zhizhao.jpg" mode="" />{{companyName}}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<div class="copyright">
|
||||
<p>Copyright © {{year}} {{config.title}}</p>
|
||||
<p>Copyright © {{year}} {{copyrightOwner}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -71,11 +66,12 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import config from '@/config'
|
||||
import { useSiteSettings } from '@/composables/useSiteSettings'
|
||||
|
||||
defineOptions({ name: 'Footer' })
|
||||
|
||||
const router = useRouter()
|
||||
const { companyName, companyUrl, icp, copyrightOwner } = useSiteSettings()
|
||||
|
||||
const guideArr = ref([
|
||||
// 导航链接
|
||||
|
||||
33
buyer/src/composables/useSiteSettings.js
Normal file
33
buyer/src/composables/useSiteSettings.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
import config from "@/config";
|
||||
|
||||
// 同页的页头、页脚共享请求;每次重新进入页面时刷新,避免旧缓存覆盖后台设置。
|
||||
const settings = ref(null);
|
||||
let pendingRequest;
|
||||
|
||||
export function loadSiteSettings() {
|
||||
if (!pendingRequest) {
|
||||
pendingRequest = getBaseSite()
|
||||
.then((res) => {
|
||||
if (res.success && res.result?.settingValue) {
|
||||
settings.value = JSON.parse(res.result.settingValue);
|
||||
}
|
||||
return settings.value;
|
||||
})
|
||||
.catch(() => settings.value)
|
||||
.finally(() => { pendingRequest = null; });
|
||||
}
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
export function useSiteSettings() {
|
||||
onMounted(loadSiteSettings);
|
||||
const siteName = computed(() => settings.value?.siteName || config.title);
|
||||
const companyName = computed(() => settings.value ? settings.value.companyName || "" : config.company?.name || "");
|
||||
const companyUrl = computed(() => settings.value ? settings.value.companyUrl || "" : config.company?.href || "");
|
||||
const website = computed(() => settings.value ? settings.value.website || "" : config.website || "");
|
||||
const icp = computed(() => settings.value ? settings.value.icp || "" : config.icpMessage || "");
|
||||
const copyrightOwner = computed(() => settings.value?.copyrightOwner?.trim() || siteName.value);
|
||||
return { settings, siteName, companyName, companyUrl, website, icp, copyrightOwner };
|
||||
}
|
||||
@@ -37,7 +37,7 @@ a {
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
resize: none !important;
|
||||
outline: 0;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
@@ -59,4 +59,4 @@ textarea {
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ function finished() {
|
||||
});
|
||||
}
|
||||
|
||||
// 父组件(member/index、memberRecycle、memberDetail、orderDetail、shopOperation)通过 ref 调用 open() 打开弹窗
|
||||
// 父组件(member/index、memberDetail、orderDetail、shopOperation)通过 ref 调用 open() 打开弹窗
|
||||
defineExpose({ open });
|
||||
</script>
|
||||
|
||||
|
||||
33
manager/src/composables/useSiteSettings.js
Normal file
33
manager/src/composables/useSiteSettings.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
import config from "@/config";
|
||||
|
||||
// 同页的页头、页脚共享请求;每次重新进入页面时刷新,避免旧缓存覆盖后台设置。
|
||||
const settings = ref(null);
|
||||
let pendingRequest;
|
||||
|
||||
export function loadSiteSettings() {
|
||||
if (!pendingRequest) {
|
||||
pendingRequest = getBaseSite()
|
||||
.then((res) => {
|
||||
if (res.success && res.result?.settingValue) {
|
||||
settings.value = JSON.parse(res.result.settingValue);
|
||||
}
|
||||
return settings.value;
|
||||
})
|
||||
.catch(() => settings.value)
|
||||
.finally(() => { pendingRequest = null; });
|
||||
}
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
export function useSiteSettings() {
|
||||
onMounted(loadSiteSettings);
|
||||
const siteName = computed(() => settings.value?.siteName || config.title);
|
||||
const companyName = computed(() => settings.value ? settings.value.companyName || "" : config.company?.name || "");
|
||||
const companyUrl = computed(() => settings.value ? settings.value.companyUrl || "" : config.company?.href || "");
|
||||
const website = computed(() => settings.value ? settings.value.website || "" : config.website || "");
|
||||
const icp = computed(() => settings.value ? settings.value.icp || "" : config.icpMessage || "");
|
||||
const copyrightOwner = computed(() => settings.value?.copyrightOwner?.trim() || siteName.value);
|
||||
return { settings, siteName, companyName, companyUrl, website, icp, copyrightOwner };
|
||||
}
|
||||
@@ -8,6 +8,8 @@ let util = {};
|
||||
/** 已弃用、前端未实现或已移除的菜单 frontRoute,注册路由时跳过 */
|
||||
const DEPRECATED_FRONT_ROUTES = new Set([
|
||||
"goods/goods-manage/spec",
|
||||
"member/list/memberRecycle",
|
||||
"order/order/fictitiousOrderList",
|
||||
]);
|
||||
|
||||
function isDeprecatedMenu(menu) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import { defineStore } from "pinia";
|
||||
import { otherRouter } from "@/router/router";
|
||||
import i18n from "@/locale";
|
||||
|
||||
const removedPageNames = new Set(["memberRecycle", "fictitiousOrderList"]);
|
||||
|
||||
// 原 vuex app 模块 + 根 state(openStyleStore/openStoreId/notices)合并
|
||||
// mutation 全部转为同名 action;两个例外:
|
||||
// - pageOpenedList mutation 与 state 同名,改名 updateOpenedList
|
||||
@@ -73,7 +75,8 @@ export const useAppStore = defineStore("app", {
|
||||
},
|
||||
initCachePage() {
|
||||
if (localStorage.cachePage) {
|
||||
this.cachePage = JSON.parse(localStorage.cachePage);
|
||||
this.cachePage = JSON.parse(localStorage.cachePage).filter((name) => !removedPageNames.has(name));
|
||||
localStorage.cachePage = JSON.stringify(this.cachePage);
|
||||
}
|
||||
},
|
||||
removeTag(name) {
|
||||
@@ -120,8 +123,9 @@ export const useAppStore = defineStore("app", {
|
||||
},
|
||||
setOpenedList() {
|
||||
this.pageOpenedList = localStorage.pageOpenedList
|
||||
? JSON.parse(localStorage.pageOpenedList)
|
||||
? JSON.parse(localStorage.pageOpenedList).filter((page) => !removedPageNames.has(page.name))
|
||||
: [otherRouter.children[0]];
|
||||
localStorage.pageOpenedList = JSON.stringify(this.pageOpenedList);
|
||||
},
|
||||
setAvatarPath(path) {
|
||||
localStorage.avatorImgPath = path;
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
|
||||
.el-textarea__inner {
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
/* Element Plus 主题覆盖(对齐原 iView 主色) */
|
||||
:root {
|
||||
// --el-color-primary: #ff5c58;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<li>多店铺管理与经营数据分析</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="brand-copyright">Copyright © {{ year }} Lilishop</p>
|
||||
<p class="brand-copyright">Copyright © {{ year }} {{ copyrightOwner }}</p>
|
||||
</div>
|
||||
|
||||
<div class="login-panel" @keyup.enter="submitLogin">
|
||||
@@ -94,11 +94,13 @@ import verify from "@/components/verify";
|
||||
import { setStore } from "@/libs/storage";
|
||||
import { md5 } from "@/utils/md5.js";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
import { useSiteSettings } from "@/composables/useSiteSettings";
|
||||
|
||||
const router = useRouter();
|
||||
const appStore = useAppStore();
|
||||
|
||||
const year = ref(new Date().getFullYear());
|
||||
const { copyrightOwner } = useSiteSettings();
|
||||
const loading = ref(false);
|
||||
const form = reactive({
|
||||
username: "",
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
<template>
|
||||
<div class="foot">
|
||||
<el-row justify="space-around" class="help">
|
||||
<el-row v-if="website" justify="space-around" class="help">
|
||||
<a class="item" :href="website" target="_blank">帮助</a>
|
||||
<a class="item" :href="website" target="_blank">隐私</a>
|
||||
<a class="item" :href="website" target="_blank">条款</a>
|
||||
</el-row>
|
||||
<el-row justify="center" class="copyright">
|
||||
Copyright © {{ year }} - Present
|
||||
<a :href="website" class="href" target="_blank" style="margin: 0 5px">{{ config.title }}</a>
|
||||
Copyright © {{ year }}
|
||||
<a :href="website || undefined" class="href" target="_blank" style="margin: 0 5px">{{ copyrightOwner }}</a>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
import config from "@/config/index";
|
||||
import { useSiteSettings } from "@/composables/useSiteSettings";
|
||||
|
||||
const year = ref(new Date().getFullYear());
|
||||
// 官网地址优先取接口下发缓存,兜底用本地配置
|
||||
const website = ref(localStorage.getItem("website") || config.website);
|
||||
const { website, copyrightOwner } = useSiteSettings();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
import { loadSiteSettings } from "@/composables/useSiteSettings";
|
||||
|
||||
const domainLogo = ref("");
|
||||
|
||||
@@ -24,11 +24,9 @@ function applyFavicon(href) {
|
||||
}
|
||||
|
||||
function getSite() {
|
||||
getBaseSite().then((res) => {
|
||||
if (res.success && res.result && res.result.settingValue) {
|
||||
const { domainLogo: logo, domainIcon, siteName, website } = JSON.parse(
|
||||
res.result.settingValue
|
||||
);
|
||||
loadSiteSettings().then((data) => {
|
||||
if (data) {
|
||||
const { domainLogo: logo, domainIcon, siteName, website } = data;
|
||||
domainLogo.value = logo || "";
|
||||
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||
localStorage.setItem("icontitle_expiration_time", expirationTime);
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<el-tabs v-if="!selectedMember" v-model="memberStatus" @tab-change="handleStatusChange">
|
||||
<el-tab-pane label="全部" name="ALL" />
|
||||
<el-tab-pane label="启用" name="OPEN" />
|
||||
<el-tab-pane label="禁用" name="CLOSE" />
|
||||
</el-tabs>
|
||||
<div v-if="!selectedMember" class="operation padding-row">
|
||||
<el-button type="primary" @click="addMember">添加会员</el-button>
|
||||
<el-button
|
||||
@@ -88,6 +93,11 @@
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" label="客户名称" min-width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="nickName" label="会员昵称" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column v-if="!selectedMember" label="状态" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.disabled ? 'success' : 'info'">{{ row.disabled ? '启用' : '禁用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="联系方式" min-width="130">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">{{ row.mobile || "" }}</span>
|
||||
@@ -142,7 +152,7 @@
|
||||
<el-dropdown-item command="edit">编辑会员</el-dropdown-item>
|
||||
<el-dropdown-item command="increaseWallet">增加余额</el-dropdown-item>
|
||||
<el-dropdown-item command="updatePoint">修改积分</el-dropdown-item>
|
||||
<el-dropdown-item command="disabled">禁用会员</el-dropdown-item>
|
||||
<el-dropdown-item command="disabled">{{ row.disabled ? '禁用会员' : '启用会员' }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
@@ -359,6 +369,7 @@ const addMemberForm = ref({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const memberStatus = ref("ALL");
|
||||
const searchForm = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
@@ -366,7 +377,7 @@ const searchForm = ref({
|
||||
id: "",
|
||||
username: "",
|
||||
mobile: "",
|
||||
disabled: "OPEN",
|
||||
disabled: props.selectedMember ? "OPEN" : "",
|
||||
groupId: "",
|
||||
});
|
||||
const picModelFlag = ref(false);
|
||||
@@ -586,6 +597,11 @@ function handleSearch() {
|
||||
searchForm.value.pageSize = 20;
|
||||
getData();
|
||||
}
|
||||
function handleStatusChange(status) {
|
||||
searchForm.value.disabled = status === "ALL" ? "" : status;
|
||||
searchForm.value.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
function editPerm(val) {
|
||||
descTitle.value = `查看用户 ${val.username}`;
|
||||
descFlag.value = true;
|
||||
@@ -609,20 +625,27 @@ function getMemberInfo(id) {
|
||||
}
|
||||
});
|
||||
}
|
||||
let memberListRequestId = 0;
|
||||
function getData() {
|
||||
const requestId = ++memberListRequestId;
|
||||
loading.value = true;
|
||||
if (!props.selectedMember) {
|
||||
selectedRows.value = [];
|
||||
clearSelection();
|
||||
}
|
||||
API_Member.getMemberListData(searchForm.value).then((res) => {
|
||||
if (res.result.records) {
|
||||
loading.value = false;
|
||||
init(res.result.records);
|
||||
total.value = res.result.total;
|
||||
syncTableSelection();
|
||||
}
|
||||
});
|
||||
API_Member.getMemberListData({ ...searchForm.value })
|
||||
.then((res) => {
|
||||
if (requestId !== memberListRequestId) return;
|
||||
if (res.success && res.result) {
|
||||
init(res.result.records || []);
|
||||
total.value = res.result.total || 0;
|
||||
syncTableSelection();
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
if (requestId === memberListRequestId) loading.value = false;
|
||||
});
|
||||
}
|
||||
function openPicSelector() {
|
||||
picModelFlag.value = true;
|
||||
@@ -666,14 +689,15 @@ function detail(row) {
|
||||
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
||||
}
|
||||
function disabled(v) {
|
||||
const action = v.disabled ? "禁用" : "启用";
|
||||
let params = {
|
||||
memberIds: [v.id],
|
||||
disabled: false,
|
||||
disabled: !v.disabled,
|
||||
};
|
||||
ElMessageBox.confirm("确认禁用此会员?", "提示", { type: "warning" }).then(() => {
|
||||
ElMessageBox.confirm(`确认${action}此会员?`, "提示", { type: "warning" }).then(() => {
|
||||
API_Member.updateMemberStatus(params).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("禁用成功");
|
||||
ElMessage.success(`${action}成功`);
|
||||
getData();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,451 +0,0 @@
|
||||
<template>
|
||||
<div class="member-recycle-page search">
|
||||
<!-- 顶部筛选条件表单 -->
|
||||
<el-card shadow="never" class="filter-card">
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="76px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="客户ID" prop="id">
|
||||
<el-input
|
||||
v-model="searchForm.id"
|
||||
placeholder="请输入客户ID"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="username">
|
||||
<el-input
|
||||
v-model="searchForm.username"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="联系方式" prop="mobile">
|
||||
<el-input
|
||||
v-model="searchForm.mobile"
|
||||
placeholder="请输入手机联系方式"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-actions">
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 回收站会员表格卡片 -->
|
||||
<el-card shadow="never" class="table-card mt_10">
|
||||
<template #header>
|
||||
<div class="card-header-bar">
|
||||
<div class="header-title">
|
||||
<span>会员回收站列表</span>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<el-button size="small" @click="getData">刷新</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="table"
|
||||
v-loading="loading"
|
||||
border
|
||||
stripe
|
||||
:data="data"
|
||||
style="width: 100%"
|
||||
>
|
||||
<!-- 客户ID -->
|
||||
<el-table-column prop="id" label="客户ID" min-width="190" show-overflow-tooltip />
|
||||
|
||||
<!-- 客户名称 -->
|
||||
<el-table-column prop="username" label="客户名称" min-width="150" show-overflow-tooltip />
|
||||
|
||||
<!-- 昵称 -->
|
||||
<el-table-column prop="nickName" label="昵称" min-width="140" show-overflow-tooltip />
|
||||
|
||||
<!-- 联系方式 -->
|
||||
<el-table-column label="联系方式" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row && row.mobile ? row.mobile : "-" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 积分数量 -->
|
||||
<el-table-column label="积分数量" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row ? (row.point == void 0 ? "0" : row.point) : "0" }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<!-- 注册时间 -->
|
||||
<el-table-column prop="createTime" label="注册时间" min-width="180" align="center" />
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<el-table-column label="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<template v-if="selectedMember">
|
||||
<a class="link-text" @click="callback(row)">
|
||||
{{ row.___selected ? "已选择" : "选择" }}
|
||||
</a>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a class="link-text" @click="detail(row)">查看</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="enable(row)">启用</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="editPerm(row)">编辑</a>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 底部分页 -->
|
||||
<div class="pagination-wrap mt_10">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@current-change="changePage"
|
||||
@size-change="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 编辑会员弹窗 -->
|
||||
<el-dialog
|
||||
v-model="descFlag"
|
||||
:title="descTitle"
|
||||
width="540px"
|
||||
destroy-on-close
|
||||
class="edit-member-dialog"
|
||||
>
|
||||
<el-form ref="formValidateRef" :model="formValidate" :rules="ruleValidate" label-width="90px">
|
||||
<el-form-item label="客户ID">
|
||||
<span>{{ formValidate.id }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="头像">
|
||||
<div class="avatar-edit-box">
|
||||
<el-avatar :size="50" :src="formValidate.face || undefined" class="dialog-avatar">
|
||||
{{ (formValidate.username || "用")[0] }}
|
||||
</el-avatar>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
class="upload-btn"
|
||||
@click="openPicSelector"
|
||||
>
|
||||
修改头像
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户名称" prop="name">
|
||||
<el-input v-model="formValidate.username" style="width: 260px" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称" prop="name">
|
||||
<el-input v-model="formValidate.nickName" placeholder="请输入用户昵称" style="width: 260px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="sex">
|
||||
<el-radio-group v-model="formValidate.sex">
|
||||
<el-radio-button :value="1">男</el-radio-button>
|
||||
<el-radio-button :value="0">女</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="重置密码" prop="password">
|
||||
<el-input
|
||||
v-model="formValidate.newPassword"
|
||||
type="password"
|
||||
show-password
|
||||
placeholder="留空则保持原密码"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="生日" prop="birthday">
|
||||
<el-date-picker
|
||||
v-model="formValidate.birthday"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
placeholder="请选择生日"
|
||||
style="width: 260px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所在地" prop="mail">
|
||||
<div class="region-box">
|
||||
<span class="region-text">{{ formValidate.region || "暂无地址" }}</span>
|
||||
<el-button size="small" @click="$refs.map.open()">选择地区</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="descFlag = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmitModal">确定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="picModelFlag" width="1200px" title="选择头像图片">
|
||||
<ossManage
|
||||
ref="ossManageRef"
|
||||
@callback="callbackSelected"
|
||||
:isComponent="true"
|
||||
:initialize="picModelFlag"
|
||||
/>
|
||||
</el-dialog>
|
||||
<multipleMap ref="map" @callback="selectedRegion" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, nextTick, onMounted } from "vue";
|
||||
import * as API_Member from "@/api/member.js";
|
||||
import ossManage from "@/views/sys/oss-manage/ossManage";
|
||||
import multipleMap from "@/components/map/multiple-map";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
import { md5 } from "@/utils/md5.js";
|
||||
|
||||
defineOptions({ name: "memberRecycle" });
|
||||
|
||||
const emit = defineEmits(["callback"]);
|
||||
|
||||
const searchFormRef = ref(null);
|
||||
const table = ref(null);
|
||||
const formValidateRef = ref(null);
|
||||
const ossManageRef = ref(null);
|
||||
|
||||
const selectedMember = ref(false);
|
||||
const descTitle = ref("");
|
||||
const descFlag = ref(false);
|
||||
const loading = ref(true);
|
||||
|
||||
const searchForm = ref({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
order: "desc",
|
||||
id: "",
|
||||
username: "",
|
||||
mobile: "",
|
||||
disabled: "CLOSE",
|
||||
});
|
||||
|
||||
const picModelFlag = ref(false);
|
||||
const formValidate = ref({});
|
||||
const ruleValidate = ref({});
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
|
||||
function callback(val) {
|
||||
emit("callback", val);
|
||||
}
|
||||
|
||||
function init() {
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePage() {
|
||||
getData();
|
||||
}
|
||||
|
||||
function changePageSize() {
|
||||
searchForm.value.pageNumber = 1;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
searchForm.value.pageNumber = 1;
|
||||
searchForm.value.pageSize = 20;
|
||||
getData();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
searchForm.value.pageNumber = 1;
|
||||
searchForm.value.id = "";
|
||||
searchForm.value.username = "";
|
||||
searchForm.value.mobile = "";
|
||||
getData();
|
||||
}
|
||||
|
||||
function editPerm(val) {
|
||||
descTitle.value = `编辑停用客户 - ${val.username || "客户"}`;
|
||||
descFlag.value = true;
|
||||
getMemberInfo(val.id);
|
||||
}
|
||||
|
||||
function getMemberInfo(id) {
|
||||
API_Member.getMemberInfoData(id).then((res) => {
|
||||
if (res.result) {
|
||||
formValidate.value = res.result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getData() {
|
||||
loading.value = true;
|
||||
API_Member.getMemberListData(searchForm.value)
|
||||
.then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success && res.result) {
|
||||
data.value = res.result.records || [];
|
||||
total.value = res.result.total || 0;
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
}
|
||||
|
||||
function openPicSelector() {
|
||||
picModelFlag.value = true;
|
||||
nextTick(() => {
|
||||
if (ossManageRef.value) {
|
||||
ossManageRef.value.selectImage = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function callbackSelected(val) {
|
||||
picModelFlag.value = false;
|
||||
formValidate.value.face = val.url;
|
||||
}
|
||||
|
||||
function selectedRegion(val) {
|
||||
if (val.type === "select") {
|
||||
const paths = val.data.map((item) => item.name).join(",");
|
||||
const ids = val.data.map((item) => item.id).join(",");
|
||||
formValidate.value.region = paths;
|
||||
formValidate.value.regionId = ids;
|
||||
} else {
|
||||
formValidate.value.region = val.data.addr;
|
||||
formValidate.value.regionId = val.data.addrId;
|
||||
}
|
||||
}
|
||||
|
||||
function detail(row) {
|
||||
customRouterPush({ name: "member-detail", query: { id: row.id } });
|
||||
}
|
||||
|
||||
function enable(v) {
|
||||
let params = {
|
||||
memberIds: [v.id],
|
||||
disabled: true,
|
||||
};
|
||||
ElMessageBox.confirm(`确定启用客户 "${v.username || v.nickName || "该客户"}" 吗?`, "提示", {
|
||||
type: "warning",
|
||||
confirmButtonText: "确定启用",
|
||||
cancelButtonText: "取消",
|
||||
})
|
||||
.then(() => {
|
||||
API_Member.updateMemberStatus(params).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("启用成功");
|
||||
getData();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
function handleSubmitModal() {
|
||||
const { nickName, sex, username, face, newPassword, id, regionId, region } = formValidate.value;
|
||||
let time = new Date(formValidate.value.birthday);
|
||||
let birthday = time.getFullYear() + "-" + (time.getMonth() + 1) + "-" + time.getDate();
|
||||
let submit = {
|
||||
regionId: regionId,
|
||||
region: region,
|
||||
nickName,
|
||||
sex,
|
||||
birthday,
|
||||
face: face || "",
|
||||
id,
|
||||
};
|
||||
if (newPassword) {
|
||||
submit.password = md5(newPassword);
|
||||
}
|
||||
API_Member.updateMember(submit).then((res) => {
|
||||
if (res.result) {
|
||||
ElMessage.success("修改成功!");
|
||||
descFlag.value = false;
|
||||
init();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.member-recycle-page {
|
||||
.filter-card {
|
||||
.search-form {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 0;
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 0;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-edit-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
|
||||
.dialog-avatar {
|
||||
background-color: #f56c6c;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.region-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.region-text {
|
||||
color: #606266;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,235 +0,0 @@
|
||||
<template>
|
||||
<div class="search">
|
||||
<el-card>
|
||||
<el-form
|
||||
ref="searchFormRef"
|
||||
:model="searchForm"
|
||||
inline
|
||||
label-width="70px"
|
||||
class="search-form"
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<el-form-item label="订单号" prop="orderSn">
|
||||
<el-input
|
||||
v-model="searchForm.orderSn"
|
||||
placeholder="请输入订单号"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="会员名称" prop="buyerName">
|
||||
<el-input
|
||||
v-model="searchForm.buyerName"
|
||||
placeholder="请输入会员名称"
|
||||
clearable
|
||||
style="width: 240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="下单时间">
|
||||
<el-date-picker
|
||||
v-model="selectDate"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD"
|
||||
clearable
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
placeholder="选择起始时间"
|
||||
style="width: 240px"
|
||||
@change="selectDateRange"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" class="search-btn" @click="handleSearch">搜索</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card>
|
||||
<div class="order-tab">
|
||||
<el-tabs v-model="currentStatus" @tab-click="onStatusTabClick">
|
||||
<el-tab-pane
|
||||
v-for="(item, index) in orderStatus"
|
||||
:key="index"
|
||||
:label="item.title"
|
||||
:name="item.value"
|
||||
/>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
border
|
||||
:data="data"
|
||||
class="mt_10"
|
||||
style="width: 100%"
|
||||
@sort-change="changeSort"
|
||||
>
|
||||
<el-table-column prop="sn" label="订单号" min-width="230" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="下单时间" width="200" />
|
||||
<el-table-column label="订单来源" width="95">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row">
|
||||
<span v-if="row.clientType == 'H5'">移动端</span>
|
||||
<span v-else-if="row.clientType == 'PC'">PC端</span>
|
||||
<span v-else-if="row.clientType == 'WECHAT_MP'">小程序端</span>
|
||||
<span v-else-if="row.clientType == 'APP'">移动应用端</span>
|
||||
<span v-else>{{ row.clientType }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="memberName" label="买家名称" width="130" />
|
||||
<el-table-column label="订单金额" prop="flowPrice" min-width="120" sortable="custom">
|
||||
<template #default="{ row }">
|
||||
<priceColorScheme v-if="row" :value="row.flowPrice" :color="$mainColor" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单状态" width="95">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<el-tag v-if="row.orderStatus == 'UNPAID'" type="danger">未付款</el-tag>
|
||||
<el-tag v-else-if="row.orderStatus == 'PAID'" type="primary">已付款</el-tag>
|
||||
<el-tag v-else-if="row.orderStatus == 'COMPLETED'" type="success">已完成</el-tag>
|
||||
<el-tag v-else-if="row.orderStatus == 'TAKE'" type="warning">待核验</el-tag>
|
||||
<el-tag v-else-if="row.orderStatus == 'CANCELLED'" type="info">已关闭</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row">
|
||||
<a
|
||||
class="link-text"
|
||||
:class="{ disabled: row.orderStatus != 'UNPAID' }"
|
||||
@click="row.orderStatus == 'UNPAID' && confirmPrice(row)"
|
||||
>收款</a>
|
||||
<span class="op-split">|</span>
|
||||
<a class="link-text" @click="detail(row)">查看</a>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt_10" style="display: flex; justify-content: flex-end">
|
||||
<el-pagination
|
||||
v-model:current-page="searchForm.pageNumber"
|
||||
v-model:page-size="searchForm.pageSize"
|
||||
:page-sizes="[20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
size="small"
|
||||
@current-change="changePage"
|
||||
@size-change="changePageSize"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import * as API_Order from "@/api/order";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { customRouterPush } from "@/utils/filters";
|
||||
|
||||
defineOptions({ name: "fictitiousOrderList" });
|
||||
|
||||
const searchFormRef = ref(null);
|
||||
const tableRef = ref(null);
|
||||
|
||||
const loading = ref(true);
|
||||
const searchForm = reactive({
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
sort: "",
|
||||
order: "",
|
||||
startDate: "",
|
||||
endDate: "",
|
||||
orderType: "VIRTUAL",
|
||||
orderSn: "",
|
||||
buyerName: "",
|
||||
orderStatus: "",
|
||||
});
|
||||
const selectDate = ref(null);
|
||||
const data = ref([]);
|
||||
const total = ref(0);
|
||||
const orderStatus = ref([
|
||||
{ title: "全部", value: "" },
|
||||
{ title: "未付款", value: "UNPAID" },
|
||||
{ title: "已付款", value: "PAID" },
|
||||
{ title: "待核验", value: "TAKE" },
|
||||
{ title: "已完成", value: "COMPLETED" },
|
||||
{ title: "已关闭", value: "CANCELLED" },
|
||||
]);
|
||||
const currentStatus = ref("");
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
function init() {
|
||||
getDataList();
|
||||
}
|
||||
function changePage() {
|
||||
getDataList();
|
||||
}
|
||||
function changePageSize() {
|
||||
searchForm.pageNumber = 1;
|
||||
getDataList();
|
||||
}
|
||||
function handleSearch() {
|
||||
searchForm.pageNumber = 1;
|
||||
searchForm.pageSize = 20;
|
||||
getDataList();
|
||||
}
|
||||
function changeSort(e) {
|
||||
searchForm.sort = e.prop;
|
||||
searchForm.order = e.order === "ascending" ? "asc" : e.order === "descending" ? "desc" : "";
|
||||
getDataList();
|
||||
}
|
||||
function selectDateRange(v) {
|
||||
if (v) {
|
||||
searchForm.startDate = v[0];
|
||||
searchForm.endDate = v[1];
|
||||
}
|
||||
}
|
||||
function getDataList() {
|
||||
loading.value = true;
|
||||
API_Order.getOrderList(searchForm).then((res) => {
|
||||
loading.value = false;
|
||||
if (res.success) {
|
||||
data.value = res.result.records;
|
||||
total.value = res.result.total;
|
||||
}
|
||||
});
|
||||
}
|
||||
function confirmPrice(v) {
|
||||
ElMessageBox.confirm("您确定要收款吗?", "确认收款", { type: "warning" }).then(() => {
|
||||
API_Order.orderPay(v.sn).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("收款成功");
|
||||
getDataList();
|
||||
}
|
||||
});
|
||||
}).catch(() => {});
|
||||
}
|
||||
function detail(v) {
|
||||
let sn = v.sn;
|
||||
customRouterPush({
|
||||
name: "order-detail",
|
||||
query: { sn: sn, orderType: v.orderType },
|
||||
});
|
||||
}
|
||||
function onStatusTabClick(tab) {
|
||||
const item = tab.paneName;
|
||||
currentStatus.value = item;
|
||||
searchForm.orderStatus = item;
|
||||
getDataList();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-tab {
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<div class="order-detail-page">
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<h1>订单详情</h1>
|
||||
<p>查看订单信息、商品明细与履约进度</p>
|
||||
</div>
|
||||
<el-button class="back-btn" :icon="ArrowLeft" @click="goBack">返回列表</el-button>
|
||||
</div>
|
||||
<!-- 顶部一体化状态与履约进度卡片 -->
|
||||
<el-card shadow="never" class="top-overview-card">
|
||||
<!-- 第一层:返回导航 + 核心状态/单号 + 操作按钮组 -->
|
||||
<div class="overview-header">
|
||||
<div class="header-left">
|
||||
<el-button link class="back-btn" @click="goBack">
|
||||
<el-icon><ArrowLeft /></el-icon>
|
||||
<span>返回列表</span>
|
||||
</el-button>
|
||||
<div class="header-divider"></div>
|
||||
<el-tag
|
||||
:type="getOrderStatusTagType(orderInfo.order.orderStatus)"
|
||||
effect="dark"
|
||||
effect="light"
|
||||
class="main-status-tag"
|
||||
>
|
||||
{{ orderInfo.orderStatusValue || "加载中..." }}
|
||||
@@ -26,6 +28,7 @@
|
||||
type="primary"
|
||||
:icon="CopyDocument"
|
||||
class="copy-icon-btn"
|
||||
aria-label="复制订单编号"
|
||||
@click="copyText(orderInfo.order.sn || sn, '订单编号')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
@@ -207,6 +210,7 @@
|
||||
link
|
||||
type="primary"
|
||||
:icon="CopyDocument"
|
||||
aria-label="复制手机号"
|
||||
@click="copyText(orderInfo.order.consigneeMobile, '手机号')"
|
||||
/>
|
||||
</el-tooltip>
|
||||
@@ -349,7 +353,6 @@
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
border
|
||||
:data="data"
|
||||
class="goods-table"
|
||||
style="width: 100%"
|
||||
@@ -517,7 +520,7 @@
|
||||
<el-dialog v-model="orderCancelModal" title="取消订单" width="480px" destroy-on-close>
|
||||
<el-form ref="orderCancelFormRef" :model="orderCancelForm" label-width="90px" :rules="orderCancelValidate">
|
||||
<el-form-item label="取消原因" prop="reason">
|
||||
<el-input v-model="orderCancelForm.reason" type="textarea" :rows="3" placeholder="请输入取消订单的具体原因" />
|
||||
<el-input v-model="orderCancelForm.reason" type="textarea" :rows="3" resize="none" placeholder="请输入取消订单的具体原因" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
@@ -1403,510 +1406,229 @@ onMounted(() => {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.order-detail-page {
|
||||
--detail-ink: #1e293b;
|
||||
--detail-muted: #748196;
|
||||
--detail-line: #e8edf4;
|
||||
width: 100%;
|
||||
padding-bottom: 28px;
|
||||
max-width: 1680px;
|
||||
margin: 0 auto;
|
||||
padding: 8px 12px 32px;
|
||||
box-sizing: border-box;
|
||||
color: var(--detail-ink);
|
||||
|
||||
// 顶部一体化状态与履约步骤卡片
|
||||
.top-overview-card {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background: #fff;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.03);
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 22px 18px;
|
||||
}
|
||||
|
||||
.overview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
|
||||
.back-btn {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
padding: 4px 2px;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-divider {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: #cbd5e1;
|
||||
}
|
||||
|
||||
.main-status-tag {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
padding: 0 12px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.sn-title-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
.sn-label {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.sn-value {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.copy-icon-btn {
|
||||
margin-left: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.meta-tags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// 紧凑型横向履约进度条
|
||||
.fulfillment-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
padding: 14px 24px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 8px;
|
||||
gap: 12px;
|
||||
|
||||
.step-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&:last-child {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.step-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-circle {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
border: 2px solid #cbd5e1;
|
||||
background: #fff;
|
||||
color: #94a3b8;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.step-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
|
||||
.step-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.step-desc {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.3;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
|
||||
}
|
||||
}
|
||||
|
||||
.step-connector {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: #e2e8f0;
|
||||
margin: 0 20px;
|
||||
border-radius: 2px;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
// 已完成状态
|
||||
&.is-done {
|
||||
.step-circle {
|
||||
border-color: #10b981;
|
||||
background: #ecfdf5;
|
||||
color: #10b981;
|
||||
}
|
||||
.step-text .step-title {
|
||||
color: #0f172a;
|
||||
}
|
||||
.step-text .step-desc {
|
||||
color: #10b981;
|
||||
}
|
||||
.step-connector {
|
||||
background: #10b981;
|
||||
}
|
||||
}
|
||||
|
||||
// 当前进行中状态
|
||||
&.is-active {
|
||||
.step-circle {
|
||||
border-color: #3b82f6;
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.14);
|
||||
}
|
||||
.step-text .step-title {
|
||||
color: #1e293b;
|
||||
font-weight: 700;
|
||||
}
|
||||
.step-text .step-desc {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
// 已取消状态
|
||||
&.is-cancelled {
|
||||
.step-circle {
|
||||
border-color: #e2e8f0;
|
||||
background: #f1f5f9;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 中部三列等高卡片网格
|
||||
.info-grid-row {
|
||||
margin-bottom: 16px;
|
||||
.page-heading {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
:deep(.el-col) {
|
||||
display: flex;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-bottom: 22px;
|
||||
h1 { margin: 0; font-size: 24px; font-weight: 650; line-height: 34px; letter-spacing: .5px; }
|
||||
p { margin: 5px 0 0; color: var(--detail-muted); font-size: 13px; line-height: 22px; }
|
||||
.back-btn { border-radius: 8px; color: #526078; }
|
||||
}
|
||||
|
||||
.section-card {
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
.top-overview-card, .section-card {
|
||||
border: 1px solid var(--detail-line);
|
||||
border-radius: 12px;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.03);
|
||||
width: 100%;
|
||||
box-shadow: 0 3px 12px rgba(31, 51, 82, .025);
|
||||
}
|
||||
|
||||
&.equal-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
.top-overview-card {
|
||||
margin-bottom: 20px;
|
||||
background: linear-gradient(115deg, #f5f9ff 0, #fff 58%);
|
||||
:deep(.el-card__body) { padding: 24px 26px; }
|
||||
}
|
||||
.overview-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
}
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
.main-status-tag { height: 34px; padding: 0 14px; border-radius: 7px; font-size: 15px; font-weight: 600; }
|
||||
.sn-title-group { display: flex; align-items: center; min-width: 0; font-size: 13px; }
|
||||
.sn-label { color: var(--detail-muted); white-space: nowrap; }
|
||||
.sn-value { color: #22324b; font-size: 16px; font-weight: 650; overflow-wrap: anywhere; }
|
||||
.copy-icon-btn { margin-left: 4px; }
|
||||
.meta-tags { display: flex; gap: 6px; }
|
||||
.meta-tags .el-tag { background: #fff; border-color: #e1e8f0; color: #748196; border-radius: 4px; }
|
||||
}
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
.el-button { margin: 0; border-radius: 7px; font-size: 13px; }
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-card__header) {
|
||||
padding: 13px 18px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
:deep(.el-card__body) {
|
||||
padding: 16px 18px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
.fulfillment-stepper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 24px;
|
||||
padding: 24px 8px 2px;
|
||||
border-top: 1px solid #e5edf8;
|
||||
gap: 12px;
|
||||
.step-item { display: flex; align-items: center; flex: 1; min-width: 0; }
|
||||
.step-item:last-child { flex: 0 0 auto; }
|
||||
.step-node { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||||
.step-circle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
border: 1px solid #dce3ed;
|
||||
background: #f7f9fc;
|
||||
color: #8b98ab;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
|
||||
.el-icon {
|
||||
font-size: 16px;
|
||||
color: #ff6a00;
|
||||
}
|
||||
|
||||
&.justify-between {
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.flex-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
margin-left: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.step-title { color: #8290a4; font-size: 14px; font-weight: 600; line-height: 22px; }
|
||||
.step-desc { margin-top: 4px; color: #8b98ab; font-size: 12px; line-height: 18px; font-variant-numeric: tabular-nums; }
|
||||
.step-connector { flex: 1; height: 2px; min-width: 16px; margin: 0 24px; background: #e8edf4; }
|
||||
.is-done {
|
||||
.step-circle { color: #20a684; background: #eaf8f2; border-color: #c7eadd; }
|
||||
.step-title { color: #45566b; }
|
||||
.step-connector { background: #b9e3d5; }
|
||||
}
|
||||
.is-active {
|
||||
.step-circle { background: #3875ed; border-color: #3875ed; color: #fff; box-shadow: 0 0 0 5px #eaf1ff; }
|
||||
.step-title, .step-desc { color: #326bdb; }
|
||||
}
|
||||
.is-cancelled .step-circle { color: #a3adbb; background: #f1f4f8; }
|
||||
}
|
||||
|
||||
// 水平对齐键值对列表
|
||||
.info-grid-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 16px;
|
||||
margin-bottom: 20px;
|
||||
:deep(.el-col) { display: flex; }
|
||||
}
|
||||
.section-card {
|
||||
width: 100%;
|
||||
&.equal-card { display: flex; flex-direction: column; height: 100%; }
|
||||
&.equal-card :deep(.el-card__body) { flex: 1; }
|
||||
:deep(.el-card__header) { padding: 18px 22px; background: #fff; border-bottom: 1px solid var(--detail-line); }
|
||||
:deep(.el-card__body) { padding: 20px 22px; }
|
||||
.card-title, .flex-title { display: flex; align-items: center; gap: 10px; }
|
||||
.card-title { min-height: 24px; color: #25354b; font-size: 15px; font-weight: 600; }
|
||||
.card-title > .el-icon, .flex-title > .el-icon {
|
||||
box-sizing: content-box;
|
||||
padding: 6px;
|
||||
font-size: 16px;
|
||||
color: #4c7bdd;
|
||||
background: #eff4ff;
|
||||
border-radius: 7px;
|
||||
}
|
||||
.justify-between { justify-content: space-between; }
|
||||
.count-badge { margin-left: 4px; font-size: 12px; font-weight: 400; color: #748196; border: 0; background: #f3f5f9; }
|
||||
}
|
||||
.kv-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.kv-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-size: 13px;
|
||||
line-height: 20px;
|
||||
|
||||
.kv-label {
|
||||
width: 82px;
|
||||
flex-shrink: 0;
|
||||
color: #64748b;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.kv-val {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
word-break: break-all;
|
||||
|
||||
&.address-val {
|
||||
line-height: 20px;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
&.flex-col {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.member-name-text {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
.sub-pay-method {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.form-ans-line {
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
line-height: 20px;
|
||||
|
||||
.f-label {
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
}
|
||||
gap: 17px;
|
||||
.kv-row { display: flex; align-items: flex-start; gap: 12px; font-size: 13px; line-height: 22px; }
|
||||
.kv-label { width: 76px; flex-shrink: 0; color: var(--detail-muted); }
|
||||
.kv-val { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
||||
.address-val { line-height: 23px; }
|
||||
.flex-col { flex-direction: column; align-items: flex-start; gap: 4px; }
|
||||
.member-name-text { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.sub-pay-method, .f-label { font-size: 12px; color: var(--detail-muted); }
|
||||
.form-ans-line { font-size: 13px; line-height: 22px; }
|
||||
}
|
||||
|
||||
// 商品表格与底部结算卡
|
||||
.goods-section-card {
|
||||
margin-bottom: 0;
|
||||
|
||||
.goods-table {
|
||||
:deep(th.el-table__cell) {
|
||||
background-color: #f8fafc;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
.goods-table {
|
||||
--el-table-border-color: #edf1f6;
|
||||
--el-table-header-bg-color: #f7f9fc;
|
||||
--el-table-row-hover-bg-color: #f8faff;
|
||||
color: #43516a;
|
||||
:deep(th.el-table__cell) { height: 46px; background: #f7f9fc; color: #6b788d; font-size: 12px; font-weight: 500; }
|
||||
:deep(td.el-table__cell) { padding: 18px 0; }
|
||||
:deep(.cell) { padding-left: 16px; padding-right: 16px; }
|
||||
}
|
||||
|
||||
// 商品列表单元格
|
||||
.goods-item-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 4px 0;
|
||||
|
||||
.goods-thumb {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.goods-name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
line-height: 18px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.goods-specs {
|
||||
margin-top: 6px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
|
||||
.spec-tag {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
background: #f1f5f9;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
gap: 16px;
|
||||
.goods-thumb { width: 68px; height: 68px; border-radius: 8px; border: 1px solid #e8edf4; flex-shrink: 0; }
|
||||
.goods-info { flex: 1; min-width: 0; }
|
||||
.goods-name { color: #26364d; font-size: 14px; font-weight: 600; line-height: 22px; overflow-wrap: anywhere; }
|
||||
.goods-specs { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.spec-tag { font-size: 12px; color: #7b8799; background: #f4f6f9; padding: 2px 8px; border-radius: 4px; }
|
||||
}
|
||||
|
||||
.sub-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.el-icon {
|
||||
color: #ff6a00;
|
||||
}
|
||||
}
|
||||
|
||||
// 商品清单底部金额汇总排版
|
||||
.sub-header-title { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 14px; font-weight: 600; }
|
||||
.sub-header-title .el-icon { color: #4c7bdd; }
|
||||
.goods-summary-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
|
||||
.summary-rows {
|
||||
width: 340px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #f1f5f9;
|
||||
border-radius: 8px;
|
||||
padding: 14px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.summary-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
|
||||
.label {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #1e293b;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&.final-total {
|
||||
padding-top: 4px;
|
||||
align-items: baseline;
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.final-price {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
height: 1px;
|
||||
background: #e2e8f0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
}
|
||||
margin-top: 24px;
|
||||
.summary-rows { width: 360px; max-width: 100%; padding: 20px 24px; box-sizing: border-box; background: #f7f9fc; border: 1px solid #edf1f7; border-radius: 10px; display: flex; flex-direction: column; gap: 12px; }
|
||||
.summary-item { display: flex; align-items: center; justify-content: space-between; gap: 16px; font-size: 13px; }
|
||||
.label { color: var(--detail-muted); }
|
||||
.value { font-weight: 500; }
|
||||
.summary-divider { height: 1px; background: #e1e7f0; margin: 4px 0; }
|
||||
.final-total { align-items: baseline; }
|
||||
.final-total .label { color: #334155; font-size: 14px; font-weight: 600; }
|
||||
.final-price { font-size: 26px; font-weight: 650; white-space: nowrap; letter-spacing: -.5px; }
|
||||
}
|
||||
|
||||
.addr-select-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.text-danger { color: #ef4444; }
|
||||
.text-success { color: #10b981; }
|
||||
.text-warning { color: #f59e0b; }
|
||||
.text-primary { color: #2563eb; }
|
||||
.text-gray { color: #94a3b8; }
|
||||
.text-dark { color: #0f172a; }
|
||||
.addr-select-line { display: flex; align-items: center; gap: 10px; }
|
||||
.text-danger { color: #e45454; }
|
||||
.text-success { color: #20a684; }
|
||||
.text-warning { color: #d9972b; }
|
||||
.text-primary { color: #3875ed; }
|
||||
.text-gray { color: #8b98ab; }
|
||||
.text-dark { color: #1e293b; }
|
||||
.font-bold { font-weight: 600; }
|
||||
.font-mono { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace; }
|
||||
.font-mono { font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', monospace; font-variant-numeric: tabular-nums; }
|
||||
.mt_16 { margin-top: 16px; }
|
||||
.mb_10 { margin-bottom: 10px; }
|
||||
.pr_12 { padding-right: 12px; }
|
||||
|
||||
@media (max-width: 1440px) {
|
||||
padding: 6px 4px 28px;
|
||||
.overview-header { align-items: flex-start; }
|
||||
.header-actions { width: 100%; padding-top: 16px; border-top: 1px solid #e5edf8; }
|
||||
.fulfillment-stepper { margin-top: 20px; }
|
||||
.section-card :deep(.el-card__header), .section-card :deep(.el-card__body) { padding-left: 18px; padding-right: 18px; }
|
||||
.kv-list .kv-row { gap: 8px; }
|
||||
.kv-list .kv-label { width: 70px; }
|
||||
.fulfillment-stepper .step-connector { margin: 0 16px; }
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.info-grid-row :deep(.el-col) { width: 100%; max-width: 100%; flex: 0 0 100%; }
|
||||
.fulfillment-stepper .step-node { flex-direction: column; align-items: flex-start; gap: 10px; }
|
||||
.fulfillment-stepper .step-item { align-items: flex-start; }
|
||||
.fulfillment-stepper .step-connector { margin-top: 17px; }
|
||||
.fulfillment-stepper .step-desc { max-width: 120px; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
padding: 4px 0 20px;
|
||||
.page-heading { margin-bottom: 16px; }
|
||||
.page-heading h1 { font-size: 21px; }
|
||||
.page-heading p { max-width: 190px; }
|
||||
.top-overview-card :deep(.el-card__body) { padding: 18px 16px; }
|
||||
.header-left { gap: 10px; }
|
||||
.header-left .sn-title-group { width: 100%; }
|
||||
.header-left .sn-value { font-size: 13px; }
|
||||
.fulfillment-stepper { align-items: stretch; flex-direction: column; gap: 20px; padding: 20px 4px 0; }
|
||||
.fulfillment-stepper .step-item { flex: auto; }
|
||||
.fulfillment-stepper .step-node { flex-direction: row; align-items: center; }
|
||||
.fulfillment-stepper .step-desc { max-width: none; }
|
||||
.fulfillment-stepper .step-connector { display: none; }
|
||||
.goods-summary-footer .summary-rows { width: 100%; padding: 18px; }
|
||||
.goods-summary-footer .final-price { font-size: 23px; }
|
||||
.section-card .flex-title { flex-wrap: wrap; }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -49,6 +49,10 @@
|
||||
<el-form-item label="ICP备案号" prop="icp">
|
||||
<el-input v-model="formValidate.icp" />
|
||||
</el-form-item>
|
||||
<el-form-item label="版权归属" prop="copyrightOwner">
|
||||
<el-input v-model="formValidate.copyrightOwner" placeholder="留空时使用商城名称" maxlength="100" />
|
||||
<span class="field-desc">仅填写版权归属名称,Copyright © 和当前年份由系统自动显示</span>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="submit-bar">
|
||||
@@ -91,6 +95,7 @@ const formValidate = ref({
|
||||
buyerSideLogo: "",
|
||||
companyName: "",
|
||||
companyUrl: "",
|
||||
copyrightOwner: "",
|
||||
domainLogo: "",
|
||||
icp: "",
|
||||
storeSideLogo: "",
|
||||
@@ -145,8 +150,9 @@ function setupSetting() {
|
||||
}
|
||||
function init() {
|
||||
result.value = JSON.parse(props.res);
|
||||
formValidate.value = { ...result.value };
|
||||
formValidate.value = { copyrightOwner: "", ...result.value };
|
||||
Object.keys(result.value).forEach((item) => {
|
||||
if (item === "copyrightOwner") return;
|
||||
ruleValidate[item] = [
|
||||
{
|
||||
required: true,
|
||||
|
||||
@@ -69,10 +69,6 @@ export const saveCategoryBrand = (category_id, params) => {
|
||||
export const getBrandListData = params => {
|
||||
return getRequest("/goods/brand/all", params);
|
||||
};
|
||||
// 获取所有可用规格
|
||||
export const getSpecificationList = params => {
|
||||
return getRequest("/goods/spec/all", params);
|
||||
};
|
||||
|
||||
// 获取店铺分类列表数据
|
||||
export const getAllList = id => {
|
||||
|
||||
33
seller/src/composables/useSiteSettings.js
Normal file
33
seller/src/composables/useSiteSettings.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
import config from "@/config";
|
||||
|
||||
// 同页的页头、页脚共享请求;每次重新进入页面时刷新,避免旧缓存覆盖后台设置。
|
||||
const settings = ref(null);
|
||||
let pendingRequest;
|
||||
|
||||
export function loadSiteSettings() {
|
||||
if (!pendingRequest) {
|
||||
pendingRequest = getBaseSite()
|
||||
.then((res) => {
|
||||
if (res.success && res.result?.settingValue) {
|
||||
settings.value = JSON.parse(res.result.settingValue);
|
||||
}
|
||||
return settings.value;
|
||||
})
|
||||
.catch(() => settings.value)
|
||||
.finally(() => { pendingRequest = null; });
|
||||
}
|
||||
return pendingRequest;
|
||||
}
|
||||
|
||||
export function useSiteSettings() {
|
||||
onMounted(loadSiteSettings);
|
||||
const siteName = computed(() => settings.value?.siteName || config.title);
|
||||
const companyName = computed(() => settings.value ? settings.value.companyName || "" : config.company?.name || "");
|
||||
const companyUrl = computed(() => settings.value ? settings.value.companyUrl || "" : config.company?.href || "");
|
||||
const website = computed(() => settings.value ? settings.value.website || "" : config.website || "");
|
||||
const icp = computed(() => settings.value ? settings.value.icp || "" : config.icpMessage || "");
|
||||
const copyrightOwner = computed(() => settings.value?.copyrightOwner?.trim() || siteName.value);
|
||||
return { settings, siteName, companyName, companyUrl, website, icp, copyrightOwner };
|
||||
}
|
||||
@@ -113,9 +113,9 @@
|
||||
z-index: 4000 !important;
|
||||
}
|
||||
|
||||
/* 全局禁止 textarea 拖拽改变大小 */
|
||||
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
|
||||
.el-textarea__inner {
|
||||
resize: none;
|
||||
resize: none !important;
|
||||
}
|
||||
|
||||
/* 统一空状态:表格 / 树 / 列表无数据时的展示(未自定义 #empty 插槽的页面均生效) */
|
||||
|
||||
@@ -99,15 +99,15 @@
|
||||
<div class="login-btn"><a @click="$router.push('login')">前往登录</a></div>
|
||||
</div>
|
||||
<div class="foot">
|
||||
<el-row justify="space-around" class="help">
|
||||
<a class="item" href="https://pickmall.cn/" target="_blank">帮助</a>
|
||||
<a class="item" href="https://pickmall.cn/" target="_blank">隐私</a>
|
||||
<a class="item" href="https://pickmall.cn/" target="_blank">条款</a>
|
||||
<el-row v-if="website" justify="space-around" class="help">
|
||||
<a class="item" :href="website" target="_blank">帮助</a>
|
||||
<a class="item" :href="website" target="_blank">隐私</a>
|
||||
<a class="item" :href="website" target="_blank">条款</a>
|
||||
</el-row>
|
||||
<el-row justify="center" class="copyright">
|
||||
Copyright © {{ year }} - Present
|
||||
<a href="https://pickmall.cn/" target="_blank" style="margin: 0 5px">{{
|
||||
siteName
|
||||
Copyright © {{ year }}
|
||||
<a :href="website || undefined" target="_blank" style="margin: 0 5px">{{
|
||||
copyrightOwner
|
||||
}}</a>
|
||||
版权所有
|
||||
</el-row>
|
||||
@@ -124,7 +124,7 @@ import { md5 } from "@/utils/md5.js";
|
||||
import * as RegExp from "@/utils/regular.js";
|
||||
import * as apiLogin from "@/api/index.js";
|
||||
import { sendSms } from "@/api/common.js";
|
||||
import util from "@/libs/util.js";
|
||||
import { useSiteSettings } from "@/composables/useSiteSettings";
|
||||
import verify from "@/views/my-components/verify";
|
||||
import { useAppStore } from "@/stores/app";
|
||||
|
||||
@@ -146,8 +146,7 @@ const form = reactive({
|
||||
oncePasd: "",
|
||||
});
|
||||
const year = new Date().getFullYear();
|
||||
// 站点名优先取接口下发缓存,兜底用本地配置
|
||||
const siteName = util.getSiteName();
|
||||
const { website, copyrightOwner } = useSiteSettings();
|
||||
const step = ref(0);
|
||||
const ruleInline = {
|
||||
mobile: [
|
||||
|
||||
@@ -2,46 +2,30 @@
|
||||
<div class="foot">
|
||||
<div class="information footer-bottom">
|
||||
<div class="icp">
|
||||
<li v-if="config.icpCard">
|
||||
<a
|
||||
:href="
|
||||
'https://tsm.miit.gov.cn/dxxzsp/xkz/xkzgl/resource/qiyesearch.jsp?num=' +
|
||||
config.icpCard +
|
||||
'&type=xuke'
|
||||
"
|
||||
target="_blank"
|
||||
>
|
||||
{{ config.icpCard }}
|
||||
</a>
|
||||
</li>
|
||||
<li v-if="config.icpMessage" class="footer-bottom">
|
||||
<li v-if="icp" class="footer-bottom">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank">
|
||||
{{ config.icpMessage }}
|
||||
{{ icp }}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<a class="company-link" :href="companyUrl" target="_blank">
|
||||
<a v-if="companyName" class="company-link" :href="companyUrl || undefined" target="_blank">
|
||||
<img class="zhizhao" :src="zhizhaoImg" alt="" />
|
||||
{{ companyName }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="copyright">
|
||||
<p>Copyright © {{ year }} {{ siteName }}</p>
|
||||
<p>Copyright © {{ year }} {{ copyrightOwner }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import config from "@/config/index";
|
||||
import util from "@/libs/util.js";
|
||||
import { useSiteSettings } from "@/composables/useSiteSettings";
|
||||
import zhizhaoImg from "@/assets/images/zhizhao.jpg";
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
// 站点名/公司信息优先取接口下发缓存,兜底用本地配置
|
||||
const siteName = util.getSiteName();
|
||||
const companyName = localStorage.getItem("sellerCompanyName") || config.company.name;
|
||||
const companyUrl = localStorage.getItem("sellerCompanyUrl") || config.company.href;
|
||||
const { companyName, companyUrl, icp, copyrightOwner } = useSiteSettings();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -8,27 +8,15 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { getBaseSite } from "@/api/common.js";
|
||||
import { loadSiteSettings } from "@/composables/useSiteSettings";
|
||||
|
||||
const storeSideLogo = ref(require("@/assets/logo-lilishop.png"));
|
||||
|
||||
function init() {
|
||||
if (
|
||||
!localStorage.getItem("sellersiteName") ||
|
||||
!localStorage.getItem("sellerlogoImg") ||
|
||||
!localStorage.getItem("sellerIconImg") ||
|
||||
!localStorage.getItem("seller_expiration_time")
|
||||
) {
|
||||
getSite();
|
||||
} else if (new Date() > localStorage.getItem("seller_expiration_time")) {
|
||||
getSite();
|
||||
} else {
|
||||
storeSideLogo.value = localStorage.getItem("sellerlogoImg");
|
||||
window.document.title = localStorage.getItem("sellersiteName");
|
||||
applyFavicon(localStorage.getItem("sellerIconImg"));
|
||||
}
|
||||
getSite();
|
||||
}
|
||||
function applyFavicon(href) {
|
||||
if (!href) return;
|
||||
const link =
|
||||
document.querySelector("link[rel*='icon']") ||
|
||||
document.createElement("link");
|
||||
@@ -38,10 +26,9 @@ function applyFavicon(href) {
|
||||
document.getElementsByTagName("head")[0].appendChild(link);
|
||||
}
|
||||
function getSite() {
|
||||
getBaseSite()
|
||||
.then((res) => {
|
||||
if (res.success && res.result.settingValue) {
|
||||
const data = JSON.parse(res.result.settingValue);
|
||||
loadSiteSettings()
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
const expirationTime = new Date().setHours(new Date().getHours() + 1);
|
||||
localStorage.setItem("seller_expiration_time", expirationTime);
|
||||
localStorage.setItem("sellersiteName", data.siteName);
|
||||
@@ -49,8 +36,8 @@ function getSite() {
|
||||
localStorage.setItem("sellerIconImg", data.storeSideIcon);
|
||||
localStorage.setItem("sellerCompanyName", data.companyName || "");
|
||||
localStorage.setItem("sellerCompanyUrl", data.companyUrl || "");
|
||||
storeSideLogo.value = data.storeSideLogo;
|
||||
window.document.title = data.siteName;
|
||||
storeSideLogo.value = data.storeSideLogo || storeSideLogo.value;
|
||||
if (data.siteName) window.document.title = data.siteName;
|
||||
applyFavicon(data.storeSideIcon);
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user