Compare commits

...

7 Commits

Author SHA1 Message Date
dzq 1582ca6f2f feat(router): 添加企业微信登录校验逻辑
在全局前置守卫中添加企业微信登录校验,当URL中包含corpid参数时,直接放行。同时,在订单提交逻辑中,根据用户类型(企业微信用户、汇邦云用户、外部用户)设置isInternal字段,并完善订单提交数据的字段。
2025-04-12 11:32:01 +08:00
dzq 4891d9376c refactor: 优化登录页面和用户存储逻辑
- 将API请求路径中的`/api`前缀移除,简化URL
- 使用`encodeURIComponent`和`decodeURIComponent`替代`btoa`和`atob`,提高数据存储安全性
- 重构登录页面,使用Vant组件替换Element UI,优化用户体验
2025-04-11 16:39:08 +08:00
dzq 7a9847bb25 feat(登录): 新增AB98登录功能及相关路由和状态管理
添加了AB98登录页面、路由配置、白名单管理、用户状态存储及API接口,实现了手机验证码登录功能。同时更新了用户信息展示逻辑,确保登录后正确显示用户信息。
2025-04-11 11:11:13 +08:00
dzq 533a94ca9b refactor(api): 将 `message` 字段统一改为 `msg` 并更新相关逻辑
统一接口响应中的 `message` 字段为 `msg`,并在相关页面更新错误提示逻辑,确保一致性。同时在审批处理页面增加对审核图片的校验,并优化错误处理逻辑。
2025-04-10 10:30:55 +08:00
dzq adf5bc4d20 refactor(approval): 优化审批页面样式和逻辑
- 移除不必要的 `height: 100px` 样式
- 调整审批状态选项的文本
- 增加退款金额的验证逻辑
- 添加商品封面显示和历史审批数据填充功能
- 优化表单字段的只读状态和样式
- 调整提交按钮的定位和样式
2025-04-10 08:19:05 +08:00
dzq ac5d9292ca feat(审批): 新增审批处理页面及相关功能
新增审批处理页面,支持审批状态的更新、退款金额的填写、审核说明的输入以及审核凭证的上传。同时,优化了审批列表页面的交互,点击列表项可跳转至审批处理页面。引入了Pinia状态管理,用于存储当前审批单的详细信息。
2025-04-09 16:41:54 +08:00
dzq 9c81c228ee feat(approval): 添加审批中心功能,包括审批列表和提交审批
新增审批中心页面,支持管理员查看审批列表,包含搜索、分页和状态筛选功能。同时优化了提交审批页面,增加退货备注字段,并改进上传文件后的反馈提示。
2025-04-09 10:28:48 +08:00
21 changed files with 1138 additions and 23 deletions

View File

@ -0,0 +1,55 @@
import { request } from '@/http/axios'
import {
GetTokenParams,
LoginData,
LogoutResponse,
SmsSendResponse,
TokenResponse,
VerifySmsParams,
WechatQrCodeParams
} from './type'
/** 获取临时令牌 */
export function getTokenApi(appName: string) {
return request<ApiResponseData<TokenResponse>>({
url: '/wx/login/getToken',
method: 'get',
params: { appName }
})
}
/** 获取微信登录二维码 */
export function getWechatQrCodeApi(token: string) {
return request<ApiResponseData<string>>({
url: '/wx/login/wechat/qrcode',
method: 'get',
params: { token }
})
}
/** 发送短信验证码 */
export function sendSmsApi(token: string, tel: string) {
return request<ApiResponseData<SmsSendResponse>>({
url: '/wx/login/sendSms',
method: 'post',
params: { token, tel }
})
}
/** 验证短信验证码 */
export function verifySmsApi(params: VerifySmsParams) {
return request<ApiResponseData<LoginData>>({
url: '/wx/login/verifySms',
method: 'post',
params
})
}
/** 用户退出登录 */
export function logoutApi(token: string) {
return request<ApiResponseData<LogoutResponse>>({
url: '/wx/login/logout',
method: 'post',
params: { token }
})
}

View File

@ -0,0 +1,59 @@
/** 令牌响应 */
export interface TokenResponse {
/** 认证令牌 */
token: string
}
/** 退出登录响应 */
export interface LogoutResponse {
/** 是否成功 */
success: boolean
}
/** 短信发送响应 */
export interface SmsSendResponse {
/** 发送状态 */
success: boolean
/** 错误信息 */
message?: string
}
/** 登录数据 */
export interface LoginData {
/** 用户头像 */
face_img: string
/** 登录状态 */
success: boolean
/** 用户性别 */
sex: string
/** 用户姓名 */
name: string
/** 用户ID */
userid: string
/** 是否已注册 */
registered: boolean
/** 联系电话 */
tel: string
}
/** 获取令牌参数 */
export type GetTokenParams = {
/** 应用名称 */
appName: string
}
/** 微信二维码参数 */
export type WechatQrCodeParams = {
/** 认证令牌 */
token: string
}
/** 短信验证参数 */
export type VerifySmsParams = {
/** 认证令牌 */
token: string
/** 手机号码 */
tel: string
/** 验证码 */
vcode: string
}

View File

