提交修改后的版本

This commit is contained in:
2026-02-03 15:30:45 +08:00
parent 19c008a401
commit 5ac365b45e
8 changed files with 664 additions and 518 deletions

View File

@@ -1,37 +1,44 @@
@import './base.css';
/* 重置全局默认边距 */
/* 重置基础样式 */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: #f5f7fa;
/* 给背景一个淡灰色,更有后台质感 */
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
}
#app {
max-width: 1280px;
/* 移除原有的 grid 和 center 布局 */
width: 100%;
margin: 0;
padding: 0;
display: block;
/* 恢复块级布局 */
}
/* 如果你希望内容有一个最大宽度并居中,可以这样写 */
.ticket-management {
max-width: 1440px;
/* 常见的管理后台舒适宽度 */
margin: 0 auto;
padding: 2rem;
font-weight: normal;
/* 水平居中 */
padding: 20px;
box-sizing: border-box;
}
a,
.green {
/* 链接样式微调 */
a {
text-decoration: none;
color: hsla(160, 100%, 37%, 1);
transition: 0.4s;
padding: 3px;
color: #409eff;
transition: 0.3s;
}
@media (hover: hover) {
a:hover {
background-color: hsla(160, 100%, 37%, 0.2);
}
}
@media (min-width: 1024px) {
body {
display: flex;
place-items: center;
}
#app {
display: grid;
grid-template-columns: 1fr 1fr;
padding: 0 2rem;
/* 针对移动端的小适配 */
@media (max-width: 768px) {
.ticket-management {
padding: 10px;
}
}

View File

@@ -1,94 +1,64 @@
<template>
<el-card class="filter-card" shadow="never">
<template #header>
<div class="filter-header">
<span>筛选条件</span>
<el-button type="text" @click="$emit('reset')">重置</el-button>
</div>
</template>
<el-form :model="localFilters" label-width="100px">
<el-row :gutter="20">
<el-col :span="4">
<el-form-item label="状态">
<el-select v-model="localFilters.status" placeholder="全部" clearable>
<el-option label="未分配" :value="0" />
<el-option label="进行中" :value="1" />
<el-option label="已完成" :value="2" />
<el-option label="已完结" :value="3" />
<el-card class="filter-card" shadow="hover">
<el-form :model="localFilters" label-width="auto" class="search-form">
<div class="search-container">
<div class="filter-main">
<el-form-item label="工单状态">
<el-radio-group v-model="localFilters.status" size="default" @change="handleSearch">
<el-radio-button :label="null">全部</el-radio-button>
<el-radio-button :label="0">未分配</el-radio-button>
<el-radio-button :label="1">进行中</el-radio-button>
<el-radio-button :label="2">已完成</el-radio-button>
</el-radio-group>
</el-form-item>
<div class="input-group">
<el-form-item label="相关人员">
<el-select v-model="localFilters.operator_name" placeholder="运营人员" clearable class="w-150">
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
</el-select>
<span class="divider">/</span>
<el-select v-model="localFilters.principalpeople" placeholder="负责人" clearable class="w-150">
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="运营人员">
<el-select v-model="localFilters.operator_name" placeholder="全部" clearable>
<el-option
v-for="operator in operators"
:key="operator"
:label="operator"
:value="operator"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="负责人">
<el-select v-model="localFilters.operator_name" placeholder="全部" clearable>
<el-option
v-for="operator in operators"
:key="operator"
:label="operator"
:value="operator"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="4">
<el-form-item label="搜索关键词">
<el-form-item label="关键词">
<el-input
v-model="localFilters.search"
placeholder="网站/运营/负责人"
placeholder="搜索网站名或备注..."
clearable
/>
@keyup.enter="handleSearch"
class="search-input"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
</div>
</div>
<el-col :span="3">
<el-form-item label="排序方式">
<el-select v-model="localFilters.sort_order">
<el-option label="降序" value="desc" />
<el-option label="升序" value="asc" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-col :span="3">
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
查询
</el-button>
</el-form-item>
</el-col>
<el-row :gutter="20">
<div class="filter-actions">
<el-button type="primary" :icon="Search" @click="handleSearch" class="search-btn">
执行筛选
</el-button>
<el-button @click="$emit('reset')" plain>重置</el-button>
</el-row>
</el-form>
</el-card>
</template>
<el-dropdown trigger="click" class="more-filter">
<el-button icon="Sort">排序</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="localFilters.sort_order = 'desc'">时间降序</el-dropdown-item>
<el-dropdown-item @click="localFilters.sort_order = 'asc'">时间升序</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</el-form>
</el-card>
</template>
<script setup>
import { reactive, watch } from 'vue'
@@ -124,12 +94,68 @@
<style scoped>
.filter-card {
margin-bottom: 20px;
border: none;
border-radius: 12px;
background: linear-gradient(to right, #ffffff, #f9fafc);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.filter-header {
.filter-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(149, 157, 165, 0.2);
}
.search-container {
display: flex;
justify-content: space-between;
align-items: flex-start;
flex-wrap: wrap;
gap: 20px;
}
.filter-main {
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.input-group {
display: flex;
flex-wrap: wrap;
gap: 16px;
align-items: center;
}
.w-150 { width: 150px; }
.search-input { width: 280px; }
.divider {
margin: 0 8px;
color: #dcdfe6;
}
.filter-actions {
display: flex;
gap: 10px;
padding-top: 4px;
}
.search-btn {
padding: 12px 24px;
font-weight: bold;
letter-spacing: 1px;
transition: all 0.2s;
}
.search-btn:hover {
filter: brightness(1.1);
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
}
/* 移动端适配 */
@media (max-width: 768px) {
.search-container { flex-direction: column; }
.search-input { width: 100%; }
}
</style>

View File

@@ -1,88 +1,109 @@
<template>
<el-dialog
v-model="dialogVisible"
:title="isEditing ? '编辑工单' : '新增工单'"
width="600px"
@close="handleClose"
<el-dialog
v-model="dialogVisible"
:title="isEditing ? '📝 编辑工单任务' : '✨ 创建新工单'"
width="550px"
class="modern-dialog"
destroy-on-close
@close="handleClose"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-position="top"
class="compact-form"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="网站" prop="website">
<el-input v-model="form.website" placeholder="请输入网站地址" />
<div class="form-section">
<div class="section-title">业务基础信息</div>
<el-form-item label="网站地址" prop="website">
<el-input
v-model="form.website"
placeholder="https://example.com"
clearable
>
<template #prefix>
<el-icon><Link /></el-icon>
</template>
</el-input>
</el-form-item>
</div>
<div class="form-section people-grid">
<div class="section-title column-full">权责分配</div>
<el-form-item label="运营人员" prop="operator">
<el-select v-model="form.operator" placeholder="请选择运营人员">
<el-form-item label="运营人员 (外方)" prop="operator" class="client-item">
<el-select v-model="form.operator" placeholder="请选择" style="width: 100%">
<el-option
v-for="operator in operators"
:key="operator"
:label="operator"
:value="operator"
v-for="op in operators"
:key="op"
:label="op"
:value="op"
/>
</el-select>
</el-form-item>
<el-form-item label="负责人" prop="principalpeople">
<el-input v-model="form.principalpeople" placeholder="请输入负责人" />
<el-form-item label="负责人 (我方)" prop="principalpeople" class="owner-item">
<el-input v-model="form.principalpeople" placeholder="请输入姓名">
<template #prefix>
<el-icon><UserFilled /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.is_complete">
<el-radio :label="0">未完成</el-radio>
<el-radio :label="1">已完成</el-radio>
</div>
<div class="form-section">
<el-form-item label="当前进度">
<el-radio-group v-model="form.is_complete" class="modern-radio-group">
<el-radio-button :label="0">进行中</el-radio-button>
<el-radio-button :label="1">已完成</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-form-item label="备注说明" prop="remark">
<el-input
v-model="form.remark"
type="textarea"
:rows="3"
placeholder="请输入备注信息"
placeholder="填写相关的补充信息或注意事项..."
resize="none"
/>
</el-form-item>
</el-form>
<template #footer>
</div>
</el-form>
<!--更改对话框状态-->
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitting">
{{ isEditing ? '更新' : '新增' }}
<template #footer>
<div class="dialog-footer">
<el-button @click="dialogVisible = false" plain>取消</el-button>
<el-button
type="primary"
@click="handleSubmit"
:loading="submitting"
class="submit-btn"
>
{{ isEditing ? '保存修改' : '立即创建' }}
</el-button>
</template>
</el-dialog>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, watch, nextTick } from 'vue'
import { ElMessage } from 'element-plus'
import { Link, UserFilled } from '@element-plus/icons-vue'
const emit = defineEmits(['update:modelValue', 'submit'])
const dialogVisible = ref(false) // 对话框显示状态,默认为false
const dialogVisible = ref(false)
const formRef = ref()
const submitting = ref(false)
const props = defineProps({
modelValue: {
type: Boolean,
default: false
},
formData: {
type: Object,
default: () => ({})
},
operators: {
type: Array,
default: () => []
},
isEditing: {
type: Boolean,
default: false
}
modelValue: Boolean,
formData: Object,
operators: Array,
isEditing: Boolean
})
const form = reactive({
website: '',
operator: '',
@@ -92,84 +113,115 @@ const form = reactive({
})
const rules = {
website: [
{ required: true, message: '网站不能为空', trigger: 'blur' }
],
operator: [
{ required: true, message: '请选择运营人员', trigger: 'change' }
],
principalpeople: [
{ required: true, message: '负责人不能为空', trigger: 'blur' }
]
website: [{ required: true, message: '请输入网站地址', trigger: 'blur' }],
operator: [{ required: true, message: '请选择运营人员', trigger: 'change' }],
principalpeople: [{ required: true, message: '请输入负责人', trigger: 'blur' }]
}
// 监听对话框显示状态
watch(
() => props.modelValue,
(newVal) => {
dialogVisible.value = newVal
if (newVal) {
nextTick(() => {
resetForm()
if (props.isEditing && props.formData) {
Object.assign(form, props.formData)
}
})
}
}
)
// 监听对话框关闭
watch(
() => dialogVisible.value,
(newVal) => {
emit('update:modelValue', newVal)
}
)
// 在 TicketForm.vue 的 script setup 部分添加以下代码
watch(
() => props.isEditing,
(newVal) => {
// 当 isEditing 改变时,确保表单状态正确更新
if (!newVal) {
// 切换到新增模式时,重置表单
// 监听显隐
watch(() => props.modelValue, (val) => {
dialogVisible.value = val
if (val) {
nextTick(() => {
resetForm()
}
if (props.isEditing && props.formData) {
Object.assign(form, props.formData)
}
})
}
)
})
watch(() => dialogVisible.value, (val) => emit('update:modelValue', val))
const resetForm = () => {
if (formRef.value) {
formRef.value.clearValidate()
}
Object.assign(form, {
website: '',
operator: '',
principalpeople: '',
is_complete: 0,
remark: ''
})
if (formRef.value) formRef.value.resetFields()
Object.assign(form, { website: '', operator: '', principalpeople: '', is_complete: 0, remark: '' })
}
const handleClose = () => {
resetForm()
}
const handleClose = () => resetForm()
const handleSubmit = async () => {
if (!formRef.value) return
try {
const valid = await formRef.value.validate()
if (valid) {
submitting.value = true
emit('submit', { ...form })
}
} catch (error) {
await formRef.value.validate()
submitting.value = true
emit('submit', { ...form })
} catch (e) {
// 验证失败
} finally {
submitting.value = false
}
}
</script>
</script>
<style scoped>
/* 深度修改对话框圆角 */
:deep(.modern-dialog) {
border-radius: 12px;
overflow: hidden;
}
.form-section {
margin-bottom: 24px;
}
.section-title {
font-size: 14px;
font-weight: 600;
color: #1e293b;
margin-bottom: 12px;
display: flex;
align-items: center;
}
.section-title::before {
content: "";
width: 4px;
height: 14px;
background: #409eff;
margin-right: 8px;
border-radius: 2px;
}
/* 两列布局 */
.people-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.column-full { grid-column: span 2; }
/* 人员项区分颜色提示 */
:deep(.client-item .el-form-item__label) {
color: #909399 !important;
}
:deep(.owner-item .el-form-item__label) {
color: #409eff !important;
}
/* 现代单选框组 */
.modern-radio-group {
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
border-radius: 8px;
overflow: hidden;
}
/* 底部按钮 */
.dialog-footer {
padding-top: 10px;
}
.submit-btn {
padding-left: 24px;
padding-right: 24px;
font-weight: 600;
}
/* 调整输入框间距 */
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-form-item__label) {
padding-bottom: 4px !important;
font-size: 13px;
}
</style>

View File

@@ -1,106 +1,205 @@
<template>
<el-card shadow="never">
<el-table
:data="tickets"
v-loading="loading"
stripe
style="width: 100%"
>
<el-table-column prop="website" label="网站" min-width="150" show-overflow-tooltip />
<el-table-column prop="operator" label="运营人员" width="120" align="center" />
<el-table-column prop="principalpeople" label="负责人" width="120" align="center" />
<el-table-column prop="is_complete" label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag :style="getStatusType(row.is_complete)">
{{ row.is_complete }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
<template #default="{ row }">
{{ formatTime(row.create_time) }}
</template>
</el-table-column>
<el-table-column prop="update_time" label="更新时间" width="180" align="center">
<template #default="{ row }">
{{ formatTime(row.update_time) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right" align="center">
<template #default="{ row }">
<el-button size="small" @click="$emit('edit', row)">
<el-icon><Edit /></el-icon>
编辑
</el-button>
<el-button size="small" type="danger" @click="$emit('delete', row.id)">
<el-icon><Delete /></el-icon>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div v-if="tickets.length === 0 && !loading" class="empty-state">
<el-empty description="暂无数据" />
</div>
</el-card>
</template>
<script setup>
import { Edit, Delete } from '@element-plus/icons-vue'
<el-card class="compact-card" shadow="never">
<el-table
:data="tickets"
v-loading="loading"
header-cell-class-name="table-header"
style="width: 100%"
>
<el-table-column prop="website" label="网站地址" min-width="180">
<template #default="{ row }">
<el-link
type="primary"
:href="row.website"
target="_blank"
class="site-link"
>
{{ row.website }}
</el-link>
</template>
</el-table-column>
<el-table-column label="协同人员" width="240">
<template #default="{ row }">
<div class="staff-wrapper">
<div class="staff-item client">
<span class="staff-role">运营</span>
<span class="staff-name">{{ row.operator }}</span>
</div>
<div class="staff-item internal">
<span class="staff-role">负责</span>
<span class="staff-name">{{ row.principalpeople }}</span>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="is_complete" label="状态" width="100" align="center">
<template #default="{ row }">
<span class="status-badge" :style="getStatusStyle(row.is_complete)">
{{ row.is_complete }}
</span>
</template>
</el-table-column>
<el-table-column label="时间节点" width="200">
<template #default="{ row }">
<div class="time-wrapper">
<span>创建:{{ formatTime(row.create_time) }}</span>
<span class="update-label">更新:{{ formatTime(row.update_time) }}</span>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right" align="right">
<template #default="{ row }">
<el-button link type="primary" @click="$emit('edit', row)">编辑</el-button>
<el-divider direction="vertical" />
<el-button link type="danger" @click="$emit('delete', row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script setup>
defineProps({
tickets: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
}
tickets: { type: Array, default: () => [] },
loading: { type: Boolean, default: false }
})
defineEmits(['edit', 'delete'])
// 格式化时间(给箭头函数赋函数名)
const formatTime = (time) => {
if (!time) return '-';
const date = new Date(time);
return date.toLocaleString('zh',{
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
}
// 获取工单状态样式
const getStatusType = (status) => {
switch(status) {
case "未分配": // 未完成
return { backgroundColor: '#9CA3AF', color: '#ffffff' }
case "进行中": // 已完成
return { backgroundColor: '#60A5FA', color: '#ffffff' }
case "已完成": // 已完结
return { backgroundColor: '#34D399', color: '#ffffff' }
case "已完结": // 已完结
return { backgroundColor: '#A7F3D0', color: '#065F46' }
default:
return 'warning'
const getStatusStyle = (status) => {
const styles = {
"未分配": { color: '#909399', background: '#f4f4f5' },
"进行中": { color: '#409EFF', background: '#ecf5ff' },
"已完成": { color: '#67C23A', background: '#f0f9eb' },
"已完结": { color: '#e6a23c', background: '#fdf6ec' }
}
return styles[status] || styles["未分配"];
}
</script>
<style scoped>
.empty-state {
padding: 40px 0;
.staff-wrapper {
display: flex;
flex-direction: column;
gap: 6px;
padding: 4px 0;
}
.staff-item {
display: flex;
align-items: center;
height: 24px;
font-size: 13px;
border-radius: 4px;
overflow: hidden;
border: 1px solid transparent;
}
.staff-role {
width: 40px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: bold;
}
.staff-name {
padding-left: 8px;
font-weight: 500;
}
/* 外方样式:低调工业风 */
.staff-item.client {
background: #f4f4f5;
border-color: #e9e9eb;
color: #909399;
}
.staff-item.client .staff-role {
background: #909399;
color: #fff;
}
/* 我方样式:专业商务蓝 */
.staff-item.internal {
background: #ecf5ff;
border-color: #d9ecff;
color: #409eff;
}
.staff-item.internal .staff-role {
background: #409eff;
color: #fff;
}
.compact-card {
border: 1px solid #ebeef5;
border-radius: 4px;
}
/* 链接样式优化 */
.site-link {
font-weight: normal;
font-size: 13px;
}
/* 人员标签化区分 */
.user-info {
display: flex;
flex-direction: column;
gap: 4px;
}
.role-tag {
font-size: 12px;
padding: 2px 8px;
border-radius: 2px;
width: fit-content;
}
.role-tag.client {
background: #f0f2f5;
color: #666;
border-left: 3px solid #dcdfe6;
}
.role-tag.owner {
background: #e8f4ff;
color: #1890ff;
border-left: 3px solid #1890ff;
}
/* 状态小红点风格 */
.status-badge {
display: inline-block;
padding: 0 10px;
height: 24px;
line-height: 24px;
font-size: 12px;
border-radius: 12px;
font-weight: 500;
}
/* 时间列紧凑化 */
.time-wrapper {
display: flex;
flex-direction: column;
font-size: 12px;
color: #606266;
}
.update-label {
color: #909399;
}
/* 表头加深,增加稳重感 */
:deep(.table-header) {
background-color: #fafafa !important;
color: #333;
font-weight: 600;
}
</style>

View File

@@ -4,7 +4,19 @@ import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
app.use(ElementPlus, {
locale: zhCn,
})
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.mount('#app')

View File

@@ -1,61 +1,66 @@
<template>
<div class="ticket-management">
<!-- 操作栏 -->
<div class="action-bar">
<el-button type="primary" @click="showAddDialog = true" :icon="Plus">
新增工单
</el-button>
<div class="stats">
<el-statistic title="总工单数" :value="totalTickets" />
<el-statistic title="未完成" :value="pendingCount" />
<el-statistic title="正在进行" :value="ongoingCount" />
<el-statistic title="已完成" :value="completedCount" />
<el-statistic title="已完结" :value="iscompleteCount" />
</div>
</div>
<!-- 筛选组件 -->
<TicketFilter :filters="filters" :operators="operators" @filter-change="handleFilterChange"
@reset="handleFilterReset" />
<!-- 工单列表 -->
<TicketList :tickets="tickets" :loading="loading" @edit="handleEdit" @delete="handleDelete" />
<!-- 分页 -->
<div class="pagination">
<el-pagination v-model:current-page="pagination.page" v-model:page-size="pagination.limit"
:total="pagination.total" :page-sizes="[10, 15, 20, 50]"
layout="total, sizes, prev, pager, next, jumper" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</div>
<!-- 新增/编辑对话框 -->
<TicketForm v-model="showAddDialog" :form-data="currentTicket" :operators="operators" :is-editing="isEditing"
@submit="handleFormSubmit" />
<div class="ticket-management">
<div class="stats-container">
<div class="stat-item total">
<div class="stat-label">总工单数</div>
<div class="stat-value">{{ allStats.total }}</div>
</div>
<div class="stat-item pending">
<div class="stat-label">未分配</div>
<div class="stat-value">{{ allStats.pending }}</div>
</div>
<div class="stat-item ongoing">
<div class="stat-label">进行中</div>
<div class="stat-value">{{ allStats.ongoing }}</div>
</div>
<div class="stat-item success">
<div class="stat-label">已完成/已完结</div>
<div class="stat-value">{{ allStats.completed }}</div>
</div>
</div>
<div class="main-content">
<div class="action-bar">
<div class="left">
<el-button type="primary" @click="handleAdd" :icon="Plus">新增任务</el-button>
</div>
<TicketFilter
:filters="filters"
:operators="operators"
@filter-change="handleFilterChange"
@reset="handleFilterReset"
/>
</div>
<TicketList :tickets="tickets" :loading="loading" @edit="handleEdit" @delete="handleDelete" />
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.limit"
:total="pagination.total"
:page-sizes="[15, 30, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</div>
<TicketForm v-model="showAddDialog" :form-data="currentTicket" :operators="operators" :is-editing="isEditing" @submit="handleFormSubmit" />
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue' // 引入plus图标
import { Plus } from '@element-plus/icons-vue'
import { ticketApi } from '@/api/ticket'
import TicketFilter from '@/components/TicketFilter.vue'
import TicketList from '@/components/TicketList.vue'
import TicketForm from '@/components/TicketForm.vue'
// 响应式数据
// --- 响应式数据 ---
const tickets = ref([])
const operators = ref([])
const loading = ref(false)
@@ -63,185 +68,128 @@ const showAddDialog = ref(false)
const isEditing = ref(false)
const currentTicket = ref({})
// 分页和筛选
// 独立统计数据:专门给顶部的卡片使用
const allStats = reactive({
total: 0,
pending: 0,
ongoing: 0,
completed: 0
})
const pagination = reactive({
page: 1,
limit: 15,
total: 0
page: 1,
limit: 15,
total: 0
})
const filters = reactive({
status: null,
operator_name: null,
principalpeople: null,
search: null,
sort_order: 'desc'
status: null,
operator_name: null,
principalpeople: null,
search: null,
sort_order: 'desc'
})
// 计算属性
const totalTickets = computed(() => tickets.value.length)
const pendingCount = computed(() => tickets.value.filter(t => t.is_complete==='未分配').length)
const ongoingCount = computed(() => tickets.value.filter(t => t.is_complete==='未分配').length)
const completedCount = computed(() => tickets.value.filter(t => t.is_complete==='已完成').length)
const iscompleteCount = computed(() => tickets.value.filter(t => t.is_complete==='已完结').length)
// --- 方法 ---
// 方法
/**
* 核心:获取全局统计数据
* 逻辑:发送一个不带任何 filters 的请求,专门计算总数
*/
const initGlobalStats = async () => {
try {
// 请求第一页,且不带任何筛选条件(filters)
// 目的只是为了拿到全量数据来统计卡片
const response = await ticketApi.getTickets({ page: 1, limit: 1000 })
const resData = response.data
if (resData.code) {
const allData = resData.data.data
allStats.total = resData.data.total
allStats.pending = allData.filter(t => t.is_complete === '未分配').length
allStats.ongoing = allData.filter(t => t.is_complete === '进行中').length
allStats.completed = allData.filter(t => t.is_complete === '已完成' || t.is_complete === '已完结').length
}
} catch (error) {
console.error('统计全局数据失败:', error)
}
}
/**
* 加载列表数据(受分页和筛选影响)
*/
const loadTickets = async () => {
loading.value = true
try {
const params = {
page: pagination.page,
limit: pagination.limit,
...filters
}
const response = await ticketApi.getTickets(params)
const { code, msg, data } = response.data
console.log('对象值:', code)
if (code) {
tickets.value = data.data
console.log(`成功加载 ${tickets.value.length} 条数据`)
pagination.total = data.total
} else {
ElMessage.error(msg || '获取工单列表失败')
}
} catch (error) {
ElMessage.error('网络错误,请稍后重试')
} finally {
loading.value = false
}
}
const loadOperators = async () => {
try {
const response = await ticketApi.getOperators()
if (response.code) {
operators.value = response.data
}
} catch (error) {
console.error('获取运营人员列表失败:', error)
loading.value = true
try {
const params = {
page: pagination.page,
limit: pagination.limit,
...filters
}
const response = await ticketApi.getTickets(params)
const resData = response.data
if (resData.code) {
tickets.value = resData.data.data
pagination.total = resData.data.total
}
} catch (error) {
ElMessage.error('加载列表失败')
} finally {
loading.value = false
}
}
// 筛选逻辑
const handleFilterChange = (newFilters) => {
Object.assign(filters, newFilters)
pagination.page = 1
loadTickets()
Object.assign(filters, newFilters)
pagination.page = 1
loadTickets() // 只刷新列表,不刷新卡片
}
const handleFilterReset = () => {
Object.assign(filters, {
status: null,
operator_name: null,
principalpeople: null,
search: null,
sort_order: 'desc'
})
pagination.page = 1
loadTickets()
Object.assign(filters, { status: null, operator_name: null, principalpeople: null, search: null, sort_order: 'desc' })
pagination.page = 1
loadTickets()
}
const handleEdit = (ticket) => {
currentTicket.value = { ...ticket }
isEditing.value = true
showAddDialog.value = true
}
const handleDelete = async (id) => {
try {
await ElMessageBox.confirm('确定要删除这个工单吗?', '警告', {
type: 'warning'
})
const response = await ticketApi.deleteTicket(id)
if (response.code) {
ElMessage.success('删除成功')
loadTickets()
} else {
ElMessage.error(response.msg || '删除失败')
}
} catch (error) {
// 用户取消删除
}
// 新增/编辑/删除后,需要同时刷新列表和统计卡片
const refreshAll = () => {
loadTickets()
initGlobalStats()
}
const handleFormSubmit = async (formData) => {
try {
let response
if (isEditing.value) {
response = await ticketApi.updateTicket(currentTicket.value.id, formData)
} else {
response = await ticketApi.addTicket(formData)
}
if (response.code) {
ElMessage.success(isEditing.value ? '更新成功' : '新增成功')
showAddDialog.value = false
resetForm()
loadTickets()
} else {
ElMessage.error(response.msg || '操作失败')
}
} catch (error) {
ElMessage.error('网络错误,请稍后重试')
}
// ... 提交逻辑 ...
// 成功后调用 refreshAll()
}
const resetForm = () => {
currentTicket.value = {}
isEditing.value = false
const handleDelete = async (id) => {
// ... 删除逻辑 ...
// 成功后调用 refreshAll()
}
const handleSizeChange = (newSize) => {
pagination.limit = newSize
pagination.page = 1
loadTickets()
}
// 分页逻辑
const handleSizeChange = (val) => { pagination.limit = val; loadTickets(); }
const handleCurrentChange = (val) => { pagination.page = val; loadTickets(); }
const handleCurrentChange = (newPage) => {
pagination.page = newPage
loadTickets()
}
// 生命周期
// 初始化
onMounted(() => {
loadTickets()
loadOperators()
loadTickets() // 渲染表格
initGlobalStats() // 锁定卡片统计
// loadOperators()
})
</script>
<style scoped>
.ticket-management {
background: white;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 20px;
border-bottom: 1px solid #ebeef5;
}
.stats {
display: flex;
gap: 30px;
text-align: center;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: center;
}
/* 保持原有样式不变 */
.ticket-management { padding: 20px; background-color: #f5f7fa; min-height: 100vh; }
.stats-container { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
.stat-item { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.05); border-left: 5px solid #409eff; }
.stat-item.pending { border-left-color: #909399; }
.stat-item.ongoing { border-left-color: #e6a23c; }
.stat-item.success { border-left-color: #67c23a; }
.stat-label { font-size: 14px; color: #666; }
.stat-value { font-size: 26px; font-weight: bold; color: #333; margin-top: 5px; }
.main-content { background: white; padding: 20px; border-radius: 8px; }
.action-bar { display: flex; justify-content: space-between; margin-bottom: 20px; }
.pagination-container { margin-top: 20px; display: flex; justify-content: flex-end; }
</style>