增加店铺列表页面,增加vue-amap,修改登录相关接口类型

This commit is contained in:
2025-02-12 13:52:52 +08:00
parent a599adc63e
commit 81d6ef44ef
18 changed files with 1399 additions and 62 deletions

View File

@@ -11,8 +11,8 @@
<h3 class="title">银收客后台管理</h3>
<el-form-item>
<el-radio-group v-model="state.loginForm.loginType">
<el-radio-button value="merchant">商户</el-radio-button>
<el-radio-button value="staff">员工</el-radio-button>
<el-radio-button :value="0">商户</el-radio-button>
<el-radio-button :value="1">员工</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item prop="merchantName" v-if="state.loginForm.loginType == 'staff'">
@@ -94,11 +94,11 @@ const state = reactive({
loginForm: {
username: "",
password: "",
rememberMe: false,
// rememberMe: false,
code: "",
uuid: "",
merchantName: "",
loginType: "merchant",
loginType: 0,
},
loginRules: {
username: [{ required: true, trigger: "blur", message: "用户名不能为空" }],
@@ -176,12 +176,12 @@ function handleLogin() {
if (valid) {
state.loading = true;
const user = { ...state.loginForm };
user.password = encrypt(user.password);
// user.password = encrypt(user.password);
console.log(user);
userStore
.login(user)
.then(async (res) => {
await userStore.getUserInfo();
// await userStore.getUserInfo();
const { path, queryParams } = parseRedirect();
router.push({ path: path, query: queryParams });
})

View File

View File

@@ -0,0 +1,523 @@
<template>
<el-dialog
:title="state.form.id ? '编辑店铺' : '添加店铺'"
v-model="state.dialogVisible"
@close="reset"
>
<div style="height: 50vh; overflow-y: auto">
<el-form
ref="form"
:model="state.form"
:rules="state.rules"
label-width="120px"
label-position="left"
>
<el-form-item label="店铺名称" prop="shopName">
<el-input v-model="state.form.shopName" placeholder="请输入门店名称"></el-input>
</el-form-item>
<el-form-item label="店铺类型">
<el-radio-group v-model="state.form.type">
<el-radio-button value="only">单店</el-radio-button>
<el-radio-button value="chain">连锁店</el-radio-button>
<el-radio-button value="join">加盟店</el-radio-button>
</el-radio-group>
<div class="tips">请谨慎修改</div>
</el-form-item>
<el-form-item label="主店账号" prop="mainId" v-if="state.form.type != 'only'">
<el-select
v-model="state.form.mainId"
placeholder="请选择主店铺"
filterable
remote
reserve-keyword
:remote-method="getTableData"
:loading="state.shopListLoading"
>
<el-option
v-for="item in state.shopList"
:label="`ID:${item.id} - 名称:${item.shopName}`"
:value="item.id"
:key="item.id"
></el-option>
</el-select>
</el-form-item>
<el-form-item label="连锁店扩展店名">
<el-input v-model="state.form.chainName" placeholder="请输入连锁店扩展店名"></el-input>
</el-form-item>
<el-form-item label="门店logo" prop="logo">
<el-image
:src="state.form.logo || uploadImg"
fit="contain"
style="width: 80px; height: 80px"
@click="
state.showUpload = true;
state.uploadIndex = 1;
"
></el-image>
</el-form-item>
<el-form-item label="门店照片">
<el-image
:src="state.form.coverImg || uploadImg"
fit="contain"
style="width: 80px; height: 80px"
@click="
state.showUpload = true;
state.uploadIndex = 2;
"
></el-image>
</el-form-item>
<el-form-item label="经营模式">
<el-radio-group v-model="state.form.registerType">
<el-radio-button value="munchies">快餐版</el-radio-button>
<el-radio-button value="restaurant">餐饮版</el-radio-button>
</el-radio-group>
<div class="tips">请谨慎修改</div>
</el-form-item>
<el-form-item label="管理方式" v-if="state.form.type != 'only'">
<el-radio-group v-model="state.form.tube_type">
<el-radio-button value="0">不可直接管理</el-radio-button>
<el-radio-button value="1">直接管理</el-radio-button>
</el-radio-group>
<div class="tips">请谨慎修改</div>
</el-form-item>
<el-form-item label="试用/正式">
<el-radio-group v-model="state.form.profiles">
<el-radio-button value="probation">试用</el-radio-button>
<el-radio-button value="release">正式</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="激活码">
<el-input v-model="state.form.registerCode" placeholder="请输入激活码"></el-input>
<div class="tips">输入有效激活码表示添加的同时直接激活该店铺</div>
</el-form-item>
<el-form-item label="登录账号" prop="account">
<el-input v-model="state.form.account" placeholder="请输入登录账号"></el-input>
</el-form-item>
<el-form-item label="登录密码" prop="password" v-if="!state.form.id">
<el-input
type="password"
show-password
v-model="state.form.password"
placeholder="请输入登录密码"
></el-input>
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="state.form.phone" placeholder="请输入联系电话"></el-input>
</el-form-item>
<el-form-item label="设备数量">
<el-input-number
v-model="state.form.supportDeviceNumber"
controls-position="right"
:min="1"
:step="1"
step-strictly
></el-input-number>
</el-form-item>
<!-- <el-form-item label="外卖起送金额">
<el-input-number v-model="form.takeaway_money" placeholder="0.00" controls-position="right"
:min="0"></el-input-number>
</el-form-item> -->
<el-form-item label="店铺经度" prop="provinces">
<el-row>
<el-col :span="9" v-if="state.form.provinces">
<el-input
:value="`${state.form.provinces}-${state.form.cities}-${state.form.districts}`"
disabled
/>
</el-col>
<el-col :span="4" v-if="state.form.lng">
<el-input v-model="state.form.lng" placeholder="经度" disabled></el-input>
</el-col>
<el-col :span="4" v-if="state.form.lng">
<el-input v-model="state.form.lat" placeholder="纬度" disabled></el-input>
</el-col>
<el-col :span="4">
<el-button
type="primary"
plain
icon="el-icon-place"
@click="state.showLocation = true"
>
选择坐标
</el-button>
</el-col>
</el-row>
</el-form-item>
<el-form-item label="店铺详细地址">
<el-input
type="textarea"
v-model="state.form.address"
placeholder="请输入门店详细地址"
></el-input>
</el-form-item>
<el-form-item label="店铺简介">
<el-input
type="textarea"
v-model="state.form.detail"
placeholder="请输入店铺简介"
></el-input>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="state.form.status">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</div>
<el-dialog
title="选择地址"
v-model="state.showLocation"
:modal="false"
:modal-append-to-body="false"
>
<div class="map_box">
<div class="map">
<el-amap ref="map" :center="state.amapOptions.center">
<el-amap-marker :position="state.amapOptions.center"></el-amap-marker>
<el-amap-search-box
:visible="true"
@select="onSearchResult"
@choose="onSearchResult"
></el-amap-search-box>
</el-amap>
</div>
<!-- <div class="search_box">
<el-amap-search-box
:search-option="searchOption"
:on-search-result="onSearchResult"
></el-amap-search-box>
</div> -->
<div class="search_wrap">
<div class="item" v-for="item in state.locationSearchList" :key="item.id">
<div class="left">
<div class="name">{{ item.name }}-{{ item.address }}</div>
<div class="location">经纬度{{ item.lng }},{{ item.lat }}</div>
</div>
<div class="btn">
<el-button type="primary" @click="selectLocationHandle(item)">选择</el-button>
</div>
</div>
</div>
</div>
</el-dialog>
<el-dialog
v-model="state.showUpload"
:close-on-click-modal="false"
append-to-body
width="500px"
@close="state.showUpload = false"
>
<el-upload
:before-remove="handleBeforeRemove"
:on-success="handleSuccess"
:on-error="handleError"
:file-list="state.fileList"
:headers="headers"
:action="qiNiuUploadApi"
class="upload-demo"
multiple
>
<el-button size="small" type="primary">点击上传</el-button>
<template #tip>
<div style="display: block" class="el-upload__tip">请勿上传违法文件且文件不超过15M</div>
</template>
</el-upload>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="doSubmit">确认</el-button>
<el-button @click="uploadClose">取消</el-button>
</div>
</template>
</el-dialog>
<template #footer>
<div class="dialog-footer">
<el-button @click="close">取消</el-button>
<el-button type="primary" @click="submitHandle" :loading="state.formLoading">
<span v-if="!state.formLoading">保存</span>
<span v-else>保存中...</span>
</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { getToken } from "@/utils/auth";
import uploadImg from "@/assets/images/upload.png";
// { geocode, ShopApi.getList }
import ShopApi from "@/api/account/shop";
const validateLogo = (rule, value, callback) => {
if (!this.form.logo) {
callback(new Error("请上传门店logo"));
} else {
callback();
}
};
const state = reactive({
uploadImg: uploadImg,
dialogVisible: false,
showLocation: false,
showUpload: false,
uploadIndex: 1,
startTime: "",
endTime: "",
formLoading: false,
form: {
id: "",
shopName: "",
mainId: "",
type: "only",
tube_type: "0",
registerType: "restaurant",
profiles: "release",
registerCode: "",
account: "",
password: "",
phone: "",
supportDeviceNumber: 1,
lat: "",
lng: "",
address: "",
detail: "",
status: 1,
logo: "",
coverImg: "",
provinces: "",
cities: "",
districts: "",
chainName: "",
},
resetForm: "",
rules: {
shopName: [
{
required: true,
message: " ",
trigger: "blur",
},
],
mainId: [
{
required: true,
message: " ",
trigger: "blur",
},
],
provinces: [
{
required: true,
message: "请选择坐标",
trigger: "change",
},
],
logo: [
{
required: true,
validator: validateLogo,
trigger: "change",
},
],
account: [
{
required: true,
message: " ",
trigger: "change",
},
],
password: [
{
required: true,
message: " ",
trigger: "change",
},
],
},
fileList: [],
files: [],
headers: {
Authorization: getToken(),
},
searchOption: {
city: "西安",
citylimit: false,
},
locationSearchList: [],
amapOptions: {
center: [108.946465, 34.347984],
position: [],
},
shopListLoading: false,
shopList: [],
});
onMounted(() => {
state.resetForm = { ...state.form };
});
// 获取商家列表
async function getTableData(query = "") {
state.shopListLoading = true;
try {
const res = await ShopApi.getList({
page: 1,
size: 100,
shopName: query,
type: "only",
});
state.shopListLoading = false;
state.shopList = res.content;
} catch (error) {
state.shopListLoading = false;
console.log(error);
}
}
function onSearchResult(res) {
console.log("res");
console.log(res);
state.locationSearchList = res;
state.amapOptions.center = [res[0].lng, res[0].lat];
}
// 确认地址选择
async function selectLocationHandle(item) {
console.log(item);
state.form.lng = item.lng;
state.form.lat = item.lat;
state.form.address = item.address;
state.showLocation = false;
const position = `${item.lng},${item.lat}`;
const res = JSON.parse(await geocode({ location: position }));
console.log(res);
state.form.provinces = res.addressComponent.province;
state.form.cities = res.addressComponent.city;
state.form.districts = res.addressComponent.district;
}
const emits = defineEmits(["close", "success"]);
// 保存
function submitHandle() {
state.$refs.form.validate(async (valid) => {
if (valid) {
state.formLoading = true;
try {
await ShopApi.getListPost(state.form, state.form.id ? "put" : "post");
emits("success");
state.formLoading = false;
$notify({
title: "成功",
message: `${this.form.id ? "编辑" : "添加"}成功`,
type: "success",
});
close();
} catch (error) {
state.formLoading = false;
console.log(error);
}
}
});
}
function handleSuccess(response, file, fileList) {
// const uid = file.uid
// const id = response.id
// this.files.push({ uid, id })
console.log("上传成功", response);
state.files = response.data;
}
function handleBeforeRemove(file, fileList) {
for (let i = 0; i < state.files.length; i++) {
if (this.files[i].uid === file.uid) {
crudQiNiu.del([state.files[i].id]).then((res) => {});
return true;
}
}
}
function handlePictureCardPreview(file) {
state.dialogImageUrl = file.url;
state.dialogVisible = true;
}
// 监听上传失败
function handleError(e, file, fileList) {
const msg = JSON.parse(e.message);
state.crud.notify(msg.message, CRUD.NOTIFICATION_TYPE.ERROR);
}
// 刷新列表数据
function doSubmit() {
state.fileList = [];
state.showUpload = false;
switch (state.uploadIndex) {
case 1:
state.form.logo = state.files[0];
break;
case 2:
state.form.coverImg = state.files[0];
break;
default:
break;
}
}
function show(obj) {
getTableData();
state.dialogVisible = true;
if (obj && obj.id) {
console.log(obj);
state.form = { ...obj };
}
}
function close() {
state.dialogVisible = false;
}
function uploadClose() {
state.showUpload = false;
}
function reset() {
state.form = { ...this.resetForm };
}
defineExpose({
show,
close,
reset,
});
</script>
<style scoped lang="scss">
.map_box {
width: 100%;
position: relative;
.map {
height: 300px;
}
.search_box {
position: absolute;
top: 10px;
left: 10px;
}
.search_wrap {
padding: 6px 0;
.item {
display: flex;
padding: 12px 0;
.left {
flex: 1;
display: flex;
flex-direction: column;
padding-right: 20px;
.location {
color: #999;
padding-top: 4px;
}
}
}
}
}
.amap-sug-result {
z-index: 1000;
}
</style>

View File

@@ -0,0 +1,141 @@
<template>
<el-dialog v-model="dialogVisible" :show-close="false" @close="reset">
<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="商户号">
<el-input v-model="form.appId" placeholder="请输入商户号"></el-input>
</el-form-item>
<el-form-item label="商户密钥">
<el-input
type="textarea"
v-model="form.appToken"
placeholder="请输入商户密钥"
></el-input>
</el-form-item>
<el-form-item label="支付密码">
<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-form-item>
<el-form-item label="支付宝appid">
<el-input
v-model="form.alipaySmallAppid"
placeholder="请输入支付宝小程序appid"
></el-input>
</el-form-item>
<!-- <el-form-item label="支付宝商户号">
<el-input v-model="form.alipayAppId" placeholder="请输入支付宝商户号"></el-input>
</el-form-item>
<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-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>
</el-dialog>
</template>
<script>
// import { tbMerchantThirdApply, tbMerchantThirdApplyPut } from "@/api/shop";
import ShopApi from "@/api/account/shop";
export default {
data() {
return {
dialogVisible: false,
activeName: "pay",
formLoading: false,
form: {
appToken: "",
id: "",
payPassword: "",
status: 1,
appId: "",
smallAppid: "",
storeId: "",
alipaySmallAppid: "",
alipayAppToken: "",
alipayAppId: "",
},
};
},
methods: {
// 保存
async submitHandle() {
this.formLoading = true;
try {
await tbMerchantThirdApplyPut(this.form);
this.$emit("success");
this.formLoading = false;
this.$notify({
title: "成功",
message: `提交成功`,
type: "success",
});
this.close();
} catch (error) {
this.formLoading = false;
console.log(error);
}
},
close() {
this.dialogVisible = false;
},
reset() {
this.form.appToken = "";
this.form.id = "";
this.form.payPassword = "";
this.form.status = 1;
this.form.appId = "";
},
// 详情(配置三方支付)
async getDetail(id) {
try {
const res = await tbMerchantThirdApply(id);
this.form.appToken = res.appToken;
this.form.payPassword = res.payPassword;
this.form.status = res.status;
this.form.appId = res.appId;
this.form.smallAppid = res.smallAppid;
this.form.alipaySmallAppid = res.alipaySmallAppid;
//this.form.alipayAppToken = res.alipayAppToken
//this.form.alipayAppId = res.alipayAppId
this.form.storeId = res.storeId;
this.dialogVisible = true;
} catch (error) {
console.log(error);
}
},
show(obj) {
if (obj && obj.id) {
this.form.id = obj.merchantId;
this.getDetail(obj.merchantId);
}
},
},
};
</script>
<style scoped lang="scss">
::v-deep(.el-dialog__header) {
padding: 0;
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<div class="app-container">
<div class="head-container">
<el-row :gutter="20">
<el-col :span="3">
<el-input
v-model="query.name"
clearable
placeholder="请输入店铺名称"
style="width: 100%"
class="filter-item"
@keyup.enter="getTableData"
/>
</el-col>
<el-col :span="3">
<el-input
v-model="query.account"
clearable
placeholder="请输入商户号"
style="width: 100%"
class="filter-item"
@keyup.enter="getTableData"
/>
</el-col>
<el-col :span="3">
<el-select v-model="query.status" placeholder="请选择店铺状态" style="width: 100%">
<el-option
:label="item.label"
:value="item.type"
v-for="item in status"
:key="item.type"
/>
</el-select>
</el-col>
<el-col :span="6">
<el-button type="primary" @click="getTableData">查询</el-button>
<el-button @click="resetHandle">重置</el-button>
</el-col>
</el-row>
</div>
<div class="head-container">
<el-button type="primary" icon="el-icon-plus" @click="$refs.addShop.show()">
添加店铺
</el-button>
</div>
<div class="head-container">
<el-table :data="tableData.list" v-loading="tableData.loading">
<el-table-column label="店铺信息" width="200">
<template v-slot="scope">
<div class="shop_info">
<el-image
:src="scope.row.logo"
style="width: 50px; height: 50px; border-radius: 4px; background-color: #efefef"
>
<div class="img_error" slot="error">
<i class="icon el-icon-document-delete"></i>
</div>
</el-image>
<div class="info">
<span>{{ scope.row.shopName }}</span>
<div class="tag_wrap">
<el-tag type="info" effect="dark" v-if="scope.row.profiles == 'no'">
未激活
</el-tag>
<el-tag type="warning" effect="dark" v-if="scope.row.profiles == 'probation'">
试用
</el-tag>
<el-tag type="success" effect="dark" v-if="scope.row.profiles == 'release'">
正式
</el-tag>
<el-tag type="primary" effect="dark" v-if="scope.row.isWxMaIndependent">
独立小程序
</el-tag>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="registerType" label="类型">
<template v-slot="scope">
<span v-if="scope.row.registerType == 'munchies'">快餐版</span>
<span v-if="scope.row.registerType == 'restaurant'">餐饮版</span>
</template>
</el-table-column>
<el-table-column prop="address" label="商户号"></el-table-column>
<el-table-column prop="lowPrice" label="来源"></el-table-column>
<el-table-column prop="lowPrice" label="认证状态">-</el-table-column>
<el-table-column prop="status" label="店铺状态">
<template v-slot="scope">
<el-switch
v-model="scope.row.status"
:active-value="1"
:inactive-value="0"
disabled
></el-switch>
</template>
</el-table-column>
<el-table-column prop="createdAt" label="到期时间">
<template v-slot="scope">
{{ dayjs(scope.row.expireAt).format("YYYY-MM-DD HH:mm:ss") }}
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template v-slot="scope">
<el-link icon="edit" @click="$refs.addShop.show(scope.row)">编辑</el-link>
<el-dropdown @command="dropdownClick">
<el-link icon="arrow-down">更多</el-link>
<el-dropdown-menu>
<template #dropdown>
<el-dropdown-item :command="{ row: scope.row, command: 1 }">
三方配置
</el-dropdown-item>
<el-dropdown-item :command="2">续费记录</el-dropdown-item>
<el-dropdown-item :command="3">前往店铺</el-dropdown-item>
<el-dropdown-item :command="4">重置密码</el-dropdown-item>
<el-dropdown-item divided :command="5">删除</el-dropdown-item>
</template>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</div>
<div class="head-container">
<el-pagination
:total="tableData.total"
v-model:current-page="tableData.page"
:page-size="tableData.size"
@current-change="paginationChange"
layout="total, sizes, prev, pager, next, jumper"
></el-pagination>
</div>
<addShop ref="addShop" @success="getTableData" />
<detailModal ref="detailModal" />
</div>
</template>
<script>
import dayjs from "dayjs";
import ShopApi from "@/api/account/shop";
import addShop from "./components/addShop.vue";
import detailModal from "./components/detailModal.vue";
export default {
components: { addShop, detailModal },
data() {
return {
dayjs,
query: {
name: "",
account: "",
status: "",
},
status: [
{
type: 1,
label: "开启",
},
{
type: 0,
label: "关闭",
},
],
tableData: {
list: [],
page: 1,
size: 10,
loading: false,
total: 0,
},
};
},
mounted() {
this.getTableData();
},
methods: {
dropdownClick(e) {
switch (e.command) {
case 1:
this.$refs.detailModal.show(e.row);
break;
default:
break;
}
},
// 重置查询
resetHandle() {
this.query.name = "";
this.query.account = "";
this.query.status = "";
this.getTableData();
},
// 分页回调
paginationChange(e) {
this.tableData.page = e;
this.getTableData();
},
// 获取商家列表
async getTableData() {
this.tableData.loading = true;
try {
const res = await ShopApi.getList({
page: this.tableData.page,
size: this.tableData.size,
shopName: this.query.name,
account: this.query.account,
status: this.query.status,
});
this.tableData.loading = false;
this.tableData.list = res.records;
this.tableData.total = res.totalRow;
} catch (error) {
console.log(error);
}
},
},
};
</script>
<style scoped lang="scss">
.head-container {
margin-bottom: 20px;
}
.shop_info {
display: flex;
.info {
flex: 1;
padding-left: 4px;
}
}
</style>