This commit is contained in:
duan
2025-02-18 18:47:37 +08:00
56 changed files with 20317 additions and 380 deletions

View File

@@ -13,7 +13,7 @@ VITE_APP_API_URL=https://tapi.cashier.sxczgkj.cn/ # 正式
# VITE_APP_API_URL=http://localhost:8989 # 本地
# WebSocket 端点(不配置则关闭),线上 ws://api.youlai.tech/ws ,本地 ws://localhost:8989/ws
VITE_APP_WS_ENDPOINT=
VITE_APP_WS_ENDPOINT=wss://sockets.sxczgkj.com/wss
# 启用 Mock 服务
VITE_MOCK_DEV_SERVER=false

View File

@@ -9,6 +9,8 @@
订单,支付相关:<https://tapi.cashier.sxczgkj.cn/order/>
商品,耗材相关:<https://tapi.cashier.sxczgkj.cn/product/>
系统相关:<https://tapi.cashier.sxczgkj.cn/system/>
购物车websocket
<https://apifox.com/apidoc/shared-2c47d665-b476-409c-9cb5-0e68a69b0292>
## 项目特色

View File

@@ -52,6 +52,7 @@
"path-to-regexp": "^8.2.0",
"pinia": "^2.3.1",
"qs": "^6.14.0",
"sockjs-client": "^1.6.1",
"sortablejs": "^1.15.6",
"vue": "^3.5.13",
"vue-i18n": "^11.1.0",

View File

@@ -0,0 +1,85 @@
import request from "@/utils/request";
import { Account_BaseUrl } from "@/api/config";
const baseURL = Account_BaseUrl + "/admin/activate";
const API = {
getList() {
return request({
url: `${baseURL}`,
method: "get",
});
},
edit(data: editRequest) {
return request({
url: `${baseURL}`,
method: "put",
data: data,
});
},
add(data: addRequest) {
return request({
url: `${baseURL}`,
method: "post",
data: data,
});
}
}
export default API;
export interface addRequest {
/**
* 充值金额
*/
amount?: number;
/**
* 优惠卷id
*/
couponId?: number;
/**
* 赠送金额
*/
giftAmount?: number;
/**
* 赠送积分
*/
giftPoints?: number;
/**
* 是否赠送优惠卷 0否 1是
*/
isGiftCoupon?: number;
/**
* 优惠卷数量
*/
num?: number;
[property: string]: any;
}
export interface editRequest {
/**
* 充值金额
*/
amount: number;
/**
* 优惠卷id
*/
couponId?: number;
/**
* 赠送金额
*/
giftAmount?: number;
/**
* 赠送积分
*/
giftPoints?: number;
id: number;
/**
* 是否赠送优惠卷 0否 1是
*/
isGiftCoupon?: number;
/**
* 优惠卷数量
*/
num?: number;
shopId?: number;
[property: string]: any;
}

View File

