67 Commits

Author SHA1 Message Date
gyq
380a8c6572 新增商品按字母搜索 2026-05-15 13:40:42 +08:00
gyq
a719807b0e 优化订单无法再次打印的问题 2026-05-13 09:48:03 +08:00
gyq
32e24c6d52 问题修复 2026-05-08 17:12:37 +08:00
66e8da53de 增加web端订单列表退菜显示 2026-05-08 11:30:37 +08:00
9b1e9cc8a9 冲突解决 2026-05-07 15:36:18 +08:00
598609e050 合并代码 2026-05-07 15:35:48 +08:00
13bd39fbed 管理端细节修改 2026-05-07 15:34:38 +08:00
gyq
ea5a4c14e9 修复总结后的问题,详见企业微信文档 2026-05-07 14:34:03 +08:00
gyq
ffad9432c5 优化路由切换白屏问题 2026-04-23 14:19:04 +08:00
gyq
6d09813aa4 优化打印机配置 2026-04-22 10:09:50 +08:00
gyq
e2338b3888 Merge branch 'test' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into test 2026-04-14 18:28:57 +08:00
gyq
c7f22e193a 优化商品库存耗材 2026-04-14 18:28:54 +08:00
50139a5e57 Merge branch 'prod' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into test 2026-04-11 18:12:48 +08:00
209be704e1 修复打印机切换分类打印后,部分分类时无法选择商品分类问题 2026-04-11 18:12:19 +08:00
gyq
6a20930a7d 优化交班记录 2026-04-09 14:51:10 +08:00
gyq
506dcbb804 优化 2026-04-08 15:07:19 +08:00
gyq
75e46ec6dd 优化首页 2026-04-02 16:59:48 +08:00
gyq
c87c999d42 新增欢迎首页 2026-04-01 10:39:24 +08:00
f648a7ea5e Merge branch 'test' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into prod 2026-04-01 10:20:32 +08:00
0ab8852656 修复退款方式不显示问题 2026-03-31 17:22:26 +08:00
03883e1b26 修复optType:stock 传参时多了个空格问题 2026-03-31 14:11:01 +08:00
gyq
b9308c1f21 网络打印机新增声音开关 2026-03-31 13:48:59 +08:00
be12089ffe 修复库存开关按钮无效问题,增加商品上下架sku联动,修复商品绑定耗材编辑回显问题 2026-03-30 18:07:58 +08:00
e5cdf0a3dc 修复耗材显示不全问题 2026-03-30 16:09:01 +08:00
51812fb0cc 台桌列表页面金额显示保持两位小数 2026-03-28 18:51:43 +08:00
6e39a94f01 台桌页面增加统计显示 2026-03-28 18:50:02 +08:00
d6a33aea3b 代码合并 2026-03-28 18:19:42 +08:00
397f4b2bcf 增加用户是否可以自主结账开关,增加台桌更多信息显示 2026-03-28 18:16:57 +08:00
gyq
2f2a152b46 优化订单跳转代客下单拉取不到历史数据的问题 2026-03-28 17:38:38 +08:00
gyq
e3ddd70ce6 优化代客下单 2026-03-27 17:17:52 +08:00
gyq
3c68d25286 Merge branch 'prod' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into test 2026-03-27 17:13:26 +08:00
gyq
f5105cad87 本地服务切换到测试接口 2026-03-27 10:09:08 +08:00
gyq
372a1b35ec 更新优化换桌功能 2026-03-27 09:32:02 +08:00
c734bbe353 Merge branch 'prod' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into prod 2026-03-26 10:55:40 +08:00
d5a2f0ba24 修复支付方式不显示问题 2026-03-26 10:55:35 +08:00
gyq
e058aaacf7 修复存酒会员选择 2026-03-26 09:35:40 +08:00
2a47effa45 修复清空历史订单时消失又出现问题 2026-03-20 10:45:47 +08:00
gyq
b3d153ab86 tablecode 2026-03-19 17:25:24 +08:00
gyq
392d4aa676 优化存酒新增会员筛选 2026-03-18 10:45:24 +08:00
gyq
40b9d09671 显示应用中心 2026-03-17 09:24:48 +08:00
gyq
4fcd2cc2cf 优化 2026-03-16 10:03:26 +08:00
gyq
13890e3f8d 新增帮助中心 2026-03-13 16:30:56 +08:00
gyq
d30407b26f 优化全民股东 2026-03-06 14:38:42 +08:00
gyq
647e32567b Merge branch 'test' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into test 2026-02-05 14:55:29 +08:00
gyq
ead5aaf458 财务报表新增挂账退款金额 2026-02-05 14:55:23 +08:00
e2fa92d33d Merge branch 'test' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into test 2026-02-05 10:13:33 +08:00
f01cbf0f5b 修复代客下单选择用户搜索无用问题 2026-02-05 10:13:27 +08:00
gyq
ea2b9fcf9e 优化 2026-02-05 09:10:45 +08:00
gyq
b87b4c000e 优化财务报表 2026-02-04 16:32:55 +08:00
gyq
ddd39ec031 优化首页字段显示 2026-02-04 14:55:32 +08:00
gyq
76af0f5a83 1.新增批量导入 2.新增财务报表 2026-02-04 14:51:29 +08:00
a5b11cf4f4 修改管理员店铺列表筛选,商户号修改为手机号,正式/测试修改为商户版本, 2026-02-03 10:26:09 +08:00
gyq
782bd19e4e 修复添加分销员分店不能选择的问题 2026-01-31 15:22:17 +08:00
gyq
25a8fbbe9f Merge branch 'prod' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into test 2026-01-31 09:27:27 +08:00
gyq
058c8dcadc 新增导出 2026-01-31 09:26:25 +08:00
gyq
20de427175 Merge branch 'test' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into prod 2026-01-30 16:05:18 +08:00
gyq
3855ed4b75 新增商品导出 2026-01-30 16:01:44 +08:00
gyq
60ea128a10 优化店铺信息配置改变后取餐模式不生效的问题 2026-01-29 18:24:57 +08:00
gyq
45727d8005 优化 2026-01-29 10:35:34 +08:00
gyq
c035442628 优化进件查询 2026-01-29 10:09:30 +08:00
gyq
62c6b755af 优化添加点餐页轮播图 2026-01-28 17:47:25 +08:00
gyq
71bec03475 用户列表新增导出功能 2026-01-28 16:16:04 +08:00
gyq
ca182dc325 新增分享配置 2026-01-28 15:02:24 +08:00
gyq
ee3a54abf0 Merge branch 'test' of https://newgitea.sxczgkj.cn/czg_team/cashier-web into prod 2026-01-26 18:00:14 +08:00
84116bb951 更新ysk工具库 2026-01-26 15:51:20 +08:00
gyq
000f715bd8 更改分销字段名称 2026-01-26 15:30:19 +08:00
gyq
9991248b08 优化店铺装修默认选择首页 2026-01-23 10:59:57 +08:00
151 changed files with 10818 additions and 3292 deletions

View File

@@ -10,6 +10,8 @@ VITE_APP_BASE_API=/dev-api
# VITE_APP_API_URL=https://cashier.sxczgkj.com/ # 正式 # VITE_APP_API_URL=https://cashier.sxczgkj.com/ # 正式
VITE_APP_API_URL=http://192.168.1.42/ # 本地 VITE_APP_API_URL=http://192.168.1.42/ # 本地
VITE_APP_API_PHP_URL=http://192.168.1.42:8000 #php抖音美团测试环境 VITE_APP_API_PHP_URL=http://192.168.1.42:8000 #php抖音美团测试环境
VITE_APP_API_PHP_IMPORT_URL=http://192.168.1.42:8789 #本地php批量导入
# VITE_APP_API_PHP_IMPORT_URL=https://diftcs.sxczgkj.com #本地线上php批量导入
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws # WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws

View File

@@ -12,6 +12,9 @@ VITE_APP_BASE_API = https://cashier.sxczgkj.com/
VITE_APP_API_PHP_URL=https://newblockwlx.sxczgkj.cn #php抖音美团正式环境 VITE_APP_API_PHP_URL=https://newblockwlx.sxczgkj.cn #php抖音美团正式环境
VITE_APP_API_PHP_IMPORT_URL=https://diftcs.sxczgkj.com #线上php批量导入
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws # WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws

View File

@@ -59,15 +59,17 @@
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"path-to-regexp": "^8.2.0", "path-to-regexp": "^8.2.0",
"pinia": "^2.3.1", "pinia": "^2.3.1",
"pinyin-match": "^1.2.10",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"qs": "^6.14.0", "qs": "^6.14.0",
"sockjs-client": "^1.6.1", "sockjs-client": "^1.6.1",
"sortablejs": "^1.15.6", "sortablejs": "^1.15.6",
"vue": "^3.5.13", "vue": "^3.5.13",
"vue-clipboard3": "^2.0.0", "vue-clipboard3": "^2.0.0",
"vue-draggable-plus": "^0.6.1",
"vue-i18n": "^11.1.0", "vue-i18n": "^11.1.0",
"vue-router": "^4.5.0", "vue-router": "^4.5.0",
"ysk-utils": "^1.0.77" "ysk-utils": "^1.0.91"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.7.1", "@commitlint/cli": "^19.7.1",
@@ -115,6 +117,7 @@
"vite-plugin-mock-dev-server": "^1.8.3", "vite-plugin-mock-dev-server": "^1.8.3",
"vite-plugin-strip-code": "^1.1.0", "vite-plugin-strip-code": "^1.1.0",
"vite-plugin-svg-icons": "^2.0.1", "vite-plugin-svg-icons": "^2.0.1",
"vite-plugin-vue-devtools": "^8.1.0",
"vue-eslint-parser": "^9.4.3", "vue-eslint-parser": "^9.4.3",
"vue-tsc": "^2.2.0" "vue-tsc": "^2.2.0"
}, },

View File

@@ -3,7 +3,17 @@
<!-- 开启水印 --> <!-- 开启水印 -->
<el-watermark :font="{ color: fontColor }" :content="watermarkEnabled ? defaultSettings.watermarkContent : ''" <el-watermark :font="{ color: fontColor }" :content="watermarkEnabled ? defaultSettings.watermarkContent : ''"
:z-index="9999" class="wh-full"> :z-index="9999" class="wh-full">
<router-view /> <!-- 🔴 修改开始使用 v-slot 处理路由组件 -->
<router-view v-slot="{ Component, route }">
<!--
1. 使用 <transition> 包裹防止渲染冲突
2. 加上 :key="route.path" 强制 Vue 在路由变化时重新渲染避免复用导致的空白
-->
<transition name="fade" mode="out-in">
<component :is="Component" :key="route.path" />
</transition>
</router-view>
<!-- 🟢 修改结束 -->
</el-watermark> </el-watermark>
</el-config-provider> </el-config-provider>
</template> </template>

View File

@@ -27,11 +27,34 @@ const Api = {
method: "post", method: "post",
data data
}); });
},
// 排序
sort(params: sortRequest) {
return request<any>({
url: `${baseURL}/sort`,
method: "get",
params
});
} }
}; };
export default Api; export default Api;
export interface sortRequest {
/**
* 拖动的支付类型ID
*/
id: number;
/**
* 店铺ID
*/
shopId: number;
/**
* 目标位置排序号
*/
targetSort: number;
[property: string]: any;
}
interface getRequset { interface getRequset {
id?: string id?: string

View File

@@ -59,6 +59,22 @@ const ShopApi = {
params, params,
}); });
}, },
// 重置密码
putPassword(data: any) {
return request({
url: `/account/admin/sysUser/pwd`,
method: "put",
data,
});
},
// 续期记录
registerRecord(params: any) {
return request({
url: `${baseURL}/registerRecord`,
method: "get",
params,
});
},
}; };
export default ShopApi; export default ShopApi;

View File

@@ -71,7 +71,16 @@ const API = {
method: "get", method: "get",
params params
}); });
} },
// 导出
export(params: any) {
return request({
url: `${baseURL}/export`,
method: "get",
params,
responseType: 'blob'
});
},
} }
export default API; export default API;
export interface getRequest { export interface getRequest {

View File

@@ -45,14 +45,13 @@ const API = {
responseType: "blob", responseType: "blob",
}); });
}, },
// 重置密码
pwd(data: any) { pwd(data: any) {
return request({ return request({
url: `${baseURL}/pwd`, url: `${baseURL}/pwd`,
method: "put", method: "put",
data data
}); });
} },
,
} }
export default API; export default API;

View File

@@ -12,6 +12,19 @@ export const getRegion = () => {
}); });
} }
/**
* 获取所有短信签名
* @param params
* @returns
*/
export const getSms = ({ type }: { type: string }) => {
return request<any, any[]>({
url: `/account/admin/common/sms`,
method: "get",
params: { type }
});
}
/** /**
* 获取所有银行 * 获取所有银行
* @param params * @param params

View File

@@ -801,6 +801,68 @@ export function attendanceDetail(params) {
}); });
} }
// 分享奖励基础:新增/修改
export function shareBasePost(data) {
return request({
url: `${Market_BaseUrl}/admin/shareBase`,
method: 'POST',
data
});
}
// 分享奖励基础 查询
export function shareBaseGet(params) {
return request({
url: `${Market_BaseUrl}/admin/shareBase`,
method: 'GET',
params
});
}
// 轮播图配置:新增/修改
export function shareCarouselPost(data) {
return request({
url: `${Market_BaseUrl}/admin/carousel`,
method: 'POST',
data
});
}
// 分享轮播图配置
export function shareCarouselGet(params) {
return request({
url: `${Market_BaseUrl}/admin/carousel`,
method: 'GET',
params
});
}
// 分享 轮播图配置 删除
export function shareCarouselDel(id) {
return request({
url: `${Market_BaseUrl}/admin/carousel/${id}`,
method: 'DELETE'
});
}
// 全民股东群聊 配置信息获取
export function disGroupGet(params) {
return request({
url: `${Market_BaseUrl}/admin/disGroup`,
method: 'get',
params
});
}
// 全民股东群聊:新增/修改
export function disGroupPost(data) {
return request({
url: `${Market_BaseUrl}/admin/disGroup`,
method: 'POST',
data
});
}

26
src/api/order/fince.ts Normal file
View File

@@ -0,0 +1,26 @@
import request from "@/utils/request";
import { Order_BaseUrl } from "@/api/config";
const baseURL = Order_BaseUrl + "/admin";
const FinanceApi = {
financeBase(params: Request) {
return request<any, any>({
url: `${baseURL}/finance/base`,
method: "get",
params
});
},
};
export interface Request {
/**
* 日期yyyy-MM-dd
*/
date: string;
shopId: number;
type: string;
[property: string]: any;
}
export default FinanceApi;

View File

@@ -72,6 +72,39 @@ const OrderApi = {
data data
}); });
}, },
// 查询财务报表
financeSts(params: any) {
return request<any>({
url: `${Order_BaseUrl}/admin/finance/sts`,
method: "get",
params
});
},
// 导出财务报表
financeExport(params: any) {
return request<any>({
url: `${Order_BaseUrl}/admin/finance/export`,
method: "get",
params,
responseType: 'blob'
});
},
// 打印经营日报
printDayReport(params: any) {
return request<any>({
url: `${Order_BaseUrl}/admin/finance/printDayReport`,
method: "get",
params
});
},
// 打印日结单
printDaySettle(params: any) {
return request<any>({
url: `${Order_BaseUrl}/admin/finance/printDaySettle`,
method: "get",
params
});
},
}; };
export default OrderApi; export default OrderApi;

View File

@@ -26,7 +26,14 @@ const Api = {
method: "get", method: "get",
params, params,
responseType: 'blob' responseType: 'blob'
});
},
// 商品报表打印
print(params: any) {
return request<any>({
url: `${baseURL}/print`,
method: "get",
params
}); });
}, },
}; };

View File

@@ -51,6 +51,15 @@ const AuthAPI = {
method: "delete", method: "delete",
}); });
}, },
// 导出常用单位
exportUnits(params: any) {
return request<any, Responseres>({
url: `${baseURL}/export`,
method: "get",
params,
responseType: 'blob'
});
}
}; };

View File

@@ -48,6 +48,14 @@ const AuthAPI = {
data, data,
}); });
}, },
// 商品-标记自动售罄
markIsAutoSoldOut(data: Object) {
return request<any, Responseres>({
url: `${baseURL}/markIsAutoSoldOut`,
method: "post",
data,
});
},
// 删除 // 删除
deleteByIds(id: number | String) { deleteByIds(id: number | String) {
return request<any, Responseres>({ return request<any, Responseres>({
@@ -72,7 +80,7 @@ const AuthAPI = {
}); });
}, },
// 耗材列表 // 耗材列表分页
productcons(params: any) { productcons(params: any) {
return request<any, Responseres>({ return request<any, Responseres>({
url: `/product/admin/product/cons/page`, url: `/product/admin/product/cons/page`,
@@ -80,7 +88,22 @@ const AuthAPI = {
params params
}); });
}, },
// 耗材列表
productconsList(params: any) {
return request<any, Responseres>({
url: `/product/admin/product/cons/list`,
method: "get",
params
});
},
// 耗材列表
consStock(params: any) {
return request<any, Responseres>({
url: `/product/admin/product/cons/consStock`,
method: "get",
params
});
},
// 上下架 // 上下架
onOff(data: any) { onOff(data: any) {
return request<any, Responseres>({ return request<any, Responseres>({
@@ -181,6 +204,23 @@ const AuthAPI = {
method: "get", method: "get",
params, params,
}); });
},
// 导出商品
exportProducts(params: any) {
return request<any, Responseres>({
url: `${baseURL}/export`,
method: "get",
params,
responseType: 'blob'
});
},
// 商品-批量操作
batchOperate(params: any) {
return request<any, Responseres>({
url: `${baseURL}/batchOperate`,
method: "get",
params
});
} }
}; };

View File

@@ -48,7 +48,15 @@ const AuthAPI = {
method: "delete", method: "delete",
}); });
}, },
// 导出商品分类
exportCategories(params: any) {
return request<any, Responseres>({
url: `${baseURL}/export`,
method: "get",
params,
responseType: 'blob'
});
}
}; };

15
src/api/system/index.js Normal file
View File