@ -1,5 +1,15 @@
import { request } from '@/http/axios'
import { SubmitApprovalRequestData, SubmitApprovalResponseData } from './type'
import { SubmitApprovalRequestData, SubmitApprovalResponseData, SearchApiReturnApprovalQuery, ApiResponsePageData, ReturnApprovalEntity, HandleApprovalRequestData } from './type'
export const getApprovalListApi = (params: SearchApiReturnApprovalQuery) => {
return request<ApiResponsePageData<ReturnApprovalEntity>>({
url: 'approval/list',
method: 'get',
params
})
}
export const submitApprovalApi = (data: SubmitApprovalRequestData) => {
return request<SubmitApprovalResponseData>({
@ -8,3 +18,11 @@ export const submitApprovalApi = (data: SubmitApprovalRequestData) => {
data
})
}
export const handleApprovalApi = (data: HandleApprovalRequestData) => {
return request<ApiResponseMsgData<string>>({
url: 'approval/handle',
method: 'post',
data
})
}

View File

@ -2,6 +2,65 @@ export interface SubmitApprovalRequestData {
orderGoodsId: number
returnQuantity: number
returnImages: string
returnRemark: string
}
export interface HandleApprovalRequestData {
/** 审批ID */
approvalId: number
/** 审批状态 */
status: number
returnAmount: number
auditImages: string
auditRemark: string
}
export interface SearchApiReturnApprovalQuery {
pageNum: number
pageSize: number
approvalId?: number
orderId?: number
goodsId?: number
status?: number
startTime?: string
endTime?: string
}
export interface ApiResponsePageData<T> {
code: number
msg: string
data: {
total: number
rows: T[]
}
}
export interface ReturnApprovalEntity {
approvalId: number
orderId: number
goodsId: number
/** 关联订单商品ID */
orderGoodsId: number
/** 归还数量 */
returnQuantity: number
/** 商品单价 */
goodsPrice: number
status: number
returnAmount: number
/** 归还图片路径数组 */
returnImages: string
/** 审核图片路径数组 */
auditImages: string
/** 归还说明 */
returnRemark: string
/** 审核说明 */
auditRemark: string
createTime: string
updateTime: string
/** 商品名称 */
goodsName: string
/** 封面图URL */
coverImg: string
}
export type SubmitApprovalResponseData = ApiResponseMsgData<{

View File

@ -16,10 +16,21 @@ export type category = {
}
export interface SubmitOrderRequestData {
/** 微信用户唯一标识 */
openid: string;
/** 系统用户ID */
userid: string;
/** 企业ID */
corpid: string;
/** 支付类型 wechat:微信 balance:余额 */
paymentType: 'wechat' | 'balance';
/** 联系电话 */
mobile: string;
/** 企业微信用户ID或汇邦云用户ID */
qyUserid: string;
/** 是否内部订单 0否 1汇邦云用户 2企业微信用户 */
isInternal: number;
/** 订单商品明细列表 */
goodsList: Array<{
goodsId: number
quantity: number

View File

@ -3,7 +3,7 @@ const router = useRouter()
const tabbarItemList = computed(() => {
const routes = router.getRoutes()
return routes.filter(route => route.meta.layout?.tabbar?.showTabbar)
return routes.filter(route => route.meta.layout?.tabbar?.showTabbar && route.path !== '/cabinet')
.map(route => ({
title: route.meta.title,
icon: route.meta.layout?.tabbar?.icon,

View File

@ -0,0 +1,358 @@
<script setup lang="ts">
import { ref } from 'vue'
import { showConfirmDialog, showSuccessToast, showToast, UploaderFileListItem, Popup, Picker } from 'vant'
const showStatusPicker = ref(false)
const showPreview = ref(false)
const currentPreviewImage = ref('')
const statusMap: { [key: number]: string } = {
1: '待审核',
2: '已通过',
3: '已驳回'
}
const statusOptions = [
{ text: '待审核', value: 1 },
{ text: '通过', value: 2 },
{ text: '驳回', value: 3 }
]
import axios from "axios"
import { handleApprovalApi } from '@/common/apis/approval'
import type { HandleApprovalRequestData } from '@/common/apis/approval/type'
import { useRoute, useRouter } from 'vue-router'
import { useApprovalStore } from '@/pinia/stores/approval'
const { VITE_APP_BASE_API } = import.meta.env;
const router = useRouter()
const route = useRoute()
const approvalStore = useApprovalStore()
const formData = ref<HandleApprovalRequestData>({
approvalId: approvalStore.currentApproval?.approvalId || 0,
status: 1,
returnAmount: 0,
auditImages: '',
auditRemark: ''
})
const submitting = ref(false)
const fileList = ref<UploaderFileListItem[]>([])
const uploading = ref(false)
const validateForm = () => {
if (!formData.value.approvalId || isNaN(formData.value.approvalId)) {
showConfirmDialog({ title: '错误', message: '审批单ID参数错误' })
return false
}
if (formData.value.returnAmount <= 0) {
showConfirmDialog({ title: '提示', message: '退款金额需大于0' })
return false
}
if (null != approvalStore.currentApproval &&
formData.value.returnAmount > approvalStore.currentApproval.goodsPrice) {
showConfirmDialog({ title: '提示', message: '退款金额不能超过商品价格' })
return false
}
if (!formData.value.auditImages) {
showConfirmDialog({ title: '提示', message: '请上传审核图片' })
return false
}
if (formData.value.status === 3 && !formData.value.auditRemark) {
showConfirmDialog({ title: '提示', message: '驳回时必须填写审核说明' })
return false
}
return true
}
const handleFileUpload = async (items: UploaderFileListItem | UploaderFileListItem[]) => {
const files = Array.isArray(items) ? items : [items]
uploading.value = true
try {
const uploadPromises = files.map(async (item) => {
item.status = 'uploading'
item.message = '上传中...'
const file = item.file as File
const formData = new FormData()
formData.append('file', file)
const { data } = await axios.post<{
code: number
data: { url: string }
message?: string
}>(VITE_APP_BASE_API + '/file/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' }
})
if (data.code !== 0) {
throw new Error(data.message || '文件上传失败')
}
return { url: data.data.url }
})
const urls = await Promise.all(uploadPromises)
files.forEach((item, index) => {
item.status = 'done'
item.message = '上传成功'
item.url = urls[index].url
})
formData.value.auditImages = fileList.value.map(item => item.url).join(',')
} catch (error) {
showConfirmDialog({
title: '上传失败',
message: error instanceof Error ? error.message : '未知错误'
})
} finally {
uploading.value = false
}
}
const onStatusConfirm = ({ selectedOptions }: { selectedOptions: { value: number }[] }) => {
submitting.value = true
try {
if (!selectedOptions?.[0]?.value) {
throw new Error('请选择有效的审批状态')
}
formData.value.status = selectedOptions[0].value
showStatusPicker.value = false
} catch (error) {
showConfirmDialog({
title: '操作失败',
message: error instanceof Error ? error.message : '状态选择异常'
})
} finally {
submitting.value = false
}
}
const previewImage = (url: string) => {
currentPreviewImage.value = url
showPreview.value = true
}
onMounted(() => {
if (!approvalStore.currentApproval) {
router.push('/approval/list')
} else if (approvalStore.currentApproval.status !== 1) {
//
formData.value = {
...formData.value,
status: approvalStore.currentApproval.status,
returnAmount: approvalStore.currentApproval.returnAmount,
auditRemark: approvalStore.currentApproval.auditRemark,
auditImages: approvalStore.currentApproval.auditImages
}
//
if (approvalStore.currentApproval.auditImages) {
fileList.value = approvalStore.currentApproval.auditImages.split(',').map(url => ({
url,
status: 'done',
message: '已上传'
}))
}
}
})
const handleSubmit = async () => {
if (!validateForm()) return
submitting.value = true
try {
const { code, msg } = await handleApprovalApi(formData.value)
if (code === 0) {
showSuccessToast('操作成功')
await showConfirmDialog({
title: "操作成功",
message: `审批处理已完成`
})
router.push('/approval/list')
} else {
console.error('操作失败code:', code, 'msg:', msg)
showConfirmDialog({
title: '操作失败',
message: msg || '操作失败'
})
}
} catch (error) {
console.error('提交失败:', error)
showConfirmDialog({
title: '提交失败',
message: error instanceof Error ? error.message : '网络请求异常'
})
} finally {
submitting.value = false
}
}
</script>
<template>
<div class="approval-container">
<van-nav-bar title="审批处理" left-text="返回" left-arrow fixed @click-left="() => router.go(-1)" />
<div class="content-wrapper">
<van-cell-group>
<van-cell title="商品名称" :value="approvalStore.currentApproval?.goodsName" />
<van-cell title="商品封面" v-if="approvalStore.currentApproval?.coverImg">
<van-image
:src="approvalStore.currentApproval.coverImg"
fit="cover"
width="80"
height="80"
@click="previewImage(approvalStore.currentApproval.coverImg)"
style="margin-top: 8px"
/>
</van-cell>
<van-cell title="退还数量" :value="approvalStore.currentApproval?.returnQuantity" />
<van-cell title="商品单价" :value="`¥${approvalStore.currentApproval?.goodsPrice}`" />
<van-cell title="当前状态" :value="statusMap[approvalStore.currentApproval?.status || 1]" />
<van-cell title="退还说明" :value="approvalStore.currentApproval?.returnRemark" />
</van-cell-group>
<van-cell-group class="image-section">
<van-cell title="退还凭证">
<van-grid :column-num="3" gutter="10">
<van-grid-item v-for="(img, index) in approvalStore.currentApproval?.returnImages.split(',')"
:key="index">
<van-image :src="img" fit="cover" @click="previewImage(img)" />
</van-grid-item>
</van-grid>
</van-cell>
</van-cell-group>
<van-cell-group>
<!-- 原有表单字段保持不变 -->
<van-field
v-model="formData.returnAmount"
:readonly="approvalStore.currentApproval?.status !== 1"
label="退款金额"
type="number"
class="audit-remark-field"
:min="0"
/>
<van-field
:model-value="statusOptions.find(opt => opt.value === formData.status)?.text || ''"
label="审批结果"
readonly
@click="showStatusPicker = true"
:disabled="approvalStore.currentApproval?.status !== 1"
class="clickable-status-field"
/>
<van-field
v-model="formData.auditRemark"
label="审核说明"
type="textarea"
rows="2"
autosize
class="audit-remark-field"
/>
</van-cell-group>
<van-popup v-model:show="showStatusPicker" position="bottom">
<van-picker :columns="statusOptions" @confirm="onStatusConfirm" @cancel="showStatusPicker = false" />
</van-popup>
<van-popup v-model:show="showPreview" position="center" :style="{ width: '90%', height: '80%' }" round>
<div class="preview-wrapper">
<van-icon name="cross" class="close-icon" @click="showPreview = false" />
<van-image :src="currentPreviewImage" fit="contain" class="preview-image" />
</div>
</van-popup>
<van-cell-group class="upload-section">
<van-cell title="审核凭证">
<template #extra>
<van-uploader v-model="fileList" multiple max-count="3" :after-read="handleFileUpload" />
</template>
</van-cell>
</van-cell-group>
<div class="submit-bar"
v-if="approvalStore.currentApproval?.status !== 2">
<van-button
type="primary"
block
:loading="submitting"
loading-text="提交中..."
@click="handleSubmit"
>
提交审批
</van-button>
</div>
</div>
</div>
</template>
<style scoped>
.approval-container {
padding: 12px 16px;
}
.content-wrapper {
padding-top: 46px;
}
.upload-section {
margin: 20px 0;
}
.preview-wrapper {
position: relative;
height: 100%;
padding: 20px;
.close-icon {
position: absolute;
top: 10px;
right: 10px;
z-index: 1;
font-size: 24px;
color: #fff;
background: rgba(0, 0, 0, 0.3);
border-radius: 50%;
padding: 4px;
}
.preview-image {
width: 100%;
height: 100%;
}
}
.submit-bar {
position: sticky;
bottom: 20px;
padding: 16px;
background: #fff;
box-shadow: 0 -2px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
margin: 0 16px;
z-index: 1;
}
.content-wrapper {
padding-bottom: 100px;
}
.audit-remark-field::v-deep .van-field__control {
background-color: #fffbe6;
padding: 8px;
border-radius: 4px;
}
.clickable-status-field:not(:disabled)::v-deep .van-field__control {
cursor: pointer;
transition: all 0.3s ease;
background-color: #f7f8fa;
}
.clickable-status-field:not(:disabled):hover::v-deep .van-field__control {
background-color: #f2f3f5;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.clickable-status-field:not(:disabled):active::v-deep .van-field__control {
background-color: #ebedf0;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
</style>

192
src/pages/approval/list.vue Normal file
View File

@ -0,0 +1,192 @@
<template>
<div class="approval-list-container">
<!-- 搜索表单 -->
<van-form @submit="handleSearch">
<van-field v-model="searchParams.approvalId" label="审批单号" type="number" placeholder="请输入审批单号" />
<van-field v-model="searchParams.orderId" label="订单编号" type="number" placeholder="请输入订单编号" />
<van-field v-model="searchParams.goodsId" label="商品ID" type="number" placeholder="请输入商品ID" />
<van-field name="status" label="审批状态" readonly clickable v-model="statusText" placeholder="请选择状态"
@click="showStatusPicker = true" />
<van-popup v-model:show="showStatusPicker" position="bottom">
<van-picker :columns="statusOptions" @confirm="onStatusConfirm" @cancel="showStatusPicker = false" />
</van-popup>
<!-- <van-field
readonly
clickable
name="date"
:value="dateRangeText"
label="申请时间"
placeholder="选择时间范围"
@click="showDatePicker = true"
/>
<van-popup v-model:show="showDatePicker" position="bottom">
<van-datetime-picker
type="daterange"
@confirm="onDateConfirm"
@cancel="showDatePicker = false"
/>
</van-popup> -->
<div style="margin: 16px;">
<van-button block type="primary" native-type="submit">搜索</van-button>
<van-button block plain type="primary" style="margin-top: 10px;" @click="handleReset">重置</van-button>
</div>
</van-form>
<!-- 审批列表 -->
<van-list v-model:loading="loading" :finished="finished" finished-text="没有更多了" @load="onLoad">
<van-cell v-for="item in list" :key="item.approvalId" :title="`审批单号:${item.approvalId}`" clickable
@click="handleCellClick(item.approvalId)">
<template #label>
<div>商品名称{{ item.goodsName }}</div>
<div>申请时间{{ item.createTime }}</div>
<van-tag :type="statusTagType(item.status)">
{{ statusMap[item.status] }}
</van-tag>
</template>
</van-cell>
</van-list>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue'
import { getApprovalListApi } from '@/common/apis/approval'
import type { SearchApiReturnApprovalQuery, ReturnApprovalEntity } from '@/common/apis/approval/type'
import type { PickerConfirmEventParams } from 'vant/es';
import { useApprovalStore } from '@/pinia/stores/approval';
const router = useRouter()
//
const searchParams = reactive<SearchApiReturnApprovalQuery>({
pageNum: 1,
pageSize: 10,
})
//
const showStatusPicker = ref(false)
const statusOptions = [
{ text: '全部', value: undefined },
{ text: '待审核', value: 1 },
{ text: '已通过', value: 2 },
{ text: '已驳回', value: 3 },
]
const statusMap: { [key: number]: string } = {
1: '待审核',
2: '已通过',
3: '已驳回'
}
const statusText = ref('')
//
const showDatePicker = ref(false)
const dateRangeText = ref('')
//
const list = ref<ReturnApprovalEntity[]>([])
const loading = ref(false)
const finished = ref(false)
const handleCellClick = (approvalId: number) => {
const approvalStore = useApprovalStore()
const currentItem = list.value.find(item => item.approvalId === approvalId)
if (currentItem) {
approvalStore.setCurrentApproval(currentItem)
}
router.push(`/approval/handle/${approvalId}`)
}
//
const statusTagType = (status: number) => {
switch (status) {
case 1: return 'warning'
case 2: return 'success'
case 3: return 'danger'
default: return 'default'
}
}
//
const onStatusConfirm = (e: PickerConfirmEventParams) => {
const { selectedOptions } = e;
searchParams.status = selectedOptions[0]?.value ? Number(selectedOptions[0]?.value) : undefined;
showStatusPicker.value = false;
// statusText
statusText.value = String(selectedOptions[0]?.text || '');
}
//
const onDateConfirm = (values: Date[]) => {
const [start, end] = values
searchParams.startTime = start.toISOString().split('T')[0]
searchParams.endTime = end.toISOString().split('T')[0]
dateRangeText.value = `${searchParams.startTime}${searchParams.endTime}`
showDatePicker.value = false
}
//
const handleSearch = () => {
list.value = []
searchParams.pageNum = 1
onLoad()
}
//
const handleReset = () => {
Object.assign(searchParams, {
pageNum: 1,
pageSize: 10,
approvalId: undefined,
orderId: undefined,
goodsId: undefined,
status: undefined,
startTime: undefined,
endTime: undefined
})
statusText.value = ''
dateRangeText.value = ''
handleSearch()
}
//
const onLoad = async () => {
try {
const { data } = await getApprovalListApi(searchParams)
list.value.push(...data.rows)
loading.value = false
if (list.value.length >= data.total) {
finished.value = true
} else {
searchParams.pageNum++
}
} catch (error) {
console.error('获取审批列表失败', error)
finished.value = true
}
}
</script>
<style scoped>
.approval-list-container {
padding: 12px;
}
.van-cell__title {
font-size: 14px;
color: #333;
}
.van-cell__label {
margin-top: 8px;
color: #666;
font-size: 12px;
}
</style>

View File

@ -13,7 +13,8 @@ const route = useRoute()
const formData = ref<SubmitApprovalRequestData>({
orderGoodsId: Number(route.query.orderGoodsId),
returnQuantity: 1,
returnImages: ''
returnImages: '',
returnRemark: ''
})
watch(() => route.query.orderGoodsId, (newVal) => {
@ -71,10 +72,9 @@ const handleFileUpload = async (items: UploaderFileListItem | UploaderFileListIt
const urls = await Promise.all(uploadPromises)
files.forEach((item, index) => {
item.status = 'done'
item.message = ''
item.message = '上传成功'
item.url = urls[index].url
})
showToast('上传成功')
} catch (error) {
showConfirmDialog({
title: '上传失败',
@ -96,7 +96,13 @@ const handleSubmit = async () => {
if (code === 0) {
showSuccessToast('提交成功')
router.push('/order/' + orderId)
try {
await showConfirmDialog({
title: "提交成功",
message: `退货申请已提交,等待管理员审核`
})
} catch (error) { }
router.push('/order/' + orderId.value)
}
} catch (error) {
console.error('提交失败:', error)
@ -118,6 +124,7 @@ const handleSubmit = async () => {
<van-cell-group>
<!-- 移除订单ID和商品ID的输入框 -->
<van-field v-model="formData.returnQuantity" label="退还数量" type="number" :min="1" />
<van-field v-model="formData.returnRemark" label="退货备注" type="textarea" rows="2" autosize />
</van-cell-group>
<van-cell-group class="upload-section">

View File

@ -251,7 +251,6 @@ loadCabinetDetail()
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100px;
position: relative;
padding-left: 3px;
}

View File

@ -0,0 +1,135 @@
<template>
<div class="login-container">
<van-form @submit="handleSubmit">
<van-field
v-model="form.tel"
name="手机号"
label="手机号"
placeholder="请输入手机号"
:rules="[{ required: true, message: '请填写手机号' }, { pattern: /^1[3-9]\d{9}$/, message: '手机号格式错误' }]"
/>
<van-field
v-model="form.vcode"
center
clearable
name="验证码"
label="验证码"
placeholder="请输入验证码"
:rules="[{ required: true, message: '请填写验证码' }]"
>
<template #button>
<van-button
size="small"
:disabled="countdown > 0"
@click="sendSms"
native-type="button"
>
{{ countdown > 0 ? `${countdown}秒后重试` : '获取验证码' }}
</van-button>
</template>
</van-field>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
立即登录
</van-button>
</div>
</van-form>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { showSuccessToast, showFailToast } from 'vant'
import { getTokenApi, sendSmsApi, verifySmsApi } from '@/common/apis/ab98'
import { useAb98UserStore } from '@/pinia/stores/ab98-user'
const userStore = useAb98UserStore()
const router = useRouter()
const form = ref({
tel: '',
vcode: ''
})
const countdown = ref(0)
const loading = ref(true)
let timer: number | null = null
onMounted(async () => {
try {
const { data } = await getTokenApi('ab98_app')
if (data.token) {
userStore.setToken(data.token)
} else {
showFailToast('令牌获取失败')
}
} catch (err) {
showFailToast('网络异常,请重试')
} finally {
loading.value = false
}
})
const sendSms = async () => {
try {
if (!/^1[3-9]\d{9}$/.test(form.value.tel)) {
showFailToast('手机号格式错误')
return
}
const { data } = await sendSmsApi(userStore.token, form.value.tel)
if (data.success) {
startCountdown()
showSuccessToast('验证码已发送')
} else {
showFailToast(data.message || '发送失败')
}
} catch (err) {
showFailToast('请求异常,请稍后重试')
}
}
const startCountdown = () => {
countdown.value = 60
timer = window.setInterval(() => {
if (countdown.value <= 0 && timer) {
window.clearInterval(timer)
return
}
countdown.value--
}, 1000)
}
const handleSubmit = async () => {
try {
const { data } = await verifySmsApi({
token: userStore.token,
tel: form.value.tel,
vcode: form.value.vcode
})
if (data.success) {
userStore.setTel(form.value.tel)
userStore.setUserInfo(data)
userStore.setIsLogin(true)
showSuccessToast('登录成功')
router.push('/')
} else {
showFailToast('验证码错误')
}
} catch (err) {
console.error(err)
showFailToast('登录失败,请稍后重试')
}
}
</script>
<style scoped>
.login-container {
padding: 20px;
margin-top: 30%;
}
</style>

View File

@ -1,17 +1,22 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useWxStore } from '@/pinia/stores/wx'
import { computed } from 'vue'
import { useAb98UserStore } from '@/pinia/stores/ab98-user'
import { storeToRefs } from 'pinia'
import { publicPath } from "@/common/utils/path"
const router = useRouter()
const wxStore = useWxStore()
const balance = computed(() => wxStore.balance)
const ab98UserStore = useAb98UserStore()
const { balance } = storeToRefs(wxStore)
const { name: userName, sex: userSex, face_img } = storeToRefs(ab98UserStore)
const userAvatar = face_img.value ? face_img.value : `${publicPath}img/1.jpg`
</script>
<template>
<div un-py-16px>
<!-- 用户信息区域 -->
<van-cell-group class="user-card">
<van-cell :border="false">
<template #title>
@ -20,13 +25,12 @@ const balance = computed(() => wxStore.balance)
round
width="80"
height="80"
:src="`${publicPath}img/1.jpg`"
:src="userAvatar"
class="mr-4"
/>
<div>
<div class="text-lg font-bold mb-2">{{ '' }}</div>
<van-tag type="primary" class="mr-2">{{ 20 }}</van-tag>
<van-tag type="success">{{ '男' }}</van-tag>
<div class="text-lg font-bold mb-2">{{ userName }}</div>
<van-tag type="primary" class="mr-2">{{ userSex }}</van-tag>
</div>
</div>
</template>
@ -75,6 +79,7 @@ const balance = computed(() => wxStore.balance)
<van-cell-group>
<van-cell title="订单列表" is-link @click="router.push('/order-list')" />
<van-cell title="柜机管理" is-link @click="router.push('/cabinet')" v-if="wxStore.isCabinetAdmin"/>
<van-cell title="审批中心" is-link @click="router.push('/approval/list')" v-if="wxStore.isCabinetAdmin"/>
</van-cell-group>
</div>
</template>

View File

@ -20,7 +20,7 @@ async function handleOpenCabinet(orderId: number, orderGoodsId: number) {
try {
const result = await openCabinetApi(orderId, orderGoodsId)
if (result.code !== 0) {
showFailToast(result.message)
showFailToast(result.msg || '开启失败,请稍后重试')
return
}
showSuccessToast('柜口已成功开启')

View File

@ -1,6 +1,7 @@
<script setup lang="ts">
import { useCartStore } from "@/pinia/stores/cart"
import { useWxStore } from "@/pinia/stores/wx"
import { useAb98UserStore } from '@/pinia/stores/ab98-user'
import { storeToRefs } from "pinia"
import { showConfirmDialog } from "vant"
import { submitOrderApi } from "@/common/apis/shop"
@ -13,7 +14,10 @@ const cartStore = useCartStore()
const { cartItems, totalPrice } = storeToRefs(cartStore)
const wxStore = useWxStore()
const { openid, balance } = storeToRefs(wxStore)
const { openid, balance, corpid, userid: qyUserid } = storeToRefs(wxStore)
const ab98UserStore = useAb98UserStore()
const { tel, userid: ab98Userid } = storeToRefs(ab98UserStore)
const selectedPayment = ref<'wechat' | 'balance'>('wechat')
const contact = ref("")
@ -72,6 +76,11 @@ async function handleSubmit() {
submitting.value = true
try {
//
// 2 - corpid
// 1 - qyUserid
// 0 -
const isInternal = corpid.value ? 2 : qyUserid.value ? 1 : 0;
const requestData: SubmitOrderRequestData = {
openid: openid.value,
userid: wxStore.userid,
@ -80,7 +89,10 @@ async function handleSubmit() {
goodsId: item.product.id,
quantity: item.quantity
})),
paymentType: selectedPayment.value
paymentType: selectedPayment.value,
mobile: tel.value,
qyUserid: isInternal === 2 ? qyUserid.value : ab98Userid.value,
isInternal: isInternal
}
const { code, data } = await submitOrderApi(requestData)
@ -155,7 +167,7 @@ async function handleSubmit() {
<van-field label="支付方式" :model-value="selectedPayment" readonly>
<template #input>
<van-radio-group v-model="selectedPayment" direction="horizontal">
<van-radio name="wechat">
<van-radio name="wechat" v-if="!wxStore.corpid">
<van-icon name="wechat" class="method-icon" />
微信支付
</van-radio>

View File

@ -0,0 +1,123 @@
import { pinia } from "@/pinia"
import { LoginData } from "@/common/apis/ab98/type"
// 本地存储键名常量
const STORAGE_KEYS = {
FACE: 'ab98_face',
SEX: 'ab98_sex',
NAME: 'ab98_name',
USERID: 'ab98_userid',
REGISTERED: 'ab98_registered',
TEL: 'ab98_tel',
TOKEN: 'ab98_token'
}
/**
* AB98用户信息存储
* @description AB98系统用户相关状态信息
*/
export const useAb98UserStore = defineStore("ab98User", () => {
// 用户面部图像URL
const storedFace = localStorage.getItem(STORAGE_KEYS.FACE)
const face_img = ref<string>(storedFace ? decodeURIComponent(storedFace) : '')
// 用户性别(男/女)
const storedSex = localStorage.getItem(STORAGE_KEYS.SEX)
const sex = ref<string>(storedSex ? decodeURIComponent(storedSex) : '')
// 用户真实姓名
const storedName = localStorage.getItem(STORAGE_KEYS.NAME)
const name = ref<string>(storedName ? decodeURIComponent(storedName) : '')
// AB98系统用户唯一标识
const storedUserId = localStorage.getItem(STORAGE_KEYS.USERID)
const userid = ref<string>(storedUserId ? decodeURIComponent(storedUserId) : "")
// 是否已完成注册流程
const registered = ref<boolean>(JSON.parse(localStorage.getItem(STORAGE_KEYS.REGISTERED) || "false"))
// 用户绑定手机号
const storedTel = localStorage.getItem(STORAGE_KEYS.TEL)
const tel = ref<string>(storedTel ? decodeURIComponent(storedTel) : "")
// 用户认证令牌
const storedToken = localStorage.getItem(STORAGE_KEYS.TOKEN)
const token = ref<string>(storedToken ? decodeURIComponent(storedToken) : "")
// 用户登录状态
const isLogin = ref<boolean>(false);
isLogin.value = tel.value ? true : false;
/**
*
* @param data -
*/
const setUserInfo = (data: LoginData) => {
face_img.value = data.face_img
localStorage.setItem(STORAGE_KEYS.FACE, encodeURIComponent(data.face_img))
sex.value = data.sex
localStorage.setItem(STORAGE_KEYS.SEX, encodeURIComponent(data.sex))
name.value = data.name
localStorage.setItem(STORAGE_KEYS.NAME, encodeURIComponent(data.name))
userid.value = data.userid
localStorage.setItem(STORAGE_KEYS.USERID, encodeURIComponent(data.userid))
registered.value = data.registered
localStorage.setItem(STORAGE_KEYS.REGISTERED, JSON.stringify(data.registered))
tel.value = data.tel
localStorage.setItem(STORAGE_KEYS.TEL, encodeURIComponent(data.tel))
}
/**
*
* @description
*/
const clearUserInfo = () => {
face_img.value = ""
localStorage.removeItem(STORAGE_KEYS.FACE)
sex.value = ""
localStorage.removeItem(STORAGE_KEYS.SEX)
name.value = ""
localStorage.removeItem(STORAGE_KEYS.NAME)
userid.value = ""
localStorage.removeItem(STORAGE_KEYS.USERID)
registered.value = false
localStorage.removeItem(STORAGE_KEYS.REGISTERED)
tel.value = ""
localStorage.removeItem(STORAGE_KEYS.TEL)
localStorage.removeItem(STORAGE_KEYS.TOKEN)
}
/**
*
* @param value - JWT格式的认证令牌
*/
const setToken = (value: string) => {
localStorage.setItem(STORAGE_KEYS.TOKEN, encodeURIComponent(value))
token.value = value
}
const setTel = (value: string) => {
localStorage.setItem(STORAGE_KEYS.TEL, btoa(value))
tel.value = value
}
const setIsLogin = (value: boolean) => {
isLogin.value = value;
}
return {
face_img,
sex,
name,
userid,
registered,
tel,
token,
isLogin,
setUserInfo,
setToken,
setTel,
setIsLogin,
clearUserInfo
}
})
/**
* @description setup上下文或SSR场景中使用store
*/
export function useAb98UserStoreOutside() {
return useAb98UserStore(pinia)
}

View File

@ -0,0 +1,25 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { ReturnApprovalEntity } from '@/common/apis/approval/type'
export interface ApprovalDetail extends ReturnApprovalEntity {
goodsName: string
coverImg: string
}
export const useApprovalStore = defineStore('approval', () => {
const currentApproval = ref<ApprovalDetail | null>(null)
const setCurrentApproval = (approval: ApprovalDetail) => {
currentApproval.value = approval
}
return {
currentApproval,
setCurrentApproval
}
})
export function useApprovalStoreOutside() {
return useApprovalStore()
}

View File

@ -5,18 +5,34 @@ import { isWhiteList } from "@/router/whitelist"
import { useTitle } from "@@/composables/useTitle"
import { getToken } from "@@/utils/cache/cookies"
import NProgress from "nprogress"
import { useAb98UserStore } from '@/pinia/stores/ab98-user'
NProgress.configure({ showSpinner: false })
const { setTitle } = useTitle()
const LOGIN_PATH = "/login"
const LOGIN_PATH = "/ab98"
export function registerNavigationGuard(router: Router) {
// 全局前置守卫
router.beforeEach((to, _from) => {
NProgress.start()
// 企业微信登录
const urlParams = new URLSearchParams(window.location.search);
const corpid = urlParams.get('corpid') || undefined;
if (corpid) {
return true;
}
const userStore = useAb98UserStore()
if (!userStore.isLogin) {
// 如果在免登录的白名单中,则直接进入
if (isWhiteList(to)) return true
// 其他没有访问权限的页面将被重定向到登录页面
return LOGIN_PATH
}
return true;
// const userStore = useUserStore()
// // 如果没有登录

View File

@ -34,6 +34,11 @@ export const routes: RouteRecordRaw[] = [
component: () => import('@/pages/approval/submit.vue'),
meta: { requiresAuth: true }
},
{
path: '/approval/handle/:approvalId',
component: () => import('@/pages/approval/handle.vue'),
meta: { requiresAuth: true }
},
{
path: '/order-success',
name: 'OrderSuccess',
@ -75,6 +80,25 @@ export const routes: RouteRecordRaw[] = [
meta: {
title: '柜机管理',
keepAlive: true,
layout: {
navBar: {
showNavBar: false,
showLeftArrow: false
},
tabbar: {
showTabbar: true,
icon: "home-o"
}
}
}
},
{
path: '/approval/list',
component: () => import('@/pages/approval/list.vue'),
name: "Approval",
meta: {
title: '审批中心',
keepAlive: true,
layout: {
navBar: {
showNavBar: false,
@ -123,6 +147,14 @@ export const routes: RouteRecordRaw[] = [
}
}
}
},
{
path: "/ab98",
component: () => import("@/pages/login/Ab98Login.vue"),
name: "Ab98Login",
meta: {
title: "登录"
}
}
]
@ -132,6 +164,11 @@ export const routes: RouteRecordRaw[] = [
component: () => import('@/pages/approval/submit.vue'),
meta: { requiresAuth: true }
},
{
path: '/approval/handle/:approvalId',
component: () => import('@/pages/approval/handle.vue'),
meta: { requiresAuth: true }
},
{
path: '/order-success',
name: 'OrderSuccess',

View File

@ -1,7 +1,7 @@
import type { RouteLocationNormalizedGeneric, RouteRecordNameGeneric } from "vue-router"
/** 免登录白名单(匹配路由 path */
const whiteListByPath: string[] = ["/login"]
const whiteListByPath: string[] = ["/login", "/ab98"]
/** 免登录白名单(匹配路由 name */
const whiteListByName: RouteRecordNameGeneric[] = []

4
types/api.d.ts vendored
View File

@ -2,11 +2,11 @@
interface ApiResponseData<T> {
code: number
data: T
message: string
msg: string
}
interface ApiResponseMsgData<T> {
code: number
data: T
message: string
msg: string
}

View File

@ -17,12 +17,16 @@ declare module 'vue' {
VanConfigProvider: typeof import('vant/es')['ConfigProvider']
VanDivider: typeof import('vant/es')['Divider']
VanField: typeof import('vant/es')['Field']
VanForm: typeof import('vant/es')['Form']
VanGrid: typeof import('vant/es')['Grid']
VanGridItem: typeof import('vant/es')['GridItem']
VanIcon: typeof import('vant/es')['Icon']
VanImage: typeof import('vant/es')['Image']
VanList: typeof import('vant/es')['List']
VanLoading: typeof import('vant/es')['Loading']
VanNavBar: typeof import('vant/es')['NavBar']
VanPicker: typeof import('vant/es')['Picker']
VanPopup: typeof import('vant/es')['Popup']
VanRadio: typeof import('vant/es')['Radio']
VanRadioGroup: typeof import('vant/es')['RadioGroup']
VanSidebar: typeof import('vant/es')['Sidebar']