refactor(site-settings): 统一站点配置管理并清理废弃页面

- 新增 useSiteSettings composable 替代各端硬编码的配置引用
- 移除 memberRecycle 和 fictitiousOrderList 废弃页面并增加缓存过滤
- 版权信息改为通过接口动态获取,后台新增版权归属配置字段
- 全局禁止 textarea 拖拽缩放并统一三端样式规则
- 会员列表增加状态 tab 筛选及启用/禁用双向切换功能
- 删除未使用的 getSpecificationList API 方法
This commit is contained in:
pikachu1995@126.com
2026-11-03 09:14:05 +08:00
parent 22f3e9b5e9
commit fb99824a3a
24 changed files with 422 additions and 1274 deletions

View File

@@ -162,6 +162,8 @@ lilishop-ui
## 8. 升级计划
> 更新日期:2026-11-03,依据各端近期提交及当前代码实现同步。
### ✅ 已完成特性
| 功能模块 | 功能模块 |
| :--- | :--- |
@@ -170,17 +172,21 @@ lilishop-ui
| ✅ 置顶评论 | ✅ 客户等级体系 |
| ✅ 购物卡 | ✅ 腾讯云-云直播 |
| ✅ 卡密商品 | ✅ 二级分销 |
| ✅ 定时任务管理 | ✅ 预约服务商品 |
| ✅ 定时任务管理(数据库配置驱动) | ✅ 预约服务商品 |
| ✅ 主题色管理 | ✅ 新增无需配送发货方式 |
| ✅ 多维统计(营业/商品/会员/积分/储值/营销/分销) | ✅ 经营报表(店铺业绩/商品同比环比/订单明细/销售汇总) |
| ✅ 分类页多模板设置 | ✅ 移动端楼层装修组件库 |
| ✅ PC端楼层装修重构 | ✅ 全站统一顶栏 |
| ✅ PC端店铺独立列表 | ✅ 敏感词场景化过滤 |
| ✅ PC端楼层装修重构(自适应高度 / 可视化热区绘制) | ✅ 全站统一顶栏 / 商品与店铺双模式搜索 |
| ✅ PC端店铺独立列表 | ✅ 敏感词场景化过滤与分组开关 |
| ✅ 店铺结算汇总 / 结算台账 | ✅ 支付方式汇总 / 钱包流水 |
| ✅ 平台经营报表 / 商家周期财务汇总 | ✅ 财务报表与流水 Excel 导出 / 结算单批量下载 |
| ✅ 会员消息四渠道异步通知(站内信 / 短信 / 公众号 / 小程序) | ✅ 消息模板多渠道矩阵管理(模板配置 / 渠道开关) |
| ✅ 平台订单详情优化 / 标准 A4 出库发货单打印 | ✅ 售后详情展示优化 / 移动端售后入口整合至订单列表 |
| ✅ 商品详情视频内嵌相册 | ✅ 评价管理增强(回复状态 / 图片筛选 / 评价分级) |
| ✅ PC会员中心订单快捷入口 / 资产卡片 | ✅ 平台表单抽屉化 / 文章独立编辑页 |
### 🔜 近期规划中
- 🔜 店铺结算汇总 & 结算台账
- 🔜 支付方式汇总 & 钱包流水 & 财务汇总
- 🔜 微信小店对接
- 🔜 抖店对接
- 🔜 微信小程序原生重构与体验对齐
- 🔜 微信小程序原生重构与体验对齐(持续完善;已接入预约交易、分类多模板与首页楼层装修)
- 🔜 OpenAPI 开放平台与开发者中心

View File

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

View File

@@ -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([
// 导航链接

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

View File

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

View File

@@ -119,7 +119,7 @@ function finished() {
});
}
// 父组件(member/index、memberRecycle、memberDetail、orderDetail、shopOperation)通过 ref 调用 open() 打开弹窗
// 父组件(member/index、memberDetail、orderDetail、shopOperation)通过 ref 调用 open() 打开弹窗
defineExpose({ open });
</script>

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

View File

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

View File

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

View File

@@ -1,3 +1,8 @@
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
.el-textarea__inner {
resize: none !important;
}
/* Element Plus 主题覆盖(对齐原 iView 主色) */
:root {
// --el-color-primary: #ff5c58;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -113,9 +113,9 @@
z-index: 4000 !important;
}
/* 全局禁止 textarea 拖拽改变大小 */
/* 全局禁止多行输入框拖动缩放,覆盖组件生成的内联 resize 样式。 */
.el-textarea__inner {
resize: none;
resize: none !important;
}
/* 统一空状态:表格 / 树 / 列表无数据时的展示(未自定义 #empty 插槽的页面均生效) */

View File

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

View File

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

View File

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