@@ -0,0 +1,15 @@
import request from "@/utils/request";
import {
Account_BaseUrl,
Product_BaseUrl,
Market_BaseUrl,
System_BaseUrl
} from "@/api/config";
// 帮助中心
export function getHelp() {
return request({
url: `${System_BaseUrl + "/user/getHelp"}`,
method: 'get'
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
src/assets/index_bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
src/assets/index_quick1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
src/assets/index_quick2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
src/assets/index_quick3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
src/assets/index_quick4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
src/assets/index_quick5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
src/assets/index_quick6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

@@ -3,7 +3,7 @@
<!-- 表格工具栏 --> <!-- 表格工具栏 -->
<div class="flex-x-between mb-[10px]"> <div class="flex-x-between mb-[10px]">
<!-- 左侧工具栏 --> <!-- 左侧工具栏 -->
<div> <div style="display: flex">
<template v-for="item in toolbar" :key="item"> <template v-for="item in toolbar" :key="item">
<template v-if="typeof item === 'string'"> <template v-if="typeof item === 'string'">
<!-- 新增 --> <!-- 新增 -->
@@ -65,6 +65,8 @@
</el-button> </el-button>
</template> </template>
</template> </template>
<!-- 插槽 -->
<slot name="custom" />
</div> </div>
<!-- 右侧工具栏 --> <!-- 右侧工具栏 -->
<div> <div>
@@ -530,6 +532,7 @@ const emit = defineEmits<{
editClick: [row: IObject]; editClick: [row: IObject];
operatClick: [data: IOperatData]; operatClick: [data: IOperatData];
filterChange: [data: IObject]; filterChange: [data: IObject];
selectChange: []
}>(); }>();
// 主键 // 主键
@@ -634,6 +637,7 @@ function handleSelectionChange(selection: any[]) {
console.log("defaultSelData.value", defaultSelData.value); console.log("defaultSelData.value", defaultSelData.value);
selectionData.value = selection; selectionData.value = selection;
removeIds.value = selection.map((item) => item[pk]); removeIds.value = selection.map((item) => item[pk]);
emit('selectChange', getselectTable())
} }
// 获取选中的表格数据 // 获取选中的表格数据
function getselectTable() { function getselectTable() {
@@ -1108,6 +1112,7 @@ defineExpose({
pagination, pagination,
test, test,
setSelectTable, setSelectTable,
pageData,
}); });
</script> </script>
@@ -1115,17 +1120,21 @@ defineExpose({
:deep(.el-table .el-table__cell) { :deep(.el-table .el-table__cell) {
z-index: inherit; z-index: inherit;
} }
.el-card { .el-card {
overflow: visible; overflow: visible;
} }
:deep(.el-table) { :deep(.el-table) {
overflow: visible; overflow: visible;
.el-table__header-wrapper { .el-table__header-wrapper {
position: sticky; position: sticky;
z-index: calc(var(--el-table-index) + 2); z-index: calc(var(--el-table-index) + 2);
top: 0; top: 0;
} }
} }
:deep(.el-table td.el-table__cell div) { :deep(.el-table td.el-table__cell div) {
vertical-align: middle; vertical-align: middle;
} }

View File

@@ -177,7 +177,7 @@ const formItems = reactive(props.searchConfig.formItems);
// 是否可展开/收缩 // 是否可展开/收缩
const isExpandable = ref(props.searchConfig.isExpandable ?? true); const isExpandable = ref(props.searchConfig.isExpandable ?? true);
// 是否已展开 // 是否已展开
const isExpand = ref(false); const isExpand = ref(props.searchConfig.isExpand ?? false);
// 表单项展示数量,若可展开,超出展示数量的表单项隐藏 // 表单项展示数量,若可展开,超出展示数量的表单项隐藏
const showNumber = computed(() => { const showNumber = computed(() => {
if (isExpandable.value === true) { if (isExpandable.value === true) {

View File

@@ -31,6 +31,7 @@ export interface ISearchConfig {
// 页面名称(参与组成权限标识,如sys:user:xxx) // 页面名称(参与组成权限标识,如sys:user:xxx)
pageName: string; pageName: string;
inline?: Boolean; inline?: Boolean;
isExpand?: boolean;
// 表单项 // 表单项
formItems: Array<{ formItems: Array<{
// 组件类型(如input,select等) // 组件类型(如input,select等)
@@ -58,6 +59,7 @@ export interface ISearchConfig {
} }
export interface IContentConfig<T = any> { export interface IContentConfig<T = any> {
rowDraggable?: boolean;
resultListKey?: string; resultListKey?: string;
// 页面名称(参与组成权限标识,如sys:user:xxx) // 页面名称(参与组成权限标识,如sys:user:xxx)
pageName: string; pageName: string;
@@ -189,6 +191,7 @@ export interface IContentConfig<T = any> {
| "icon" | "icon"
| "date" | "date"
| "tool" | "tool"
| "drag-handle"
| "custom"; | "custom";
// image模板相关参数 // image模板相关参数
imageWidth?: number; imageWidth?: number;

View File

@@ -5,18 +5,8 @@
<el-input v-model="searhForm.name" placeholder="商品名称"></el-input> <el-input v-model="searhForm.name" placeholder="商品名称"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-select <el-select style="width: 200px" v-model="searhForm.category" placeholder="商品分类" :disabled="disableCategory">
style="width: 200px" <el-option :label="item.name" :value="item.id" v-for="item in categoryList" :key="item.id"></el-option>
v-model="searhForm.category"
placeholder="商品分类"
:disabled="disableCategory"
>
<el-option
:label="item.name"
:value="item.id"
v-for="item in categoryList"
:key="item.id"
></el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
@@ -25,7 +15,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
<div class="head-container"> <div class="head-container">
<el-table ref="table" :data="tableData.list" height="500" v-loading="tableData.loading"> <el-table ref="table" :data="tableData.list" height="500" border stripe v-loading="tableData.loading">
<el-table-column type="selection" width="55" align="center" v-if="!radio"></el-table-column> <el-table-column type="selection" width="55" align="center" v-if="!radio"></el-table-column>
<el-table-column label="商品信息"> <el-table-column label="商品信息">
<template v-slot="scope"> <template v-slot="scope">
@@ -72,14 +62,9 @@
</el-table-column> </el-table-column>
</el-table> </el-table>
</div> </div>
<el-pagination <el-pagination :total="tableData.total" :current-page="tableData.page" :page-size="tableData.size"
:total="tableData.total" @current-change="paginationChange" @size-change="sizeChange"
:current-page="tableData.page" layout="total, sizes, prev, pager, next, jumper"></el-pagination>
:page-size="tableData.size"
@current-change="paginationChange"
@size-change="sizeChange"
layout="total, sizes, prev, pager, next, jumper"
></el-pagination>
<template #footer> <template #footer>
<span class="dialog-footer" v-if="!radio"> <span class="dialog-footer" v-if="!radio">
<el-button @click="dialogVisible = false"> </el-button> <el-button @click="dialogVisible = false"> </el-button>
@@ -132,7 +117,7 @@ export default {
}, },
// 确定选商品 // 确定选商品
confirmHandle() { confirmHandle() {
let res = this.$refs.table.selection; let res = this.$refs.table.getSelectionRows();
console.log(res); console.log(res);
if (!res) { if (!res) {
return ElMessage.error("请选择商品"); return ElMessage.error("请选择商品");
@@ -148,6 +133,7 @@ export default {
this.tableData.page = 1; this.tableData.page = 1;
this.tableData.size = 10; this.tableData.size = 10;
this.tableData.list = []; this.tableData.list = [];
this.getTableData();
}, },
// 分页大小改变 // 分页大小改变
sizeChange(e) { sizeChange(e) {

View File

@@ -1,44 +1,42 @@
<!-- 文件上传组件 --> <!-- 文件上传组件 -->
<template> <template>
<div> <div>
<el-upload <el-upload v-model:file-list="fileList" :style="props.style" :before-upload="handleBeforeUpload"
v-model:file-list="fileList" :http-request="handleUpload" :on-progress="handleProgress" :on-success="handleSuccess" :on-error="handleError"
:style="props.style" :accept="props.accept" :limit="props.limit" multiple drag :tip="'支持多个文件上传,单个文件不超过 ' + props.maxFileSize + 'MB'">
:before-upload="handleBeforeUpload" <!-- 拖拽上传区域 -->
:http-request="handleUpload" <div>
:on-progress="handleProgress" <el-icon class="el-icon--upload">
:on-success="handleSuccess" <UploadFilled />
:on-error="handleError" </el-icon>
:accept="props.accept" <div class="el-upload__text">
:limit="props.limit" 将文件拖到此处 <em>点击上传</em>
multiple <br />
> <small>支持格式{{ props.accept }}</small>
<!-- 上传文件按钮 --> </div>
<el-button type="primary" :disabled="fileList.length >= props.limit"> </div>
{{ props.uploadBtnText }}
</el-button>
<!-- 文件列表 --> <!-- 文件列表 -->
<template #file="{ file }"> <template #file="{ file }">
<div class="el-upload-list__item-info"> <div class="el-upload-list__item-info">
<a class="el-upload-list__item-name" @click="handleDownload(file)"> <a class="el-upload-list__item-name" @click="handleDownload(file)">
<el-icon><Document /></el-icon> <el-icon>
<Document />
</el-icon>
<span class="el-upload-list__item-file-name">{{ file.name }}</span> <span class="el-upload-list__item-file-name">{{ file.name }}</span>
<span class="el-icon--close" @click="handleRemove(file.url!)"> <span class="el-icon--close" @click="handleRemove(file.url)">
<el-icon><Close /></el-icon> <el-icon>
<Close />
</el-icon>
</span> </span>
</a> </a>
</div> </div>
</template> </template>
</el-upload> </el-upload>
<el-progress :style="{
<el-progress
:style="{
display: showProgress ? 'inline-flex' : 'none', display: showProgress ? 'inline-flex' : 'none',
width: '100%', width: '100%',
}" marginTop: '10px',
:percentage="progressPercent" }" :percentage="progressPercent" />
/>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@@ -48,9 +46,14 @@ import {
UploadProgressEvent, UploadProgressEvent,
UploadRequestOptions, UploadRequestOptions,
} from "element-plus"; } from "element-plus";
import { Upload, Document, Close } from "@element-plus/icons-vue";
import CommonApi, { FileInfo, uploadResponse } from "@/api/account/common"; import CommonApi, { FileInfo, uploadResponse } from "@/api/account/common";
const emit = defineEmits<{
"upload-success": [fileInfo: string];
}>();
const props = defineProps({ const props = defineProps({
/** /**
* 请求携带的额外参数 * 请求携带的额外参数
@@ -96,7 +99,6 @@ const props = defineProps({
type: String, type: String,
default: "上传文件", default: "上传文件",
}, },
/** /**
* 样式 * 样式
*/ */
@@ -104,7 +106,7 @@ const props = defineProps({
type: Object, type: Object,
default: () => { default: () => {
return { return {
width: "300px", width: "100%",
}; };
}, },
}, },
@@ -147,6 +149,7 @@ function handleBeforeUpload(file: UploadRawFile) {
ElMessage.warning("上传图片不能大于" + props.maxFileSize + "M"); ElMessage.warning("上传图片不能大于" + props.maxFileSize + "M");
return false; return false;
} }
showProgress.value = true;
return true; return true;
} }
@@ -190,10 +193,13 @@ const handleProgress = (event: UploadProgressEvent) => {
const handleSuccess = (fileInfo: string) => { const handleSuccess = (fileInfo: string) => {
ElMessage.success("上传成功"); ElMessage.success("上传成功");
modelValue.value = [...modelValue.value, fileInfo]; modelValue.value = [...modelValue.value, fileInfo];
emit("upload-success", fileInfo);
showProgress.value = false;
}; };
const handleError = (error: any) => { const handleError = (error: any) => {
ElMessage.error("上传失败"); ElMessage.error("上传失败");
showProgress.value = false;
}; };
/** /**

View File

@@ -0,0 +1,242 @@
<!-- 文件上传组件 -->
<template>
<div>
<el-upload v-model:file-list="fileList" :style="props.style" :before-upload="handleBeforeUpload"
:on-change="handleChange" :accept="accept" multiple drag :tip="'支持多个文件上传,单个文件不超过 ' + props.maxFileSize + 'MB'"
:auto-upload="false">
<!-- 拖拽上传区域 -->
<div>
<el-icon class="el-icon--upload">
<UploadFilled />
</el-icon>
<div class="el-upload__text">
将文件拖到此处 <em>点击上传</em>
<br />
<small>支持格式{{ accept }}</small>
</div>
</div>
<!-- 文件列表 -->
<template #file="{ file }">
<div class="el-upload-list__item-info">
<a class="el-upload-list__item-name" @click="handleDownload(file)">
<el-icon>
<Document />
</el-icon>
<span class="el-upload-list__item-file-name">{{ file.name }}</span>
<span class="el-icon--close" @click="handleRemove(file.url)">
<el-icon>
<Close />
</el-icon>
</span>
</a>
</div>
</template>
</el-upload>
</div>
</template>
<script setup>
import { UploadFilled, Document, Close } from "@element-plus/icons-vue";
const emit = defineEmits({
"file-selected": null,
});
const props = defineProps({
/**
* 文件上传数量限制
*/
limit: {
type: Number,
default: 10,
},
/**
* 单个文件上传大小限制(单位MB)
*/
maxFileSize: {
type: Number,
default: 10,
},
/**
* 上传文件类型
*/
accept: {
type: String,
default: "*",
},
/**
* 选择文件成功的回调函数
*/
successCallback: {
type: Function,
default: null,
},
/**
* 样式
*/
style: {
type: Object,
default: () => {
return {
width: "100%",
};
},
},
});
const fileList = defineModel({
type: Array,
default: () => [],
});
/**
* 文件选择变化
*/
function handleChange(file, fileList) {
if (file.status === 'ready') {
// 如果限制为1个文件移除之前的文件只保留当前新文件
if (props.limit === 1 && fileList.length > 1) {
fileList.splice(0, fileList.length - 1);
}
const rawFile = file.raw;
// 限制文件类型
if (props.accept !== "*") {
const acceptedTypes = props.accept.split(',').map(type => type.trim());
const isAccepted = acceptedTypes.some(type => {
let checkType = type;
if (!type.startsWith('.') && !type.includes('/')) {
checkType = '.' + type;
}
if (checkType.startsWith('.')) {
return rawFile.name.toLowerCase().endsWith(checkType.toLowerCase());
} else if (checkType.includes('*')) {
const [main] = checkType.split('/');
return rawFile.type.startsWith(main + '/');
} else {
return rawFile.type === checkType;
}
});
if (!isAccepted) {
ElMessage.warning("文件类型不符合要求");
// 从 fileList 中移除不符合的文件
fileList.splice(fileList.indexOf(file), 1);
return;
}
}
if (props.successCallback) {
props.successCallback(rawFile);
} else {
emit("file-selected", rawFile);
}
}
}
/**
* 上传前校验
*/
function handleBeforeUpload(file) {
// 限制文件大小
if (file.size > props.maxFileSize * 1024 * 1024) {
ElMessage.warning("上传文件不能大于" + props.maxFileSize + "M");
return false;
}
// 限制文件类型
if (props.accept !== "*") {
const acceptedTypes = props.accept.split(',').map(type => type.trim());
const isAccepted = acceptedTypes.some(type => {
if (type.startsWith('.')) {
return file.name.toLowerCase().endsWith(type.toLowerCase());
} else {
return file.type === type;
}
});
if (!isAccepted) {
ElMessage.warning("文件类型不符合要求");
return false;
}
}
return true;
}
/**
* 删除文件
*/
function handleRemove(fileUrl) {
// 从 fileList 中移除
fileList.value = fileList.value.filter(f => f.url !== fileUrl);
}
/**
* 下载文件
*/
function handleDownload(file) {
// 本地文件,不支持下载
}
</script>
<style lang="scss" scoped>
.el-upload-list__item .el-icon--close {
position: absolute;
top: 50%;
right: 5px;
color: var(--el-text-color-regular);
cursor: pointer;
opacity: 0.75;
transition: opacity var(--el-transition-duration);
transform: translateY(-50%);
&:hover {
opacity: 1;
color: var(--el-color-danger);
}
}
:deep(.el-upload-list) {
margin: 10px 0 0 0;
}
:deep(.el-upload-list__item) {
margin: 5px 0;
background: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 4px;
padding: 8px 12px;
display: flex;
align-items: center;
transition: all 0.3s ease;
&:hover {
background: #f0f0f0;
border-color: var(--el-color-primary-light-3);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.el-upload-list__item-info {
flex: 1;
a {
display: flex;
align-items: center;
text-decoration: none;
color: var(--el-text-color-primary);
&:hover {
color: var(--el-color-primary);
}
.el-icon {
margin-right: 8px;
color: var(--el-color-primary);
}
.el-upload-list__item-file-name {
flex: 1;
font-size: 14px;
font-weight: 500;
}
.el-icon--close {
margin-left: 8px;
}
}
}
}
</style>

View File

@@ -0,0 +1,305 @@
<!-- 批量导入数据dialog -->
<template>
<div>
<div class="btn_row">
<el-button type="success" icon="RefreshRight" @click="updateData">数据更新</el-button>
<el-button type="primary" icon="Upload" @click="show">批量导入</el-button>
<el-button icon="Download" @click="downloadTemplateAjax">下载银收客模板</el-button>
</div>
<el-dialog
title="批量导入"
width="800px"
v-model="visible"
:close-on-click-modal="false"
:close-on-press-escape="false"
@close="dialogClose"
>
<div class="row">
<tabHeader v-model="tabActive" :list="tabs" />
</div>
<div class="row mt14">
<div class="import_container" v-if="tabActive == 0">
<div class="header_title">第一步选择模板</div>
<div class="row mt14 pb50">
<div class="list">
<div
class="item"
:class="{ active: platformActive == index }"
v-for="(item, index) in platformList"
:key="item.id"
@click="selectPlatform(item)"
>
<img class="img" :src="item.img" alt="" />
</div>
</div>
</div>
<div class="header_title">
第二步上传文件
<span>单次仅可上传一个文件</span>
</div>
<div class="row mt14">
<GfileUpload
v-model="form.files"
:accept="platformList[platformActive]?.file_type || ''"
:limit="1"
@file-selected="fileSelected"
/>
</div>
<div class="row mt14">
<div class="footer_wrap">
<el-button @click="visible = false"> </el-button>
<el-button
type="primary"
:disabled="!form.files.length"
:loading="loading"
@click="startImportHandle"
>
<template v-if="!form.files.length">请选择文件</template>
<template v-else>开始导入</template>
</el-button>
</div>
</div>
</div>
<el-table :data="tableData" border stripe v-if="tabActive == 1" height="419px">
<el-table-column prop="file_name" label="文件名称" width="300" />
<el-table-column prop="created_time" label="导入时间" width="200" />
<el-table-column prop="status_text" label="导入状态" width="150">
<template #default="scope">
<el-tag v-if="scope.row.status == 0" type="info" disable-transitions>
待处理...
</el-tag>
<el-tag v-else-if="scope.row.status == 1" type="warning" disable-transitions>
处理中...
</el-tag>
<el-tag v-else-if="scope.row.status == 2" type="success" disable-transitions>
处理完成
</el-tag>
<el-tag v-else-if="scope.row.status == -1" type="danger" disable-transitions>
导入失败
</el-tag>
</template>
</el-table-column>
<el-table-column prop="importResult" label="导入结果">
<template #default="scope">
<template v-if="scope.row.status == 2">
<div class="column">
<div>
<el-text type="success">成功{{ scope.row.success_num }} </el-text>
</div>
<div>
<el-text type="danger">失败{{ scope.row.fail_num }} </el-text>
</div>
</div>
</template>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import { getplatlist, uploadFile, importlist, downloadTemp } from "@/importDataApi/index.js";
import GfileUpload from "../Upload/GfileUpload.vue";
import tabHeader from "@/views/marketing_center/components/tabHeader.vue";
import { ElMessage } from "element-plus";
const props = defineProps({
type: {
type: [String, Number],
default: 3, // 3商品 4台桌区域 5台桌 6会员 7菜品销售统计 8台桌销售统计 9订单销售统计
},
});
const platformList = ref([]);
const platformActive = ref(0);
function selectPlatform(item) {
form.value.files = [];
platformActive.value = platformList.value.findIndex((i) => i.id === item.id);
}
const tabs = ref([
{ label: "导入数据", name: "importData" },
{ label: "导入记录", name: "importRecord" },
]);
const tabActive = ref(0);
watch(tabActive, (newVal) => {
if (newVal === 1) {
getImportRecord();
}
});
const visible = ref(false);
const loading = ref(false);
const form = ref({
files: [],
platform: "",
});
function fileSelected(file) {
console.log("fileSelected", file);
}
// 开始导入
async function startImportHandle() {
try {
form.value.platform = platformList.value[platformActive.value]?.id;
const formData = new FormData();
formData.append("file", form.value.files[0].raw);
formData.append("shop_id", localStorage.getItem("shopId"));
formData.append("type", props.type);
formData.append("platform", form.value.platform);
loading.value = true;
await uploadFile(formData);
ElMessage.success("文件上传成功,正在导入数据,请在导入记录中查看导入结果");
form.value.files = [];
tabActive.value = 1;
} catch (error) {
console.log(error);
}
setTimeout(() => {
loading.value = false;
}, 500);
}
// 获取平台列表
async function getPlatformList() {
try {
const res = await getplatlist({ plat_type: props.type });
platformList.value = res;
} catch (error) {
console.log(error);
}
}
// 导入记录
const tableData = ref([]);
async function getImportRecord() {
try {
const res = await importlist({ shop_id: localStorage.getItem("shopId") });
tableData.value = res;
} catch (error) {
console.log(error);
}
}
// 下载模板
async function downloadTemplateAjax() {
try {
const res = await downloadTemp({ plat_type: props.type });
window.open(res, "_blank");
} catch (error) {
console.log(error);
}
}
const emits = defineEmits(["close", "update"]);
function updateData() {
// 把 ElMessage 改成 ElMessageBox 就好了
ElMessageBox.confirm("确认需要更新经营数据吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
emits("update");
})
.catch(() => {
// 取消操作,空着就行
});
}
function dialogClose() {
emits("close");
}
onMounted(() => {
getPlatformList();
});
function show() {
visible.value = true;
}
defineExpose({
show,
});
</script>
<style scoped lang="scss">
.btn_row {
display: flex;
gap: 4px;
margin-left: 14px;
}
.header_title {
font-size: 16px;
font-weight: bold;
color: #333333;
display: flex;
align-items: center;
span {
font-size: 12px;
color: #999999;
margin-left: 10px;
}
}
.row {
&.mt14 {
margin-top: 14px;
}
&.pb50 {
padding-bottom: 50px;
}
}
.import_container {
.list {
display: flex;
gap: 24px;
.item {
width: 122px;
height: 42px;
border: 1px solid #fff;
border-radius: 4px;
overflow: hidden;
&:hover {
cursor: pointer;
border-color: var(--el-color-primary);
}
&.active {
border-color: var(--el-color-primary);
}
.img {
width: 100%;
height: 100%;
}
}
}
.footer_wrap {
display: flex;
gap: 14px;
justify-content: flex-end;
}
}
.column {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,338 @@
<!-- 批量导入数据dialog -->
<template>
<div>
<div class="btn_row">
<el-button type="success" icon="RefreshRight" @click="updateData">数据更新</el-button>
<el-button type="primary" icon="Upload" @click="show">批量导入</el-button>
<el-button icon="Download" @click="downloadTemplateAjax">下载银收客模板</el-button>
</div>
<el-dialog
title="批量导入"
width="800px"
v-model="visible"
:close-on-click-modal="false"
:close-on-press-escape="false"
@close="dialogClose"
>
<div class="row">
<tabHeader v-model="tabActive" :list="tabs" />
</div>
<div class="row mt14">
<div class="import_container" v-if="tabActive == 0">
<div class="header_title">第一步选择模板</div>
<div class="row mt14 pb50">
<div class="list">
<div
class="item"
:class="{ active: platformActive == index }"
v-for="(item, index) in platformList"
:key="item.id"
@click="selectPlatform(item)"
>
<img class="img" :src="item.img" alt="" />
</div>
</div>
</div>
<div class="header_title">
第二步上传文件
<span>单次仅可上传一个文件</span>
</div>
<div class="row mt14">
<GfileUpload
v-model="form.files"
:accept="platformList[platformActive]?.file_type || ''"
:limit="1"
@file-selected="fileSelected"
/>
</div>
<div class="row mt14">
<div class="footer_wrap">
<el-button @click="visible = false"> </el-button>
<el-button
type="primary"
:disabled="!form.files.length"
:loading="loading"
@click="startImportHandle"
>
<template v-if="!form.files.length">请选择文件</template>
<template v-else>开始导入</template>
</el-button>
</div>
</div>
</div>
<el-table :data="tableData" border stripe v-if="tabActive == 1" height="419px">
<el-table-column prop="file_name" label="文件名称" width="300" />
<el-table-column prop="created_time" label="导入时间" width="200" />
<el-table-column prop="status_text" label="导入状态" width="150">
<template #default="scope">
<el-tag v-if="scope.row.status == 0" type="info" disable-transitions>
待处理...
</el-tag>
<el-tag v-else-if="scope.row.status == 1" type="warning" disable-transitions>
处理中...
</el-tag>
<el-tag v-else-if="scope.row.status == 2" type="success" disable-transitions>
处理完成
</el-tag>
<el-tag v-else-if="scope.row.status == -1" type="danger" disable-transitions>
导入失败
</el-tag>
</template>
</el-table-column>
<el-table-column prop="importResult" label="导入结果">
<template #default="scope">
<template v-if="scope.row.status == 2">
<div class="column">
<div>
<el-text type="success">成功{{ scope.row.success_num }} </el-text>
</div>
<div>
<el-text type="danger">失败{{ scope.row.fail_num }} </el-text>
</div>
</div>
</template>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<!-- 日期选择弹窗 -->
<el-dialog
v-model="dateDialogVisible"
title="选择更新日期"
width="500px"
@close="dateDialogVisible = false"
>
<div style="padding: 20px 0">
<el-date-picker
v-model="selectDate"
type="date"
label="选择日期"
value-format="YYYY-MM-DD"
placeholder="请选择日期"
style="width: 100%"
/>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dateDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmUpdateDate">确认更新</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import { getplatlist, uploadFile, importlist, downloadTemp } from "@/importDataApi/index.js";
import GfileUpload from "../Upload/GfileUpload.vue";
import tabHeader from "@/views/marketing_center/components/tabHeader.vue";
import { ElMessage, ElMessageBox } from "element-plus";
const props = defineProps({
type: {
type: [String, Number],
default: 3,
},
});
const platformList = ref([]);
const platformActive = ref(0);
function selectPlatform(item) {
form.value.files = [];
platformActive.value = platformList.value.findIndex((i) => i.id === item.id);
}
const tabs = ref([
{ label: "导入数据", name: "importData" },
{ label: "导入记录", name: "importRecord" },
]);
const tabActive = ref(0);
watch(tabActive, (newVal) => {
if (newVal === 1) {
getImportRecord();
}
});
const visible = ref(false);
const loading = ref(false);
const form = ref({
files: [],
platform: "",
});
// 日期选择弹窗
const dateDialogVisible = ref(false);
const today = new Date().toISOString().split("T")[0];
const selectDate = ref(today); // 默认当天
function fileSelected(file) {
console.log("fileSelected", file);
}
// 开始导入
async function startImportHandle() {
try {
form.value.platform = platformList.value[platformActive.value]?.id;
const formData = new FormData();
formData.append("file", form.value.files[0].raw);
formData.append("shop_id", localStorage.getItem("shopId"));
formData.append("type", props.type);
formData.append("platform", form.value.platform);
loading.value = true;
await uploadFile(formData);
ElMessage.success("文件上传成功,正在导入数据,请在导入记录中查看导入结果");
form.value.files = [];
tabActive.value = 1;
} catch (error) {
console.log(error);
}
setTimeout(() => {
loading.value = false;
}, 500);
}
// 获取平台列表
async function getPlatformList() {
try {
const res = await getplatlist({ plat_type: props.type });
platformList.value = res;
} catch (error) {
console.log(error);
}
}
// 导入记录
const tableData = ref([]);
async function getImportRecord() {
try {
const res = await importlist({ shop_id: localStorage.getItem("shopId") });
tableData.value = res;
} catch (error) {
console.log(error);
}
}
// 下载模板
async function downloadTemplateAjax() {
try {
const res = await downloadTemp({ plat_type: props.type });
window.open(res, "_blank");
} catch (error) {
console.log(error);
}
}
const emits = defineEmits(["close", "update"]);
// ==================================
// 数据更新(打开日期选择框)
// ==================================
function updateData() {
dateDialogVisible.value = true;
}
// 确认选择日期并更新
function confirmUpdateDate() {
if (!selectDate.value) {
ElMessage.warning("请选择日期");
return;
}
dateDialogVisible.value = false;
emits("update", selectDate.value); // 把选中的日期传给父组件
// ElMessage.success("已选择日期:" + selectDate.value);
}
function dialogClose() {
emits("close");
}
onMounted(() => {
getPlatformList();
});
function show() {
visible.value = true;
}
defineExpose({
show,
});
</script>
<style scoped lang="scss">
.btn_row {
display: flex;
gap: 4px;
margin-left: 14px;
}
.header_title {
font-size: 16px;
font-weight: bold;
color: #333333;
display: flex;
align-items: center;
span {
font-size: 12px;
color: #999999;
margin-left: 10px;
}
}
.row {
&.mt14 {
margin-top: 14px;
}
&.pb50 {
padding-bottom: 50px;
}
}
.import_container {
.list {
display: flex;
gap: 24px;
.item {
width: 122px;
height: 42px;
border: 1px solid #fff;
border-radius: 4px;
overflow: hidden;
&:hover {
cursor: pointer;
border-color: var(--el-color-primary);
}
&.active {
border-color: var(--el-color-primary);
}
.img {
width: 100%;
height: 100%;
}
}
}
.footer_wrap {
display: flex;
gap: 14px;
justify-content: flex-end;
}
}
.column {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,120 @@
<template>
<el-dialog title="打印确认" width="400px" v-model="showDayBusiness">
<div class="business_wrap">
<div class="title">
<el-text>请选择要打印的日期</el-text> <el-text type="danger">周期最长为7天</el-text>
</div>
<div class="row">
<el-date-picker v-model="printDayBusinessParams.date" type="daterange" start-placeholder="开始日期"
end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" :disabled-date="disabledDate"
@change="handleDateChange" clearable />
</div>
<div class="business_tips">
<el-text type="info">若数据过多打印时间会比较长请耐心等待</el-text>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="showDayBusiness = false"> </el-button>
<el-button type="primary" @click="confirmHandle" :loading="printDayBusinessLoading"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import dayjs from "dayjs";
import { ref, onMounted } from "vue";
import { ElMessage } from 'element-plus'
const props = defineProps({
title: {
type: String,
default: '经营日报'
}
})
const showDayBusiness = ref(false)
const emits = defineEmits(['success'])
// 打印参数
const printDayBusinessParams = ref({
date: [],
})
const printDayBusinessLoading = ref(false)
// 取消
const confirmHandle = () => {
// 校验日期
if (!printDayBusinessParams.value.date || printDayBusinessParams.value.date.length !== 2) {
ElMessage.warning('请选择日期范围')
return
}
showDayBusiness.value = false
emits('success', { date: printDayBusinessParams.value.date })
}
// 日期禁用规则:只能选昨天及更早,不能选未来
const disabledDate = (time) => {
const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD')
return dayjs(time).isAfter(yesterday)
}
// 日期选择后校验区间长度不能超过7天
const handleDateChange = (val) => {
if (!val || val.length !== 2) return
const [start, end] = val
const days = dayjs(end).diff(start, 'day') + 1 // 包含起止日
if (days > 7) {
ElMessage.warning('日期范围最多只能选择7天')
printDayBusinessParams.value.date = [] // 清空选择
}
}
// 判断当前时间 是否在 00:00 ~ 05:20 之间
const time = ref(['00:00', '05:20'])
const isInTimeRange = () => {
const now = dayjs()
const startTime = dayjs(time.value[0], 'HH:mm')
const endTime = dayjs(time.value[1], 'HH:mm')
return now.isAfter(startTime) && now.isBefore(endTime)
}
// 打开弹窗
const show = () => {
if (isInTimeRange()) {
ElMessage.warning(`当前时间不能打印${props.title},打印时间:${time.value[0]}点至${time.value[1]}`)
return
}
showDayBusiness.value = true
}
// 暴露方法
defineExpose({ show })
// 初始化默认选中昨天
onMounted(() => {
const yesterday = dayjs().subtract(1, 'day').format('YYYY-MM-DD')
printDayBusinessParams.value.date = [yesterday, yesterday]
})
</script>
<style scoped lang="scss">
.business_wrap {
.title {
display: flex;
gap: 6px;
align-items: center;
}
.row {
padding-top: 14px;
}
.business_tips {
padding-top: 6px;
display: flex;
justify-content: center;
}
}
</style>

View File

@@ -0,0 +1,83 @@
<template>
<el-dialog title="提示" width="450px" v-model="visible">
<div class="refund_content">
<div class="title_wrap">请确认当前菜品是否已上菜</div>
<div class="list_wrap">
<div class="item" v-for="(item, index) in list" :key="index">
<span>{{ item.name }}</span>
<span>x{{ item.num }}</span>
</div>
</div>
</div>
<div class="dialog_footer">
<div class="btn">
<el-button @click="handleCancel" style="width: 100%;">未上菜退还库存</el-button>
</div>
<div class="btn">
<el-button type="primary" @click="handleOk" style="width: 100%;">已上菜不退库存</el-button>
</div>
</div>
</el-dialog>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(false)
const props = defineProps({
list: {
type: Array,
default: () => []
}
})
const emits = defineEmits(['success'])
// 未上菜 1退菜图库存
function handleCancel() {
visible.value = false
emits('success', 1)
}
// 已上菜 2仅退菜不退库存
function handleOk() {
visible.value = false
emits('success', 2)
}
function show() {
visible.value = true
}
defineExpose({
show
})
</script>
<style scoped lang="scss">
.refund_content {
.title_wrap {
font-size: 16px;
}
.list_wrap {
padding-top: 14px;
.item {
display: flex;
justify-content: space-between;
}
}
}
.dialog_footer {
display: flex;
justify-content: center;
gap: 20px;
padding-top: 20px;
.btn {
flex: 1;
}
}
</style>

View File

@@ -0,0 +1,123 @@
<template>
<!-- 验证码按钮 - 纯功能高度定制版 -->
<el-button :type="buttonType" :size="buttonSize" :disabled="disabled || isCountingDown" :loading="isLoading"
class="captcha-btn" @click="handleGetCaptcha">
<!-- 倒计时状态展示 -->
<span v-if="isCountingDown">{{ countDown }} 秒后重新获取</span>
<!-- 正常状态展示 -->
<span v-else>{{ buttonText }}</span>
</el-button>
</template>
<script setup>
import { ref } from 'vue'
import { getSms } from "@/api/common";
// 接收父组件传递的自定义参数
const props = defineProps({
// 验证码类型
// editShopInfoOpePwd 店铺操作密码
// wxMiniPwd 微信小程序用户登录密码
// shopPwd 店铺登录密码
type: {
type: String,
default: 'editShopInfoOpePwd'
},
// 按钮类型primary/success/warning/danger/info/default
buttonType: {
type: String,
default: 'primary'
},
// 按钮尺寸large/default/small
buttonSize: {
type: String,
default: 'default'
},
// 正常状态按钮文字
buttonText: {
type: String,
default: '获取验证码'
},
// 倒计时总秒数
countDownNum: {
type: Number,
default: 60
},
// 手动控制按钮禁用状态
disabled: {
type: Boolean,
default: false
}
})
// 发射事件给父组件
const emit = defineEmits(['get-captcha'])
// 加载状态(请求验证码时)
const isLoading = ref(false)
// 是否正在倒计时
const isCountingDown = ref(false)
// 倒计时秒数
const countDown = ref(0)
// 倒计时定时器
let timer = null
// 获取验证码点击事件
const handleGetCaptcha = async () => {
// 倒计时中 / 加载中 / 禁用状态 禁止点击
if (isCountingDown.value || isLoading.value || props.disabled) return
try {
// 开启加载状态
isLoading.value = true
// 触发父组件的请求方法(真正的接口请求写在父组件)
await getSms({ type: props.type });
ElNotification.success({
title: '成功',
message: '验证码已发送,请注意查收',
});
// ======================
// 请求成功 → 开始倒计时
// ======================
startCountDown()
} catch (error) {
// 请求失败 → 不触发倒计时,控制台提示
console.error('', error)
} finally {
// 关闭加载状态
setTimeout(() => {
isLoading.value = false
}, 500) // 适当延迟,避免闪烁
}
}
// 倒计时核心逻辑
const startCountDown = () => {
// 初始化倒计时时间
countDown.value = props.countDownNum
isCountingDown.value = true
// 开启定时器
timer = setInterval(() => {
countDown.value--
// 倒计时结束
if (countDown.value <= 0) {
clearInterval(timer)
timer = null
isCountingDown.value = false
}
}, 1000)
}
// 页面销毁时清除定时器(防止内存泄漏)
onUnmounted(() => {
if (timer) clearInterval(timer)
})
</script>
<style scoped>
/* 你可以在这里自定义按钮默认样式,也可以在父组件覆盖 */
.captcha-btn {
min-width: 120px;
}
</style>

View File

@@ -0,0 +1,180 @@
<template>
<el-dialog title="修改密码" modal-append-to-body append-to-body v-model="dialogVisible" @close="reset" width="400px">
<el-form ref="refForm" :model="form" :rules="rules" label-width="100px">
<el-form-item label="手机号">
<el-input :value="maskPhone(shopInfo.phone)" disabled></el-input>
</el-form-item>
<el-form-item label="旧密码" prop="originalPassword">
<el-input type="password" show-password v-model="form.originalPassword" placeholder="请输入旧密码"></el-input>
</el-form-item>
<el-form-item label="新密码" prop="password">
<el-input type="password" show-password v-model="form.password" placeholder="请输入新密码"></el-input>
</el-form-item>
<el-form-item style="margin-top: -10px;"><el-text :type="isPws ? 'danger' : 'info'" size="small"
style="line-height: 16px;">注意新密码必须至少包含字母数字特殊符号中的两种且长度6-18</el-text></el-form-item>
<el-form-item label="确认新密码" prop="checkPassword">
<el-input type="password" show-password v-model="form.checkPassword" placeholder="请再次输入新密码"></el-input>
</el-form-item>
<el-form-item label="验证码" prop="code">
<div class="center">
<el-input v-model="form.code" placeholder="请输入验证码"></el-input>
<captcha-btn type="shopPwd" />
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="formLoading" @click="submitHandle"> </el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import sysUser from "@/api/account/sysUser";
import { ElNotification } from "element-plus";
import { maskPhone } from "@/utils";
import CaptchaBtn from "./CaptchaBtn.vue";
const props = defineProps({
// 修改成功后的跳转类型 1- 跳转到登录页 2- 留在当前页
type: {
type: [String, Number],
default: 1
}
})
const shopInfo = ref({
phone: ''
})
const router = useRouter();
const dialogVisible = ref(false);
const formLoading = ref(false);
const isPws = ref(false);
const form = reactive({
id: '',
originalPassword: "", // 原密码
code: '', // 验证码
checkPassword: "", // 确认新密码
password: "", // 新密码
});
function reset() {
form.originalPassword = "";
form.checkPassword = "";
form.password = "";
form.code = "";
refForm.value.resetFields();
}
const reg = /^(?![0-9]+$)(?![a-zA-Z]+$)(?![^\da-zA-Z]+$).{6,}$/;
const validateNewPass = (rule: any, value: string, callback: (error?: Error) => void) => {
if (!form.password) {
callback(new Error(" "));
} else if (form.password === form.originalPassword) {
callback(new Error("请输入与旧密码不同的新密码"));
} else if (form.password.length < 6 || form.password.length > 18) {
// 密码长度6 - 18位
callback(new Error("密码长度应为6-18位"));
} else if (!reg.test(form.password)) {
// 新密码长度不能小于6 需包含字母、数字、特殊符号中至少两种
isPws.value = true;
callback(new Error(""));
} else {
isPws.value = false;
callback();
}
};
const validateRnewPass = (rule: any, value: string, callback: (error?: Error) => void) => {
if (!form.checkPassword) {
callback(new Error(" "));
} else if (form.checkPassword !== form.password) {
callback(new Error("两次密码输入不一致"));
} else {
callback();
}
};
const rules = {
originalPassword: [
{
required: true,
message: " ",
trigger: "blur",
},
],
password: [
{
required: true,
validator: validateNewPass,
trigger: "blur",
},
],
checkPassword: [
{
required: true,
validator: validateRnewPass,
trigger: "blur",
},
],
code: [
{
required: true,
message: " ",
trigger: "blur",
},
],
};
const emit = defineEmits(['success'])
const refForm = ref();
// 提交修改密码
function submitHandle() {
refForm.value.validate(async (vaild: boolean) => {
if (vaild) {
try {
formLoading.value = true;
await sysUser.pwd(form);
if (props.type == 1) {
ElNotification.success("密码修改成功,请重新登录");
setTimeout(() => {
router.push("/login");
}, 1000);
} else {
ElNotification.success("密码修改成功");
dialogVisible.value = false;
}
emit('success')
} catch (error) {
formLoading.value = false;
console.log(error);
}
}
});
}
function show(e: object) {
console.log(e);
shopInfo.value = e
form.id = e.id
dialogVisible.value = true
}
defineExpose({
show,
});
</script>
<style lang="scss" scoped>
.center {
display: flex;
align-items: center;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<el-select v-model="modelValue" placeholder="请选择优惠券" clearable style="width: 300px;" @change="changHandle">
<el-option v-for="coupon in couponList" :key="coupon.id" :label="coupon.title" :value="coupon.id" />
</el-select>
</template>
<script setup>
import couponApi from "@/api/market/coupon";
import { ref, onMounted } from "vue";
const modelValue = defineModel({
type: [String, Number],
default: ''
});
const name = defineModel('name', {
type: String,
default: ''
});
function changHandle(value) {
name.value = couponList.value.find(item => item.id === value)?.title || '';
}
// 优惠券列表
const couponList = ref([]);
onMounted(() => {
couponApi.getList({ size: 999 }).then((res) => {
if (res) {
couponList.value = res.records || [];
}
});
});
</script>

View File

@@ -0,0 +1,57 @@
import request from "@/utils/request-import-php";
/**
* 平台列表
* @param {*} data
* @returns
*/
export function getplatlist(data) {
return request({
url: "plat/getplatlist",
method: "post",
data
});
}
/**
* 导入
* @param {*} data
* @returns
*/
export function uploadFile(data) {
return request({
url: "upload",
method: "post",
headers: {
'Content-Type': 'multipart/form-data',
},
data
});
}
/**
* 导入记录
* @param {*} data
* @returns
*/
export function importlist(data) {
return request({
url: "plat/importlist",
method: "post",
data
});
}
/**
* 下载模板
* @param {*} data
* @returns
*/
export function downloadTemp(data) {
return request({
url: "plat/downloadTemp",
method: "post",
data
});
}

View File

@@ -0,0 +1,212 @@
<template>
<div>
<el-text size="large" style="margin: 0 14px;" @click="visible = true">帮助中心</el-text>
<el-dialog title="帮助中心" width="1000px" v-model="visible" append-to-body>
<div class="help_container">
<div class="header">关注官方公众号查看详细教程视频快速掌握使用方法</div>
<div class="wrap">
<div class="item">
<div class="title">联系方式</div>
<div class="row">
<div class="left">
<div class="icon">
<el-icon color="#fff">
<PhoneFilled />
</el-icon>
</div>
<el-text>客服电话</el-text>
</div>
<div class="right hover" @click="copyHandle(helpInfo.service_phone)">
<el-text type="primary">{{ helpInfo.service_phone }}</el-text>
<el-icon color="#666">
<CopyDocument />
</el-icon>
</div>
</div>
<div class="row">
<div class="left">
<div class="icon">
<el-icon color="#fff">
<UserFilled />
</el-icon>
</div>
<el-text>QQ告前咨询</el-text>
</div>
<div class="right hover" @click="copyHandle(helpInfo.qq_consult)">
<el-text type="primary">{{ helpInfo.qq_consult }}</el-text>
<el-icon color="#666">
<CopyDocument />
</el-icon>
</div>
</div>
<div class="row" style="margin-top: 4px;">
<div class="left">
<div class="icon" style="background-color: #fff;">
</div>
<el-text>QQ投诉通道</el-text>
</div>
<div class="right hover" @click="copyHandle(helpInfo.qq_complaint)">
<el-text type="primary">{{ helpInfo.qq_complaint }}</el-text>
<el-icon color="#666">
<CopyDocument />
</el-icon>
</div>
</div>
<div class="row">
<div class="left">
<div class="icon">
<el-icon color="#fff">
<Check />
</el-icon>
</div>
<el-text>上班时间</el-text>
</div>
<div class="right">
<el-text type="primary">{{ helpInfo.work_time }}</el-text>
</div>
</div>
</div>
<div class="item">
<div class="title">官方公众号</div>
<div class="intro">
<el-text type="info">关注后即可查看所有操作教程视频</el-text>
</div>
<div class="ewm_wrap">
<el-image :src="helpInfo.help_ac_qrcode" style="width: 145px;height:145px;"></el-image>
<el-text type="danger">关注后即可查看所有操作教程视频</el-text>
<el-text type="info">微信扫描识别二维码关注获取更多服务</el-text>
</div>
<div class="step_wrap" v-if="helpInfo.official_account">
<div class="row" v-for="(item, index) in helpInfo.official_account.split(';')" :key="index">
<div class="left">
<div class="icon">
<span class="t">{{ index + 1 }}</span>
</div>
<el-text>{{ item }}</el-text>
</div>
</div>
</div>
</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import useClipboard from "vue-clipboard3";
import { getHelp } from '@/api/system'
const { toClipboard } = useClipboard();
const visible = ref(false);
// 复制
async function copyHandle(text) {
try {
await toClipboard(text);
ElNotification({
title: "成功",
message: `复制成功`,
type: "success",
});
console.log("Copied to clipboard");
} catch (e) {
console.error(e);
}
}
// 获取帮助中心信息
const helpInfo = ref({});
async function getHelpAjax() {
try {
const res = await getHelp();
helpInfo.value = res;
} catch (error) {
console.log(error);
}
}
onMounted(() => {
getHelpAjax()
});
</script>
<style lang="scss" scoped>
.header {
font-size: 16px;
color: #333;
}
.wrap {
display: flex;
margin-top: 37px;
padding: 0 28px;
.item {
flex: 1;
&:first-child {
border-right: 2px solid #EDEDED;
padding-right: 38px;
}
&:last-child {
padding-left: 38px;
}
.title {
font-size: 16px;
color: #333;
font-weight: bold;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
.left {
display: flex;
align-items: center;
gap: 8px;
.icon {
width: 22px;
height: 22px;
border-radius: 50%;
background-color: var(--el-color-primary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.t {
font-size: 12px;
color: #fff;
}
}
}
.right {
display: flex;
align-items: center;
gap: 4px;
&.hover {
cursor: pointer;
}
}
}
.ewm_wrap {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 16px;
gap: 16px;
}
}
}
</style>

View File

@@ -2,6 +2,8 @@
<div class="navbar__right"> <div class="navbar__right">
<!-- 非手机设备窄屏才显示 --> <!-- 非手机设备窄屏才显示 -->
<template v-if="!isMobile"> <template v-if="!isMobile">
<!-- 帮助中心 -->
<HellpCenter />
<!-- 搜索 --> <!-- 搜索 -->
<MenuSearch /> <MenuSearch />
@@ -35,6 +37,7 @@ import { useAppStore, useSettingsStore } from "@/store";
import UserProfile from "./UserProfile.vue"; import UserProfile from "./UserProfile.vue";
import Notification from "./Notification.vue"; import Notification from "./Notification.vue";
import HellpCenter from "./HellpCenter.vue";
const appStore = useAppStore(); const appStore = useAppStore();
const settingStore = useSettingsStore(); const settingStore = useSettingsStore();
@@ -48,7 +51,7 @@ const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE);
align-items: center; align-items: center;
justify-content: center; justify-content: center;
& > * { &>* {
display: inline-block; display: inline-block;
min-width: 40px; min-width: 40px;
height: $navbar-height; height: $navbar-height;
@@ -67,12 +70,12 @@ const isMobile = computed(() => appStore.device === DeviceEnum.MOBILE);
margin: 10px 0; margin: 10px 0;
} }
.dark .navbar__right > *:hover { .dark .navbar__right>*:hover {
background: rgb(255 255 255 / 20%); background: rgb(255 255 255 / 20%);
} }
.layout-top .navbar__right > *, .layout-top .navbar__right>*,
.layout-mix .navbar__right > * { .layout-mix .navbar__right>* {
color: #333; color: #333;
} }
</style> </style>

View File

@@ -10,41 +10,25 @@
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item @click="handleOpenUserProfile">店铺配置</el-dropdown-item> <el-dropdown-item @click="handleOpenUserProfile">店铺配置</el-dropdown-item>
<el-dropdown-item divided @click="dialogVisible = true">修改密码</el-dropdown-item> <el-dropdown-item divided @click="refResetPasswordRef.show(shopInfo)">修改密码</el-dropdown-item>
<el-dropdown-item divided @click="logout">退出登录</el-dropdown-item> <el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<el-dialog title="修改密码" modal-append-to-body append-to-body v-model="dialogVisible" @close="reset" width="400px"> <resetPassword ref="refResetPasswordRef" />
<el-form ref="refForm" :model="form" :rules="rules">
<el-form-item label="旧密码" prop="oldPass">
<el-input type="password" show-password v-model="form.oldPass" placeholder="请输入旧密码"></el-input>
</el-form-item>
<el-form-item label="新密码" prop="newPass">
<el-input type="password" show-password v-model="form.newPass" placeholder="请输入新密码"></el-input>
</el-form-item>
<el-form-item label="确认新密码" prop="rnewPass">
<el-input type="password" show-password v-model="form.rnewPass" placeholder="请再次输入新密码"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="formLoading" @click="submitHandle"> </el-button>
</span>
</template>
</el-dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import sysUser from "@/api/account/sysUser";
defineOptions({ defineOptions({
name: "UserProfile", name: "UserProfile",
}); });
import { ElNotification } from "element-plus";
import { useTagsViewStore, useUserStore } from "@/store"; import { useTagsViewStore, useUserStore } from "@/store";
import { useCartsStore } from '@/store/modules/carts' import { useCartsStore } from '@/store/modules/carts'
import resetPassword from "@/components/resetPassword/index.vue";
const shopInfo = JSON.parse(localStorage.getItem("userInfo") || "{}");
const refResetPasswordRef = ref(null);
const tagsViewStore = useTagsViewStore(); const tagsViewStore = useTagsViewStore();
const userStore = useUserStore(); const userStore = useUserStore();
@@ -53,91 +37,6 @@ const cartStore = useCartsStore()
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const dialogVisible = ref(false);
const formLoading = ref(false);
const form = reactive({
oldPass: "",
newPass: "",
rnewPass: "",
});
function reset() {
form.oldPass = "";
form.newPass = "";
form.rnewPass = "";
refForm.value.resetFields();
}
const validateNewPass = (rule: any, value: string, callback: (error?: Error) => void) => {
if (!form.newPass) {
callback(new Error(" "));
} else if (form.newPass === form.oldPass) {
callback(new Error("请输入与旧密码不同的新密码"));
} else {
callback();
}
};
const validateRnewPass = (rule: any, value: string, callback: (error?: Error) => void) => {
if (!form.rnewPass) {
callback(new Error(" "));
} else if (form.rnewPass !== form.newPass) {
callback(new Error("两次密码输入不一致"));
} else {
callback();
}
};
const rules = {
oldPass: [
{
required: true,
message: " ",
trigger: "blur",
},
],
newPass: [
{
required: true,
validator: validateNewPass,
trigger: "blur",
},
],
rnewPass: [
{
required: true,
validator: validateRnewPass,
trigger: "blur",
},
],
};
const refForm = ref();
// 提交修改密码
function submitHandle() {
refForm.value.validate(async (vaild: boolean) => {
if (vaild) {
try {
formLoading.value = true;
const res = await sysUser.pwd({
originalPassword: form.oldPass,
checkPassword: form.newPass,
password: form.newPass,
});
ElNotification.success("修改成功,请重新登陆");
userStore
.logout()
.then(() => {
tagsViewStore.delAllViews();
})
.then(() => {
router.push(`/login?redirect=${route.fullPath}`);
});
} catch (error) {
formLoading.value = false;
console.log(error);
}
}
});
}
/** /**
* 打开个人中心页面 * 打开个人中心页面
*/ */

View File

@@ -16,6 +16,8 @@ import {
OrderCostSummary, OrderCostSummary,
} from "./types"; } from "./types";
import { getCompatibleFieldValue } from "./utils";
/** /**
* 返回商品单价 * 返回商品单价
* @param goods 商品 * @param goods 商品
@@ -46,7 +48,13 @@ export function returnGoodsPrice(
// 限时折扣 // 限时折扣
if (limitTimeDiscount && limitTimeDiscount.id) { if (limitTimeDiscount && limitTimeDiscount.id) {
//优先使用 //优先使用
if (goods.isTimeDiscount || goods.is_time_discount) { // 兼容 isTimeDiscount/is_time_discount这里顺便处理该字段的命名兼容
const isTimeDiscount = getCompatibleFieldValue(
goods,
"isTimeDiscount",
"is_time_discount"
);
if (isTimeDiscount) {
return new BigNumber(goods.salePrice) return new BigNumber(goods.salePrice)
.times(limitTimeDiscount.discountRate / 100) .times(limitTimeDiscount.discountRate / 100)
.decimalPlaces(2, BigNumber.ROUND_UP) .decimalPlaces(2, BigNumber.ROUND_UP)
@@ -169,7 +177,15 @@ export function returnCanDikouGoodsArr(args: CanDikouGoodsArrArgs) {
}) })
.filter((v) => { .filter((v) => {
const canUseNum = (v.num ?? 0) - (v.returnNum || 0); const canUseNum = (v.num ?? 0) - (v.returnNum || 0);
if (canUseNum <= 0 || v.is_temporary || v.is_gift) { // 兼容 is_temporary/isTemporary 和 is_gift/isGift
const isTemporary = getCompatibleFieldValue(
v,
"isTemporary",
"is_temporary"
);
const isGift = getCompatibleFieldValue(v, "isGift", "is_gift");
if (canUseNum <= 0 || isTemporary || isGift) {
return false; return false;
} }
@@ -188,7 +204,13 @@ function returnGoodsIsUseVipPrice(
user: ShopUserInfo, user: ShopUserInfo,
goods: BaseCartItem goods: BaseCartItem
) { ) {
if (goods.is_time_discount) { // 兼容 isTimeDiscount/is_time_discount
const isTimeDiscount = getCompatibleFieldValue(
goods,
"isTimeDiscount",
"is_time_discount"
);
if (isTimeDiscount) {
return false; return false;
} }
if (shopInfo.isMemberPrice != 1 || user.isVip != 1) { if (shopInfo.isMemberPrice != 1 || user.isVip != 1) {
@@ -214,12 +236,16 @@ function returnCanCalcGoodsList(
user: ShopUserInfo user: ShopUserInfo
) { ) {
return canCalcGoodsArr.filter((goods) => { return canCalcGoodsArr.filter((goods) => {
if ( // 兼容 isTimeDiscount/is_time_discount
!coupon.discountShare && const isTimeDiscount = getCompatibleFieldValue(
(goods.is_time_discount || goods.isTimeDiscount) goods,
) { "isTimeDiscount",
"is_time_discount"
);
if (!coupon.discountShare && isTimeDiscount) {
return false; return false;
} }
if ( if (
!coupon.vipPriceShare && !coupon.vipPriceShare &&
returnGoodsIsUseVipPrice(shopInfo, user, goods) returnGoodsIsUseVipPrice(shopInfo, user, goods)
@@ -288,12 +314,14 @@ export function returnCouponCanUse(args: couponCalcParams) {
return coupon.thresholdFoods.find((food) => food.id == v.productId); return coupon.thresholdFoods.find((food) => food.id == v.productId);
}); });
} }
canCalcGoodsArr = returnCanCalcGoodsList( canCalcGoodsArr = returnCanCalcGoodsList(
canCalcGoodsArr, canCalcGoodsArr,
coupon, coupon,
shopInfo, shopInfo,
user user
); );
fullAmount = canCalcGoodsArr.reduce((pre, cur) => { fullAmount = canCalcGoodsArr.reduce((pre, cur) => {
return ( return (
pre + pre +
@@ -328,6 +356,7 @@ export function returnCouponCanUse(args: couponCalcParams) {
reason: "当前选中的券不可与其他券同享", reason: "当前选中的券不可与其他券同享",
}; };
} }
// 满减券和折扣券计算门槛金额是否满足 // 满减券和折扣券计算门槛金额是否满足
if ([1, 3].includes(coupon.type)) { if ([1, 3].includes(coupon.type)) {
if (canCalcGoodsArr.length <= 0) { if (canCalcGoodsArr.length <= 0) {
@@ -438,7 +467,7 @@ export function calcDiscountGoodsArrPrice(
) { ) {
let hasCountNum = 0; let hasCountNum = 0;
let discountPrice = 0; let discountPrice = 0;
let hasDiscountGoodsArr = []; let hasDiscountGoodsArr:BaseCartItem[] = [];
for (let i = 0; i < discountGoodsArr.length; i++) { for (let i = 0; i < discountGoodsArr.length; i++) {
if (hasCountNum >= discountNum) { if (hasCountNum >= discountNum) {
@@ -457,12 +486,15 @@ export function calcDiscountGoodsArrPrice(
discountPrice += realPrice * num; discountPrice += realPrice * num;
hasCountNum += num; hasCountNum += num;
if(goods){
hasDiscountGoodsArr.push({ hasDiscountGoodsArr.push({
...goods, ...goods,
num, num,
}); });
} }
}
return { return {
discountPrice, discountPrice,
hasDiscountGoodsArr, hasDiscountGoodsArr,
@@ -609,7 +641,7 @@ export function returnCouponProductDiscount(
let { useFoods, discountNum, useRule } = coupon; let { useFoods, discountNum, useRule } = coupon;
discountNum = discountNum || 0; discountNum = discountNum || 0;
//抵扣商品数组 //抵扣商品数组
let discountGoodsArr = []; let discountGoodsArr:BaseCartItem[] = [];
//抵扣全部商品 //抵扣全部商品
if (useFoods.length === 0) { if (useFoods.length === 0) {
@@ -656,9 +688,17 @@ function returnCouponBuyOneGiveOneDiscount(
) { ) {
const { useFoods, useRule } = coupon; const { useFoods, useRule } = coupon;
//抵扣商品 //抵扣商品
let discountGoods = undefined; let discountGoods:BaseCartItem | undefined = undefined;
//符合买一送一条件的商品 //符合买一送一条件的商品(数量>=2 + 非临时/非赠品)
const canUseGoods = canDikouGoodsArr.filter((v) => (v.num || 0) >= 2); const canUseGoods = canDikouGoodsArr.filter((v) => {
const isTemporary = getCompatibleFieldValue(
v,
"isTemporary",
"is_temporary"
);
const isGift = getCompatibleFieldValue(v, "isGift", "is_gift");
return (v.num || 0) >= 2 && !isTemporary && !isGift;
});
//抵扣全部商品 //抵扣全部商品
if (useFoods.length === 0) { if (useFoods.length === 0) {
if (useRule == "price_asc") { if (useRule == "price_asc") {
@@ -710,9 +750,17 @@ function returnSecoendDiscount(
) { ) {
const { useFoods, useRule } = coupon; const { useFoods, useRule } = coupon;
//抵扣商品 //抵扣商品
let discountGoods = undefined; let discountGoods:BaseCartItem | undefined = undefined;
//符合条件的商品 //符合条件的商品(数量>=2 + 非临时/非赠品)
const canUseGoods = canDikouGoodsArr.filter((v) => (v.num || 0) >= 2); const canUseGoods = canDikouGoodsArr.filter((v) => {
const isTemporary = getCompatibleFieldValue(
v,
"isTemporary",
"is_temporary"
);
const isGift = getCompatibleFieldValue(v, "isGift", "is_gift");
return (v.num || 0) >= 2 && !isTemporary && !isGift;
});
//抵扣全部商品 //抵扣全部商品
if (useFoods.length === 0) { if (useFoods.length === 0) {
if (useRule == "price_asc") { if (useRule == "price_asc") {
@@ -767,7 +815,14 @@ export function returnCanDikouGoods(
) { ) {
const result = arr const result = arr
.filter((v) => { .filter((v) => {
return !v.is_temporary && !v.is_gift; // 兼容 is_temporary/isTemporary 和 is_gift/isGift
const isTemporary = getCompatibleFieldValue(
v,
"isTemporary",
"is_temporary"
);
const isGift = getCompatibleFieldValue(v, "isGift", "is_gift");
return !isTemporary && !isGift;
}) })
.filter((v) => { .filter((v) => {
return (v.num || 0) > 0; return (v.num || 0) > 0;
@@ -781,9 +836,6 @@ export function returnCanDikouGoods(
return result; return result;
} }
export const utils = { export const utils = {
returnGoodsPrice, returnGoodsPrice,
returnGoodsGroupMap, returnGoodsGroupMap,

View File

@@ -117,7 +117,7 @@ export function returnCanUseLimitTimeDiscount(
useVipPrice: boolean, useVipPrice: boolean,
idKey = "product_id" idKey = "product_id"
) { ) {
goods={...goods,product_id:goods.product_id||goods.productId|| goods.id|| ''} goods = { ...goods, product_id: goods.product_id || goods.productId || goods.id || '' }
if (!limitTimeDiscount || !limitTimeDiscount.id) { if (!limitTimeDiscount || !limitTimeDiscount.id) {
return false; return false;
} }
@@ -137,10 +137,10 @@ export function returnCanUseLimitTimeDiscount(
} }
if (useVipPrice && goods.hasOwnProperty("memberPrice")) { if (useVipPrice && goods.hasOwnProperty("memberPrice")) {
if ( goods.memberPrice * 1 <= 0) { if (goods.memberPrice * 1 <= 0) {
return true; return true;
}else{ } else {
return false; return false;
} }
} }
@@ -171,40 +171,26 @@ function returnLimitPrice(
const discountRate = new BigNumber(limitTimeDiscount.discountRate).dividedBy( const discountRate = new BigNumber(limitTimeDiscount.discountRate).dividedBy(
100 100
); );
let canuseLimit = false;
const canuseLimit = returnCanUseLimitTimeDiscount( if (goods.hasOwnProperty('isTimeDiscount') || goods.hasOwnProperty('is_time_discount')) {
canuseLimit = goods.isTimeDiscount ? true : goods.is_time_discount ? true : false;
} else {
canuseLimit = returnCanUseLimitTimeDiscount(
goods, goods,
limitTimeDiscount, limitTimeDiscount,
useVipPrice useVipPrice
); );
}
if (canuseLimit) { if (canuseLimit) {
//可以使用限时折扣 //可以使用限时折扣
if (limitTimeDiscount.discountPriority == "limit-time") {
//限时价优先
const result = BigNumber(goods.salePrice) const result = BigNumber(goods.salePrice)
.times(discountRate) .times(discountRate)
.decimalPlaces(2, BigNumber.ROUND_UP) .decimalPlaces(2, BigNumber.ROUND_UP)
.toNumber(); .toNumber();
return result; return result;
} }
if (limitTimeDiscount.discountPriority == "vip-price") {
//会员价优先
if (useVipPrice && goods.memberPrice && goods.memberPrice * 1 > 0) {
//使用会员价
return returnMemberPrice(useVipPrice, goods);
} else {
//不使用会员价
const result = BigNumber(goods.salePrice)
.times(discountRate)
.decimalPlaces(2, BigNumber.ROUND_UP)
.toNumber();
return result;
}
}
} else {
//不可以使用限时折扣
//会员价优先 //会员价优先
if (useVipPrice) { if (useVipPrice) {
//使用会员价 //使用会员价
@@ -212,7 +198,6 @@ function returnLimitPrice(
} else { } else {
return goods.salePrice; return goods.salePrice;
} }
}
} }
/** /**
@@ -287,7 +272,7 @@ export function calcFullReductionActivityFullAmount(
let amount = 0; let amount = 0;
for (let goods of goodsList) { for (let goods of goodsList) {
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
if (goods.is_temporary ||goods.isTemporary || goods.is_gift ||goods.isGift || availableNum <= 0) { if (goods.is_temporary || goods.isTemporary || goods.is_gift || goods.isGift || availableNum <= 0) {
//临时菜,赠菜,数量<=0的商品不计算 //临时菜,赠菜,数量<=0的商品不计算
continue; continue;
} }
@@ -396,7 +381,7 @@ export function truncateToTwoDecimals(num: number | string): number {
* @returns 是否临时菜 * @returns 是否临时菜
*/ */
export function isTemporaryGoods(goods: BaseCartItem): boolean { export function isTemporaryGoods(goods: BaseCartItem): boolean {
return !!goods.is_temporary|| !!goods.isTemporary; return !!goods.is_temporary || !!goods.isTemporary;
} }
/** /**
@@ -581,9 +566,15 @@ export function calcSingleGoodsRealPrice(
const { isMember, memberDiscountRate, limitTimeDiscount: activity } = config; const { isMember, memberDiscountRate, limitTimeDiscount: activity } = config;
//如果是增菜价格为0 //如果是增菜价格为0
if (goods.is_gift||goods.isGift) { if (goods.is_gift || goods.isGift) {
return 0; return 0;
} }
//临时菜
if (goods.is_temporary || goods.isTemporary) {
return new BigNumber(goods.discountSaleAmount || 0)
.toNumber()
}
console.log("calcSingleGoodsRealPrice", goods);
// 1. 优先级1商家改价改价后单价>0才生效 // 1. 优先级1商家改价改价后单价>0才生效
if (goods.discountSaleAmount && goods.discountSaleAmount > 0) { if (goods.discountSaleAmount && goods.discountSaleAmount > 0) {
@@ -594,14 +585,16 @@ export function calcSingleGoodsRealPrice(
const memberPrice = new BigNumber( const memberPrice = new BigNumber(
calcMemberPrice(goods, isMember, memberDiscountRate) calcMemberPrice(goods, isMember, memberDiscountRate)
); );
if(goods.is_time_discount||goods.isTimeDiscount){
if (goods.is_time_discount || goods.isTimeDiscount) {
//限时折扣优先 //限时折扣优先
return truncateToTwoDecimals( const limitPrice = truncateToTwoDecimals(
new BigNumber(goods.salePrice) new BigNumber(goods.salePrice)
.times((activity?activity.discountRate:100) / 100) .times((activity ? activity.discountRate : 100) / 100)
.decimalPlaces(2, BigNumber.ROUND_UP) .decimalPlaces(2, BigNumber.ROUND_UP)
.toNumber() .toNumber()
); );
return limitPrice;
} }
// 3. 优先级3营销活动折扣如限时折扣需按商品ID匹配活动 // 3. 优先级3营销活动折扣如限时折扣需按商品ID匹配活动
let isActivityApplicable = false; let isActivityApplicable = false;
@@ -619,22 +612,22 @@ export function calcSingleGoodsRealPrice(
if (!activity || !isActivityApplicable) { if (!activity || !isActivityApplicable) {
return memberPrice.toNumber(); return memberPrice.toNumber();
} }
// //限时折扣优先或者会员价优先但是不是会员或者未开启会员价格时限时折扣优先
// if (
// activity.discountPriority == "limit-time" ||
// (activity.discountPriority == "vip-price" && !isMember) ||
// (activity.discountPriority == "vip-price" && isMember && !goods.memberPrice)
// )
// {
// //限时折扣优先
// return truncateToTwoDecimals(
// new BigNumber(goods.salePrice)
// .times(activity.discountRate / 100)
// .decimalPlaces(2, BigNumber.ROUND_UP)
// .toNumber()
// );
// }
//限时折扣优先或者会员价优先但是不是会员或者未开启会员价格时限时折扣优先
if (
activity.discountPriority == "limit-time" ||
(activity.discountPriority == "vip-price" && !isMember) ||
(activity.discountPriority == "vip-price" && isMember && !goods.memberPrice)
)
{
//限时折扣优先
return truncateToTwoDecimals(
new BigNumber(goods.salePrice)
.times(activity.discountRate / 100)
.decimalPlaces(2, BigNumber.ROUND_UP)
.toNumber()
);
}
if (activity.discountPriority == "vip-price" && isMember) { if (activity.discountPriority == "vip-price" && isMember) {
return memberPrice.toNumber(); return memberPrice.toNumber();
} }
@@ -671,17 +664,21 @@ export function calcGoodsOriginalAmount(goodsList: BaseCartItem[]): number {
let total = new BigNumber(0); let total = new BigNumber(0);
for (const goods of goodsList) { for (const goods of goodsList) {
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); const availableNum = Number(goods.number || 0) - Number(goods.returnNum || 0);
const safeNum = Math.max(0, availableNum);
let basePrice = new BigNumber(0); let basePrice = new BigNumber(0);
if (goods.is_temporary||goods.isTemporary) {
basePrice = new BigNumber(goods?.discountSaleAmount ?? 0); if (goods.is_temporary || goods.isTemporary) {
} else if (goods.is_gift||goods.isGift) { // ✅ 修复这里:用 ?? 0永远不会把 0 变成 NaN
basePrice = new BigNumber(Number(goods.discountSaleAmount) ?? 0);
} else if (goods.is_gift || goods.isGift) {
basePrice = new BigNumber(0); basePrice = new BigNumber(0);
} else { } else {
basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice); // SKU原价优先 basePrice = new BigNumber(goods.skuData?.salePrice ?? goods.salePrice ?? 0);
} }
total = total.plus(basePrice.multipliedBy(availableNum)); total = total.plus(basePrice.multipliedBy(safeNum));
} }
return truncateToTwoDecimals(total.toNumber()); return truncateToTwoDecimals(total.toNumber());
@@ -707,6 +704,7 @@ export function calcGoodsRealAmount(
const availableNum = Math.max(0, goods.number - (goods.returnNum || 0)); const availableNum = Math.max(0, goods.number - (goods.returnNum || 0));
if (availableNum <= 0) continue; if (availableNum <= 0) continue;
const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, config)); const realPrice = new BigNumber(calcSingleGoodsRealPrice(goods, config));
console.log("realPrice", realPrice.toNumber());
total = total.plus(realPrice.multipliedBy(availableNum)); total = total.plus(realPrice.multipliedBy(availableNum));
} }
@@ -980,12 +978,12 @@ export function calcPointDeduction(
function calcVipDiscountAmount( function calcVipDiscountAmount(
goodsRealAmount: number, goodsRealAmount: number,
shopUserInfo: ShopUserInfo shopUserInfo: ShopUserInfo,
isMember: boolean,
): number { ): number {
if (!isMember) return 0;
if (!shopUserInfo.isVip || shopUserInfo.discount === 0) return 0; if (!shopUserInfo.isVip || shopUserInfo.discount === 0) return 0;
if (shopUserInfo.isVip == 1 && shopUserInfo.isMemberPrice != 1) {
return 0;
}
return truncateToTwoDecimals( return truncateToTwoDecimals(
new BigNumber(goodsRealAmount) new BigNumber(goodsRealAmount)
.times((100 - (shopUserInfo.discount || 100)) / 100) .times((100 - (shopUserInfo.discount || 100)) / 100)
@@ -1039,7 +1037,8 @@ export function calculateOrderCostSummary(
goodsOriginalAmount, goodsOriginalAmount,
goodsRealAmount goodsRealAmount
); // 商品折扣金额 ); // 商品折扣金额
console.log("goodsOriginalAmount", goodsOriginalAmount);
console.log("goodsRealAmount", goodsRealAmount);
const newUserDiscount = config.newUserDiscount || 0; // 新客立减 const newUserDiscount = config.newUserDiscount || 0; // 新客立减
// 其他费用计算(原有逻辑不变) ------------------------------ // 其他费用计算(原有逻辑不变) ------------------------------
@@ -1252,7 +1251,8 @@ export function calculateOrderCostSummary(
.minus(newUserDiscount) .minus(newUserDiscount)
.minus(fullReductionAmount) .minus(fullReductionAmount)
.toNumber(), .toNumber(),
shopUserInfo shopUserInfo,
config.isMember,
); );
// ------------------------------ 6. 最终实付金额计算 ------------------------------ // ------------------------------ 6. 最终实付金额计算 ------------------------------
const finalPayAmountBn = new BigNumber(goodsRealAmount) const finalPayAmountBn = new BigNumber(goodsRealAmount)

View File

@@ -29,9 +29,7 @@ export function canUseLimitTimeDiscount(
) { ) {
shopInfo = shopInfo || {}; shopInfo = shopInfo || {};
shopUserInfo = shopUserInfo || {}; shopUserInfo = shopUserInfo || {};
if(shopInfo.isMemberPrice){
shopUserInfo.isMemberPrice=1
}
if (!limitTimeDiscountRes || !limitTimeDiscountRes.id) { if (!limitTimeDiscountRes || !limitTimeDiscountRes.id) {
return false; return false;
} }
@@ -88,6 +86,9 @@ export function returnPrice(args: returnPriceArgs) {
shopId: 0, shopId: 0,
useType: "", useType: "",
}; };
if(!goods){
return 0;
}
const canUseFoods = (limitTimeDiscountRes.foods || "").split(","); const canUseFoods = (limitTimeDiscountRes.foods || "").split(",");
const includesGoods = const includesGoods =
limitTimeDiscountRes.foodType == 1 || limitTimeDiscountRes.foodType == 1 ||
@@ -96,8 +97,7 @@ export function returnPrice(args: returnPriceArgs) {
shopUserInfo = shopUserInfo || {}; shopUserInfo = shopUserInfo || {};
if ( if (
shopUserInfo.isMemberPrice == 1 && shopUserInfo.isMemberPrice == 1 &&
shopUserInfo.isVip == 1 && shopUserInfo.isVip == 1
shopInfo.isMemberPrice == 1
) { ) {
const memberPrice = goods.memberPrice || goods.salePrice; const memberPrice = goods.memberPrice || goods.salePrice;

33
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,33 @@
/**
* 通用字段兼容工具函数:处理驼峰/下划线命名的字段取值
* @param obj 目标对象(如商品信息 BaseCartItem
* @param camelCaseKey 驼峰命名字段(如 'isTemporary'
* @param snakeCaseKey 下划线命名字段(如 'is_temporary'
* @param defaultValue 默认值(默认 false适配布尔类型字段
* @returns 字段值(优先取存在的字段,无则返回默认值)
*/
export function getCompatibleFieldValue(
obj: Record<string, any>,
camelCaseKey: string,
snakeCaseKey: string,
defaultValue: boolean = false
): boolean {
// 优先判断驼峰字段(如果存在且不是 undefined/null
if (
obj.hasOwnProperty(camelCaseKey) &&
obj[camelCaseKey] !== undefined &&
obj[camelCaseKey] !== null
) {
return Boolean(obj[camelCaseKey]);
}
// 再判断下划线字段
if (
obj.hasOwnProperty(snakeCaseKey) &&
obj[snakeCaseKey] !== undefined &&
obj[snakeCaseKey] !== null
) {
return Boolean(obj[snakeCaseKey]);
}
// 都不存在时返回默认值(布尔类型字段默认 false
return defaultValue;
}

View File

@@ -24,53 +24,17 @@ export const constantRoutes: RouteRecordRaw[] = [
}, },
{ {
path: "/", path: '/',
component: Layout, component: Layout,
redirect: "/index", redirect: '/index',
meta: {
title: "数据中心",
icon: "数据统计",
alwaysShow: true,
},
children: [ children: [
{ {
path: "index", path: 'index',
component: () => import("@/views/data/index.vue"), component: () => import('@/views/index/index.vue'),
name: "", name: 'index',
meta: { meta: {
title: "经营数据", title: '首页',
affix: false, icon: 'homepage',
keepAlive: true,
},
},
{
path: "sales",
name: "",
component: () => import("@/views/data/sales.vue"),
meta: {
title: "销售统计",
affix: false,
keepAlive: true,
},
},
{
path: "table",
name: "",
component: () => import("@/views/data/table.vue"),
meta: {
title: "台桌统计",
affix: false,
keepAlive: true,
},
},
{
path: "work",
name: "workStatistics",
component: () => import("@/views/data/work.vue"),
meta: {
title: "收银交班记录",
affix: false,
keepAlive: true,
}, },
}, },
{ {
@@ -83,9 +47,79 @@ export const constantRoutes: RouteRecordRaw[] = [
component: () => import("@/views/error/404.vue"), component: () => import("@/views/error/404.vue"),
meta: { hidden: true }, meta: { hidden: true },
}, },
], ]
}, },
// { // {
// path: "/",
// component: Layout,
// redirect: "/index",
// meta: {
// title: "数据中心",
// icon: "数据统计",
// alwaysShow: true,
// },
// children: [
// {
// path: "index",
// component: () => import("@/views/data/index.vue"),
// name: "",
// meta: {
// title: "经营数据",
// affix: false,
// keepAlive: true,
// },
// },
// {
// path: "sales",
// name: "",
// component: () => import("@/views/data/sales.vue"),
// meta: {
// title: "销售统计",
// affix: false,
// keepAlive: true,
// },
// },
// {
// path: "table",
// name: "",
// component: () => import("@/views/data/table.vue"),
// meta: {
// title: "台桌统计",
// affix: false,
// keepAlive: true,
// },
// },
// {
// path: "finance",
// name: "",
// component: () => import("@/views/data/finance.vue"),
// meta: {
// title: "财务报表"
// },
// },
// {
// path: "work",
// name: "workStatistics",
// component: () => import("@/views/data/work.vue"),
// meta: {
// title: "收银交班记录",
// affix: false,
// keepAlive: true,
// },
// },
// {
// path: "401",
// component: () => import("@/views/error/401.vue"),
// meta: { hidden: true },
// },
// {
// path: "404",
// component: () => import("@/views/error/404.vue"),
// meta: { hidden: true },
// },
// ],
// },
// {
// path: "/shop", // path: "/shop",
// component: Layout, // component: Layout,
// meta: { // meta: {

View File

@@ -2,10 +2,11 @@ import { store } from "@/store";
import WebSocketManager, { type ApifoxModel, msgType } from "@/utils/websocket"; import WebSocketManager, { type ApifoxModel, msgType } from "@/utils/websocket";
import orderApi from "@/api/order/order"; import orderApi from "@/api/order/order";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
const shopUser = useUserStoreHook();
import { customTruncateToTwoDecimals } from '@/utils/tools' import { customTruncateToTwoDecimals } from '@/utils/tools'
import productApi from "@/api/product/index"; import productApi from "@/api/product/index";
const shopUser = useUserStoreHook();
export interface CartsState { export interface CartsState {
id: string | number; id: string | number;
[property: string]: any; [property: string]: any;

View File

@@ -3,10 +3,12 @@ import WebSocketManager, { type ApifoxModel, msgType } from "@/utils/websocket";
import orderApi from "@/api/order/order"; import orderApi from "@/api/order/order";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import productApi from "@/api/product/index"; import productApi from "@/api/product/index";
import categoryApi from "@/api/product/productclassification";
import shopUserApi from '@/api/account/shopUser' import shopUserApi from '@/api/account/shopUser'
import limitTimeDiscountApi from '@/api/market/limitTimeDiscount.js' import limitTimeDiscountApi from '@/api/market/limitTimeDiscount.js'
import { BigNumber } from "bignumber.js"; import { BigNumber } from "bignumber.js";
import _ from "lodash"; import _ from "lodash";
import PinyinMatch from 'pinyin-match'
// 导入工具库及相关类型 // 导入工具库及相关类型
import { import {
@@ -82,8 +84,6 @@ export const useCartsStore = defineStore("carts", () => {
default: productType = GoodsType.NORMAL; default: productType = GoodsType.NORMAL;
} }
return { return {
...item, ...item,
id: item.id, id: item.id,
@@ -226,7 +226,25 @@ export const useCartsStore = defineStore("carts", () => {
const isLinkFinshed = ref(false); const isLinkFinshed = ref(false);
// 当前购物车数据(现在 getAllGoodsList 能直接访问) // 当前购物车数据(现在 getAllGoodsList 能直接访问)
const list = useStorage<any[]>("carts", []); const list = useStorage<any[]>(
"carts",
[],
localStorage,
{
serializer: {
read: (rawValue) => {
// 没有值时直接返回默认空数组
if (!rawValue) return []
try {
return JSON.parse(rawValue)
} catch {
return []
}
},
write: (value) => JSON.stringify(value),
},
}
)
// 历史订单数据(现在 getAllGoodsList 能直接访问) // 历史订单数据(现在 getAllGoodsList 能直接访问)
const oldOrder = useStorage<any>("Instead_olold_order", { const oldOrder = useStorage<any>("Instead_olold_order", {
detailMap: [], detailMap: [],
@@ -291,11 +309,17 @@ export const useCartsStore = defineStore("carts", () => {
console.log('代客下单页面商品缓存.获取当前店铺可用的限时折扣', limitDiscountRes.value); console.log('代客下单页面商品缓存.获取当前店铺可用的限时折扣', limitDiscountRes.value);
// 1. 先把 query.name 提取出来,接口调用时不传,避免后端限制
const searchName = query.name || '';
const queryParams = { ...query };
delete queryParams.name;
// 2. 调用接口(不带 name 参数,拿全量数据,前端自己过滤)
const res = await productApi.getPage({ const res = await productApi.getPage({
page: 1, page: 1,
size: 999, size: 999,
status: "on_sale", status: "on_sale",
...query, ...queryParams,
}); });
interface ProductItem { interface ProductItem {
@@ -315,7 +339,21 @@ export const useCartsStore = defineStore("carts", () => {
limitDiscountPrice: number; limitDiscountPrice: number;
} }
goods.value = (res.records as ProductItem[]).map((item: ProductItem): GoodsWithDiscount => { // 3. 【核心:拼音 + 汉字 模糊过滤】
let filteredList = res.records as ProductItem[];
if (searchName) {
filteredList = filteredList.filter(item => {
if (!item.name) return false;
// 汉字模糊搜索
const hasName = item.name.includes(searchName);
// 拼音/首字母搜索zs / zhangsan / 张三 都支持)
const hasPinyin = PinyinMatch.match(item.name, searchName);
return hasName || hasPinyin;
});
}
// 4. 价格处理(完全不变)
goods.value = filteredList.map((item: ProductItem): GoodsWithDiscount => {
item.salePrice = item.lowPrice item.salePrice = item.lowPrice
item.memberPrice = item.lowMemberPrice item.memberPrice = item.lowMemberPrice
@@ -332,11 +370,61 @@ export const useCartsStore = defineStore("carts", () => {
} }
}); });
const consList = await productApi.consStock({ shopId: localStorage.getItem("shopId") })
goods.value = addGoodsSoldOutStatus(goods.value, consList)
console.log('代客下单页面商品缓存.goods.value', goods.value); console.log('代客下单页面商品缓存.goods.value', goods.value);
setGoodsMap(goods.value); setGoodsMap(goods.value);
} }
/**
* 给商品列表批量添加 isSoldOut 售罄状态字段
* @param {Array} goodsList - 商品列表 [ { consList, isAutoSoldStock } ]
* @param {Array} consStockList - 真实耗材库存列表 [ { consId, stockNumber } ]
* @returns 带 isSoldOut 字段的新商品列表
*/
function addGoodsSoldOutStatus(goodsList, consStockList) {
// console.log('addGoodsSoldOutStatus.goodsList', goodsList);
// console.log('addGoodsSoldOutStatus.consStockList', consStockList);
// 耗材ID映射真实库存保留
const consMap = _.keyBy(consStockList, item => String(item.consId));
return _.map(goodsList, goods => {
let isSoldOut = false;
// 开启自动售罄才判断
if (goods.isAutoSoldStock === 1 || goods.isAutoSoldStock === true) {
const goodsConsList = goods.consList || [];
// 无耗材 → 不售罄
if (goodsConsList.length === 0) {
isSoldOut = false;
} else {
// 核心:只要有一个耗材 真实库存 < 商品需要量 → 售罄
isSoldOut = _.some(goodsConsList, consItem => {
// 商品绑定的耗材ID对应真实库存ID
const consId = String(consItem.consInfoId);
// 商品需要消耗的数量(你的需求量)
const needStock = consItem.surplusStock || 0;
// 起售数量
const suitNum = goods.type == 'single' ? goods.skuList[0].suitNum : 1;
// 真实库存
const realStock = _.get(consMap, [consId, 'stockNumber'], 0);
// 真实库存 < 需要量 * 起售数量 → 不足 → 售罄
return realStock < needStock * suitNum;
});
}
}
return { ...goods, isSoldOut };
});
}
function setGoodsMap(goods: any[]) { function setGoodsMap(goods: any[]) {
for (let item of goods) { for (let item of goods) {
goodsMap[item.id] = item; goodsMap[item.id] = item;
@@ -642,7 +730,9 @@ export const useCartsStore = defineStore("carts", () => {
sendMessage('add', { ...basic_msg, ...data }); sendMessage('add', { ...basic_msg, ...data });
} }
// 添加购物车/编辑购物车
function add(data: any) { function add(data: any) {
// console.log('添加购物车/编辑购物车===', data);
goods.value.map(item => { goods.value.map(item => {
if (item.id == data.product_id) { if (item.id == data.product_id) {
data.is_time_discount = item.is_time_discount ? 1 : 0 data.is_time_discount = item.is_time_discount ? 1 : 0
@@ -657,8 +747,29 @@ export const useCartsStore = defineStore("carts", () => {
console.log('carts.add===', data); console.log('carts.add===', data);
if (hasCart) { if (hasCart) {
// console.log('编辑', msg);
if (hasCart.number * 1 + msg.number * 1 == 2) {
ElMessage({
type: 'warning',
message: '购物车已有该商品,请确认是否重复',
duration: 4000
})
}
update({ ...hasCart, ...msg, number: hasCart.number * 1 + msg.number * 1 }); update({ ...hasCart, ...msg, number: hasCart.number * 1 + msg.number * 1 });
} else { } else {
// console.log('添加', msg);
let arr = _.flatten(_.values(oldOrder.value.detailMap))
// console.log('添加.arr===', arr);
const isExist = _.some(arr, item => Number(item.productId) === msg.product_id)
// console.log('添加.isExist===', isExist);
if (msg.number == 1 && isExist) {
ElMessage({
type: 'warning',
message: '该商品已下单过,请确认是否重复',
duration: 4000
})
}
sendMessage('add', msg); sendMessage('add', msg);
} }
} }
@@ -670,12 +781,14 @@ export const useCartsStore = defineStore("carts", () => {
} }
function rotTable(newVal: string | number, cart_id = []) { function rotTable(newVal: string | number, cart_id = []) {
if (cart_id.length) {
sendMessage('rottable', { sendMessage('rottable', {
new_table_code: newVal, new_table_code: newVal,
table_code: table_code.value, table_code: table_code.value,
cart_id cart_id
}); });
} }
}
function del(data: any) { function del(data: any) {
sendMessage('del', { id: data.id }); sendMessage('del', { id: data.id });
@@ -833,8 +946,9 @@ export const useCartsStore = defineStore("carts", () => {
} }
// 初始话订单信息/补全历史订单信息 // 初始话订单信息/补全历史订单信息
async function setOldOrder(data: any) { async function setOldOrder(data: any, t: number) {
console.log('补全订单信息', data); console.log('补全历史订单信息来源', t);
// console.log('补全订单信息', data);
oldOrder.value = { oldOrder.value = {
...data, ...data,
detailMap: returnDetailMap(data.detailMap) detailMap: returnDetailMap(data.detailMap)
@@ -849,10 +963,17 @@ export const useCartsStore = defineStore("carts", () => {
let $initParams = {} as ApifoxModel; let $initParams = {} as ApifoxModel;
async function init(initParams: ApifoxModel, $oldOrder: any | undefined) {
await getGoods({}); // 标志位,表示是否正在清空历史订单
if ($oldOrder) setOldOrder($oldOrder); const isClearingOldOrder = ref(false);
else oldOrder.value = { detailMap: [] };
async function init(initParams: ApifoxModel | undefined, $oldOrder: any | undefined) {
console.log('cart.init.initParams', initParams);
console.log('cart.init.$oldOrder', $oldOrder);
if (initParams) { if (initParams) {
initParams.table_code = initParams.table_code || ''; initParams.table_code = initParams.table_code || '';
@@ -860,6 +981,16 @@ export const useCartsStore = defineStore("carts", () => {
$initParams = initParams; $initParams = initParams;
} }
await getGoods({});
if ($oldOrder) {
await setOldOrder($oldOrder);
} else if (table_code.value) {
await getOldOrder(table_code.value);
} else {
oldOrder.value = { detailMap: [] };
}
concocatSocket($initParams); concocatSocket($initParams);
} }
@@ -868,7 +999,7 @@ export const useCartsStore = defineStore("carts", () => {
console.log("收到消息:", msg); console.log("收到消息:", msg);
if (!msg.status) { if (!msg.status) {
if (msg.hasOwnProperty('status') && msg.status !== 1 && msg.operate_type !== 'bulk_edit') { if (msg.hasOwnProperty('status') && msg.status !== 1 && msg.operate_type !== 'bulk_edit') {
return ElMessage.error(msg.message || '操作失败'); return ElMessage.error(msg.msg || '操作失败');
} }
} }
if (msg?.data) { if (msg?.data) {
@@ -977,8 +1108,15 @@ export const useCartsStore = defineStore("carts", () => {
concocatSocket({ ...$initParams, table_code: table_code.value }); concocatSocket({ ...$initParams, table_code: table_code.value });
} }
// WebSocket 监听 product_update 消息
if (msg.operate_type === "product_update") { if (msg.operate_type === "product_update") {
init($initParams, oldOrder.value); if (isClearingOldOrder.value) {
console.log("忽略 product_update 消息,因为正在清空历史订单");
isClearingOldOrder.value = false; // 重置标志位
return;
}
console.log("处理 product_update 消息");
init($initParams, oldOrder.value); // 重新初始化
} }
if (msg.type === "bc") { if (msg.type === "bc") {
@@ -1084,7 +1222,7 @@ export const useCartsStore = defineStore("carts", () => {
getAllGoodsList, getAllGoodsList,
vipUser, vipUser,
changeTableInfo, changeTableInfo,
tableInfo, tableInfo, isClearingOldOrder,
clearHistory clearHistory
}; };
}); });

View File

@@ -11,6 +11,7 @@ import { setToken, setRefreshToken, getRefreshToken, clearToken } from "@/utils/
export const useUserStore = defineStore("user", () => { export const useUserStore = defineStore("user", () => {
// const isShopAdmin = ref(false) // 0商户 1员工 // const isShopAdmin = ref(false) // 0商户 1员工
const isShopAdmin = useStorage("isShopAdmin", false) // 0商户 1员工 const isShopAdmin = useStorage("isShopAdmin", false) // 0商户 1员工
const isAdmin = useStorage("isAdmin", false) // 是否是管理员
const userInfo = useStorage<UserInfo>("userInfo", {} as UserInfo); const userInfo = useStorage<UserInfo>("userInfo", {} as UserInfo);
const promissionList = useStorage<string[]>("promissionList", [] as string[]); const promissionList = useStorage<string[]>("promissionList", [] as string[]);
//美团抖音核销店铺信息 //美团抖音核销店铺信息
@@ -32,6 +33,7 @@ export const useUserStore = defineStore("user", () => {
AuthAPI.login(loginRequest) AuthAPI.login(loginRequest)
.then((data) => { .then((data) => {
isShopAdmin.value = data.loginType == 0 ? true : false; isShopAdmin.value = data.loginType == 0 ? true : false;
isAdmin.value = data.shopInfo.id == 1 ? true : false
Object.assign(userInfo.value, { ...data.shopInfo, shopId: data.shopInfo.id }); Object.assign(userInfo.value, { ...data.shopInfo, shopId: data.shopInfo.id });
promissionList.value = data.promissionList; promissionList.value = data.promissionList;
const token = data.tokenInfo.tokenValue; const token = data.tokenInfo.tokenValue;
@@ -39,7 +41,7 @@ export const useUserStore = defineStore("user", () => {
setRefreshToken(token); setRefreshToken(token);
localStorage.setItem("shopId", "" + data.shopInfo.id); localStorage.setItem("shopId", "" + data.shopInfo.id);
localStorage.setItem("branch_shopId", data.shopInfo.id) localStorage.setItem("branch_shopId", data.shopInfo.id)
resolve(); resolve(data);
}) })
.catch((error) => { .catch((error) => {
reject(error); reject(error);
@@ -120,6 +122,7 @@ export const useUserStore = defineStore("user", () => {
userInfo.value = {} as UserInfo userInfo.value = {} as UserInfo
meituan_douyin_info.value = {} meituan_douyin_info.value = {}
isShopAdmin.value = false isShopAdmin.value = false
isAdmin.value = false
clearToken(); clearToken();
usePermissionStoreHook().resetRouter(); usePermissionStoreHook().resetRouter();
useDictStoreHook().clearDictionaryCache(); useDictStoreHook().clearDictionaryCache();
@@ -137,7 +140,7 @@ export const useUserStore = defineStore("user", () => {
} }
return { return {
isShopAdmin, isShopAdmin, isAdmin,
meituan_douyin_info, meituan_douyin_info,
userInfo, userInfo,
promissionList, promissionList,

View File

@@ -111,8 +111,16 @@ export function downloadFile(obj: BlobPart, name: string, suffix: string, useUni
const link = document.createElement("a"); const link = document.createElement("a");
link.style.display = "none"; link.style.display = "none";
link.href = url; link.href = url;
const newFilename = useUnix ? (parseTime(new Date(), undefined) + "-") : '' + name.trim() // 期望行为:
const fileName = newFilename + "." + suffix; // - 当 useUnix 为 true 且传入 name 时:`${timestamp}-${name}`
// - 当 useUnix 为 true 且 name 为空时:仅 `${timestamp}`(不带多余的 `-`
// - 当 useUnix 为 false使用传入的 name如为空则回退到时间戳
const safeName = (name || "").trim();
const timeStamp = parseTime(new Date(), undefined);
const fileBase = useUnix
? (safeName ? `${timeStamp}-${safeName}` : timeStamp)
: (safeName || timeStamp);
const fileName = fileBase + "." + suffix;
link.setAttribute("download", fileName); link.setAttribute("download", fileName);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
@@ -314,3 +322,41 @@ export function isValidMobile(phone: string): boolean {
const mobileRegex = /^1[3-9]\d{9}$/; const mobileRegex = /^1[3-9]\d{9}$/;
return mobileRegex.test(s); return mobileRegex.test(s);
} }
/**
* 手机号脱敏:隐藏中间四位数字
* @param {string | number} phone - 原始手机号(支持数字/字符串)
* @returns {string} 脱敏后的手机号,如 138****1234
*/
export function maskPhone(phone) {
// 先转成字符串,避免传入数字类型报错
const phoneStr = String(phone).trim();
// 简单校验必须是11位数字
if (!/^1\d{10}$/.test(phoneStr)) {
console.warn('手机号格式不正确');
return phoneStr; // 格式错误直接返回原值
}
// 核心前3位 + **** + 后4位
return phoneStr.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
/**
* 金额保留两位小数 不四舍五入
* @param {number} num 金额数字
* @param {boolean} hasSymbol 是否带¥符号默认true
* @returns {string}
*/
export function formatMoney(num, hasSymbol = true) {
// 防止非数字、空值报错
if (isNaN(Number(num))) num = 0;
// 不四舍五入截断两位小数
const value = Math.floor(Math.abs(num) * 100) / 100;
// 拼接两位小数
const fixedNum = value.toFixed(2);
// 负数处理
const result = num < 0 ? `-${fixedNum}` : fixedNum;
// 默认返回带¥
return hasSymbol ? `¥${result}` : result;
}

View File

@@ -0,0 +1,131 @@
import axios from "axios";
import router from "@/router";
import { getDouyinToken, getToken } from "@/utils/auth";
// 创建axios实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_API_PHP_IMPORT_URL, // api 的 base_url
// baseURL: "/import_api", // api 的 base_url
timeout: 1000 * 20, // 请求超时时间
});
// request拦截器
service.interceptors.request.use(
(config) => {
if (getToken()) {
config.headers["token"] = getToken();
}
if (!config.headers["Content-Type"]) {
config.headers["Content-Type"] = "application/json";
}
return config;
},
(error) => {
// 确保错误回调返回Promise避免状态异常
return Promise.reject(error);
}
);
// response 拦截器
service.interceptors.response.use(
(response) => {
const data = response.data;
console.log(data);
if (data.code == 0) {
ElNotification.error({
title: data.msg,
duration: 5000,
});
return;
}
if (data.code == 439 || data.code == 303) {
ElNotification.error({
title: "请登录",
duration: 5000,
});
return;
}
if (data.code == 4399) {
ElNotification.error({
title: data.msg,
duration: 5000,
});
return data;
}
// if (data.code == 1 && !data.data) {
// ElNotification.success({
// title: data.msg,
// duration: 5000
// })
// return true;
// }
return data.data;
},
(error) => {
console.log(error);
if (axios.isCancel(error)) {
console.log("请求已取消");
ElNotification.error({
title: "请求已取消",
duration: 5000,
});
return Promise.reject("请求已取消");
}
// 兼容blob下载出错json提示
if (
error.response.data instanceof Blob &&
error.response.data.type.toLowerCase().indexOf("json") !== -1
) {
const reader = new FileReader();
reader.readAsText(error.response.data, "utf-8");
reader.onload = function (e) {
const errorMsg = JSON.parse(reader.result).message;
ElNotification.error({
title: errorMsg,
duration: 5000,
});
};
} else {
let code = 0;
try {
code = error.response.data.status;
} catch (e) {
if (error.toString().indexOf("Error: timeout") !== -1) {
ElNotification.error({
title: "网络请求超时",
duration: 5000,
});
return Promise.reject(error);
}
}
console.log(code);
if (code) {
if (code === 401) {
// store.dispatch("LogOut").then(() => {
// // 用户登录界面提示
// Cookies.set("point", 401);
// location.reload();
// });
} else if (code === 403) {
router.push({ path: "/401" });
} else {
const errorMsg = error.response.data.message;
if (errorMsg !== undefined) {
ElNotification.error({
title: errorMsg,
duration: 5000,
});
}
}
} else {
ElNotification.error({
title: "接口请求失败",
duration: 5000,
});
}
}
return Promise.reject(error);
}
);
export default service;

View File

@@ -3,6 +3,7 @@ import { useUserStoreHook } from "@/store";
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
const user = useUserStoreHook(); const user = useUserStoreHook();
let isTips = false
export interface ApifoxModel { export interface ApifoxModel {
account: string; account: string;
@@ -143,6 +144,7 @@ class WebSocketManager {
this.autoConnect = false; this.autoConnect = false;
} }
} }
// 自动重连机制 // 自动重连机制
private reconnect() { private reconnect() {
if (!this.autoConnect) { if (!this.autoConnect) {
@@ -156,7 +158,9 @@ class WebSocketManager {
}, this.reconnectDelay); }, this.reconnectDelay);
} else { } else {
clearTimeout(this.reconnectTimer); clearTimeout(this.reconnectTimer);
console.error("达到最大重连次数,停止重连"); // window.location.reload();
// console.error("达到最大重连次数,停止重连");
if (!isTips) {
ElMessageBox.confirm('达到最大重连次数' + this.maxReconnectAttempts + '次,已停止重连,是否立即重连?', '提示', { ElMessageBox.confirm('达到最大重连次数' + this.maxReconnectAttempts + '次,已停止重连,是否立即重连?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@@ -164,12 +168,15 @@ class WebSocketManager {
callback: (action: string) => { callback: (action: string) => {
console.log(action); console.log(action);
if (action == 'confirm') { if (action == 'confirm') {
isTips = false
this.setupWebSocket(); this.setupWebSocket();
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
} }
} }
}); });
} }
isTips = true
}
} }
// 清除定时器 // 清除定时器

View File

@@ -23,8 +23,8 @@ import call from "@/assets/images/application/call.png";
const list = ref([ const list = ref([
{ name: "存酒", icon: bear, path: "storingWine", desc: "用户未喝完的酒可暂存在店里" }, { name: "存酒", icon: bear, path: "storingWine", desc: "用户未喝完的酒可暂存在店里" },
{ name: "点歌", icon: song, path: "song", desc: "用户可以付费点歌" }, // { name: "点歌", icon: song, path: "song", desc: "用户可以付费点歌" },
{ name: "广告", icon: ad, path: "advertisement", desc: "添加弹窗广告" }, // { name: "广告", icon: ad, path: "advertisement", desc: "添加弹窗广告" },
{ name: "叫号", icon: call, path: "lineUplist", desc: "" }, { name: "叫号", icon: call, path: "lineUplist", desc: "" },
]); ]);

View File

@@ -60,7 +60,8 @@
<myDialog ref="myDialogRefqs" title="记录" @confirm="confirmqs" width="30%"> <myDialog ref="myDialogRefqs" title="记录" @confirm="confirmqs" width="30%">
<el-form-item label="选择用户"> <el-form-item label="选择用户">
<el-select v-model="datas.DialogForm.userId" filterable placeholder="请选择选择用户" style="width: 240px"> <el-select v-model="datas.DialogForm.userId" filterable placeholder="请选择选择用户" style="width: 240px">
<el-option v-for="item in datas.options" :key="item.userId" :label="item.nickName" :value="item.userId" /> <el-option v-for="item in datas.options" :key="item.userId" :label="`${item.nickName}/${item.phone}`"
:value="item.userId" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="数量"> <el-form-item label="数量">
@@ -128,7 +129,7 @@ function myDialogRefqsevent(item) {
myDialogRefqs.value.open() myDialogRefqs.value.open()
} }
async function getshopUser(params) { async function getshopUser(params) {
const res = await API.getshopUser({}) const res = await API.getshopUser({ page: 1, size: 999, ...params })
datas.options = res.records datas.options = res.records
} }
async function getList(data = {}) { async function getList(data = {}) {

View File

@@ -19,10 +19,11 @@ const AuthAPI = {
}); });
}, },
// 获取用户店铺列表 // 获取用户店铺列表
getshopUser() { getshopUser(params: any) {
return request<any, Responseres>({ return request<any, Responseres>({
url: `/account/admin/shopUser`, url: `/account/admin/shopUser`,
method: "get", method: "get",
params,
}); });
}, },
// 新增 // 新增

View File

@@ -12,7 +12,8 @@
<el-form ref="ruleFormRef" :rules="datas.rules" :model="datas.DialogForm" label-width="80px"> <el-form ref="ruleFormRef" :rules="datas.rules" :model="datas.DialogForm" label-width="80px">
<el-form-item label="选择用户"> <el-form-item label="选择用户">
<el-select v-model="datas.DialogForm.userId" filterable placeholder="请选择选择用户" style="width: 240px"> <el-select v-model="datas.DialogForm.userId" filterable placeholder="请选择选择用户" style="width: 240px">
<el-option v-for="item in datas.options" :key="item.userId" :label="item.nickName" :value="item.userId" /> <el-option v-for="item in datas.options" :key="item.userId" :label="`${item.nickName}/${item.phone}`"
:value="item.userId" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="选择酒品"> <el-form-item label="选择酒品">
@@ -107,7 +108,7 @@ async function getList(data = {}) {
datas.pagingConfig.pageNumber = res.pageNumber datas.pagingConfig.pageNumber = res.pageNumber
} }
async function getshopUser(params) { async function getshopUser(params) {
const res = await API.getshopUser({}) const res = await API.getshopUser({ page: 1, size: 999, ...params })
datas.options = res.records datas.options = res.records
} }
async function getstorageGood() { async function getstorageGood() {

View File

@@ -18,10 +18,11 @@ const AuthAPI = {
}); });
}, },
// 获取用户店铺列表 // 获取用户店铺列表
getshopUser() { getshopUser(params: any) {
return request<any, Responseres>({ return request<any, Responseres>({
url: `/account/admin/shopUser`, url: `/account/admin/shopUser`,
method: "get", method: "get",
params,
}); });
}, },
// 获取酒品列表 // 获取酒品列表

View File

@@ -19,18 +19,7 @@ const modalConfig: IModalConfig = {
console.log("提交之前处理", data); console.log("提交之前处理", data);
}, },
formItems: [ formItems: [
{
label: "状态",
prop: "status",
type: 'switch',
attrs: {
activeText: '开启',
inactiveText: '关闭',
activeValue: 1,
inactiveValue: 0
},
initialValue: 1
},
{ {
label: "挂账人", label: "挂账人",
@@ -66,6 +55,56 @@ const modalConfig: IModalConfig = {
}, },
initialValue: 0 initialValue: 0
}, },
// ===================== 新增 =====================
{
label: "还款提醒日",
prop: "expireRemindDay",
type: "input",
rules: [
{
validator: (rule, value, callback) => {
if (!value) return callback();
const num = Number(value);
if (!Number.isInteger(num) || num < 1 || num > 28) {
callback(new Error("请输入 1~28 之间的整数"));
}
callback();
}
}
],
attrs: {
type: "number",
placeholder: "每月几号提醒1-28",
style: { width: "200px" },
oninput: "value=value.replace(/\\D|\\./g,'')" // 禁止输入小数点
},
initialValue: "",
},
{
label: "还款提醒",
prop: "expireRemind",
type: "input",
rules: [
{
validator: (rule, value, callback) => {
if (!value) return callback();
const num = Number(value);
if (!Number.isInteger(num) || num < 0) {
callback(new Error("请输入大于等于 0 的整数"));
}
callback();
}
}
],
attrs: {
type: "number",
placeholder: "提醒多久以前的数据单位(月)",
style: { width: "240px" },
oninput: "value=value.replace(/\\D|\\./g,'')" // 禁止输入小数点
},
initialValue: "",
},
// ================================================
{ {
label: "还款方式", label: "还款方式",
prop: "repaymentMethod", prop: "repaymentMethod",
@@ -73,6 +112,18 @@ const modalConfig: IModalConfig = {
options: returnOptions('repaymentMethod'), options: returnOptions('repaymentMethod'),
initialValue: returnOptions('repaymentMethod')[0].value initialValue: returnOptions('repaymentMethod')[0].value
}, },
{
label: "状态",
prop: "status",
type: 'switch',
attrs: {
activeText: '开启',
inactiveText: '关闭',
activeValue: 1,
inactiveValue: 0
},
initialValue: 1
},
], ],
}; };

View File

@@ -58,16 +58,36 @@ const contentConfig: IContentConfig = {
label: "已挂账金额(元)", label: "已挂账金额(元)",
align: "center", align: "center",
prop: "owedAmount", prop: "owedAmount",
templet: "custom",
slotName: 'owedAmount',
}, },
// {
// label: "剩余挂账额度(元)",
// align: "center",
// prop: "remainingAmount",
// templet: "custom",
// slotName: 'remainingAmount',
// },
{ {
label: "剩余挂账额度(元", label: "账户余额(挂账额度+用户余额",
align: "center",
prop: "remainingAmount",
},
{
label: "账户余额",
align: "center", align: "center",
prop: "accountBalance", prop: "accountBalance",
templet: "custom",
slotName: 'accountBalance',
},
{
label: "还款提醒日",
align: "center",
prop: "expireRemindDay",
templet: "custom",
slotName: 'expireRemindDay',
},
{
label: "还款提醒",
align: "center",
prop: "expireRemind",
templet: "custom",
slotName: 'expireRemind',
}, },
{ {
label: "操作", label: "操作",

View File

@@ -19,18 +19,7 @@ const modalConfig: IModalConfig = {
console.log("提交之前处理", data); console.log("提交之前处理", data);
}, },
formItems: [ formItems: [
{
label: "状态",
prop: "status",
type: 'switch',
attrs: {
activeText: '开启',
inactiveText: '关闭',
activeValue: 1,
inactiveValue: 0
},
initialValue: 1
},
{ {
label: "挂账人", label: "挂账人",
@@ -66,6 +55,56 @@ const modalConfig: IModalConfig = {
}, },
initialValue: 0 initialValue: 0
}, },
// ===================== 新增 =====================
{
label: "还款提醒日",
prop: "expireRemindDay",
type: "input",
rules: [
{
validator: (rule, value, callback) => {
if (!value) return callback();
const num = Number(value);
if (!Number.isInteger(num) || num < 1 || num > 28) {
callback(new Error("请输入 1~28 之间的整数"));
}
callback();
}
}
],
attrs: {
type: "number",
placeholder: "每月几号提醒1-28",
style: { width: "200px" },
oninput: "value=value.replace(/\\D|\\./g,'')" // 禁止输入小数点
},
initialValue: "",
},
{
label: "还款提醒",
prop: "expireRemind",
type: "input",
rules: [
{
validator: (rule, value, callback) => {
if (!value) return callback();
const num = Number(value);
if (!Number.isInteger(num) || num < 0) {
callback(new Error("请输入大于等于 0 的整数"));
}
callback();
}
}
],
attrs: {
type: "number",
placeholder: "提醒多久以前的数据单位(月)",
style: { width: "240px" },
oninput: "value=value.replace(/\\D|\\./g,'')" // 禁止输入小数点
},
initialValue: "",
},
// ================================================
{ {
label: "还款方式", label: "还款方式",
prop: "repaymentMethod", prop: "repaymentMethod",
@@ -73,6 +112,18 @@ const modalConfig: IModalConfig = {
options: returnOptions('repaymentMethod'), options: returnOptions('repaymentMethod'),
initialValue: returnOptions('repaymentMethod')[0].value initialValue: returnOptions('repaymentMethod')[0].value
}, },
{
label: "状态",
prop: "status",
type: 'switch',
attrs: {
activeText: '开启',
inactiveText: '关闭',
activeValue: 1,
inactiveValue: 0
},
initialValue: 1
},
], ],
}; };

View File

@@ -2,19 +2,49 @@ import type { ISearchConfig } from "@/components/CURD/types";
const searchConfig: ISearchConfig = { const searchConfig: ISearchConfig = {
pageName: "sys:user", pageName: "sys:user",
isExpand: true,
formItems: [ formItems: [
{ {
type: "input", type: "input",
label: "挂账人", label: "挂账人",
prop: "keywords", prop: "debtor",
attrs: { attrs: {
placeholder: "请输入挂账人或手机号", placeholder: "请输入挂账人名称",
clearable: true, clearable: true,
style: { style: {
width: "200px", width: "200px",
}, },
}, },
}, },
{
type: "input",
label: "手机号",
prop: "mobile",
attrs: {
placeholder: "请输入手机号",
clearable: true,
style: {
width: "200px",
},
},
},
{
type: "select",
label: "状态",
prop: "status",
attrs: {
placeholder: "请选择状态",
clearable: true,
style: {
width: "200px",
},
},
initialValue: '',
options: [
{ label: "停用", value: '0' },
{ label: "启用", value: '1' },
],
},
{ {
type: "select", type: "select",
label: "还款状态", label: "还款状态",
@@ -28,9 +58,8 @@ const searchConfig: ISearchConfig = {
}, },
initialValue: '', initialValue: '',
options: [ options: [
{ label: "未还款", value: 'unpaid' }, { label: "有欠款", value: '0' },
{ label: "部分还款", value: 'partial' }, { label: "无欠款", value: '1' },
{ label: "已还清", value: 'paid' },
], ],
}, },
], ],

View File

@@ -28,6 +28,27 @@
<template #options="scope"> <template #options="scope">
{{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }} {{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }}
</template> </template>
<template #accountBalance="scope">
{{ scope.row.accountBalance }}
</template>
<template #owedAmount="scope">
{{
scope.row.creditAmount - scope.row.accountBalance > 0
? scope.row.creditAmount - scope.row.accountBalance
: 0
}}
</template>
<template #expireRemindDay="scope">
<el-text>{{ scope.row.expireRemindDay }}</el-text>
</template>
<template #expireRemind="scope">
<el-text>{{ scope.row.expireRemind }}月前</el-text>
</template>
<template #remainingAmount="scope">
{{ scope.row.creditAmount - scope.row.accountBalance }}
</template>
<template #gender="scope"> <template #gender="scope">
<DictLabel v-model="scope.row[scope.prop]" code="gender" /> <DictLabel v-model="scope.row[scope.prop]" code="gender" />
</template> </template>

743
src/views/data/finance.vue Normal file
View File

@@ -0,0 +1,743 @@
<template>
<div class="gyq_container">
<div class="row">
<div class="between">
<el-form :model="queryForm" inline>
<el-form-item>
<el-date-picker v-model="queryForm.queryDate" type="date" placeholder="选择日期" format="YYYY-MM-DD"
value-format="YYYY-MM-DD" :disabled-date="disabledFutureDate" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" :loading="loading" @click="handleQuery">查询</el-button>
<el-button @click="handleReset" icon="Refresh" :loading="loading">重置</el-button>
<el-button icon="Printer" @click="printBusinessDialogRef.show()">经营日报</el-button>
<el-button icon="Printer" @click="printBusinessDialogRef2.show()">日结单</el-button>
</el-form-item>
</el-form>
<el-form inline>
<el-form-item>
<el-select v-model="queryForm.platform" placeholder="选择平台" clearable style="width: 150px;">
<el-option v-for="item in platformList" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="handleExport" icon="Download">导出</el-button>
</el-form-item>
</el-form>
</div>
</div>
<div class="row" v-loading="loading">
<div class="warp" style="width: 400px;">
<div class="card">
<div class="header_title">营业额</div>
<div class="num" style="color: var(--el-color-primary);">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.turnover || 0) }}
</div>
<div class="intro">营业额</div>
<div class="pay_wrap">
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.wechat || 0) }}
</div>
<span class="t">微信支付金额</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.alipay || 0) }}
</div>
<span class="t">支付宝支付金额</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.selfScan || 0) }}
</div>
<span class="t">二维码收款</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.barScan || 0) }}
</div>
<span class="t">扫码收款</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.cash || 0) }}
</div>
<span class="t">现金收款</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.recharge || 0) }}
</div>
<span class="t">充值</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.owed || 0) }}
</div>
<span class="t">挂账</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.turnover.balance || 0) }}
</div>
<span class="t">余额支付</span>
</div>
</div>
<!-- <div class="order_info">
<div class="order_title">
<span class="dot"></span>
<span class="t">订单</span>
</div>
<div class="order_lits">
<div class="item">
<span class="t">订单金额</span>
<span class="n">{{ tableData.order.orderAmount || 0 }}</span>
</div>
<span class="line">|</span>
<div class="item">
<span class="t">订单总数</span>
<span class="n">{{ tableData.order.orderCount || 0 }}</span>
</div>
</div>
</div> -->
</div>
<div class="card">
<div class="header_title">数据统计</div>
<div class="pay_wrap">
<div class="item">
<span class="n">{{ tableData.sts.customerCount || 0 }}</span>
<span class="t">就餐人数</span>
</div>
<div class="item">
<span class="n">{{ tableData.sts.orderCount || 0 }}</span>
<span class="t">订单数</span>
</div>
<div class="item">
<span class="n">{{ tableData.sts.tableCount || 0 }}</span>
<span class="t">桌台数</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.sts.avgPayAmount || 0) }}
</div>
<div class="t">客单价
<el-tooltip class="box-item" effect="dark" content="实付金额(包含现金支付 包含会员支付 包含挂账)/就餐人数没有具体人数时默认一桌按照1人计算"
placement="top">
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="item">
<span class="n">{{ tableData.sts.turnoverRate || 0 }}%</span>
<div class="t">翻台率
<el-tooltip class="box-item" effect="dark" content="(订单数-桌台数)/桌台数*100%" placement="top">
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.sts.profitAmount || 0) }}
</div>
<div class="t">毛利润
<el-tooltip class="box-item" effect="dark" content="(订单实付金额-商品成本)" placement="top">
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.sts.productCostAmount || 0) }}
</div>
<span class="t">商品成本</span>
</div>
<div class="item">
<span class="n">{{ tableData.sts.profitRate || 0 }}%</span>
<div class="t">毛利率
<el-tooltip class="box-item" effect="dark" content="(订单实付金额-商品成本)/订单实付金额*100%" placement="top">
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<!-- <div class="item">
<span class="n">{{ tableData.sts.netProfitAmount || 0 }}%</span>
<span class="t">净利率</span>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.sts.netProfitRate || 0) }}
</div>
<span class="t">净利润</span>
</div> -->
</div>
</div>
</div>
<div class="card" style="flex: 1;">
<div class="header_title">商家经营数据</div>
<div class="content">
<div class="card" style="flex: 1;">
<div class="num" style="color: var(--el-color-success);">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.discountAmount || 0) }}
</div>
<div class="intro">优惠金额</div>
<div class="item_list">
<div class="item">
<div class="n">
{{ tableData.discount.discountCount || 0 }}
</div>
<div class="label">
优惠笔数
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.newConsumerDiscount || 0) }}
</div>
<div class="label">
新客立减
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.freeCashAmount || 0) }}
</div>
<div class="label">
霸王餐
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.fullMinusAmount || 0) }}
</div>
<div class="label">
满减活动
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.couponAmount || 0) }}
</div>
<div class="label">
优惠券
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.memberDiscount || 0) }}
</div>
<div class="label">
会员折扣
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.pointsDiscountAmount || 0) }}
</div>
<div class="label">
积分抵扣金额
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.discount.orderDiscount || 0) }}
</div>
<div class="label">
订单改价
</div>
</div>
</div>
</div>
<div class="card" style="flex: 1;">
<div class="num" style="color: var(--el-color-danger);">
<span class="i"></span>
{{ multiplyAndFormat(tableData.refund.refundAmount || 0) }}
</div>
<div class="intro">退款金额</div>
<div class="item_list">
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.refund.onlineRefundAmount || 0) }}
</div>
<div class="label">
线上退款金额
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.refund.cashRefundAmount || 0) }}
</div>
<div class="label">
现金退款金额
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.refund.memberRefundAmount || 0) }}
</div>
<div class="label">
余额退款金额
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.refund.onlineRechargeRefundAmount || 0) }}
</div>
<div class="label">
线上充值退款金额
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.refund.cashRechargeRefundAmount || 0) }}
</div>
<div class="label">
现金充值退款金额
</div>
</div>
<div class="item">
<div class="n">
<span class="i"></span>
{{ multiplyAndFormat(tableData.refund.creditRefundAmount || 0) }}
</div>
<div class="label">
挂账退款金额
</div>
</div>
</div>
</div>
<div class="card" style="flex: 1;">
<div class="num" style="color: var(--el-color-warning);">
<span class="i"></span>
{{ multiplyAndFormat(tableData.order.orderAmount || 0) }}
</div>
<div class="intro">订单金额</div>
<div class="item_list">
<div class="item">
<div class="n">
{{ tableData.order.orderCount || 0 }}
</div>
<div class="label">
订单总数
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<printBusinessDialog ref="printBusinessDialogRef" @success="printHandle" />
<printBusinessDialog ref="printBusinessDialogRef2" @success="printHandle2" />
</template>
<script setup>
import dayjs from "dayjs";
import { ref, onMounted } from "vue";
import OrderApi from "@/api/order/order";
import { downloadFile, multiplyAndFormat } from '@/utils'
import printBusinessDialog from "@/components/printBusinessDialog.vue";
const printBusinessDialogRef = ref(null)
// 打印经营日报回调
async function printHandle(res) {
try {
const shopStaff = JSON.parse(localStorage.getItem('shopStaff')) || { name: '' }
const shopName = localStorage.getItem('shopName')
await OrderApi.printDayReport({
beginDate: res.date[0],
endDate: res.date[1],
rangeType: 'CUSTOM',
shopId: localStorage.getItem('shopId') || '',
operator: shopStaff.name || shopName
})
ElMessage.success('打印成功')
} catch (error) {
console.log(error);
}
}
const printBusinessDialogRef2 = ref(null)
// 打印日结单回调
async function printHandle2(res) {
try {
const shopStaff = JSON.parse(localStorage.getItem('shopStaff')) || { name: '' }
const shopName = localStorage.getItem('shopName')
await OrderApi.printDaySettle({
beginDate: res.date[0],
endDate: res.date[1],
rangeType: 'CUSTOM',
shopId: localStorage.getItem('shopId') || '',
operator: shopStaff.name || shopName
})
ElMessage.success('打印成功')
} catch (error) {
console.log(error);
}
}
const queryForm = ref({
queryDate: dayjs().format('YYYY-MM-DD'), // 查询日期 yyyy-MM-dd
platform: 'czg',
shopId: localStorage.getItem('shopId') || '',
mainShopId: ''
});
const platformList = ref([
{ value: 'czg', label: '银收客' },
{ value: 'MTuan', label: '美团' },
{ value: 'CMeMe', label: '菜么么' },
{ value: 'KRuYun', label: '客如云' },
])
// 禁用今天以后的日期
const disabledFutureDate = (time) => {
return dayjs(time).isAfter(dayjs().startOf('day'))
}
// 查询
function handleQuery() {
getData()
}
// 重置
function handleReset() {
queryForm.value.queryDate = dayjs().format('YYYY-MM-DD')
queryForm.value.platform = 'czg'
getData()
}
// 导出
async function handleExport() {
try {
if (!queryForm.value.platform) {
ElMessage.error('请选择导出平台')
return
}
const res = await OrderApi.financeExport(queryForm.value)
downloadFile(res, '财务报表', 'xlsx')
} catch (error) {
console.log(error);
}
}
const loading = ref(false);
const tableData = ref({
turnover: {},
order: {},
discount: {},
refund: {},
sts: {}
});
// 查询财务报表
async function getData() {
try {
loading.value = true
const res = await OrderApi.financeSts(queryForm.value)
tableData.value = res
console.log('tableData.value', tableData.value);
} catch (error) {
console.log(error);
}
setTimeout(() => {
loading.value = false
}, 500);
}
onMounted(() => {
getData()
})
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.between {
flex: 1;
display: flex;
justify-content: space-between;
}
.row {
display: flex;
gap: 14px;
&.mt14 {
margin-top: 14px;
}
}
.warp {
display: flex;
flex-direction: column;
gap: 14px;
}
.card {
border-radius: 12px;
background-color: #fff;
padding: 20px;
.header_title {
font-size: 16px;
color: 333;
font-weight: bold;
}
.content {
background-color: #F8F8F8;
border-radius: 8px;
margin-top: 20px;
padding: 14px;
display: flex;
gap: 14px;
.item_list {
margin-top: 14px;
.title {
font-size: 14px;
color: #666;
}
.item {
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 14px;
&:nth-child(odd) {
background-color: #F3F7FA;
}
.n {
font-size: 16px;
color: #333;
font-weight: bold;
display: flex;
align-items: center;
.i {
font-size: 10px;
position: relative;
top: 1px;
}
}
.label {
font-size: 14px;
color: #666;
}
}
}
}
.num {
font-size: 24px;
font-weight: bold;
display: flex;
align-items: center;
.i {
font-size: 12px;
position: relative;
top: 3px;
}
}
.intro {
font-size: 14px;
color: #999;
}
.pay_wrap {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 0px;
grid-row-gap: 14px;
background-color: #F8F8F8;
padding: 14px;
margin-top: 14px;
border-radius: 8px;
.item {
display: flex;
flex-direction: column;
.n {
font-size: 20px;
font-weight: bold;
color: #333;
display: flex;
align-items: center;
.i {
font-size: 12px;
position: relative;
top: 3px;
}
}
.t {
font-size: 14px;
color: #666;
display: flex;
align-items: center;
gap: 4px;
}
}
}
.order_info {
border-top: 1px solid #ececec;
padding-top: 14px;
.order_title {
display: flex;
gap: 10px;
align-items: center;
.dot {
--size: 12px;
width: var(--size);
height: var(--size);
border-radius: 50%;
background-color: var(--el-color-danger);
}
.t {
font-size: 16px;
color: #333;
font-weight: bold;
}
}
.order_lits {
display: flex;
gap: 14px;
margin-top: 4px;
.line {
color: #ececec;
}
.item {
display: flex;
align-items: center;
gap: 14px;
.t {
font-size: 14px;
color: #666;
}
.n {
font-size: 16px;
color: #333;
font-weight: bold;
}
}
}
}
}
.waterfall {
/* 核心属性1定义瀑布流列数关键 */
column-count: 3;
/* 核心属性2列之间的间距替代margin避免卡片间距错乱 */
column-gap: 14px;
/* 可选:防止卡片内容被列分割(关键!避免卡片跨列断裂) */
break-inside: avoid;
.waterfall-card {
/* 必须:适配多列布局,避免卡片宽度溢出 */
width: 100%;
/* 卡片间距仅需设置底部外边距上下间距左右间距由column-gap控制 */
margin-bottom: 14px;
padding: 24px;
background-color: #fff;
border-radius: 12px;
/* 配合break-inside: avoid强化卡片不可分割 */
page-break-inside: avoid;
.header-title {
display: flex;
gap: 14px;
align-items: center;
padding-bottom: 10px;
.dot {
--size: 10px;
width: var(--size);
height: var(--size);
border-radius: 50%;
}
.t {
font-size: 24px;
color: #000;
font-weight: bold;
}
}
p {
margin-top: 10px;
font-size: 14px;
color: #666;
padding-left: 24px;
}
}
}
.tips {
margin-top: 6px;
font-size: 14px;
color: #999;
padding-left: 24px;
}
</style>

