feat(用户认证): 添加企业微信用户绑定功能并完善商品详情页

- 在登录流程中存储企业微信用户信息到store
- 添加企业微信用户绑定接口及相关类型定义
- 当检测到未绑定用户时显示绑定对话框
- 新增商品详情路由及页面,优化商品列表交互
- 改进商品卡片样式和点击跳转逻辑
This commit is contained in:
dzq 2025-06-03 16:25:51 +08:00
parent 8f6ae14e47
commit 12cef7b84f
9 changed files with 372 additions and 41 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
})
/**

View File

@ -103,6 +103,7 @@ const onLogin = async (formEl: FormInstance | undefined) => {
.then(({ data }) => {
// tokensessionStorage
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 }) => {
// tokensessionStorage
setTokenFromBackend(data);
wxStore.setQyUser(data.qyUser);
//
initRouter().then(() => {
router.push(getTopMenu(true).path);

View File

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

View File

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

View File

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