@@ -21,7 +21,7 @@ const MenuApi = {
return request<any, MenuVO>({
url: `${baseURL}/detail`,
method: "get",
data: { id }
params: { id }
});
},
add(data: addRequest) {
@@ -195,7 +195,7 @@ export interface editRequest {
/**
* 上级菜单,不传递则为顶级菜单
*/
pid?: number;
pid: number | string | null;
title: string;
[property: string]: any;
}

View File

@@ -0,0 +1,22 @@
import request from "@/utils/request";
import { Account_BaseUrl } from "@/api/config";
const baseURL = Account_BaseUrl + "/admin";
const ShopStaffApi = {
// 获取店铺权限列表
getshopPermission() {
return request<any>({
url: `${baseURL}/shopPermission`,
method: "get",
});
},
// 获取员工对应的权限id
getPermission(id: number | string) {
return request<any>({
url: `${baseURL}/shopStaff/permission`,
method: "get",
params: { id }
});
},
};
export default ShopStaffApi;

View File

@@ -17,20 +17,29 @@ const RoleApi = {
data,
});
},
update(data: editRequest) {
return request<any, editResponse>({
update(id: number | string | null, data: editRequest) {
const requestData = { ...data, id };
return request<any>({
url: `${baseURL}`,
method: "put",
data,
data: requestData,
});
},
delete(data: delRequest) {
return request<any, delResponse>({
url: `${baseURL}`,
method: "put",
method: "delete",
data,
});
},
// 获取角色对应的菜单id
getMenu(id: number) {
return request<any>({
url: `${baseURL}/menu`,
method: "get",
params: { id },
});
},
};
export default RoleApi;
@@ -226,3 +235,4 @@ export interface delResponse {
[property: string]: any;
}
/** delete end */

View File

@@ -0,0 +1,55 @@
import request from "@/utils/request";
import { Account_BaseUrl } from "@/api/config";
const baseURL = Account_BaseUrl + "/admin/shopMerchant";
const API = {
get(shopId: string | number) {
return request({
url: `${baseURL}`,
method: "get",
headers: {
shopId: shopId
}
});
},
edit(shopId: string | number, data: shopMerchantType) {
return request({
url: `${baseURL}`,
method: "put",
data: data,
headers: {
shopId: shopId
}
});
}
}
export default API;
/**
* ShopMerchantEditDTO
*/
export interface shopMerchantType {
/**
* 支付宝appid
*/
alipaySmallAppid: null | string;
/**
* 商户应用id
*/
appId: null | string;
/**
* 商户秘钥
*/
appSecret: null | string;
/**
* 支付密码
*/
payPassword: null | string;
/**
* 支付系统商户id
*/
storeId: null | string;
/**
* 微信appid
*/
wechatSmallAppid: null | string;
[property: string]: any;
}

160
src/api/account/shopUser.ts Normal file
View File

@@ -0,0 +1,160 @@
import request from "@/utils/request";
import { Account_BaseUrl } from "@/api/config";
const baseURL = Account_BaseUrl + "/admin/shopUser";
const API = {
// 获取店铺用户概述信息
getSummary(data: getSummaryRequest) {
return request({
url: `${baseURL}/summary`,
method: "get",
params: data
});
},
getList(data: getListRequest) {
return request({
url: `${baseURL}`,
method: "get",
params: data
});
},
edit(shopId: string | number, data: editRequest) {
return request({
url: `${baseURL}`,
method: "put",
data: data,
headers: {
shopId: shopId
}
});
},
// 店铺用户余额修改
editMoney(shopId: string | number, data: editMoneyRequest) {
return request({
url: `${baseURL}/money`,
method: "put",
data: data,
headers: {
shopId: shopId
}
});
},
add(shopId: string | number, data: addRequest) {
return request({
url: `${baseURL}`,
method: "post",
data: data,
headers: {
shopId: shopId
}
});
}
}
export default API;
export interface getSummaryRequest {
/**
* 0 非vip 1 vip
*/
isVip?: number;
[property: string]: any;
}
export interface getListRequest {
/**
* 0 非vip 1 vip
*/
isVip?: number;
/**
* 昵称或手机号
*/
key?: string;
[property: string]: any;
}
/**
* ShopUserEditDTO
*/
export interface editRequest {
/**
* 生日
*/
birthDay?: null | string;
/**
* 对应shopUserid
*/
id: number | null;
/**
* 昵称
*/
nickName?: null | string;
/**
* 性别 0女 1男
*/
sex?: number | null;
[property: string]: any;
}
/**
* ShopUserMoneyEditDTO
*/
export interface editMoneyRequest {
/**
* 对应shopUserid
*/
id: number | null;
/**
* 浮动金额
*/
money: number | null;
/**
* 备注
*/
remark?: null | string;
/**
* 0减少 1增加
*/
type: number | null;
[property: string]: any;
}
/**
* ShopUserAddDTO
*/
export interface addRequest {
/**
* 账户积分
*/
accountPoints?: number | null;
/**
* 钱包余额
*/
amount?: number | null;
/**
* 会员生日
*/
birthDay?: null | string;
/**
* 用户头像
*/
headImg?: null | string;
/**
* 是否会员,
*/
isVip?: number | null;
/**
* 用户昵称
*/
nickName: null | string;
/**
* 电话号码
*/
phone: null | string;
/**
* 0-女 1男
*/
sex?: number | null;
/**
* 用户Id
*/
userId: number | null;
[property: string]: any;
}

128
src/api/account/table.ts Normal file
View File

@@ -0,0 +1,128 @@
import request from "@/utils/request";
import { Account_BaseUrl } from "@/api/config";
const baseURL = Account_BaseUrl + "/admin/shopTable";
const API = {
// 批量生成桌码
fasetAdd(num: number) {
return request({
url: `${baseURL}/code`,
method: "post",
data: { num },
});
},
getList(data: getListRequest) {
return request({
url: `${baseURL}`,
method: "get",
params: data
});
},
edit(data: editRequest) {
return request({
url: `${baseURL}`,
method: "put",
data: data,
});
},
add(data: addRequest) {
return request({
url: `${baseURL}`,
method: "post",
data: data,
});
},
delete(id: number | string) {
return request({
url: `${baseURL}`,
method: "post",
data: { id },
});
}
}
export default API;
/**
* ShopTableAddDTO
*/
export interface addRequest {
/**
* 区域id
*/
areaId?: number | null;
/**
* 是否自动清台
*/
autoClear?: number | null;
/**
* 客座数
*/
capacity: number | null;
/**
* 结束数字
*/
end: number | null;
/**
* 台桌前缀
*/
sign: null | string;
/**
* 起始数字
*/
start: number | null;
[property: string]: any;
}
/**
* ShopTableDTO
*/
export interface editRequest {
/**
* 区域Id
*/
areaId?: number | null;
/**
* 自动清台 0手动 1自动
*/
autoClear?: number | null;
/**
* 自增id
*/
id: number | null;
/**
* 是否接受网络预定
*/
isPredate?: number | null;
/**
* 客座数,允许的客座数量
*/
maxCapacity?: number | null;
name?: null | string;
/**
* 网络预定台桌支付金额
*/
predateAmount?: number | null;
/**
* 二维码
*/
qrcode?: null | string;
/**
* 台桌排序
*/
sort?: number | null;
/**
* idle-空闲 using-使用中 subscribe预定closed--关台, opening 开台中cleaning 台桌清理中
*/
status?: null | string;
[property: string]: any;
}
export interface getListRequest {
/**
* 区域id
*/
areaId?: number;
/**
* 桌码
*/
tableCode?: string;
[property: string]: any;
}

View File

@@ -216,7 +216,7 @@ export default UserAPI;
export interface UserInfo {
/** 用户ID */
userId?: number;
id?: number | string;
/** 用户名 */
username?: string;

View File

@@ -8,37 +8,58 @@
<template v-if="typeof item === 'string'">
<!-- 新增 -->
<template v-if="item === 'add'">
<el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]" type="success" icon="plus"
@click="handleToolbar(item)">
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
type="success"
icon="plus"
@click="handleToolbar(item)"
>
新增
</el-button>
</template>
<!-- 删除 -->
<template v-else-if="item === 'delete'">
<el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]" type="danger" icon="delete"
:disabled="removeIds.length === 0" @click="handleToolbar(item)">
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
type="danger"
icon="delete"
:disabled="removeIds.length === 0"
@click="handleToolbar(item)"
>
删除
</el-button>
</template>
<!-- 导入 -->
<template v-else-if="item === 'import'">
<el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]" type="default" icon="upload"
@click="handleToolbar(item)">
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
type="default"
icon="upload"
@click="handleToolbar(item)"
>
导入
</el-button>
</template>
<!-- 导出 -->
<template v-else-if="item === 'export'">
<el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]" type="default" icon="download"
@click="handleToolbar(item)">
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
type="default"
icon="download"
@click="handleToolbar(item)"
>
导出
</el-button>
</template>
</template>
<!-- 其他 -->
<template v-else-if="typeof item === 'object'">
<el-button v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]" :icon="item.icon"
:type="item.type ?? 'default'" @click="handleToolbar(item.name)">
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]"
:icon="item.icon"
:type="item.type ?? 'default'"
@click="handleToolbar(item.name)"
>
{{ item.text }}
</el-button>
</template>
@@ -67,36 +88,68 @@
</template>
<!-- 导出 -->
<template v-else-if="item === 'exports'">
<el-button v-hasPerm="[`${contentConfig.pageName}:export`]" icon="download" circle title="导出"
@click="handleToolbar(item)" />
<el-button
v-hasPerm="[`${contentConfig.pageName}:export`]"
icon="download"
circle
title="导出"
@click="handleToolbar(item)"
/>
</template>
<!-- 导入 -->
<template v-else-if="item === 'imports'">
<el-button v-hasPerm="[`${contentConfig.pageName}:import`]" icon="upload" circle title="导入"
@click="handleToolbar(item)" />
<el-button
v-hasPerm="[`${contentConfig.pageName}:import`]"
icon="upload"
circle
title="导入"
@click="handleToolbar(item)"
/>
</template>
<!-- 搜索 -->
<template v-else-if="item === 'search'">
<el-button v-hasPerm="[`${contentConfig.pageName}:query`]" icon="search" circle title="搜索"
@click="handleToolbar(item)" />
<el-button
v-hasPerm="[`${contentConfig.pageName}:query`]"
icon="search"
circle
title="搜索"
@click="handleToolbar(item)"
/>
</template>
</template>
<!-- 其他 -->
<template v-else-if="typeof item === 'object'">
<template v-if="item.auth">
<el-button v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]" :icon="item.icon" circle
:title="item.title" @click="handleToolbar(item.name)" />
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item.auth}`]"
:icon="item.icon"
circle
:title="item.title"
@click="handleToolbar(item.name)"
/>
</template>
<template v-else>
<el-button :icon="item.icon" circle :title="item.title" @click="handleToolbar(item.name)" />
<el-button
:icon="item.icon"
circle
:title="item.title"
@click="handleToolbar(item.name)"
/>
</template>
</template>
</template>
</div>
</div>
<!-- 列表 -->
<el-table ref="tableRef" v-loading="loading" v-bind="contentConfig.table" :data="pageData" :row-key="pk"
@selection-change="handleSelectionChange" @filter-change="handleFilterChange">
<el-table
ref="tableRef"
v-loading="loading"
v-bind="contentConfig.table"
:data="pageData"
:row-key="pk"
@selection-change="handleSelectionChange"
@filter-change="handleFilterChange"
>
<template v-for="col in cols" :key="col">
<el-table-column v-if="col.show" v-bind="col">
<template #default="scope">
@@ -105,15 +158,24 @@
<template v-if="col.prop">
<template v-if="Array.isArray(scope.row[col.prop])">
<template v-for="(item, index) in scope.row[col.prop]" :key="item">
<el-image :src="item" :preview-src-list="scope.row[col.prop]" :initial-index="index"
:preview-teleported="true" :style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40
}px`" />
<el-image
:src="item"
:preview-src-list="scope.row[col.prop]"
:initial-index="index"
:preview-teleported="true"
:style="`width: ${col.imageWidth ?? 40}px; height: ${
col.imageHeight ?? 40
}px`"
/>
</template>
</template>
<template v-else>
<el-image :src="scope.row[col.prop]" :preview-src-list="[scope.row[col.prop]]"
<el-image
:src="scope.row[col.prop]"
:preview-src-list="[scope.row[col.prop]]"
:preview-teleported="true"
:style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`" />
:style="`width: ${col.imageWidth ?? 40}px; height: ${col.imageHeight ?? 40}px`"
/>
</template>
</template>
</template>
@@ -135,20 +197,30 @@
<template v-else-if="col.templet === 'switch'">
<template v-if="col.prop">
<!-- pageData.length>0: 解决el-switch组件会在表格初始化的时候触发一次change事件 -->
<el-switch v-model="scope.row[col.prop]" :active-value="col.activeValue ?? 1"
:inactive-value="col.inactiveValue ?? 0" :inline-prompt="true" :active-text="col.activeText ?? ''"
:inactive-text="col.inactiveText ?? ''" :validate-event="false"
:disabled="!hasAuth(`${contentConfig.pageName}:modify`)" @change="
<el-switch
v-model="scope.row[col.prop]"
:active-value="col.activeValue ?? 1"
:inactive-value="col.inactiveValue ?? 0"
:inline-prompt="true"
:active-text="col.activeText ?? ''"
:inactive-text="col.inactiveText ?? ''"
:validate-event="false"
:disabled="!hasAuth(`${contentConfig.pageName}:modify`)"
@change="
pageData.length > 0 && handleModify(col.prop, scope.row[col.prop], scope.row)
" />
"
/>
</template>
</template>
<!-- 生成输入框组件 -->
<template v-else-if="col.templet === 'input'">
<template v-if="col.prop">
<el-input v-model="scope.row[col.prop]" :type="col.inputType ?? 'text'"
<el-input
v-model="scope.row[col.prop]"
:type="col.inputType ?? 'text'"
:disabled="!hasAuth(`${contentConfig.pageName}:modify`)"
@blur="handleModify(col.prop, scope.row[col.prop], scope.row)" />
@blur="handleModify(col.prop, scope.row[col.prop], scope.row)"
/>
</template>
</template>
<!-- 格式化为价格 -->
@@ -180,7 +252,7 @@
{{
scope.row[col.prop]
? useDateFormat(scope.row[col.prop], col.dateFormat ?? "YYYY-MM-DD HH:mm:ss")
.value
.value
: ""
}}
</template>
@@ -191,32 +263,68 @@
<template v-if="typeof item === 'string'">
<!-- 编辑/删除 -->
<template v-if="item === 'edit' || item === 'delete'">
<el-button v-hasPerm="[`${contentConfig.pageName}:${item}`]"
:type="item === 'edit' ? 'primary' : 'danger'" :icon="item" size="small" link @click="
<el-button
v-hasPerm="[`${contentConfig.pageName}:${item}`]"
:type="item === 'edit' ? 'primary' : 'danger'"
:icon="item"
size="small"
link
@click="
handleOperat({
name: item,
row: scope.row,
column: scope.column,
$index: scope.$index,
})
">
"
>
{{ item === "edit" ? "编辑" : "删除" }}
</el-button>
</template>
</template>
<!-- 其他 -->
<template v-else-if="typeof item === 'object'">
<el-button v-if="item.render === undefined || item.render(scope.row)" v-bind="item.auth ? { 'v-hasPerm': [`${contentConfig.pageName}:${item.auth}`] } : {}
" :icon="item.icon" :type="item.type ?? 'primary'" size="small" link @click="
handleOperat({
name: item.name,
row: scope.row,
column: scope.column,
$index: scope.$index,
})
">
{{ item.text }}
</el-button>
<el-dropdown>
<el-button
v-if="item.render === undefined || item.render(scope.row)"
v-bind="
item.auth ? { 'v-hasPerm': [`${contentConfig.pageName}:${item.auth}`] } : {}
"
:icon="item.icon"
:type="item.type ?? 'primary'"
size="small"
link
@click="
handleOperat({
name: item.name,
row: scope.row,
column: scope.column,
$index: scope.$index,
})
"
>
{{ item.text }}
</el-button>
<template #dropdown v-if="item.options && item.options.length > 0">
<el-dropdown-menu>
<el-dropdown-item
@click="
handleOperat({
name: item.name,
row: scope.row,
column: scope.column,
$index: scope.$index,
command: opt.command ? opt.command : '',
})
"
v-for="opt in item.options"
:key="opt.value"
>
{{ opt.label }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</template>
</template>
@@ -232,18 +340,33 @@
<template v-if="showPagination">
<el-scrollbar>
<div class="mt-[12px]">
<el-pagination v-bind="pagination" @size-change="handleSizeChange" @current-change="handleCurrentChange" />
<el-pagination
v-bind="pagination"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-scrollbar>
</template>
<!-- 导出弹窗 -->
<el-dialog v-model="exportsModalVisible" :align-center="true" title="导出数据" width="600px" style="padding-right: 0"
@close="handleCloseExportsModal">
<el-dialog
v-model="exportsModalVisible"
:align-center="true"
title="导出数据"
width="600px"
style="padding-right: 0"
@close="handleCloseExportsModal"
>
<!-- 滚动 -->
<el-scrollbar max-height="60vh">
<!-- 表单 -->
<el-form ref="exportsFormRef" label-width="auto" style="padding-right: var(--el-dialog-padding-primary)"
:model="exportsFormData" :rules="exportsFormRules">
<el-form
ref="exportsFormRef"
label-width="auto"
style="padding-right: var(--el-dialog-padding-primary)"
:model="exportsFormData"
:rules="exportsFormRules"
>
<el-form-item label="文件名" prop="filename">
<el-input v-model="exportsFormData.filename" clearable />
</el-form-item>
@@ -253,10 +376,16 @@
<el-form-item label="数据源" prop="origin">
<el-select v-model="exportsFormData.origin">
<el-option label="当前数据 (当前页的数据)" :value="ExportsOriginEnum.CURRENT" />
<el-option label="选中数据 (所有选中的数据)" :value="ExportsOriginEnum.SELECTED"
:disabled="selectionData.length <= 0" />
<el-option label="全量数据 (所有分页的数据)" :value="ExportsOriginEnum.REMOTE"
:disabled="contentConfig.exportsAction === undefined" />
<el-option
label="选中数据 (所有选中的数据)"
:value="ExportsOriginEnum.SELECTED"
:disabled="selectionData.length <= 0"
/>
<el-option
label="全量数据 (所有分页的数据)"
:value="ExportsOriginEnum.REMOTE"
:disabled="contentConfig.exportsAction === undefined"
/>
</el-select>
</el-form-item>
<el-form-item label="字段" prop="fields">
@@ -277,17 +406,35 @@
</template>
</el-dialog>
<!-- 导入弹窗 -->
<el-dialog v-model="importModalVisible" :align-center="true" title="导入数据" width="600px" style="padding-right: 0"
@close="handleCloseImportModal">
<el-dialog
v-model="importModalVisible"
:align-center="true"
title="导入数据"
width="600px"
style="padding-right: 0"
@close="handleCloseImportModal"
>
<!-- 滚动 -->
<el-scrollbar max-height="60vh">
<!-- 表单 -->
<el-form ref="importFormRef" label-width="auto" style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData" :rules="importFormRules">
<el-form
ref="importFormRef"
label-width="auto"
style="padding-right: var(--el-dialog-padding-primary)"
:model="importFormData"
:rules="importFormRules"
>
<el-form-item label="文件名" prop="files">
<el-upload ref="uploadRef" v-model:file-list="importFormData.files" class="w-full"
<el-upload
ref="uploadRef"
v-model:file-list="importFormData.files"
class="w-full"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel"
:drag="true" :limit="1" :auto-upload="false" :on-exceed="handleFileExceed">
:drag="true"
:limit="1"
:auto-upload="false"
:on-exceed="handleFileExceed"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">
<span>将文件拖到此处,或</span>
@@ -296,8 +443,13 @@
<template #tip>
<div class="el-upload__tip">
*.xlsx / *.xls
<el-link v-if="contentConfig.importTemplate" type="primary" icon="download" :underline="false"
@click="handleDownloadTemplate">
<el-link
v-if="contentConfig.importTemplate"
type="primary"
icon="download"
:underline="false"
@click="handleDownloadTemplate"
>
下载模板
</el-link>
</div>
@@ -309,7 +461,11 @@
<!-- 弹窗底部操作按钮 -->
<template #footer>
<div style="padding-right: var(--el-dialog-padding-primary)">
<el-button type="primary" :disabled="importFormData.files.length === 0" @click="handleImportSubmit">
<el-button
type="primary"
:disabled="importFormData.files.length === 0"
@click="handleImportSubmit"
>
确 定
</el-button>
<el-button @click="handleCloseImportModal">取 消</el-button>
@@ -785,10 +941,10 @@ function fetchPageData(formData: IObject = {}, isRestart = false) {
.indexAction(
showPagination
? {
[request.pageName]: pagination.currentPage,
[request.limitName]: pagination.pageSize,
...formData,
}
[request.pageName]: pagination.currentPage,
[request.limitName]: pagination.pageSize,
...formData,
}
: formData
)
.then((data) => {

View File

@@ -78,7 +78,10 @@
<template v-else-if="item.type === 'date-picker'">
<el-date-picker v-model="formData[item.prop]" v-bind="item.attrs" />
</template>
<!-- 图片上传 -->
<template v-else-if="item.type === 'UpImage'">
<SingleImageUpload v-model="formData[item.prop]" />
</template>
<!-- Text 文本 -->
<template v-else-if="item.type === 'text'">
<el-text v-bind="item.attrs">
@@ -237,6 +240,7 @@ const props = defineProps<{
// 自定义事件
const emit = defineEmits<{
submitClick: [];
formDataChange: [string, any];
}>();
const pk = props.modalConfig.pk ?? "id";
const modalVisible = ref(false);
@@ -256,6 +260,7 @@ for (const item of formItems) {
watch(
() => formData[item.prop],
(newValue, oldValue) => {
emit("formDataChange", item.prop, newValue);
item.watch && item.watch(newValue, oldValue, formData, formItems);
}
);

View File

@@ -24,6 +24,7 @@ export interface IOperatData {
row: IObject;
column: IObject;
$index: number;
command?: string | number;
}
export interface ISearchConfig {
@@ -185,6 +186,11 @@ export interface IContentConfig<T = any> {
text: string;
type?: "primary" | "success" | "warning" | "danger" | "info";
render?: (row: IObject) => boolean;
options?: {
label: string;
command: string | number,
value?: any;
}[];
}
>;
// filter值拼接符

View File

@@ -321,7 +321,7 @@ export const constantRoutes: RouteRecordRaw[] = [
},
{
path: "table",
component: () => import("@/views/tool/table.vue"),
component: () => import("@/views/tool/table/index.vue"),
name: "table",
meta: {
title: "台桌管理",
@@ -463,7 +463,7 @@ export const constantRoutes: RouteRecordRaw[] = [
children: [
{
path: "index",
component: () => import("@/views/user/index.vue"),
component: () => import("@/views/user/list/index.vue"),
name: "userIndex",
meta: {
title: "用户列表",
@@ -473,7 +473,7 @@ export const constantRoutes: RouteRecordRaw[] = [
},
{
path: "active",
component: () => import("@/views/user/active.vue"),
component: () => import("@/views/user/active/index.vue"),
name: "userActive",
meta: {
title: "活动管理",

View File

@@ -4,6 +4,7 @@ import { store } from "@/store";
import router from "@/router";
import MenuAPI, { type RouteVO } from "@/api/account/menu";
const isTest = true//是否是本地调试
const modules = import.meta.glob("../../views/**/**.vue");
const Layout = () => import("@/layout/index.vue");
@@ -16,6 +17,8 @@ export const usePermissionStore = defineStore("permission", () => {
// 路由是否加载完成
const isRoutesLoaded = ref(false);
/**
* 获取后台动态路由数据,解析并注册到全局路由
*
@@ -25,12 +28,18 @@ export const usePermissionStore = defineStore("permission", () => {
return new Promise<RouteRecordRaw[]>((resolve, reject) => {
MenuAPI.getRoutes()
.then((data) => {
// const dynamicRoutes = parseDynamicRoutes(data);
// routes.value = [...constantRoutes, ...dynamicRoutes];
// isRoutesLoaded.value = true;
// resolve(dynamicRoutes);
isRoutesLoaded.value = true;
resolve(constantRoutes);
if (!isTest) {
const dynamicRoutes = parseDynamicRoutes(data.filter(v => v.type == 0));
routes.value = [...constantRoutes, ...dynamicRoutes];
console.log(routes.value);
isRoutesLoaded.value = true;
resolve(dynamicRoutes);
} else {
isRoutesLoaded.value = true;
resolve(constantRoutes);
}
})
.catch((error) => {
reject(error);
@@ -55,7 +64,9 @@ export const usePermissionStore = defineStore("permission", () => {
*/
const resetRouter = () => {
// 清空本地存储的路由和菜单数据
// routes.value = [];
if (!isTest) {
routes.value = [];
}
routes.value = constantRoutes;
mixedLayoutLeftRoutes.value = [];
// 从 Vue Router 中移除所有动态注册的路由
@@ -90,21 +101,33 @@ export const usePermissionStore = defineStore("permission", () => {
*/
const parseDynamicRoutes = (rawRoutes: RouteVO[]): RouteRecordRaw[] => {
const parsedRoutes: RouteRecordRaw[] = [];
rawRoutes.forEach((route) => {
const normalizedRoute = { ...route } as RouteRecordRaw;
console.log(route.path)
const normalizedRoute = {
path: route.path,
meta: {
title: route.title,
icon: route.icon,
keepAlive: route.cache,
alwaysShow: route.path && route.path.startsWith('/') ? true : false,
hidden: route.hidden,
},
children: route.children,
component: route.component,
} as RouteRecordRaw;
// 处理组件路径
normalizedRoute.component =
normalizedRoute.component?.toString() === "Layout"
!normalizedRoute.component
? Layout
: modules[`../../views/${normalizedRoute.component}.vue`] ||
modules["../../views/error-page/404.vue"];
modules["../../views/error-page/404.vue"];
// 递归解析子路由
if (normalizedRoute.children) {
// normalizedRoute.redirect = (!normalizedRoute.redirect && route.children.length <= 1) ? normalizedRoute.children[0].path : normalizedRoute.path
normalizedRoute.children = parseDynamicRoutes(route.children);
}
console.log(normalizedRoute)
parsedRoutes.push(normalizedRoute);
});

View File

@@ -4,7 +4,6 @@ import { useUserStoreHook } from "@/store/modules/user";
import { ResultEnum } from "@/enums/ResultEnum";
import { getToken } from "@/utils/auth";
import router from "@/router";
// 创建 axios 实例
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
@@ -16,6 +15,7 @@ const service = axios.create({
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
console.log(config);
const accessToken = getToken();
// 如果 Authorization 设置为 no-auth则不携带 Token用于登录、刷新 Token 等接口
if (config.headers.Authorization !== "no-auth" && accessToken) {
@@ -23,6 +23,7 @@ service.interceptors.request.use(
} else {
delete config.headers.token;
}
config.headers.shopId = config.headers.shopId || useUserStoreHook().userInfo.id;
return config;
},
(error) => Promise.reject(error)
@@ -37,6 +38,16 @@ service.interceptors.response.use(
}
const { code, data, msg } = response.data;
if (data) {
// 处理后台返回分页相关数据为字符串类型时elementui组件警告
const kes = ['pageNumber', 'pageSize', 'totalPage', 'totalRow'];
const keys = Object.keys(data);
for (let i of kes) {
if (keys.includes(i)) {
data[i] = data[i] * 1;
}
}
}
if (code === ResultEnum.SUCCESS || code === undefined || code === null) {
return data ? data : response.data;
}

View File

@@ -0,0 +1,93 @@
import { Client } from "@stomp/stompjs";
import { getToken } from "@/utils/auth";
class WebSocketManager {
private client: Client | null = null;
private messageHandlers: Map<string, ((message: string) => void)[]> = new Map();
private reconnectAttempts = 0;
private maxReconnectAttempts = 3; // 自定义最大重试次数
private reconnectDelay = 5000; // 重试延迟(单位:毫秒)
// 初始化 WebSocket 客户端
setupWebSocket() {
const endpoint = import.meta.env.VITE_APP_WS_ENDPOINT;
// 如果没有配置 WebSocket 端点或显式关闭,直接返回
if (!endpoint) {
console.log("WebSocket 已被禁用,如需打开请在配置文件中配置 VITE_APP_WS_ENDPOINT");
return;
}
if (this.client && this.client.connected) {
console.log("客户端已存在并且连接正常");
return this.client;
}
this.client = new Client({
brokerURL: endpoint,
connectHeaders: {
Authorization: getToken(),
},
heartbeatIncoming: 30000,
heartbeatOutgoing: 30000,
reconnectDelay: 0, // 设置为 0 禁用重连
onConnect: () => {
console.log(`连接到 WebSocket 服务器: ${endpoint}`);
this.reconnectAttempts = 0; // 重置重连计数
this.messageHandlers.forEach((handlers, topic) => {
handlers.forEach((handler) => {
this.subscribeToTopic(topic, handler);
});
});
},
onStompError: (frame) => {
console.error(`连接错误: ${frame.headers["message"]}`);
console.error(`错误详情: ${frame.body}`);
},
onDisconnect: () => {
console.log(`WebSocket 连接已断开: ${endpoint}`);
this.reconnectAttempts++;
if (this.reconnectAttempts < this.maxReconnectAttempts) {
console.log(`正在尝试重连... 尝试次数: ${this.reconnectAttempts}`);
} else {
console.log("重连次数已达上限,停止重连");
this.client?.deactivate();
}
},
});
this.client.activate();
}
// 订阅主题
public subscribeToTopic(topic: string, onMessage: (message: string) => void) {
console.log(`正在订阅主题: ${topic}`);
if (!this.client || !this.client.connected) {
this.setupWebSocket();
}
if (this.messageHandlers.has(topic)) {
this.messageHandlers.get(topic)?.push(onMessage);
} else {
this.messageHandlers.set(topic, [onMessage]);
}
if (this.client?.connected) {
this.client.subscribe(topic, (message) => {
const handlers = this.messageHandlers.get(topic);
handlers?.forEach((handler) => handler(message.body));
});
}
}
// 断开 WebSocket 连接
public disconnect() {
if (this.client) {
console.log("断开 WebSocket 连接");
this.client.deactivate();
this.client = null;
}
}
}
export default new WebSocketManager();

View File

@@ -1,9 +1,10 @@
import { Client } from "@stomp/stompjs";
import { getToken } from "@/utils/auth";
class WebSocketManager {
private client: Client | null = null;
private client: WebSocket | null = null;
private connected: boolean = false;
private onMessage: (message: any) => void = function () { };
private messageHandlers: Map<string, ((message: string) => void)[]> = new Map();
private type: string = 'manage';
private reconnectAttempts = 0;
private maxReconnectAttempts = 3; // 自定义最大重试次数
private reconnectDelay = 5000; // 重试延迟(单位:毫秒)
@@ -18,73 +19,52 @@ class WebSocketManager {
return;
}
if (this.client && this.client.connected) {
if (this.client && this.connected) {
console.log("客户端已存在并且连接正常");
return this.client;
}
this.client = new Client({
brokerURL: endpoint,
connectHeaders: {
Authorization: getToken(),
},
heartbeatIncoming: 30000,
heartbeatOutgoing: 30000,
reconnectDelay: 0, // 设置为 0 禁用重连
onConnect: () => {
console.log(`连接到 WebSocket 服务器: ${endpoint}`);
this.reconnectAttempts = 0; // 重置重连计数
this.messageHandlers.forEach((handlers, topic) => {
handlers.forEach((handler) => {
this.subscribeToTopic(topic, handler);
});
});
},
onStompError: (frame) => {
console.error(`连接错误: ${frame.headers["message"]}`);
console.error(`错误详情: ${frame.body}`);
},
onDisconnect: () => {
console.log(`WebSocket 连接已断开: ${endpoint}`);
this.reconnectAttempts++;
if (this.reconnectAttempts < this.maxReconnectAttempts) {
console.log(`正在尝试重连... 尝试次数: ${this.reconnectAttempts}`);
} else {
console.log("重连次数已达上限,停止重连");
this.client?.deactivate();
}
},
});
this.client.activate();
this.client = new WebSocket(endpoint)
this.client.onopen = () => {
this.connected = true;
console.log("WebSocket 连接已建立");
this.sendMessage('test')
};
this.client.onclose = () => {
this.connected = false;
console.log("WebSocket 连接已断开");
};
this.client.onerror = (error) => {
console.error("WebSocket 发生错误:", error);
};
this.client.onmessage = (event) => {
const message = event.data;
this.getMessage(message)
};
}
private getMessage(message: any) {
console.log("收到消息:", message);
}
// 订阅主题
public subscribeToTopic(topic: string, onMessage: (message: string) => void) {
console.log(`正在订阅主题: ${topic}`);
if (!this.client || !this.client.connected) {
if (!this.client || !this.connected) {
this.setupWebSocket();
}
if (this.messageHandlers.has(topic)) {
this.messageHandlers.get(topic)?.push(onMessage);
} else {
this.messageHandlers.set(topic, [onMessage]);
}
if (this.client?.connected) {
this.client.subscribe(topic, (message) => {
const handlers = this.messageHandlers.get(topic);
handlers?.forEach((handler) => handler(message.body));
});
if (this.connected) {
this.onMessage = onMessage;
}
}
public sendMessage(message: any) {
if (this.client) {
this.client.send(message);
}
}
// 断开 WebSocket 连接
public disconnect() {
if (this.client) {
console.log("断开 WebSocket 连接");
this.client.deactivate();
this.client.close();
this.client = null;
}
}

View File

@@ -48,47 +48,67 @@
<el-table-column label="图标" align="center" width="80">
<template #default="scope">
<div v-if="scope.row.icon" :class="`i-svg:${scope.row.icon}`" />
<template v-if="scope.row.icon && scope.row.icon.startsWith('el-icon')">
<el-icon style="vertical-align: -0.15em">
<component :is="scope.row.icon.replace('el-icon-', '')" />
</el-icon>
</template>
<template v-else-if="scope.row.icon">
<svg-icon :icon-class="scope.row.icon" />
</template>
</template>
</el-table-column>
<el-table-column label="类型" align="center" width="80">
<template #default="scope">
<el-tag
v-if="scope.row.type === MenuTypeEnum.MENU && scope.row.path.startsWith('/')"
type="warning"
>
目录
</el-tag>
<el-tag
v-if="scope.row.type === MenuTypeEnum.MENU && !scope.row.path.startsWith('/')"
type="success"
>
菜单
</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.BUTTON" type="danger">按钮</el-tag>
<el-tag v-if="scope.row.type === MenuTypeEnum.EXTLINK" type="info">外链</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="left" width="150" prop="menuSort" />
<el-table-column label="权限标识" align="center" width="200" prop="perm" />
<el-table-column label="路由路径" align="left" width="250" prop="path" />
<el-table-column label="组件路径" align="left" width="250" prop="component" />
<el-table-column label="外链" align="left" width="80" prop="iFrame">
<template #default="scope">
{{ scope.row.iFrame ? "是" : "否" }}
</template>
</el-table-column>
<el-table-column label="缓存" align="left" width="80" prop="iFrame">
<el-table-column label="缓存" align="left" width="80" prop="cache">
<template #default="scope">
{{ scope.row.iFrame ? "是" : "否" }}
{{ scope.row.cache ? "是" : "否" }}
</template>
</el-table-column>
<el-table-column label="可见" align="left" width="80" prop="iFrame">
<template #default="scope">
{{ scope.row.iFrame ? "" : "" }}
{{ scope.row.hidden ? "" : "" }}
</template>
</el-table-column>
<el-table-column label="商家可用" align="left" width="80" prop="iFrame">
<el-table-column label="创建日期" align="left" prop="createTime">
<template #default="scope">
{{ scope.row.iFrame ? "是" : "否" }}
</template>
</el-table-column>
<el-table-column label="商家可用" align="left" width="80" prop="iFrame">
<template #default="scope">
{{ scope.row.iFrame ? "是" : "否" }}
{{ scope.row.createTime }}
</template>
</el-table-column>
<el-table-column fixed="right" align="center" label="操作" width="220">
<template #default="scope">
<el-button
v-if="scope.row.type == 'MENU' || scope.row.type == 'MENU'"
v-if="scope.row.type == 0"
v-hasPerm="['sys:menu:add']"
type="primary"
link
size="small"
icon="plus"
@click.stop="handleOpenDialog(scope.row.id)"
@click.stop="handleOpenDialog(scope.row.menuId)"
>
新增
</el-button>
@@ -99,7 +119,7 @@
link
size="small"
icon="edit"
@click.stop="handleOpenDialog(undefined, scope.row.id)"
@click.stop="handleOpenDialog(undefined, scope.row.menuId)"
>
编辑
</el-button>
@@ -109,7 +129,7 @@
link
size="small"
icon="delete"
@click.stop="handleDelete(scope.row.id)"
@click.stop="handleDelete(scope.row.menuId)"
>
删除
</el-button>
@@ -144,10 +164,10 @@
</el-form-item>
<el-form-item v-if="formData.iFrame == 1" label="外链地址" prop="path">
<el-input v-model="formData.routePath" placeholder="请输入外链完整路径" />
<el-input v-model="formData.path" placeholder="请输入外链完整路径" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="routeName">
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="name">
<template #label>
<div class="flex-y-center">
路由名称
@@ -161,10 +181,10 @@
</el-tooltip>
</div>
</template>
<el-input v-model="formData.routeName" placeholder="User" />
<el-input v-model="formData.name" placeholder="User" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="routePath">
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="path">
<template #label>
<div class="flex-y-center">
路由路径
@@ -181,10 +201,10 @@
</template>
<el-input
v-if="formData.type == MenuTypeEnum.MENU"
v-model="formData.routePath"
v-model="formData.path"
placeholder="system"
/>
<el-input v-else v-model="formData.routePath" placeholder="user" />
<el-input v-else v-model="formData.path" placeholder="user" />
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU" prop="component">
@@ -208,85 +228,10 @@
</el-input>
</el-form-item>
<el-form-item v-if="formData.type == MenuTypeEnum.MENU">
<template #label>
<div class="flex-y-center">
路由参数
<el-tooltip placement="bottom" effect="light">
<template #content>
组件页面使用 `useRoute().query.参数名` 获取路由参数值
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<div v-if="!formData.params || formData.params.length === 0">
<el-button type="success" plain @click="formData.params = [{ key: '', value: '' }]">
添加路由参数
</el-button>
</div>
<div v-else>
<div v-for="(item, index) in formData.params" :key="index">
<el-input v-model="item.key" placeholder="参数名" style="width: 100px" />
<span class="mx-1">=</span>
<el-input v-model="item.value" placeholder="参数值" style="width: 100px" />
<el-icon
v-if="formData.params.indexOf(item) === formData.params.length - 1"
class="ml-2 cursor-pointer color-[var(--el-color-success)]"
style="vertical-align: -0.15em"
@click="formData.params.push({ key: '', value: '' })"
>
<CirclePlusFilled />
</el-icon>
<el-icon
class="ml-2 cursor-pointer color-[var(--el-color-danger)]"
style="vertical-align: -0.15em"
@click="formData.params.splice(formData.params.indexOf(item), 1)"
>
<DeleteFilled />
</el-icon>
</div>
</div>
</el-form-item>
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" prop="hidden" label="显示状态">
<el-radio-group v-model="formData.hidden">
<el-radio :value="1">显示</el-radio>
<el-radio :value="0">隐藏</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="formData.type === MenuTypeEnum.MENU || formData.type === MenuTypeEnum.MENU"
>
<template #label>
<div class="flex-y-center">
始终显示
<el-tooltip placement="bottom" effect="light">
<template #content>
选择即使目录或菜单下只有一个子节点也会显示父节点
<br />
选择如果目录或菜单下只有一个子节点则只显示该子节点隐藏父节点
<br />
如果是叶子节点请选择
</template>
<el-icon class="ml-1 cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<el-radio-group v-model="formData.alwaysShow">
<el-radio :value="1"></el-radio>
<el-radio :value="0"></el-radio>
<el-radio :value="1">隐藏</el-radio>
<el-radio :value="0">显示</el-radio>
</el-radio-group>
</el-form-item>
@@ -307,8 +252,12 @@
</el-form-item>
<!-- 权限标识 -->
<el-form-item v-if="formData.type == MenuTypeEnum.BUTTON" label="权限标识" prop="perm">
<el-input v-model="formData.perm" placeholder="sys:user:add" />
<el-form-item
v-if="formData.type == MenuTypeEnum.BUTTON"
label="权限标识"
prop="permission"
>
<el-input v-model="formData.permission" placeholder="sys:user:add" />
</el-form-item>
<el-form-item v-if="formData.type !== MenuTypeEnum.BUTTON" label="图标" prop="icon">
@@ -360,7 +309,7 @@ const menuOptions = ref<OptionType[]>([]);
}
const initialeditRequestData = ref<editRequest>({
id: "",
pid: 0,
pid: "",
hidden: 0,
menuSort: 1,
type: 0, // 默认菜单
@@ -376,12 +325,12 @@ const initialeditRequestData = ref<editRequest>({
const formData = ref({ ...initialeditRequestData.value });
// 表单验证规则
const rules = reactive({
pid: [{ required: true, message: "请选择父级菜单", trigger: "blur" }],
pid: [{ required: false, message: "请选择父级菜单", trigger: "blur" }],
title: [{ required: true, message: "请输入菜单名称", trigger: "blur" }],
type: [{ required: true, message: "请选择菜单类型", trigger: "blur" }],
routeName: [{ required: true, message: "请输入路由名称", trigger: "blur" }],
routePath: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
component: [{ required: true, message: "请输入组件路径", trigger: "blur" }],
name: [{ required: false, message: "请输入路由名称", trigger: "blur" }],
path: [{ required: true, message: "请输入路由路径", trigger: "blur" }],
component: [{ required: false, message: "请输入组件路径", trigger: "blur" }],
hidden: [{ required: true, message: "请选择显示状态", trigger: "change" }],
});
@@ -440,10 +389,19 @@ function handleOpenDialog(pid?: string, menuId?: string) {
.then(() => {
dialog.visible = true;
if (menuId) {
console.log(menuId);
dialog.title = "编辑菜单";
MenuAPI.get(menuId).then((data) => {
initialeditRequestData.value = { ...data };
formData.value = data;
console.log(data);
initialeditRequestData.value = {
...data,
};
formData.value = {
...data,
cache: data.cache ? 1 : 0,
hidden: data.hidden ? 1 : 0,
pid: !pid ? "0" : data.pid,
};
});
} else {
dialog.title = "新增菜单";
@@ -462,7 +420,7 @@ function handleMenuTypeChange() {
formData.value.component = "";
} else {
// 其他情况,保留原有的组件路径
formData.value.routePath = initialeditRequestData.value.routePath;
formData.value.path = initialeditRequestData.value.path;
formData.value.component = initialeditRequestData.value.component;
}
}
@@ -475,20 +433,26 @@ function handleMenuTypeChange() {
function handleSubmit() {
editRequestRef.value.validate((isValid: boolean) => {
if (isValid) {
const menuId = formData.value.id;
const menuId = formData.value.menuId;
const submitFormData = {
...formData.value,
cache: formData.value.cache ? 1 : 0,
hidden: formData.value.hidden ? 1 : 0,
pid: formData.value.pid == 0 ? null : formData.value.pid,
};
if (menuId) {
//修改时父级菜单不能为当前菜单
if (formData.value.pid == menuId) {
ElMessage.error("父级菜单不能为当前菜单");
return;
}
MenuAPI.edit(menuId, formData.value).then(() => {
MenuAPI.edit(menuId, submitFormData).then(() => {
ElMessage.success("修改成功");
handleCloseDialog();
handleQuery();
});
} else {
MenuAPI.add(formData.value).then(() => {
MenuAPI.add(submitFormData).then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
handleQuery();
@@ -532,7 +496,7 @@ function resetForm() {
editRequestRef.value.clearValidate();
formData.value = {
id: "",
pid: 0,
pid: "",
hidden: 0,
menuSort: 1,
type: 0, // 默认菜单

View File

@@ -0,0 +1,128 @@
<template>
<div class="flex-x-between">
<el-input v-model="permKeywords" clearable class="w-[150px]" placeholder="菜单菜单名称">
<template #prefix>
<Search />
</template>
</el-input>
<div class="flex-center ml-5">
<el-button type="primary" size="small" plain @click="togglePermTree">
<template #icon>
<Switch />
</template>
{{ isExpanded ? "收缩" : "展开" }}
</el-button>
<el-checkbox v-model="parentChildLinked" class="ml-5" @change="handleparentChildLinkedChange">
父子联动
</el-checkbox>
<el-tooltip placement="bottom">
<template #content>
如果只需勾选菜单菜单不需要勾选子菜单或者按钮菜单请关闭父子联动
</template>
<el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</div>
<el-tree
ref="permTreeRef"
node-key="value"
show-checkbox
:data="menuPermOptions"
:filter-node-method="handlePermFilter"
:default-expand-all="true"
:check-strictly="!parentChildLinked"
class="mt-5"
>
<template #default="{ data }">
{{ data.label }}
</template>
</el-tree>
</template>
<script setup>
import MenuAPI from "@/api/account/menu";
const modelValue = defineModel("modelValue", {
type: Array,
required: true,
default: () => [],
});
const permKeywords = ref("");
const isExpanded = ref(true);
const parentChildLinked = ref(true);
const permTreeRef = ref();
// 展开/收缩 菜单菜单树
function togglePermTree() {
isExpanded.value = !isExpanded.value;
if (permTreeRef.value) {
Object.values(permTreeRef.value.store.nodesMap).forEach((node) => {
if (isExpanded.value) {
node.expand();
} else {
node.collapse();
}
});
}
}
function handlePermFilter(value, data) {
if (!value) return true;
return data.label.includes(value);
}
// 父子菜单节点是否联动
function handleparentChildLinkedChange(val) {
parentChildLinked.value = val;
}
// 菜单菜单下拉
const menuPermOptions = ref([]);
function returnMenu(menu) {
return menu.map((v) => {
return {
...v,
label: v.title,
value: v.menuId,
children: v.children ? returnMenu(v.children) : [],
};
});
}
// 获取所有的菜单
async function getMenuPermOptions() {
let arr = await MenuAPI.getList();
menuPermOptions.value = returnMenu(arr);
}
getMenuPermOptions();
onMounted(() => {
console.log(modelValue.value);
});
watch(
() => modelValue.value,
(newval) => {}
);
function getPerms() {
return permTreeRef.value.getCheckedKeys();
}
function reset() {
permTreeRef.value.setCheckedKeys([]);
}
function setChecked(checkedMenuIds) {
checkedMenuIds.forEach((menuId) => {
console.log(menuId);
permTreeRef.value.setChecked(menuId, true, false);
});
}
defineExpose({
getPerms,
setChecked,
reset,
});
</script>

View File

@@ -35,18 +35,10 @@
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="角色名称" prop="name" min-width="100" />
<el-table-column label="角色编码" prop="code" width="150" />
<el-table-column label="状态" align="center" width="100">
<template #default="scope">
<el-tag v-if="scope.row.status === 1" type="success">正常</el-tag>
<el-tag v-else type="info">禁用</el-tag>
</template>
</el-table-column>
<el-table-column label="排序" align="center" width="80" prop="sort" />
<el-table-column label="角色名称" prop="name" />
<el-table-column label="角色级别" prop="level" />
<el-table-column label="描述" prop="description" />
<el-table-column label="创建日期" prop="createTime" />
<el-table-column fixed="right" label="操作" width="220">
<template #default="scope">
<el-button
@@ -56,7 +48,7 @@
icon="position"
@click="handleOpenAssignPermDialog(scope.row)"
>
分配权限
分配菜单
</el-button>
<el-button
type="primary"
@@ -101,33 +93,24 @@
<el-input v-model="formData.name" placeholder="请输入角色名称" />
</el-form-item>
<el-form-item label="角色编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入角色编码" />
<el-form-item label="角色等级" prop="level">
<el-input-number placeholder="角色等级" v-model="formData.level"></el-input-number>
</el-form-item>
<el-form-item label="数据权限" prop="dataScope">
<!-- <el-form-item label="数据菜单" prop="dataScope">
<el-select v-model="formData.dataScope">
<el-option :key="0" label="全部数据" :value="0" />
<el-option :key="1" label="部门及子部门数据" :value="1" />
<el-option :key="2" label="本部门数据" :value="2" />
<el-option :key="3" label="本人数据" :value="3" />
</el-select>
</el-form-item> -->
<el-form-item label="描述" prop="description">
<el-input type="textarea" v-model="formData.description" placeholder="描述" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :label="1">正常</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="formData.sort"
controls-position="right"
:min="0"
style="width: 100px"
/>
<el-form-item label="菜单分配" prop="menuIdList">
<menuSelect ref="refmenuSelect" v-model="formData.menuIdList"></menuSelect>
</el-form-item>
</el-form>
@@ -139,14 +122,14 @@
</template>
</el-dialog>
<!-- 分配权限弹窗 -->
<!-- 分配菜单弹窗 -->
<el-drawer
v-model="assignPermDialogVisible"
:title="'【' + checkedRole.name + '】权限分配'"
:title="'【' + checkedRole.name + '】菜单分配'"
size="500"
>
<div class="flex-x-between">
<el-input v-model="permKeywords" clearable class="w-[150px]" placeholder="菜单权限名称">
<el-input v-model="permKeywords" clearable class="w-[150px]" placeholder="菜单菜单名称">
<template #prefix>
<Search />
</template>
@@ -169,7 +152,7 @@
<el-tooltip placement="bottom">
<template #content>
如果只需勾选菜单权限不需要勾选子菜单或者按钮权限请关闭父子联动
如果只需勾选菜单菜单不需要勾选子菜单或者按钮菜单请关闭父子联动
</template>
<el-icon class="ml-1 color-[--el-color-primary] inline-block cursor-pointer">
<QuestionFilled />
@@ -207,12 +190,13 @@ defineOptions({
name: "Role",
inheritAttrs: false,
});
import menuSelect from "./components/menus.vue";
import RoleApi, { SysRole, addRequest, getListRequest } from "@/api/account/role";
import MenuAPI, { type RouteVO } from "@/api/account/menu";
const queryFormRef = ref();
const addRequestRef = ref();
const refmenuSelect = ref();
const permTreeRef = ref();
const loading = ref(false);
@@ -227,7 +211,7 @@ const queryParams = reactive<getListRequest>({
// 角色表格数据
const roleList = ref<SysRole[]>();
// 菜单权限下拉
// 菜单菜单下拉
const menuPermOptions = ref<RouteVO[]>([]);
// 弹窗
@@ -248,9 +232,7 @@ const formData = reactive<addRequest>({
const rules = reactive({
name: [{ required: true, message: "请输入角色名称", trigger: "blur" }],
code: [{ required: true, message: "请输入角色编码", trigger: "blur" }],
dataScope: [{ required: true, message: "请选择数据权限", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
level: [{ required: false, message: "请输入角色等级", trigger: "blur" }],
});
// 选中的角色
@@ -294,11 +276,19 @@ function handleSelectionChange(selection: any) {
}
// 打开角色弹窗
function handleOpenDialog(row: SysRole) {
async function handleOpenDialog(row: SysRole) {
dialog.visible = true;
if (row.id) {
if (row && row.id) {
dialog.title = "修改角色";
//获取角色菜单列表
const data = await RoleApi.getMenu(row.id);
Object.assign(formData, row);
console.log(data);
formData.menuIdList = data;
setTimeout(() => {
refmenuSelect.value.setChecked(data);
}, 100);
console.log(formData);
} else {
dialog.title = "新增角色";
}
@@ -308,10 +298,11 @@ function handleOpenDialog(row: SysRole) {
function handleSubmit() {
addRequestRef.value.validate((valid: any) => {
if (valid) {
const checkedMenuIds: number[] = refmenuSelect.value.getPerms();
loading.value = true;
const roleId = formData.id;
if (roleId) {
RoleApi.update({ ...formData, id: formData.id ?? null })
RoleApi.update(roleId, { ...formData, menuIdList: checkedMenuIds })
.then(() => {
ElMessage.success("修改成功");
handleCloseDialog();
@@ -320,7 +311,8 @@ function handleSubmit() {
.finally(() => (loading.value = false));
} else {
delete formData.id;
RoleApi.add(formData)
RoleApi.add({ ...formData, menuIdList: checkedMenuIds })
.then(() => {
ElMessage.success("新增成功");
handleCloseDialog();
@@ -338,7 +330,7 @@ function handleCloseDialog() {
addRequestRef.value.resetFields();
addRequestRef.value.clearValidate();
refmenuSelect.value.reset();
formData.id = undefined;
formData.sort = 1;
formData.status = 1;
@@ -378,7 +370,7 @@ interface typeRoute extends RouteVO {
children: typeRoute[];
}
function returnMenu(menu: RouteVO[]): typeRoute[] {
function returnMenu(menu) {
return menu.map((v) => {
return {
...v,
@@ -389,7 +381,13 @@ function returnMenu(menu: RouteVO[]): typeRoute[] {
});
}
// 打开分配菜单权限弹窗
// 获取所有的菜单
async function getMenuPermOptions() {
let arr = await MenuAPI.getList({});
menuPermOptions.value = returnMenu(arr);
}
getMenuPermOptions();
// 打开分配菜单菜单弹窗
async function handleOpenAssignPermDialog(row: SysRole) {
const roleId = row.id;
if (roleId) {
@@ -399,34 +397,31 @@ async function handleOpenAssignPermDialog(row: SysRole) {
checkedRole.value.id = roleId;
checkedRole.value.name = row.name as string;
// 获取所有的菜单
let arr: RouteVO[] = await MenuAPI.getRoutes();
menuPermOptions.value = returnMenu(arr);
// 回显角色已拥有的菜单
// RoleApi.getRoleMenuIds(roleId)
// .then((data) => {
// const checkedMenuIds = data;
// checkedMenuIds.forEach((menuId) => permTreeRef.value!.setChecked(menuId, true, false));
// })
// .finally(() => {
// loading.value = false;
// });
RoleApi.getMenu(roleId)
.then((data) => {
const checkedMenuIds = data;
checkedMenuIds.forEach((menuId) => permTreeRef.value!.setChecked(menuId, true, false));
})
.finally(() => {
loading.value = false;
});
}
}
// 分配菜单权限提交
// 分配菜单菜单提交
function handleAssignPermSubmit() {
const roleId = checkedRole.value.id;
const name = checkedRole.value.name;
if (roleId) {
const checkedMenuIds: number[] = permTreeRef
.value!.getCheckedNodes(false, true)
.map((node: any) => node.value);
loading.value = true;
RoleApi.updateRoleMenus(roleId, checkedMenuIds)
RoleApi.update(roleId, { name, menuIdList: checkedMenuIds })
.then(() => {
ElMessage.success("分配权限成功");
ElMessage.success("分配菜单成功");
assignPermDialogVisible.value = false;
handleResetQuery();
})
@@ -436,7 +431,7 @@ function handleAssignPermSubmit() {
}
}
// 展开/收缩 菜单权限
// 展开/收缩 菜单菜单
function togglePermTree() {
isExpanded.value = !isExpanded.value;
if (permTreeRef.value) {
@@ -449,8 +444,20 @@ function togglePermTree() {
});
}
}
function editTogglePermTree() {
isExpanded.value = !isExpanded.value;
if (permTreeRef.value) {
Object.values(permTreeRef.value.store.nodesMap).forEach((node: any) => {
if (isExpanded.value) {
node.expand();
} else {
node.collapse();
}
});
}
}
// 权限筛选
// 菜单筛选
watch(permKeywords, (val) => {
permTreeRef.value!.filter(val);
});

View File

@@ -1,7 +1,13 @@
<template>
<div class="login" :style="'background-image:url(' + Background + ');'">
<el-form ref="loginForm" :model="state.loginForm" :rules="state.loginRules" label-position="left" label-width="0px"
class="login-form">
<el-form
ref="loginForm"
:model="state.loginForm"
:rules="state.loginRules"
label-position="left"
label-width="0px"
class="login-form"
>
<h3 class="title">银收客后台管理</h3>
<el-form-item>
<el-radio-group v-model="state.loginForm.loginType">
@@ -10,19 +16,39 @@
</el-radio-group>
</el-form-item>
<el-form-item prop="merchantName" v-if="state.loginForm.loginType == 'staff'">
<el-input v-model="state.loginForm.merchantName" type="text" auto-complete="off" placeholder="商户号"></el-input>
<el-input
v-model="state.loginForm.merchantName"
type="text"
auto-complete="off"
placeholder="商户号"
></el-input>
</el-form-item>
<el-form-item prop="username">
<el-input v-model="state.loginForm.username" type="text" auto-complete="off" placeholder="账号"></el-input>
<el-input
v-model="state.loginForm.username"
type="text"
auto-complete="off"
placeholder="账号"
></el-input>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="state.loginForm.password" type="password" auto-complete="off" placeholder="密码"
@keyup.enter="handleLogin"></el-input>
<el-input
v-model="state.loginForm.password"
type="password"
auto-complete="off"
placeholder="密码"
@keyup.enter="handleLogin"
></el-input>
</el-form-item>
<el-form-item prop="code">
<div class="code_wrap">
<el-input v-model="state.loginForm.code" auto-complete="off" placeholder="验证码" style="width: 63%"
@keyup.enter="handleLogin"></el-input>
<el-input
v-model="state.loginForm.code"
auto-complete="off"
placeholder="验证码"
style="width: 63%"
@keyup.enter="handleLogin"
></el-input>
<div class="login-code">
<img :src="state.codeUrl" @click="getCode" />
</div>
@@ -30,8 +56,13 @@
</el-form-item>
<el-form-item style="width: 100%">
<el-button :loading="state.loading" size="default" type="primary" style="width: 100%"
@click.prevent="handleLogin">
<el-button
:loading="state.loading"
size="default"
type="primary"
style="width: 100%"
@click.prevent="handleLogin"
>
<span v-if="!state.loading"> </span>
<span v-else> 中...</span>
</el-button>
@@ -61,8 +92,8 @@ const state = reactive({
codeUrl: "",
cookiePass: "",
loginForm: {
username: "",
password: "",
username: "admin",
password: "12345",
// rememberMe: false,
code: "",
uuid: "",
@@ -152,7 +183,7 @@ function handleLogin() {
.then(async (res) => {
// await userStore.getUserInfo();
const { path, queryParams } = parseRedirect();
console.log(res, 'Denglv返回');
console.log(res, "Denglv返回");
router.push({ path: path, query: queryParams });
})
.catch(() => {

View File

@@ -3,13 +3,16 @@
<el-tabs v-model="activeName">
<el-tab-pane label="聚合支付" name="pay">
<el-form ref="form" :model="form" label-width="120px" label-position="left">
<el-form-item label="店铺id">
<el-input v-model="form.storeId" placeholder="请输入店铺id"></el-input>
</el-form-item>
<el-form-item label="商户号">
<el-input v-model="form.appId" placeholder="请输入商户号"></el-input>
</el-form-item>
<el-form-item label="商户密钥">
<el-input
type="textarea"
v-model="form.appToken"
v-model="form.appSecret"
placeholder="请输入商户密钥"
></el-input>
</el-form-item>
@@ -17,7 +20,10 @@
<el-input v-model="form.payPassword" placeholder="请输入支付密码"></el-input>
</el-form-item>
<el-form-item label="微信appid">
<el-input v-model="form.smallAppid" placeholder="请输入微信小程序appid"></el-input>
<el-input
v-model="form.wechatSmallAppid"
placeholder="请输入微信小程序appid"
></el-input>
</el-form-item>
<el-form-item label="支付宝appid">
<el-input
@@ -31,32 +37,33 @@
<el-form-item label="支付宝商户密钥">
<el-input v-model="form.alipayAppToken" placeholder="请输入支付宝商户密钥"></el-input>
</el-form-item> -->
<el-form-item label="店铺id">
<el-input v-model="form.storeId" placeholder="请输入店铺id"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :label="1">启用</el-radio>
<el-radio :label="-1">禁用</el-radio>
<el-radio :value="1">启用</el-radio>
<el-radio :value="-1">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</el-tab-pane>
</el-tabs>
<div slot="footer" class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submitHandle" :loading="formLoading">
<span v-if="!formLoading">保存</span>
<span v-else>保存中...</span>
</el-button>
</div>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submitHandle" :loading="formLoading">
<span v-if="!formLoading">保存</span>
<span v-else>保存中...</span>
</el-button>
</div>
</template>
</el-dialog>
</template>
<script>
// import { tbMerchantThirdApply, tbMerchantThirdApplyPut } from "@/api/shop";
import ShopApi from "@/api/account/shop";
import shopMerchantApi from "@/api/account/shopMerchant";
import { ElNotification } from "element-plus";
export default {
data() {
return {
@@ -64,12 +71,12 @@ export default {
activeName: "pay",
formLoading: false,
form: {
appToken: "",
appSecret: "",
id: "",
payPassword: "",
status: 1,
appId: "",
smallAppid: "",
wechatSmallAppid: "",
storeId: "",
alipaySmallAppid: "",
alipayAppToken: "",
@@ -82,12 +89,12 @@ export default {
async submitHandle() {
this.formLoading = true;
try {
await tbMerchantThirdApplyPut(this.form);
await shopMerchantApi.edit(this.form.id, this.form);
this.$emit("success");
this.formLoading = false;
this.$notify({
ElNotification({
title: "成功",
message: `提交成功`,
message: `修改成功`,
type: "success",
});
this.close();
@@ -100,7 +107,7 @@ export default {
this.dialogVisible = false;
},
reset() {
this.form.appToken = "";
this.form.appSecret = "";
this.form.id = "";
this.form.payPassword = "";
this.form.status = 1;
@@ -108,14 +115,16 @@ export default {
},
// 详情(配置三方支付)
async getDetail(id) {
console.log(id);
try {
const res = await tbMerchantThirdApply(id);
this.form.appToken = res.appToken;
const res = await shopMerchantApi.get(id);
this.form.appSecret = res.appSecret;
this.form.payPassword = res.payPassword;
this.form.status = res.status;
this.form.appId = res.appId;
this.form.smallAppid = res.smallAppid;
this.form.wechatSmallAppid = res.wechatSmallAppid;
this.form.alipaySmallAppid = res.alipaySmallAppid;
this.form.merchantName = res.merchantName;
//this.form.alipayAppToken = res.alipayAppToken
//this.form.alipayAppId = res.alipayAppId
this.form.storeId = res.storeId;
@@ -126,8 +135,8 @@ export default {
},
show(obj) {
if (obj && obj.id) {
this.form.id = obj.merchantId;
this.getDetail(obj.merchantId);
this.form.id = obj.id;
this.getDetail(obj.id);
}
},
},

View File

@@ -184,12 +184,13 @@ onMounted(() => {
const refDetailModal = ref(null);
function dropdownClick(e) {
switch (e.command) {
case 1:
refDetailModal.value.show(e.row);
break;
default:
break;
console.log(e);
if (e.command == 1) {
refDetailModal.value.show(e.row);
return;
}
if (e == 5) {
return;
}
}
// 重置查询

View File

@@ -0,0 +1 @@
<template>11</template>

View File

@@ -43,9 +43,8 @@
<!-- 新增 -->
<page-modal ref="addModalRef" :modal-config="addModalConfig" @submit-click="handleSubmitClick">
<template #url="scope">
<FileUpload v-model="scope.formData[scope.prop]" :limit="1" v-bind="scope.attrs" />
<!-- <Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" /> -->
<template #permission="scope">
<selectPermission></selectPermission>
</template>
</page-modal>
@@ -55,9 +54,8 @@
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
>
<template #url="scope">
<FileUpload v-model="scope.formData[scope.prop]" :limit="1" v-bind="scope.attrs" />
<!-- <Dict v-model="scope.formData[scope.prop]" code="gender" v-bind="scope.attrs" /> -->
<template #permission="scope">
<selectPermission></selectPermission>
</template>
</page-modal>
</div>
@@ -73,7 +71,9 @@ import searchConfig from "./config/search";
import { returnOptionsLabel } from "./config/config";
import RoleApi, { type SysRole } from "@/api/account/role";
import ShopStaffApi from "@/api/account/shopStaff";
import permissionApi from "@/api/account/permission";
import selectPermission from "./components/select-permission.vue";
permissionApi.getshopPermission();
const {
searchRef,
contentRef,

View File

@@ -1,3 +0,0 @@
<template>
<div class="app-container">11</div>
</template>

View File

@@ -0,0 +1,73 @@
<template>
<el-dialog :title="form.id ? '编辑区域' : '添加区域'" :visible.sync="dialogVisible" @close="reset">
<el-form ref="form" :model="form" :rules="rules" label-width="120px" label-position="left">
<el-form-item label="区域名称" prop="name">
<el-input v-model="form.name" placeholder="请输入区域名称"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" @click="onSubmitHandle"> </el-button>
</span>
</el-dialog>
</template>
<script>
import { tbShopArea } from '@/api/table'
export default {
data() {
return {
dialogVisible: false,
form: {
id: '',
name: ''
},
rules: {
name: [
{
required: true,
message: '请输入区域名称',
trigger: 'blur'
}
]
}
}
},
methods: {
onSubmitHandle() {
this.$refs.form.validate(async valid => {
if (valid) {
try {
let res = await tbShopArea({
...this.form,
shopId: localStorage.getItem('shopId')
}, this.form.id ? 'put' : 'post')
this.$emit('success', res)
this.close()
this.$notify({
title: '成功',
message: `${this.form.id ? '编辑' : '添加'}成功`,
type: 'success'
});
} catch (error) {
console.log(error)
}
}
})
},
show(obj) {
this.dialogVisible = true
if (obj && obj.id) {
this.form = JSON.parse(JSON.stringify(obj))
}
},
close() {
this.dialogVisible = false
},
reset() {
this.form.id = ''
this.form.name = ''
}
}
}
</script>

View File

@@ -0,0 +1,235 @@
<template>
<el-dialog
:title="form.id ? '编辑台桌' : '添加台桌'"
:visible.sync="dialogVisible"
@open="tbShopAreaGet"
@close="reset"
>
<el-form
ref="form"
:model="form"
:rules="rules"
label-width="120px"
label-position="left"
>
<el-form-item label="选择区域" prop="areaId">
<el-select v-model="form.areaId" placeholder="请选择区域"
@change="selectChange($event, 'form' , 'areaId')"
>
<el-option
:label="item.name"
:value="item.id"
v-for="item in areaList"
:key="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="台桌状态" prop="status" v-if="form.id">
<el-select v-model="form.status" placeholder="请选择台桌状态">
<el-option
:label="item.name"
:value="item.value"
v-for="item in status"
:key="item.value"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="台桌标识" prop="sign">
<div class="u-flex">
<div class="u-flex" style="width: 57px;">
<el-input
v-model="form.sign"
placeholder="A"
></el-input>
</div>
<div class="u-flex u-m-l-30" >
<div class="u-flex">
<div class="u-m-r-4">起始</div>
<el-input v-model="form.start" style="width: 57px;" placeholder="请输入起始值" >
</el-input>
</div>
<div
style="
background-color: #D9D9D9;
height: 1px;
width: 32px;
border-radius: 1px;
"
class="u-m-l-16 u-m-r-16"
></div>
<div class="u-flex">
<span class="u-m-r-4">结束</span>
<el-input v-model="form.end" style="width: 57px;" placeholder="请输入结束值">
</el-input>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="客座数">
<el-input-number
v-model="form.capacity"
:min="0"
controls-position="right"
></el-input-number>
</el-form-item>
<el-form-item label="清台管理">
<el-radio-group v-model="form.autoClear">
<el-radio-button :label="0">手动清台</el-radio-button>
<el-radio-button :label="1">自动清台</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="网络预定开关">
<el-switch
v-model="form.isPredate"
:active-value="1"
:inactive-value="2"
></el-switch>
</el-form-item>
<el-form-item label="类型">
<el-radio-group v-model="form.type">
<el-radio-button :label="0">低消</el-radio-button>
<el-radio-button :label="2">计时</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="最低消费" v-if="form.type == 0">
<el-input-number
v-model="form.amount"
:min="0"
controls-position="right"
></el-input-number>
</el-form-item>
<el-form-item label="每小时收费" v-if="form.type == 2">
<el-input-number
v-model="form.perhour"
:min="0"
controls-position="right"
></el-input-number>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="onSubmitHandle"
> </el-button
>
</span>
</el-dialog>
</template>
<script>
import { tbShopTable, tbShopAreaGet ,$fastCreateTable} from "@/api/table";
export default {
data() {
return {
dialogVisible: false,
resetForm: "",
loading: false,
status: [
{ value: "pending", name: "挂单中" },
{ value: "using", name: "开台中" },
{ value: "paying", name: "结算中" },
{ value: "idle", name: "空闲" },
{ value: "subscribe", name: "预定" },
{ value: "closed", name: "关台" },
// {value:'opening',name:'开台中'},
{ value: "cleaning", name: "台桌清理中" },
],
form: {
id: "",
areaId: "",
sign:'',
start:1,
end:10,
capacity: 0,
isPredate: 1,
type: 2,
perhour: 0,
amount: 0,
autoClear: 1,
},
rules: {
areaId: [
{
required: true,
message: "请选择区域",
trigger: ["blur","change"],
},
],
sign: [
{
required: true,
message: "请输入台桌标识",
trigger: ["blur","change"],
},
],
},
areaList: [],
};
},
mounted() {
this.resetForm = { ...this.form };
},
methods: {
//解决selectc值改变后未验证问题
selectChange($event, ref, type) {
this.$refs[ref][0].validateField(type)
},
onSubmitHandle() {
this.$refs.form.validate(async (valid) => {
if (valid) {
this.loading = true;
try {
let res = await $fastCreateTable(
{
...this.form,
qrcode: this.form.tableId,
shopId: localStorage.getItem("shopId"),
},
this.form.id ? "put" : "post"
);
this.$emit("success", res);
this.close();
this.$notify({
title: "成功",
message: `${this.form.id ? "编辑" : "添加"}成功`,
type: "success",
});
this.loading = false;
} catch (error) {
this.loading = false;
console.log(error);
}
}
});
},
show(obj) {
this.dialogVisible = true;
if (obj && obj.id) {
this.form = JSON.parse(JSON.stringify(obj));
}
},
close() {
this.dialogVisible = false;
},
reset() {
this.form = { ...this.resetForm };
},
// 获取区域
async tbShopAreaGet() {
try {
const { content } = await tbShopAreaGet({
shopId: localStorage.getItem("shopId"),
});
this.areaList = content;
} catch (error) {
console.log(error);
}
},
},
};
</script>

View File

@@ -0,0 +1,270 @@
<template>
<div class="select_desk">
<el-dialog width="410px" title="就餐人数" :visible.sync="show">
<div class="select_desk_dialog u-p-b-20">
<key-board
isCanEmpty
v-model="number"
@clear="clear"
:max="max"
:maxTips=" '最多'+max+'位'"
>
<div slot="clear">清空</div>
<div slot="input" class="u-p-l-20 u-p-r-20 u-flex w-full">
<el-input
placeholder="请输入就餐人数"
v-model="number"
@input="inputNumber"
@change="inputChange"
type="number"
>
<template slot="append"></template>
</el-input>
</div>
</key-board>
<div class="confirm_btns">
<el-button size="medium" @click="close">取消</el-button>
<el-button type="primary" size="medium" @click="confirm"
>确定</el-button
>
</div>
</div>
</el-dialog>
</div>
</template>
<script>
import keyBoard from "../keyboard.vue";
export default {
components: { keyBoard },
props:{
max:{
type:Number,
default:99
}
},
data() {
return {
number: "",
show: false,
hasOpen: false,
loading: false,
};
},
watch: {
number(newval) {
if (newval >this.max) {
this.number = this.max;
this.$message("最多只能选择"+this.max+"位就餐人数");
}
// 使用正则表达式匹配正整数
const regex = /^[1-9]\d*$/;
// 如果输入的值不是正整数,则将其设置为上一个有效值
if (!regex.test(newval)) {
this.number = newval.substring(0, newval.length - 1);
}
},
},
methods: {
inputNumber(e) {},
inputChange(e) {},
clear(e) {
console.log(e);
this.number = "";
},
confirm() {
if (this.number >this.max) {
return this.$message("最多只能选择"+this.max+"位就餐人数");
}
if (!this.number) {
return this.$message("请选择就餐人数");
}
console.log(this.number)
this.$emit("confirm", this.number);
this.close();
},
open(number) {
this.number = number || "";
this.show = true;
},
close() {
this.show = false;
this.number = "";
},
},
mounted() {},
};
</script>
<style lang="scss" scoped>
::v-deep.el-button {
padding: 12px 20px;
}
::v-deep .el-input__inner::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
::v-deep .el-input__inner::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
::v-deep .el-button--success {
border-color: #22bf64;
background-color: #22bf64;
}
.select_desk .btn {
height: 34px;
}
.tags {
font-size: 16px;
&.using {
color: rgb(234, 64, 37);
}
&.wait {
color: rgb(252, 236, 79);
}
&.idle {
color: rgb(137, 234, 71);
}
&.closed {
color: rgb(221, 221, 221);
filter: grayscale(1);
}
}
::v-deep .inputs .el-input__inner {
border-color: transparent !important;
color: rgba(0, 0, 0, 0.8);
letter-spacing: 1.25px;
font-size: 20px;
}
.select_desk .select_desk_dialog {
display: flex;
flex-direction: column;
align-items: center;
}
.select_desk .select_desk_dialog .nav {
width: 286px;
height: 38px;
background: #dcf0e8;
justify-content: space-around;
}
.select_desk .select_desk_dialog .nav .li,
.select_desk .select_desk_dialog .nav {
border-radius: 4px;
display: flex;
align-items: center;
}
.select_desk .select_desk_dialog .nav .li {
width: 140px;
height: 34px;
color: #0fc161;
justify-content: center;
font-size: 14px;
cursor: pointer;
}
.select_desk .select_desk_dialog .nav .lion {
background: #0fc161;
color: #fff;
}
.select_desk .select_desk_dialog .inputs {
width: 370px;
line-height: 54px;
margin-top: 24px;
height: 54px;
margin-bottom: 20px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
color: rgba(0, 0, 0, 0.8);
letter-spacing: 1.25px;
text-align: center;
font-size: 20px;
position: relative;
}
.select_desk .select_desk_dialog .inputs .close {
color: #aaa;
position: absolute;
right: 10px;
height: 30px;
width: 30px;
line-height: 30px;
top: 50%;
margin-top: -15px;
cursor: pointer;
}
.select_desk .select_desk_dialog .keyboard {
display: flex;
flex-wrap: wrap;
width: 100%;
margin-top: 20px;
margin-bottom: 10px;
border-right: 1px solid #dcdfe6;
border-bottom: 1px solid #dcdfe6;
}
.select_desk .select_desk_dialog .keyboard .li {
height: 60px;
width: 33.333%;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
color: #212121;
cursor: pointer;
user-select: none;
border-left: 1px solid #dcdfe6;
border-top: 1px solid #dcdfe6;
transition: all 0.1s;
}
.select_desk .select_desk_dialog .keyboard .li:hover {
background: #dcdfe6;
}
.select_desk .select_desk_dialog .keyboard .li .icon {
font-size: 1.3em;
}
.select_desk .select_desk_dialog .keyboard .confirm {
height: 140px;
background: #ff9f2e;
position: absolute;
bottom: 0;
right: 0;
border-right: none;
}
.select_desk .select_desk_dialog .keyboard .confirm:hover {
background: #f88502;
}
.confirm_btns {
display: flex;
justify-content: space-between;
width: 100%;
}
.confirm_btns .el-button {
width: 175px;
}
</style>

View File

@@ -0,0 +1,289 @@
<template>
<el-dialog title="选择用户" width="850px" :visible.sync="show">
<div class="app-container">
<div class="head-container">
<el-form :model="query" inline>
<el-form-item label="">
<el-input
v-model="query.name"
placeholder="请输入昵称或手机号"
></el-input>
</el-form-item>
<!-- <el-form-item label="是否为会员">
<el-select v-model="query.isVip" placeholder="是否是会员">
<el-option value="" label="全部"></el-option>
<el-option :value="1" label="是"></el-option>
<el-option :value="0" label="否"></el-option>
</el-select>
</el-form-item> -->
<el-form-item>
<div class="flex gap-20">
<el-button type="primary" @click="getTableData" size="medium"
>搜索</el-button
>
<!-- <el-button @click="resetHandle" size="medium">重置</el-button> -->
<el-button @click="noChooseUser" size="medium"
>不选择用户</el-button
>
<!-- <el-button @click="resetHandle" size="medium">新建用户</el-button> -->
</div>
</el-form-item>
</el-form>
</div>
<!-- <div class="head-container">
<el-button type="primary" icon="el-icon-plus" @click="$refs.addActive.show()">
添加活动
</el-button>
</div> -->
<div class="head-container">
<el-table :data="tableData.data" v-loading="tableData.loading" @cell-click="cellClick">
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="用户" prop="headImg" width="200px">
<template v-slot="scope">
<div class="user_info">
<el-image
:src="scope.row.headImg"
style="width: 40px; height: 40px; flex-shrink: 0"
>
<div slot="error" class="image-slot">
<i class="el-icon-user"></i>
</div>
</el-image>
<span class="name">{{ scope.row.nickName }}</span>
</div>
</template>
</el-table-column>
<el-table-column
label="手机号"
prop="telephone"
width="160"
></el-table-column>
<!-- <el-table-column label="性别" prop="sex">
<template v-slot="scope">
<el-tag type="priamry">{{ scope.row.sex || "未知" }}</el-tag>
</template>
</el-table-column> -->
<el-table-column label="会员" prop="isVip">
<template v-slot="scope">
<el-tag type="warning" v-if="scope.row.isVip"
>会员等级{{ scope.row.isVip }}</el-tag
>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="余额" prop="amount"></el-table-column>
<el-table-column label="积分" prop="totalScore"></el-table-column>
<el-table-column label="操作" width="90" fixed="right">
<template v-slot="scope">
<el-button type="primary" size="mini" @click="choose(scope.row)">选择</el-button>
<!-- <el-button type="text" @click="charge(scope.row)">充值</el-button> -->
</template>
</el-table-column>
</el-table>
</div>
<div class="head-container">
<el-pagination
:total="tableData.total"
:current-page="tableData.page + 1"
:page-size="tableData.size"
@size-change="sizeChange"
@current-change="paginationChange"
layout="total, sizes, prev, pager, next, jumper"
></el-pagination>
</div>
</div>
</el-dialog>
</template>
<script>
import { queryAllShopUser, queryAllShopInfo } from "@/api/shop";
import dayjs from "dayjs";
let cacheData = {};
export default {
data() {
return {
show: false,
query: {
name: "",
},
shopInfo: {
balanceTotal: 0,
userTotal: 0,
chageTotal: 0,
},
tableData: {
data: [],
page: 0,
size: 10,
loading: false,
total: 0,
},
};
},
filters: {
timeFilter(s) {
return dayjs(s).format("YYYY-MM-DD HH:mm:ss");
},
},
mounted() {
// this.getTableData();
},
methods: {
cellClick(user){
console.log(user)
this.$emit("chooseUser",user)
this.close()
},
noChooseUser(){
this.$emit("chooseUser",null)
this.close()
},
choose(user) {
this.$emit("chooseUser",user)
this.close()
},
charge(user) {
console.log(user);
},
close(){
this.show = false;
},
open() {
this.getTableData();
this.show = true;
},
toPage(type) {
const pages = {
charge: "charge_list",
cost: "cost_list",
};
this.$router.push({
name: pages[type],
});
console.log(pages[type]);
},
// 获取商家用户概述信息
async getShopInfo() {
try {
const res = await queryAllShopInfo(this.query);
this.shopInfo = res;
} catch (error) {
console.log(error);
}
},
sizeChange() {
this.tableData.page = 0;
this.getTableData();
},
// 切换状态
async statusChange(e, row) {
try {
this.tableData.loading = true;
const data = { ...row };
data.status = e;
await modityActivate(data);
this.getTableData();
} catch (error) {
console.log(error);
this.tableData.loading = false;
}
},
// 重置查询
resetHandle() {
this.query.name = "";
this.getTableData();
},
// 分页回调
paginationChange(e) {
this.tableData.page = e - 1;
this.getTableData();
},
// 获取商品列表
async getTableData() {
this.tableData.loading = true;
try {
const res = await queryAllShopUser({
...this.query,
size: this.tableData.size,
page: this.tableData.page + 1,
});
this.tableData.loading = false;
this.tableData.data = res.content;
this.tableData.total = res.totalElements;
} catch (error) {
console.log(error);
}
},
},
};
</script>
<style scoped lang="scss">
.user_info {
display: flex;
align-items: center;
.name {
margin-left: 10px;
}
}
::v-deep .el-input--small .el-input__inner{
height: 36px;
line-height: 36px;
}
::v-deep .image-slot {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #efefef;
font-size: 20px;
color: #999;
}
.card {
background-color: #f5f5f5;
padding: 0 14px;
.title {
font-size: 22px;
padding-top: 14px;
}
.row {
display: flex;
padding: 20px 0;
.item {
flex: 1;
.t {
text-align: center;
color: #555;
}
.n {
color: #000;
font-size: 20px;
font-weight: bold;
padding-top: 6px;
text-align: center;
}
}
}
}
.flex{
display: flex;
align-items: center;
}
.gap-20{
gap: 20px;
}
</style>

View File

@@ -0,0 +1,69 @@
<template>
<el-dialog title="下载桌码" width="400px" :visible.sync="dialogVisible" @open="reset">
<el-form ref="form" :model="form" label-position="left">
<el-form-item label="下载数量">
<el-input-number v-model="form.number" :min="1" :max="500"></el-input-number>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="onSubmitHandle"> </el-button>
</span>
</el-dialog>
</template>
<script>
import { downloadFile } from "@/utils/index";
import { downloadTableCode } from '@/api/table'
export default {
props: {
total: {
type: [Number, String],
default: 1
}
},
data() {
return {
dialogVisible: false,
loading: false,
form: {
number: 1
},
resetForm: ''
}
},
mounted() {
this.resetForm = { ...this.form }
},
methods: {
async onSubmitHandle() {
try {
this.loading = true
const file = await downloadTableCode({
count: this.form.number,
shopId: localStorage.getItem('shopId')
})
this.loading = false
this.dialogVisible = false
this.$message.success('下载成功')
downloadFile(file, "桌码", "zip");
} catch (error) {
this.loading = false
console.log(error);
}
},
show(obj) {
this.dialogVisible = true
// if (obj && obj.id) {
// this.form = JSON.parse(JSON.stringify(obj))
// }
},
close() {
this.dialogVisible = false
},
reset() {
this.form = { ...this.resetForm }
}
}
}
</script>

View File

@@ -0,0 +1,319 @@
<template>
<div class="simple-Keyboard-number">
<div class="carts">
<div class="box_status">
<slot name="input">
<span> {{ number }}</span>
</slot>
</div>
<div class="number_list_box">
<div class="yd-keyboard">
<div class="mini-number-box1">
<div class="mini-number">
<div class="key-line">
<div class="key" @click="keyboradAdd('1')">1</div>
<div class="key" @click="keyboradAdd('2')">2</div>
<div class="key" @click="keyboradAdd('3')">3</div>
</div>
<div class="key-line">
<div class="key" @click="keyboradAdd('4')">4</div>
<div class="key" @click="keyboradAdd('5')">5</div>
<div class="key" @click="keyboradAdd('6')">6</div>
</div>
<div class="key-line">
<div class="key" @click="keyboradAdd('7')">7</div>
<div class="key" @click="keyboradAdd('8')">8</div>
<div class="key" @click="keyboradAdd('9')">9</div>
</div>
<div class="key-line">
<div class="key" @click="clearFunction">
<slot name="clear"> </slot>
</div>
<div class="key" @click="keyboradAdd('0')">0</div>
<div
class="key"
style="font-size: 31px"
@click="keyboradReduce"
>
<svg
t="1723453480343"
class="icon"
viewBox="0 0 1664 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1467"
width="32"
height="32"
>
<path
d="M1526.08 1.6H459.84L41.28 416c-53.76 53.248-53.76 139.52 0 192.64l418.624 414.592v-0.064h1066.176a136.96 136.96 0 0 0 137.6-136.256V137.792a136.96 136.96 0 0 0-137.6-136.192z m-331.392 631.168c26.816 26.624 26.816 69.76 0 96.384-26.88 26.56-70.4 26.56-97.28 0l-121.28-120.128-123.328 122.112a69.76 69.76 0 0 1-97.92 0 68.096 68.096 0 0 1 0-96.96L878.208 512l-121.28-120.064a67.648 67.648 0 0 1 0-96.32c26.88-26.624 70.4-26.624 97.28 0l121.216 120.064 122.24-120.96a69.696 69.696 0 0 1 97.92 0 68.032 68.032 0 0 1 0 96.96l-122.24 120.96 121.344 120.064z"
fill="#333333"
p-id="1468"
></path>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
<template v-if="showConfirm">
<div class="submit" @click="keyboradConfirm">确认</div>
</template>
</div>
</div>
</template>
<script>
export default {
props: {
isCanEmpty: {
type: Boolean,
default: false,
},
maxTips: {
type: String,
default: "输入值超范围",
},
showConfirm: {
type: Boolean,
default: false,
},
max: {
type: Number,
default: Infinity,
},
value: {
type: [String, Number],
default: 0,
},
},
data() {
return {
number: 0,
};
},
watch: {
value(newval) {
this.number = newval;
},
number(newval) {
this.$emit("input", newval);
},
},
methods: {
clearFunction() {
this.$emit("clear", this.number);
},
keyboradAdd(n) {
if (Number(this.number) == 0) {
return (this.number = n);
}
const newval = this.number + n;
if (newval > this.max) {
return this.$message( this.maxTips);
}
this.number = newval;
},
keyboradReduce() {
if (this.number.length <= 1) {
return (this.number = this.isCanEmpty ? "" : "0");
}
this.number = `${this.number}`.substring(0, this.number.length - 1);
},
keyboradConfirm() {
this.$emit("confirm", this.number);
},
},
mounted() {
this.number = `${this.value}`;
},
};
</script>
<style lang="scss" scoped>
.yd-keyboard {
justify-content: center;
margin-bottom: 20px;
}
.mini-number-box1 .mini-number,
.yd-keyboard {
display: flex;
flex-direction: column;
}
.mini-number-box1 .mini-number {
border: 1px solid #dcdfe6;
}
.mini-number-box1 .mini-number .key-line {
margin-top: 0;
}
.mini-number-box1 .mini-number .key {
width: 122px;
height: 60px;
-webkit-box-shadow: none;
box-shadow: none;
border-radius: 0;
margin-left: 0;
}
.mini-number-box1 .mini-number .key:hover {
background-color: #dcdfe6;
}
.mini-number-box1 .mini-number .key:not(:last-child) {
border-right: 1px solid #dcdfe6;
}
.mini-number-box1 .mini-number .key-line:not(:last-child) {
border-bottom: 1px solid #dcdfe6;
}
.mini-number-box2 {
display: flex;
justify-content: center;
}
.mini-number-box2 .key {
width: 80px;
height: 70px;
}
.mini-number-box2 .function-button {
display: flex;
flex-direction: column;
}
.mini-number-box2 .function-button .key {
margin-top: 8px;
}
.key-line {
margin-top: 8px;
}
.key,
.key-line {
display: flex;
justify-content: center;
}
.key {
width: 64px;
height: 64px;
background: #fff;
-webkit-box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.10196078431372549);
border-radius: 4px;
align-items: center;
margin-left: 8px;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 20px;
color: rgba(0, 0, 0, 0.8);
text-align: center;
line-height: 34px;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.10196078431372549);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.simple-Keyboard-number,
.simple-Keyboard-weight {
min-width: 410px;
background: #fff;
border-radius: 4px;
overflow: hidden;
}
.submit {
width: 366px;
height: 44px;
background: #22bf64;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #fff;
font-family: PingFangSC-Regular;
font-weight: 400;
margin-bottom: 40px;
cursor: pointer;
}
.disabled-box {
position: absolute;
inset: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.carts {
flex-direction: column;
background: #fff;
height: 100%;
}
.carts .title,
.carts {
display: flex;
align-items: center;
width: 100%;
}
.carts .title {
justify-content: space-between;
height: 64px;
border-bottom: 1px solid #ebebeb;
padding: 20px;
padding-top: 30px;
}
.carts .title .left {
font-family: PingFangSC-Medium;
font-weight: 500;
font-size: 18px;
color: #000;
}
.carts .title .right {
font-size: 14.4px;
cursor: pointer;
}
.carts .box_status {
margin-bottom: 20px;
width: 370px;
height: 58px;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
font-family: MicrosoftYaHei;
font-size: 20px;
color: rgba(0, 0, 0, 0.8);
letter-spacing: 1.25px;
text-align: center;
}
.carts .box_status span {
padding: 0 5px;
}
</style>

View File

@@ -0,0 +1,339 @@
<template>
<el-dialog width="330px" :title="title" :visible.sync="show">
<div class="simple-Keyboard-number">
<div class="carts">
<div class="pad-14">
<div class="box_status">
<span class="sym"></span>
<el-input v-model="number" type="text" @input="numberInput"></el-input>
<!-- <span class="inputs" :contenteditable="true" @input="numberInput" type="text" >{{ number }}</span> -->
</div>
</div>
<div class="number_list_box">
<div class="yd-keyboard">
<div class="mini-number-box1">
<div class="mini-number">
<div class="key-line">
<div class="key" @click="keyboradAdd('1')">1</div>
<div class="key" @click="keyboradAdd('2')">2</div>
<div class="key" @click="keyboradAdd('3')">3</div>
<div
class="key"
style="font-size: 31px"
@click="keyboradReduce"
>
<svg
t="1723453480343"
class="icon"
viewBox="0 0 1664 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1467"
width="32"
height="32"
>
<path
d="M1526.08 1.6H459.84L41.28 416c-53.76 53.248-53.76 139.52 0 192.64l418.624 414.592v-0.064h1066.176a136.96 136.96 0 0 0 137.6-136.256V137.792a136.96 136.96 0 0 0-137.6-136.192z m-331.392 631.168c26.816 26.624 26.816 69.76 0 96.384-26.88 26.56-70.4 26.56-97.28 0l-121.28-120.128-123.328 122.112a69.76 69.76 0 0 1-97.92 0 68.096 68.096 0 0 1 0-96.96L878.208 512l-121.28-120.064a67.648 67.648 0 0 1 0-96.32c26.88-26.624 70.4-26.624 97.28 0l121.216 120.064 122.24-120.96a69.696 69.696 0 0 1 97.92 0 68.032 68.032 0 0 1 0 96.96l-122.24 120.96 121.344 120.064z"
fill="#333333"
p-id="1468"
></path>
</svg>
</div>
</div>
<div class="key-line">
<div class="key" @click="keyboradAdd('4')">4</div>
<div class="key" @click="keyboradAdd('5')">5</div>
<div class="key" @click="keyboradAdd('6')">6</div>
<div class="key" @click="keyboradAdd('clear')">清空</div>
</div>
<div class="key-line">
<div class="key" @click="keyboradAdd('7')">7</div>
<div class="key" @click="keyboradAdd('8')">8</div>
<div class="key" @click="keyboradAdd('9')">9</div>
<div class="key"></div>
</div>
<div class="key-line">
<div class="key" @click="keyboradAdd('.')">.</div>
<div class="key" @click="keyboradAdd('0')">0</div>
<div class="key" @click="keyboradAdd('00')">00</div>
<div class="key"></div>
</div>
<div class="confirm key" @click="confirm">确认</div>
</div>
</div>
</div>
</div>
<!-- <div class="submit" @click="keyboradConfirm">确认</div> -->
</div>
</div>
</el-dialog>
</template>
<script>
export default {
props: {
title: {
type: String,
default: "支付",
},
value: {
type: [String, Number],
default: 0,
},
},
data() {
return {
number: '0',
show:false
};
},
watch: {
number(newval) {
this.$emit("input", newval);
},
},
methods: {
confirm(){
},
open(){
this.show=true
},
close(){
this.show=false
},
numberInput(val){
console.log(val)
this.number=`${Number(val)}`
},
keyboradAdd(n) {
if (n === "clear") {
return (this.number = '0');
}
if(n==='.'){
return this.number += this.number.includes('.')?'' : '.'
}
if (`${this.number}`.length<=1&&Number(this.number)===0) {
return (this.number = n);
}
this.number += n;
},
keyboradReduce() {
if (this.number.length <= 1) {
return (this.number = this.isCanEmpty ? "" : "0");
}
this.number = this.number.substring(0, this.number.length - 1);
},
keyboradConfirm() {
this.$emit("confirm", this.number);
},
},
mounted() {
this.number = `${this.value}`;
},
};
</script>
<style lang="scss" scoped>
.pad-14 {
padding: 14px;
width: 100%;
}
::v-deep .el-dialog__body {
padding: 0;
}
::v-deep .el-input__inner::-webkit-outer-spin-button{
-webkit-appearance: none;
margin: 0;
}
::v-deep .el-input__inner::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
::v-deep .el-input__inner {
-moz-appearance: textfield;
}
.number_list_box,
.yd-keyboard,
.mini-number,
.mini-number-box1,
.key-line {
width: 100%;
}
.yd-keyboard {
justify-content: center;
}
.mini-number-box1 .mini-number,
.yd-keyboard {
display: flex;
position: relative;
flex-direction: column;
}
.mini-number-box1 .mini-number {
border-top: 1px solid #dcdfe6;
}
.mini-number-box2 {
display: flex;
justify-content: center;
}
.mini-number-box2 .function-button {
display: flex;
flex-direction: column;
}
.key,
.key-line {
display: flex;
justify-content: center;
}
.key {
width: 25%;
height: 70px;
box-sizing: border-box;
background: #fff;
align-items: center;
font-family: PingFangSC-Regular;
font-weight: 400;
font-size: 24px;
color: #212121;
text-align: center;
line-height: 34px;
cursor: pointer;
user-select: none;
}
.key:hover {
background-color: #dcdfe6;
}
.key:not(:last-child) {
border-right: 1px solid #dcdfe6;
}
.key-line:not(:last-child) {
border-bottom: 1px solid #dcdfe6;
}
.confirm {
height: 142px;
width: 25%;
background: #22bf64;
color: #fff;
position: absolute;
bottom: 0;
right: 0;
border-right: none;
}
.confirm:hover {
background: rgba(34, 191, 100, 0.8666666666666667);
}
.simple-Keyboard-number,
.simple-Keyboard-weight {
background: #fff;
border-radius: 4px;
overflow: hidden;
}
.submit {
width: 366px;
height: 44px;
background: #22bf64;
border-radius: 2px;
display: flex;
justify-content: center;
align-items: center;
font-size: 14px;
color: #fff;
font-family: PingFangSC-Regular;
font-weight: 400;
margin-bottom: 40px;
cursor: pointer;
}
.disabled-box {
position: absolute;
inset: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.carts {
flex-direction: column;
background: #fff;
height: 100%;
}
.carts .title,
.carts {
display: flex;
align-items: center;
width: 100%;
}
.carts .title {
justify-content: space-between;
height: 64px;
border-bottom: 1px solid #ebebeb;
padding: 20px;
padding-top: 30px;
}
.carts .title .left {
font-family: PingFangSC-Medium;
font-weight: 500;
font-size: 18px;
color: #000;
}
.carts .title .right {
font-size: 14.4px;
cursor: pointer;
}
.box_status {
box-sizing: border-box;
width: 100%;
background: #fff;
border: 1px solid #dcdfe6;
border-radius: 4px;
display: flex;
padding: 14px;
align-items: flex-end;
font-family: MicrosoftYaHei;
font-size: 20px;
color: rgba(0, 0, 0, 0.8);
letter-spacing: 1.25px;
.sym {
font-size: 24px;
color: #212121;
width: 30px;
margin-bottom: 3px;
}
.inputs {
font-size: 28px;
color: #212121;
outline: none;
line-height: 37px;
border: none;
max-width: 250px;
}
}
::v-deep .el-input--small .el-input__inner{
font-size: 28px;
color: #212121;
outline: none;
line-height: 37px;
border: none;
max-width: 250px;
padding-left: 0;
}
.carts .box_status span {
padding: 0 5px;
}
</style>

View File

@@ -0,0 +1,62 @@
export const orderBtns=[
{
text: "删除",
disabled: false,
},
{
text: "规格",
disabled: true,
},
{
text: "菜品打折",
disabled: false,
},
{
text: "赠菜",
disabled: false,
},
{
text: "赠菜",
disabled: false,
},
{
text: "打包",
disabled: false,
},
{
text: "等叫",
disabled: false,
},
{
text: "整单等叫",
disabled: false,
},
{
text: "单品备注",
disabled: false,
},
{
text: "退菜",
disabled: false,
},
{
text: "附加费",
disabled: false,
},
{
text: "存单",
disabled: false,
},
{
text: "取单",
disabled: false,
},
{
text: "修改价格",
disabled: false,
},
{
text: "撤单",
disabled: false,
}
]

View File

@@ -0,0 +1,76 @@
<template>
<div>
<div class="title">选择支付方式</div>
<div class="btn_group">
<div class="price_select">
<div class="pay_btns">
<el-button
size="medium"
@click="changeSel(item)"
v-for="(item, index) in list"
:key="index"
:type="sel === item.payType ? 'primary' : ''"
>
{{ item.payName }}
</el-button>
<!-- <el-button size="medium"> 余额支付 </el-button>
<el-button size="medium"> 现金支付 </el-button>
<el-button size="medium"> 挂账 </el-button> -->
</div>
</div>
</div>
</div>
</template>
<script>
import { $getPayType } from "@/api/table";
export default {
props: {
value: {
type: [String, Number],
default: '',
},
},
data() {
return {
list: [],
sel: "",
};
},
watch: {
sel(newval) {
console.log(newval);
this.$emit("input", newval);
},
},
methods: {
changeSel(item) {
this.sel = item.payType;
this.$emit("itemClick", item);
},
async init() {
const res = await $getPayType();
this.list = res.filter(v=>v.isDisplay);
console.log(res[0]);
this.sel = this.sel ? this.sel : res[0].payType;
},
},
mounted() {
this.init();
},
};
</script>
<style lang="scss" scoped>
.title {
font-size: 18px;
color: #000;
font-weight: 600;
padding: 20px 0;
}
.pay_btns .el-button {
margin-left: 0;
margin-right: 10px;
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,269 @@
<template>
<el-dialog
title="预约"
:visible.sync="dialogVisible"
width="500px"
@close="reset"
>
<el-form
ref="form"
:model="form"
:rules="rules"
label-width="120px"
label-position="left"
>
<el-form-item label="预约日期" prop="bookingDate">
<!-- <el-date-picker
v-model="form.bookingDate"
type="date"
value-format="yyyy-MM-dd"
:picker-options="pickerOptions"
placeholder="选择预约日期"
style="width: 200px;margin-right: 10px;"
@change="getShopTableList"
/> -->
<el-radio-group
v-model="form.bookingDate"
class="date"
@change="getShopTableList"
>
<el-radio-button
v-for="(item) in dateList"
:key="item.date"
:label="item.date"
>
<div style="font-size: 12px;margin-bottom: 4px;">{{ item.label }} / {{ item.date.substring(8,10) }}</div>
<div style="font-size: 12px;"> {{ item.day }}</div>
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="预约类型" prop="bookingType">
<el-radio-group v-model="form.bookingType" @change="getShopTableList">
<el-radio-button label="lunch">午餐</el-radio-button>
<el-radio-button label="dinner">晚餐</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="就餐时间" prop="bookingTime">
<el-time-picker
v-model="form.bookingTime"
value-format="HH:mm:ss"
placeholder="选择时间"
style="width: 100%;"
/>
</el-form-item>
<el-form-item label="预约桌台" prop="shopTableId">
<el-select v-model="form.shopTableId" placeholder="请选择" style="width: 100%;">
<el-option
v-for="item in shopTableList"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="用餐人数" prop="dinerNum">
<el-input-number v-model="form.dinerNum" :min="1" controls-position="right" style="width: 100%;"/>
</el-form-item>
<el-form-item label="订餐人" prop="bookingPerson">
<el-input v-model="form.bookingPerson" style="width: 200px;margin-right: 10px;" placeholder="请输入联系人姓名" />
<el-radio-group v-model="form.gender">
<el-radio-button label="1">先生</el-radio-button>
<el-radio-button label="2">女士</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="联系电话" prop="phoneNumber">
<el-input v-model="form.phoneNumber" oninput="value= value.replace(/[^0-9]/g, '')" maxlength="11" placeholder="请输入联系电话" />
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button
type="primary"
:loading="loading"
@click="onSubmitHandle"
> </el-button>
</span>
</el-dialog>
</template>
<script>
import { getShopTableList, makeShopTable } from '@/api/table'
import dayjs from 'dayjs'
export default {
data() {
return {
dialogVisible: false,
resetForm: '',
loading: false,
pickerOptions: {
disabledDate(time) {
return time.getTime() < Date.now() - 8.64e7 // 如果今天之前的日期都不可选减去8.64e7是1天的毫秒数
}
},
dateList: [
{ label: '今', day: '', date: '' },
{ label: '明', day: '', date: '' },
{ label: '后', day: '', date: '' }
],
shopTableList: [],
form: {
bookingDate: '',
bookingType: 'lunch',
shopTableId: null,
dinerNum: 1,
bookingPerson: '',
phoneNumber: '',
gender: '1',
bookingTime: '',
diningType: '普通用餐',
focus: '0',
receiveMarketingSms: '0'
},
rules: {
shopTableId: [
{
required: true,
message: '请选择预约桌台',
trigger: ['blur']
}
],
dinerNum: [
{
required: true,
message: '请选择用餐人数',
trigger: ['blur']
}
],
phoneNumber: [
{
required: true,
message: '请输入联系方式',
trigger: ['blur']
}
],
bookingPerson: [
{
required: true,
message: '请输入订餐人姓名',
trigger: ['blur']
}
],
bookingTime: [
{
required: true,
message: '请选择用餐时间',
trigger: ['change']
}
]
}
}
},
mounted() {
this.resetForm = { ...this.form }
},
methods: {
getWeekdays() {
const today = new Date()
const tomorrow = new Date(today)
const dayAfterTomorrow = new Date(today)
// 设置时间为今天
tomorrow.setDate(today.getDate() + 1)
dayAfterTomorrow.setDate(today.getDate() + 2)
// 获取星期几的方法
const getDayOfWeek = (date) => {
const days = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
return days[date.getDay()]
}
// 获取今天、明天和后天的星期几
const todayWeekday = getDayOfWeek(today)
const tomorrowWeekday = getDayOfWeek(tomorrow)
const dayAfterTomorrowWeekday = getDayOfWeek(dayAfterTomorrow)
this.dateList[0].day = todayWeekday
this.dateList[1].day = tomorrowWeekday
this.dateList[2].day = dayAfterTomorrowWeekday
this.dateList[0].date = dayjs(today).format('YYYY-MM-DD')
this.dateList[1].date = dayjs(tomorrow).format('YYYY-MM-DD')
this.dateList[2].date = dayjs(dayAfterTomorrow).format('YYYY-MM-DD')
this.form.bookingDate = this.dateList[0].date
},
/**
* 获取桌台数据
*/
async getShopTableList() {
this.form.shopTableId = null
const params = {
bookingDate: this.form.bookingDate,
bookingType: this.form.bookingType
}
const res = await getShopTableList(params)
this.shopTableList = res
console.log(res)
},
onSubmitHandle() {
this.$refs.form.validate(async(valid) => {
if (valid) {
this.loading = true
try {
const bookingTime = this.form.bookingDate + ' ' + this.form.bookingTime
const params = {
...this.form,
shopId: localStorage.getItem('shopId')
}
params.bookingTime = bookingTime
const res = await makeShopTable(params)
this.$emit('success', res)
this.close()
this.$notify({
title: '成功',
message: `预约成功`,
type: 'success'
})
this.loading = false
} catch (error) {
this.loading = false
console.log(error)
}
}
})
},
show(obj) {
this.dialogVisible = true
if (obj && obj.id) {
this.form = JSON.parse(JSON.stringify(obj))
}
this.getShopTableList()
this.getWeekdays()
},
close() {
this.dialogVisible = false
},
reset() {
this.form = { ...this.resetForm }
}
}
}
</script>
<style scoped lang="scss">
.date{
::v-deep .el-form-item__content{
// height: 36px;
// display: flex;
// justify-content: space-between;
}
::v-deep .el-radio-button{
height: 100%;
}
::v-deep .el-radio-button__inner{
padding: 4px 20px;
height: 100%;
}
}
</style>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,141 @@
<template>
<el-dialog :title="form.id ? '编辑台桌' : '添加台桌'" :visible.sync="dialogVisible" @open="tbShopAreaGet" @close="reset">
<el-form ref="form" :model="form" :rules="rules" label-width="120px" label-position="left">
<el-form-item label="选择区域" prop="areaId">
<el-select v-model="form.areaId" placeholder="请选择区域">
<el-option :label="item.name" :value="item.id" v-for="item in areaList" :key="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item label="台桌状态" prop="status" v-if="form.id">
<el-select v-model="form.status" placeholder="请选择台桌状态">
<el-option :label="item.label" :value="item.value" v-for="item in status" :key="item.value"></el-option>
</el-select>
</el-form-item>
<el-form-item label="台桌名称">
<el-input v-model="form.name" placeholder="请输入台桌名称"></el-input>
</el-form-item>
<el-form-item label="客座数">
<el-input-number v-model="form.maxCapacity" :min="0" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="清台管理">
<el-radio-group v-model="form.autoClear">
<el-radio-button :label="0">手动清台</el-radio-button>
<el-radio-button :label="1">自动清台</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="网络预定开关">
<el-switch v-model="form.isPredate" :active-value="1" :inactive-value="2"></el-switch>
</el-form-item>
<el-form-item label="类型">
<el-radio-group v-model="form.type">
<el-radio-button :label="0">低消</el-radio-button>
<el-radio-button :label="2">计时</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="最低消费" v-if="form.type == 0">
<el-input-number v-model="form.amount" :min="0" controls-position="right"></el-input-number>
</el-form-item>
<el-form-item label="每小时收费" v-if="form.type == 2">
<el-input-number v-model="form.perhour" :min="0" controls-position="right"></el-input-number>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible = false"> </el-button>
<el-button type="primary" :loading="loading" @click="onSubmitHandle"> </el-button>
</span>
</el-dialog>
</template>
<script>
import { tbShopTable, tbShopAreaGet } from '@/api/table'
import $status from "../status.js";
export default {
data() {
return {
dialogVisible: false,
resetForm: '',
loading: false,
status:[],
form: {
id: '',
name: '',
areaId: '',
maxCapacity: 0,
isPredate: 1,
type: 2,
perhour: 0,
amount: 0,
autoClear: 1,
},
rules: {
areaId: [
{
required: true,
message: '请选择区域',
trigger: 'blur'
}
]
},
areaList: []
}
},
mounted() {
this.resetForm = { ...this.form }
this.status = Object.keys($status).map(key =>{
return {...$status[key],value:key}
});
},
methods: {
onSubmitHandle() {
this.$refs.form.validate(async valid => {
if (valid) {
this.loading = true
try {
let res = await tbShopTable({
...this.form,
qrcode:this.form.tableId,
shopId: localStorage.getItem('shopId')
}, this.form.id ? 'put' : 'post')
this.$emit('success', res)
this.close()
this.$notify({
title: '成功',
message: `${this.form.id ? '编辑' : '添加'}成功`,
type: 'success'
});
this.loading = false
} catch (error) {
this.loading = false
console.log(error)
}
}
})
},
show(obj) {
this.dialogVisible = true
if (obj && obj.id) {
this.form = JSON.parse(JSON.stringify(obj))
}
},
close() {
this.dialogVisible = false
},
reset() {
this.form = { ...this.resetForm }
},
// 获取区域
async tbShopAreaGet() {
try {
const { content } = await tbShopAreaGet({
shopId: localStorage.getItem('shopId')
})
this.areaList = content
} catch (error) {
console.log(error);
}
}
}
}
</script>

View File

@@ -0,0 +1,47 @@
//判断商品是否可以下单
export function isCanBuy(goods,isStock) {
return goods.isGrounding && goods.isPauseSale == 0 && (isStock?goods.stockNumber > 0:true) ;
}
// 一个数组是否包含另外一个数组全部元素
export function arrayContainsAll(arr1, arr2) {
for (let i = 0; i < arr2.length; i++) {
if (!arr1.includes(arr2[i])) {
return false;
}
}
return true;
}
//n项 n-1项组合生成全部结果
export function generateCombinations(arr, k) {
let result = [];
function helper(index, current) {
if (current.length === k) {
result.push(current.slice()); // 使用slice()来避免直接修改原始数组
} else {
for (let i = index; i < arr.length; i++) {
current.push(arr[i]); // 将当前元素添加到组合中
helper(i + 1, current); // 递归调用,索引增加以避免重复选择相同的元素
current.pop(); // 回溯,移除当前元素以便尝试其他组合
}
}
}
helper(0, []); // 从索引0开始初始空数组作为起点
return result;
}
export function returnReverseVal(val, isReturnString = true) {
const isBol = typeof val === "boolean";
const isString = typeof val === "string";
let reverseNewval = "";
if (isBol) {
reverseNewval = !val;
}
if (isString) {
reverseNewval = val === "true" ? "false" : "true";
}
return reverseNewval;
}

View File

@@ -0,0 +1 @@
<template></template>

View File

@@ -0,0 +1,10 @@
export default {
pending: { label: '挂单中', type: '#E6A23C' },
cleaning: { label: '待清台', type: '#FAAD14' },
using: { label: '开台中', type: '#FF4D4F' },
idle: { label: '空闲', type: '#3F9EFF' },
paying: { label: '结算中', type: '#E6A23C' },
closed: { label: '关台', type: '#DDDDDD' },
subscribe: { label: '预约', type: '#52C41A ' },
unbind: { label: '未绑定', type: 'rgb(221,221,221)' }
}

View File

@@ -0,0 +1,155 @@
<template>
<div class="app-container">
<!-- 列表 -->
<!-- 搜索 -->
<page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 列表 -->
<page-content
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
<template #options="scope">
{{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }}
</template>
<template #bol="scope">
{{ scope.row[scope.prop] ? "是" : "否" }}
</template>
<template #gender="scope">
<el-tag
:type="
scope.row[scope.prop] == null
? 'info'
: scope.row[scope.prop] == 1
? 'success'
: 'warning'
"
>
{{ scope.row[scope.prop] === null ? "未知" : scope.row[scope.prop] == 1 ? "男" : "女" }}
</el-tag>
</template>
<template #user="scope">
<div class="flex align-center">
<el-avatar :src="scope.row.headImg" />
<el-tag>{{ scope.row.nickName }}</el-tag>
</div>
</template>
<template #link="scope">
<el-link>{{ scope.row[scope.prop] }}</el-link>
</template>
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
</template>
</page-content>
<!-- 新增 -->
<page-modal
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
@formDataChange="formDataChange"
></page-modal>
<!-- 编辑 -->
<page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
@formDataChange="formDataChange"
></page-modal>
</div>
</template>
<script setup >
import usePage from "@/components/CURD/usePage";
import addModalConfig from "./config/add";
import contentConfig from "./config/content";
import editModalConfig from "./config/edit";
import searchConfig from "./config/search";
import { returnOptionsLabel } from "./config/config";
const editMoneyModalRef = ref(null);
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
// handleAddClick,
// handleEditClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 修改金额表单类型
function formDataChange(type, val) {
console.log(type, val);
}
// 新增
async function handleAddClick() {
addModalRef.value?.setModalVisible();
}
// 获取优惠券
async function getCouponList() {}
// 编辑
async function handleEditClick(row) {
editModalRef.value?.handleDisabled(false);
editModalRef.value?.setModalVisible();
// 根据id获取数据进行填充
// const data = await VersionApi.getFormData(row.id);
editModalRef.value?.setFormData({ ...row, headImg: row.headImg ? [row.headImg] : "" });
}
// 其他工具栏
function handleToolbarClick(name) {
console.log(name);
if (name === "custom1") {
ElMessage.success("点击了自定义1按钮");
}
}
// 其他操作列
async function handleOperatClick(data) {
const row = data.row;
if (data.name == "more") {
if (data.command === "change-money") {
editMoneyModalRef.value.setModalVisible();
editMoneyModalRef.value.setFormData({ ...row, headImg: row.headImg ? [row.headImg] : "" });
return;
}
return;
}
}
// 切换示例
const isA = ref(true);
</script>
<style scoped>
.align-center {
align-items: center;
}
</style>

View File

@@ -0,0 +1,135 @@
import shopUserApi, { type addRequest } from "@/api/account/shopUser";
import type { IModalConfig } from "@/components/CURD/types";
const modalConfig: IModalConfig<addRequest> = {
pageName: "sys:user",
dialog: {
title: "添加用户",
width: 800,
draggable: true,
},
form: {
labelWidth: 140,
},
formAction: function (data: addRequest) {
return shopUserApi.add(data.shopid, data);
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
{
label: "用户头像",
prop: "headImg",
rules: [{ required: false, message: "请选择用户头像", trigger: "blur" }],
type: "UpImage",
attrs: {
placeholder: "请选择用户头像",
},
},
{
label: "用户昵称",
prop: "nickName",
rules: [{ required: true, message: "请输入用户昵称", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入用户昵称",
},
col: {
xs: 24,
sm: 12,
},
},
{
type: "input",
label: "手机号码",
prop: "phone",
rules: [
{
required: true,
pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
message: "请输入正确的手机号码",
trigger: "blur",
},
],
attrs: {
placeholder: "请输入手机号码",
maxlength: 11,
},
col: {
xs: 24,
sm: 12,
},
},
{
label: "会员生日",
prop: "birthDay",
rules: [{ required: false, message: "请选择会员生日", trigger: "blur" }],
type: "date-picker",
attrs: {
placeholder: "请选择会员生日",
},
col: {
xs: 24,
sm: 12,
},
},
{
type: "radio",
label: "性别",
prop: "sex",
rules: [{ required: false, message: "请选择性别", trigger: "blur" }],
attrs: {
placeholder: "请选择性别",
},
initialValue: '',
options: [
{ label: '男', value: 1 },
{ label: '女', value: 0 },
],
col: {
xs: 24,
sm: 12,
},
},
{
label: "账户积分",
prop: "accountPoints",
rules: [{ required: false, message: "请输入账户积分", trigger: "blur" }],
type: "input-number",
attrs: {
placeholder: "请输入账户积分",
},
col: {
xs: 24,
sm: 12,
},
},
{
label: "钱包余额",
prop: "amount",
rules: [{ required: false, message: "请输入钱包余额", trigger: "blur" }],
type: "input-number",
attrs: {
placeholder: "请输入钱包余额",
},
col: {
xs: 24,
sm: 12,
},
},
{
type: "radio",
label: "是否会员",
prop: "isVip",
options: [
{ label: '是', value: 1 },
{ label: '否', value: 0 },
],
},
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,46 @@
export const isVipOptions: statusOptions[] = [
{ label: "全部", value: "" },
{ label: "非会员", value: "0" },
{ label: "会员", value: "1" },
];
export type optionsType = "isVip";
export function returnOptions(type: optionsType) {
if (type === "isVip") {
return isVipOptions;
}
}
export function returnOptionsLabel(optionsType: optionsType, value: string | number) {
const options = returnOptions(optionsType);
if (!options) {
return "";
}
const option = options.find((item) => item.value === value);
return option ? option.label : "";
}
export interface options {
label: string;
value: string | number;
[property: string]: any;
}
export interface statusOptions extends options {
}
export type payTypeValue =
| ""
| "cash"
| "bank"
| "scanCode"
| "deposit"
| "vipPay"
| "arrears"
| "virtual"
| "arrears";
export interface payTypeOptions extends options {
value: payTypeValue;
}

View File

@@ -0,0 +1,106 @@
import shopUserApi, { type getListRequest } from "@/api/account/shopUser";
import type { IContentConfig } from "@/components/CURD/types";
const contentConfig: IContentConfig<any> = {
pageName: "sys:user",
table: {
border: true,
highlightCurrentRow: true,
},
pagination: {
background: true,
layout: "prev,pager,next,jumper,total,sizes",
pageSize: 20,
pageSizes: [10, 20, 30, 50],
},
indexAction: function (params: getListRequest) {
return shopUserApi.getList(params);
},
// deleteAction: shopUserApi.delete,
// modifyAction: function (data) {
// // return shopUserApi.edit(data);
// },
pk: "id",
toolbar: ["add"],
defaultToolbar: ["refresh", "filter", "search"],
cols: [
{ type: "selection", width: 50, align: "center" },
{ label: "id", align: "center", prop: "id", width: 100, show: true },
{
label: "用户",
align: "center",
prop: "user",
templet: "custom",
slotName: "user",
},
{
label: "性别",
align: "center",
prop: "sex",
templet: "custom",
slotName: "gender",
},
{
label: "会员",
align: "center",
prop: "isVip",
templet: "custom",
slotName: "bol",
},
{
label: "手机号",
align: "center",
prop: "phone",
width: 140,
templet: "custom",
slotName: "mobile",
},
{
label: "余额",
align: "center",
prop: "amount",
},
{
label: "积分",
align: "center",
prop: "accountPoints",
},
{
label: "消费累计",
align: "center",
prop: "consumeAmount",
},
{
label: "消费次数累计",
align: "center",
prop: "consumeAmount",
},
{
label: "注册时间",
align: "center",
prop: "createTime",
},
{
label: "操作",
align: "center",
fixed: "right",
width: 180,
templet: "tool",
operat: [
{
name: "edit",
text: "编辑",
},
{
name: "more",
text: "更多",
options: [
{ label: '增减余额', command: 'change-money' }
]
},
],
},
],
};
export default contentConfig;

View File

@@ -0,0 +1,145 @@
import shopUserApi, { type editMoneyRequest } from "@/api/account/shopUser";
import type { IModalConfig } from "@/components/CURD/types";
import { min } from "lodash";
// 增减或者减少余额的类型
export interface optuions {
label: string;
value: string | number;
}
export const addOptions: optuions[] = [
{ label: '增加充值', value: 'adminIn' },
{ label: '增加消费退款', value: 'adminRefund' },
]
export const reduceOptions: optuions[] = [
{ label: '扣除消费', value: 'adminOut' },
{ label: '扣除充值退款', value: 'adminInOut' },
]
const modalConfig: IModalConfig<editMoneyRequest> = {
pageName: "sys:user",
dialog: {
title: "增减余额",
width: 800,
draggable: true,
},
pk: "id",
form: {
labelWidth: 140,
},
formAction: function (data) {
return shopUserApi.editMoney(data.shopid, data);
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
{
label: "用户昵称",
prop: "nickName",
rules: [{ required: false, message: "请输入用户昵称", trigger: "blur" }],
type: "input",
disabled: true,
attrs: {
placeholder: "请输入用户昵称",
},
col: {
xs: 24,
sm: 12,
},
},
{
label: "钱包余额",
prop: "amount",
disabled: true,
rules: [{ required: false, message: "", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "",
},
col: {
xs: 24,
sm: 12,
},
},
{
type: "radio",
label: "增减余额",
prop: "type",
rules: [{ required: true, message: "增减余额", trigger: "blur" }],
initialValue: 1,
options: [
{ label: '增加', value: 1 },
{ label: '减少', value: 0 },
],
col: {
xs: 24,
sm: 12,
},
watch() {
}
},
{
label: "金额",
prop: "money",
rules: [{ required: false, message: "请输入要增加或者减少的金额", trigger: "blur" }],
type: "input-number",
attrs: {
placeholder: "请输入要增加或者减少的金额",
min: 0,
},
col: {
xs: 24,
sm: 12,
},
initialValue: 0
},
{
label: "类型",
prop: "bizEnum",
rules: [{ required: true, message: "请选择类型", trigger: "blur" }],
type: "radio-button",
attrs: {
placeholder: "请选择类型",
},
options: addOptions,
},
{
label: "备注",
prop: "remark",
rules: [{ required: false, message: "请输入备注", trigger: "blur" }],
type: "textarea",
attrs: {
placeholder: "请输入备注",
},
},
// {
// label: "钱包余额",
// prop: "amount",
// rules: [{ required: false, message: "请输入钱包余额", trigger: "blur" }],
// type: "input-number",
// attrs: {
// placeholder: "请输入钱包余额",
// },
// col: {
// xs: 24,
// sm: 12,
// },
// },
// {
// type: "radio",
// label: "是否会员",
// prop: "isVip",
// options: [
// { label: '是', value: 1 },
// { label: '否', value: 0 },
// ],
// },
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,136 @@
import shopUserApi, { type editRequest } from "@/api/account/shopUser";
import type { IModalConfig } from "@/components/CURD/types";
const modalConfig: IModalConfig<editRequest> = {
pageName: "sys:user",
dialog: {
title: "修改用户",
width: 800,
draggable: true,
},
pk: "id",
form: {
labelWidth: 140,
},
formAction: function (data: editRequest) {
return shopUserApi.edit(data.shopid, data);
},
beforeSubmit(data) {
console.log("提交之前处理", data);
},
formItems: [
// {
// label: "用户头像",
// prop: "headImg",
// rules: [{ required: false, message: "请选择用户头像", trigger: "blur" }],
// type: "UpImage",
// attrs: {
// placeholder: "请选择用户头像",
// },
// },
{
label: "用户昵称",
prop: "nickName",
rules: [{ required: false, message: "请输入用户昵称", trigger: "blur" }],
type: "input",
attrs: {
placeholder: "请输入用户昵称",
},
col: {
xs: 24,
sm: 12,
},
},
// {
// type: "input",
// label: "手机号码",
// prop: "phone",
// rules: [
// {
// required: true,
// pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/,
// message: "请输入正确的手机号码",
// trigger: "blur",
// },
// ],
// attrs: {
// placeholder: "请输入手机号码",
// maxlength: 11,
// },
// col: {
// xs: 24,
// sm: 12,
// },
// },
{
label: "会员生日",
prop: "birthDay",
rules: [{ required: false, message: "请选择会员生日", trigger: "blur" }],
type: "date-picker",
attrs: {
placeholder: "请选择会员生日",
},
col: {
xs: 24,
sm: 12,
},
},
{
type: "radio",
label: "性别",
prop: "sex",
rules: [{ required: false, message: "请选择性别", trigger: "blur" }],
attrs: {
placeholder: "请选择性别",
},
initialValue: '',
options: [
{ label: '男', value: 1 },
{ label: '女', value: 0 },
],
col: {
xs: 24,
sm: 12,
},
},
// {
// label: "账户积分",
// prop: "accountPoints",
// rules: [{ required: false, message: "请输入账户积分", trigger: "blur" }],
// type: "input-number",
// attrs: {
// placeholder: "请输入账户积分",
// },
// col: {
// xs: 24,
// sm: 12,
// },
// },
// {
// label: "钱包余额",
// prop: "amount",
// rules: [{ required: false, message: "请输入钱包余额", trigger: "blur" }],
// type: "input-number",
// attrs: {
// placeholder: "请输入钱包余额",
// },
// col: {
// xs: 24,
// sm: 12,
// },
// },
// {
// type: "radio",
// label: "是否会员",
// prop: "isVip",
// options: [
// { label: '是', value: 1 },
// { label: '否', value: 0 },
// ],
// },
],
};
// 如果有异步数据会修改配置的推荐用reactive包裹而纯静态配置的可以直接导出
export default reactive(modalConfig);

View File

@@ -0,0 +1,35 @@
import type { ISearchConfig } from "@/components/CURD/types";
import { isVipOptions } from "./config";
const searchConfig: ISearchConfig = {
pageName: "sys:user",
inline: true,
isExpandable: false,
formItems: [
{
type: "radio-button",
label: "是否会员",
prop: "isVip",
attrs: {
placeholder: "请选择",
clearable: true,
},
options: isVipOptions,
},
{
type: "input",
label: "昵称或手机号",
prop: "key",
attrs: {
placeholder: "请输入昵称或手机号",
clearable: true,
style: {
width: "200px",
},
},
},
],
};
export default searchConfig;

View File

@@ -0,0 +1,169 @@
<template>
<div class="app-container">
<!-- 列表 -->
<!-- 搜索 -->
<page-search
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
/>
<!-- 列表 -->
<page-content
ref="contentRef"
:content-config="contentConfig"
@add-click="handleAddClick"
@edit-click="handleEditClick"
@export-click="handleExportClick"
@search-click="handleSearchClick"
@toolbar-click="handleToolbarClick"
@operat-click="handleOperatClick"
@filter-change="handleFilterChange"
>
<template #status="scope">
<el-tag :type="scope.row[scope.prop] == 1 ? 'success' : 'info'">
{{ scope.row[scope.prop] == 1 ? "启用" : "禁用" }}
</el-tag>
</template>
<template #options="scope">
{{ returnOptionsLabel(scope.prop, scope.row[scope.prop]) }}
</template>
<template #bol="scope">
{{ scope.row[scope.prop] ? "是" : "否" }}
</template>
<template #gender="scope">
<el-tag
:type="
scope.row[scope.prop] == null
? 'info'
: scope.row[scope.prop] == 1
? 'success'
: 'warning'
"
>
{{ scope.row[scope.prop] === null ? "未知" : scope.row[scope.prop] == 1 ? "男" : "女" }}
</el-tag>
</template>
<template #user="scope">
<div class="flex align-center">
<el-avatar :src="scope.row.headImg" />
<el-tag>{{ scope.row.nickName }}</el-tag>
</div>
</template>
<template #link="scope">
<el-link>{{ scope.row[scope.prop] }}</el-link>
</template>
<template #mobile="scope">
<el-text>{{ scope.row[scope.prop] }}</el-text>
<copy-button
v-if="scope.row[scope.prop]"
:text="scope.row[scope.prop]"
style="margin-left: 2px"
/>
</template>
</page-content>
<!-- 新增 -->
<page-modal
ref="addModalRef"
:modal-config="addModalConfig"
@submit-click="handleSubmitClick"
></page-modal>
<!-- 编辑 -->
<page-modal
ref="editModalRef"
:modal-config="editModalConfig"
@submit-click="handleSubmitClick"
></page-modal>
<!-- 用户余额修改 -->
<page-modal
ref="editMoneyModalRef"
:modal-config="editMoneyModalConfig"
@formDataChange="formDataChange"
@submit-click="handleSubmitClick"
></page-modal>
</div>
</template>
<script setup >
import usePage from "@/components/CURD/usePage";
import addModalConfig from "./config/add";
import contentConfig from "./config/content";
import editModalConfig from "./config/edit";
import editMoneyModalConfig, { addOptions, reduceOptions } from "./config/edit-money";
import searchConfig from "./config/search";
import { returnOptionsLabel } from "./config/config";
const editMoneyModalRef = ref(null);
const {
searchRef,
contentRef,
addModalRef,
editModalRef,
handleQueryClick,
handleResetClick,
// handleAddClick,
// handleEditClick,
handleSubmitClick,
handleExportClick,
handleSearchClick,
handleFilterChange,
} = usePage();
// 修改金额表单类型
function formDataChange(type, val) {
if (type == "type") {
if (val == 1) {
editMoneyModalConfig.formItems[4].options = addOptions;
} else {
editMoneyModalConfig.formItems[4].options = reduceOptions;
}
}
}
// 新增
async function handleAddClick() {
addModalRef.value?.setModalVisible();
// 加载部门下拉数据源
// addModalConfig.formItems[2]!.attrs!.data = await DeptAPI.getOptions();
// 加载角色下拉数据源
// addModalConfig.formItems[4]!.options = await RoleAPI.getOptions();
}
// 编辑
async function handleEditClick(row) {
editModalRef.value?.handleDisabled(false);
editModalRef.value?.setModalVisible();
// 根据id获取数据进行填充
// const data = await VersionApi.getFormData(row.id);
editModalRef.value?.setFormData({ ...row, headImg: row.headImg ? [row.headImg] : "" });
}
// 其他工具栏
function handleToolbarClick(name) {
console.log(name);
if (name === "custom1") {
ElMessage.success("点击了自定义1按钮");
}
}
// 其他操作列
async function handleOperatClick(data) {
const row = data.row;
if (data.name == "more") {
if (data.command === "change-money") {
editMoneyModalRef.value.setModalVisible();
editMoneyModalRef.value.setFormData({ ...row, headImg: row.headImg ? [row.headImg] : "" });
return;
}
return;
}
}
// 切换示例
const isA = ref(true);
</script>
<style scoped>
.align-center {
align-items: center;
}
</style>