View File

@@ -52,14 +52,35 @@
<span>营业</span> <span>营业</span>
</div> </div>
<div class="u-flex" style="flex-wrap: wrap"> <div class="u-flex" style="flex-wrap: wrap">
<el-select v-if="isHeadShop == 1 && loginType == 0" v-model="shopId" placeholder="选择分店" <importData
style="width: 200px; margin-right: 10px;" @change="shopChange"> :type="9"
<el-option v-for="item in branchList" :key="item.shopId" :label="item.shopName" :value="item.shopId" /> style="margin-right: 14px"
@close="importDataClose"
@update="updateData"
/>
<el-select
v-if="isHeadShop == 1 && loginType == 0"
v-model="shopId"
placeholder="选择分店"
style="width: 200px; margin-right: 10px"
@change="shopChange"
>
<el-option
v-for="item in branchList"
:key="item.shopId"
:label="item.shopName"
:value="item.shopId"
/>
</el-select> </el-select>
<div class="time_wrap u-flex" style="flex-shrink: 0"> <div class="time_wrap u-flex" style="flex-shrink: 0">
<div class="date_list"> <div class="date_list">
<div class="item" :class="{ active: dataListActive == index }" v-for="(item, index) in dateList" <div
:key="item.value" @click="timeChange(item.value, index)"> class="item"
:class="{ active: dataListActive == index }"
v-for="(item, index) in dateList"
:key="item.value"
@click="timeChange(item.value, index)"
>
<!-- 标签文本 --> <!-- 标签文本 -->
<span class="date-tab-item">{{ item.label }}</span> <span class="date-tab-item">{{ item.label }}</span>
<!-- 分隔符非最后一项才显示 --> <!-- 分隔符非最后一项才显示 -->
@@ -67,9 +88,16 @@
</div> </div>
</div> </div>
<div class="u-flex"> <div class="u-flex">
<el-date-picker v-if="timeValue == 'custom'" v-model="query.createdAt" type="daterange" <el-date-picker
range-separator="" start-placeholder="开始日期" end-placeholder="结束日期" value-format="YYYY-MM-DD" v-if="timeValue == 'custom'"
@change="summarytrade" /> v-model="query.createdAt"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
@change="summarytrade"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -78,7 +106,9 @@
<div class="top"> <div class="top">
<div class="item earnings"> <div class="item earnings">
<div class="num_wrap"> <div class="num_wrap">
<div class="num">{{ formatDecimal(trade.payAmount + trade.rechargeAmount || 0) }}</div> <div class="num">
{{ formatDecimal(trade.payAmount + trade.rechargeAmount || 0) }}
</div>
<div class="tips"> <div class="tips">
营业额() 营业额()
<el-tooltip popper-class="popper" effect="light" placement="bottom"> <el-tooltip popper-class="popper" effect="light" placement="bottom">
@@ -101,7 +131,14 @@
<div class="item"> <div class="item">
<div class="left"> <div class="left">
<img class="icon" src="@/assets/images/data/scan.png" /> <img class="icon" src="@/assets/images/data/scan.png" />
<span>主扫收款</span> <span>二维码收款</span>
</div>
<span class="num">{{ trade.mainScanPayAmount || 0 }}</span>
</div>
<div class="item">
<div class="left">
<img class="icon" src="@/assets/images/data/scan.png" />
<span>扫码收款</span>
</div> </div>
<span class="num">{{ trade.backScanPayAmount || 0 }}</span> <span class="num">{{ trade.backScanPayAmount || 0 }}</span>
</div> </div>
@@ -148,20 +185,27 @@
<!-- <div class="t">{{ formatDecimal(tradeSale.totalpayAmount || 0) }}</div> --> <!-- <div class="t">{{ formatDecimal(tradeSale.totalpayAmount || 0) }}</div> -->
</div> </div>
<div class="line_gropress"> <div class="line_gropress">
<div class="gropress l" :style="{ <div
width: `${trade.payAmount class="gropress l"
? (trade.payAmount / (trade.payAmount * 1 + trade.refundAmount * 1)) * :style="{
100 width: `${
trade.payAmount
? (trade.payAmount / (trade.payAmount * 1 + trade.refundAmount * 1)) * 100
: 0 : 0
}%`, }%`,
}" /> }"
<div class="gropress r" :style="{ />
width: `${trade.refundAmount <div
class="gropress r"
:style="{
width: `${
trade.refundAmount
? (trade.refundAmount / (trade.payAmount * 1 + trade.refundAmount * 1)) * ? (trade.refundAmount / (trade.payAmount * 1 + trade.refundAmount * 1)) *
100 100
: 0 : 0
}%`, }%`,
}" /> }"
/>
</div> </div>
<div class="line_btm"> <div class="line_btm">
<el-icon class="icon el-icon-caret-right" /> <el-icon class="icon el-icon-caret-right" />
@@ -177,22 +221,30 @@
<!-- <div class="t">{{ formatDecimal(tradeSale.totalVipAmount || 0) }}</div> --> <!-- <div class="t">{{ formatDecimal(tradeSale.totalVipAmount || 0) }}</div> -->
</div> </div>
<div class="line_gropress"> <div class="line_gropress">
<div class="gropress l" :style="{ <div
width: `${trade.rechargeAmount class="gropress l"
:style="{
width: `${
trade.rechargeAmount
? (trade.rechargeAmount / ? (trade.rechargeAmount /
(trade.memberPayAmount + trade.rechargeRefundAmount * 1)) * (trade.rechargeAmount * 1 + trade.rechargeRefundAmount * 1)) *
100 100
: 0 : 0
}%`, }%`,
}" /> }"
<div class="gropress r" :style="{ />
width: `${trade.rechargeRefundAmount <div
class="gropress r"
:style="{
width: `${
trade.rechargeRefundAmount
? (trade.rechargeRefundAmount / ? (trade.rechargeRefundAmount /
(trade.memberPayAmount + trade.rechargeRefundAmount * 1)) * (trade.rechargeAmount * 1 + trade.rechargeRefundAmount * 1)) *
100 100
: 0 : 0
}%`, }%`,
}" /> }"
/>
</div> </div>
<div class="line_btm"> <div class="line_btm">
<el-icon class="icon el-icon-caret-right" /> <el-icon class="icon el-icon-caret-right" />
@@ -243,8 +295,11 @@
<div class="item item1"> <div class="item item1">
<div class="title"> <div class="title">
客单价 客单价
<el-tooltip effect="dark" :content="`订单实付金额(${trade.payAmount}/就餐人数(${trade.customerCount || 0}`" <el-tooltip
placement="top"> effect="dark"
:content="`订单实付金额(${trade.payAmount}/就餐人数(${trade.customerCount || 0}`"
placement="top"
>
<el-icon> <el-icon>
<QuestionFilled /> <QuestionFilled />
</el-icon> </el-icon>
@@ -252,15 +307,19 @@
</div> </div>
<div class="icon_wrap"> <div class="icon_wrap">
<img class="img" src="@/assets/images/data_home_item1_icon.png" /> <img class="img" src="@/assets/images/data_home_item1_icon.png" />
<div class="t" style="color: #0080FF;">{{ formatDecimal(trade.avgPayAmount || 0) }}</div> <div class="t" style="color: #0080ff">
{{ formatDecimal(trade.avgPayAmount || 0) }}
</div>
</div> </div>
</div> </div>
<div class="item item2"> <div class="item item2">
<div class="title"> <div class="title">
翻台率 翻台率
<el-tooltip effect="dark" <el-tooltip
effect="dark"
:content="`翻台率=(客单数(${trade.customerCount || 0}-桌台数(${trade.tableCount || 0}/桌台数*100%`" :content="`翻台率=(客单数(${trade.customerCount || 0}-桌台数(${trade.tableCount || 0}/桌台数*100%`"
placement="top"> placement="top"
>
<el-icon> <el-icon>
<QuestionFilled /> <QuestionFilled />
</el-icon> </el-icon>
@@ -268,7 +327,7 @@
</div> </div>
<div class="icon_wrap"> <div class="icon_wrap">
<img class="img" src="@/assets/images/data_home_item2_icon.png" /> <img class="img" src="@/assets/images/data_home_item2_icon.png" />
<div class="t" style="color: #FFB200;">{{ trade.turnoverRate || 0 }}%</div> <div class="t" style="color: #ffb200">{{ trade.turnoverRate || 0 }}%</div>
</div> </div>
</div> </div>
<div class="item item3"> <div class="item item3">
@@ -287,43 +346,57 @@
<div class="left"> <div class="left">
<span>新客立减</span> <span>新客立减</span>
</div> </div>
<span class="num">{{ multiplyAndFormat(trade.newCustomerDiscountAmount || 0) }}</span> <span class="num">
{{ multiplyAndFormat(trade.newCustomerDiscountAmount || 0) }}
</span>
</div> </div>
<div class="item"> <div class="item">
<div class="left"> <div class="left">
<span>满减活动</span> <span>满减活动</span>
</div> </div>
<span class="num">{{ multiplyAndFormat(trade.fullDiscountAmount || 0) }}</span> <span class="num">
{{ multiplyAndFormat(trade.fullDiscountAmount || 0) }}
</span>
</div> </div>
<div class="item"> <div class="item">
<div class="left"> <div class="left">
<span>优惠券抵扣</span> <span>优惠券抵扣</span>
</div> </div>
<span class="num">{{ multiplyAndFormat(trade.couponDiscountAmount || 0) }}</span> <span class="num">
{{ multiplyAndFormat(trade.couponDiscountAmount || 0) }}
</span>
</div> </div>
<div class="item"> <div class="item">
<div class="left"> <div class="left">
<span>积分抵扣</span> <span>积分抵扣</span>
</div> </div>
<span class="num">{{ multiplyAndFormat(trade.pointDiscountAmount || 0) }}</span> <span class="num">
{{ multiplyAndFormat(trade.pointDiscountAmount || 0) }}
</span>
</div> </div>
<div class="item"> <div class="item">
<div class="left"> <div class="left">
<span>霸王餐</span> <span>霸王餐</span>
</div> </div>
<span class="num">{{ multiplyAndFormat(trade.backDiscountAmount || 0) }}</span> <span class="num">
{{ multiplyAndFormat(trade.backDiscountAmount || 0) }}
</span>
</div> </div>
<div class="item"> <div class="item">
<div class="left"> <div class="left">
<span>会员折扣</span> <span>会员折扣</span>
</div> </div>
<span class="num">{{ multiplyAndFormat(trade.memberDiscountAmount || 0) }}</span> <span class="num">
{{ multiplyAndFormat(trade.memberDiscountAmount || 0) }}
</span>
</div> </div>
<div class="item"> <div class="item">
<div class="left"> <div class="left">
<span>订单改价</span> <span>订单改价</span>
</div> </div>
<span class="num">{{ multiplyAndFormat(trade.orderPriceDiscountAmount || 0) }}</span> <span class="num">
{{ multiplyAndFormat(trade.orderPriceDiscountAmount || 0) }}
</span>
</div> </div>
</div> </div>
</template> </template>
@@ -334,14 +407,16 @@
</div> </div>
<div class="icon_wrap"> <div class="icon_wrap">
<img class="img" src="@/assets/images/data_home_item3_icon.png" /> <img class="img" src="@/assets/images/data_home_item3_icon.png" />
<div class="t" style="color: #FF8000;">{{ formatDecimal(trade.discountAmount || 0) }}</div> <div class="t" style="color: #ff8000">
{{ formatDecimal(trade.discountAmount || 0) }}
</div>
</div> </div>
</div> </div>
<div class="item item4"> <div class="item item4">
<div class="title">优惠笔数</div> <div class="title">优惠笔数</div>
<div class="icon_wrap"> <div class="icon_wrap">
<img class="img" src="@/assets/images/data_home_item4_icon.png" /> <img class="img" src="@/assets/images/data_home_item4_icon.png" />
<div class="t" style="color: #00CB71;">{{ trade.discountCount || 0 }}</div> <div class="t" style="color: #00cb71">{{ trade.discountCount || 0 }}</div>
</div> </div>
</div> </div>
<div class="item item5"> <div class="item item5">
@@ -349,13 +424,13 @@
<div class="row"> <div class="row">
<div class="title">毛利润</div> <div class="title">毛利润</div>
<div class="icon_wrap"> <div class="icon_wrap">
<div class="t" style="color: #0000C2;">{{ trade.profitAmount || 0 }}</div> <div class="t" style="color: #0000c2">{{ trade.profitAmount || 0 }}</div>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="title">毛利率</div> <div class="title">毛利率</div>
<div class="icon_wrap"> <div class="icon_wrap">
<div class="t" style="color: #0000C2;">{{ trade.profitRate || 0 }}%</div> <div class="t" style="color: #0000c2">{{ trade.profitRate || 0 }}%</div>
</div> </div>
</div> </div>
</div> </div>
@@ -384,10 +459,18 @@
<div class="item"> <div class="item">
<div class="header"> <div class="header">
<div class="tab_wrap"> <div class="tab_wrap">
<div class="item" :class="{ active: lineChartType == 0 }" @click="lineChartTypeChange(0)"> <div
class="item"
:class="{ active: lineChartType == 0 }"
@click="lineChartTypeChange(0)"
>
销售趋势 销售趋势
</div> </div>
<div class="item" :class="{ active: lineChartType == 1 }" @click="lineChartTypeChange(1)"> <div
class="item"
:class="{ active: lineChartType == 1 }"
@click="lineChartTypeChange(1)"
>
支付占比 支付占比
</div> </div>
</div> </div>
@@ -396,9 +479,20 @@
<el-radio-button value="30">30</el-radio-button> <el-radio-button value="30">30</el-radio-button>
</el-radio-group> </el-radio-group>
</div> </div>
<div v-show="lineChartType == 0" ref="saleChart" v-loading="saleLoading" class="chart" style="height: 350px" /> <div
<div v-show="lineChartType == 1" ref="payChart" v-loading="payChartLoading" class="chart" v-show="lineChartType == 0"
style="height: 350px" /> ref="saleChart"
v-loading="saleLoading"
class="chart"
style="height: 350px"
/>
<div
v-show="lineChartType == 1"
ref="payChart"
v-loading="payChartLoading"
class="chart"
style="height: 350px"
/>
</div> </div>
<!-- 商品销售排行 --> <!-- 商品销售排行 -->
<div class="item"> <div class="item">
@@ -458,7 +552,12 @@
</el-radio-group> </el-radio-group>
</div> </div>
</div> </div>
<div ref="initInterestRate" v-loading="initInterestRateLoading" class="chart" style="height: 350px" /> <div
ref="initInterestRate"
v-loading="initInterestRateLoading"
class="chart"
style="height: 350px"
/>
</div> </div>
<!-- 成本 --> <!-- 成本 -->
<div class="item"> <div class="item">
@@ -495,15 +594,21 @@
</template> </template>
<script> <script>
import importData from "@/components/importData/index.vue";
import dataSummaryApi from "@/api/order/data-summary"; import dataSummaryApi from "@/api/order/data-summary";
import finceApi from "@/api/order/fince";
import ShopApi from "@/api/account/shop"; import ShopApi from "@/api/account/shop";
import dayjs from "dayjs"; import dayjs from "dayjs";
import * as echarts from "echarts"; import * as echarts from "echarts";
import { debounce, formatDecimal } from "@/utils/tools"; import { debounce, formatDecimal } from "@/utils/tools";
import { formatDateRange } from './utils/index.js' import { formatDateRange } from "./utils/index.js";
import { multiplyAndFormat } from '@/utils/index.js' import { multiplyAndFormat } from "@/utils/index.js";
import { ElMessage, ElMessageBox } from "element-plus";
export default { export default {
name: "home", name: "home",
components: { importData },
data() { data() {
return { return {
multiplyAndFormat, multiplyAndFormat,
@@ -625,12 +730,12 @@ export default {
shopInfo: JSON.parse(localStorage.getItem("userInfo")), shopInfo: JSON.parse(localStorage.getItem("userInfo")),
initInterestRateLoading: true, initInterestRateLoading: true,
initInterestRate: null, initInterestRate: null,
initInterestRateTime: '', initInterestRateTime: "",
costLoading: true, costLoading: true,
costRef: null, costRef: null,
costUpdateTime: '', costUpdateTime: "",
interestRateDay: '7', interestRateDay: "7",
costDay: '7' costDay: "7",
}; };
}, },
computed: { computed: {
@@ -650,11 +755,11 @@ export default {
}, },
}, },
mounted() { mounted() {
let shopInfo = JSON.parse(localStorage.getItem('userInfo')) let shopInfo = JSON.parse(localStorage.getItem("userInfo"));
if (shopInfo.isHeadShop) { if (shopInfo.isHeadShop) {
this.shopId = shopInfo.id this.shopId = shopInfo.id;
} else { } else {
this.shopId = localStorage.getItem('shopId') this.shopId = localStorage.getItem("shopId");
} }
// 增加首页提示是否账号30天过期 // 增加首页提示是否账号30天过期
@@ -670,8 +775,8 @@ export default {
this.dateProduct(); this.dateProduct();
// this.summaryDateGet(); // this.summaryDateGet();
this.timeChange(this.timeValue); this.timeChange(this.timeValue);
this.profitRateBarChart() this.profitRateBarChart();
this.costLineChart() this.costLineChart();
this.__resizeHandler = debounce(() => { this.__resizeHandler = debounce(() => {
if (this.saleChart) { if (this.saleChart) {
@@ -700,39 +805,63 @@ export default {
// } // }
}, 100); }, 100);
window.addEventListener("resize", this.__resizeHandler); window.addEventListener("resize", this.__resizeHandler);
this.geiShopList() this.geiShopList();
// this.initCardUserChart(); // this.initCardUserChart();
}, },
methods: { methods: {
updateData(e) {
finceApi
.financeBase({
shopId: this.shopId,
type: "all",
date: e,
})
.then((res) => {
ElMessageBox.confirm("经营数据重新计算中,请稍后刷新页面查看", "提示", {
confirmButtonText: "知道了",
showCancelButton: false,
type: "warning",
});
});
},
importDataClose() {
// this.summaryGet();
this.dateAmount();
this.dateProduct();
// this.summaryDateGet();
this.timeChange(this.timeValue);
this.profitRateBarChart();
this.costLineChart();
},
/** /**
* 获取分店列表 * 获取分店列表
*/ */
async geiShopList() { async geiShopList() {
try { try {
if (this.shopInfo.isHeadShop) { if (this.shopInfo.isHeadShop) {
let res = await ShopApi.getBranchList() let res = await ShopApi.getBranchList();
this.branchList = res; this.branchList = res;
this.shopId = res[0].shopId this.shopId = res[0].shopId;
} else { } else {
this.shopId = this.shopInfo.id this.shopId = this.shopInfo.id;
} }
} catch (error) { } catch (error) {
console.log('获取分店列表===', error); console.log("获取分店列表===", error);
} }
}, },
shopChange() { shopChange() {
this.summarytrade(); this.summarytrade();
this.lineChartTypeChange(this.lineChartType) this.lineChartTypeChange(this.lineChartType);
this.dateProduct() this.dateProduct();
this.profitRateBarChart() this.profitRateBarChart();
this.costLineChart() this.costLineChart();
}, },
// 切换时间 // 切换时间
timeChange(e, index = 0) { timeChange(e, index = 0) {
this.dataListActive = index; this.dataListActive = index;
this.timeValue = e; this.timeValue = e;
this.query.createdAt = formatDateRange(e) this.query.createdAt = formatDateRange(e);
if (e != "custom") { if (e != "custom") {
this.summarytrade(); this.summarytrade();
} }
@@ -748,7 +877,7 @@ export default {
beginDate: this.query.createdAt[0], beginDate: this.query.createdAt[0],
endDate: this.query.createdAt[1], endDate: this.query.createdAt[1],
rangeType: this.timeValue, rangeType: this.timeValue,
shopId: this.shopId shopId: this.shopId,
}); });
this.trade = res; this.trade = res;
this.tradeSale = res.sale; this.tradeSale = res.sale;
@@ -971,7 +1100,7 @@ export default {
backgroundColor: "#fff", backgroundColor: "#fff",
borderColor: "#eee", borderColor: "#eee",
borderWidth: 1, borderWidth: 1,
boxShadow: "0 2px 8px rgba(0,0,0,0.08)" boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
}, },
xAxis: [ xAxis: [
{ {
@@ -987,11 +1116,11 @@ export default {
}, },
], ],
grid: { grid: {
top: '5%', top: "5%",
right: '5%', right: "5%",
bottom: '8%', bottom: "8%",
left: '8%', left: "8%",
containLabel: true // 确保标签不溢出,不影响点的显示 containLabel: true, // 确保标签不溢出,不影响点的显示
}, },
color: "#165DFF", // 折线和默认点的颜色 color: "#165DFF", // 折线和默认点的颜色
yAxis: [ yAxis: [
@@ -1016,8 +1145,8 @@ export default {
borderColor: "#000", // 黑色边框 borderColor: "#000", // 黑色边框
borderWidth: 2, // 边框宽度 borderWidth: 2, // 边框宽度
// 悬浮时填充色不变(仍为 #165DFF // 悬浮时填充色不变(仍为 #165DFF
color: "#165DFF" color: "#165DFF",
} },
}, },
// 确保所有数据点都显示(避免自动隐藏) // 确保所有数据点都显示(避免自动隐藏)
showAllSymbol: true, showAllSymbol: true,
@@ -1025,8 +1154,8 @@ export default {
itemStyle: { itemStyle: {
color: "#165DFF", // 点的填充色(与折线一致) color: "#165DFF", // 点的填充色(与折线一致)
borderWidth: 1, // 基础状态可加细边框(可选,不加则无) borderWidth: 1, // 基础状态可加细边框(可选,不加则无)
borderColor: "#fff" // 基础状态边框颜色(与背景区分,可选) borderColor: "#fff", // 基础状态边框颜色(与背景区分,可选)
} },
}, },
], ],
}); });
@@ -1112,8 +1241,8 @@ export default {
this.initInterestRate = echarts.init(this.$refs.initInterestRate); this.initInterestRate = echarts.init(this.$refs.initInterestRate);
// 预处理数据:将毛利率和净利率按索引对应(关键,用于手动匹配) // 预处理数据:将毛利率和净利率按索引对应(关键,用于手动匹配)
const profitRateData = data.map(item => item.profitRate || 0); const profitRateData = data.map((item) => item.profitRate || 0);
const netProfitRateData = data.map(item => item.netProfitRate || 0); const netProfitRateData = data.map((item) => item.netProfitRate || 0);
this.initInterestRate.setOption({ this.initInterestRate.setOption({
tooltip: { tooltip: {
@@ -1140,7 +1269,7 @@ export default {
backgroundColor: "#fff", backgroundColor: "#fff",
borderColor: "#eee", borderColor: "#eee",
borderWidth: 1, borderWidth: 1,
boxShadow: "0 2px 8px rgba(0,0,0,0.08)" boxShadow: "0 2px 8px rgba(0,0,0,0.08)",
}, },
xAxis: [ xAxis: [
{ {
@@ -1165,10 +1294,10 @@ export default {
}, },
], ],
grid: { grid: {
top: '5%', top: "5%",
right: '5%', right: "5%",
bottom: '8%', bottom: "8%",
left: '8%', left: "8%",
}, },
series: [ series: [
{ {
@@ -1177,7 +1306,7 @@ export default {
barGap: "0%", // 保持柱子紧贴 barGap: "0%", // 保持柱子紧贴
barWidth: time.length <= 7 ? "50%" : "30%", barWidth: time.length <= 7 ? "50%" : "30%",
data: profitRateData, // 预处理后的毛利率数据 data: profitRateData, // 预处理后的毛利率数据
} },
], ],
}); });
}, },
@@ -1340,9 +1469,12 @@ export default {
async profitRateBarChart() { async profitRateBarChart() {
try { try {
this.initInterestRateLoading = true; this.initInterestRateLoading = true;
const res = await dataSummaryApi.profitRateBarChart({ day: this.interestRateDay, shopId: this.shopId }); const res = await dataSummaryApi.profitRateBarChart({
day: this.interestRateDay,
shopId: this.shopId,
});
this.initInterestRateTime = dayjs().format('HH:mm') this.initInterestRateTime = dayjs().format("HH:mm");
const data = res.map((item) => { const data = res.map((item) => {
return { return {
@@ -1366,7 +1498,7 @@ export default {
this.costLoading = true; this.costLoading = true;
const res = await dataSummaryApi.costLineChart({ day: this.costDay, shopId: this.shopId }); const res = await dataSummaryApi.costLineChart({ day: this.costDay, shopId: this.shopId });
this.costUpdateTime = dayjs().format('HH:mm') this.costUpdateTime = dayjs().format("HH:mm");
const data = res.map((item) => item.productCostAmount || 0); const data = res.map((item) => item.productCostAmount || 0);
const time = res.map((item) => item.tradeDay); const time = res.map((item) => item.tradeDay);

View File

@@ -44,6 +44,8 @@
<span v-if="!downloadLoading">导出Excel</span> <span v-if="!downloadLoading">导出Excel</span>
<span v-else>下载中...</span> <span v-else>下载中...</span>
</el-button> </el-button>
<el-button icon="Printer" :loading="printLoading" @click="$refs.printBusinessDialogRef.show()">打印</el-button>
<importData :type="7" @close="getTableData" />
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@@ -184,19 +186,23 @@
@current-change="paginationChange" @size-change="sizeChange" @current-change="paginationChange" @size-change="sizeChange"
layout="total, sizes, prev, pager, next, jumper"></el-pagination> layout="total, sizes, prev, pager, next, jumper"></el-pagination>
</div> </div>
<printBusinessDialog ref="printBusinessDialogRef" title="商品报表" @success="printBusinessConfirm" />
</div> </div>
</template> </template>
<script> <script>
import _ from 'lodash' import _ from 'lodash'
import importData from "@/components/importData/index.vue";
import saleSummaryApi from "@/api/order/sale-summary"; import saleSummaryApi from "@/api/order/sale-summary";
import categoryApi from "@/api/product/productclassification"; import categoryApi from "@/api/product/productclassification";
import ShopApi from "@/api/account/shop"; import ShopApi from "@/api/account/shop";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { downloadFile, multiplyAndFormat } from "@/utils/index"; import { downloadFile, multiplyAndFormat } from "@/utils/index";
import { formatDateRange } from './utils/index.js' import { formatDateRange } from './utils/index.js'
import printBusinessDialog from '@/components/printBusinessDialog.vue';
export default { export default {
components: { importData, printBusinessDialog },
data() { data() {
return { return {
multiplyAndFormat, multiplyAndFormat,
@@ -230,7 +236,8 @@ export default {
// dayjs(time):将原生 Date 转为 dayjs 对象 // dayjs(time):将原生 Date 转为 dayjs 对象
// isAfter判断目标日期是否在今天之后 // isAfter判断目标日期是否在今天之后
return dayjs(time).isAfter(dayjs().startOf('day')); return dayjs(time).isAfter(dayjs().startOf('day'));
} },
printLoading: false
}; };
}, },
filters: { filters: {
@@ -252,6 +259,34 @@ export default {
this.geiShopList(); this.geiShopList();
}, },
methods: { methods: {
// 确认打印
printBusinessConfirm(res) {
this.printHandle(res.date)
},
// 打印
async printHandle(date) {
try {
const shopStaff = JSON.parse(localStorage.getItem('shopStaff')) || { name: '' }
const shopName = localStorage.getItem('shopName')
this.printLoading = true
await saleSummaryApi.print({
beginDate: date[0],
endDate: date[1],
categoryId: this.query.prodCategoryId,
productName: this.query.productName,
shopId: this.shopId,
rangeType: this.timeValue,
operator: shopStaff.name || shopName
})
ElMessage.success('操作成功')
} catch (error) {
console.log(error);
}
this.printLoading = false
},
/** /**
* 获取分店列表 * 获取分店列表
*/ */

View File

@@ -27,13 +27,14 @@
<el-option v-for="item in branchList" :key="item.shopId" :label="item.shopName" :value="item.shopId" /> <el-option v-for="item in branchList" :key="item.shopId" :label="item.shopName" :value="item.shopId" />
</el-select> </el-select>
</div> </div>
<div> <div style="display: flex;">
<el-button type="primary" @click="getTableData">查询</el-button> <el-button type="primary" @click="getTableData">查询</el-button>
<el-button @click="resetHandle">重置</el-button> <el-button @click="resetHandle">重置</el-button>
<el-button icon="download" v-loading="downloadLoading" @click="downloadHandle"> <el-button icon="download" v-loading="downloadLoading" @click="downloadHandle">
<span v-if="!downloadLoading">导出Excel</span> <span v-if="!downloadLoading">导出Excel</span>
<span v-else>下载中...</span> <span v-else>下载中...</span>
</el-button> </el-button>
<importData :type="8" @close="getTableData" />
</div> </div>
</div> </div>
</el-form> </el-form>
@@ -90,16 +91,17 @@
</el-table-column> </el-table-column>
</el-table> --> </el-table> -->
</div> </div>
<!-- <div class="head-container"> <div class="head-container">
<el-pagination :total="tableData.total" :current-page="tableData.page + 1" :page-size="tableData.size" <el-pagination :total="tableData.total" :current-page="tableData.page" :page-size="tableData.size"
@current-change="paginationChange" @size-change="sizeChange" @current-change="paginationChange" @size-change="sizeChange"
layout="total, sizes, prev, pager, next, jumper"></el-pagination> layout="total, sizes, prev, pager, next, jumper"></el-pagination>
</div> --> </div>
</div> </div>
</template> </template>
<script> <script>
import _ from "lodash"; import _ from "lodash";
import importData from "@/components/importData/index.vue";
import tableSummaryApi from "@/api/order/table-summary"; import tableSummaryApi from "@/api/order/table-summary";
import ShopApi from "@/api/account/shop"; import ShopApi from "@/api/account/shop";
import dayjs from "dayjs"; import dayjs from "dayjs";
@@ -107,6 +109,7 @@ import { downloadFile } from "@/utils/index";
import { formatDateRange } from './utils/index.js' import { formatDateRange } from './utils/index.js'
export default { export default {
components: { importData },
data() { data() {
return { return {
timeValue: "today", timeValue: "today",
@@ -199,13 +202,14 @@ export default {
}, },
// 重置查询 // 重置查询
resetHandle() { resetHandle() {
this.timeValue = ""; this.timeValue = "today";
this.query = { ...this.resetQuery }; this.query = { ...this.resetQuery };
this.page = 1; this.page = 1;
this.getTableData(); this.getTableData();
}, },
// 分页大小改变 // 分页大小改变
sizeChange(e) { sizeChange(e) {
this.tableData.page = 1
this.tableData.size = e; this.tableData.size = e;
this.getTableData(); this.getTableData();
}, },
@@ -225,8 +229,11 @@ export default {
// page: this.tableData.page, // page: this.tableData.page,
// size: this.tableData.size, // size: this.tableData.size,
rangeType: this.timeValue, rangeType: this.timeValue,
// rangeType: 'custom',
beginDate: this.query.createdAt[0], beginDate: this.query.createdAt[0],
endDate: this.query.createdAt[1], endDate: this.query.createdAt[1],
// beginDate: '2026-01-26',
// endDate: '2026-01-26',
shopId: this.shopId shopId: this.shopId
}); });
this.tableData.loading = false; this.tableData.loading = false;

View File

@@ -4,10 +4,10 @@
<el-table-column prop="shopName" align="center" label="商户名称" /> <el-table-column prop="shopName" align="center" label="商户名称" />
<el-table-column prop="staffName" align="center" label="职员名称" /> <el-table-column prop="staffName" align="center" label="职员名称" />
<el-table-column prop="orderCount" align="center" label="订单数量" /> <el-table-column prop="orderCount" align="center" label="订单数量" />
<el-table-column prop="handAmount" align="center" label="应交金额" /> <el-table-column prop="orderTurnover" align="center" label="应交金额" />
<el-table-column prop="quickInAmount" align="center" label="快捷收款金额" /> <el-table-column prop="balance" align="center" label="余额支付" />
<el-table-column prop="refundAmount" align="center" label="退款金额" /> <el-table-column prop="refundAmount" align="center" label="退款金额" />
<el-table-column prop="handAmount" align="center" label="总收入" /> <el-table-column prop="turnover" align="center" label="总收入" />
<el-table-column prop="loginTime" align="center" label="开始时间" /> <el-table-column prop="loginTime" align="center" label="开始时间" />
<el-table-column prop="handoverTime" align="center" label="交班时间" /> <el-table-column prop="handoverTime" align="center" label="交班时间" />
<el-table-column label="操作" align="center"> <el-table-column label="操作" align="center">

View File

@@ -15,9 +15,13 @@ const modalConfig: IModalConfig<addRequest> = {
}, },
formAction: function (data) { formAction: function (data) {
let obj = { ...data } let obj = { ...data }
obj.printType = data.printType.join(',') console.log("打印类型", data);
// obj.printType = data.printType.join(',')
obj.categoryIds = JSON.stringify(data.categoryIdsArr) obj.categoryIds = JSON.stringify(data.categoryIdsArr)
obj.categoryList = JSON.stringify(data.categoryIdsArr) obj.categoryList = JSON.stringify(data.categoryIdsArr)
if (data.classifyPrint == 0) {
obj.categoryIds = ''
}
// obj.categoryIds = '[' + data.categoryIdsArr.join(',') + ']' // obj.categoryIds = '[' + data.categoryIdsArr.join(',') + ']'
return printerApi.add(obj); return printerApi.add(obj);
}, },
@@ -85,7 +89,7 @@ const modalConfig: IModalConfig<addRequest> = {
{ {
type: "select", type: "select",
label: "打印类型", label: "打印类型",
prop: "subType", prop: "printType",
rules: [{ required: false, message: "请选择打印类型", trigger: "blur" }], rules: [{ required: false, message: "请选择打印类型", trigger: "blur" }],
attrs: { attrs: {
placeholder: "请选择打印类型", placeholder: "请选择打印类型",
@@ -103,7 +107,7 @@ const modalConfig: IModalConfig<addRequest> = {
{ {
type: "select", type: "select",
label: "打印机品牌", label: "打印机品牌",
prop: "contentType", prop: "brand",
rules: [{ required: true, message: "请选择打印机品牌", trigger: "blur" }], rules: [{ required: true, message: "请选择打印机品牌", trigger: "blur" }],
attrs: { attrs: {
placeholder: "请选择打印机品牌", placeholder: "请选择打印机品牌",
@@ -139,27 +143,27 @@ const modalConfig: IModalConfig<addRequest> = {
label: "", label: "",
initialValue: [] initialValue: []
}, },
{ {
type: "radio", type: "radio",
label: "打印数量", label: "打印数量",
prop: "printQty", prop: "printNum",
options: options.printQty, options: options.printQty,
initialValue: options.printQty[0].value initialValue: options.printQty[0].value
}, },
{ {
type: "radio", type: "radio",
label: "打印方式", label: "打印方式",
prop: "printMethod", prop: "kitchenPrintMode",
options: options.printMethod, options: options.printMethod,
initialValue: options.printMethod[0].value initialValue: options.printMethod[0].value
}, },
{ {
type: "checkbox", type: "custom",
label: "打印类型", label: "打印类型",
prop: "printType", prop: "printContentType",
options: options.printType, options: options.printType,
initialValue: options.printType.map(v => v.value) initialValue: options.printType.map(v => v.value),
slotName: 'printTypeSlot'
}, },
{ {
label: "打印机状态", label: "打印机状态",

View File

@@ -1,8 +1,8 @@
export const options: optionObject = { export const options: optionObject = {
connectionType: [ connectionType: [
{ label: "USB", value: 'USB' }, { label: "USB", value: 'USB' },
{ label: "网络", value: '网络' }, { label: "云打印", value: '云打印' },
{ label: "蓝牙", value: '蓝牙' }, { label: "局域网", value: '局域网' },
], ],
subType: [ subType: [
{ label: "标签", value: 'label' }, { label: "标签", value: 'label' },

View File

@@ -59,8 +59,8 @@ const contentConfig: IContentConfig<getListRequest> = {
label: "状态", label: "状态",
align: "center", align: "center",
prop: "status", prop: "status",
templet: "switch", templet: "custom",
slotName: "status", slotName: "status"
}, },
{ label: "创建时间", align: "center", prop: "createTime" }, { label: "创建时间", align: "center", prop: "createTime" },
// { // {

View File

@@ -20,6 +20,9 @@ const modalConfig: IModalConfig<editRequest> = {
obj.categoryIds = JSON.stringify(data.categoryIdsArr) obj.categoryIds = JSON.stringify(data.categoryIdsArr)
obj.categoryList = JSON.stringify(data.categoryIdsArr) obj.categoryList = JSON.stringify(data.categoryIdsArr)
} }
if (data.classifyPrint == 0) {
obj.categoryIds = ''
}
return printerApi.edit(obj); return printerApi.edit(obj);
}, },
beforeSubmit(data) { beforeSubmit(data) {
@@ -157,13 +160,12 @@ const modalConfig: IModalConfig<editRequest> = {
initialValue: options.printMethod[0].value initialValue: options.printMethod[0].value
}, },
{ {
type: "checkbox", type: "custom",
label: "打印类型", label: "打印类型",
prop: "printType", prop: "printContentType",
options: options.printType, options: '',
initialValue: options.printType.map(v => v.value) initialValue: ''
}, },
{ {
label: "打印机状态", label: "打印机状态",
prop: "status", prop: "status",
@@ -174,6 +176,16 @@ const modalConfig: IModalConfig<editRequest> = {
inactiveValue: 0, inactiveValue: 0,
} }
}, },
{
label: "媒体音开关",
prop: "volumeSwitch",
type: "switch",
initialValue: 1,
attrs: {
activeValue: 1,
inactiveValue: 0,
}
},
], ],
}; };

View File

@@ -9,16 +9,16 @@
<page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick" <page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick"
@edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick" @edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick"
@toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange"> @toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange">
<template #status="scope"> <!-- <template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'"> <el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }} {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
</el-tag> </el-tag>
</template> </template> -->
<template #contentType="scope"> <template #contentType="scope">
{{ scope.row.contentType == 'yxyPrinter' ? "云想印" : "飞鹅" }} {{ scope.row.contentType == "yxyPrinter" ? "云想印" : "飞鹅" }}
</template> </template>
<template #subType="scope"> <template #subType="scope">
{{ scope.row.subType == 'label' ? "标签" : "小票" }} {{ scope.row.subType == "label" ? "标签" : "小票" }}
</template> </template>
<template #caozuo="scope"> <template #caozuo="scope">
{{ scope }} {{ scope }}
@@ -26,10 +26,18 @@
<template #gender="scope"> <template #gender="scope">
<DictLabel v-model="scope.row[scope.prop]" code="gender" /> <DictLabel v-model="scope.row[scope.prop]" code="gender" />
</template> </template>
<template #status="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0"
@click="statusChange($event, scope.row)"></el-switch>
</template>
<template #operate="scope"> <template #operate="scope">
<div v-if="scope.row.connectionType != 'USB'"> <div v-if="scope.row.connectionType != 'USB'">
<el-button @click="handleEditClick(scope.row)" icon="Edit" type="primary" link>编辑</el-button> <el-button @click="handleEditClick(scope.row)" icon="Edit" type="primary" link>
<el-button @click="handdeleteevent(scope.row)" icon="Delete" type="danger" link>删除</el-button> 编辑
</el-button>
<el-button @click="handdeleteevent(scope.row)" icon="Delete" type="danger" link>
删除
</el-button>
</div> </div>
</template> </template>
<!-- <template #mobile="scope"> <!-- <template #mobile="scope">
@@ -39,10 +47,21 @@
</page-content> </page-content>
<!-- 新增 --> <!-- 新增 -->
<page-modal ref="addModalRef" :modal-config="addModalConfig" @submit-click="handleSubmitClick"> <!-- <page-modal ref="addModalRef" :modal-config="addModalConfig" @submit-click="handleSubmitClick">
<template #gender="scope"> <template #gender="scope">
<Dict v-model="scope.formData[scope.prop]" code="gender" /> <Dict v-model="scope.formData[scope.prop]" code="gender" />
</template> </template>
<template #printTypeSlot="scope">
<div class="row" v-for="(item, index) in printTypeList" :key="index">
<div class="title">{{ item.label }}</div>
<div class="cont">
<el-checkbox-group v-model="item.values" @change="printTypeChange($event, index, scope)">
<el-checkbox :label="item.label" :value="item.value" v-for="item in item.list"
:key="item.value"></el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
<template #classifyPrintData="scope"> <template #classifyPrintData="scope">
<template v-if="scope.formData.classifyPrint == 1"> <template v-if="scope.formData.classifyPrint == 1">
<el-checkbox-group v-model="scope.formData.categoryIdsArr"> <el-checkbox-group v-model="scope.formData.categoryIdsArr">
@@ -52,13 +71,24 @@
</el-checkbox-group> </el-checkbox-group>
</template> </template>
</template> </template>
</page-modal> </page-modal> -->
<!-- 编辑 --> <!-- 编辑 -->
<page-modal ref="editModalRef" :modal-config="editModalConfig" @submit-click="handleSubmitClick"> <!-- <page-modal ref="editModalRef" :modal-config="editModalConfig" @submit-click="handleSubmitClick">
<template #gender="scope"> <template #gender="scope">
<Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" /> <Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
</template> </template>
<template #printTypeSlot="scope">
<div class="row" v-for="(item, index) in printTypeList" :key="index">
<div class="title">{{ item.label }}</div>
<div class="cont">
<el-checkbox-group v-model="item.values" @change="printTypeChange($event, index, scope)">
<el-checkbox :label="item.label" :value="item.value" v-for="item in item.list"
:key="item.value"></el-checkbox>
</el-checkbox-group>
</div>
</div>
</template>
<template #classifyPrintData="scope"> <template #classifyPrintData="scope">
<template v-if="scope.formData.classifyPrint == 1"> <template v-if="scope.formData.classifyPrint == 1">
<el-checkbox-group v-model="scope.formData.categoryIdsArr"> <el-checkbox-group v-model="scope.formData.categoryIdsArr">
@@ -68,11 +98,103 @@
</el-checkbox-group> </el-checkbox-group>
</template> </template>
</template> </template>
</page-modal> </page-modal> -->
<el-dialog :title="form.id ? '编辑打印机' : '添加打印机'" width="800px" v-model="visible" @closed="dialogClosed"
@open="dialogOpen">
<div style="height: 60vh;overflow-y: auto;" ref="formDivRef">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
<el-form-item label="设备名称" prop="name">
<el-input v-model="form.name" placeholder="请输入设备名称"></el-input>
</el-form-item>
<el-form-item label="设备类型">
<el-radio-group v-model="form.connectionType">
<el-radio-button label="USB" value="USB"></el-radio-button>
<el-radio-button label="云打印" value="云打印"></el-radio-button>
<el-radio-button label="局域网" value="局域网"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="小票类型">
<el-radio-group v-model="form.printType">
<el-radio-button label="标签" value="label"></el-radio-button>
<el-radio-button label="小票" value="cash"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="打印机品牌" prop="brand">
<el-radio-group v-model="form.brand">
<el-radio-button label="飞鹅"></el-radio-button>
<el-radio-button label="云想印"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="ip地址/MAC地址" prop="address">
<el-input v-model="form.address" placeholder="请输入ip地址/MAC地址"></el-input>
</el-form-item>
<el-form-item label="端口" prop="port">
<el-input v-model="form.port" placeholder="请输入端口"></el-input>
</el-form-item>
<el-form-item label="小票尺寸" prop="receiptSize">
<el-radio-group v-model="form.receiptSize">
<el-radio-button label="58mm"></el-radio-button>
<el-radio-button label="80mm"></el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="打印数量" prop="printNum">
<el-input-number v-model="form.printNum" :step="1" :min="1"></el-input-number>
</el-form-item>
<el-form-item label="打印内容" prop="printContentType">
<div class="row" v-for="(item, index) in printTypeList" :key="index">
<div class="title">{{ item.label }}</div>
<div class="cont">
<el-checkbox-group v-model="item.values" @change="printTypeChange($event, index)">
<el-checkbox :label="item.label" :value="item.value" v-for="item in item.list"
:key="item.value"></el-checkbox>
</el-checkbox-group>
</div>
</div>
</el-form-item>
<!-- <el-form-item label="打印模式" prop="kitchenPrintMode">
<el-radio-group v-model="form.kitchenPrintMode">
<el-radio-button label="整单" value="all"></el-radio-button>
<el-radio-button label="单个" value="only"></el-radio-button>
</el-radio-group>
<el-text type="info" style="margin-left: 14px;">仅针对厨房制作单的打印</el-text>
</el-form-item> -->
<el-form-item label="分类打印">
<div class="column">
<div style="display: flex;align-items: center;">
<el-radio-group v-model="form.classifyPrint">
<el-radio-button label="所有" value="0"></el-radio-button>
<el-radio-button label="部分分类" value="1" v-if="printTypeList[1].values.length > 0"></el-radio-button>
</el-radio-group>
<el-text type="info" style="margin-left: 14px;">仅针对厨房制作单的打印</el-text>
</div>
<template v-if="form.classifyPrint == 1">
<el-checkbox-group v-model="form.categoryIds">
<el-checkbox v-for="item in PrinterTypeList" :value="item.id" :label="item.name"></el-checkbox>
</el-checkbox-group>
</template>
</div>
</el-form-item>
<el-form-item label="媒体音量">
<el-switch v-model="form.volumeSwitch" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
</el-form>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="submitHandle"> </el-button>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import _ from 'lodash'
import UserAPI from "@/api/account/printer"; import UserAPI from "@/api/account/printer";
import type { IObject, IOperatData } from "@/components/CURD/types"; import type { IObject, IOperatData } from "@/components/CURD/types";
import usePage from "@/components/CURD/usePage"; import usePage from "@/components/CURD/usePage";
@@ -80,6 +202,166 @@ import addModalConfig from "./config/add";
import contentConfig from "./config/content"; import contentConfig from "./config/content";
import editModalConfig from "./config/edit"; import editModalConfig from "./config/edit";
import searchConfig from "./config/search"; import searchConfig from "./config/search";
import { options } from './config/config'
import printerApi, { type addRequest } from "@/api/account/printer";
const printTypeList = ref([
{
label: '前台',
values: [],
list: [
{ label: '客看单', value: 'GUEST_ORDER' },
{ label: '预结算单', value: 'PRE_ORDER' },
{ label: '结算单', value: 'ORDER' },
{ label: '退菜单', value: 'RETURN_ORDER' },
{ label: '退款单', value: 'REFUND_ORDER' },
]
},
{
label: '后厨',
values: [],
list: [
{ label: '后厨-整单', value: 'ALL_KITCHEN' },
{ label: '后厨-分单', value: 'ONLY_KITCHEN' },
{ label: '后厨-退菜单', value: 'REFUND_KITCHEN' },
]
},
{
label: '其它',
values: [],
list: [
{ label: '交班单', value: 'HANDOVER' },
{ label: '排队取号', value: 'CALL' },
{ label: '储值单', value: 'RECHARGE' },
{ label: '入库单', value: 'STOCK' },
{ label: '盘点单', value: 'STOCK_CHECK' },
{ label: '商品报表', value: 'PRODUCT_REPORT' },
{ label: '经营日报', value: 'DAY_REPORT' },
{ label: '日结单', value: 'DAY_ORDER' },
]
}
])
const loading = ref(false)
const visible = ref(false)
const formRef = ref(null)
const obj = {
id: '',
name: '', // 设备名称
connectionType: '云打印', // 连接方式 USB、云打印、局域网
printType: 'cash', // 打印类型 label标签 cash小票
brand: '', // 打印机品牌 飞鹅/云想印
address: '', // ip地址/MAC地址
port: '', // 端口
receiptSize: '58mm', // 小票尺寸 58mm 80mm
printNum: 1, // 打印数量
printContentType: '', // 打印内容
kitchenPrintMode: 'all', // 打印模式(厨房打印菜品) all整单 /only单个
classifyPrint: '0', // 分类打印 0-所有 1-部分分类
categoryIds: [], // 分类Id
status: 1, // 0 禁用 1启用
volumeSwitch: 1, // 媒体声音开关 0关1开
}
const form = ref({ ...obj })
const rules = {
name: [
{
required: true,
message: '请输入设备名称',
trigger: 'blur'
}
],
brand: [
{
required: true,
message: '请选择打印机品牌',
trigger: 'change'
}
],
printNum: [
{
required: true,
message: '请选择打印数量',
trigger: 'change'
}
],
// kitchenPrintMode: [
// {
// required: true,
// message: '请选择打印模式',
// trigger: 'change'
// }
// ]
}
function dialogClosed() {
formRef.value.resetFields()
form.value = { ...obj }
}
const formDivRef = ref(null)
async function dialogOpen() {
await nextTick()
if (formDivRef.value) {
// console.log('开始滚动到顶部')
formDivRef.value.scrollTop = 0
}
}
// 打印类型切换
function printTypeChange(e, index, scope) {
if (index == 1 && printTypeList.value[index].values.length == 0) {
form.value.categoryList = []
}
let arr = []
printTypeList.value.forEach(item => {
arr.push(...item.values)
})
form.value.printContentType = arr.join(',')
}
function submitHandle() {
console.log('submitHandle===', form.value);
formRef.value.validate(async valid => {
try {
if (valid) {
loading.value = true
const data = { ...form.value }
data.categoryIds = form.value.categoryIds.join(',')
if (form.value.id) {
await printerApi.edit(data)
} else {
await printerApi.add(data)
}
ElMessage.success(form.value.id ? '编辑成功' : '添加成功')
visible.value = false
handleQueryClick();
}
} catch (error) {
console.log(error);
}
setTimeout(() => {
loading.value = false
}, 300);
})
}
// 更改状态
async function statusChange(e, data) {
try {
await printerApi.edit({
id: data.id,
status: data.status
});
handleQueryClick();
} catch (error) {
console.log(error);
}
}
const { const {
searchRef, searchRef,
@@ -98,11 +380,11 @@ const {
onMounted(() => { onMounted(() => {
getPrinterType(); getPrinterType();
}); });
let PrinterTypeList = ref([]) let PrinterTypeList = ref([]);
// 获取商品分类 // 获取商品分类
async function getPrinterType() { async function getPrinterType() {
let res = await UserAPI.getPrinterType(); let res = await UserAPI.getPrinterType();
PrinterTypeList.value = res.records PrinterTypeList.value = res.records;
} }
function handdeleteevent(item) { function handdeleteevent(item) {
ElMessageBox.confirm("确认删除?", "警告", { ElMessageBox.confirm("确认删除?", "警告", {
@@ -114,28 +396,45 @@ function handdeleteevent(item) {
ElMessage.success("删除成功"); ElMessage.success("删除成功");
handleQueryClick(); handleQueryClick();
}); });
}); });
} }
// 新增 // 新增
async function handleAddClick() { async function handleAddClick() {
addModalRef.value?.setModalVisible(); // addModalRef.value?.setModalVisible();
visible.value = true
} }
// 编辑 // 编辑
async function handleEditClick(row: IObject) { async function handleEditClick(row: IObject) {
editModalRef.value?.handleDisabled(false); form.value = { ...row }
editModalRef.value?.setModalVisible(); visible.value = true
// 根据id获取数据进行填充
let data = await UserAPI.get(row.id);
data.printType = data.printType.split(',');
if (data.categoryIds) { form.value.categoryIds = form.value.categoryIds.split(',')
data.categoryIdsArr = JSON.parse(data.categoryIds)
}
data.classifyPrint = data.classifyPrint * 1; const printContentTypes = row.printContentType.split(',')
printTypeList.value.forEach(val => {
val.values = _.map(
_.filter(val.list, item => printContentTypes.includes(item.value)),
'value'
);
})
editModalRef.value?.setFormData(data); // editModalRef.value?.handleDisabled(false);
// editModalRef.value?.setModalVisible();
// // 根据id获取数据进行填充
// let data = await UserAPI.get(row.id);
// data.printType = data.printType.split(",");
// if (data.categoryIds) {
// data.categoryIdsArr = JSON.parse(data.categoryIds);
// } else {
// data.categoryIdsArr = [];
// }
// console.log(data.categoryIdsArr);
// console.log(data);
// data.classifyPrint = data.classifyPrint * 1;
// editModalRef.value?.setFormData(data);
} }
// 其他工具栏 // 其他工具栏
function handleToolbarClick(name: string) { function handleToolbarClick(name: string) {

View File

@@ -1,5 +1,112 @@
<template> <template>
<div> <div class="gyq_container">
<h1>Index</h1> <div class="item_wrap">
<div class="title">您好欢迎登录</div>
<div class="item_list">
<div class="item" v-for="(item, index) in quickStore.quickMenus.splice(0, 6)" :key="item.id"
@click="menuClick(item.menuId)">
<img class="icon" :src="icons[index + 1]" alt="">
{{ returnMenuName(item.menuId) }}
</div>
</div>
</div>
</div> </div>
</template> </template>
<script setup>
import { onMounted, nextTick } from "vue";
import { useQuickStore, usePermissionStore, useUserStore } from "@/store";
import icon1 from "@/assets/index_quick1.png";
import icon2 from "@/assets/index_quick2.png";
import icon3 from "@/assets/index_quick3.png";
import icon4 from "@/assets/index_quick4.png";
import icon5 from "@/assets/index_quick5.png";
import icon6 from "@/assets/index_quick6.png";
const icons = {
1: icon1,
2: icon2,
3: icon3,
4: icon4,
5: icon5,
6: icon6,
};
const userStore = useUserStore();
const quickStore = useQuickStore();
const permissionStore = usePermissionStore();
function returnMenuName(menuId) {
return permissionStore.returnMenuName(menuId);
}
function menuClick(menuId) {
permissionStore.menuJump(menuId);
}
onMounted(async () => {
await nextTick();
console.log('quickStore.quickMenus', quickStore.quickMenus);
})
</script>
<style scoped lang="scss">
.gyq_container {
width: 100%;
height: 100%;
background: url('@/assets/index_bg.png') no-repeat center center / cover;
display: flex;
align-items: center;
padding-bottom: 50px;
}
.item_wrap {
padding-left: 300px;
.title {
font-size: 40px;
margin-bottom: 50px;
font-weight: bold;
}
.item_list {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
grid-column-gap: 90px;
grid-row-gap: 90px;
.item {
width: 152px;
height: 83px;
border-radius: 20px;
background: linear-gradient(115deg, #9EC4FF 5.9%, #6668E8 111.62%);
position: relative;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: #fff;
padding-left: 30px;
&:hover {
cursor: pointer;
.icon {
transform: translateY(-10px);
}
}
.icon {
position: absolute;
left: -30px;
top: -20px;
width: 83px;
height: 78px;
transition: all 0.3s ease-in-out;
}
}
}
}
</style>

View File

@@ -0,0 +1,81 @@
<template>
<el-dialog :title="form.id ? '编辑耗材类型' : '添加耗材类型'" width="600px" v-model="visible" @close="onClose">
<el-form ref="formRef" :model="form" :rules="rules" label-width="120" label-position="right">
<el-form-item label="耗材类型名称" prop="name">
<el-input v-model="form.name" placeholder="请输入耗材类型名称" :maxlength="20"></el-input>
</el-form-item>
<el-form-item label="启用">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="handleOk" :loading="confirmLoading"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import _ from 'lodash'
import { ref } from 'vue'
import Api from "@/api/product/cons-group";
const visible = ref(false)
const formRef = ref(null)
const form = ref({
name: '',
status: 1
})
const rules = ref({
name: [
{
required: true,
message: '请输入耗材类型名称',
trigger: 'blur'
}
]
})
const emit = defineEmits(['success'])
const confirmLoading = ref(false)
function handleOk() {
formRef.value.validate(async vaild => {
try {
if (vaild) {
confirmLoading.value = true
if (form.value.id) {
await Api.edit(form.value)
ElMessage.success('编辑成功')
} else {
await Api.add(form.value)
ElMessage.success('添加成功')
}
emit('success')
visible.value = false
}
} catch (error) {
console.log(error);
} finally {
confirmLoading.value = false
}
})
}
function onClose() {
formRef.value.resetFields()
form.value.name = ''
form.value.status = 1
}
function open(obj) {
visible.value = true
if (obj && obj.id) {
form.value = _.cloneDeep(obj)
}
}
defineExpose({ open })
</script>

View File

@@ -9,17 +9,9 @@
@reset-click="handleResetClick" @reset-click="handleResetClick"
/> --> /> -->
<!-- 列表 --> <!-- 列表 -->
<page-content <page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick"
ref="contentRef" @edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick"
:content-config="contentConfig" @toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange">
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<template #status="scope"> <template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'"> <el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }} {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
@@ -29,36 +21,22 @@
{{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }} {{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }}
</template> </template>
<template #switch="scope"> <template #switch="scope">
<el-switch <el-switch v-model="scope.row[scope.prop]" disabled :active-value="1" :inactive-value="0"></el-switch>
v-model="scope.row[scope.prop]"
disabled
:active-value="1"
:inactive-value="0"
></el-switch>
</template> </template>
<template #mobile="scope"> <template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text> <el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button <copy-button v-if="scope.row[scope.prop]" :text="scope.row[scope.prop]" style="margin-left: 2px" />
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
</template> </template>
</page-content> </page-content>
<!-- 新增 --> <!-- 新增 -->
<page-modal <page-modal ref="addModalRef" :modal-config="addModalConfig" @submit-click="handleSubmitClick"></page-modal>
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
></page-modal>
<!-- 编辑 --> <!-- 编辑 -->
<page-modal <page-modal ref="editModalRef" :modal-config="editModalConfig" @submit-click="handleSubmitClick"></page-modal>
ref="editModalRef"
:modal-config="editModalConfig" <!-- 添加分类 -->
@submit-click="handleSubmitClick" <addClassificationModal ref="addClassificationModalRef" @success="handleResetClick" />
></page-modal>
</div> </div>
</template> </template>
@@ -72,6 +50,9 @@ import editModalConfig from "./config/edit";
import searchConfig from "./config/search"; import searchConfig from "./config/search";
import { returnOptionsLabel } from "./config/config"; import { returnOptionsLabel } from "./config/config";
import { isSyncStatus } from "@/utils/index"; import { isSyncStatus } from "@/utils/index";
import addClassificationModal from "./components/addClassificationModal.vue";
const addClassificationModalRef = ref(null)
const { const {
searchRef, searchRef,
@@ -98,16 +79,18 @@ if (isSyncStatus()) {
// 新增 // 新增
async function handleAddClick() { async function handleAddClick() {
addModalRef.value?.setModalVisible(); addClassificationModalRef.value.open()
// addModalRef.value?.setModalVisible();
// addModalConfig.formItems[2]!.attrs!.data = // addModalConfig.formItems[2]!.attrs!.data =
} }
// 编辑 // 编辑
async function handleEditClick(row: IObject) { async function handleEditClick(row: IObject) {
editModalRef.value?.handleDisabled(false); addClassificationModalRef.value.open(row)
editModalRef.value?.setModalVisible(); // editModalRef.value?.handleDisabled(false);
// 根据id获取数据进行填充 // editModalRef.value?.setModalVisible();
console.log(row); // // 根据id获取数据进行填充
editModalRef.value?.setFormData({ ...row }); // console.log(row);
// editModalRef.value?.setFormData({ ...row });
} }
1; 1;
// 其他工具栏 // 其他工具栏

View File

@@ -6,10 +6,13 @@
<el-input v-model="form.conName" placeholder="请输入耗材名称"></el-input> <el-input v-model="form.conName" placeholder="请输入耗材名称"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="耗材分类" prop="consGroupId"> <el-form-item label="耗材分类" prop="consGroupId">
<div class="center" style="gap: 14px;">
<el-select v-model="form.consGroupId" placeholder="请选择耗材分类" style="width: 200px"> <el-select v-model="form.consGroupId" placeholder="请选择耗材分类" style="width: 200px">
<el-option v-for="option in consGroups" :key="option.conTypeId" :label="option.label" <el-option v-for="option in consGroups" :key="option.conTypeId" :label="option.label"
:value="option.id"></el-option> :value="option.id"></el-option>
</el-select> </el-select>
<el-button type="primary" icon="Plus" @click="addClassificationModalRef.open()">添加耗材类型</el-button>
</div>
</el-form-item> </el-form-item>
<el-form-item label="耗材价格" prop="price"> <el-form-item label="耗材价格" prop="price">
<div class="center"> <div class="center">
@@ -54,6 +57,7 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-dialog> </el-dialog>
<addClassificationModal ref="addClassificationModalRef" @success="getConsGroups" />
</template> </template>
@@ -62,6 +66,9 @@ import { ref, reactive, computed } from "vue";
import consApi from "@/api/product/cons"; import consApi from "@/api/product/cons";
import consGroupApi from "@/api/product/cons-group"; import consGroupApi from "@/api/product/cons-group";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import addClassificationModal from "../../classification/components/addClassificationModal.vue";
const addClassificationModalRef = ref(null)
const consGroups = ref([]); const consGroups = ref([]);
const rules = { const rules = {

View File

@@ -97,13 +97,13 @@ const contentConfig: IContentConfig = {
align: "center", align: "center",
prop: "conName", prop: "conName",
}, },
{ // {
label: "单位", // label: "单位",
align: "center", // align: "center",
prop: "conUnit", // prop: "conUnit",
templet: "custom", // templet: "custom",
slotName: "conUnit", // slotName: "conUnit",
}, // },
{ {
label: "所属商品", label: "所属商品",
align: "center", align: "center",
@@ -112,14 +112,24 @@ const contentConfig: IContentConfig = {
}, },
{ {
label: "库存数量", label: "库存数量",
align: "center", align: "left",
prop: "stockNumber", prop: "stockNumber",
templet: "custom",
slotName: "stockNumber",
}, },
{ {
label: "预警值", label: "预警值",
align: "center", align: "center",
prop: "conWarning", prop: "conWarning",
}, },
{
width: '120',
label: "是否检测耗材",
align: "center",
prop: "isStock",
templet: "custom",
slotName: "isStock",
},
{ {
label: "是否启用", label: "是否启用",
align: "center", align: "center",

View File

@@ -10,6 +10,25 @@
<page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick" <page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick"
@edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick" @edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick"
@toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange"> @toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange">
<template #stockNumber="scope">
<div class="columne">
<div class="center">
<el-text>第一单位</el-text>
<el-text>{{ scope.row.stockNumber }}/{{ scope.row.conUnit }}</el-text>
</div>
<div class="center" v-if="scope.row.conUnitTwo">
<el-text>第二单位</el-text>
<el-text>{{ scope.row.stockNumber / scope.row.conUnitTwoConvert }}/{{ scope.row.conUnitTwo }}</el-text>
</div>
<div class="center" v-else>
<el-text type="info">未设置第二单位</el-text>
</div>
</div>
</template>
<template #isStock="scope">
<el-switch v-model="scope.row.isStock" :active-value="1" :inactive-value="0"
@click="isStockChange($event, scope.row)"></el-switch>
</template>
<template #status="scope"> <template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'"> <el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }} {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
@@ -124,6 +143,12 @@ const {
handleFilterChange, handleFilterChange,
} = usePage(); } = usePage();
async function isStockChange(e, row) {
// console.log('isStockChange.e', e);
// console.log('isStockChange.row', row);
await consApi.edit(row)
}
function toGoods(id: number | string) { function toGoods(id: number | string) {
router.push({ path: "/product/index", query: { id: id } }); router.push({ path: "/product/index", query: { id: id } });
} }
@@ -280,6 +305,11 @@ onMounted(() => {
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.center {
display: flex;
gap: 10px;
}
.goodslang { .goodslang {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;

View File

@@ -158,7 +158,7 @@
<!-- 选择耗材 --> <!-- 选择耗材 -->
<ConsumableList ref="ConsumableList" @success="selectConsumable" /> <ConsumableList ref="ConsumableList" @success="selectConsumable" />
<el-dialog v-model="showResult" :show-close="false" :close-on-press-escape="false" :close-on-click-modal="false"> <el-dialog v-model="showResult" :show-close="false" :close-on-press-escape="false" :close-on-click-modal="false">
<el-result icon="success" title="入库提交成功" :subTitle="`共操作${tableData.list.length}件商品`"> <el-result icon="success" :title="`提交成功3秒后自动关闭`" :subTitle="`共操作${tableData.list.length}件商品`">
<template #extra> <template #extra>
<template> <template>
<el-button type="primary" size="medium" @click="resetHandle">创建新的入库单</el-button> <el-button type="primary" size="medium" @click="resetHandle">创建新的入库单</el-button>
@@ -173,7 +173,7 @@
</template> </template>
<script> <script>
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage } from "element-plus";
import consApi from "@/api/product/cons"; import consApi from "@/api/product/cons";
import Decimal from "decimal.js"; import Decimal from "decimal.js";
import vendorApi from "@/api/product/vendor"; import vendorApi from "@/api/product/vendor";
@@ -402,13 +402,17 @@ export default {
} }
this.queryFormLoading = false; this.queryFormLoading = false;
// const title = this.type == "in" ? "入库" : "出库"; // const title = this.type == "in" ? "入库" : "出库";
ElMessage({ // ElMessage({
// message: title + "提交成功", // // message: title + "提交成功",
message: "提交成功", // message: "提交成功",
type: "success", // type: "success",
}); // });
this.$router.push("/inventory/consumables");
this.showResult = true; this.showResult = true;
setTimeout(() => {
this.$router.push("/inventory/consumables");
}, 3000);
// this.$refs.shopList.reset()//清除选项 // this.$refs.shopList.reset()//清除选项
// this.$refs.ConsumableList.reset()//清除选项 // this.$refs.ConsumableList.reset()//清除选项
} catch (error) { } catch (error) {

View File

@@ -70,16 +70,23 @@ const env = process.env.NODE_ENV
// DEV-START // DEV-START
const accountList = reactive([ const accountList = reactive([
{ username: "admin", type: 'primary', label: 'admin' }, { username: "admin", type: 'primary', label: 'admin' },
// { username: "19191703856", type: 'warning', label: '喜气洋洋' }, { username: "18049104914", type: 'warning', label: '东风的店铺' },
// { username: "19107220837", type: 'danger', label: '快乐时光店铺' }, { username: "19107220837", type: 'danger', label: '快乐时光店铺' },
// { username: "18199991111", type: 'success', label: '草莓加盟主店可直接管理' }, // { username: "18199991111", type: 'success', label: '草莓加盟主店可直接管理' },
{ username: "18821670757", type: 'success', label: '高歌的小店' }, { username: "18821670757", type: 'success', label: '高歌的小店' },
// { username: "19112345678", type: 'danger', label: '酸橘子·云贵小馆' }, { username: "18821670757", staffUserName: '18821670758', type: 'primary', label: '高歌的小店的员工-张三' },
{ username: "191123456", type: 'primary', label: '酸橘子' },
]); ]);
// 快捷模拟登录 // 快捷模拟登录
function accountHandle(item) { function accountHandle(item) {
state.loginForm.username = item.username; state.loginForm.username = item.username;
if (item.staffUserName) {
state.loginForm.loginType = 1
state.loginForm.staffUserName = item.staffUserName
} else {
state.loginForm.loginType = 0
}
state.loginForm.code = 666666 state.loginForm.code = 666666
const d = new Date(); const d = new Date();
state.loginForm.password = `czg${d.getHours().toString().padStart(2, '0')}${d.getMinutes().toString().padStart(2, '0')}`; state.loginForm.password = `czg${d.getHours().toString().padStart(2, '0')}${d.getMinutes().toString().padStart(2, '0')}`;
@@ -191,6 +198,12 @@ function handleLogin() {
userStore userStore
.login(user) .login(user)
.then(async (res) => { .then(async (res) => {
console.log('login===', res);
localStorage.setItem('shopStaff', JSON.stringify(res.shopStaff))
const token = getToken(); const token = getToken();
console.log("token", token); console.log("token", token);
$douyin_checkIn({ $douyin_checkIn({

View File

@@ -12,7 +12,7 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, nextTick } from 'vue' import { ref, onMounted, nextTick, watch } from 'vue'
const props = defineProps({ const props = defineProps({
// {label: '设置',value: 1} // {label: '设置',value: 1}
@@ -29,10 +29,8 @@ const modelValue = defineModel('modelValue', {
required: true required: true
}) })
// 改变索引 // 更新active_wrap位置
function changeHandle(index) { function updateActivePosition(index) {
modelValue.value = index
let left = 0 let left = 0
itemsWidth.value.forEach((val, i) => { itemsWidth.value.forEach((val, i) => {
if (i < index) { if (i < index) {
@@ -40,7 +38,12 @@ function changeHandle(index) {
} }
}) })
leftValue.value = left + gap.value * index leftValue.value = left + gap.value * index
}
// 改变索引
function changeHandle(index) {
modelValue.value = index
updateActivePosition(index)
emits('change', index) emits('change', index)
} }
@@ -57,10 +60,14 @@ onMounted(() => {
console.log('itemRefs===', itemRefs.value); console.log('itemRefs===', itemRefs.value);
console.log('itemsWidth===', itemsWidth.value); console.log('itemsWidth===', itemsWidth.value);
updateActivePosition(modelValue.value)
changeHandle(modelValue.value)
}) })
}) })
// 监听modelValue变化更新位置
watch(modelValue, (newVal) => {
updateActivePosition(newVal)
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -32,10 +32,10 @@ export const newMenus = [
intro: '协助商家拉来新客户,拓展客户群体', intro: '协助商家拉来新客户,拓展客户群体',
childrenList: [ childrenList: [
{ {
name: "分销", name: "全民股东",
icon: "zhcz", icon: "zhcz",
pathName: "distribution_page", pathName: "distribution_page",
intro: "用户成为业务员,可促进消费" intro: "用户成为股东,可促进消费"
}, },
{ {
name: "套餐推广", name: "套餐推广",
@@ -55,6 +55,12 @@ export const newMenus = [
pathName: "group_booking", pathName: "group_booking",
intro: "拼团" intro: "拼团"
}, },
{
name: "分享配置",
icon: "fxpz",
pathName: "share_setting",
intro: "商家可配置用户分享后可获得的奖励"
},
] ]
}, },
{ {

View File

@@ -128,7 +128,8 @@ function submitHandle() {
// 过滤条件 // 过滤条件
function isDistribtion(str) { function isDistribtion(str) {
let s = str.split('_') let s = str.split('_')
return s[1] let shopId = s[0]
return s[1] && shopId == localStorage.getItem('shopId') ? 1 : 0
} }
const selectUser = ref([]) const selectUser = ref([])

View File

@@ -0,0 +1,290 @@
<template>
<div class="form_content">
<div class="left">
<div class="new_preview">
<div class="header">{{ shopInfo.shopName }}</div>
<div class="content">
<div class="title">{{ form.title }}</div>
<div class="img_wrap">
<div class="img">
<el-image :src="form.groupUrl" style="width: 100%;height: 100%;border-radius: 4px;"></el-image>
</div>
</div>
<div class="intro">
{{ form.content }}
</div>
<div class="foot">
{{ defaultNote }}
</div>
</div>
</div>
</div>
<div class="form">
<el-form ref="formRef" :model="form" :rules="rules" label-position="right" label-width="120px">
<el-form-item label="群二维码" prop="groupUrl">
<single-image-upload style="width: 120px; height: 120px" v-model="form.groupUrl"></single-image-upload>
</el-form-item>
<el-form-item label="模块标题" prop="title">
<el-input :placeholder="defaultTitle" :maxlength="15" v-model.trim="form.title"
style="width: 300px;"></el-input>
</el-form-item>
<el-form-item label="模块内容">
<el-input type="textarea" :rows="4" :maxlength="20" show-word-limit placeholder="请输入内容"
v-model.trim="form.content" style="width: 300px;"></el-input>
</el-form-item>
<el-form-item label="模块提示语">
<div class="tips">{{ defaultNote }}</div>
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="form.isEnable" :active-value="1" :inactive-value="0"></el-switch>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="submitHandle">保存</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElNotification } from 'element-plus'
import { disGroupGet, disGroupPost } from '@/api/coupon/index'
const defaultTitle = ref('扫码进群,优惠多多')
const defaultNote = ref('如果长按不能识别,可截图或保存二维码图片至相册,通过微信扫码入群')
const defaultContent = ref('长按识别上方企微好友码,添加【福利官】')
const shopInfo = ref('')
const form = ref({
id: '',
groupUrl: '',
title: defaultTitle.value,
content: defaultContent.value,
isEnable: 1,
})
const rules = reactive({
groupUrl: [
{
required: true,
message: '请上传群二维码',
triiger: 'change'
}
],
title: [
{
required: true,
message: '请输入模块标题',
triiger: 'change'
}
],
content: [
{
required: true,
message: '请输入内容',
triiger: 'change'
}
]
})
// 开始提交
const formRef = ref(null)
const emits = defineEmits(["success"]);
const loading = ref(false);
function submitHandle() {
formRef.value.validate(async (valid) => {
try {
if (valid) {
loading.value = true;
let data = { ...form.value }
if (data.title == '') {
data.title = defaultTitle.value
}
if (data.content == '') {
data.content = defaultContent.value
}
await disGroupPost(data);
ElNotification({
title: '注意',
message: '保存成功',
type: 'success'
})
}
} catch (err) {
console.log(err);
}
loading.value = false;
});
}
// 获取配置信息
async function drainageConfigGetAjax() {
try {
const res = await disGroupGet()
form.value = res
if (form.value.title == '') {
form.value.title = defaultTitle.value
}
if (form.value.content == '') {
form.value.content = defaultContent.value
}
} catch (error) {
console.log(error);
}
}
onMounted(() => {
shopInfo.value = JSON.parse(localStorage.getItem('userInfo'))
drainageConfigGetAjax()
})
</script>
<style scoped lang="scss">
.form_content {
padding: 14px 0 0;
display: flex;
.left {
width: 374px;
height: 720px;
position: relative;
background: url('https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/3/7da616a1c56a409dbdea8bf2f41cf14b.png') no-repeat center center / 100% 100%;
&::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
}
.preview {
width: 350px;
height: 130px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border-radius: 6px;
display: flex;
align-items: center;
padding: 14px;
.info {
flex: 1;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 5px 0;
.top {
flex: 1;
.title {
font-size: 16px;
color: #333;
font-weight: bold;
}
.content {
font-size: 12px;
color: #666;
}
}
.btm {
font-size: 14px;
color: #999;
}
}
.img_wrap {
width: 100px;
height: 100px;
.img {
width: 100%;
height: 100%;
}
}
}
.new_preview {
--bg: #3F3B37;
--color: #F6DFC4;
--borderColor: #f6dfc45b;
width: 90%;
background-color: var(--bg);
border-radius: 4px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.header {
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: var(--color);
height: 50px;
border-bottom: 1px dashed var(--borderColor);
}
.content {
padding-bottom: 14px;
.title {
font-size: 14px;
color: var(--color);
height: 50px;
display: flex;
align-items: center;
justify-content: center;
}
.img_wrap {
display: flex;
justify-content: center;
.img {
--size: 220px;
width: var(--size);
height: var(--size);
}
}
.intro {
height: 40px;
font-size: 14px;
color: var(--color);
display: flex;
align-items: center;
justify-content: center;
padding: 0 14px;
}
.foot {
height: 40px;
color: var(--borderColor);
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 14px;
text-align: center;
}
}
}
}
}
</style>

View File

@@ -18,7 +18,7 @@
<el-form-item label="获得佣金条件" prop="inviteCount"> <el-form-item label="获得佣金条件" prop="inviteCount">
<div class="column"> <div class="column">
<el-input v-model="form.inviteCount" placeholder="请输入" :maxlength="8" style="width: 300px;" <el-input v-model="form.inviteCount" placeholder="请输入" :maxlength="8" style="width: 300px;"
@input="e => form.inviteCount = filterNumberInput(e, 1)"> @input="e => form.inviteCount = filterNumberInput(e, 0)">
<template #append></template> <template #append></template>
</el-input> </el-input>
<div class="tips">邀请达到指定人数才可赚取佣金</div> <div class="tips">邀请达到指定人数才可赚取佣金</div>
@@ -135,8 +135,8 @@
<span class="required">*</span>分成比例 <span class="required">*</span>分成比例
</div> </div>
<div class="ipt"> <div class="ipt">
<el-input v-model="item.levelOneCommission" placeholder="请输入" :maxlength="5" <el-input v-model="item.commission" placeholder="请输入" :maxlength="5" style="width: 200px;"
style="width: 200px;" @input="e => item.levelOneCommission = filterNumberInput(e)"> @input="e => item.commission = filterNumberInput(e)">
<template #append>%</template> <template #append>%</template>
</el-input> </el-input>
</div> </div>
@@ -202,15 +202,15 @@ const formRef = ref(null)
const formLoading = ref(false) const formLoading = ref(false)
const levelConfigListObj = ref({ const levelConfigListObj = ref({
name: '', // 名称 name: '', // 名称
inviteCount: '', // 有效人数 inviteCount: 0, // 有效人数
costAmount: '', // 消费金额 costAmount: '', // 消费金额
levelOneCommission: '', // 一级分销比例 commission: '', // 一级分销比例
levelTwoCommission: '', // 二级分销比例 levelTwoCommission: '', // 二级分销比例
}) })
const form = ref({ const form = ref({
id: '', id: '',
openType: 'pay', // pay购买开通 auto自动开通 manual手动开通 openType: 'pay', // pay购买开通 auto自动开通 manual手动开通
inviteCount: '', // 邀请条件人数 inviteCount: 0, // 邀请条件人数
inviteConsume: 0, // 被邀请人消费有效 0 1 inviteConsume: 0, // 被邀请人消费有效 0 1
payAmount: '', // 购买开通金额 payAmount: '', // 购买开通金额
rewardCount: '', // 每人奖励次数 rewardCount: '', // 每人奖励次数
@@ -281,7 +281,7 @@ const rules = ref({
callback(new Error(tips)) callback(new Error(tips))
return return
} }
if (item.levelOneCommission === '' || item.levelOneCommission == 0) { if (item.commission === '' || item.commission == 0) {
tips = `请输入${index + 1}级的分成比例` tips = `请输入${index + 1}级的分成比例`
callback(new Error(tips)) callback(new Error(tips))
return return

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="gyq_container"> <div class="gyq_container">
<div class="gyq_content"> <div class="gyq_content">
<HeaderCard name="分销" intro="用户成为业务员,可促进消费" icon="xffx" showSwitch v-model:isOpen="form.isEnable"> <HeaderCard name="全民股东" intro="用户成为股东,可促进消费" icon="xffx" showSwitch v-model:isOpen="form.isEnable">
</HeaderCard> </HeaderCard>
<div class="tips"> <div class="tips">
<el-alert title="请记得前往《分销明细》充值余额,余额不足时用户收益将无法正常入账" type="primary" show-icon :closable="false" /> <el-alert title="请记得前往《分销明细》充值余额,余额不足时用户收益将无法正常入账" type="primary" show-icon :closable="false" />
@@ -18,6 +18,8 @@
<activation_record key="activationRecord" v-if="tabActiveIndex == 2" /> <activation_record key="activationRecord" v-if="tabActiveIndex == 2" />
<!-- 分销明细 --> <!-- 分销明细 -->
<distribution_details key="distributionDetails" v-if="tabActiveIndex == 3" /> <distribution_details key="distributionDetails" v-if="tabActiveIndex == 3" />
<!-- 股东管理群 -->
<groupSetting name="groupSetting" key="groupSetting" v-if="tabActiveIndex == 4" />
</div> </div>
</div> </div>
</div> </div>
@@ -30,6 +32,7 @@ import setting from "./components/setting.vue";
import distributor from "./components/distributor.vue"; import distributor from "./components/distributor.vue";
import activation_record from "./components/activation_record.vue"; import activation_record from "./components/activation_record.vue";
import distribution_details from "./components/distribution_details.vue"; import distribution_details from "./components/distribution_details.vue";
import groupSetting from "./components/groupSetting.vue";
import { ref, onMounted, watch } from 'vue' import { ref, onMounted, watch } from 'vue'
import { distributionGet, distributionPut } from '@/api/coupon' import { distributionGet, distributionPut } from '@/api/coupon'
@@ -53,6 +56,10 @@ const tabList = ref([
label: '分销明细', label: '分销明细',
value: 3 value: 3
}, },
{
label: '股东管理群',
value: 3
},
]) ])
const form = ref({ const form = ref({

View File

@@ -0,0 +1,179 @@
<template>
<el-dialog :title="form.id ? '编辑' : '添加'" width="800px" v-model="visible" @closed="resetForm">
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px" label-position="left">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入轮播图名称" :maxlength="20" style="width: 300px;"></el-input>
</el-form-item>
<el-form-item label="轮播图片" prop="imageUrl">
<div class="column">
<div class="center">
<SingleImageUpload v-model="form.imageUrl" />
</div>
<div class="tips" style="margin-bottom: 8px;">建议尺寸750x300像素支持jpgpnggif格式大小不超过2MB</div>
</div>
</el-form-item>
<el-form-item label="是否可分享" prop="isShareable">
<div class="column">
<div>
<el-radio-group v-model="form.isShareable">
<el-radio label="开启" :value="1"></el-radio>
<el-radio label="关闭" :value="0"></el-radio>
</el-radio-group>
</div>
<div class="tips">开启时用户端会显示单独的分享按钮</div>
</div>
</el-form-item>
<el-form-item label="跳转页面" prop="jumpPageId">
<linkCard :includes-names="includesNames" v-model="form.jumpPageId" v-model:extendParam="form.extendParam" />
</el-form-item>
<el-form-item label="排序值" prop="sort">
<el-input v-model="form.sort" @input="e => form.sort = filterNumberInput(e, 1)" style="width: 300px;"
placeholder="排序值越大越靠前"></el-input>
</el-form-item>
<el-form-item label="是否启用">
<el-radio-group v-model="form.isEnabled">
<el-radio label="启用" :value="1"></el-radio>
<el-radio label="禁用" :value="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="submitForm" :loading="loading"> </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import _ from 'lodash';
import { ref } from 'vue';
import { filterNumberInput } from '@/utils';
import SingleImageUpload from '@/components/Upload/SingleImageUpload.vue';
import linkCard from './linkCard.vue';
import { shareCarouselPost } from '@/api/coupon/index.js';
const visible = ref(false);
const loading = ref(false);
const formObj = ref({
id: '', // 轮播图ID编辑时传递
name: '', // 轮播图名称20字内
imageUrl: '', // 轮播图片地址
isShareable: 1, // 是否可分享 1=开启 0=关闭
jumpPageId: '', // 跳转页面 tb_mini_app_pages的 id
extendParam: '', // 扩展参数
sort: '', // 排序值,数值越大越靠前
isEnabled: 1, // 是否启用 1=启用 0=禁用
});
const form = ref(_.cloneDeep(formObj.value));
function resetForm() {
form.value = _.cloneDeep(formObj.value);
}
const formRef = ref(null);
const rules = {
name: [
{ required: true, message: '请输入轮播图名称', trigger: 'blur' }
],
imageUrl: [
{ required: true, message: '请上传轮播图片', trigger: 'change' }
],
jumpPageId: [
{
validator: (rule, value, callback) => {
if (form.value.jumpPageId && _.includes(includesNames.value.map(item => item.id), form.value.jumpPageId)) {
if (!form.value.extendParam) {
callback(new Error('请选择具体菜品'));
} else {
callback();
}
} else {
callback();
}
},
trigger: 'change'
}
],
sort: [
{ required: true, message: '请输入排序值', trigger: 'blur' }
],
}
const includesNames = ref([
{
id: '4',
name: '套餐推广商品详情页'
},
{
id: '6',
name: '商品拼团详情页'
},
{
id: '10',
name: '点餐商品详情弹窗页'
},
{
id: '12',
name: '积分商品详情页面'
}
]);
function submitForm() {
console.log(form.value);
formRef.value.validate((valid) => {
if (valid) {
shareCarouselPostAjax();
}
});
}
const emits = defineEmits(['success']);
// 轮播图配置:新增/修改
async function shareCarouselPostAjax() {
try {
loading.value = true;
await shareCarouselPost(form.value);
ElNotification({
title: '成功',
message: '保存成功',
type: 'success',
duration: 2000
});
emits('success');
visible.value = false;
} catch (error) {
console.log(error);
} finally {
loading.value = false;
}
}
function show(obj) {
visible.value = true;
if (obj && obj.id) {
form.value = _.cloneDeep(obj)
}
}
defineExpose({
show
})
</script>
<style scoped lang="scss">
.center {
display: flex;
}
.column {
display: flex;
flex-direction: column;
}
.tips {
font-size: 14px;
color: #999;
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<div class="row">
<el-form :model="queryForm" inline>
<el-form-item>
<el-button type="primary" icon="Plus" @click="AddBannerRef.show()">添加</el-button>
</el-form-item>
<el-form-item>
<el-select v-model="queryForm.isShareable" style="width: 150px;">
<el-option label="全部" :value="''" />
<el-option label="开启" :value="1" />
<el-option label="关闭" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="queryForm.isEnabled" style="width: 150px;">
<el-option label="全部" :value="''" />
<el-option label="启用" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-input v-model="queryForm.name" placeholder="请输入名称搜索" style="width: 200px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" :loading="tableData.loading" @click="queryHandle">搜索</el-button>
<el-button icon="Refresh" :loading="tableData.loading" @click="resetHandle">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="row">
<el-table :data="tableData.list" border stripe v-loading="tableData.loading">
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="名称" prop="name"></el-table-column>
<el-table-column label="图片" prop="imageUrl">
<template v-slot="scope">
<el-image :src="scope.row.imageUrl" style="width: 50px;height: 50px;"></el-image>
</template>
</el-table-column>
<!-- <el-table-column label="分享" prop="isShareable">
<template v-slot="scope">
<span v-if="scope.row.isShareable == 1">开启</span>
<span v-else>关闭</span>
</template>
</el-table-column> -->
<el-table-column label="启用状态" prop="isEnabled">
<template v-slot="scope">
<el-tag v-if="scope.row.isEnabled == 1" type="primary" disable-transitions>已启用</el-tag>
<el-tag v-if="scope.row.isEnabled == 0" type="info" disable-transitions>已禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="排序值" prop="sort"></el-table-column>
<el-table-column label="创建时间" prop="createTime" width="200"></el-table-column>
<el-table-column label="操作" width="150">
<template v-slot="scope">
<el-button link type="primary" @click="AddBannerRef.show(scope.row)">编辑</el-button>
<el-popconfirm title="确认要删除吗?" @confirm="deleteHandle(scope.row)">
<template #reference>
<el-button type="danger" link>删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</div>
<AddBanner @success="queryHandle" ref="AddBannerRef" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import AddBanner from './add_banner.vue';
import { shareCarouselGet, shareCarouselDel } from '@/api/coupon/index.js';
const AddBannerRef = ref(null);
const queryForm = ref({
name: '',
isShareable: '', // 是否可分享 1=开启 0=关闭
isEnabled: '', // 是否启用 1=启用 0=禁用
})
const tableData = ref({
loading: false,
list: []
})
// 搜索
function queryHandle() {
getTableData();
}
// 重置
function resetHandle() {
queryForm.value = {
name: '',
isShareable: '',
isEnabled: '',
};
queryHandle();
}
// 轮播图配置
async function getTableData() {
try {
tableData.value.loading = true;
const res = await shareCarouselGet(queryForm.value);
tableData.value.list = res;
} catch (error) {
console.log(error);
}
tableData.value.loading = false;
}
// 删除轮播图配置
async function deleteHandle(row) {
try {
await shareCarouselDel(row.id);
getTableData();
} catch (error) {
console.log(error);
}
}
onMounted(() => {
getTableData();
})
</script>
<style scoped lang="scss">
.row {
&.mt14 {
margin-top: 14px;
}
}
</style>

View File

@@ -0,0 +1,213 @@
<template>
<div>
<el-select v-model="jumpPageId" style="width: 300px;" placeholder="请选择跳转页面" @change="jumpPageIdChange">
<el-option v-for="item in miniPageList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
<div class="tips">选填轮播图点击后跳转的目标页面</div>
<el-select v-model="goodsId" style="width: 300px;" placeholder="请选择具体菜品,可搜索" clearable filterable remote
:remote-method="remoteMethod" :loading="loading" v-if="jumpPageId && isIncludeFlag" @change="goodsIdChange">
<el-option v-for="item in goodsList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</div>
</template>
<script setup>
import _ from 'lodash';
import { ref, onMounted, watch } from 'vue';
import { pointsGoodsPage } from '@/api/points/index.js'
import API from '@/views/application/list/advertisement/indexconfig/api';
import { packageGet, getGbWarePage } from '@/api/market/ware'
import productApi from '@/api/product';
const props = defineProps({
includesNames: {
type: Array,
default: () => ([])
}
});
const jumpPageId = defineModel({
type: [String, Number],
default: ''
});
const jumpPageName = ref('');
const isIncludeFlag = ref(false);
function checkIsInclude() {
// 增加边界判断避免includesNames为空或item.id不存在导致的错误
if (!jumpPageId.value || !Array.isArray(props.includesNames) || props.includesNames.length === 0) {
return false;
}
// 提取id数组并判断包含关系
const idList = props.includesNames.map(item => item?.id); // 可选链避免item.id不存在
return _.includes(idList, jumpPageId.value);
}
watch(jumpPageId, async (newVal, oldVal) => {
if (newVal) {
await nextTick()
watchJumpPageIdChange(newVal)
}
}, {
immediate: true, // 可选:首次加载时立即执行一次(根据需求决定)
deep: false // 无需深度监听因为值是String/Number
})
const miniPageList = ref([]);
// 获取小程序跳转页面列表
async function getList() {
try {
const res = await API.miniAppPagesPage({ page: 1, size: 999 });
miniPageList.value = res.records;
} catch (error) {
console.log(error);
}
}
function jumpPageIdChange(value) {
goodsId.value = '';
extendParam.value = ''
jumpPageName.value = miniPageList.value.find(item => item.id == value)?.name || '';
// 清空具体菜品选择
if (checkIsInclude()) {
isIncludeFlag.value = true;
getGoodsList()
} else {
isIncludeFlag.value = false;
}
}
// watch监听jumpPageId变化
async function watchJumpPageIdChange(value) {
if (miniPageList.value.length === 0) {
await getList()
}
// 清空具体菜品选择
if (checkIsInclude()) {
jumpPageName.value = miniPageList.value.find(item => item.id == value)?.name || '';
isIncludeFlag.value = true;
getGoodsList()
} else {
isIncludeFlag.value = false;
}
}
const loading = ref(false);
const goodsList = ref([]); // 具体菜品列表
const goodsId = defineModel('goodsId', {
type: [String, Number],
default: ''
});
const extendParam = defineModel('extendParam', {
type: String,
default: ''
});
// 监听extendParam的变化解析并赋值给goodsId
watch(extendParam, async (newExtendParam, oldExtendParam) => {
// console.log('extendParam变化', newExtendParam);
// 1. 空值判断如果extendParam为空重置goodsId
if (!newExtendParam) {
goodsId.value = '';
return;
}
// 2. 解析字符串匹配goodsId=xxx的格式
// 正则表达式说明匹配goodsId=后接任意字符(非空),直到字符串结束或遇到&等分隔符
const reg = /goodsId=([^&]+)/;
const matchResult = newExtendParam.match(reg);
// 3. 提取值并赋值:匹配成功则赋值,否则重置
if (matchResult && matchResult[1]) {
let extractedGoodsId = matchResult[1];
// 尝试将 id 转为数字(如果看起来像数字)以保持与后端 id 类型一致
const numeric = Number(extractedGoodsId);
const parsedId = !Number.isNaN(numeric) ? numeric : extractedGoodsId;
// 确保 options 已加载,这样 el-select 能根据 value 匹配到对应的 label
try {
await getGoodsList();
} catch (e) {
// ignore
}
// 统一使用字符串类型的 id 以保证与 options 中的 value 匹配
goodsId.value = String(parsedId);
} else {
goodsId.value = ''; // 匹配失败时清空
}
}, {
immediate: true, // 初始加载时立即执行一次解析
deep: false // 字符串是基本类型,无需深度监听
});
// 具体菜品选择变化
function goodsIdChange(value) {
// 设置扩展参数,处理清空情况以避免出现 'undefined'
if (value === null || typeof value === 'undefined' || value === '') {
extendParam.value = '';
goodsId.value = '';
return;
}
extendParam.value = `goodsId=${value}`;
}
function remoteMethod(value) {
getGoodsList(value);
}
// 获取商品咧白哦
async function getGoodsList(value = '') {
try {
// 获取套餐列表
loading.value = true;
let list = [];
if (jumpPageName.value == '套餐推广商品详情页') {
const res = await packageGet({ page: 1, size: 999, packageName: value });
list = res.records.map(item => ({
id: String(item.id),
name: item.packageName
}));
} else if (jumpPageName.value == '商品拼团详情页') {
const res = await getGbWarePage({ page: 1, size: 999, wareName: value });
list = res.records.map(item => ({
id: String(item.id),
name: item.wareName
}));
} else if (jumpPageName.value == '点餐商品详情弹窗页') {
const res = await productApi.getPage({ name: value });
list = res.records.map(item => ({
id: String(item.id),
name: item.name
}));
} else if (jumpPageName.value == '积分商品详情页面') {
const res = await pointsGoodsPage({ page: 1, size: 999 });
list = res.records.map(item => ({
id: String(item.id),
name: item.goodsName
}));
}
goodsList.value = list;
loading.value = false;
} catch (error) {
console.log(error);
loading.value = false;
}
}
onMounted(() => {
getList()
})
</script>
<style scoped lang="scss">
.tips {
font-size: 14px;
color: #999;
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-width="160px" label-position="left">
<el-form-item label="功能开启">
<el-switch v-model="form.isEnabled" :active-value="1" :inactive-value="0" />
</el-form-item>
<el-form-item label="可获得奖励的分享" prop="rewardSharePages">
<el-checkbox-group v-model="form.rewardSharePages">
<el-checkbox :label="item.label" :value="item.value" v-for="item in typesList" :key="item.value"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<div class="header">分享人可获得奖励</div>
<el-form-item label="选择优惠券" prop="sharerCouponId">
<SelectCoupon v-model="form.sharerCouponId" v-model:name="form.sharerCouponName" />
</el-form-item>
<el-form-item label="单次获得优惠券数量" prop="sharerCouponNum">
<el-input v-model="form.sharerCouponNum" placeholder="请输入数量"
@input="e => form.sharerCouponNum = filterNumberInput(e, 1)" style="width: 300px;" />
</el-form-item>
<el-form-item label="可获得奖励次数">
<el-radio-group v-model="form.rewardTimesType">
<el-radio label="仅1次" :value="1"></el-radio>
<el-radio label="每次分享成功" :value="2"></el-radio>
</el-radio-group>
</el-form-item>
<div class="header">被分享人可获得奖励</div>
<el-form-item label="选择优惠券">
<SelectCoupon v-model="form.sharedUserCouponId" v-model:name="form.sharedUserCouponName" />
</el-form-item>
<el-form-item label="单次获得优惠券数量">
<el-input v-model="form.sharedUserCouponNum" placeholder="请输入数量"
@input="e => form.sharedUserCouponNum = filterNumberInput(e, 1)" style="width: 300px;" />
</el-form-item>
<el-form-item label="被分享人弹窗">
<el-radio-group v-model="form.isSharedUserPopup">
<el-radio label="是" :value="1"></el-radio>
<el-radio label="否" :value="0"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm">保存</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import _ from 'lodash'
import { ref, onMounted } from 'vue'
import typesList from '../data.js'
import SelectCoupon from '@/components/selectCoupon/index.vue'
import { filterNumberInput } from '@/utils/index.js'
import { shareBasePost, shareBaseGet } from '@/api/coupon/index.js'
const formRef = ref(null)
const form = ref({
isEnabled: 0,
rewardSharePages: [], // 可获得奖励的分享页面,用逗号分隔 typesList
sharerCouponId: '', // 分享人可获得的优惠券ID
sharerCouponName: '', // 分享人可获得的优惠券名称
sharerCouponNum: '', // 分享人可获得的优惠券数量
rewardTimesType: 1, // 可获得奖励次数 1=仅1次 2=每次分享成功
sharedUserCouponId: '', // 被分享人可获得的优惠券ID
sharedUserCouponName: '', // 被分享人可获得的优惠券名称
sharedUserCouponNum: '', // 被分享人可获得的优惠券数量
isSharedUserPopup: 1, // 被分享人获得奖励时是否弹窗 1=是 0=否
})
const rules = {
rewardSharePages: [
{ required: true, message: '请选择可获得奖励的分享', trigger: 'change' }
],
sharerCouponId: [
{ required: true, message: '请选择优惠券', trigger: 'change' }
],
sharerCouponNum: [
{ required: true, message: '请输入单次获得优惠券数量', trigger: 'blur' }
]
}
function submitForm() {
console.log(form.value);
formRef.value.validate((valid) => {
if (valid) {
console.log('提交表单', form.value)
shareBasePostAjax()
} else {
console.log('表单验证失败')
return false
}
})
}
// 分享奖励基础:新增/修改
async function shareBasePostAjax() {
try {
const data = _.cloneDeep(form.value);
data.rewardSharePages = data.rewardSharePages.join(',');
await shareBasePost(data);
ElNotification({
title: '成功',
message: '保存成功',
type: 'success',
duration: 2000
});
} catch (error) {
console.log(error);
}
}
// 分享奖励基础 查询
async function shareBaseGetAjax() {
try {
const res = await shareBaseGet();
if (res) {
form.value = { ...form.value, ...res };
form.value.rewardSharePages = res.rewardSharePages ? res.rewardSharePages.split(',') : [];
}
} catch (error) {
console.log(error);
}
}
onMounted(() => {
// 初始化数据
shareBaseGetAjax();
});
</script>
<style scoped lang="scss">
.header {
color: #333;
font-size: 16px;
font-weight: 600;
margin: 0 0 20px 0;
}
</style>

View File

@@ -0,0 +1,38 @@
export default [
{
label: '店铺首页',
value: 'index'
},
// {
// label: '我的',
// value: 'dine'
// },
{
label: '点餐页',
value: 'eat'
},
{
label: '点餐页-详情',
value: 'eat-detail'
},
{
label: '套餐推广-列表',
value: 'pp-list'
},
{
label: '套餐推广-详情',
value: 'pp-detail'
},
{
label: '商品拼团-列表',
value: 'gb-list'
},
{
label: '商品拼团-详情',
value: 'gb-detail'
},
{
label: '全民股东',
value: 'dis'
}
]

View File

@@ -0,0 +1,56 @@
<template>
<div class="gyq_container">
<div class="gyq_content">
<HeaderCard name="分享配置" intro="商家可配置用户分享后可获得的奖励" icon="fxpz">
</HeaderCard>
<div class="row mt14">
<tabHeader v-model="tabActiveIndex" :list="tabList" />
</div>
<div class="row mt14">
<!-- 基础设置 -->
<setting key="setting" name="setting" v-if="tabActiveIndex == 0" />
<!-- 点餐页轮播图 -->
<bannerSetting key="bannerSetting" name="bannerSetting" v-if="tabActiveIndex == 1" />
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import HeaderCard from "../components/headerCard.vue";
import tabHeader from "../components/tabHeader.vue";
import setting from "./components/setting.vue";
import bannerSetting from "./components/banner_setting.vue";
const tabActiveIndex = ref(0)
const tabList = ref([
{
label: '基础设置',
value: 0
},
{
label: '点餐页轮播图',
value: 1
},
])
</script>
<style scoped lang="scss">
.gyq_container {
padding: 14px;
.gyq_content {
padding: 14px;
background-color: #fff;
border-radius: 8px;
}
}
.row {
&.mt14 {
margin-top: 14px;
}
}
</style>

View File

@@ -0,0 +1,163 @@
<template>
<el-dialog :title="forms.id ? '编辑分组' : '添加分组'" width="600px" v-model="visible" @close="onClose">
<el-form :model="forms" label-width="120px" ref="elFormref" :rules="rules">
<el-form-item label="分组名称" prop="name">
<el-input v-model="forms.name" />
</el-form-item>
<el-form-item label="选择商品">
<el-button type="primary" icon="Plus" @click="addgoods">添加商品</el-button>
</el-form-item>
<el-form-item label="" v-if="selectData.length">
<!-- 选责商品列表 -->
<selectGoodslist @deleteItememit="deleteItem($event)" :list="selectData"></selectGoodslist>
</el-form-item>
<el-form-item label="分组状态">
<el-radio-group v-model="forms.status">
<el-radio :value="1" label="启用" />
<el-radio :value="0" label="禁用" />
</el-radio-group>
</el-form-item>
<el-form-item label="售卖时间管控">
<el-radio-group v-model="forms.useTime">
<el-radio :value="1" label="启用" />
<el-radio :value="0" label="禁用" />
</el-radio-group>
</el-form-item>
<el-form-item label="时间选择" v-if="forms.useTime == 1">
<el-time-picker value-format="HH:mm:ss" v-model="forms.time" is-range range-separator="到"
start-placeholder="开始时间" end-placeholder="结束时间" />
</el-form-item>
<el-form-item label="排列方式">
<el-radio-group v-model="forms.sortMode">
<el-radio value="0" label="默认" />
<el-radio value="1" label="价格由高到低" />
<el-radio value="2" label="价格由低到高" />
<el-radio value="3" label="销量由高到低" />
<el-radio value="4" label="销量由低到高" />
</el-radio-group>
</el-form-item>
<el-form-item label="分组排序">
<el-input-number v-model="forms.sort" controls-position="right" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="visible = false"> </el-button>
<el-button type="primary" @click="addConfirm" :loading="confirmLoading"> </el-button>
</div>
</template>
</el-dialog>
<!-- 新增添加商品 -->
<GoodsSelect ref="GoodsSelectRef" @success="e => selectData = e" />
</template>
<script setup>
import _ from 'lodash'
import { ref } from 'vue'
import selectGoodslist from '../goodsGroupconfig/selectGoodslist.vue';
import UserAPI from "@/api/onlineShop/goodsGroupconfig";
import GoodsSelect from '@/components/GoodsSelect/index.vue'
const visible = ref(false)
const GoodsSelectRef = ref(null)
const elFormref = ref(null);
let selectData = ref([]);
const formsObj = {
name: "",
status: 1,
useTime: 0,
sortMode: "0",
sort: 1,
time: "",
saleEndTime: '',
saleStartTime: '',
productIds: '',
productList: ''
}
let forms = ref(_.cloneDeep(formsObj));
const rules = ref({
name: [{ required: true, message: "请输入分组名称", trigger: "blur" }],
});
// 重置表单
function onClose() {
forms.value = _.cloneDeep(formsObj)
elFormref.value.resetFields()
selectData.value = []
}
// 添加商品
async function addgoods() {
GoodsSelectRef.value.show(selectData.value);
}
// 删除商品
function deleteItem(data) {
selectData.value = data;
}
const emit = defineEmits(['success'])
const confirmLoading = ref(false)
// 添加编辑商品分组
function addConfirm() {
elFormref.value.validate(async (valid, fields) => {
try {
if (valid) {
let obj = {
...forms.value,
productList: selectData.value,
};
// 商品选择的合集
if (selectData.value.length) {
let arr = [];
selectData.value.forEach((item, index) => {
arr.push(item.id);
});
obj.productIds = arr;
}
console.log(obj, "商品分组的参数");
if (obj.time && obj.time.length) {
obj.saleStartTime = obj.time[0];
obj.saleEndTime = obj.time[1];
}
if (forms.value.id) {
if (obj.useTime == 0) {
obj.saleStartTime = null;
obj.saleEndTime = null;
}
let res = await UserAPI.update(obj);
if (res.code == 200) {
ElMessage.success("编辑成功");
}
} else {
let res = await UserAPI.addunit(obj);
if (res.code == 200) {
ElMessage.success("添加成功");
}
}
visible.value = false
emit('success')
} else {
console.log("error submit!", fields);
}
} catch (error) {
console.log(error);
}
});
}
function open(obj) {
visible.value = true
if (obj && obj.id) {
forms.value = _.cloneDeep(obj)
selectData.value = obj.productList || []
forms.value.time = [obj.saleStartTime, obj.saleEndTime]
}
}
defineExpose({
open
})
</script>

View File

@@ -5,17 +5,9 @@
<!-- 搜索 --> <!-- 搜索 -->
<!-- 列表 --> <!-- 列表 -->
<page-content <page-content ref="contentRef" :content-config="contentConfig" @add-click="handleAddClick"
ref="contentRef" @edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick"
:content-config="contentConfig" @toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange">
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<template #status="scope"> <template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'"> <el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }} {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
@@ -29,11 +21,7 @@
</template> </template>
<template #mobile="scope"> <template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text> <el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button <copy-button v-if="scope.row[scope.prop]" :text="scope.row[scope.prop]" style="margin-left: 2px" />
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
</template> </template>
</page-content> </page-content>
@@ -62,11 +50,7 @@
</page-modal> --> </page-modal> -->
<!-- 编辑 --> <!-- 编辑 -->
<page-modal <page-modal ref="editModalRef" :modal-config="editModalConfig" @submit-click="handleSubmitClick">
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
>
<template #gender="scope"> <template #gender="scope">
<Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" /> <Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" />
</template> </template>
@@ -79,33 +63,20 @@
添加商品 添加商品
</el-button> </el-button>
<!-- 选责商品列表 --> <!-- 选责商品列表 -->
<selectGoodslist <selectGoodslist @deleteItememit="deleteItem($event)" :list="selectData"></selectGoodslist>
@deleteItememit="deleteItem($event)"
:list="selectData"
></selectGoodslist>
</div> </div>
</template> </template>
<template #addmanagementtime="scope"> <template #addmanagementtime="scope">
<template v-if="scope.formData.useTime == 1"> <template v-if="scope.formData.useTime == 1">
{{ scope.formData }} {{ scope.formData }}
<el-time-picker <el-time-picker value-format="HH:mm:ss" v-model="scope.formData.saleTime" is-range range-separator="到"
value-format="HH:mm:ss" start-placeholder="开始时间" end-placeholder="结束时间" />
v-model="scope.formData.saleTime"
is-range
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
/>
</template> </template>
</template> </template>
</page-modal> </page-modal>
</template> </template>
<template v-else> <template v-else>
<page-content <page-content ref="contentRef" :content-config="contentConfig2" @operat-click="handleOperatClick">
ref="contentRef"
:content-config="contentConfig2"
@operat-click="handleOperatClick"
>
<template #status="scope"> <template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'"> <el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }} {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
@@ -113,32 +84,17 @@
</template> </template>
</page-content> </page-content>
</template> </template>
<!-- 添加/编辑分组 -->
<addGoodsGroup ref="addGoodsGroupRef" @success="handleQueryClick" />
<!-- 新增添加商品 --> <!-- 新增添加商品 -->
<myDialog <myDialog title="选择商品" width="50%" ref="myDialogRef" @Confirm="subitgood" @close="resetSelectData">
title="选择商品" <page-search ref="searchRefs" :search-config="searchConfig2" @query-click="searchs"
width="50%" @reset-click="handleResetClick" />
ref="myDialogRef" <page-content ref="contentRefs" v-if="switchref" :content-config="contentConfig2" @add-click="handleAddClick"
@Confirm="subitgood" @edit-click="handleEditClick" @export-click="handleExportClick" @search-click="handleSearchClick"
@close="resetSelectData" @toolbar-click="handleToolbarClick" @operat-click="handleOperatClick" @filter-change="handleFilterChange">
>
<page-search
ref="searchRefs"
:search-config="searchConfig2"
@query-click="searchs"
@reset-click="handleResetClick"
/>
<page-content
ref="contentRefs"
v-if="switchref"
:content-config="contentConfig2"
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<template #status="scope"> <template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'"> <el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }} {{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
@@ -152,21 +108,17 @@
</template> </template>
<template #mobile="scope"> <template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text> <el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button <copy-button v-if="scope.row[scope.prop]" :text="scope.row[scope.prop]" style="margin-left: 2px" />
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
</template> </template>
</page-content> </page-content>
<!-- <el-table :data="selectData" border style="width: 100%"> <el-table :data="selectData" border style="width: 100%">
<el-table-column prop="date" align="center" label="Date" /> <el-table-column prop="date" align="center" label="Date" />
<el-table-column prop="address" align="center" label="Address" /> <el-table-column prop="address" align="center" label="Address" />
</el-table> --> </el-table>
</myDialog> </myDialog>
<!-- 新增 --> <!-- 新增 -->
<myDialog :title="title" width="30%" ref="myDialogRefAdd" @Confirm="addConfirm()"> <!-- <myDialog :title="title" width="50%" ref="myDialogRefAdd" @Confirm="addConfirm()">
<el-form :model="forms" label-width="120px" ref="elFormref" :rules="rules"> <el-form :model="forms" label-width="120px" ref="elFormref" :rules="rules">
<el-form-item label="分组名称" prop="name"> <el-form-item label="分组名称" prop="name">
<el-input v-model="forms.name" /> <el-input v-model="forms.name" />
@@ -175,11 +127,7 @@
<el-button type="primary" icon="Plus" @click="addgoods">添加商品</el-button> <el-button type="primary" icon="Plus" @click="addgoods">添加商品</el-button>
</el-form-item> </el-form-item>
<el-form-item label="" v-if="selectData.length"> <el-form-item label="" v-if="selectData.length">
<!-- 选责商品列表 --> <selectGoodslist @deleteItememit="deleteItem($event)" :list="selectData"></selectGoodslist>
<selectGoodslist
@deleteItememit="deleteItem($event)"
:list="selectData"
></selectGoodslist>
</el-form-item> </el-form-item>
<el-form-item label="分组状态"> <el-form-item label="分组状态">
<el-radio-group v-model="forms.status"> <el-radio-group v-model="forms.status">
@@ -194,14 +142,8 @@
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<el-form-item label="时间选择" v-if="forms.useTime == 1"> <el-form-item label="时间选择" v-if="forms.useTime == 1">
<el-time-picker <el-time-picker value-format="HH:mm:ss" v-model="forms.time" is-range range-separator="到"
value-format="HH:mm:ss" start-placeholder="开始时间" end-placeholder="结束时间" />
v-model="forms.time"
is-range
range-separator=""
start-placeholder="开始时间"
end-placeholder="结束时间"
/>
</el-form-item> </el-form-item>
<el-form-item label="排列方式"> <el-form-item label="排列方式">
<el-radio-group v-model="forms.sortMode"> <el-radio-group v-model="forms.sortMode">
@@ -216,7 +158,7 @@
<el-input-number v-model="forms.sort" controls-position="right" /> <el-input-number v-model="forms.sort" controls-position="right" />
</el-form-item> </el-form-item>
</el-form> </el-form>
</myDialog> </myDialog> -->
</div> </div>
</template> </template>
@@ -235,6 +177,7 @@ import searchConfig2 from "./goodsGroupconfig/search2";
import myDialog from "@/components/mycomponents/myDialog.vue"; import myDialog from "@/components/mycomponents/myDialog.vue";
import selectGoodslist from "./goodsGroupconfig/selectGoodslist.vue"; import selectGoodslist from "./goodsGroupconfig/selectGoodslist.vue";
import { isSyncStatus } from "@/utils/index"; import { isSyncStatus } from "@/utils/index";
import addGoodsGroup from "./components/addGoodsGroup.vue";
const { const {
searchRef, searchRef,
@@ -268,10 +211,13 @@ if (isSyncStatus()) {
let switchref = ref(false); let switchref = ref(false);
const addGoodsGroupRef = ref(null)
// 新增 // 新增
async function handleAddClick() { async function handleAddClick() {
addModalRef.value?.setModalVisible(); addGoodsGroupRef.value.open()
selectData.value = []; // addModalRef.value?.setModalVisible();
// selectData.value = [];
// 加载上级规格下拉数据源 // 加载上级规格下拉数据源
// addModalConfig.formItems[2]!.attrs!.data = await UserAPI.getPage({ name: "" }); // addModalConfig.formItems[2]!.attrs!.data = await UserAPI.getPage({ name: "" });
// 加载角色下拉数据源 // 加载角色下拉数据源
@@ -377,22 +323,22 @@ function deleteItem(data: any) {
// 编辑 // 编辑
async function handleEditClick(row: IObject) { async function handleEditClick(row: IObject) {
editModalRef.value?.handleDisabled(false); // editModalRef.value?.handleDisabled(false);
title.value = "编辑分组"; // title.value = "编辑分组";
// 加载部门下拉数据源 // 加载部门下拉数据源
// editModalConfig.formItems[2]!.attrs!.data = await UserAPI.getPage({ name: "" }); // editModalConfig.formItems[2]!.attrs!.data = await UserAPI.getPage({ name: "" });
// editModalConfig.formItems[2]!.attrs!.data = await DeptAPI.getOptions(); // editModalConfig.formItems[2]!.attrs!.data = await DeptAPI.getOptions();
// 加载角色下拉数据源 // 加载角色下拉数据源
// editModalConfig.formItems[4]!.options = await RoleAPI.getOptions(); // editModalConfig.formItems[4]!.options = await RoleAPI.getOptions();
// 根据id获取数据进行填充 // 根据id获取数据进行填充
const data = await UserAPI.getunitinfo(row.id); // const data = await UserAPI.getunitinfo(row.id);
let obj = { ...data }; // let obj = { ...data };
selectData.value = data.productList; // selectData.value = data.productList;
obj.time = [obj.saleStartTime, obj.saleEndTime]; // obj.time = [obj.saleStartTime, obj.saleEndTime];
for (let key in obj) { // for (let key in obj) {
forms[key] = obj[key]; // forms[key] = obj[key];
} // }
myDialogRefAdd.value.open(); addGoodsGroupRef.value.open(row)
// editModalRef.value?.setFormData(obj); // editModalRef.value?.setFormData(obj);
// editModalRef.value?.setModalVisible(); // editModalRef.value?.setModalVisible();
} }
@@ -400,19 +346,20 @@ async function handleEditClick(row: IObject) {
function handleToolbarClick(name: string) { function handleToolbarClick(name: string) {
console.log(name); console.log(name);
if (name === "custom1") { if (name === "custom1") {
forms = reactive({ // forms = reactive({
name: "", // name: "",
status: 1, // status: 1,
useTime: 0, // useTime: 0,
sortMode: "0", // sortMode: "0",
sort: 1, // sort: 1,
time: "", // time: "",
}); // });
selectData.value = []; // selectData.value = [];
title.value = "新增分组"; // title.value = "新增分组";
// 新增 // // 新增
myDialogRefAdd.value.open(); // myDialogRefAdd.value.open();
addGoodsGroupRef.value.open()
} }
} }
// 其他操作列 // 其他操作列

View File

@@ -9,13 +9,20 @@
<div class="form"> <div class="form">
<div class="preview_wrap"> <div class="preview_wrap">
<div class="phone_wrap"> <div class="phone_wrap">
<!-- 首页背景 -->
<div class="index_bg" v-if="tableActive == 'index_bg'"> <div class="index_bg" v-if="tableActive == 'index_bg'">
<img class="bg" v-if="!isJsonArrayString(selectItem.value)" :src="selectItem.value" alt=""> <!-- 多图轮播 -->
<el-carousel height="500px" v-else> <el-carousel height="500px"
<el-carousel-item v-for="item in JSON.parse(selectItem.value)"> v-if="isJsonArrayString(selectItem.value) && JSON.parse(selectItem.value).length > 0">
<el-carousel-item v-for="(item, idx) in JSON.parse(selectItem.value)" :key="idx">
<img class="bg" :src="item" /> <img class="bg" :src="item" />
</el-carousel-item> </el-carousel-item>
</el-carousel> </el-carousel>
<!-- 单图渲染 -->
<img class="bg" v-else-if="selectItem.value" :src="selectItem.value" alt="" />
<!-- 空状态提示 -->
<div class="empty-tip" v-else>暂无图片请上传</div>
<div class="menu_wrap"> <div class="menu_wrap">
<div class="menu_wrap_div"> <div class="menu_wrap_div">
<div class="left"> <div class="left">
@@ -46,8 +53,11 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 个人中心背景 -->
<div class="my_bg" v-if="tableActive == 'my_bg'"> <div class="my_bg" v-if="tableActive == 'my_bg'">
<img class="bg" :src="selectItem.value" /> <img class="bg" :src="selectItem.value" v-if="selectItem.value" />
<div class="empty-tip my-empty-tip" v-else>暂无图片请上传</div>
<div class="content"> <div class="content">
<div class="item" style="display: flex"> <div class="item" style="display: flex">
<div class="left"> <div class="left">
@@ -108,10 +118,13 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 会员背景 -->
<div class="member_bg" v-if="tableActive == 'member_bg'"> <div class="member_bg" v-if="tableActive == 'member_bg'">
<div class="card_wrap"> <div class="card_wrap">
<div class="card"> <div class="card">
<img class="bg" :src="selectItem.value" /> <img class="bg" :src="selectItem.value" v-if="selectItem.value" />
<div class="empty-tip member-empty-tip" v-else>暂无图片请上传</div>
<div class="content"> <div class="content">
<div class="ewm"> <div class="ewm">
<i class="icon el-icon-menu"></i> <i class="icon el-icon-menu"></i>
@@ -155,18 +168,29 @@
</div> </div>
</div> </div>
</div> </div>
<!-- 商品列表背景 -->
<div class="shopinfo_bg" v-if="tableActive == 'shopinfo_bg'"> <div class="shopinfo_bg" v-if="tableActive == 'shopinfo_bg'">
<img class="bg" :src="selectItem.value" v-if="!isJsonArrayString(selectItem.value)" /> <!-- 多图轮播 -->
<el-carousel height="120px" v-else> <el-carousel height="120px"
<el-carousel-item v-for="item in JSON.parse(selectItem.value)"> v-if="isJsonArrayString(selectItem.value) && JSON.parse(selectItem.value).length > 0">
<el-carousel-item v-for="(item, idx) in JSON.parse(selectItem.value)" :key="idx">
<img class="bg" :src="item" /> <img class="bg" :src="item" />
</el-carousel-item> </el-carousel-item>
</el-carousel> </el-carousel>
<!-- 单图渲染 -->
<img class="bg" v-else-if="selectItem.value" :src="selectItem.value" alt="" />
<!-- 空状态提示 -->
<div class="empty-tip shopinfo-empty-tip" v-else>暂无图片请上传</div>
<div class="shop_name">{{ shopName }}</div> <div class="shop_name">{{ shopName }}</div>
<img class="content" src="@/assets/images/shop_editor_bg.png" alt="" /> <img class="content" src="@/assets/images/shop_editor_bg.png" alt="" />
</div> </div>
<!-- 小票logo -->
<div class="ticket_wrap" v-if="tableActive == 'ticket_logo'"> <div class="ticket_wrap" v-if="tableActive == 'ticket_logo'">
<img class="logo" :src="selectItem.value" /> <img class="logo" :src="selectItem.value" v-if="selectItem.value" />
<div class="empty-tip ticket-empty-tip" v-else>暂无图片请上传</div>
<img class="ewm" src="@/assets/images/1024.png" /> <img class="ewm" src="@/assets/images/1024.png" />
<div class="row"> <div class="row">
<span class="num">123</span> <span class="num">123</span>
@@ -185,42 +209,48 @@
</div> </div>
</div> </div>
</div> </div>
<div class="editor_wrap"> <div class="editor_wrap">
<div class="header" style="padding-bottom: 20px"> <div class="header" style="padding-bottom: 20px">
<div class="t1"> <div class="t1">{{ selectItem.name }}</div>
{{ selectItem.name }}
</div>
<div class="t2">点击图片更换</div> <div class="t2">点击图片更换</div>
</div> </div>
<div class="form_item"> <div class="form_item">
<div class="upload_wrap" style="display: flex;flex-direction: column;gap: 28px;" <!-- 多图上传 -->
<div class="upload_wrap" style="display: flex; flex-direction: column; gap: 28px"
v-if="selectItem.autoKey == 'index_bg' || selectItem.autoKey == 'shopinfo_bg'"> v-if="selectItem.autoKey == 'index_bg' || selectItem.autoKey == 'shopinfo_bg'">
<MultiImageUpload v-model="imgList" @uploadStart="uploading = true" @uploadAllSuccess="MultiOnSuccess" /> <MultiImageUpload v-model="imgList" @uploadStart="uploading = true" @uploadAllSuccess="MultiOnSuccess" />
<div>
<el-button type="primary" size="large" @click="doSubmit" :loading="uploading"
loading-text="图片上传中...">确认修改</el-button>
</div> </div>
</div> <!-- 单图上传 -->
<div v-else> <div v-else style="display: flex;">
<SingleImageUpload v-model="selectItem.value" @onSuccess="onSuccess"> <SingleImageUpload v-model="selectItem.value" @onSuccess="onSuccess">
<img v-if="selectItem.value" :src="selectItem.value" class="avatar" /> <img v-if="selectItem.value" :src="selectItem.value" class="avatar" />
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <i v-else class="el-icon-plus avatar-uploader-icon"></i>
</SingleImageUpload> </SingleImageUpload>
</div> </div>
<!-- <el-upload <div class="model_wapra">
:headers="headers" <!-- 个人中心模板图 -->
class="avatar-uploader" <template v-if="selectItem.autoKey == 'my_bg'">
:action="qiNiuUploadApi" <div class="title">个人中心模板图</div>
:show-file-list="false" <div class="user-bg-list" v-if="userBgList.length > 0">
:on-success="handleSuccess" <img v-for="(url, index) in userBgList" :key="index" :src="url" @click="selectUserBg(url)"
style="width: 200px; height: 200px" class="user-bg-item" />
> </div>
<img v-if="selectItem.value" :src="selectItem.value" class="avatar" /> </template>
<i v-else class="el-icon-plus avatar-uploader-icon"></i> <!-- VIP背景模板图 -->
</el-upload> --> <template v-else-if="selectItem.autoKey == 'member_bg'">
<!-- <div class="title" style="padding-left: 20px;">跳转路径</div> <div class="title">会员卡背景模板图</div>
<el-input style="width: 300px;margin-left: 20px;" /> <div class="user-bg-list" v-if="vipBgList.length > 0">
<el-button type="primary" style="margin-left: 20px;">修改</el-button> --> <img v-for="(url, index) in vipBgList" :key="index" :src="url" @click="selectVipBg(url)"
class="user-bg-item" />
</div>
</template>
</div>
<div>
<el-button type="primary" size="large" @click="doSubmit" :loading="uploading" loading-text="图片上传中...">
确认修改
</el-button>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -230,99 +260,165 @@
<script> <script>
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import shopExtendApi from "@/api/account/shopExtend"; import shopExtendApi from "@/api/account/shopExtend";
// 确保SvgIcon组件已正确引入
import SvgIcon from "@/components/SvgIcon/index.vue";
export default { export default {
components: {
SvgIcon,
},
data() { data() {
return { return {
tableActive: "ticket_logo", tableActive: "index_bg",
tableData: [], tableData: [],
selectItem: {}, selectItem: {},
imageUrl: "", imageUrl: "",
imgList: [], imgList: [],
shopName: '', shopName: "",
uploading: false uploading: false,
userBgList: [
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/5c8673150d8e449ba035e3f65866b4ed.png',
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/c442af63d29443c687b24d2a3abbf02b.png',
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/1da89980fa0f4c7abffbcdacc2c3a059.png',
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/16296e40f2634d0da0cf1ff377b2b848.png',
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/f442c0d005b94664ac4b37f7fa19731b.png',
],
vipBgList: [
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/c2add99b9b21464889bc14823b97c40e.png',
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/c0e65f576a1a485da7fac34658b87c24.png',
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/629b8c12b8904140bf84df3a57b3c021.png',
'https://cashier-oss.oss-cn-beijing.aliyuncs.com/upload/2/cd06e01ad7e848359cdca718f5a06231.png'
]
}; };
}, },
mounted() { mounted() {
this.getList(); this.getList();
}, },
methods: { methods: {
// 刷新列表数据 // 选择会员背景模板图
selectVipBg(url) {
this.selectItem.value = url;
// this.doSubmit();
},
// 选择个人中心背景模板图
selectUserBg(url) {
this.selectItem.value = url;
// this.doSubmit();
},
// 提交修改
async doSubmit() { async doSubmit() {
// console.log('this.selectItem.value', this.selectItem.value); // 多图类型强制同步imgList到selectItem.value
// return if (["index_bg", "shopinfo_bg"].includes(this.selectItem.autoKey)) {
this.selectItem.value =
this.imgList.length > 0 ? JSON.stringify(this.imgList) : "";
}
// this.selectItem.value = JSON.stringify(this.imgList) let data = {
await shopExtendApi.edit({
...this.selectItem, ...this.selectItem,
autokey: this.selectItem.autoKey, autokey: this.selectItem.autoKey,
};
if (data.value === "") {
ElMessage({
message: "请上传图片",
type: "warning",
}); });
return;
}
try {
await shopExtendApi.edit(data);
ElMessage({ ElMessage({
message: "编辑成功", message: "编辑成功",
type: "success", type: "success", // 修复提示类型错误
}); });
this.getList(); this.getList();
} catch (error) {
ElMessage({
message: "编辑失败:" + (error.message || "未知错误"),
type: "error",
});
}
}, },
// 单图上传成功 // 单图上传成功
onSuccess(response) { onSuccess(response) {
this.doSubmit(); this.doSubmit();
}, },
// 多图上传成功 // 多图上传成功
async MultiOnSuccess(response) { async MultiOnSuccess(response) {
console.log(response); this.uploading = false;
// console.log(this.imgList); // 确保imgList有值再序列化
// console.log(this.imgList); this.selectItem.value =
// await nextTick() this.imgList.length > 0 ? JSON.stringify(this.imgList) : "";
this.uploading = false // 强制触发视图更新
this.selectItem.value = JSON.stringify(this.imgList) this.$forceUpdate();
console.log('onSuccess.selectItem.value', this.selectItem.value); ElMessage({
message: "图片上传成功",
type: "success",
});
}, },
/** /**
* 判断字符串是否为合法的 JSON 数组 * 判断字符串是否为合法的 JSON 数组
* @param {string} str - 待判断的字符串 * @param {string} str - 待判断的字符串
* @returns {boolean} true=是JSON数组字符串 / false=普通字符串/其他 * @returns {boolean} true=是JSON数组字符串 / false=普通字符串/其他
*/ */
isJsonArrayString(str) { isJsonArrayString(str) {
// 1. 非字符串直接返回 false if (typeof str !== "string" || str.trim() === "") {
if (typeof str !== 'string') {
return false; return false;
} }
// 2. 空字符串返回 false根据业务可调整
if (str.trim() === '') {
return false;
}
try { try {
// 3. 尝试解析 JSON
const parsed = JSON.parse(str); const parsed = JSON.parse(str);
// 4. 校验解析结果是否为数组
return Array.isArray(parsed); return Array.isArray(parsed);
} catch (e) { } catch (e) {
// 解析失败(普通字符串/非法 JSON→ 返回 false
return false; return false;
} }
}, },
// 切换类型 // 切换类型
selectItemChange(key) { selectItemChange(key) {
this.tableActive = key; this.tableActive = key;
const { autoKey, id, name, value } = this.tableData.find((item) => item.autoKey == key); const targetItem = this.tableData.find((item) => item.autoKey === key) || {};
this.selectItem = { autoKey, id, name, value }; // 深拷贝避免引用问题
if (this.isJsonArrayString(value)) { this.selectItem = { ...targetItem };
this.imgList = JSON.parse(value)
// 多图类型特殊处理
if (["index_bg", "shopinfo_bg"].includes(key)) {
if (this.isJsonArrayString(targetItem.value)) {
this.imgList = JSON.parse(targetItem.value);
} else { } else {
this.imgList = [] // 非JSON数组时若有单图值则转为数组否则置空
this.imgList = targetItem.value ? [targetItem.value] : [];
}
// 同步更新selectItem.value为JSON数组
this.selectItem.value = JSON.stringify(this.imgList);
} else {
this.imgList = [];
// 单图类型保留原有值
this.selectItem.value = targetItem.value || "";
} }
console.log(this.selectItem);
}, },
// 获取装修数据 // 获取装修数据
async getList() { async getList() {
try { try {
let res = await shopExtendApi.get({}); const res = await shopExtendApi.get({});
this.tableData = res; this.tableData = res || [];
this.tableActive = !this.tableActive ? res[0].autoKey : this.tableActive; // 确保有数据时再切换
if (this.tableData.length > 0) {
this.tableActive = this.tableActive || this.tableData[0].autoKey;
this.selectItemChange(this.tableActive); this.selectItemChange(this.tableActive);
}
// 补充店铺名称(根据实际接口返回调整)
this.shopName = "我的店铺";
} catch (error) { } catch (error) {
console.log(error); console.error("获取装修数据失败:", error);
ElMessage({
message: "获取数据失败,请刷新重试",
type: "error",
});
} }
}, },
}, },
@@ -336,7 +432,7 @@ export default {
gap: 30px; gap: 30px;
.btn { .btn {
width: 100px; padding: 0 20px;
height: 40px; height: 40px;
border: 1px solid $color; border: 1px solid $color;
border-radius: 4px; border-radius: 4px;
@@ -366,6 +462,64 @@ export default {
background-color: #fff; background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
// 空状态通用样式
.empty-tip {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
background-color: #f5f5f5;
}
// 首页空状态
.index_bg .empty-tip {
height: 500px;
}
// 商品列表空状态
.shopinfo_bg .empty-tip {
height: 120px;
}
// 个人中心空状态
.my-empty-tip {
height: 180px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
background-color: #f5f5f5;
}
// 会员背景空状态
.member-empty-tip {
height: 144px;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
background-color: #f5f5f5;
}
// 小票logo空状态
.ticket-empty-tip {
height: 80px;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 12px;
background-color: #f5f5f5;
border-radius: 4px;
}
.index_bg { .index_bg {
padding-bottom: 50px; padding-bottom: 50px;
@@ -653,35 +807,36 @@ export default {
} }
.info_wrap { .info_wrap {
padding: 20px 10px; padding: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px;
.avatar { .avatar {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
background-color: #efefef; background-color: #f5f5f5;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
.icon { .icon {
color: #555; font-size: 20px;
font-size: 24px; color: #999;
} }
} }
.info { .info {
flex: 1;
.t1 { .t1 {
font-size: 14px; font-size: 14px;
color: #333;
} }
.t2 { .t2 {
font-size: 10px; font-size: 12px;
color: #999; color: #999;
margin-top: 4px;
} }
} }
} }
@@ -696,52 +851,55 @@ export default {
} }
.shop_name { .shop_name {
padding: 10px 10px 0 10px; padding: 10px;
font-size: 16px;
color: #333;
font-weight: 500;
} }
.content { .content {
width: 100%; width: 100%;
height: auto; height: auto;
display: block;
} }
} }
.ticket_wrap { .ticket_wrap {
padding: 15px; padding: 10px;
position: relative;
.logo {
width: 80px;
height: 80px;
object-fit: contain;
margin-bottom: 10px;
border-radius: 4px;
}
.ewm { .ewm {
width: 80px; width: 80px;
height: 80px; height: 80px;
position: absolute; object-fit: contain;
top: 10px; margin-bottom: 10px;
right: 15px;
}
.logo {
width: 90px;
height: 30px;
object-fit: cover;
} }
.row { .row {
margin-top: 5px;
display: flex;
align-items: center;
.sku {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
margin-bottom: 4px;
.num {
font-size: 14px;
color: #333;
font-weight: 500;
margin-right: 10px;
}
.sku {
color: #999;
} }
.b { .b {
font-weight: bold; font-weight: 500;
} color: #333;
.num {
font-size: 18px;
font-weight: bold;
margin-right: 10px;
} }
} }
} }
@@ -749,28 +907,63 @@ export default {
} }
.editor_wrap { .editor_wrap {
padding-left: 20px; flex: 1;
padding-left: 40px;
.header {
.t2 {
color: #999;
font-size: 12px;
}
}
.form_item { .form_item {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 30px;
.avatar { .model_wapra {
display: block; .title {
width: 200px; font-size: 16px;
height: 200px; color: #333;
object-fit: cover; margin-bottom: 10px;
}
} }
.title { .user-bg-list {
flex-shrink: 0; display: flex;
gap: 10px;
.user-bg-item {
width: 120px;
height: 80px;
object-fit: cover;
border-radius: 4px;
cursor: pointer;
border: 2px solid transparent;
&:hover {
border-color: #40a9ff;
}
}
}
.avatar-uploader {
width: 200px;
height: 200px;
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
.avatar-uploader-icon {
font-size: 28px;
color: #8c8c8c;
width: 178px;
height: 178px;
line-height: 178px;
text-align: center;
}
.avatar {
width: 100%;
height: 100%;
display: block;
}
} }
} }
} }

View File

@@ -0,0 +1,99 @@
<template>
<div class="goods_info">
<div v-for="(item, index) in showList" :key="item.id || index" class="row">
<el-image :src="item.productImg" class="cover" lazy />
<div class="info">
<div class="name">
<span :class="[item.isVip == 1 ? 'colorStyle' : '']">
{{ item.productName }}
</span>
<span v-if="item.refundNum" class="refund">(退 - {{ item.refundNum }})</span>
<span v-else-if="item.returnNum" class="refund">(退 - {{ item.returnNum }})</span>
</div>
<div class="sku">{{ item.skuName }}</div>
</div>
</div>
<div v-if="needShowMore" class="show-more" @click="showAll = !showAll">
{{ showAll ? "收起" : "查看全部" }}
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from "vue";
// 接收商品列表
const props = defineProps<{
goods: any[];
}>();
// 组件内部独立状态,自己控制,不影响外部
const showAll = ref(false);
// 计算需要展示的列表
const showList = computed(() => {
const list = props.goods || [];
return showAll.value ? list : list.slice(0, 2);
});
// 是否需要显示查看更多
const needShowMore = computed(() => {
return (props.goods || []).length > 2;
});
</script>
<style scoped lang="scss">
.goods_info {
.row {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
&:not(:first-child) {
margin-top: 10px;
}
.cover {
width: 40px;
height: 40px;
object-fit: cover;
}
.info {
flex: 1;
display: flex;
flex-direction: column;
margin-top: 2px;
text-align: center;
.name {
font-size: 12px;
}
.sku {
color: #999;
font-size: 12px;
}
}
}
}
.show-more {
color: #1989fa;
font-size: 12px;
margin-top: 6px;
cursor: pointer;
text-align: center;
}
.colorStyle {
color: #ffc315;
}
.refund {
color: #ff9731;
font-weight: bold;
}
</style>

View File

@@ -81,9 +81,9 @@
<div>优惠券抵扣{{ detail.fullCouponDiscountAmount }}</div> <div>优惠券抵扣{{ detail.fullCouponDiscountAmount }}</div>
</div> </div>
<div class="row"> <div class="row">
<div>满减活动{{ detail.discountActAmount || '-' }}</div> <div>满减活动{{ detail.discountActAmount || "-" }}</div>
<div>新客立减{{ detail.newCustomerDiscountAmount || '-' }}</div> <div>新客立减{{ detail.newCustomerDiscountAmount || "-" }}</div>
<div>会员折扣{{ detail.vipDiscountAmount || '-' }}</div> <div>会员折扣{{ detail.vipDiscountAmount || "-" }}</div>
</div> </div>
<div class="row"> <div class="row">
<div>退单金额{{ detail.refundAmount }}</div> <div>退单金额{{ detail.refundAmount }}</div>
@@ -111,7 +111,11 @@
退款详情> 退款详情>
</span> --> </span> -->
</div> </div>
<div class="color-red">退款方式{{ detail.refundType }}</div> <div class="color-red">
<span v-if="detail.refundType">
退款方式{{ detail.refundType === "cash" ? "现金退款" : "原路退回" }}
</span>
</div>
<div></div> <div></div>
<div></div> <div></div>
</div> </div>
@@ -143,7 +147,9 @@
</span> </span>
<span style="color: #999">{{ scope.row.productSkuName }}</span> <span style="color: #999">{{ scope.row.productSkuName }}</span>
<div> <div>
<el-tag v-if="scope.row.isTimeDiscount" type="danger" disable-transitions>限时折扣</el-tag> <el-tag v-if="scope.row.isTimeDiscount" type="danger" disable-transitions>
限时折扣
</el-tag>
</div> </div>
</div> </div>
</div> </div>
@@ -330,8 +336,8 @@ export default {
if (!payType) { if (!payType) {
return ""; return "";
} }
console.log(payType.replace("_pay", "")); console.log(payType);
return returnOptionsLabel("payType", payType.replace("_pay", "")); return returnOptionsLabel("payType", payType);
}, },
to2(n) { to2(n) {
return Number(n).toFixed(2); return Number(n).toFixed(2);
@@ -345,7 +351,8 @@ export default {
}, },
sendTypeFilter(t) { sendTypeFilter(t) {
if (t) { if (t) {
return orderEnum.sendType.find((item) => item.key == t).label; const item = orderEnum.sendType.find((item) => item.key == t);
return item ? item.label : "-";
} else { } else {
return t; return t;
} }
@@ -400,6 +407,12 @@ export default {
}, },
tuikuan(item) { tuikuan(item) {
if (!item) { if (!item) {
let arrs = []
for (let i in this.detail.detailMap) {
this.detail.detailMap[i].map((v) => {
arrs.push(v);
});
}
this.$refs.refReturnMoney.open([], this.detail); this.$refs.refReturnMoney.open([], this.detail);
return; return;
} }
@@ -418,6 +431,9 @@ export default {
if (arr.length == 0) { if (arr.length == 0) {
return ElMessage.error("请选择要退款的商品和数量"); return ElMessage.error("请选择要退款的商品和数量");
} }
console.log('tuikuan===', arr);
this.$refs.refReturnMoney.open(arr, this.detail); this.$refs.refReturnMoney.open(arr, this.detail);
}, },
tuicai(item) { tuicai(item) {
@@ -436,7 +452,7 @@ export default {
if (arr.length == 0) { if (arr.length == 0) {
return ElMessage.error("请选择要退菜的商品和数量"); return ElMessage.error("请选择要退菜的商品和数量");
} }
console.log(arr); console.log('tuicai===', arr);
this.$refs.refReturnCart.open(arr, this.detail); this.$refs.refReturnCart.open(arr, this.detail);
}, },

View File

@@ -4,11 +4,7 @@
<div class="u-flex u-col-top" v-if="goodsList && goodsList.length"> <div class="u-flex u-col-top" v-if="goodsList && goodsList.length">
<span class="u-m-0">退款商品</span> <span class="u-m-0">退款商品</span>
<div class="u-p-l-20 goods-list"> <div class="u-p-l-20 goods-list">
<div <div class="u-flex u-font-12 goods-list-item" v-for="(goods, index) in goodsList" :key="index">
class="u-flex u-font-12 goods-list-item"
v-for="(goods, index) in goodsList"
:key="index"
>
<span class=""> <span class="">
{{ goods.productName }} {{ goods.productName }}
</span> </span>
@@ -35,12 +31,7 @@
<div class="flex u-row-between"> <div class="flex u-row-between">
<span class="color-red">退款金额</span> <span class="color-red">退款金额</span>
<div class="u-flex u-flex-1 u-p-l-20"> <div class="u-flex u-flex-1 u-p-l-20">
<el-input-number <el-input-number type="number" v-model="number" :min="min" :max="canReturnMoney"></el-input-number>
type="number"
v-model="number"
:min="min"
:max="canReturnMoney"
></el-input-number>
<span class="u-m-l-10">可退{{ canReturnMoney }}</span> <span class="u-m-l-10">可退{{ canReturnMoney }}</span>
</div> </div>
</div> </div>
@@ -73,13 +64,8 @@
</div> </div>
<div class="u-flex u-flex-wrap tags"> <div class="u-flex u-flex-wrap tags">
<div <div class="tag" v-for="(tag, index) in tags" @click="changeSel(tag)" :key="index"
class="tag" :class="{ active: tag.checked }">
v-for="(tag, index) in tags"
@click="changeSel(tag)"
:key="index"
:class="{ active: tag.checked }"
>
{{ tag.label }} {{ tag.label }}
</div> </div>
</div> </div>
@@ -94,16 +80,22 @@
</template> </template>
</el-dialog> </el-dialog>
<safe-password ref="refPassword" @confirm="pwdConfirm"></safe-password> <safe-password ref="refPassword" @confirm="pwdConfirm"></safe-password>
<!-- 退款退菜推库存的操作弹窗 -->
<refundConsModal ref="refundConsModalRef" :list="refundList" @success="refundConsModalSuccess" />
</div> </div>
</template> </template>
<script> <script>
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import safePassword from "./password.vue"; import safePassword from "./password.vue";
import { useUserStore } from "@/store/modules/user"; import { useUserStore } from "@/store/modules/user";
import { useCartsStore } from "@/store/modules/carts";
import categoryApi from "@/api/product/productclassification";
import refundConsModal from "@/components/refundConsModal.vue";
const shopUser = useUserStore(); const shopUser = useUserStore();
export default { export default {
components: { components: {
safePassword, safePassword,
refundConsModal
}, },
props: { props: {
modal: { modal: {
@@ -135,6 +127,8 @@ export default {
goods: { goods: {
productId: -999, productId: -999,
}, },
refundList: [],
refundStock: ''
}; };
}, },
computed: { computed: {
@@ -196,18 +190,26 @@ export default {
.join(","); .join(",");
const note = selTag + (this.note.length > 0 ? "," + this.note : ""); const note = selTag + (this.note.length > 0 ? "," + this.note : "");
console.log(note); console.log(note);
this.$emit("confirm", {
const data = {
refundAmount: this.number, refundAmount: this.number,
cash: this.cash, cash: this.cash,
refundReason: note, refundReason: note,
refundDetails: this.goodsList.map((v) => { refundDetails: this.goodsList.map((v) => {
return { id: v.id, num: v.num }; return { id: v.id, num: v.selNumber };
}), }),
...e, ...e,
}); refundStock: this.refundStock
}
this.$emit("confirm", data);
this.close(); this.close();
}, },
confirm() { // 退款推库存的操作
refundConsModalSuccess(e) {
this.refundStock = e
this.emitTuikuan()
},
async confirm() {
const selTag = this.tags const selTag = this.tags
.filter((item) => item.checked) .filter((item) => item.checked)
.map((item) => item.label) .map((item) => item.label)
@@ -220,14 +222,58 @@ export default {
this.$refs.refPassword.open(); this.$refs.refPassword.open();
return; return;
} }
// 在这里给订单的商品补全库存信息 start
const carts = useCartsStore();
let categorys = JSON.parse(localStorage.getItem('categorys'))
let shopInfo = JSON.parse(localStorage.getItem('userInfo'))
if (!categorys) {
categorys = await categoryApi.getList({})
}
this.goodsList.forEach(item => {
carts.goods.forEach(val => {
if (item.productId == val.id) {
if (shopInfo.refundMode == 1) {
// 跟随分类退款模式
categorys.forEach(v => {
if (val.categoryId == v.id) {
item.refundMode = v.refundMode
}
})
} else {
// 跟随商品退款模式及
item.refundMode = val.refundMode
}
}
})
})
console.log('this.goodsList===', this.goodsList);
// 在这里给订单的商品补全库存信息 end
this.goodsList.forEach(item => {
if (item.refundMode == 3) {
this.refundList.push({
name: item.productName,
num: item.selNumber
})
}
})
if (this.refundList.length > 0) {
this.$refs.refundConsModalRef.show()
return
}
this.emitTuikuan(); this.emitTuikuan();
}, },
}, },
mounted() {}, mounted() { },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
:deep(.el-tag) { :deep(.el-tag) {
margin-top: 10px; margin-top: 10px;
margin-right: 10px; margin-right: 10px;
@@ -237,6 +283,7 @@ export default {
line-height: 35px; line-height: 35px;
height: 35px; height: 35px;
} }
.tags { .tags {
.tag { .tag {
margin: 10px 10px 0 0; margin: 10px 10px 0 0;
@@ -246,6 +293,7 @@ export default {
font-size: 14px; font-size: 14px;
color: #000; color: #000;
cursor: pointer; cursor: pointer;
&.active { &.active {
color: #1890ff; color: #1890ff;
background: #e8f4ff; background: #e8f4ff;
@@ -253,14 +301,17 @@ export default {
} }
} }
} }
:deep(.number-box .el-input__inner::-webkit-inner-spin-button) { :deep(.number-box .el-input__inner::-webkit-inner-spin-button) {
-webkit-appearance: none; -webkit-appearance: none;
margin: 0; margin: 0;
} }
:deep(.number-box .el-input__inner::-webkit-outer-spin-button) { :deep(.number-box .el-input__inner::-webkit-outer-spin-button) {
-webkit-appearance: none; -webkit-appearance: none;
margin: 0; margin: 0;
} }
.goods-list-item { .goods-list-item {
height: 24px; height: 24px;
line-height: 24px; line-height: 24px;
@@ -268,6 +319,7 @@ export default {
border-radius: 3px; border-radius: 3px;
padding: 0 10px; padding: 0 10px;
} }
.goods-list-item:not(:first-child) { .goods-list-item:not(:first-child) {
margin-top: 6px; margin-top: 6px;
} }

View File

@@ -2,10 +2,10 @@ import type { statusType } from "@/api/order/order";
export const statusOptions: statusOptions[] = [ export const statusOptions: statusOptions[] = [
{ label: "全部", value: "" }, { label: "全部", value: "" },
{ label: "待支付", value: "unpaid" }, { label: "待支付", value: "unpaid" },
{ label: "制作中", value: "in-production" }, // { label: "制作中", value: "in-production" },
{ label: "待取餐", value: "wait-out" }, // { label: "待取餐", value: "wait-out" },
{ label: "订单完成", value: "done" }, { label: "订单完成", value: "done" },
{ label: "申请退单", value: "refunding" }, // { label: "申请退单", value: "refunding" },
{ label: "退单", value: "refund" }, { label: "退单", value: "refund" },
{ label: "部分退单", value: "part_refund" }, { label: "部分退单", value: "part_refund" },
{ label: "取消订单", value: "cancelled" }, { label: "取消订单", value: "cancelled" },
@@ -107,6 +107,14 @@ export function returnOptionsLabel(optionsType: optionsType, value: string | num
const option = options.find((item) => item.value === value); const option = options.find((item) => item.value === value);
return option ? option.label : ""; return option ? option.label : "";
} }
export function returnPayTypeOptionsLabel(optionsType: optionsType, value: string | number) {
const options = returnOptions(optionsType).filter(v => v.value);
if (!options) {
return "";
}
const option = options.find((item) => item.value === value);
return option ? option.label : "";
}
export interface options { export interface options {

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