feat(用户认证): 添加企业微信用户绑定功能并完善商品详情页
- 在登录流程中存储企业微信用户信息到store - 添加企业微信用户绑定接口及相关类型定义 - 当检测到未绑定用户时显示绑定对话框 - 新增商品详情路由及页面,优化商品列表交互 - 改进商品卡片样式和点击跳转逻辑
This commit is contained in:
parent
8f6ae14e47
commit
12cef7b84f
|
@ -84,6 +84,12 @@ export interface UpdateAb98UserCommand extends AddAb98UserCommand {
|
|||
ab98UserId: number;
|
||||
}
|
||||
|
||||
export interface BindQyUserCommand {
|
||||
qyUserId: number;
|
||||
name: string;
|
||||
idNum: string;
|
||||
}
|
||||
|
||||
export const getAb98UserListApi = (params: Ab98UserQuery) => {
|
||||
return http.request<ResponseData<PageDTO<Ab98UserDTO>>>("get", "/ab98/users", { params });
|
||||
};
|
||||
|
@ -102,4 +108,8 @@ export const deleteAb98UserApi = (ids: number[]) => {
|
|||
|
||||
export const getAb98UserDetailApi = (id: number) => {
|
||||
return http.request<ResponseData<Ab98UserDetailDTO>>("get", `/ab98/users/detail/${id}`);
|
||||
};
|
||||
|
||||
export const bindQyUserApi = (data: BindQyUserCommand) => {
|
||||
return http.request<ResponseData<void>>("post", "/ab98/users/bindQyUser", { data });
|
||||
};
|
|
@ -37,6 +37,8 @@ export type TokenDTO = {
|
|||
token: string;
|
||||
/** 当前登录的用户 */
|
||||
currentUser: CurrentLoginUserDTO;
|
||||
/** 企业微信用户信息 */
|
||||
qyUser: QyUserLoginDTO;
|
||||
};
|
||||
|
||||
export type CurrentLoginUserDTO = {
|
||||
|
@ -121,4 +123,68 @@ type qyUserinfoQuery = {
|
|||
/** 获取企业微信访问用户身份接口 */
|
||||
export const getQyUserinfo = (params: qyUserinfoQuery) => {
|
||||
return http.request<ResponseData<string>>("get", "/getQyUserinfo", { params });
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
export interface QyUserLoginDTO {
|
||||
/** 用户ID */
|
||||
id?: number;
|
||||
/** 全局唯一ID */
|
||||
openUserid?: string;
|
||||
/** 企业用户ID */
|
||||
userid?: string;
|
||||
/** 汇邦云用户ID */
|
||||
ab98UserId?: number;
|
||||
/** 用户姓名 */
|
||||
name?: string;
|
||||
/** 手机号码 */
|
||||
mobile?: string;
|
||||
/** 所属部门 */
|
||||
department?: string;
|
||||
/** 部门排序 */
|
||||
userOrder?: string;
|
||||
/** 职务信息 */
|
||||
position?: string;
|
||||
/** 性别 */
|
||||
gender?: string;
|
||||
/** 邮箱 */
|
||||
email?: string;
|
||||
/** 企业邮箱 */
|
||||
bizMail?: string;
|
||||
/** 部门负责人 */
|
||||
isLeaderInDept?: string;
|
||||
/** 直属上级 */
|
||||
directLeader?: string;
|
||||
/** 头像地址 */
|
||||
avatar?: string;
|
||||
/** 座机号码 */
|
||||
telephone?: string;
|
||||
/** 别名 */
|
||||
alias?: string;
|
||||
/** 激活状态 */
|
||||
status?: string;
|
||||
/** 个人二维码 */
|
||||
qrCode?: string;
|
||||
/** 操作人 */
|
||||
operator?: string;
|
||||
/** 有效状态 */
|
||||
enableStatus?: string;
|
||||
/** 创建时间 */
|
||||
createTimeStr?: string;
|
||||
/** 企业ID */
|
||||
corpid?: string;
|
||||
/** 应用ID */
|
||||
appid?: string;
|
||||
/** 用户余额 */
|
||||
balance?: number;
|
||||
/** 已使用用户余额 */
|
||||
useBalance?: number;
|
||||
/** 用户余额额度 */
|
||||
balanceLimit?: number;
|
||||
/** 系统角色ID */
|
||||
sysRoleId?: number;
|
||||
/** 角色ID */
|
||||
roleId?: number;
|
||||
/** 角色名称 */
|
||||
roleName?: string;
|
||||
}
|
|
@ -130,6 +130,7 @@ router.beforeEach(async (to: ToRouteType, _from, next) => {
|
|||
});
|
||||
// 登录成功后 将token存储到sessionStorage中
|
||||
setTokenFromBackend(data);
|
||||
wxStore.setQyUser(data.qyUser);
|
||||
// 获取后端路由
|
||||
await initRouter();
|
||||
next({ path: getTopMenu(true).path });
|
||||
|
|
|
@ -39,6 +39,14 @@ export default {
|
|||
meta: {
|
||||
title: "用户详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/shop/goods/detail",
|
||||
name: "GoodsDetail",
|
||||
component: () => import("@/views/shop/goods/detail.vue"),
|
||||
meta: {
|
||||
title: "商品详情"
|
||||
}
|
||||
}
|
||||
]
|
||||
} as RouteConfigsTable;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { store } from "@/store";
|
||||
import { QyUserLoginDTO } from "@/api/common/login";
|
||||
|
||||
|
||||
export const useWxStore = defineStore("wx", () => {
|
||||
|
@ -15,12 +16,18 @@ export const useWxStore = defineStore("wx", () => {
|
|||
const userid = ref<string>("")
|
||||
// 初始化
|
||||
const isInit = ref<boolean>(false);
|
||||
const qyUser = ref<QyUserLoginDTO>(null);
|
||||
|
||||
// 设置 userid
|
||||
const setUserid = (id: string) => {
|
||||
userid.value = id
|
||||
}
|
||||
|
||||
const setQyUser = (user: QyUserLoginDTO) => {
|
||||
if (!user) return;
|
||||
qyUser.value = user;
|
||||
}
|
||||
|
||||
const initWx = () => {
|
||||
if (isInit.value) return;
|
||||
isInit.value = true;
|
||||
|
@ -49,7 +56,10 @@ export const useWxStore = defineStore("wx", () => {
|
|||
}
|
||||
}
|
||||
|
||||
return { corpid, code, state, userid, isInit, setUserid, handleWxCallback, initWx }
|
||||
return {
|
||||
corpid, code, state, userid, isInit, qyUser,
|
||||
setUserid, handleWxCallback, initWx, setQyUser
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
|
|
|
@ -103,6 +103,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
|
|||
.then(({ data }) => {
|
||||
// 登录成功后 将token存储到sessionStorage中
|
||||
setTokenFromBackend(data);
|
||||
wxStore.setQyUser(data.qyUser);
|
||||
// 获取后端路由
|
||||
initRouter().then(() => {
|
||||
router.push(getTopMenu(true).path);
|
||||
|
@ -132,6 +133,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
|
|||
.then(({ data }) => {
|
||||
// 登录成功后 将token存储到sessionStorage中
|
||||
setTokenFromBackend(data);
|
||||
wxStore.setQyUser(data.qyUser);
|
||||
// 获取后端路由
|
||||
initRouter().then(() => {
|
||||
router.push(getTopMenu(true).path);
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { getGoodsInfo, GoodsDTO } from "@/api/shop/goods";
|
||||
import { ElMessage } from "element-plus";
|
||||
const { VITE_PUBLIC_IMG_PATH: IMG_PATH } = import.meta.env;
|
||||
|
||||
defineOptions({
|
||||
name: "GoodsDetail"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const goodsInfo = ref<GoodsDTO>({
|
||||
goodsName: "",
|
||||
categoryId: 0,
|
||||
price: 0,
|
||||
stock: 0,
|
||||
status: 0,
|
||||
autoApproval: 0,
|
||||
coverImg: "",
|
||||
goodsDetail: ""
|
||||
});
|
||||
const loading = ref(false);
|
||||
const activeTab = ref('basic');
|
||||
const goodsId = ref<number>(0);
|
||||
|
||||
async function fetchGoodsDetail() {
|
||||
try {
|
||||
loading.value = true;
|
||||
const { data } = await getGoodsInfo(goodsId.value);
|
||||
goodsInfo.value = data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
goodsId.value = Number(route.query.id);
|
||||
fetchGoodsDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="detail-container">
|
||||
<div class="flex-container">
|
||||
<el-card class="goods-info-card">
|
||||
<div class="goods-header">
|
||||
<img :src="goodsInfo.coverImg" class="goods-image" />
|
||||
<div class="goods-name">{{ goodsInfo.goodsName }}</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<el-descriptions class="goods-details" :column="1" border>
|
||||
<el-descriptions-item label="名称">{{ goodsInfo.goodsName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="价格">{{ goodsInfo.price }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库存">{{ goodsInfo.stock }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ goodsInfo.status === 1 ? '已上架' : '已下架' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
|
||||
<el-card class="info-card">
|
||||
<div class="tab-header">
|
||||
<el-tabs type="card" v-model="activeTab">
|
||||
<el-tab-pane label="基本信息" name="basic"></el-tab-pane>
|
||||
<el-tab-pane label="库存记录" name="stock"></el-tab-pane>
|
||||
<el-tab-pane label="销售记录" name="sales"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
||||
<div class="info-details" v-if="activeTab === 'basic'">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="名称">{{ goodsInfo.goodsName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="价格">{{ goodsInfo.price }}</el-descriptions-item>
|
||||
<el-descriptions-item label="库存">{{ goodsInfo.stock }}</el-descriptions-item>
|
||||
<el-descriptions-item label="已分配库存">{{ goodsInfo.totalStock }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ goodsInfo.status === 1 ? '已上架' : '已下架' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="自动审批">{{ goodsInfo.autoApproval === 1 ? '开启' : '关闭' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="商品描述" :span="2">
|
||||
<div v-html="goodsInfo.goodsDetail"></div>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="使用说明" :span="2">
|
||||
{{ goodsInfo.usageInstruction || '暂无说明' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="stock-details" v-if="activeTab === 'stock'">
|
||||
<!-- 库存记录表格 -->
|
||||
</div>
|
||||
|
||||
<div class="sales-details" v-if="activeTab === 'sales'">
|
||||
<!-- 销售记录表格 -->
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.detail-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.flex-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
|
||||
.goods-info-card {
|
||||
width: 20%;
|
||||
height: 88vh;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
width: 80%;
|
||||
height: 88vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.goods-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.goods-image {
|
||||
width: 100%;
|
||||
height: 320px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.goods-name {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-header {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -14,11 +14,14 @@ import GoodsFormModal from "./goods-form-modal.vue";
|
|||
import GoodsEditModal from "./goods-edit-modal.vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { deleteGoodsApi } from "@/api/shop/goods";
|
||||
import { useMultiTagsStoreHook } from "@/store/modules/multiTags";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
defineOptions({
|
||||
name: "ShopGoods"
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const formRef = ref();
|
||||
const tableRef = ref();
|
||||
const modalVisible = ref(false);
|
||||
|
@ -126,6 +129,25 @@ const handleEdit = (row: GoodsDTO) => {
|
|||
editVisible.value = true;
|
||||
};
|
||||
|
||||
const handleViewDetail = (row: GoodsDTO) => {
|
||||
// 保存信息到标签页
|
||||
useMultiTagsStoreHook().handleTags("push", {
|
||||
path: `/shop/goods/detail`,
|
||||
name: "GoodsDetail",
|
||||
query: { id: row.goodsId },
|
||||
meta: {
|
||||
title: `${row.goodsName}`,
|
||||
dynamicLevel: 3
|
||||
}
|
||||
});
|
||||
router.push({
|
||||
path: '/shop/goods/detail',
|
||||
query: {
|
||||
id: row.goodsId
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -164,14 +186,14 @@ const handleEdit = (row: GoodsDTO) => {
|
|||
<div class="card-content">
|
||||
<el-image :src="item.coverImg" :preview-src-list="[item.coverImg]" class="goods-image" fit="contain" />
|
||||
<div class="goods-info">
|
||||
<div class="name">名称:{{ item.goodsName }}</div>
|
||||
<div class="price">价格:{{ item.price }}</div>
|
||||
<div class="stock">库存:{{ item.stock }}</div>
|
||||
<div class="info-item">状态:{{ item.status === 1 ? '已上架' : '已下架' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-divider class="divider" />
|
||||
<el-button class="detail-btn" :icon="useRenderIcon(View)" @click="handleEdit(item)" />
|
||||
<div class="detail-btn" @click="handleEdit(item)">
|
||||
{{ item.goodsName }}
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
@ -223,52 +245,67 @@ const handleEdit = (row: GoodsDTO) => {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0px;
|
||||
/* &:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
} */
|
||||
}
|
||||
|
||||
.goods-image {
|
||||
width: 55%;
|
||||
height: 200px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
.card-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0px;
|
||||
|
||||
.goods-image {
|
||||
width: 55%;
|
||||
height: 180px;
|
||||
object-fit: contain;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
padding: 16px 0 0 0;
|
||||
|
||||
.name,
|
||||
.info-item,
|
||||
.price,
|
||||
.stock {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.goods-info {
|
||||
flex: 1;
|
||||
padding: 16px 0;
|
||||
|
||||
.name,
|
||||
.info-item,
|
||||
.price,
|
||||
.stock {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
/* .price,
|
||||
/* .price,
|
||||
.info-item,
|
||||
.stock {
|
||||
color: #909399;
|
||||
} */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
&:hover {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 5px 0px;
|
||||
}
|
||||
|
||||
.detail-btn {
|
||||
width: 100%;
|
||||
border: 0;
|
||||
margin-top: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
width: 100%;
|
||||
margin-top: auto;
|
||||
padding: 4px 0;
|
||||
text-align: center;
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.grid-container {
|
||||
|
|
|
@ -2,11 +2,16 @@
|
|||
import { Goods, Shop, Document, Money } from '@element-plus/icons-vue';
|
||||
import { getStats, TodayLatestOrderGoodsDTO, TopGoodsDTO } from '@/api/shop/stats';
|
||||
import { markRaw, onMounted, ref } from 'vue';
|
||||
import { useWxStore } from '@/store/modules/wx';
|
||||
import { ElDialog, ElForm, ElFormItem, ElInput, ElMessage } from 'element-plus';
|
||||
import { bindQyUserApi } from '@/api/ab98/user';
|
||||
|
||||
defineOptions({
|
||||
name: "Welcome"
|
||||
});
|
||||
|
||||
const wxStore = useWxStore();
|
||||
|
||||
const shopData = ref<{
|
||||
name: string;
|
||||
icon: any; // 假设这里是 Element Plus 的图标组件
|
||||
|
@ -32,6 +37,31 @@ const topGoods = ref<TopGoodsDTO[]>([]);
|
|||
const todayLatestOrderGoods = ref<TodayLatestOrderGoodsDTO[]>([]);
|
||||
const maxOccurrenceCount = ref(0);
|
||||
|
||||
const showDialog = ref(false);
|
||||
const form = ref({ name: '', idCard: '' });
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
||||
idCard: [
|
||||
{ required: true, message: '请输入身份证号', trigger: 'blur' },
|
||||
{ pattern: /^\d{17}[\dXx]$/, message: '身份证格式不正确', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const { data } = await bindQyUserApi({
|
||||
qyUserId: wxStore.qyUser?.id,
|
||||
name: form.value.name,
|
||||
idNum: form.value.idCard
|
||||
});
|
||||
ElMessage.success('绑定成功');
|
||||
showDialog.value = false;
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '绑定失败');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await getStats();
|
||||
|
@ -59,6 +89,11 @@ onMounted(async () => {
|
|||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error);
|
||||
}
|
||||
|
||||
|
||||
if (wxStore.qyUser && !wxStore.qyUser.ab98UserId) {
|
||||
showDialog.value = true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -187,6 +222,20 @@ onMounted(async () => {
|
|||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-dialog v-model="showDialog" title="请绑定汇邦云账号" :close-on-click-modal="false" width="30%">
|
||||
<el-form :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="姓名" prop="name">
|
||||
<el-input v-model="form.name" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证号" prop="idCard">
|
||||
<el-input v-model="form.idCard" maxlength="18" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button type="primary" @click="handleSubmit">提交</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
Loading…
Reference in New Issue