优化backend端相关功能

This commit is contained in:
2026-09-05 17:28:40 +08:00
parent 3d53f03246
commit 0f74abc5d4
25 changed files with 781 additions and 440 deletions
+1
View File
@@ -29,6 +29,7 @@ declare module 'vue' {
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
ElColorPicker: typeof import('element-plus/es')['ElColorPicker']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+4 -1
View File
@@ -1,8 +1,11 @@
<script setup>
import zhCn from 'element-plus/es/locale/lang/zh-cn'
</script>
<template>
<router-view />
<el-config-provider :locale="zhCn">
<router-view />
</el-config-provider>
</template>
<style scoped>
+8
View File
@@ -89,3 +89,11 @@ export function getPositionList(params) {
params
})
}
// 获取职位详情(编辑联系人时,已选职位若不在当前部门列表内用于回显)
export function getPositionDetail(id) {
return request({
url: `/backend/erp/getPositionDetail/${id}`,
method: 'get'
})
}
+8
View File
@@ -80,6 +80,14 @@ export function getOpenVerify() {
});
}
// 获取当前登录用户信息(含租户名称)
export function getCurrentUser() {
return request({
url: '/backend/getCurrentUser',
method: 'get'
});
}
// 注册
export function register(data) {
return request({
+22
View File
@@ -69,6 +69,9 @@
</template>
</el-dropdown>
<span v-if="companyName" class="company-name" title="当前登录的企业">
{{ companyName }}
</span>
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link" style="cursor: pointer;">
<img :src="getImageUrl('user')" class="user" />
@@ -289,6 +292,11 @@ const displayName = computed(() => {
return user.account || '';
});
// 当前登录的企业(租户)名称
const companyName = computed(() => {
return authStore.user?.tenant_name || '';
});
const handleCollapse = () => {
store.state.isCollapse = !store.state.isCollapse;
};
@@ -544,6 +552,20 @@ onUnmounted(() => {
}
}
.company-name {
max-width: 200px;
padding: 4px 12px;
font-size: 13px;
font-weight: 500;
color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
border: 1px solid var(--el-color-primary-light-7);
border-radius: 14px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.el-dropdown-link {
display: flex;
align-items: center;
+24 -1
View File
@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
import { getCurrentUser } from '@/api/login'
// 用户信息类型
const defaultUser = {
@@ -8,6 +9,7 @@ const defaultUser = {
name: '',
group_id: '',
type: 'backend',
tenant_name: '',
avatar: ''
}
@@ -29,8 +31,27 @@ export const useAuthStore = defineStore('auth', () => {
}
}
// 从后端拉取最新用户信息(含租户名称),用于已有会话补充 tenant_name
async function fetchCurrentUser() {
if (!token.value) return
try {
const res = await getCurrentUser()
if (res && res.code === 200 && res.data) {
Object.assign(user, res.data)
localStorage.setItem('userInfo', JSON.stringify({ ...user }))
}
} catch (e) {
// 静默失败,不影响登录态
console.error('Failed to fetch current user info:', e)
}
}
// 初始化时加载用户信息
loadUserFromCache()
// 若已登录但缺少租户名称,异步从后端补齐
if (token.value) {
fetchCurrentUser()
}
// 保存登录信息(token 和用户信息)
function setLoginInfo(loginData) {
@@ -42,6 +63,7 @@ export const useAuthStore = defineStore('auth', () => {
name: userInfo.name || '',
group_id: userInfo.group_id || '',
type: 'backend',
tenant_name: userInfo.tenant_name || '',
tid: userInfo.tid || '',
avatar: userInfo.avatar || ''
}
@@ -101,7 +123,8 @@ export const useAuthStore = defineStore('auth', () => {
setToken,
clearToken,
checkAuth,
updateUserInfo
updateUserInfo,
fetchCurrentUser
}
})
@@ -23,22 +23,22 @@
/>
</div>
<div class="stats-row">
<div class="stat-card blue">
<div class="stat-card">
<div class="stat-label">核算员工</div>
<div class="stat-value">{{ dashboard.employee_count || 0 }}</div>
<div class="stat-sub">{{ dashboard.payroll_month || dashboardMonth }} 薪资单</div>
</div>
<div class="stat-card purple">
<div class="stat-card">
<div class="stat-label">应发工资</div>
<div class="stat-value">¥{{ money(dashboard.total_gross_salary) }}</div>
<div class="stat-sub">税前及各类补贴合计</div>
</div>
<div class="stat-card green">
<div class="stat-card">
<div class="stat-label">实发工资</div>
<div class="stat-value">¥{{ money(dashboard.total_net_salary) }}</div>
<div class="stat-sub">扣除五险一金及个税后</div>
</div>
<div class="stat-card orange">
<div class="stat-card">
<div class="stat-label">待处理</div>
<div class="stat-value">{{ (dashboard.draft_count || 0) + (dashboard.confirmed_count || 0) }}</div>
<div class="stat-sub">草稿 {{ dashboard.draft_count || 0 }} / 待发放 {{ dashboard.confirmed_count || 0 }}</div>
@@ -248,7 +248,7 @@
</el-table>
<div class="scheme-form" v-if="schemeFormVisible">
<div class="section-title">{{ schemeEditingID ? "编辑薪酬方案" : "新增薪酬方案" }}</div>
<el-form ref="schemeFormRef" :model="schemeForm" :rules="schemeRules" label-width="100px">
<el-form ref="schemeFormRef" :model="schemeForm" :rules="schemeRules" label-width="120px">
<div class="form-grid">
<el-form-item label="方案名称" prop="scheme_name"><el-input v-model="schemeForm.scheme_name" /></el-form-item>
<el-form-item label="适用员工" prop="employee_id"><el-select v-model="schemeForm.employee_id" filterable style="width: 100%"><el-option v-for="employee in employees" :key="employee.id" :label="employeeLabel(employee)" :value="employee.id" /></el-select></el-form-item>
@@ -340,7 +340,7 @@ function money(value) { return Number(value || 0).toLocaleString("zh-CN", { mini
function dateTime(value) { return value ? String(value).replace("T", " ").slice(0, 19) : "-" }
function statusText(status) { return ["草稿", "已确认", "已发放", "已作废"][status] || "未知" }
function statusType(status) { return ["info", "warning", "success", "danger"][status] || "info" }
function employeeLabel(employee) { return `${employee.name}${employee.department ? `(${employee.department})` : ""}` }
function employeeLabel(employee) { return `${employee.name}${employee.department_name ? `(${employee.department_name})` : (employee.department ? `(${employee.department})` : "")}` }
function employeeName(id) { return employees.value.find(item => item.id === id)?.name || "-" }
function fixedSchemeAmount(item) { return sum(item, ["base_salary", "post_allowance", "performance_salary", "transport_allowance", "meal_allowance", "communication_allowance"]) }
@@ -443,7 +443,7 @@ onMounted(async () => { await Promise.all([loadEmployees(), loadSchemes(), loadD
.header-actions, .filter-bar { gap: 12px; }
.dashboard-toolbar { justify-content: flex-end; gap: 10px; margin: -2px 0 10px; color: #606266; font-size: 13px; }
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; }
.stat-card { padding: 15px 18px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; border-top: 3px solid #409eff; }
.stat-card { padding: 15px 18px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; }
.stat-card.purple { border-top-color: #8b5cf6; }.stat-card.green { border-top-color: #67c23a; }.stat-card.orange { border-top-color: #e6a23c; }
.stat-label, .stat-sub { color: #909399; font-size: 13px; }.stat-value { margin: 7px 0 4px; font-size: 25px; font-weight: 600; color: #303133; }.stat-sub { font-size: 12px; }
.table-card { padding: 16px; background: #fff; border: 1px solid #ebeef5; border-radius: 8px; }.filter-bar { flex-wrap: wrap; margin-bottom: 14px; }.pagination { display: flex; justify-content: flex-end; margin-top: 16px; }
@@ -61,6 +61,9 @@
<el-descriptions-item label="微信号">
<span>{{ contact.wechat || '-' }}</span>
</el-descriptions-item>
<el-descriptions-item label="QQ">
<span>{{ contact.qq || '-' }}</span>
</el-descriptions-item>
</el-descriptions>
</div>
@@ -2,7 +2,7 @@
<el-dialog
v-model="visible"
:title="title"
width="620px"
width="720px"
:close-on-click-modal="false"
@close="handleClose"
>
@@ -13,150 +13,200 @@
label-width="100px"
label-position="right"
>
<el-form-item label="联系人类型" prop="contact_type">
<el-radio-group v-model="form.contact_type">
<el-radio :value="1">内部员工</el-radio>
<el-radio :value="2">外部联系人</el-radio>
</el-radio-group>
</el-form-item>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="联系人类型" prop="contact_type">
<el-radio-group v-model="form.contact_type">
<el-radio :value="1">内部员工</el-radio>
<el-radio :value="2">外部联系人</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="姓名" prop="contact_name">
<el-input
v-model="form.contact_name"
placeholder="请输入联系人姓名"
maxlength="64"
show-word-limit
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="姓名" prop="contact_name">
<el-input
v-model="form.contact_name"
placeholder="请输入联系人姓名"
maxlength="64"
show-word-limit
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="form.gender">
<el-radio :value="1">男</el-radio>
<el-radio :value="2">女</el-radio>
<el-radio :value="0">未知</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="性别" prop="gender">
<el-radio-group v-model="form.gender">
<el-radio :value="1">男</el-radio>
<el-radio :value="2">女</el-radio>
<el-radio :value="0">未知</el-radio>
</el-radio-group>
</el-form-item>
<el-row :gutter="20" v-if="form.contact_type === 1">
<el-col :span="12">
<el-form-item label="所属部门" prop="org_id">
<el-tree-select
v-model="form.org_id"
:data="orgOptions"
:props="orgTreeProps"
placeholder="请选择组织架构/部门"
clearable
check-strictly
style="width: 100%"
@change="handleOrgChange"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="职位" prop="position_id">
<el-select
v-model="form.position_id"
placeholder="请选择职位"
clearable
filterable
:loading="positionLoading"
style="width: 100%"
@change="handlePositionChange"
>
<el-option
v-for="item in positionOptions"
:key="item.id"
:label="item.position_name"
:value="item.id"
/>
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="form.contact_type === 1" label="所属部门" prop="org_id">
<el-tree-select
v-model="form.org_id"
:data="orgOptions"
:props="orgTreeProps"
placeholder="请选择组织架构/部门"
clearable
check-strictly
style="width: 100%"
@change="handleOrgChange"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="手机号" prop="phone">
<el-input
v-model="form.phone"
placeholder="请输入手机号"
maxlength="20"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="办公电话" prop="work_phone">
<el-input
v-model="form.work_phone"
placeholder="请输入办公电话"
maxlength="20"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="form.contact_type === 1" label="职位" prop="position_id">
<el-select
v-model="form.position_id"
placeholder="请选择职位"
clearable
filterable
:loading="positionLoading"
style="width: 100%"
@change="handlePositionChange"
>
<el-option
v-for="item in positionOptions"
:key="item.id"
:label="item.position_name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="邮箱" prop="email">
<el-input
v-model="form.email"
placeholder="请输入邮箱"
maxlength="128"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="微信号" prop="wechat">
<el-input
v-model="form.wechat"
placeholder="请输入微信号"
maxlength="64"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="手机号" prop="phone">
<el-input
v-model="form.phone"
placeholder="请输入手机号"
maxlength="20"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="QQ" prop="qq">
<el-input
v-model="form.qq"
placeholder="请输入QQ号"
maxlength="20"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="1">正常</el-radio>
<el-radio :value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="办公电话" prop="work_phone">
<el-input
v-model="form.work_phone"
placeholder="请输入办公电话"
maxlength="20"
/>
</el-form-item>
<el-row :gutter="20" v-if="form.contact_type === 2">
<el-col :span="12">
<el-form-item label="公司名称" prop="company_name">
<el-input
v-model="form.company_name"
placeholder="请输入公司名称"
maxlength="128"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="部门" prop="dept_name">
<el-input
v-model="form.dept_name"
placeholder="请输入部门名称"
maxlength="128"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="邮箱" prop="email">
<el-input
v-model="form.email"
placeholder="请输入邮箱"
maxlength="128"
/>
</el-form-item>
<el-row :gutter="20" v-if="form.contact_type === 2">
<el-col :span="24">
<el-form-item label="职位" prop="position_title">
<el-input
v-model="form.position_title"
placeholder="请输入职位"
maxlength="100"
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="微信号" prop="wechat">
<el-input
v-model="form.wechat"
placeholder="请输入微信号"
maxlength="64"
/>
</el-form-item>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="地址" prop="address">
<el-input
v-model="form.address"
type="textarea"
:rows="2"
placeholder="请输入地址"
maxlength="512"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
<el-form-item v-if="form.contact_type === 2" label="公司名称" prop="company_name">
<el-input
v-model="form.company_name"
placeholder="请输入公司名称"
maxlength="128"
/>
</el-form-item>
<el-form-item v-if="form.contact_type === 2" label="部门" prop="dept_name">
<el-input
v-model="form.dept_name"
placeholder="请输入部门名称"
maxlength="128"
/>
</el-form-item>
<el-form-item v-if="form.contact_type === 2" label="职位" prop="position_title">
<el-input
v-model="form.position_title"
placeholder="请输入职位"
maxlength="100"
/>
</el-form-item>
<el-form-item label="地址" prop="address">
<el-input
v-model="form.address"
type="textarea"
:rows="2"
placeholder="请输入地址"
maxlength="512"
show-word-limit
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="请输入备注"
maxlength="512"
show-word-limit
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number v-model="form.sort" :min="0" :max="99999" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="form.status">
<el-radio :value="1">正常</el-radio>
<el-radio :value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="备注" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="2"
placeholder="请输入备注"
maxlength="512"
show-word-limit
/>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
@@ -173,7 +223,7 @@
<script setup>
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { getOrganizationList, getPositionList } from '@/api/contactOA'
import { getOrganizationList, getPositionList, getPositionDetail } from '@/api/contactOA'
const props = defineProps({
modelValue: { type: Boolean, default: false },
@@ -201,6 +251,7 @@ const emptyForm = () => ({
work_phone: '',
email: '',
wechat: '',
qq: '',
avatar: '',
org_id: null,
company_name: '',
@@ -209,7 +260,6 @@ const emptyForm = () => ({
position_title: '',
address: '',
remark: '',
sort: 0,
status: 1
})
@@ -270,45 +320,39 @@ const loadOrgOptions = async () => {
const positionOptions = ref([])
const positionLoading = ref(false)
// 收集部门自身及其所有上级组织ID(职位可挂在任意层级组织上,如集团级“总经理”)
const collectOrgSelfAndAncestors = (deptId) => {
const ids = new Set()
if (!deptId) return ids
const parentMap = {}
const walk = (nodes) => {
nodes.forEach((n) => {
parentMap[n.id] = n.parent_id
if (n.children?.length) walk(n.children)
})
}
walk(orgOptions.value)
let cur = Number(deptId)
while (cur && !ids.has(cur)) {
ids.add(cur)
cur = parentMap[cur]
}
return ids
}
// 职位归属由 department_id 决定:只取挂在所选部门下的职位,
// 不再向上追溯到上级组织(否则选了"运维部"会把"总经办"的职位也带出来)。
const loadPositionOptions = async (deptId) => {
positionLoading.value = true
try {
const res = await getPositionList()
const params = deptId ? { department_id: deptId } : {}
const res = await getPositionList(params)
const data = res?.data || res || []
const list = Array.isArray(data) ? data : []
const enabled = list.filter((p) => p.status === 1)
if (!deptId) {
positionOptions.value = enabled
} else {
// 允许选择本部门及其上级组织的职位
const allowed = collectOrgSelfAndAncestors(deptId)
positionOptions.value = enabled.filter((p) => allowed.has(Number(p.department_id)))
}
positionOptions.value = list.filter((p) => p.status === 1)
} catch (error) {
positionOptions.value = []
} finally {
positionLoading.value = false
}
await ensureCurrentPosition()
}
// 编辑历史数据时,已选职位可能挂在其它组织上(如集团级的“总经理”),
// 这时把它补进选项,保证回显出职位名称而不是一个裸 ID。
const ensureCurrentPosition = async () => {
const positionId = form.value?.position_id
if (!positionId) return
if (positionOptions.value.some((p) => Number(p.id) === Number(positionId))) return
try {
const res = await getPositionDetail(positionId)
const detail = res?.data || res
if (detail?.id) {
positionOptions.value = [detail, ...positionOptions.value]
}
} catch (error) {
// silent
}
}
const handleOrgChange = (val) => {
+27 -38
View File
@@ -94,7 +94,7 @@
{{ row.contact_name?.charAt(0) }}
</el-avatar>
<div class="contact-name-info">
<span class="contact-name">{{ row.contact_name }}</span>
<span class="contact-name" @click.stop="openDetail(row)">{{ row.contact_name }}</span>
<el-tag
:type="row.contact_type === 1 ? 'primary' : 'info'"
size="small"
@@ -106,38 +106,37 @@
</div>
</template>
</el-table-column>
<el-table-column prop="phone" label="手机号" width="140" />
<el-table-column prop="org_name" label="部门" min-width="140">
<template #default="{ row }">
<span>{{ row.org_name || row.dept_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="position_title" label="职位" min-width="120">
<el-table-column prop="position_title" label="职位" width="140">
<template #default="{ row }">
<span>{{ row.position_title || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="org_name" label="部门" width="140">
<template #default="{ row }">
<span>{{ row.org_name || row.dept_name || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="phone" label="手机号" width="140" />
<!-- <el-table-column prop="wechat" label="微信" width="180">
<template #default="{ row }">
<span>{{ row.wechat || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="qq" label="QQ" width="140">
<template #default="{ row }">
<span>{{ row.qq || '-' }}</span>
</template>
</el-table-column>-->
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip>
<template #default="{ row }">
<span>{{ row.email || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="wechat" label="微信" width="120">
<template #default="{ row }">
<span>{{ row.wechat || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="120">
<el-table-column label="操作" fixed="right" width="140" align="center">
<template #default="{ row }">
<el-button link type="primary" size="small" title="编辑" @click.stop="openEditContact(row)">
<el-icon><Edit /></el-icon>
</el-button>
<el-button link type="primary" size="small" title="详情" @click.stop="openDetail(row)">
<el-icon><View /></el-icon>
</el-button>
<el-button link type="danger" size="small" title="删除" @click.stop="handleDelete(row)">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-table-column>
</el-table>
@@ -323,6 +322,7 @@ const editFormData = reactive({
work_phone: '',
email: '',
wechat: '',
qq: '',
avatar: '',
org_id: null,
company_name: '',
@@ -331,7 +331,6 @@ const editFormData = reactive({
position_title: '',
address: '',
remark: '',
sort: 0,
status: 1
})
@@ -354,7 +353,6 @@ const openCreateContact = () => {
position_title: '',
address: '',
remark: '',
sort: 0,
status: 1
})
editDialogVisible.value = true
@@ -371,6 +369,7 @@ const openEditContact = (row) => {
work_phone: row.work_phone,
email: row.email,
wechat: row.wechat,
qq: row.qq,
avatar: row.avatar,
org_id: row.org_id,
company_name: row.company_name,
@@ -379,7 +378,6 @@ const openEditContact = (row) => {
position_title: row.position_title,
address: row.address,
remark: row.remark,
sort: row.sort,
status: row.status
})
editDialogVisible.value = true
@@ -415,20 +413,6 @@ const handleDetailEdit = (row) => {
openEditContact(row)
}
// ---- 删除 ----
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(`确认删除联系人「${row.contact_name}」吗?`, '删除确认', { type: 'warning' })
await deleteContact(row.id)
ElMessage.success('删除成功')
await loadContactList()
} catch (error) {
if (error !== 'cancel' && error !== 'close') {
ElMessage.error(error?.message || '删除失败')
}
}
}
onMounted(() => {
loadOrgTree()
loadContactList()
@@ -585,7 +569,12 @@ onMounted(() => {
.contact-name {
font-weight: 500;
color: #303133;
color: #409eff;
cursor: pointer;
}
.contact-name:hover {
text-decoration: underline;
}
.contact-type-tag {
+187 -14
View File
@@ -29,7 +29,7 @@
<!-- 图表区域 -->
<el-row :gutter="16" class="chart-row">
<el-col :span="14" :xs="24">
<div class="chart-card">
<div class="chart-card trend-card">
<div class="chart-title">近6个月报销趋势</div>
<div ref="trendChartRef" class="chart-container"></div>
</div>
@@ -42,9 +42,21 @@
>查看全部</el-button
>
</div>
<el-tabs v-model="scheduleTab" class="schedule-tabs">
<el-tab-pane name="pending">
<template #label>
待完成 ({{ schedulePendingCount }})
</template>
</el-tab-pane>
<el-tab-pane name="done">
<template #label>
已完成 ({{ scheduleDoneCount }})
</template>
</el-tab-pane>
</el-tabs>
<div v-loading="scheduleLoading" class="schedule-list">
<div
v-for="item in scheduleReminders"
v-for="item in filteredScheduleReminders"
:key="item.id"
class="schedule-item"
:class="{ done: item.status === 1 }"
@@ -52,9 +64,9 @@
>
<el-checkbox
:model-value="item.status === 1"
@change="toggleScheduleFinish(item)"
@change="openScheduleAction(item)"
/>
<div class="schedule-item-main" @click="toggleScheduleFinish(item)">
<div class="schedule-item-main" @click="openScheduleAction(item)">
<div
class="schedule-item-title"
:class="{ done: item.status === 1 }"
@@ -81,11 +93,65 @@
</div>
</div>
<el-empty
v-if="!scheduleReminders.length && !scheduleLoading"
description="近期暂无日程安排"
v-if="!filteredScheduleReminders.length && !scheduleLoading"
:description="scheduleTab === 'done' ? '暂无已完成日程' : '近期暂无待完成日程'"
:image-size="60"
/>
</div>
<!-- 日程操作确认弹窗 -->
<el-dialog
v-model="scheduleActionVisible"
title="日程操作"
width="420px"
align-center
>
<div v-if="activeSchedule" class="schedule-action-content">
<div class="schedule-action-title">{{ activeSchedule.title }}</div>
<div class="schedule-action-meta">
<span :class="{ overdue: isOverdueSchedule(activeSchedule) }">{{
scheduleDateTag(activeSchedule)
}}</span>
<span>{{ scheduleTimeText(activeSchedule) }}</span>
<el-tag
v-if="activeSchedule.priority > 0"
:type="activeSchedule.priority === 2 ? 'danger' : 'warning'"
size="small"
effect="light"
>
{{ activeSchedule.priority === 2 ? "紧急" : "重要" }}
</el-tag>
<el-tag
:type="activeSchedule.status === 1 ? 'success' : 'info'"
size="small"
>
{{ activeSchedule.status === 1 ? "已完成" : "待完成" }}
</el-tag>
</div>
<div class="schedule-action-tip">
请选择要执行的操作:
</div>
<div class="schedule-action-btns">
<el-button @click="editSchedule(activeSchedule)">
修改
</el-button>
<el-button
v-if="activeSchedule.status === 0"
type="success"
@click="confirmScheduleFinish(activeSchedule)"
>
标记完成
</el-button>
<el-button
v-if="activeSchedule.status === 1"
type="warning"
@click="confirmScheduleFinish(activeSchedule)"
>
反审核
</el-button>
</div>
</div>
</el-dialog>
</div>
</el-col>
</el-row>
@@ -248,6 +314,18 @@ const responseData = (res) => res?.data?.data ?? res?.data ?? res ?? {};
const router = useRouter();
const scheduleReminders = ref([]);
const scheduleLoading = ref(false);
const scheduleTab = ref("pending");
const schedulePendingCount = computed(
() => scheduleReminders.value.filter((r) => r.status === 0).length,
);
const scheduleDoneCount = computed(
() => scheduleReminders.value.filter((r) => r.status === 1).length,
);
const filteredScheduleReminders = computed(() => {
const status = scheduleTab.value === "done" ? 1 : 0;
return scheduleReminders.value.filter((r) => r.status === status);
});
const pad2 = (n) => String(n).padStart(2, "0");
const fmtDate = (d) =>
@@ -335,18 +413,46 @@ const scheduleTimeText = (item) =>
const isOverdueSchedule = (item) =>
item.status === 0 && item.schedule_date < fmtDate(new Date());
const toggleScheduleFinish = async (item) => {
const res = await finishSchedule(item.id);
if (res?.code === 200) {
ElMessage.success(res.data?.status === 1 ? "已完成" : "已恢复待办");
loadScheduleReminders();
} else {
ElMessage.error(res?.msg || "操作失败");
// 点击日程项:打开操作弹窗,不直接改状态
const scheduleActionVisible = ref(false);
const activeSchedule = ref(null);
const scheduleActioning = ref(false);
const openScheduleAction = (item) => {
activeSchedule.value = item;
scheduleActionVisible.value = true;
};
// 在弹窗中确认执行「已完成 / 反审核」操作
const confirmScheduleFinish = async (item) => {
if (scheduleActioning.value) return;
scheduleActioning.value = true;
try {
const res = await finishSchedule(item.id);
if (res?.code === 200) {
ElMessage.success(res.data?.status === 1 ? "已完成" : "已恢复待办");
scheduleActionVisible.value = false;
loadScheduleReminders();
} else {
ElMessage.error(res?.msg || "操作失败");
}
} catch (error) {
ElMessage.error(error?.message || "操作失败");
} finally {
scheduleActioning.value = false;
}
};
const goSchedule = () => router.push("/apps/oa/schedule");
// 跳转到日程管理页并打开该日程的编辑抽屉
const editSchedule = (item) => {
// 通过 sessionStorage 把待编辑日程传递给日程管理页,由其打开编辑抽屉
sessionStorage.setItem("schedule_edit_item", JSON.stringify(item));
scheduleActionVisible.value = false;
router.push("/apps/oa/schedule");
};
const loadDashboard = async () => {
try {
const data = responseData(await getReimbursementDashboard());
@@ -484,12 +590,24 @@ onBeforeUnmount(() => {
}
}
.trend-card {
height: 398px;
display: flex;
flex-direction: column;
.chart-container {
flex: 1;
min-height: 0;
height: auto;
}
}
.type-card {
margin-bottom: 0;
}
.schedule-card {
height: 100%;
height: 398px;
display: flex;
flex-direction: column;
@@ -499,8 +617,27 @@ onBeforeUnmount(() => {
justify-content: space-between;
}
.schedule-tabs {
margin-bottom: 20px;
:deep(.el-tabs__header) {
margin: 0;
}
:deep(.el-tabs__nav-wrap::after) {
height: 1px;
}
:deep(.el-tabs__item) {
height: 36px;
line-height: 36px;
}
}
.schedule-list {
flex: 1;
min-height: 0;
overflow-y: auto;
}
}
@@ -564,6 +701,42 @@ onBeforeUnmount(() => {
}
}
.schedule-action-content {
.schedule-action-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin-bottom: 8px;
}
.schedule-action-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
font-size: 13px;
color: #606266;
margin-bottom: 16px;
.overdue {
color: #f56c6c;
font-weight: 500;
}
}
.schedule-action-tip {
font-size: 13px;
color: #909399;
margin-bottom: 12px;
}
.schedule-action-btns {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
}
.status-dist,
.type-dist {
display: flex;
@@ -50,9 +50,8 @@
</div>
</div>
<template #footer>
<el-button @click="visible = false">关闭</el-button>
<el-button @click="emit('toggle', schedule)">{{
schedule?.status === 1 ? "取消完成" : "完成"
schedule?.status === 1 ? "反审核" : "完成"
}}</el-button>
<el-button type="primary" @click="emit('edit', schedule)"
>编辑</el-button
+2 -2
View File
@@ -208,7 +208,7 @@ const performLoginRequest = async () => {
localStorage.setItem("loginRememberMe", "false");
}
authStore.setLoginInfo(res.data);
authStore.setLoginInfo({ ...res.data, tenant_name: tenant_name.value });
// 重置 Tabs 状态
const { useTabsStore } = await import("@/stores");
@@ -339,7 +339,7 @@ const startGeetest4 = async () => {
localStorage.setItem("loginRememberMe", "false");
}
authStore.setLoginInfo(loginRes.data);
authStore.setLoginInfo({ ...loginRes.data, tenant_name: tenant_name.value });
const { useTabsStore } = await import("@/stores");
const tabsStore = useTabsStore();
tabsStore.resetTabs();