master #16

Merged
shiran merged 7 commits from master into deploy 2026-01-01 03:30:10 +08:00
10 changed files with 2199 additions and 1239 deletions
Showing only changes of commit 4cc684eca6 - Show all commits
+1 -1
View File
@@ -37,7 +37,7 @@ jobs:
deploy:
needs: build
runs-on: ubuntu-latest
runs-on: hongKong
steps:
- name: Download Artifact
uses: actions/download-artifact@v3
+7 -2
View File
@@ -5,8 +5,13 @@ import request from "@/utils/request.js";
* @returns {Promise}
*/
export function getTickerList(count, page, status) {
return request.get('/api/v1/admin/work_order/list', { count, page, status })
export function getTickerList(count, page, status, orderBy, order) {
const params = { count, page }
if (status !== undefined && status !== '') params.status = status
if (orderBy) params.orderBy = orderBy
if (order) params.order = order
console.log('工单列表请求参数:', params) // 调试日志
return request.get('/api/v1/admin/work_order/list', params)
}
// 待处理
+8 -6
View File
@@ -6,8 +6,14 @@ export const menus = [
},
{
path: '/ticket',
title: '工单理',
icon: 'DataBoard'
title: '工单理',
icon: 'Tickets',
children: [
{
path: '/ticket/list',
title: '工单列表'
}
]
},
{
path: '/user',
@@ -18,10 +24,6 @@ export const menus = [
path: '/user/list',
title: '用户列表'
},
{
path: '/user/balance',
title: '用户余额管理'
},
{
path: '/user/group',
title: '用户组管理'
+21 -1
View File
@@ -39,7 +39,27 @@ const routes = [
title: '工单管理',
icon: 'Tickets'
},
component: () => import('../views/ticket/TicketChat.vue'),
redirect: '/ticket/list',
children: [
{
path: 'list',
name: 'TicketList',
component: () => import('../views/ticket/TicketList.vue'),
meta: {
title: '工单列表'
}
},
{
path: 'detail',
name: 'TicketDetail',
component: () => import('../views/ticket/TicketDetail.vue'),
meta: {
title: '工单详情',
hidden: true,
activeMenu: '/ticket/list'
}
}
]
},
// ACS管理路由
+16 -4
View File
@@ -706,7 +706,10 @@ const paramValueForm = reactive({
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
attr_price: 0,
index: 0,
attr_range: 0,
range_type: 'equal'
})
const paramValueRules = {
@@ -853,7 +856,10 @@ const handleAddParamValue = () => {
attr_id: undefined,
attr_name: '',
attr_value: '',
attr_price: 0
attr_price: 0,
index: 0,
attr_range: 0,
range_type: 'equal'
})
paramValueFormRef.value?.resetFields()
}
@@ -866,7 +872,10 @@ const handleEditParamValue = (row) => {
attr_id: row.id,
attr_name: row.name,
attr_value: row.value,
attr_price: row.price / 100
attr_price: row.price / 100,
index: row.index || 0,
attr_range: row.attr_range || 0,
range_type: row.range_type || 'equal'
})
}
@@ -900,7 +909,10 @@ const submitParamValueForm = () => {
arg_id: Number(currentParam.value.id),
attr_name: paramValueForm.attr_name,
attr_value: paramValueForm.attr_value,
attr_price: paramValueForm.attr_price
attr_price: paramValueForm.attr_price,
index: Number(paramValueForm.index),
attr_range: Number(paramValueForm.attr_range),
range_type: paramValueForm.range_type
}
if (paramValueFormType.value === 'edit') {
submitData.attr_id = paramValueForm.attr_id
File diff suppressed because it is too large Load Diff
+394
View File
@@ -0,0 +1,394 @@
<template>
<div class="ticket-list-page">
<!-- 顶部工具栏 -->
<div class="toolbar">
<div class="status-tabs">
<div class="tab-item pending" :class="{ active: activeStatus === 'pending' }" @click="filterByStatus('pending')">
待处理 <span class="count">{{ stats.pending }}</span>
</div>
<div class="tab-item processing" :class="{ active: activeStatus === 'processing' }" @click="filterByStatus('processing')">
处理中 <span class="count">{{ stats.processing }}</span>
</div>
<div class="tab-item replied" :class="{ active: activeStatus === 'replied' }" @click="filterByStatus('replied')">
已回复 <span class="count">{{ stats.replied }}</span>
</div>
<div class="tab-item completed" :class="{ active: activeStatus === 'completed' }" @click="filterByStatus('completed')">
已完成 <span class="count">{{ stats.completed }}</span>
</div>
<div class="tab-item" :class="{ active: activeStatus === '' }" @click="filterByStatus('')">
全部 <span class="count">{{ stats.total }}</span>
</div>
</div>
<div class="toolbar-right">
<el-select v-model="sortBy" placeholder="排序方式" clearable style="width: 140px" @change="handleSortChange">
<el-option label="不排序" value="" />
<el-option label="创建时间" value="created_at" />
<el-option label="更新时间" value="updated_at" />
<el-option label="工单号" value="id" />
</el-select>
<el-select v-model="sortOrder" placeholder="排序顺序" clearable style="width: 100px" @change="handleSortChange">
<el-option label="默认" value="" />
<el-option label="降序" value="desc" />
<el-option label="升序" value="asc" />
</el-select>
<el-input
v-model="searchKeyword"
placeholder="搜索工单号、标题、用户名"
prefix-icon="Search"
clearable
style="width: 240px"
@input="handleSearch"
/>
<el-button icon="Refresh" @click="refreshList">刷新</el-button>
</div>
</div>
<!-- 工单表格 -->
<el-table
v-loading="isLoading"
:data="filteredTickets"
stripe
style="width: 100%"
@row-click="handleRowClick"
>
<el-table-column prop="id" label="工单号" width="100" />
<el-table-column label="用户" width="180">
<template #default="{ row }">
<div class="user-info">
<el-avatar :size="32" :src="row.avatar">{{ row.username?.charAt(0) }}</el-avatar>
<span class="username">{{ row.username }}</span>
</div>
</template>
</el-table-column>
<el-table-column prop="title" label="工单标题" min-width="200" show-overflow-tooltip />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="180" />
<el-table-column prop="lastReplyTime" label="最后回复" width="180" />
<el-table-column label="操作" width="150" fixed="right">
<template #default="{ row }">
<el-button type="primary" size="small" @click.stop="goToDetail(row)">
回复
</el-button>
<el-button
v-if="row.status !== 'completed'"
type="success"
size="small"
@click.stop="handleComplete(row)"
>
结束
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="totalCount"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</div>
</template>
<script setup>
import { ref, reactive, computed, onMounted, onActivated } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
getTickerList,
closeTicket,
getTicketCount
} from '@/api/ticket'
const router = useRouter()
// 分页
const currentPage = ref(1)
const pageSize = ref(10)
const totalCount = ref(0)
const isLoading = ref(false)
// 工单数据
const ticketList = ref([])
const searchKeyword = ref('')
const activeStatus = ref('pending') // 默认选中"待处理"
// 排序
const sortBy = ref('') // 默认不排序
const sortOrder = ref('') // 默认不选择排序顺序
// 统计数据
const stats = reactive({
pending: 0,
processing: 0,
replied: 0,
completed: 0,
total: 0
})
// 状态转换
const convertStatusToString = (status) => {
const statusMap = { 0: 'pending', 1: 'processing', 2: 'replied', 3: 'completed' }
return statusMap[status] || 'processing'
}
const getStatusText = (status) => {
const statusMap = { pending: '待处理', processing: '处理中', replied: '已回复', completed: '已完成' }
return statusMap[status] || status
}
const getStatusType = (status) => {
const typeMap = { pending: 'warning', processing: 'primary', replied: 'info', completed: 'success' }
return typeMap[status] || ''
}
// 获取工单列表
const fetchTicketList = async () => {
try {
isLoading.value = true
let statusParam = ''
if (activeStatus.value) {
const statusMap = { pending: '0', processing: '1', replied: '2', completed: '3' }
statusParam = statusMap[activeStatus.value] || ''
}
console.log('调用getTickerList,排序参数:', { sortBy: sortBy.value, sortOrder: sortOrder.value })
const res = await getTickerList(
pageSize.value,
currentPage.value,
statusParam,
sortBy.value,
sortOrder.value
)
if (res.code === 200) {
ticketList.value = (res.data.data || []).map(item => ({
id: item.work_id,
title: item.name,
username: item.user?.userName || `用户${item.user?.userId || 'Unknown'}`,
userId: item.user?.userId,
avatar: item.user?.coverUrl || '',
createTime: new Date(item.created_at).toLocaleString(),
lastReplyTime: new Date(item.update_time).toLocaleString(),
status: convertStatusToString(item.status)
}))
totalCount.value = res.data.all_count || 0
} else {
ElMessage.error(res.message || '获取工单列表失败')
}
} catch (error) {
console.error('获取工单列表出错:', error)
ElMessage.error('网络错误,请稍后重试')
} finally {
isLoading.value = false
}
}
// 获取统计数据
const fetchStats = async () => {
try {
const res = await getTicketCount()
if (res.code === 200) {
const data = res.data
stats.total = data.all_count
stats.pending = data.wait_count
stats.replied = data.reply_count
stats.completed = data.close_count
stats.processing = data.all_count - data.wait_count - data.reply_count - data.close_count
}
} catch (error) {
console.error('获取统计数据出错:', error)
}
}
// 过滤后的工单列表
const filteredTickets = computed(() => {
if (!searchKeyword.value) return ticketList.value
const keyword = searchKeyword.value.toLowerCase()
return ticketList.value.filter(ticket =>
ticket.title.toLowerCase().includes(keyword) ||
ticket.username.toLowerCase().includes(keyword) ||
String(ticket.id).includes(keyword)
)
})
// 按状态过滤
const filterByStatus = (status) => {
if (activeStatus.value === status) return
activeStatus.value = status
currentPage.value = 1
fetchTicketList()
}
// 排序变化处理
const handleSortChange = () => {
currentPage.value = 1
fetchTicketList()
}
// 搜索处理
const handleSearch = () => {}
// 分页处理
const handleSizeChange = () => {
currentPage.value = 1
fetchTicketList()
}
const handlePageChange = () => {
fetchTicketList()
}
// 刷新列表
const refreshList = () => {
fetchTicketList()
fetchStats()
}
// 跳转到详情页
const goToDetail = (row) => {
router.push({ path: '/ticket/detail', query: { id: row.id } })
}
const handleRowClick = (row) => {
goToDetail(row)
}
// 结束工单
const handleComplete = (ticket) => {
ElMessageBox.confirm('确定要结束此工单吗?结束后将无法继续回复。', '确认操作', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
try {
const res = await closeTicket(ticket.id)
if (res.code === 200) {
ElMessage.success('工单已成功结束')
refreshList()
} else {
ElMessage.error(res.message || '结束工单失败')
}
} catch (error) {
ElMessage.error('网络错误,请稍后重试')
}
}).catch(() => {})
}
let isFirstLoad = true
onMounted(() => {
fetchTicketList()
fetchStats()
})
// 当页面被激活时(从详情页返回时)
onActivated(() => {
// 跳过首次加载,只在从其他页面返回时刷新
if (!isFirstLoad) {
refreshList()
}
isFirstLoad = false
})
</script>
<style scoped>
.ticket-list-page {
padding: 0;
height: calc(100vh - 100px);
display: flex;
flex-direction: column;
background: #fff;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 20px;
height: 50px;
border-bottom: 1px solid #ebeef5;
}
.status-tabs {
display: flex;
gap: 8px;
}
.tab-item {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
color: #606266;
transition: all 0.2s;
}
.tab-item:hover {
background: #f5f7fa;
}
.tab-item.active {
background: #409eff;
color: #fff;
}
.tab-item.pending.active { background: #e6a23c; }
.tab-item.processing.active { background: #409eff; }
.tab-item.replied.active { background: #909399; }
.tab-item.completed.active { background: #67c23a; }
.tab-item .count {
margin-left: 4px;
font-weight: 500;
}
.toolbar-right {
display: flex;
align-items: center;
gap: 8px;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
}
.username {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pagination-wrapper {
display: flex;
justify-content: flex-end;
padding: 12px 20px;
border-top: 1px solid #ebeef5;
}
:deep(.el-table) {
flex: 1;
}
:deep(.el-table tr) {
cursor: pointer;
}
</style>
File diff suppressed because it is too large Load Diff
+6
View File
@@ -393,6 +393,10 @@ const Edit = EditIcon
const route = useRoute()
const router = useRouter()
// 引入tagsViewStore
import { useTagsViewStore } from '@/store/tagsViewStore'
const tagsViewStore = useTagsViewStore()
// 用户信息
const userInfo = ref({})
const loading = ref(false)
@@ -546,6 +550,8 @@ const refreshData = () => {
// 返回上一页
const goBack = () => {
// 关闭当前tab
tagsViewStore.delVisitedView(route)
router.go(-1)
}
+28 -24
View File
@@ -129,6 +129,7 @@
<el-dropdown-item command="password">修改密码</el-dropdown-item>
<el-dropdown-item command="group">修改用户组</el-dropdown-item>
<el-dropdown-item command="realname">实名信息</el-dropdown-item>
<el-dropdown-item command="balance">余额管理</el-dropdown-item>
<el-dropdown-item command="loginHistory">登录记录</el-dropdown-item>
<el-dropdown-item command="operationHistory">操作记录</el-dropdown-item>
<el-dropdown-item command="simulateLogin">模拟登录</el-dropdown-item>
@@ -512,12 +513,24 @@ const fetchUserList = async () => {
const res = await getUserList(queryParams)
console.log("获取用户列表:", res)
if (res.data.code === 200) {
userList.value = res.data.data.data || []
// 映射 API 返回的字段到组件使用的字段格式
userList.value = (res.data.data.data || []).map(user => ({
UserId: user.user_id,
UserName: user.user_name,
Phone: user.phone,
Email: user.email,
Sex: user.sex,
Age: user.age,
IsAdmin: user.is_admin,
CoverID: user.cover_id,
avatarUrl: user.cover || '', // 直接使用 cover 字段作为头像 URL
UserGroup: user.user_group,
RealName: user.real_name,
IsDeleted: user.is_deleted,
CreatedAt: user.created_at
}))
console.log("用户列表:", userList.value)
total.value = res.data.data.all_count || 0
// 为每个用户加载头像URL
await loadAvatarsForUsers()
}
} catch (error) {
ElMessage.error('获取用户列表失败')
@@ -526,26 +539,6 @@ const fetchUserList = async () => {
}
}
// 为用户列表加载头像URL
const loadAvatarsForUsers = async () => {
const promises = userList.value.map(async (user) => {
if (user.CoverID) {
try {
const res = await getFileDetail({ file_id: user.CoverID })
if (res.data.code === 200) {
user.avatarUrl = res.data.data.url
}
} catch (error) {
console.error('加载头像失败:', error)
user.avatarUrl = ''
}
} else {
user.avatarUrl = ''
}
})
await Promise.all(promises)
}
// 查询用户列表
const handleQuery = () => {
queryParams.page = 1
@@ -676,6 +669,9 @@ const handleCommand = (command, row) => {
case 'realname':
handleRealnameModify(row)
break
case 'balance':
handleBalanceManage(row)
break
case 'loginHistory':
handleLoginHistory(row)
break
@@ -691,6 +687,14 @@ const handleCommand = (command, row) => {
}
}
// 余额管理
const handleBalanceManage = (row) => {
router.push({
path: '/user/balance',
query: { user_id: row.UserId }
})
}
// 模拟登录
const handleSimulateLogin = async (row) => {