提交修改后的版本
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -8,6 +8,7 @@
|
|||||||
"name": "vue_tickets",
|
"name": "vue_tickets",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"element-plus": "^2.11.8",
|
"element-plus": "^2.11.8",
|
||||||
"vue": "^3.5.22"
|
"vue": "^3.5.22"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"element-plus": "^2.11.8",
|
"element-plus": "^2.11.8",
|
||||||
"vue": "^3.5.22"
|
"vue": "^3.5.22"
|
||||||
|
|||||||
@@ -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 {
|
#app {
|
||||||
max-width: 1280px;
|
/* 移除原有的 grid 和 center 布局 */
|
||||||
|
width: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: block;
|
||||||
|
/* 恢复块级布局 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 如果你希望内容有一个最大宽度并居中,可以这样写 */
|
||||||
|
.ticket-management {
|
||||||
|
max-width: 1440px;
|
||||||
|
/* 常见的管理后台舒适宽度 */
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 2rem;
|
/* 水平居中 */
|
||||||
font-weight: normal;
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
a,
|
/* 链接样式微调 */
|
||||||
.green {
|
a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: hsla(160, 100%, 37%, 1);
|
color: #409eff;
|
||||||
transition: 0.4s;
|
transition: 0.3s;
|
||||||
padding: 3px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
/* 针对移动端的小适配 */
|
||||||
a:hover {
|
@media (max-width: 768px) {
|
||||||
background-color: hsla(160, 100%, 37%, 0.2);
|
.ticket-management {
|
||||||
}
|
padding: 10px;
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1024px) {
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
place-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,94 +1,64 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-card class="filter-card" shadow="never">
|
<el-card class="filter-card" shadow="hover">
|
||||||
<template #header>
|
<el-form :model="localFilters" label-width="auto" class="search-form">
|
||||||
<div class="filter-header">
|
<div class="search-container">
|
||||||
<span>筛选条件</span>
|
<div class="filter-main">
|
||||||
<el-button type="text" @click="$emit('reset')">重置</el-button>
|
<el-form-item label="工单状态">
|
||||||
</div>
|
<el-radio-group v-model="localFilters.status" size="default" @change="handleSearch">
|
||||||
</template>
|
<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>
|
||||||
|
|
||||||
<el-form :model="localFilters" label-width="100px">
|
<div class="input-group">
|
||||||
<el-row :gutter="20">
|
<el-form-item label="相关人员">
|
||||||
<el-col :span="4">
|
<el-select v-model="localFilters.operator_name" placeholder="运营人员" clearable class="w-150">
|
||||||
<el-form-item label="状态">
|
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
|
||||||
<el-select v-model="localFilters.status" placeholder="全部" clearable>
|
</el-select>
|
||||||
<el-option label="未分配" :value="0" />
|
<span class="divider">/</span>
|
||||||
<el-option label="进行中" :value="1" />
|
<el-select v-model="localFilters.principalpeople" placeholder="负责人" clearable class="w-150">
|
||||||
<el-option label="已完成" :value="2" />
|
<el-option v-for="op in operators" :key="op" :label="op" :value="op" />
|
||||||
<el-option label="已完结" :value="3" />
|
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
|
||||||
|
|
||||||
<el-col :span="4">
|
<el-form-item label="关键词">
|
||||||
<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-input
|
<el-input
|
||||||
v-model="localFilters.search"
|
v-model="localFilters.search"
|
||||||
placeholder="网站/运营/负责人"
|
placeholder="搜索网站名或备注..."
|
||||||
clearable
|
clearable
|
||||||
/>
|
@keyup.enter="handleSearch"
|
||||||
|
class="search-input"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-col>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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-col :span="3">
|
<el-dropdown trigger="click" class="more-filter">
|
||||||
<el-form-item label="排序方式">
|
<el-button icon="Sort">排序</el-button>
|
||||||
<el-select v-model="localFilters.sort_order">
|
<template #dropdown>
|
||||||
<el-option label="降序" value="desc" />
|
<el-dropdown-menu>
|
||||||
<el-option label="升序" value="asc" />
|
<el-dropdown-item @click="localFilters.sort_order = 'desc'">时间降序</el-dropdown-item>
|
||||||
</el-select>
|
<el-dropdown-item @click="localFilters.sort_order = 'asc'">时间升序</el-dropdown-item>
|
||||||
</el-form-item>
|
</el-dropdown-menu>
|
||||||
</el-col>
|
</template>
|
||||||
</el-row>
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<el-col :span="3">
|
</el-form>
|
||||||
<el-form-item>
|
</el-card>
|
||||||
<el-button type="primary" @click="handleSearch">
|
</template>
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
查询
|
|
||||||
</el-button>
|
|
||||||
</el-form-item>
|
|
||||||
</el-col>
|
|
||||||
|
|
||||||
|
|
||||||
<el-row :gutter="20">
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</el-form>
|
|
||||||
</el-card>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, watch } from 'vue'
|
import { reactive, watch } from 'vue'
|
||||||
@@ -124,12 +94,68 @@
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.filter-card {
|
.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;
|
display: flex;
|
||||||
justify-content: space-between;
|
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;
|
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>
|
</style>
|
||||||
@@ -1,88 +1,109 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="dialogVisible"
|
v-model="dialogVisible"
|
||||||
:title="isEditing ? '编辑工单' : '新增工单'"
|
:title="isEditing ? '📝 编辑工单任务' : '✨ 创建新工单'"
|
||||||
width="600px"
|
width="550px"
|
||||||
@close="handleClose"
|
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">
|
<div class="form-section">
|
||||||
<el-form-item label="网站" prop="website">
|
<div class="section-title">业务基础信息</div>
|
||||||
<el-input v-model="form.website" placeholder="请输入网站地址" />
|
<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>
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-form-item label="运营人员" prop="operator">
|
<div class="form-section people-grid">
|
||||||
<el-select v-model="form.operator" placeholder="请选择运营人员">
|
<div class="section-title column-full">权责分配</div>
|
||||||
|
|
||||||
|
<el-form-item label="运营人员 (外方)" prop="operator" class="client-item">
|
||||||
|
<el-select v-model="form.operator" placeholder="请选择" style="width: 100%">
|
||||||
<el-option
|
<el-option
|
||||||
v-for="operator in operators"
|
v-for="op in operators"
|
||||||
:key="operator"
|
:key="op"
|
||||||
:label="operator"
|
:label="op"
|
||||||
:value="operator"
|
:value="op"
|
||||||
/>
|
/>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="负责人" prop="principalpeople">
|
<el-form-item label="负责人 (我方)" prop="principalpeople" class="owner-item">
|
||||||
<el-input v-model="form.principalpeople" placeholder="请输入负责人" />
|
<el-input v-model="form.principalpeople" placeholder="请输入姓名">
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><UserFilled /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-form-item label="状态">
|
<div class="form-section">
|
||||||
<el-radio-group v-model="form.is_complete">
|
<el-form-item label="当前进度">
|
||||||
<el-radio :label="0">未完成</el-radio>
|
<el-radio-group v-model="form.is_complete" class="modern-radio-group">
|
||||||
<el-radio :label="1">已完成</el-radio>
|
<el-radio-button :label="0">进行中</el-radio-button>
|
||||||
|
<el-radio-button :label="1">已完成</el-radio-button>
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="备注">
|
<el-form-item label="备注说明" prop="remark">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="form.remark"
|
v-model="form.remark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:rows="3"
|
:rows="3"
|
||||||
placeholder="请输入备注信息"
|
placeholder="填写相关的补充信息或注意事项..."
|
||||||
|
resize="none"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
<!--更改对话框状态-->
|
<el-button @click="dialogVisible = false" plain>取消</el-button>
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
<el-button
|
||||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
type="primary"
|
||||||
{{ isEditing ? '更新' : '新增' }}
|
@click="handleSubmit"
|
||||||
|
:loading="submitting"
|
||||||
|
class="submit-btn"
|
||||||
|
>
|
||||||
|
{{ isEditing ? '保存修改' : '立即创建' }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</div>
|
||||||
</el-dialog>
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, watch, nextTick } from 'vue'
|
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 emit = defineEmits(['update:modelValue', 'submit'])
|
||||||
const dialogVisible = ref(false) // 对话框显示状态,默认为false
|
const dialogVisible = ref(false)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
modelValue: {
|
modelValue: Boolean,
|
||||||
type: Boolean,
|
formData: Object,
|
||||||
default: false
|
operators: Array,
|
||||||
},
|
isEditing: Boolean
|
||||||
formData: {
|
|
||||||
type: Object,
|
|
||||||
default: () => ({})
|
|
||||||
},
|
|
||||||
operators: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
isEditing: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
website: '',
|
website: '',
|
||||||
operator: '',
|
operator: '',
|
||||||
@@ -92,84 +113,115 @@ const form = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
website: [
|
website: [{ required: true, message: '请输入网站地址', trigger: 'blur' }],
|
||||||
{ required: true, message: '网站不能为空', trigger: 'blur' }
|
operator: [{ required: true, message: '请选择运营人员', trigger: 'change' }],
|
||||||
],
|
principalpeople: [{ required: true, message: '请输入负责人', trigger: 'blur' }]
|
||||||
operator: [
|
|
||||||
{ required: true, message: '请选择运营人员', trigger: 'change' }
|
|
||||||
],
|
|
||||||
principalpeople: [
|
|
||||||
{ required: true, message: '负责人不能为空', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听对话框显示状态
|
// 监听显隐
|
||||||
watch(
|
watch(() => props.modelValue, (val) => {
|
||||||
() => props.modelValue,
|
dialogVisible.value = val
|
||||||
(newVal) => {
|
if (val) {
|
||||||
dialogVisible.value = newVal
|
nextTick(() => {
|
||||||
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) {
|
|
||||||
// 切换到新增模式时,重置表单
|
|
||||||
resetForm()
|
resetForm()
|
||||||
}
|
if (props.isEditing && props.formData) {
|
||||||
|
Object.assign(form, props.formData)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
)
|
})
|
||||||
|
|
||||||
|
watch(() => dialogVisible.value, (val) => emit('update:modelValue', val))
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
if (formRef.value) {
|
if (formRef.value) formRef.value.resetFields()
|
||||||
formRef.value.clearValidate()
|
Object.assign(form, { website: '', operator: '', principalpeople: '', is_complete: 0, remark: '' })
|
||||||
}
|
|
||||||
Object.assign(form, {
|
|
||||||
website: '',
|
|
||||||
operator: '',
|
|
||||||
principalpeople: '',
|
|
||||||
is_complete: 0,
|
|
||||||
remark: ''
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => resetForm()
|
||||||
resetForm()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
if (!formRef.value) return
|
if (!formRef.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const valid = await formRef.value.validate()
|
await formRef.value.validate()
|
||||||
if (valid) {
|
submitting.value = true
|
||||||
submitting.value = true
|
emit('submit', { ...form })
|
||||||
emit('submit', { ...form })
|
} catch (e) {
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 验证失败
|
// 验证失败
|
||||||
} finally {
|
} finally {
|
||||||
submitting.value = false
|
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>
|
||||||
@@ -1,106 +1,205 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-card shadow="never">
|
<el-card class="compact-card" shadow="never">
|
||||||
<el-table
|
<el-table
|
||||||
:data="tickets"
|
:data="tickets"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
stripe
|
header-cell-class-name="table-header"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<el-table-column prop="website" label="网站" min-width="150" show-overflow-tooltip />
|
<el-table-column prop="website" label="网站地址" min-width="180">
|
||||||
<el-table-column prop="operator" label="运营人员" width="120" align="center" />
|
<template #default="{ row }">
|
||||||
<el-table-column prop="principalpeople" label="负责人" width="120" align="center" />
|
<el-link
|
||||||
<el-table-column prop="is_complete" label="状态" width="100" align="center">
|
type="primary"
|
||||||
<template #default="{ row }">
|
:href="row.website"
|
||||||
<el-tag :style="getStatusType(row.is_complete)">
|
target="_blank"
|
||||||
{{ row.is_complete }}
|
class="site-link"
|
||||||
</el-tag>
|
>
|
||||||
</template>
|
{{ row.website }}
|
||||||
</el-table-column>
|
</el-link>
|
||||||
<el-table-column prop="create_time" label="创建时间" width="180" align="center">
|
</template>
|
||||||
<template #default="{ row }">
|
</el-table-column>
|
||||||
{{ 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-table-column label="协同人员" width="240">
|
||||||
<el-empty description="暂无数据" />
|
<template #default="{ row }">
|
||||||
</div>
|
<div class="staff-wrapper">
|
||||||
</el-card>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Edit, Delete } from '@element-plus/icons-vue'
|
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
tickets: {
|
tickets: { type: Array, default: () => [] },
|
||||||
type: Array,
|
loading: { type: Boolean, default: false }
|
||||||
default: () => []
|
|
||||||
},
|
|
||||||
loading: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
defineEmits(['edit', 'delete'])
|
defineEmits(['edit', 'delete'])
|
||||||
|
|
||||||
|
|
||||||
// 格式化时间(给箭头函数赋函数名)
|
|
||||||
const formatTime = (time) => {
|
const formatTime = (time) => {
|
||||||
if (!time) return '-';
|
if (!time) return '-';
|
||||||
const date = new Date(time);
|
const date = new Date(time);
|
||||||
return date.toLocaleString('zh',{
|
return `${date.getMonth() + 1}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}`;
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: false
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStatusStyle = (status) => {
|
||||||
// 获取工单状态样式
|
const styles = {
|
||||||
const getStatusType = (status) => {
|
"未分配": { color: '#909399', background: '#f4f4f5' },
|
||||||
switch(status) {
|
"进行中": { color: '#409EFF', background: '#ecf5ff' },
|
||||||
case "未分配": // 未完成
|
"已完成": { color: '#67C23A', background: '#f0f9eb' },
|
||||||
return { backgroundColor: '#9CA3AF', color: '#ffffff' }
|
"已完结": { color: '#e6a23c', background: '#fdf6ec' }
|
||||||
case "进行中": // 已完成
|
|
||||||
return { backgroundColor: '#60A5FA', color: '#ffffff' }
|
|
||||||
case "已完成": // 已完结
|
|
||||||
return { backgroundColor: '#34D399', color: '#ffffff' }
|
|
||||||
case "已完结": // 已完结
|
|
||||||
return { backgroundColor: '#A7F3D0', color: '#065F46' }
|
|
||||||
default:
|
|
||||||
return 'warning'
|
|
||||||
}
|
}
|
||||||
|
return styles[status] || styles["未分配"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.empty-state {
|
.staff-wrapper {
|
||||||
padding: 40px 0;
|
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>
|
</style>
|
||||||
14
src/main.js
14
src/main.js
@@ -4,7 +4,19 @@ import { createApp } from 'vue'
|
|||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import ElementPlus from 'element-plus'
|
import ElementPlus from 'element-plus'
|
||||||
import 'element-plus/dist/index.css'
|
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)
|
const app = createApp(App)
|
||||||
app.use(ElementPlus)
|
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 注册所有图标
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
|
||||||
app.mount('#app')
|
app.mount('#app')
|
||||||
@@ -1,61 +1,66 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="ticket-management">
|
<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="action-bar">
|
<div class="stat-item pending">
|
||||||
<el-button type="primary" @click="showAddDialog = true" :icon="Plus">
|
<div class="stat-label">未分配</div>
|
||||||
新增工单
|
<div class="stat-value">{{ allStats.pending }}</div>
|
||||||
</el-button>
|
</div>
|
||||||
|
<div class="stat-item ongoing">
|
||||||
<div class="stats">
|
<div class="stat-label">进行中</div>
|
||||||
<el-statistic title="总工单数" :value="totalTickets" />
|
<div class="stat-value">{{ allStats.ongoing }}</div>
|
||||||
<el-statistic title="未完成" :value="pendingCount" />
|
</div>
|
||||||
<el-statistic title="正在进行" :value="ongoingCount" />
|
<div class="stat-item success">
|
||||||
<el-statistic title="已完成" :value="completedCount" />
|
<div class="stat-label">已完成/已完结</div>
|
||||||
<el-statistic title="已完结" :value="iscompleteCount" />
|
<div class="stat-value">{{ allStats.completed }}</div>
|
||||||
</div>
|
</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>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
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 { ticketApi } from '@/api/ticket'
|
||||||
import TicketFilter from '@/components/TicketFilter.vue'
|
import TicketFilter from '@/components/TicketFilter.vue'
|
||||||
import TicketList from '@/components/TicketList.vue'
|
import TicketList from '@/components/TicketList.vue'
|
||||||
import TicketForm from '@/components/TicketForm.vue'
|
import TicketForm from '@/components/TicketForm.vue'
|
||||||
|
|
||||||
// 响应式数据
|
// --- 响应式数据 ---
|
||||||
const tickets = ref([])
|
const tickets = ref([])
|
||||||
const operators = ref([])
|
const operators = ref([])
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -63,185 +68,128 @@ const showAddDialog = ref(false)
|
|||||||
const isEditing = ref(false)
|
const isEditing = ref(false)
|
||||||
const currentTicket = ref({})
|
const currentTicket = ref({})
|
||||||
|
|
||||||
// 分页和筛选
|
// 独立统计数据:专门给顶部的卡片使用
|
||||||
|
const allStats = reactive({
|
||||||
|
total: 0,
|
||||||
|
pending: 0,
|
||||||
|
ongoing: 0,
|
||||||
|
completed: 0
|
||||||
|
})
|
||||||
|
|
||||||
const pagination = reactive({
|
const pagination = reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
limit: 15,
|
limit: 15,
|
||||||
total: 0
|
total: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const filters = reactive({
|
const filters = reactive({
|
||||||
status: null,
|
status: null,
|
||||||
operator_name: null,
|
operator_name: null,
|
||||||
principalpeople: null,
|
principalpeople: null,
|
||||||
search: null,
|
search: null,
|
||||||
sort_order: 'desc'
|
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 () => {
|
const loadTickets = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = {
|
const params = {
|
||||||
page: pagination.page,
|
page: pagination.page,
|
||||||
limit: pagination.limit,
|
limit: pagination.limit,
|
||||||
...filters
|
...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)
|
|
||||||
}
|
}
|
||||||
|
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) => {
|
const handleFilterChange = (newFilters) => {
|
||||||
Object.assign(filters, newFilters)
|
Object.assign(filters, newFilters)
|
||||||
pagination.page = 1
|
pagination.page = 1
|
||||||
loadTickets()
|
loadTickets() // 只刷新列表,不刷新卡片
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleFilterReset = () => {
|
const handleFilterReset = () => {
|
||||||
Object.assign(filters, {
|
Object.assign(filters, { status: null, operator_name: null, principalpeople: null, search: null, sort_order: 'desc' })
|
||||||
status: null,
|
pagination.page = 1
|
||||||
operator_name: null,
|
loadTickets()
|
||||||
principalpeople: null,
|
|
||||||
search: null,
|
|
||||||
sort_order: 'desc'
|
|
||||||
})
|
|
||||||
pagination.page = 1
|
|
||||||
loadTickets()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleEdit = (ticket) => {
|
// 新增/编辑/删除后,需要同时刷新列表和统计卡片
|
||||||
currentTicket.value = { ...ticket }
|
const refreshAll = () => {
|
||||||
isEditing.value = true
|
loadTickets()
|
||||||
showAddDialog.value = true
|
initGlobalStats()
|
||||||
}
|
|
||||||
|
|
||||||
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 handleFormSubmit = async (formData) => {
|
const handleFormSubmit = async (formData) => {
|
||||||
try {
|
// ... 提交逻辑 ...
|
||||||
let response
|
// 成功后调用 refreshAll()
|
||||||
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('网络错误,请稍后重试')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetForm = () => {
|
const handleDelete = async (id) => {
|
||||||
currentTicket.value = {}
|
// ... 删除逻辑 ...
|
||||||
isEditing.value = false
|
// 成功后调用 refreshAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSizeChange = (newSize) => {
|
// 分页逻辑
|
||||||
pagination.limit = newSize
|
const handleSizeChange = (val) => { pagination.limit = val; loadTickets(); }
|
||||||
pagination.page = 1
|
const handleCurrentChange = (val) => { pagination.page = val; loadTickets(); }
|
||||||
loadTickets()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCurrentChange = (newPage) => {
|
// 初始化
|
||||||
pagination.page = newPage
|
|
||||||
loadTickets()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生命周期
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadTickets()
|
loadTickets() // 渲染表格
|
||||||
loadOperators()
|
initGlobalStats() // 锁定卡片统计
|
||||||
|
// loadOperators()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.ticket-management {
|
/* 保持原有样式不变 */
|
||||||
background: white;
|
.ticket-management { padding: 20px; background-color: #f5f7fa; min-height: 100vh; }
|
||||||
border-radius: 8px;
|
.stats-container { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
|
||||||
padding: 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; }
|
||||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
.stat-item.pending { border-left-color: #909399; }
|
||||||
}
|
.stat-item.ongoing { border-left-color: #e6a23c; }
|
||||||
|
.stat-item.success { border-left-color: #67c23a; }
|
||||||
.action-bar {
|
.stat-label { font-size: 14px; color: #666; }
|
||||||
display: flex;
|
.stat-value { font-size: 26px; font-weight: bold; color: #333; margin-top: 5px; }
|
||||||
justify-content: space-between;
|
.main-content { background: white; padding: 20px; border-radius: 8px; }
|
||||||
align-items: center;
|
.action-bar { display: flex; justify-content: space-between; margin-bottom: 20px; }
|
||||||
margin-bottom: 20px;
|
.pagination-container { margin-top: 20px; display: flex; justify-content: flex-end; }
|
||||||
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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
Reference in New Issue
Block a user