Compare commits

...

3 Commits

Author SHA1 Message Date
sugarBecomer 1f58572b8f ✨ 请假功能 2026-03-25 13:47:11 +08:00
sugarBecomer a8125cc224 🐛 修复样式问题 2026-03-24 23:00:28 +08:00
sugarBecomer 1f519767af 🐛 修复样式问题 2026-03-24 22:48:06 +08:00
19 changed files with 772 additions and 116 deletions
+11
View File
@@ -15,6 +15,7 @@ import type {
CpTaskExecuteParams,
CpTaskFormItem,
CpLeaveReportParams,
CpLeaveApplyParams,
TaskStatisticsItem,
TaskStatisticsV2Item,
DashboardSummary,
@@ -117,4 +118,14 @@ export const cpLeaveApi = {
request.post<any, ApiResponse>('/v1/cpdaily/leave/report', data),
uploadImage: (formData: FormData) =>
request.post<any, ApiResponse>('/v1/cpdaily/leave/upload', formData),
checkLeave: (cpUserId: number) =>
request.get<any, ApiResponse>(`/v1/cpdaily/leave/check/${cpUserId}`),
getLeaveTypes: (cpUserId: number) =>
request.get<any, ApiResponse>(`/v1/cpdaily/leave/types/${cpUserId}`),
getLeaveTypeCfg: (data: { cpUserId: number; code: number }) =>
request.post<any, ApiResponse>('/v1/cpdaily/leave/typeCfg', data),
getApproveFlow: (data: { cpUserId: number; leaveType: number; startTime: string; endTime: string }) =>
request.post<any, ApiResponse>('/v1/cpdaily/leave/approveFlow', data),
apply: (data: CpLeaveApplyParams) =>
request.post<any, ApiResponse>('/v1/cpdaily/leave/apply', data),
}
+4
View File
@@ -82,6 +82,10 @@ request.interceptors.response.use(
localStorage.removeItem('token')
const { default: router } = await import('@/router')
router.push('/login')
} else if (error.response?.data?.message) {
message.error(error.response.data.message)
} else if (error.message && !error.message.includes('canceled')) {
message.error(error.message)
}
return Promise.reject(error)
}
+2 -2
View File
@@ -209,8 +209,8 @@ defineExpose({ refresh })
</div>
<!-- 分页 -->
<div v-if="total > 0" class="mt-4 flex flex-col sm:flex-row items-center justify-between gap-2">
<span class="text-sm text-muted-foreground">共 {{ total }} 条</span>
<div v-if="total > 0" class="mt-4 flex items-center justify-between gap-2">
<span class="text-sm text-muted-foreground whitespace-nowrap">共 {{ total }} 条</span>
<Pagination
:total="total"
:items-per-page="size"
+34
View File
@@ -183,3 +183,37 @@ export interface CpUserCustomDeviceParams {
mobileCode: string
mobileType: 'random' | 'manual'
}
export interface CpLeaveType {
id: string
code: number
name: string
mustOut: number
reasonWordLimit: number
attachmentNumberLimit: number
}
export interface CpLeaveApprover {
userWid: string
userId: string
userName: string
}
export interface CpLeaveApplyParams {
cpUserId: number
leaveType: number
startTime: string
endTime: string
urgencyMobile: string
leaveReason: string
outStatus: boolean
address: string
detailAddress: string
longitude: string
latitude: string
locationAddr: string
imageUrl?: string
approverWid?: string
approverId?: string
approverName?: string
}
+4 -4
View File
@@ -83,8 +83,8 @@ const onAddSubmit = handleAddSubmit(async (values) => {
toast.success('生成卡密成功')
addDialogOpen.value = false
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '生成卡密失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
addLoading.value = false
}
@@ -97,8 +97,8 @@ function handleBan(id: number) {
await cardApi.ban(id)
toast.success('禁用成功')
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '禁用失败')
} catch {
// 全局拦截器已处理错误提示
}
})
}
+534 -27
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { ref, h, watch } from 'vue'
import { ref, h, watch, computed } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { message as toast } from '@/composables/useMessage'
import { ClipboardCheck, Upload, X, Eye, Loader2 } from 'lucide-vue-next'
import { ClipboardCheck, Upload, X, Eye, Loader2, CalendarPlus } from 'lucide-vue-next'
import DataTable from '@/components/common/DataTable.vue'
import type { SearchField } from '@/components/common/DataTable.vue'
import { Badge } from '@/components/ui/badge'
@@ -11,6 +11,15 @@ import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Card, CardContent } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { Switch } from '@/components/ui/switch'
import { Textarea } from '@/components/ui/textarea'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Dialog,
DialogContent,
@@ -19,7 +28,7 @@ import {
DialogDescription,
} from '@/components/ui/dialog'
import { cpLeaveApi, cpUserApi } from '@/api/cpdaily'
import type { CpLeave, CpLeaveReportParams } from '@/types/cpdaily'
import type { CpLeave, CpLeaveReportParams, CpLeaveApplyParams, CpLeaveType, CpLeaveApprover } from '@/types/cpdaily'
import type { PageParams, PageResponse } from '@/types/api'
import { extractPageData } from '@/types/api'
@@ -49,6 +58,12 @@ function getLeaveStatusInfo(status: number) {
return systemLeaveStatusMap[status] || { label: '未知', color: '#909399' }
}
// ---- 类型映射 ----
const typeMap: Record<string, string> = {
report: '销假',
apply: '请假',
}
// ---- 表格列定义 ----
const columns: ColumnDef<CpLeave, any>[] = [
{
@@ -60,6 +75,15 @@ const columns: ColumnDef<CpLeave, any>[] = [
},
},
{ accessorKey: 'title', header: '标题' },
{
id: 'type',
header: '类型',
cell: ({ row }) => {
const d = row.original as any
const t = d.type || 'report'
return h(Badge, { variant: t === 'apply' ? 'secondary' : 'outline' }, () => typeMap[t] || t)
},
},
{
accessorKey: 'status',
header: '状态',
@@ -107,9 +131,9 @@ const fetchFn = async (params: PageParams): Promise<PageResponse<CpLeave>> => {
return extractPageData(res)
}
// ---- 执行销假弹窗(两步流程) ----
// ==================== 执行销假弹窗(两步流程) ====================
const reportDialogOpen = ref(false)
const reportStep = ref<1 | 2 | 3>(1) // 1=选用户 2=选请假记录 3=填写销假信息
const reportStep = ref<1 | 2 | 3>(1)
const reportLoading = ref(false)
// 步骤1:选择用户
@@ -135,7 +159,7 @@ const reportForm = ref<CpLeaveReportParams>({
imageUrl: '',
})
// 图片上传
// 图片上传(销假)
const uploadLoading = ref(false)
const uploadedImageUrl = ref('')
const previewOpen = ref(false)
@@ -198,8 +222,8 @@ async function handleFetchSystemLeaves() {
} else {
reportStep.value = 2
}
} catch (err: any) {
toast.error(err.response?.data?.message || '获取请假列表失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
systemLeavesLoading.value = false
}
@@ -265,8 +289,8 @@ async function handleImageUpload(event: Event) {
} else {
toast.error('上传失败')
}
} catch (err: any) {
toast.error(err.response?.data?.message || '图片上传失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
uploadLoading.value = false
input.value = ''
@@ -294,22 +318,328 @@ async function handleReport() {
toast.success('销假成功')
reportDialogOpen.value = false
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '销假失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
reportLoading.value = false
}
}
// ==================== 请假弹窗 ====================
const applyDialogOpen = ref(false)
const applyStep = ref<1 | 2>(1) // 1=选用户 2=填写表单
const applyLoading = ref(false)
// 步骤1:选择用户
const applyUserSearchText = ref('')
const applyUserResults = ref<{ label: string; value: number }[]>([])
const applyUserSearching = ref(false)
const applySelectedUserId = ref<number | null>(null)
let applyUserTimer: ReturnType<typeof setTimeout> | null = null
const applyCheckLoading = ref(false)
// 步骤2:请假表单
const leaveTypes = ref<CpLeaveType[]>([])
const leaveTypesLoading = ref(false)
const selectedLeaveTypeCode = ref<string>('')
const needApproval = ref(false)
const approvers = ref<CpLeaveApprover[]>([])
const approversLoading = ref(false)
const selectedApproverWid = ref<string>('')
// 请假图片上传
const applyUploadLoading = ref(false)
const applyUploadedImageUrl = ref('')
const applyForm = ref<CpLeaveApplyParams>({
cpUserId: 0,
leaveType: 0,
startTime: '',
endTime: '',
urgencyMobile: '',
leaveReason: '',
outStatus: false,
address: '',
detailAddress: '',
longitude: '',
latitude: '',
locationAddr: '',
imageUrl: '',
approverWid: '',
approverId: '',
approverName: '',
})
// 日期时间拆分
const applyStartDate = ref('')
const applyStartTime = ref('')
const applyEndDate = ref('')
const applyEndTime = ref('')
const currentLeaveType = computed(() => {
if (!selectedLeaveTypeCode.value) return null
return leaveTypes.value.find(t => String(t.code) === selectedLeaveTypeCode.value) || null
})
watch(applyUserSearchText, (val) => {
if (applyUserTimer) clearTimeout(applyUserTimer)
if (!val || val.length < 1) {
applyUserResults.value = []
return
}
if (applySelectedUserId.value && applyUserResults.value.length === 0) return
applyUserTimer = setTimeout(async () => {
applyUserSearching.value = true
try {
const res = await cpUserApi.getList({ page: 1, size: 10, username: val })
const data = res.data as any
const list = Array.isArray(data?.pageData) ? data.pageData : Array.isArray(data) ? data : []
applyUserResults.value = list.map((u: any) => ({
label: `${u.username}${u.edges?.school?.name ? ' - ' + u.edges.school.name : ''}`,
value: u.id,
}))
} catch {
applyUserResults.value = []
} finally {
applyUserSearching.value = false
}
}, 400)
})
function selectApplyUser(user: { label: string; value: number }) {
applySelectedUserId.value = user.value
applyUserSearchText.value = user.label
applyUserResults.value = []
}
function openApplyDialog() {
applyStep.value = 1
applySelectedUserId.value = null
applyUserSearchText.value = ''
applyUserResults.value = []
leaveTypes.value = []
selectedLeaveTypeCode.value = ''
needApproval.value = false
approvers.value = []
selectedApproverWid.value = ''
applyUploadedImageUrl.value = ''
applyStartDate.value = ''
applyStartTime.value = ''
applyEndDate.value = ''
applyEndTime.value = ''
applyForm.value = {
cpUserId: 0, leaveType: 0, startTime: '', endTime: '',
urgencyMobile: '', leaveReason: '', outStatus: false,
address: '', detailAddress: '', longitude: '', latitude: '',
locationAddr: '', imageUrl: '', approverWid: '', approverId: '', approverName: '',
}
applyDialogOpen.value = true
}
async function handleCheckAndFetchTypes() {
if (!applySelectedUserId.value) {
toast.error('请选择用户')
return
}
applyCheckLoading.value = true
try {
// 检查是否允许请假
await cpLeaveApi.checkLeave(applySelectedUserId.value)
// 获取请假类型
leaveTypesLoading.value = true
const res = await cpLeaveApi.getLeaveTypes(applySelectedUserId.value)
const data = res.data
leaveTypes.value = Array.isArray(data) ? data : []
if (leaveTypes.value.length === 0) {
toast.info('未获取到请假类型')
return
}
applyForm.value.cpUserId = applySelectedUserId.value
applyStep.value = 2
} catch {
// 全局拦截器已处理错误提示
} finally {
applyCheckLoading.value = false
leaveTypesLoading.value = false
}
}
// 选择请假类型后的处理
watch(selectedLeaveTypeCode, async (val) => {
if (!val) return
const lt = leaveTypes.value.find(t => String(t.code) === val)
if (!lt) return
applyForm.value.leaveType = lt.code
// mustOut=1 时强制离校
if (Number(lt.mustOut) === 1) {
applyForm.value.outStatus = true
} else {
applyForm.value.outStatus = false
}
// 查询是否需要审批
needApproval.value = false
approvers.value = []
selectedApproverWid.value = ''
try {
const res = await cpLeaveApi.getLeaveTypeCfg({ cpUserId: applySelectedUserId.value!, code: lt.code })
const cfg = res.data
if (cfg && cfg.needApproval === 1) {
needApproval.value = true
// 如果时间已填,自动获取审批人
tryAutoFetchApprovers()
}
} catch {
// ignore
}
})
// 时间变化时自动获取审批人
watch([applyStartDate, applyStartTime, applyEndDate, applyEndTime], () => {
if (needApproval.value) {
tryAutoFetchApprovers()
}
})
function tryAutoFetchApprovers() {
const startTime = buildDateTime(applyStartDate.value, applyStartTime.value)
const endTime = buildDateTime(applyEndDate.value, applyEndTime.value)
if (startTime && endTime && applyForm.value.leaveType) {
handleFetchApprovers()
}
}
async function handleFetchApprovers() {
if (!applyForm.value.leaveType) {
toast.error('请选择请假类型')
return
}
const startTime = buildDateTime(applyStartDate.value, applyStartTime.value)
const endTime = buildDateTime(applyEndDate.value, applyEndTime.value)
if (!startTime || !endTime) {
toast.error('请填写开始和结束时间')
return
}
approversLoading.value = true
try {
const res = await cpLeaveApi.getApproveFlow({
cpUserId: applySelectedUserId.value!,
leaveType: applyForm.value.leaveType,
startTime,
endTime,
})
const data = res.data
// 审批人可能在 approveFlow 或直接是数组
const flowList = data?.approveFlow || data?.approvers || (Array.isArray(data) ? data : [])
approvers.value = Array.isArray(flowList) ? flowList : []
if (approvers.value.length === 0) {
toast.info('未获取到审批人')
}
} catch {
// 全局拦截器已处理错误提示
} finally {
approversLoading.value = false
}
}
function buildDateTime(date: string, time: string): string {
if (!date) return ''
return `${date} ${time || '00:00'}:00`
}
// 请假图片上传
async function handleApplyImageUpload(event: Event) {
const input = event.target as HTMLInputElement
const file = input.files?.[0]
if (!file) return
applyUploadLoading.value = true
try {
const compressed = await compressImage(file)
const formData = new FormData()
formData.append('file', compressed)
formData.append('cpUserId', String(applySelectedUserId.value))
const res = await cpLeaveApi.uploadImage(formData)
const url = res.data?.url || res.data?.previewUrl || (typeof res.data === 'string' ? res.data : '')
if (url) {
applyUploadedImageUrl.value = url
applyForm.value.imageUrl = url
toast.success('图片上传成功')
} else {
toast.error('上传失败')
}
} catch {
// 全局拦截器已处理错误提示
} finally {
applyUploadLoading.value = false
input.value = ''
}
}
function removeApplyImage() {
applyUploadedImageUrl.value = ''
applyForm.value.imageUrl = ''
}
async function handleApply() {
const f = applyForm.value
if (!f.leaveType) { toast.error('请选择请假类型'); return }
const startTime = buildDateTime(applyStartDate.value, applyStartTime.value)
const endTime = buildDateTime(applyEndDate.value, applyEndTime.value)
if (!startTime || !endTime) { toast.error('请填写开始和结束时间'); return }
if (!f.urgencyMobile) { toast.error('请填写紧急联系人手机号'); return }
if (!f.leaveReason) { toast.error('请填写请假原因'); return }
// 检查字数限制
const lt = currentLeaveType.value
if (lt && lt.reasonWordLimit > 0 && f.leaveReason.length < lt.reasonWordLimit) {
toast.error(`请假原因至少 ${lt.reasonWordLimit} 字`)
return
}
if (!f.address) { toast.error('请填写目的地'); return }
if (!f.detailAddress) { toast.error('请填写详细地址'); return }
if (!f.longitude || !f.latitude) { toast.error('请填写经纬度'); return }
if (!f.locationAddr) { toast.error('请填写定位地址'); return }
if (needApproval.value && !selectedApproverWid.value) { toast.error('请选择审批人'); return }
// 填充审批人
if (selectedApproverWid.value) {
const approver = approvers.value.find(a => a.userWid === selectedApproverWid.value)
if (approver) {
f.approverWid = approver.userWid
f.approverId = approver.userId
f.approverName = approver.userName
}
}
f.startTime = startTime
f.endTime = endTime
applyLoading.value = true
try {
await cpLeaveApi.apply(f)
toast.success('请假申请提交成功')
applyDialogOpen.value = false
tableRef.value?.refresh()
} catch {
// 全局拦截器已处理错误提示
} finally {
applyLoading.value = false
}
}
</script>
<template>
<div class="space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-2xl font-bold tracking-tight">请假销假管理</h2>
<Button size="sm" @click="openReportDialog">
<ClipboardCheck class="mr-1 h-4 w-4" />
执行销假
</Button>
<div class="flex gap-2">
<Button size="sm" variant="outline" @click="openApplyDialog">
<CalendarPlus class="mr-1 h-4 w-4" />
请假
</Button>
<Button size="sm" @click="openReportDialog">
<ClipboardCheck class="mr-1 h-4 w-4" />
执行销假
</Button>
</div>
</div>
<DataTable
@@ -319,7 +649,7 @@ async function handleReport() {
:search-fields="searchFields"
/>
<!-- 执行销假弹窗 -->
<!-- ==================== 执行销假弹窗 ==================== -->
<Dialog v-model:open="reportDialogOpen">
<DialogContent class="w-[calc(100vw-2rem)] max-w-lg max-h-[85vh] overflow-y-auto">
<DialogHeader>
@@ -375,7 +705,6 @@ async function handleReport() {
@click="selectLeave(leave)"
>
<CardContent class="p-3 space-y-2">
<!-- 类型标签 + 状态 -->
<div class="flex items-center justify-between">
<Badge variant="outline" class="text-xs">{{ leave.leaveType || '请假' }}</Badge>
<span
@@ -383,13 +712,10 @@ async function handleReport() {
:style="{ backgroundColor: getLeaveStatusInfo(leave.status).color }"
>{{ getLeaveStatusInfo(leave.status).label }}</span>
</div>
<!-- 原因(允许换行) -->
<p class="text-sm break-all line-clamp-2">{{ leave.reason || leave.title || '无' }}</p>
<!-- 图片 -->
<div v-if="leave.imageUrl || leave.image" class="size-14 rounded-md overflow-hidden border">
<img :src="leave.imageUrl || leave.image" class="size-full object-cover" alt="" />
</div>
<!-- 时间 -->
<div class="text-xs text-muted-foreground space-y-0.5">
<div>创建:{{ leave.createTime || '-' }}</div>
<div>结束:{{ leave.aceEndTime || '-' }}</div>
@@ -404,7 +730,6 @@ async function handleReport() {
<!-- 步骤3:填写销假信息 -->
<div v-show="reportStep === 3" class="space-y-4">
<!-- 已选请假记录摘要 -->
<div v-if="selectedLeave" class="rounded-md border p-3 bg-muted/30 text-sm space-y-1">
<div class="flex items-center gap-2">
<Badge variant="outline" class="text-xs">{{ selectedLeave.leaveType || '请假' }}</Badge>
@@ -414,9 +739,7 @@ async function handleReport() {
预计结束:{{ selectedLeave.aceEndTime || '-' }}
</div>
</div>
<Separator />
<div class="space-y-2">
<Label>地址</Label>
<Input v-model="reportForm.address" placeholder="请输入地址" />
@@ -434,7 +757,6 @@ async function handleReport() {
<div class="space-y-2">
<Label>销假图片</Label>
<div class="flex flex-wrap gap-2">
<!-- 已上传图片 -->
<div v-if="uploadedImageUrl" class="relative group size-20 rounded-md border overflow-hidden">
<img :src="uploadedImageUrl" class="size-full object-cover" />
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1">
@@ -442,7 +764,6 @@ async function handleReport() {
<button type="button" class="p-1 text-white" @click="removeUploadedImage"><X class="h-4 w-4" /></button>
</div>
</div>
<!-- 上传按钮 -->
<label v-if="!uploadedImageUrl" class="size-20 rounded-md border-2 border-dashed flex flex-col items-center justify-center cursor-pointer hover:border-primary transition-colors text-muted-foreground">
<Loader2 v-if="uploadLoading" class="h-5 w-5 animate-spin" />
<template v-else>
@@ -453,7 +774,6 @@ async function handleReport() {
</label>
</div>
</div>
<div class="flex justify-between pt-2">
<Button variant="outline" @click="reportStep = 2">上一步</Button>
<Button :disabled="reportLoading || !selectedLeave" @click="handleReport">
@@ -465,6 +785,193 @@ async function handleReport() {
</DialogContent>
</Dialog>
<!-- ==================== 请假弹窗 ==================== -->
<Dialog v-model:open="applyDialogOpen">
<DialogContent class="w-[calc(100vw-2rem)] max-w-lg max-h-[85vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>请假申请</DialogTitle>
<DialogDescription>
{{ applyStep === 1 ? '选择用户' : '填写请假信息' }}
</DialogDescription>
</DialogHeader>
<!-- 步骤指示 -->
<div class="flex items-center gap-2 text-sm mb-2">
<span :class="applyStep === 1 ? 'text-primary font-medium' : 'text-muted-foreground'">① 选择用户</span>
<span class="text-muted-foreground">→</span>
<span :class="applyStep === 2 ? 'text-primary font-medium' : 'text-muted-foreground'">② 填写表单</span>
</div>
<!-- 步骤1:选择用户 -->
<div v-show="applyStep === 1" class="space-y-4">
<div class="space-y-2">
<Label>选择用户</Label>
<Input v-model="applyUserSearchText" placeholder="输入用户名搜索" />
<div v-if="applyUserSearching" class="text-sm text-muted-foreground">搜索中...</div>
<div v-if="applyUserResults.length > 0" class="max-h-32 overflow-y-auto rounded-md border">
<div
v-for="u in applyUserResults"
:key="u.value"
class="cursor-pointer px-3 py-2 text-sm hover:bg-accent"
@click="selectApplyUser(u)"
>
{{ u.label }}
</div>
</div>
<p v-if="applySelectedUserId" class="text-sm text-primary">已选择:{{ applyUserSearchText }}</p>
</div>
<div class="flex justify-center pt-2">
<Button :disabled="applyCheckLoading || !applySelectedUserId" @click="handleCheckAndFetchTypes">
<Loader2 v-if="applyCheckLoading" class="mr-1 h-4 w-4 animate-spin" />
{{ applyCheckLoading ? '检查中...' : '下一步' }}
</Button>
</div>
</div>
<!-- 步骤2:填写请假表单 -->
<div v-show="applyStep === 2" class="space-y-4">
<!-- 请假类型 -->
<div class="space-y-2">
<Label>请假类型</Label>
<Select v-model="selectedLeaveTypeCode">
<SelectTrigger class="w-full">
<SelectValue placeholder="请选择请假类型" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="lt in leaveTypes" :key="lt.code" :value="String(lt.code)">
{{ lt.name }}
</SelectItem>
</SelectContent>
</Select>
</div>
<!-- 开始时间 -->
<div class="space-y-2">
<Label>开始时间</Label>
<div class="grid grid-cols-2 gap-2">
<Input type="date" v-model="applyStartDate" />
<Input type="time" v-model="applyStartTime" />
</div>
</div>
<!-- 结束时间 -->
<div class="space-y-2">
<Label>结束时间</Label>
<div class="grid grid-cols-2 gap-2">
<Input type="date" v-model="applyEndDate" />
<Input type="time" v-model="applyEndTime" />
</div>
</div>
<!-- 是否离校 -->
<div class="flex items-center justify-between">
<Label>是否离校</Label>
<Switch
:checked="applyForm.outStatus"
:disabled="Number(currentLeaveType?.mustOut) === 1"
@update:checked="(val: boolean) => applyForm.outStatus = val"
/>
</div>
<p v-if="Number(currentLeaveType?.mustOut) === 1" class="text-xs text-muted-foreground">该请假类型要求必须离校</p>
<!-- 紧急联系人手机号 -->
<div class="space-y-2">
<Label>紧急联系人手机号</Label>
<Input v-model="applyForm.urgencyMobile" placeholder="请输入手机号" />
</div>
<!-- 目的地 -->
<div class="space-y-2">
<Label>目的地</Label>
<Input v-model="applyForm.address" placeholder="格式:xx省/xx市/xx区" />
</div>
<!-- 详细地址 -->
<div class="space-y-2">
<Label>详细地址</Label>
<Input v-model="applyForm.detailAddress" placeholder="请输入详细地址" />
</div>
<!-- 请假原因 -->
<div class="space-y-2">
<Label>
请假原因
<span v-if="currentLeaveType?.reasonWordLimit" class="text-xs text-muted-foreground ml-1">
(最少 {{ currentLeaveType.reasonWordLimit }} 字,当前 {{ applyForm.leaveReason.length }} 字)
</span>
</Label>
<Textarea v-model="applyForm.leaveReason" placeholder="请输入请假原因" :rows="3" />
</div>
<!-- 经纬度和定位地址 -->
<div class="grid grid-cols-2 gap-3">
<div class="space-y-2">
<Label>经度</Label>
<Input v-model="applyForm.longitude" placeholder="如:116.397" />
</div>
<div class="space-y-2">
<Label>纬度</Label>
<Input v-model="applyForm.latitude" placeholder="如:39.908" />
</div>
</div>
<div class="space-y-2">
<Label>定位地址</Label>
<Input v-model="applyForm.locationAddr" placeholder="请输入定位地址" />
</div>
<!-- 图片上传 -->
<div class="space-y-2">
<Label>图片附件(可选)</Label>
<div class="flex flex-wrap gap-2">
<div v-if="applyUploadedImageUrl" class="relative group size-20 rounded-md border overflow-hidden">
<img :src="applyUploadedImageUrl" class="size-full object-cover" />
<div class="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center gap-1">
<button type="button" class="p-1 text-white" @click="openPreview(applyUploadedImageUrl)"><Eye class="h-4 w-4" /></button>
<button type="button" class="p-1 text-white" @click="removeApplyImage"><X class="h-4 w-4" /></button>
</div>
</div>
<label v-if="!applyUploadedImageUrl" class="size-20 rounded-md border-2 border-dashed flex flex-col items-center justify-center cursor-pointer hover:border-primary transition-colors text-muted-foreground">
<Loader2 v-if="applyUploadLoading" class="h-5 w-5 animate-spin" />
<template v-else>
<Upload class="h-5 w-5" />
<span class="text-xs mt-1">上传</span>
</template>
<input type="file" class="hidden" accept="image/*" :disabled="applyUploadLoading" @change="handleApplyImageUpload" />
</label>
</div>
</div>
<!-- 审批人(需要审批时显示) -->
<div v-if="needApproval" class="space-y-2">
<Label>
审批人
<Loader2 v-if="approversLoading" class="inline h-3 w-3 animate-spin ml-1" />
</Label>
<Select v-if="approvers.length > 0" v-model="selectedApproverWid">
<SelectTrigger>
<SelectValue placeholder="请选择审批人" />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="a in approvers" :key="a.userWid" :value="a.userWid">
{{ a.userName }}
</SelectItem>
</SelectContent>
</Select>
<p v-else-if="!approversLoading" class="text-xs text-muted-foreground">请先选择请假类型并填写开始和结束时间</p>
</div>
<!-- 操作按钮 -->
<div class="flex justify-between pt-2">
<Button variant="outline" @click="applyStep = 1">上一步</Button>
<Button :disabled="applyLoading" @click="handleApply">
<Loader2 v-if="applyLoading" class="mr-1 h-4 w-4 animate-spin" />
{{ applyLoading ? '提交中...' : '提交请假' }}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<!-- 图片预览 -->
<Dialog v-model:open="previewOpen">
<DialogContent class="sm:max-w-lg p-2">
+9 -9
View File
@@ -198,7 +198,7 @@ async function handleImageUpload(field: CpTaskFormItem, event: Event) {
list[idx] = { ...list[idx], url: previewUrl, previewUrl, status: 'finished' }
updateImageContent(field)
} else { removeImage(field, id); toast.error('上传失败') }
} catch { removeImage(field, id); toast.error('上传失败') }
} catch { removeImage(field, id) }
}
input.value = ''
}
@@ -262,8 +262,8 @@ async function loadForm() {
}
if (field.type === 4) initImageField(field)
})
} catch (err: any) {
toast.error(err.response?.data?.message || '获取表单数据失败')
} catch {
// 全局拦截器已处理错误提示
} finally { formLoading.value = false }
}
@@ -281,8 +281,8 @@ async function loadExecute() {
execute_hour: data.execute_hour ?? 8, execute_minute: data.execute_minute ?? 0,
execute_days: Array.isArray(data.execute_days) && data.execute_days.length ? data.execute_days : [1, 2, 3, 4, 5, 6, 7],
}
} catch (err: any) {
toast.error(err.response?.data?.message || '获取执行参数失败')
} catch {
// 全局拦截器已处理错误提示
} finally { executeLoading.value = false }
}
@@ -298,8 +298,8 @@ async function saveForm() {
const formData = formItems.value.map(f => ({ ...f, content: f.content || '', otherValue: f.otherValue || '' }))
await cpTaskApi.updateForm(taskId.value, formData)
toast.success('表单保存成功')
} catch (err: any) {
toast.error(err.response?.data?.message || '保存表单失败')
} catch {
// 全局拦截器已处理错误提示
} finally { savingForm.value = false }
}
@@ -314,8 +314,8 @@ async function saveExecute() {
execute_days: executeParams.value.execute_days,
})
toast.success('执行参数保存成功')
} catch (err: any) {
toast.error(err.response?.data?.message || '保存执行参数失败')
} catch {
// 全局拦截器已处理错误提示
} finally { savingExecute.value = false }
}
+44 -12
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { ref, h, watch } from 'vue'
import { ref, h, watch, onMounted } from 'vue'
import type { ColumnDef } from '@tanstack/vue-table'
import { message as toast } from '@/composables/useMessage'
import { useRouter } from 'vue-router'
import { useRouter, useRoute } from 'vue-router'
import { PlusCircle, Play, Pause, Trash2, Pencil, ScrollText } from 'lucide-vue-next'
import DataTable from '@/components/common/DataTable.vue'
import type { SearchField } from '@/components/common/DataTable.vue'
@@ -25,6 +25,7 @@ import type { PageParams, PageResponse } from '@/types/api'
import { extractPageData } from '@/types/api'
const router = useRouter()
const route = useRoute()
const tableRef = ref<{ refresh: () => void } | null>(null)
// ---- 确认弹窗状态 ----
@@ -59,9 +60,14 @@ const userSearchText = ref('')
const userSearchResults = ref<{ label: string; value: number }[]>([])
const userSearching = ref(false)
let userSearchTimer: ReturnType<typeof setTimeout> | null = null
let skipNextSearch = false
watch(userSearchText, (val) => {
if (userSearchTimer) clearTimeout(userSearchTimer)
if (skipNextSearch) {
skipNextSearch = false
return
}
if (!val || val.length < 1) {
userSearchResults.value = []
return
@@ -86,6 +92,7 @@ watch(userSearchText, (val) => {
function selectUser(user: { label: string; value: number }) {
addUserId.value = user.value
skipNextSearch = true
userSearchText.value = user.label
userSearchResults.value = []
}
@@ -139,8 +146,8 @@ async function handleQueryRecent() {
const res = await cpTaskApi.getRecent({ userId: addUserId.value, type: addType.value })
recentTasks.value = res.data || []
addStep.value = 2
} catch (err: any) {
toast.error(err.response?.data?.message || '查询失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
recentLoading.value = false
}
@@ -159,6 +166,31 @@ function openAddDialog() {
addDialogOpen.value = true
}
// 支持从用户管理页面跳转过来时自动打开弹窗并预填用户
function openAddDialogWithUser(userId: number, username: string) {
addStep.value = 1
addUserId.value = userId
addType.value = ''
addTitle.value = ''
selectedRecent.value = ''
recentTasks.value = []
skipNextSearch = true
userSearchText.value = username
userSearchResults.value = []
isMatched.value = false
addDialogOpen.value = true
}
onMounted(() => {
const qUserId = route.query.addUserId as string
const qUsername = route.query.addUsername as string
if (qUserId && qUsername) {
openAddDialogWithUser(Number(qUserId), qUsername)
// 清除 query 参数,避免刷新时重复弹窗
router.replace({ path: route.path })
}
})
function prevStep() {
addStep.value = 1
selectedRecent.value = ''
@@ -181,8 +213,8 @@ async function handleSaveTask() {
toast.success('添加任务成功')
addDialogOpen.value = false
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '添加任务失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
addLoading.value = false
}
@@ -194,8 +226,8 @@ async function handleExecuteNow(id: number) {
await cpTaskApi.executeNow(id)
toast.success('任务已开始执行')
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '执行失败')
} catch {
// 全局拦截器已处理错误提示
}
}
@@ -206,8 +238,8 @@ function handlePause(id: number, currentStatus: string) {
await cpTaskApi.pause(id)
toast.success(`${action}成功`)
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || `${action}失败`)
} catch {
// 全局拦截器已处理错误提示
}
})
}
@@ -218,8 +250,8 @@ function handleDelete(id: number) {
await cpTaskApi.delete(id)
toast.success('删除成功')
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '删除失败')
} catch {
// 全局拦截器已处理错误提示
}
})
}
+29 -18
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { ref, h, watch, nextTick, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import type { ColumnDef } from '@tanstack/vue-table'
import { message as toast } from '@/composables/useMessage'
import { useForm } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import * as z from 'zod'
import QRCode from 'qrcode'
import { PlusCircle, Trash2, KeyRound, Smartphone, QrCode, Loader2 } from 'lucide-vue-next'
import { PlusCircle, Trash2, KeyRound, Smartphone, QrCode, Loader2, ListPlus } from 'lucide-vue-next'
import DataTable from '@/components/common/DataTable.vue'
import type { SearchField } from '@/components/common/DataTable.vue'
import IconButton from '@/components/common/IconButton.vue'
@@ -31,6 +32,16 @@ import type { PageParams, PageResponse } from '@/types/api'
import { extractPageData } from '@/types/api'
const tableRef = ref<{ refresh: () => void } | null>(null)
const router = useRouter()
// 生成 UUID v4
function generateUUID(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0
const v = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString(16)
})
}
// ---- 确认弹窗状态 ----
const confirmOpen = ref(false)
@@ -182,7 +193,7 @@ const onAddByPasswordSubmit = handleAddSubmit(async (values) => {
tableRef.value?.refresh()
}
} catch (err: any) {
toast.error(err.response?.data?.message || '添加账号失败')
// 全局拦截器已处理错误提示
} finally {
addLoading.value = false
}
@@ -238,7 +249,7 @@ async function onCaptchaConfirm(result: { captchaKey: string; dots: Array<{ x: n
mobileCodeSent.value = true
startCountdown()
} catch (err: any) {
toast.error(err.response?.data?.message || '获取验证码失败')
// 全局拦截器已处理错误提示
} finally {
mobileCodeLoading.value = false
addDialogOpen.value = true
@@ -257,7 +268,6 @@ async function onCaptchaConfirm(result: { captchaKey: string; dots: Array<{ x: n
toast.success('添加账号成功')
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '添加失败')
addDialogOpen.value = true
} finally {
mobileAddLoading.value = false
@@ -308,7 +318,7 @@ async function onConfirmAddSubmit() {
dynamicCode.value = ''
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '验证失败')
// 全局拦截器已处理错误提示
} finally {
confirmAddLoading.value = false
}
@@ -350,7 +360,7 @@ const onChangePwdSubmit = handleChangePwdSubmit(async (values) => {
toast.success('密码修改成功')
changePwdOpen.value = false
} catch (err: any) {
toast.error(err.response?.data?.message || '修改密码失败')
// 全局拦截器已处理错误提示
} finally {
changePwdLoading.value = false
}
@@ -364,7 +374,7 @@ function handleDelete(id: number) {
toast.success('删除成功')
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '删除失败')
// 全局拦截器已处理错误提示
}
})
}
@@ -399,7 +409,7 @@ async function onCustomDeviceSubmit() {
customDeviceOpen.value = false
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '设置失败')
// 全局拦截器已处理错误提示
} finally {
customDeviceLoading.value = false
}
@@ -418,7 +428,6 @@ async function openQrDialog() {
const res = await cpUserApi.generateQr()
qrUuid.value = res.data.uuid
} catch (err: any) {
toast.error(err.response?.data?.message || '生成二维码失败')
qrDialogOpen.value = false
} finally {
qrLoading.value = false
@@ -477,13 +486,10 @@ const columns: ColumnDef<CpUser, any>[] = [
header: '授权方式',
cell: ({ row }) => {
const type = row.original.mobile_type
const map: Record<string, { label: string; variant: 'default' | 'secondary' | 'outline' }> = {
random: { label: '随机', variant: 'outline' },
authorize: { label: '授权', variant: 'default' },
manual: { label: '手动', variant: 'secondary' },
if (type === 'manual') {
return h(Badge, { variant: 'default' }, () => '手动')
}
const config = map[type] || { label: type || '未知', variant: 'outline' as const }
return h(Badge, { variant: config.variant }, () => config.label)
return h(Badge, { variant: 'secondary' }, () => '随机')
},
},
{
@@ -505,6 +511,11 @@ const columns: ColumnDef<CpUser, any>[] = [
cell: ({ row }) => {
const user = row.original
return h('div', { class: 'flex items-center gap-1' }, [
h(IconButton, {
icon: ListPlus,
tooltip: '添加任务',
onClick: () => router.push({ path: '/cpdaily/task', query: { addUserId: String(user.id), addUsername: user.username } }),
}),
h(IconButton, {
icon: Smartphone,
tooltip: '自定义设备码',
@@ -710,13 +721,13 @@ const fetchFn = async (params: PageParams): Promise<PageResponse<CpUser>> => {
<div class="space-y-2">
<Label>设备码类型</Label>
<div class="flex rounded-md border overflow-hidden">
<button type="button" class="flex-1 px-4 py-2 text-sm font-medium transition-colors" :class="customDeviceType === 'manual' ? 'bg-primary text-primary-foreground' : 'bg-background hover:bg-accent'" @click="customDeviceType = 'manual'">手动</button>
<button type="button" class="flex-1 px-4 py-2 text-sm font-medium transition-colors border-l" :class="customDeviceType === 'random' ? 'bg-primary text-primary-foreground' : 'bg-background hover:bg-accent'" @click="customDeviceType = 'random'">随机</button>
<button type="button" class="flex-1 px-4 py-2 text-sm font-medium transition-colors" :class="customDeviceType === 'manual' ? 'bg-primary text-primary-foreground' : 'bg-background hover:bg-accent'" @click="customDeviceType = 'manual'; customDeviceCode = ''">手动</button>
<button type="button" class="flex-1 px-4 py-2 text-sm font-medium transition-colors border-l" :class="customDeviceType === 'random' ? 'bg-primary text-primary-foreground' : 'bg-background hover:bg-accent'" @click="customDeviceType = 'random'; customDeviceCode = generateUUID()">随机</button>
</div>
</div>
<div class="space-y-2">
<Label>设备码</Label>
<Input v-model="customDeviceCode" :placeholder="customDeviceType === 'random' ? '留空则随机生成' : '请输入设备码'" :disabled="customDeviceType === 'random'" />
<Input v-model="customDeviceCode" :placeholder="customDeviceType === 'random' ? '已自动生成' : '请输入设备码'" :disabled="customDeviceType === 'random'" />
</div>
<DialogFooter>
<Button type="button" variant="outline" @click="customDeviceOpen = false">取消</Button>
+13 -2
View File
@@ -93,8 +93,14 @@ onMounted(() => {
<template>
<div class="space-y-4 sm:space-y-6">
<!-- 公告 -->
<Card v-if="announcement" class="announcement-card border-primary/20 bg-primary/5">
<CardContent class="p-3 sm:p-4">
<Card v-if="announcementLoading">
<CardContent class="space-y-2">
<div class="h-4 w-3/4 rounded bg-muted animate-pulse" />
<div class="h-4 w-1/2 rounded bg-muted animate-pulse" />
</CardContent>
</Card>
<Card v-else-if="announcement">
<CardContent>
<MdPreview :modelValue="announcement" previewTheme="github" class="announcement-preview" />
</CardContent>
</Card>
@@ -221,8 +227,13 @@ onMounted(() => {
<style scoped>
.announcement-preview :deep(.md-editor-preview-wrapper) {
padding: 0;
background: transparent;
}
.announcement-preview :deep(.md-editor-preview) {
font-size: 0.875rem;
background: transparent;
}
.announcement-preview :deep(.md-editor-preview .md-editor-admonition) {
margin: 0;
}
</style>
+46
View File
@@ -38,6 +38,10 @@ const [inviteCode, inviteCodeAttrs] = defineField('inviteCode')
const captchaOpen = ref(false)
const submitting = ref(false)
const registered = ref(false)
const registeredEmail = ref('')
const resending = ref(false)
const resendCaptchaOpen = ref(false)
onMounted(async () => {
try {
@@ -76,6 +80,8 @@ async function onCaptchaConfirm(result: { captchaKey: string; dots: Array<{ x: n
dots: result.dots,
})
toast.success('注册成功,请前往邮箱验证')
registeredEmail.value = email.value!
registered.value = true
captchaOpen.value = false
} catch (err: any) {
if (err?.code === 10003) {
@@ -88,6 +94,38 @@ async function onCaptchaConfirm(result: { captchaKey: string; dots: Array<{ x: n
submitting.value = false
}
}
function onResendClick() {
const targetEmail = email.value || registeredEmail.value
if (!targetEmail) {
toast.error('请先填写邮箱')
return
}
registeredEmail.value = targetEmail
resendCaptchaOpen.value = true
}
async function onResendCaptchaConfirm(result: { captchaKey: string; dots: Array<{ x: number; y: number }> }) {
resending.value = true
try {
await authApi.resendVerification({
email: registeredEmail.value,
captchaKey: result.captchaKey,
dots: result.dots,
})
toast.success('验证邮件已重新发送,请查收')
resendCaptchaOpen.value = false
} catch (err: any) {
if (err?.code === 10003) {
resendCaptchaOpen.value = false
setTimeout(() => { resendCaptchaOpen.value = true }, 100)
} else {
resendCaptchaOpen.value = false
}
} finally {
resending.value = false
}
}
</script>
<template>
@@ -134,7 +172,15 @@ async function onCaptchaConfirm(result: { captchaKey: string; dots: Array<{ x: n
<router-link to="/login" class="text-primary hover:underline ml-1">去登录</router-link>
</div>
<div class="text-center text-sm">
<span class="text-muted-foreground">没有收到验证邮件?</span>
<button type="button" class="text-primary hover:underline ml-1" :disabled="resending" @click="onResendClick">
{{ resending ? '发送中...' : '点击这里重新发送' }}
</button>
</div>
<CaptchaDialog v-model:open="captchaOpen" @confirm="onCaptchaConfirm" />
<CaptchaDialog v-model:open="resendCaptchaOpen" @confirm="onResendCaptchaConfirm" />
</div>
</div>
</template>
+2 -2
View File
@@ -61,8 +61,8 @@ const onSubmit = handleSubmit(async (values) => {
})
toast.success('密码重置成功,请重新登录')
setTimeout(() => router.push('/login'), 1500)
} catch (err: any) {
toast.error(err?.response?.data?.message || err?.message || '重置失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
submitting.value = false
}
+4 -4
View File
@@ -64,7 +64,7 @@ async function fetchData() {
}
aiTypes.value = typesRes.data?.types ?? []
} catch {
toast.error('获取 AI 配置失败')
// 全局拦截器已处理错误提示
} finally {
loading.value = false
}
@@ -80,8 +80,8 @@ const onSave = handleSubmit(async (values) => {
apiKey: values.apiKey,
})
toast.success('AI 配置保存成功')
} catch (err: any) {
toast.error(err.response?.data?.message || '保存失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
saveLoading.value = false
}
@@ -105,7 +105,7 @@ const onTest = handleSubmit(async (values) => {
}
} catch (err: any) {
testResult.value = { success: false, message: err.response?.data?.message || '测试请求失败' }
toast.error(testResult.value.message)
// 全局拦截器已处理错误提示
} finally {
testLoading.value = false
}
+5 -5
View File
@@ -23,8 +23,8 @@ async function fetchAnnouncement() {
const val = res.data?.value ?? ''
announcementContent.value = val
originalAnnouncement.value = val
} catch (err: any) {
if (err.response?.status !== 404) toast.error('获取公告失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
announcementLoading.value = false
}
@@ -44,7 +44,7 @@ async function saveAnnouncement() {
toast.success('公告保存成功')
originalAnnouncement.value = announcementContent.value
} catch {
toast.error('保存失败')
// 全局拦截器已处理错误提示
} finally {
announcementSaving.value = false
}
@@ -82,7 +82,7 @@ async function saveRegisterConfig() {
})
toast.success('注册开关保存成功')
} catch {
toast.error('保存失败')
// 全局拦截器已处理错误提示
} finally {
registerSaving.value = false
}
@@ -119,7 +119,7 @@ async function savePriceConfig() {
await systemPriceApi.update(priceConfig.value)
toast.success('费率配置保存成功')
} catch {
toast.error('保存失败')
// 全局拦截器已处理错误提示
} finally {
priceSaving.value = false
}
+7 -7
View File
@@ -62,7 +62,7 @@ async function fetchMenuTree() {
updateChildMenuList()
}
} catch {
toast.error('获取菜单树失败')
// 全局拦截器已处理错误提示
} finally {
loading.value = false
}
@@ -199,8 +199,8 @@ function handleDelete(id: number, name: string) {
toast.success('删除成功')
if (selectedMenu.value?.id === id) selectedMenu.value = null
fetchMenuTree()
} catch (err: any) {
toast.error(err.response?.data?.message || '删除失败')
} catch {
// 全局拦截器已处理错误提示
}
})
}
@@ -298,8 +298,8 @@ const onMenuSubmit = handleMenuSubmit(async (values) => {
fetchMenuTree()
// 刷新按钮列表
if (selectedMenu.value) loadButtons(selectedMenu.value.id)
} catch (err: any) {
toast.error(err.response?.data?.message || '操作失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
editLoading.value = false
}
@@ -313,8 +313,8 @@ function handleDeleteBtn(id: number, name: string) {
toast.success('删除成功')
if (selectedMenu.value) loadButtons(selectedMenu.value.id)
fetchMenuTree()
} catch (err: any) {
toast.error(err.response?.data?.message || '删除失败')
} catch {
// 全局拦截器已处理错误提示
}
})
}
+3 -3
View File
@@ -52,7 +52,7 @@ async function fetchPrice() {
})
}
} catch {
toast.error('获取费率配置失败')
// 全局拦截器已处理错误提示
} finally {
loading.value = false
}
@@ -63,8 +63,8 @@ const onSave = handleSubmit(async (values) => {
try {
await systemPriceApi.update(values)
toast.success('费率配置保存成功')
} catch (err: any) {
toast.error(err.response?.data?.message || '保存失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
saveLoading.value = false
}
+7 -7
View File
@@ -104,8 +104,8 @@ const onRoleSubmit = handleRoleSubmit(async (values) => {
}
editDialogOpen.value = false
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '操作失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
editLoading.value = false
}
@@ -118,8 +118,8 @@ function handleDelete(id: number) {
await systemRoleApi.delete(id)
toast.success('删除成功')
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '删除失败')
} catch {
// 全局拦截器已处理错误提示
}
})
}
@@ -157,7 +157,7 @@ async function openMenuAuth(roleId: number) {
const ids = roleMenuRes.data
selectedMenuIds.value = Array.isArray(ids) ? ids : []
} catch {
toast.error('获取菜单数据失败')
// 全局拦截器已处理错误提示
} finally {
menuLoading.value = false
}
@@ -178,8 +178,8 @@ async function submitMenuAuth() {
await systemRoleApi.authorizeMenu(menuRoleId.value, selectedMenuIds.value)
toast.success('菜单授权成功')
menuDialogOpen.value = false
} catch (err: any) {
toast.error(err.response?.data?.message || '菜单授权失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
menuLoading.value = false
}
+10 -10
View File
@@ -81,8 +81,8 @@ const onResetPwdSubmit = handleResetPwdSubmit(async (values) => {
await systemUserApi.resetPassword(resetPwdUserId.value, values.password)
toast.success('密码重置成功')
resetPwdOpen.value = false
} catch (err: any) {
toast.error(err.response?.data?.message || '密码重置失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
resetPwdLoading.value = false
}
@@ -105,8 +105,8 @@ async function openRoleDialog(userId: number, currentRoles?: any[]) {
roleList.value = Array.isArray(data) ? data : (data as any)?.pageData ?? []
// 预选用户当前角色
selectedRoleIds.value = currentRoles?.map((r: any) => r.id) ?? []
} catch (err: any) {
toast.error('获取角色列表失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
roleLoading.value = false
}
@@ -128,8 +128,8 @@ async function submitRoleAuth() {
toast.success('角色授权成功')
roleDialogOpen.value = false
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '角色授权失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
roleLoading.value = false
}
@@ -144,8 +144,8 @@ async function handleBlock(id: number, currentBlock: boolean) {
await systemUserApi.blockUser(id, willBlock)
toast.success(`${action}成功`)
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || `${action}失败`)
} catch {
// 全局拦截器已处理错误提示
}
})
}
@@ -156,8 +156,8 @@ async function handleDelete(id: number) {
await systemUserApi.deleteUser(id)
toast.success('删除成功')
tableRef.value?.refresh()
} catch (err: any) {
toast.error(err.response?.data?.message || '删除失败')
} catch {
// 全局拦截器已处理错误提示
}
})
}
+4 -4
View File
@@ -50,8 +50,8 @@ const onPasswordSubmit = handlePasswordSubmit(async (values) => {
await userApi.resetPassword(values)
toast.success('密码修改成功')
resetPasswordForm()
} catch (err: any) {
toast.error(err.response?.data?.message || '密码修改失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
passwordLoading.value = false
}
@@ -69,8 +69,8 @@ async function handleRecharge() {
toast.success('充值成功')
cardCode.value = ''
userStore.fetchProfile()
} catch (err: any) {
toast.error(err.response?.data?.message || '充值失败')
} catch {
// 全局拦截器已处理错误提示
} finally {
rechargeLoading.value = false
}