first commit
This commit is contained in:
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Cypress
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Vitest
|
||||
__screenshots__/
|
||||
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vite App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
8
jsconfig.json
Normal file
8
jsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
3051
package-lock.json
generated
Normal file
3051
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
package.json
Normal file
24
package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "vue_tickets",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.2",
|
||||
"element-plus": "^2.11.8",
|
||||
"vue": "^3.5.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"vite": "^7.1.11",
|
||||
"vite-plugin-vue-devtools": "^8.0.3"
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.ico
Normal file
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
389
src/App.vue
Normal file
389
src/App.vue
Normal file
@@ -0,0 +1,389 @@
|
||||
<template>
|
||||
<div class="full-screen-layout">
|
||||
<el-container class="full-screen-container">
|
||||
<!-- 侧边栏 -->
|
||||
<el-aside width="200px" class="siderbar">
|
||||
<div class="sidebar-header">
|
||||
<div class="logo-container">
|
||||
<!-- 添加 logo 图片 -->
|
||||
<img src="@/assets/logo.jpg" alt="方舟网络" class="logo-image" />
|
||||
<div class="text-content">
|
||||
<h2 class="company-name">方舟网络</h2>
|
||||
<p class="system-name">工单管理系统</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 这里可以放菜单 -->
|
||||
<el-menu
|
||||
default-active="1"
|
||||
class="sidebar-menu"
|
||||
router
|
||||
>
|
||||
<el-menu-item index="1">
|
||||
<el-icon><icon-menu /></el-icon>
|
||||
<span>工单管理</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="2">
|
||||
<el-icon><document /></el-icon>
|
||||
<span>数据统计</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-aside>
|
||||
|
||||
<!-- 主内容区域 -->
|
||||
<el-container class="main-container">
|
||||
<!-- 顶部导航栏 -->
|
||||
<el-header class="main-header">
|
||||
<div class="header-content">
|
||||
<div class="user-info">
|
||||
<el-avatar :size="32" :src="user.avatar" />
|
||||
<span class="username">{{ user.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
|
||||
<!-- 主要内容 -->
|
||||
<el-main class="main-content">
|
||||
<TicketManagement />
|
||||
<!-- <router-view /> -->
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
|
||||
<style scoped>
|
||||
.full-screen-layout {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.full-screen-container {
|
||||
height: 100vh;
|
||||
width: 100vw;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.siderbar {
|
||||
background-color: #001529;
|
||||
color: white;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.system-name {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.sidebar-menu .el-menu-item {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.sidebar-menu .el-menu-item:hover {
|
||||
background-color: rgba(24, 144, 255, 0.2);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.sidebar-menu .el-menu-item.is-active {
|
||||
background-color: #1890ff;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.main-container {
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
height: calc(100vh - 60px);
|
||||
margin: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
import { reactive } from 'vue'
|
||||
import TicketManagement from './views/TicketManagement.vue'
|
||||
|
||||
const user = reactive({
|
||||
name: '管理员',
|
||||
avatar: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
||||
})
|
||||
</script>
|
||||
|
||||
<!-- <style scoped>
|
||||
|
||||
/* 关键:禁止水平滚动和溢出 */
|
||||
.full-screen-layout {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
overflow: hidden !important; /* 禁止任何滚动 */
|
||||
max-width: 100vw !important;
|
||||
}
|
||||
|
||||
|
||||
/* 关键:让 el-container 铺满 */
|
||||
.full-screen-container {
|
||||
height: 100vh !important;
|
||||
width: 100vw !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 侧边栏样式 */
|
||||
.el-aside {
|
||||
background-color: #001529;
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
background-color: #001529;
|
||||
color: white;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
|
||||
.sidebar-header {
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
height: 80px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #001529 0%, #002140 100%);
|
||||
}
|
||||
|
||||
.logo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.company-name {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.system-name {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.sidebar-header h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar-header p {
|
||||
margin: 5px 0 0 0;
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.sidebar-menu {
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.sidebar-menu .el-menu-item {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar-menu .el-menu-item:hover {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 主内容区域头部 */
|
||||
.main-header {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 主要内容区域 */
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
background-color: #f5f7fa;
|
||||
height: calc(100vh - 60px); /* 减去头部高度 */
|
||||
overflow-y: auto; /* 允许内容滚动 */
|
||||
}
|
||||
</style> -->
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!-- <style scoped>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
background: #409EFF;
|
||||
}
|
||||
|
||||
.app-header {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background: #409EFF;
|
||||
}
|
||||
|
||||
.header-content h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
padding: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style> -->
|
||||
33
src/api/ticket.js
Normal file
33
src/api/ticket.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const request = axios.create({
|
||||
baseURL: '/api', // 所有通过实例发送的请求都会加上前缀
|
||||
timeout: 10000 // 单位为ms
|
||||
})
|
||||
|
||||
export const ticketApi = {
|
||||
// 获取工单列表
|
||||
getTickets(params) {
|
||||
return request.get('/ticketsfind', { params })
|
||||
},
|
||||
|
||||
// 新增工单
|
||||
addTicket(data) {
|
||||
return request.post('/addticket', data)
|
||||
},
|
||||
|
||||
// 更新工单
|
||||
updateTicket(id, data) {
|
||||
return request.put(`/ticket/${id}`, data)
|
||||
},
|
||||
|
||||
// 删除工单
|
||||
deleteTicket(id) {
|
||||
return request.delete(`/ticket/${id}`)
|
||||
},
|
||||
|
||||
// 获取运营人员列表
|
||||
getOperators() {
|
||||
return request.get('/operators')
|
||||
}
|
||||
}
|
||||
87
src/assets/base.css
Normal file
87
src/assets/base.css
Normal file
@@ -0,0 +1,87 @@
|
||||
/* color palette from <https://github.com/vuejs/theme> */
|
||||
:root {
|
||||
--vt-c-white: #ffffff;
|
||||
--vt-c-white-soft: #f8f8f8;
|
||||
--vt-c-white-mute: #f2f2f2;
|
||||
|
||||
--vt-c-black: #181818;
|
||||
--vt-c-black-soft: #222222;
|
||||
--vt-c-black-mute: #282828;
|
||||
|
||||
--vt-c-indigo: #2c3e50;
|
||||
|
||||
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
|
||||
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
|
||||
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
|
||||
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
|
||||
|
||||
--vt-c-text-light-1: var(--vt-c-indigo);
|
||||
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
|
||||
--vt-c-text-dark-1: var(--vt-c-white);
|
||||
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
:root {
|
||||
--color-background: var(--vt-c-white);
|
||||
--color-background-soft: var(--vt-c-white-soft);
|
||||
--color-background-mute: var(--vt-c-white-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-light-2);
|
||||
--color-border-hover: var(--vt-c-divider-light-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-light-1);
|
||||
--color-text: var(--vt-c-text-light-1);
|
||||
|
||||
--section-gap: 160px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-background: var(--vt-c-black);
|
||||
--color-background-soft: var(--vt-c-black-soft);
|
||||
--color-background-mute: var(--vt-c-black-mute);
|
||||
|
||||
--color-border: var(--vt-c-divider-dark-2);
|
||||
--color-border-hover: var(--vt-c-divider-dark-1);
|
||||
|
||||
--color-heading: var(--vt-c-text-dark-1);
|
||||
--color-text: var(--vt-c-text-dark-2);
|
||||
}
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
color: var(--color-text);
|
||||
background: var(--color-background);
|
||||
transition:
|
||||
color 0.5s,
|
||||
background-color 0.5s;
|
||||
line-height: 1.6;
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
Roboto,
|
||||
Oxygen,
|
||||
Ubuntu,
|
||||
Cantarell,
|
||||
'Fira Sans',
|
||||
'Droid Sans',
|
||||
'Helvetica Neue',
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
BIN
src/assets/logo.jpg
Normal file
BIN
src/assets/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 220 KiB |
37
src/assets/main.css
Normal file
37
src/assets/main.css
Normal file
@@ -0,0 +1,37 @@
|
||||
@import './base.css';
|
||||
|
||||
/* 重置全局默认边距 */
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a,
|
||||
.green {
|
||||
text-decoration: none;
|
||||
color: hsla(160, 100%, 37%, 1);
|
||||
transition: 0.4s;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
135
src/components/TicketFilter.vue
Normal file
135
src/components/TicketFilter.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<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-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-input
|
||||
v-model="localFilters.search"
|
||||
placeholder="网站/运营/负责人"
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
|
||||
<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">
|
||||
|
||||
|
||||
|
||||
</el-row>
|
||||
|
||||
|
||||
|
||||
</el-form>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, watch } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
filters: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
operators: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['filter-change', 'reset'])
|
||||
|
||||
const localFilters = reactive({ ...props.filters })
|
||||
|
||||
watch(
|
||||
() => props.filters,
|
||||
(newFilters) => {
|
||||
Object.assign(localFilters, newFilters)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const handleSearch = () => {
|
||||
emit('filter-change', { ...localFilters })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.filter-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.filter-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
175
src/components/TicketForm.vue
Normal file
175
src/components/TicketForm.vue
Normal file
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="isEditing ? '编辑工单' : '新增工单'"
|
||||
width="600px"
|
||||
@close="handleClose"
|
||||
>
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="网站" prop="website">
|
||||
<el-input v-model="form.website" placeholder="请输入网站地址" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="运营人员" prop="operator">
|
||||
<el-select v-model="form.operator" placeholder="请选择运营人员">
|
||||
<el-option
|
||||
v-for="operator in operators"
|
||||
:key="operator"
|
||||
:label="operator"
|
||||
:value="operator"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="负责人" prop="principalpeople">
|
||||
<el-input v-model="form.principalpeople" placeholder="请输入负责人" />
|
||||
</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>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="form.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
|
||||
<!--更改对话框状态-->
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||||
{{ isEditing ? '更新' : '新增' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch, nextTick } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'submit'])
|
||||
const dialogVisible = ref(false) // 对话框显示状态,默认为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
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const form = reactive({
|
||||
website: '',
|
||||
operator: '',
|
||||
principalpeople: '',
|
||||
is_complete: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
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) {
|
||||
// 切换到新增模式时,重置表单
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const resetForm = () => {
|
||||
if (formRef.value) {
|
||||
formRef.value.clearValidate()
|
||||
}
|
||||
Object.assign(form, {
|
||||
website: '',
|
||||
operator: '',
|
||||
principalpeople: '',
|
||||
is_complete: 0,
|
||||
remark: ''
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
// 验证失败
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
106
src/components/TicketList.vue
Normal file
106
src/components/TicketList.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<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'
|
||||
|
||||
defineProps({
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 获取工单状态样式
|
||||
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'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty-state {
|
||||
padding: 40px 0;
|
||||
}
|
||||
</style>
|
||||
40
src/components/icon/ArkLogo.vue
Normal file
40
src/components/icon/ArkLogo.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<svg
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:class="className"
|
||||
>
|
||||
<!-- 外圆环代表网络 -->
|
||||
<circle cx="12" cy="12" r="8" :stroke="color" stroke-width="1.5"/>
|
||||
<!-- 方舟船体 -->
|
||||
<path d="M7 15L12 18L17 15" :stroke="color" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M7 12L12 15L17 12" :stroke="color" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- 网络节点 -->
|
||||
<circle cx="12" cy="8" r="1" :fill="color"/>
|
||||
<circle cx="8" cy="12" r="1" :fill="color"/>
|
||||
<circle cx="16" cy="12" r="1" :fill="color"/>
|
||||
<!-- 连接线 -->
|
||||
<path d="M12 8L8 12" :stroke="color" stroke-width="1"/>
|
||||
<path d="M12 8L16 12" :stroke="color" stroke-width="1"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
size: {
|
||||
type: [String, Number],
|
||||
default: 24
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: 'currentColor'
|
||||
},
|
||||
className: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
5
src/components/icon/index.js
Normal file
5
src/components/icon/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
// 统一导出所有图标组件
|
||||
export { default as ArkLogo } from './ArkLogo.vue'
|
||||
// 可以继续添加其他图标组件
|
||||
// export { default as TicketIcon } from './TicketIcon.vue'
|
||||
// export { default as UserIcon } from './UserIcon.vue'
|
||||
10
src/main.js
Normal file
10
src/main.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import './assets/main.css'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(ElementPlus)
|
||||
app.mount('#app')
|
||||
247
src/views/TicketManagement.vue
Normal file
247
src/views/TicketManagement.vue
Normal file
@@ -0,0 +1,247 @@
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Plus } from '@element-plus/icons-vue' // 引入plus图标
|
||||
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)
|
||||
const showAddDialog = ref(false)
|
||||
const isEditing = ref(false)
|
||||
const currentTicket = ref({})
|
||||
|
||||
// 分页和筛选
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
limit: 15,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const filters = reactive({
|
||||
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)
|
||||
|
||||
// 方法
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
const handleFilterChange = (newFilters) => {
|
||||
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()
|
||||
}
|
||||
|
||||
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 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('网络错误,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
currentTicket.value = {}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
const handleSizeChange = (newSize) => {
|
||||
pagination.limit = newSize
|
||||
pagination.page = 1
|
||||
loadTickets()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (newPage) => {
|
||||
pagination.page = newPage
|
||||
loadTickets()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadTickets()
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
31
vite.config.js
Normal file
31
vite.config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 8080, // 前端启动的端口
|
||||
proxy: {
|
||||
'/api': { // 所有以api开头的请求都会被代理
|
||||
target: 'http://localhost:80',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
})
|
||||
Reference in New Issue
Block a user