feat: 第四阶段财务与人事薪资模块

- 新增2个migration(11张表): finance(6表)/hr(5表)
- 新增11个Model + 11个Controller
- 新增50条API路由(总计262条)
- 新增2个前端API模块 + 10个管理页面
- 迁移运行通过, vite build验证通过
This commit is contained in:
li
2026-03-13 20:35:19 +08:00
parent e7d4d9b414
commit fb5e0daf1a
37 changed files with 3157 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
import request from '@/utils/request.js'
// ─── Finance Categories ──────────────────────────────────────────────────────
export const financeCategoryApi = {
getList: (params) => request.get('/finance/categories', { params }),
getDetail: (id) => request.get(`/finance/categories/${id}`),
create: (data) => request.post('/finance/categories', data),
update: (id, data) => request.put(`/finance/categories/${id}`, data),
delete: (id) => request.delete(`/finance/categories/${id}`)
}
// ─── Finance Records ─────────────────────────────────────────────────────────
export const financeRecordApi = {
getList: (params) => request.get('/finance/records', { params }),
getDetail: (id) => request.get(`/finance/records/${id}`),
create: (data) => request.post('/finance/records', data),
update: (id, data) => request.put(`/finance/records/${id}`, data),
delete: (id) => request.delete(`/finance/records/${id}`),
audit: (id, data) => request.put(`/finance/records/${id}/audit`, data)
}
// ─── Customer Accounts ───────────────────────────────────────────────────────
export const customerAccountApi = {
getList: (params) => request.get('/finance/accounts', { params }),
recharge: (data) => request.post('/finance/accounts/recharge', data)
}
// ─── Account Transactions ────────────────────────────────────────────────────
export const accountTransactionApi = {
getList: (params) => request.get('/finance/transactions', { params })
}
// ─── Prepaid Cards ───────────────────────────────────────────────────────────
export const prepaidCardApi = {
getList: (params) => request.get('/finance/prepaid-cards', { params }),
getDetail: (id) => request.get(`/finance/prepaid-cards/${id}`),
create: (data) => request.post('/finance/prepaid-cards', data),
update: (id, data) => request.put(`/finance/prepaid-cards/${id}`, data),
delete: (id) => request.delete(`/finance/prepaid-cards/${id}`)
}
// ─── Invoices ────────────────────────────────────────────────────────────────
export const invoiceApi = {
getList: (params) => request.get('/finance/invoices', { params }),
getDetail: (id) => request.get(`/finance/invoices/${id}`),
create: (data) => request.post('/finance/invoices', data),
update: (id, data) => request.put(`/finance/invoices/${id}`, data),
delete: (id) => request.delete(`/finance/invoices/${id}`),
issue: (id) => request.put(`/finance/invoices/${id}/issue`)
}
+45
View File
@@ -0,0 +1,45 @@
import request from '@/utils/request.js'
// ─── Employee Profiles ───────────────────────────────────────────────────────
export const employeeProfileApi = {
getList: (params) => request.get('/hr/profiles', { params }),
getDetail: (id) => request.get(`/hr/profiles/${id}`),
create: (data) => request.post('/hr/profiles', data),
update: (id, data) => request.put(`/hr/profiles/${id}`, data),
delete: (id) => request.delete(`/hr/profiles/${id}`)
}
// ─── Schedules ───────────────────────────────────────────────────────────────
export const scheduleApi = {
getList: (params) => request.get('/hr/schedules', { params }),
create: (data) => request.post('/hr/schedules', data)
}
// ─── Attendance Records ──────────────────────────────────────────────────────
export const attendanceApi = {
getList: (params) => request.get('/hr/attendances', { params }),
create: (data) => request.post('/hr/attendances', data),
clockIn: () => request.post('/hr/attendances/clock-in'),
clockOut: () => request.post('/hr/attendances/clock-out')
}
// ─── Leave Requests ──────────────────────────────────────────────────────────
export const leaveRequestApi = {
getList: (params) => request.get('/hr/leaves', { params }),
getDetail: (id) => request.get(`/hr/leaves/${id}`),
create: (data) => request.post('/hr/leaves', data),
update: (id, data) => request.put(`/hr/leaves/${id}`, data),
delete: (id) => request.delete(`/hr/leaves/${id}`),
audit: (id, data) => request.put(`/hr/leaves/${id}/audit`, data)
}
// ─── Salary Records ──────────────────────────────────────────────────────────
export const salaryRecordApi = {
getList: (params) => request.get('/hr/salaries', { params }),
getDetail: (id) => request.get(`/hr/salaries/${id}`),
create: (data) => request.post('/hr/salaries', data),
update: (id, data) => request.put(`/hr/salaries/${id}`, data),
delete: (id) => request.delete(`/hr/salaries/${id}`),
confirm: (id) => request.put(`/hr/salaries/${id}/confirm`),
pay: (id) => request.put(`/hr/salaries/${id}/pay`)
}
@@ -0,0 +1,162 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { customerAccountApi, accountTransactionApi } from '@/api/finance.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const searchForm = reactive({ customer_id: '' })
const pagination = reactive({ page: 1, per_page: 20 })
// ─── Recharge Dialog ────────────────────────────────────────────────────────
const rechargeVisible = ref(false)
const rechargeLoading = ref(false)
const rechargeFormRef = ref(null)
const rechargeForm = reactive({ customer_account_id: null, customer_id: null, amount: 0, pay_method: '' })
const rechargeRules = {
amount: [{ required: true, message: '请输入充值金额', trigger: 'blur' }]
}
const payMethods = [
{ label: '现金', value: 'cash' },
{ label: '微信', value: 'wechat' },
{ label: '支付宝', value: 'alipay' },
{ label: '银行转账', value: 'bank' },
{ label: '储值卡', value: 'card' }
]
// ─── Transaction History Dialog ─────────────────────────────────────────────
const txVisible = ref(false)
const txLoading = ref(false)
const txData = ref([])
const txTotal = ref(0)
const txAccountId = ref(null)
const txPagination = reactive({ page: 1, per_page: 20 })
const txTypeMap = { 1: '充值', 2: '消费', 3: '退款', 4: '积分变动' }
const txTypeType = { 1: 'success', 2: 'warning', 3: 'danger', 4: 'info' }
// ─── Main List ──────────────────────────────────────────────────────────────
async function fetchList() {
loading.value = true
try {
const res = await customerAccountApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { customer_id: '' }); handleSearch() }
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
// ─── Recharge ───────────────────────────────────────────────────────────────
function handleRecharge(row) {
Object.assign(rechargeForm, { customer_account_id: row.id, customer_id: row.customer_id, amount: 0, pay_method: '' })
rechargeVisible.value = true
}
async function submitRecharge() {
if (!rechargeFormRef.value) return
await rechargeFormRef.value.validate(async (valid) => {
if (!valid) return
rechargeLoading.value = true
try {
await customerAccountApi.recharge(rechargeForm)
ElMessage.success('充值成功')
rechargeVisible.value = false
fetchList()
} finally { rechargeLoading.value = false }
})
}
// ─── Transaction History ────────────────────────────────────────────────────
function handleViewTx(row) {
txAccountId.value = row.id
txPagination.page = 1
txVisible.value = true
fetchTxList()
}
async function fetchTxList() {
txLoading.value = true
try {
const res = await accountTransactionApi.getList({ customer_account_id: txAccountId.value, ...txPagination })
txData.value = res.data?.list || res.data?.data || []
txTotal.value = res.data?.total || 0
} finally { txLoading.value = false }
}
function handleTxPageChange(val) { txPagination.page = val; fetchTxList() }
function handleTxSizeChange(val) { txPagination.per_page = val; txPagination.page = 1; fetchTxList() }
onMounted(() => fetchList())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="searchForm.customer_id" placeholder="客户ID" style="width:160px" clearable @keydown.enter="handleSearch" />
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column label="客户姓名" min-width="120">
<template #default="{ row }">{{ row.customer?.name || '-' }}</template>
</el-table-column>
<el-table-column prop="cash_balance" label="现金余额" min-width="120" align="right" />
<el-table-column prop="card_balance" label="储值卡余额" min-width="120" align="right" />
<el-table-column prop="points" label="积分" min-width="100" align="right" />
<el-table-column label="操作" width="180" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleRecharge(row)">充值</el-button>
<el-button size="small" type="primary" link @click="handleViewTx(row)">查看流水</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10, 20, 50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<!-- Recharge Dialog -->
<el-dialog v-model="rechargeVisible" title="账户充值" width="480px" destroy-on-close>
<el-form ref="rechargeFormRef" :model="rechargeForm" :rules="rechargeRules" label-width="90px">
<el-form-item label="客户ID">
<el-input :model-value="rechargeForm.customer_id" disabled />
</el-form-item>
<el-form-item label="充值金额" prop="amount">
<el-input-number v-model="rechargeForm.amount" :min="0.01" :precision="2" :step="100" style="width:100%" />
</el-form-item>
<el-form-item label="支付方式">
<el-select v-model="rechargeForm.pay_method" placeholder="请选择支付方式" style="width:100%">
<el-option v-for="m in payMethods" :key="m.value" :label="m.label" :value="m.value" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="rechargeVisible = false">取消</el-button>
<el-button type="primary" :loading="rechargeLoading" @click="submitRecharge">确定</el-button>
</template>
</el-dialog>
<!-- Transaction History Dialog -->
<el-dialog v-model="txVisible" title="账户流水" width="800px" destroy-on-close>
<el-table v-loading="txLoading" :data="txData" border stripe style="width:100%">
<el-table-column label="类型" width="110" align="center">
<template #default="{ row }">
<el-tag :type="txTypeType[row.type]" size="small">{{ txTypeMap[row.type] || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额" min-width="100" align="right" />
<el-table-column prop="balance_after" label="变动后余额" min-width="120" align="right" />
<el-table-column prop="description" label="描述" min-width="180" show-overflow-tooltip />
<el-table-column prop="created_at" label="时间" min-width="160" />
</el-table>
<el-pagination v-model:current-page="txPagination.page" v-model:page-size="txPagination.per_page" :total="txTotal" :page-sizes="[10, 20, 50]" layout="total,sizes,prev,pager,next,jumper" background style="margin-top:12px" @current-change="handleTxPageChange" @size-change="handleTxSizeChange" />
</el-dialog>
</div>
</template>
@@ -0,0 +1,143 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { financeCategoryApi } from '@/api/finance.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ type: '', status: '' })
const pagination = reactive({ page: 1, per_page: 50 })
const formRef = ref(null)
const form = reactive({ id: null, name: '', type: 1, parent_id: 0, level: 1, sort: 0, status: 1 })
const isEdit = ref(false)
const formRules = {
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }]
}
const typeMap = { 1: '收入', 2: '支出' }
async function fetchList() {
loading.value = true
try {
const res = await financeCategoryApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { type: '', status: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false
Object.assign(form, { id: null, name: '', type: 1, parent_id: 0, level: 1, sort: 0, status: 1 })
dialogVisible.value = true
}
async function handleEdit(row) {
isEdit.value = true
Object.assign(form, {
id: row.id,
name: row.name,
type: row.type,
parent_id: row.parent_id || 0,
level: row.level || 1,
sort: row.sort || 0,
status: row.status
})
dialogVisible.value = true
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) { await financeCategoryApi.update(form.id, form); ElMessage.success('更新成功') }
else { await financeCategoryApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除分类「${row.name}」吗?`, '删除确认', { type: 'warning' })
await financeCategoryApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => fetchList())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px">
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增分类</el-button></div>
<el-table v-loading="loading" :data="tableData" border stripe row-key="id" default-expand-all style="width:100%">
<el-table-column prop="name" label="分类名称" min-width="180" />
<el-table-column label="类型" width="100" align="center">
<template #default="{ row }">
<el-tag :type="row.type === 1 ? 'success' : 'danger'" size="small">{{ typeMap[row.type] }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="level" label="层级" width="80" align="center" />
<el-table-column prop="sort" label="排序" width="80" align="center" />
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="row.status ? 'success' : 'danger'" size="small">{{ row.status ? '启用' : '停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" min-width="160" />
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[20,50,100]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑分类' : '新增分类'" width="450px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
<el-form-item label="分类名称" prop="name"><el-input v-model="form.name" placeholder="请输入分类名称" /></el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="form.type" style="width:100%">
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
<el-form-item label="上级分类">
<el-input-number v-model="form.parent_id" :min="0" style="width:100%" />
<div style="font-size:12px;color:var(--color-text-secondary)">0 表示顶级分类</div>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort" :min="0" :max="9999" style="width:100%" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
@@ -0,0 +1,118 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { invoiceApi } from '@/api/finance.js'
const statusMap = { 0: '待开', 1: '已开', 2: '已红冲' }
const statusType = { 0: 'warning', 1: 'success', 2: 'danger' }
const typeMap = { 1: '电子', 2: '纸质' }
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ invoice_no: '', status: '', type: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, invoice_no: '', customer_id: '', type: 1, amount: 0, tax_amount: 0, title: '', tax_no: '' })
const isEdit = ref(false)
const formRules = { invoice_no: [{ required: true, message: '请输入发票号', trigger: 'blur' }] }
async function fetchList() {
loading.value = true
try {
const res = await invoiceApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { invoice_no: '', status: '', type: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false
Object.assign(form, { id: null, invoice_no: '', customer_id: '', type: 1, amount: 0, tax_amount: 0, title: '', tax_no: '' })
dialogVisible.value = true
}
async function handleEdit(row) { isEdit.value = true; Object.assign(form, { ...row }); dialogVisible.value = true }
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) { await invoiceApi.update(form.id, form); ElMessage.success('更新成功') }
else { await invoiceApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除发票「${row.invoice_no}」吗?`, '删除确认', { type: 'warning' })
await invoiceApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
async function handleIssue(row) {
await ElMessageBox.confirm(`确定开具发票「${row.invoice_no}」吗?`, '开票确认', { type: 'info' })
await invoiceApi.issue(row.id); ElMessage.success('开票成功'); fetchList()
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => fetchList())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="searchForm.invoice_no" placeholder="发票号" style="width:180px" clearable @keydown.enter="handleSearch" />
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-select v-model="searchForm.type" placeholder="类型" clearable style="width:120px">
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增发票</el-button></div>
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column prop="invoice_no" label="发票号" min-width="150" />
<el-table-column prop="customer_id" label="客户ID" width="100" align="center" />
<el-table-column label="类型" width="80" align="center"><template #default="{ row }"><el-tag size="small">{{ typeMap[row.type] }}</el-tag></template></el-table-column>
<el-table-column prop="amount" label="金额" width="100" align="right" />
<el-table-column prop="tax_amount" label="税额" width="100" align="right" />
<el-table-column prop="title" label="发票抬头" min-width="180" show-overflow-tooltip />
<el-table-column label="状态" width="90" align="center"><template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template></el-table-column>
<el-table-column prop="issued_at" label="开票时间" width="160" />
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button v-if="row.status===0" size="small" type="success" link @click="handleIssue(row)">开票</el-button>
<el-button v-if="row.status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button v-if="row.status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑发票':'新增发票'" width="550px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-form-item label="发票号" prop="invoice_no"><el-input v-model="form.invoice_no" placeholder="请输入发票号" /></el-form-item>
<el-form-item label="客户ID"><el-input v-model="form.customer_id" placeholder="请输入客户ID" /></el-form-item>
<el-form-item label="类型"><el-select v-model="form.type" style="width:100%"><el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" /></el-select></el-form-item>
<el-form-item label="金额"><el-input-number v-model="form.amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="税额"><el-input-number v-model="form.tax_amount" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="发票抬头"><el-input v-model="form.title" placeholder="请输入发票抬头" /></el-form-item>
<el-form-item label="税号"><el-input v-model="form.tax_no" placeholder="请输入税号" /></el-form-item>
</el-form>
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button></template>
</el-dialog>
</div>
</template>
@@ -0,0 +1,104 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { prepaidCardApi } from '@/api/finance.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ name: '', status: '' })
const pagination = reactive({ page: 1, per_page: 15 })
const formRef = ref(null)
const form = reactive({ id: null, name: '', face_value: 0, sell_price: 0, discount_rate: 1, validity_days: 365, status: 1, sort: 0 })
const isEdit = ref(false)
const formRules = { name: [{ required: true, message: '请输入储值卡名称', trigger: 'blur' }] }
async function fetchList() {
loading.value = true
try {
const res = await prepaidCardApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { name: '', status: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false
Object.assign(form, { id: null, name: '', face_value: 0, sell_price: 0, discount_rate: 1, validity_days: 365, status: 1, sort: 0 })
dialogVisible.value = true
}
async function handleEdit(row) { isEdit.value = true; Object.assign(form, { ...row }); dialogVisible.value = true }
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) { await prepaidCardApi.update(form.id, form); ElMessage.success('更新成功') }
else { await prepaidCardApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除储值卡「${row.name}」吗?`, '删除确认', { type: 'warning' })
await prepaidCardApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => fetchList())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="searchForm.name" placeholder="储值卡名称" style="width:180px" clearable @keydown.enter="handleSearch" />
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
<el-option label="启用" :value="1" />
<el-option label="停用" :value="0" />
</el-select>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增储值卡</el-button></div>
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column prop="name" label="储值卡名称" min-width="150" />
<el-table-column prop="face_value" label="面值" width="100" align="right" />
<el-table-column prop="sell_price" label="售价" width="100" align="right" />
<el-table-column prop="discount_rate" label="折扣率" width="90" align="center" />
<el-table-column prop="validity_days" label="有效期(天)" width="100" align="center" />
<el-table-column label="状态" width="80" align="center"><template #default="{ row }"><el-tag :type="row.status?'success':'danger'" size="small">{{ row.status?'启用':'停用' }}</el-tag></template></el-table-column>
<el-table-column label="操作" width="160" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,15,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<el-dialog v-model="dialogVisible" :title="isEdit?'编辑储值卡':'新增储值卡'" width="500px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-form-item label="储值卡名称" prop="name"><el-input v-model="form.name" placeholder="请输入储值卡名称" /></el-form-item>
<el-form-item label="面值"><el-input-number v-model="form.face_value" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="售价"><el-input-number v-model="form.sell_price" :min="0" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="折扣率"><el-input-number v-model="form.discount_rate" :min="0" :max="1" :step="0.01" :precision="2" style="width:100%" /></el-form-item>
<el-form-item label="有效期(天)"><el-input-number v-model="form.validity_days" :min="1" style="width:100%" /></el-form-item>
<el-form-item label="状态"><el-switch v-model="form.status" :active-value="1" :inactive-value="0" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.sort" :min="0" :max="999" /></el-form-item>
</el-form>
<template #footer><el-button @click="dialogVisible=false">取消</el-button><el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button></template>
</el-dialog>
</div>
</template>
@@ -0,0 +1,220 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { financeRecordApi } from '@/api/finance.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('新增收支记录')
const submitLoading = ref(false)
const auditVisible = ref(false)
const currentRecord = ref(null)
const isEdit = ref(false)
const searchForm = reactive({ type: '', audit_status: '', record_no: '' })
const pagination = reactive({ page: 1, per_page: 20 })
const formRef = ref(null)
const form = reactive({
id: null, record_no: '', type: '', category_id: '',
amount: 0, pay_method: '', description: '', record_date: ''
})
const formRules = {
record_no: [{ required: true, message: '请输入记录编号', trigger: 'blur' }],
type: [{ required: true, message: '请选择类型', trigger: 'change' }],
amount: [{ required: true, message: '请输入金额', trigger: 'blur' }],
record_date: [{ required: true, message: '请选择日期', trigger: 'change' }]
}
const auditForm = reactive({ status: 1, audit_remark: '' })
const typeMap = { 1: '收入', 2: '支出' }
const typeType = { 1: 'success', 2: 'danger' }
const auditMap = { 0: '待审核', 1: '已通过', 2: '已驳回' }
const auditType = { 0: 'warning', 1: 'success', 2: 'danger' }
const payMethodMap = { 1: '现金', 2: '微信', 3: '支付宝', 4: '银行转账' }
async function fetchList() {
loading.value = true
try {
const res = await financeRecordApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { type: '', audit_status: '', record_no: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false; dialogTitle.value = '新增收支记录'
Object.assign(form, { id: null, record_no: '', type: '', category_id: '', amount: 0, pay_method: '', description: '', record_date: '' })
dialogVisible.value = true
}
async function handleEdit(row) {
if (row.audit_status !== 0) { ElMessage.warning('只能编辑待审核记录'); return }
isEdit.value = true; dialogTitle.value = '编辑收支记录'
const res = await financeRecordApi.getDetail(row.id)
const d = res.data
Object.assign(form, {
id: d.id, record_no: d.record_no, type: d.type, category_id: d.category_id || '',
amount: d.amount, pay_method: d.pay_method || '', description: d.description || '', record_date: d.record_date || ''
})
dialogVisible.value = true
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) { await financeRecordApi.update(form.id, form); ElMessage.success('更新成功') }
else { await financeRecordApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
if (row.audit_status !== 0) { ElMessage.warning('只能删除待审核记录'); return }
await ElMessageBox.confirm('确定删除该收支记录吗?', '删除确认', { type: 'warning' })
await financeRecordApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
function openAudit(row) {
currentRecord.value = row
Object.assign(auditForm, { status: 1, audit_remark: '' })
auditVisible.value = true
}
async function submitAudit() {
await financeRecordApi.audit(currentRecord.value.id, auditForm)
ElMessage.success('审核完成')
auditVisible.value = false; fetchList()
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => { fetchList() })
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-select v-model="searchForm.type" placeholder="收支类型" clearable style="width:120px">
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-select v-model="searchForm.audit_status" placeholder="审核状态" clearable style="width:120px">
<el-option v-for="(v, k) in auditMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-input v-model="searchForm.record_no" placeholder="记录编号" style="width:180px" clearable @keydown.enter="handleSearch" />
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增记录</el-button></div>
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column prop="record_no" label="记录编号" min-width="150" />
<el-table-column label="类型" width="90" align="center">
<template #default="{ row }"><el-tag :type="typeType[row.type]" size="small">{{ typeMap[row.type] || '-' }}</el-tag></template>
</el-table-column>
<el-table-column prop="category" label="分类" min-width="120">
<template #default="{ row }">{{ row.category?.name || row.category || '-' }}</template>
</el-table-column>
<el-table-column prop="amount" label="金额" min-width="110" align="right" />
<el-table-column label="支付方式" width="110" align="center">
<template #default="{ row }">{{ payMethodMap[row.pay_method] || '-' }}</template>
</el-table-column>
<el-table-column label="审核状态" width="100" align="center">
<template #default="{ row }"><el-tag :type="auditType[row.audit_status]" size="small">{{ auditMap[row.audit_status] }}</el-tag></template>
</el-table-column>
<el-table-column prop="record_date" label="记录日期" min-width="120" />
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button v-if="row.audit_status===0" size="small" type="success" link @click="openAudit(row)">审核</el-button>
<el-button v-if="row.audit_status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button v-if="row.audit_status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
<span v-if="row.audit_status!==0" class="text-muted">-</span>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<!-- Add/Edit -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="650px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="记录编号" prop="record_no">
<el-input v-model="form.record_no" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="类型" prop="type">
<el-select v-model="form.type" placeholder="选择类型" style="width:100%">
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="分类">
<el-input v-model="form.category_id" placeholder="分类ID" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="金额" prop="amount">
<el-input-number v-model="form.amount" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="支付方式">
<el-select v-model="form.pay_method" placeholder="选择支付方式" clearable style="width:100%">
<el-option v-for="(v, k) in payMethodMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="记录日期" prop="record_date">
<el-date-picker v-model="form.record_date" type="date" value-format="YYYY-MM-DD" placeholder="选择日期" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="描述">
<el-input v-model="form.description" type="textarea" :rows="3" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
<!-- Audit -->
<el-dialog v-model="auditVisible" title="收支审核" width="450px" destroy-on-close>
<el-form label-width="80px">
<el-form-item label="审核结果">
<el-radio-group v-model="auditForm.status">
<el-radio :value="1">通过</el-radio>
<el-radio :value="2">驳回</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="审核备注">
<el-input v-model="auditForm.audit_remark" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="auditVisible = false">取消</el-button>
<el-button type="primary" @click="submitAudit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
+141
View File
@@ -0,0 +1,141 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { attendanceApi } from '@/api/hr.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ user_id: '', status: '', date_range: [] })
const pagination = reactive({ page: 1, per_page: 20 })
const formRef = ref(null)
const form = reactive({ user_id: '', attendance_date: '', clock_in: '', clock_out: '', status: 1, overtime_hours: 0, remark: '' })
const statusMap = { 1: '正常', 2: '迟到', 3: '早退', 4: '旷工', 5: '请假' }
const statusType = { 1: 'success', 2: 'warning', 3: 'warning', 4: 'danger', 5: 'info' }
async function fetchList() {
loading.value = true
try {
const params = { ...pagination, user_id: searchForm.user_id, status: searchForm.status }
if (searchForm.date_range?.length === 2) {
params.start_date = searchForm.date_range[0]
params.end_date = searchForm.date_range[1]
}
const res = await attendanceApi.getList(params)
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { user_id: '', status: '', date_range: [] }); handleSearch() }
async function handleClockIn() {
try {
await attendanceApi.clockIn()
ElMessage.success('上班打卡成功'); fetchList()
} catch (e) { ElMessage.error(e.response?.data?.message || '打卡失败') }
}
async function handleClockOut() {
try {
await attendanceApi.clockOut()
ElMessage.success('下班打卡成功'); fetchList()
} catch (e) { ElMessage.error(e.response?.data?.message || '打卡失败') }
}
function handleAdd() {
Object.assign(form, { user_id: '', attendance_date: '', clock_in: '', clock_out: '', status: 1, overtime_hours: 0, remark: '' })
dialogVisible.value = true
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
await attendanceApi.create(form)
ElMessage.success('创建成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => { fetchList() })
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="searchForm.user_id" placeholder="员工ID" clearable style="width:140px" />
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-date-picker v-model="searchForm.date_range" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" />
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions">
<el-button type="success" @click="handleClockIn">上班打卡</el-button>
<el-button type="warning" @click="handleClockOut">下班打卡</el-button>
<el-button type="primary" @click="handleAdd">手动录入</el-button>
</div>
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column label="员工" min-width="100">
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
</el-table-column>
<el-table-column prop="attendance_date" label="考勤日期" width="120" />
<el-table-column prop="clock_in" label="上班打卡" width="100" />
<el-table-column prop="clock_out" label="下班打卡" width="100" />
<el-table-column prop="overtime_hours" label="加班(h)" width="90" align="center" />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
<el-table-column prop="created_at" label="创建时间" min-width="160" />
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<el-dialog v-model="dialogVisible" title="手动录入考勤" width="500px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="{ user_id: [{ required: true, message: '请输入员工ID' }], attendance_date: [{ required: true, message: '请选择考勤日期' }] }" label-width="90px">
<el-form-item label="员工ID" prop="user_id">
<el-input v-model="form.user_id" placeholder="请输入员工ID" />
</el-form-item>
<el-form-item label="考勤日期" prop="attendance_date">
<el-date-picker v-model="form.attendance_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="上班时间">
<el-time-picker v-model="form.clock_in" value-format="HH:mm:ss" placeholder="上班打卡时间" style="width:100%" />
</el-form-item>
<el-form-item label="下班时间">
<el-time-picker v-model="form.clock_out" value-format="HH:mm:ss" placeholder="下班打卡时间" style="width:100%" />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="form.status" style="width:100%">
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
<el-form-item label="加班时长">
<el-input-number v-model="form.overtime_hours" :min="0" :max="24" :precision="1" :step="0.5" style="width:100%" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="form.remark" type="textarea" :rows="2" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
+160
View File
@@ -0,0 +1,160 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { leaveRequestApi } from '@/api/hr.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
const searchForm = reactive({ type: '', status: '' })
const pagination = reactive({ page: 1, per_page: 20 })
const formRef = ref(null)
const form = reactive({ id: null, user_id: '', type: 1, start_date: '', end_date: '', days: 1, reason: '' })
const typeMap = { 1: '年假', 2: '事假', 3: '病假', 4: '调休', 5: '产假', 6: '婚假' }
const statusMap = { 0: '待审批', 1: '已批准', 2: '已拒绝' }
const statusType = { 0: 'warning', 1: 'success', 2: 'danger' }
const formRules = {
user_id: [{ required: true, message: '请输入员工ID' }],
type: [{ required: true, message: '请选择请假类型' }],
start_date: [{ required: true, message: '请选择开始日期' }],
end_date: [{ required: true, message: '请选择结束日期' }],
days: [{ required: true, message: '请输入请假天数' }]
}
async function fetchList() {
loading.value = true
try {
const res = await leaveRequestApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { type: '', status: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false
Object.assign(form, { id: null, user_id: '', type: 1, start_date: '', end_date: '', days: 1, reason: '' })
dialogVisible.value = true
}
function handleEdit(row) {
isEdit.value = true
Object.assign(form, { id: row.id, user_id: row.user_id, type: row.type, start_date: row.start_date, end_date: row.end_date, days: row.days, reason: row.reason })
dialogVisible.value = true
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) { await leaveRequestApi.update(form.id, form) } else { await leaveRequestApi.create(form) }
ElMessage.success(isEdit.value ? '更新成功' : '创建成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleAudit(row, status) {
const label = status === 1 ? '批准' : '拒绝'
await ElMessageBox.confirm(`确定${label}该请假申请吗?`, '审批确认', { type: 'warning' })
await leaveRequestApi.audit(row.id, { status })
ElMessage.success(`${label}成功`); fetchList()
}
async function handleDelete(row) {
await ElMessageBox.confirm('确定删除该请假申请吗?', '删除确认', { type: 'warning' })
await leaveRequestApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => { fetchList() })
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-select v-model="searchForm.type" placeholder="请假类型" clearable style="width:130px">
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-select v-model="searchForm.status" placeholder="审批状态" clearable style="width:130px">
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions">
<el-button type="primary" @click="handleAdd">新增请假</el-button>
</div>
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column label="员工" min-width="100">
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
</el-table-column>
<el-table-column label="类型" width="80" align="center">
<template #default="{ row }">
<el-tag size="small">{{ typeMap[row.type] }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="start_date" label="开始日期" width="120" />
<el-table-column prop="end_date" label="结束日期" width="120" />
<el-table-column prop="days" label="天数" width="70" align="center" />
<el-table-column prop="reason" label="事由" min-width="180" show-overflow-tooltip />
<el-table-column label="状态" width="90" align="center">
<template #default="{ row }">
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<template v-if="row.status === 0">
<el-button size="small" type="success" link @click="handleAudit(row, 1)">批准</el-button>
<el-button size="small" type="danger" link @click="handleAudit(row, 2)">拒绝</el-button>
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑请假' : '新增请假'" width="500px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
<el-form-item label="员工ID" prop="user_id">
<el-input v-model="form.user_id" placeholder="请输入员工ID" />
</el-form-item>
<el-form-item label="请假类型" prop="type">
<el-select v-model="form.type" style="width:100%">
<el-option v-for="(v, k) in typeMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="start_date">
<el-date-picker v-model="form.start_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="结束日期" prop="end_date">
<el-date-picker v-model="form.end_date" type="date" value-format="YYYY-MM-DD" style="width:100%" />
</el-form-item>
<el-form-item label="天数" prop="days">
<el-input-number v-model="form.days" :min="0.5" :max="365" :precision="1" :step="0.5" style="width:100%" />
</el-form-item>
<el-form-item label="事由">
<el-input v-model="form.reason" type="textarea" :rows="3" placeholder="请输入请假事由" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
+166
View File
@@ -0,0 +1,166 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { employeeProfileApi } from '@/api/hr.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('新增员工档案')
const submitLoading = ref(false)
const searchForm = reactive({ employee_no: '', status: '' })
const pagination = reactive({ page: 1, per_page: 20 })
const formRef = ref(null)
const form = reactive({
id: null, user_id: '', employee_no: '', hire_date: '', birth_date: '',
gender: 0, id_card: '', education: '', emergency_contact: '', emergency_phone: '',
base_salary: 0, position_salary: 0, status: 1
})
const isEdit = ref(false)
const formRules = {
user_id: [{ required: true, message: '请输入用户ID', trigger: 'blur' }]
}
const statusMap = { 1: '在职', 2: '试用', 3: '离职' }
const statusType = { 1: 'success', 2: 'warning', 3: 'danger' }
const genderMap = { 0: '未知', 1: '男', 2: '女' }
async function fetchList() {
loading.value = true
try {
const res = await employeeProfileApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { employee_no: '', status: '' }); handleSearch() }
function handleAdd() {
isEdit.value = false; dialogTitle.value = '新增员工档案'
Object.assign(form, {
id: null, user_id: '', employee_no: '', hire_date: '', birth_date: '',
gender: 0, id_card: '', education: '', emergency_contact: '', emergency_phone: '',
base_salary: 0, position_salary: 0, status: 1
})
dialogVisible.value = true
}
async function handleEdit(row) {
isEdit.value = true; dialogTitle.value = '编辑员工档案'
const res = await employeeProfileApi.getDetail(row.id)
const d = res.data
Object.assign(form, {
id: d.id, user_id: d.user_id || '', employee_no: d.employee_no || '',
hire_date: d.hire_date || '', birth_date: d.birth_date || '',
gender: d.gender ?? 0, id_card: d.id_card || '', education: d.education || '',
emergency_contact: d.emergency_contact || '', emergency_phone: d.emergency_phone || '',
base_salary: d.base_salary || 0, position_salary: d.position_salary || 0, status: d.status || 1
})
dialogVisible.value = true
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) { await employeeProfileApi.update(form.id, form); ElMessage.success('更新成功') }
else { await employeeProfileApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
await ElMessageBox.confirm(`确定删除员工「${row.user?.name || row.employee_no}」的档案吗?`, '删除确认', { type: 'warning' })
await employeeProfileApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => fetchList())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="searchForm.employee_no" placeholder="工号" style="width:160px" clearable @keydown.enter="handleSearch" />
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
<el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增员工档案</el-button></div>
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column prop="employee_no" label="工号" width="120" />
<el-table-column label="姓名" min-width="100">
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
</el-table-column>
<el-table-column label="性别" width="70" align="center">
<template #default="{ row }">{{ genderMap[row.gender] || '未知' }}</template>
</el-table-column>
<el-table-column prop="hire_date" label="入职日期" min-width="110" />
<el-table-column prop="education" label="学历" width="90" />
<el-table-column prop="base_salary" label="基本工资" width="100" align="right" />
<el-table-column prop="position_salary" label="岗位工资" width="100" align="right" />
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }">
<el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="140" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="700px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="90px">
<el-row :gutter="16">
<el-col :span="12"><el-form-item label="用户ID" prop="user_id"><el-input v-model="form.user_id" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="工号"><el-input v-model="form.employee_no" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="入职日期"><el-date-picker v-model="form.hire_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="出生日期"><el-date-picker v-model="form.birth_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="性别">
<el-select v-model="form.gender" style="width:100%">
<el-option v-for="(v,k) in genderMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12"><el-form-item label="身份证号"><el-input v-model="form.id_card" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="学历"><el-input v-model="form.education" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="紧急联系人"><el-input v-model="form.emergency_contact" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="紧急电话"><el-input v-model="form.emergency_phone" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="基本工资"><el-input-number v-model="form.base_salary" :min="0" :precision="2" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="岗位工资"><el-input-number v-model="form.position_salary" :min="0" :precision="2" style="width:100%" /></el-form-item></el-col>
<el-col :span="12">
<el-form-item label="状态">
<el-select v-model="form.status" style="width:100%">
<el-option v-for="(v,k) in statusMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
+274
View File
@@ -0,0 +1,274 @@
<script setup>
import { ref, reactive, computed, watch, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { salaryRecordApi, employeeProfileApi } from '@/api/hr.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const employees = ref([])
const searchForm = reactive({ year_month: '', status: '' })
const pagination = reactive({ page: 1, per_page: 20 })
const formRef = ref(null)
const form = reactive({
id: null, user_id: '', year_month: '',
base_salary: 0, position_salary: 0, performance: 0,
overtime_pay: 0, subsidy: 0,
social_insurance: 0, housing_fund: 0, tax: 0,
absence_deduction: 0, other_deduction: 0,
actual_amount: 0
})
const isEdit = ref(false)
const autoCompute = ref(true)
const formRules = {
user_id: [{ required: true, message: '请选择员工', trigger: 'change' }],
year_month: [{ required: true, message: '请输入工资月份', trigger: 'blur' }]
}
const statusMap = { 0: '待确认', 1: '已确认', 2: '已发放' }
const statusType = { 0: 'warning', 1: 'primary', 2: 'success' }
const computedActual = computed(() => {
const income = Number(form.base_salary || 0)
+ Number(form.position_salary || 0)
+ Number(form.performance || 0)
+ Number(form.overtime_pay || 0)
+ Number(form.subsidy || 0)
const deduction = Number(form.social_insurance || 0)
+ Number(form.housing_fund || 0)
+ Number(form.tax || 0)
+ Number(form.absence_deduction || 0)
+ Number(form.other_deduction || 0)
return Math.round((income - deduction) * 100) / 100
})
watch(computedActual, (val) => {
if (autoCompute.value) form.actual_amount = val
})
function getDeductionTotal(row) {
return Number(row.social_insurance || 0)
+ Number(row.housing_fund || 0)
+ Number(row.tax || 0)
+ Number(row.absence_deduction || 0)
+ Number(row.other_deduction || 0)
}
async function fetchList() {
loading.value = true
try {
const res = await salaryRecordApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
async function fetchEmployees() {
try {
const res = await employeeProfileApi.getList({ per_page: 500 })
employees.value = res.data?.list || res.data?.data || []
} catch {}
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { year_month: '', status: '' }); handleSearch() }
const defaultForm = {
id: null, user_id: '', year_month: '',
base_salary: 0, position_salary: 0, performance: 0,
overtime_pay: 0, subsidy: 0,
social_insurance: 0, housing_fund: 0, tax: 0,
absence_deduction: 0, other_deduction: 0,
actual_amount: 0
}
function handleAdd() {
isEdit.value = false; autoCompute.value = true
Object.assign(form, { ...defaultForm })
dialogVisible.value = true
}
async function handleEdit(row) {
if (row.status !== 0) { ElMessage.warning('只能编辑待确认记录'); return }
isEdit.value = true; autoCompute.value = false
const res = await salaryRecordApi.getDetail(row.id)
const d = res.data
Object.assign(form, {
id: d.id, user_id: d.user_id, year_month: d.year_month,
base_salary: d.base_salary || 0, position_salary: d.position_salary || 0,
performance: d.performance || 0, overtime_pay: d.overtime_pay || 0,
subsidy: d.subsidy || 0, social_insurance: d.social_insurance || 0,
housing_fund: d.housing_fund || 0, tax: d.tax || 0,
absence_deduction: d.absence_deduction || 0, other_deduction: d.other_deduction || 0,
actual_amount: d.actual_amount || 0
})
autoCompute.value = true
dialogVisible.value = true
}
async function handleSubmit() {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (!valid) return
submitLoading.value = true
try {
if (isEdit.value) { await salaryRecordApi.update(form.id, form); ElMessage.success('更新成功') }
else { await salaryRecordApi.create(form); ElMessage.success('创建成功') }
dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
})
}
async function handleDelete(row) {
if (row.status !== 0) { ElMessage.warning('只能删除待确认记录'); return }
await ElMessageBox.confirm('确定删除该工资记录吗?', '删除确认', { type: 'warning' })
await salaryRecordApi.delete(row.id); ElMessage.success('删除成功'); fetchList()
}
async function handleConfirm(row) {
await ElMessageBox.confirm('确认该工资记录?确认后不可编辑。', '确认操作', { type: 'warning' })
await salaryRecordApi.confirm(row.id); ElMessage.success('确认成功'); fetchList()
}
async function handlePay(row) {
await ElMessageBox.confirm('确定标记为已发放吗?', '发放确认', { type: 'warning' })
await salaryRecordApi.pay(row.id); ElMessage.success('发放成功'); fetchList()
}
function handlePageChange(val) { pagination.page = val; fetchList() }
function handleSizeChange(val) { pagination.per_page = val; pagination.page = 1; fetchList() }
onMounted(() => { fetchList(); fetchEmployees() })
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="searchForm.year_month" placeholder="工资月份 (如 2024-03)" style="width:200px" clearable @keydown.enter="handleSearch" />
<el-select v-model="searchForm.status" placeholder="状态" clearable style="width:120px">
<el-option v-for="(v, k) in statusMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增工资记录</el-button></div>
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column label="员工" min-width="100">
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
</el-table-column>
<el-table-column prop="year_month" label="工资月份" width="110" align="center" />
<el-table-column prop="base_salary" label="基本工资" width="110" align="right" />
<el-table-column prop="position_salary" label="岗位工资" width="110" align="right" />
<el-table-column prop="performance" label="绩效" width="100" align="right" />
<el-table-column prop="overtime_pay" label="加班费" width="100" align="right" />
<el-table-column prop="subsidy" label="补贴" width="90" align="right" />
<el-table-column label="扣除合计" width="110" align="right">
<template #default="{ row }">{{ getDeductionTotal(row).toFixed(2) }}</template>
</el-table-column>
<el-table-column label="实发金额" width="120" align="right">
<template #default="{ row }"><span style="font-weight:700">{{ row.actual_amount }}</span></template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }"><el-tag :type="statusType[row.status]" size="small">{{ statusMap[row.status] }}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center">
<template #default="{ row }">
<el-button v-if="row.status===0" size="small" type="success" link @click="handleConfirm(row)">确认</el-button>
<el-button v-if="row.status===1" size="small" type="success" link @click="handlePay(row)">发放</el-button>
<el-button v-if="row.status===0" size="small" type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button v-if="row.status===0" size="small" type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.per_page" :total="total" :page-sizes="[10,20,50]" layout="total,sizes,prev,pager,next,jumper" background @current-change="handlePageChange" @size-change="handleSizeChange" />
</div>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑工资记录' : '新增工资记录'" width="750px" destroy-on-close>
<el-form ref="formRef" :model="form" :rules="formRules" label-width="100px">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="员工" prop="user_id">
<el-select v-model="form.user_id" placeholder="选择员工" filterable style="width:100%">
<el-option v-for="e in employees" :key="e.id" :label="e.user?.name || e.name || `员工${e.id}`" :value="e.user_id || e.id" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="工资月份" prop="year_month">
<el-input v-model="form.year_month" placeholder="如 2024-03" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="基本工资">
<el-input-number v-model="form.base_salary" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="岗位工资">
<el-input-number v-model="form.position_salary" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="绩效">
<el-input-number v-model="form.performance" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="加班费">
<el-input-number v-model="form.overtime_pay" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="补贴">
<el-input-number v-model="form.subsidy" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="24"><el-divider content-position="left">扣除项</el-divider></el-col>
<el-col :span="8">
<el-form-item label="社保">
<el-input-number v-model="form.social_insurance" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="公积金">
<el-input-number v-model="form.housing_fund" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="个税">
<el-input-number v-model="form.tax" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="缺勤扣款">
<el-input-number v-model="form.absence_deduction" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="其他扣款">
<el-input-number v-model="form.other_deduction" :min="0" :precision="2" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="24"><el-divider /></el-col>
<el-col :span="12">
<el-form-item label="实发金额">
<el-input-number v-model="form.actual_amount" :precision="2" style="width:100%" />
<div style="font-size:12px;color:var(--color-text-secondary)">自动计算收入合计 - 扣除合计 = {{ computedActual.toFixed(2) }}</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
+97
View File
@@ -0,0 +1,97 @@
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { scheduleApi } from '@/api/hr.js'
const tableData = ref([])
const total = ref(0)
const loading = ref(false)
const dialogVisible = ref(false)
const submitLoading = ref(false)
const searchForm = reactive({ user_id: '', start_date: '', end_date: '' })
const pagination = reactive({ page: 1, per_page: 31 })
const form = reactive({ user_id: '', schedule_date: '', shift_type: 1, start_time: '', end_time: '' })
const shiftMap = { 1: '早班', 2: '中班', 3: '晚班', 4: '夜班', 5: '休息' }
const shiftType = { 1: 'success', 2: 'primary', 3: 'warning', 4: 'danger', 5: 'info' }
async function fetchList() {
loading.value = true
try {
const res = await scheduleApi.getList({ ...searchForm, ...pagination })
tableData.value = res.data?.list || res.data?.data || []
total.value = res.data?.total || 0
} finally { loading.value = false }
}
function handleSearch() { pagination.page = 1; fetchList() }
function handleReset() { Object.assign(searchForm, { user_id: '', start_date: '', end_date: '' }); handleSearch() }
function handleAdd() {
Object.assign(form, { user_id: '', schedule_date: '', shift_type: 1, start_time: '', end_time: '' })
dialogVisible.value = true
}
async function handleSubmit() {
if (!form.user_id || !form.schedule_date || !form.shift_type) { ElMessage.warning('请填写必要信息'); return }
submitLoading.value = true
try {
await scheduleApi.create(form)
ElMessage.success('排班成功'); dialogVisible.value = false; fetchList()
} finally { submitLoading.value = false }
}
function handlePageChange(val) { pagination.page = val; fetchList() }
onMounted(() => fetchList())
</script>
<template>
<div class="page-container">
<div class="search-bar">
<el-input v-model="searchForm.user_id" placeholder="员工ID" style="width:160px" clearable @keydown.enter="handleSearch" />
<el-date-picker v-model="searchForm.start_date" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" />
<el-date-picker v-model="searchForm.end_date" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" />
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="handleReset">重置</el-button>
</div>
<div class="table-container">
<div class="table-actions"><el-button type="primary" @click="handleAdd">新增排班</el-button></div>
<el-table v-loading="loading" :data="tableData" border stripe style="width:100%">
<el-table-column label="员工" min-width="100">
<template #default="{ row }">{{ row.user?.name || '-' }}</template>
</el-table-column>
<el-table-column prop="schedule_date" label="排班日期" width="120" />
<el-table-column label="班次" width="90" align="center">
<template #default="{ row }">
<el-tag :type="shiftType[row.shift_type]" size="small">{{ shiftMap[row.shift_type] }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="start_time" label="开始时间" width="100" />
<el-table-column prop="end_time" label="结束时间" width="100" />
</el-table>
<el-pagination v-model:current-page="pagination.page" :total="total" layout="total,prev,pager,next" background @current-change="handlePageChange" />
</div>
<el-dialog v-model="dialogVisible" title="新增排班" width="500px" destroy-on-close>
<el-form label-width="80px">
<el-form-item label="员工ID"><el-input v-model="form.user_id" /></el-form-item>
<el-form-item label="排班日期"><el-date-picker v-model="form.schedule_date" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item>
<el-form-item label="班次">
<el-select v-model="form.shift_type" style="width:100%">
<el-option v-for="(v,k) in shiftMap" :key="k" :label="v" :value="Number(k)" />
</el-select>
</el-form-item>
<el-form-item label="开始时间"><el-time-picker v-model="form.start_time" format="HH:mm" value-format="HH:mm" style="width:100%" /></el-form-item>
<el-form-item label="结束时间"><el-time-picker v-model="form.end_time" format="HH:mm" value-format="HH:mm" style="width:100%" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>