mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-25 21:57:48 +00:00
feat: 新增12个业务页面功能
- 新增模块监控模块:在线用户页、登录日志页、操作日志页、数据概览页 - 新增AI助手模块:AI模型配置页、聊天会话页 - 新增系统管理模块:通知公告页、用户详情页、用户管理页、工单管理页、工单详情页
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
<script setup lang="ts">
|
||||
import type { NoticeForm, NoticeItem } from '@/api/module_system/notice'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { NoticeAPI } from '@/api/module_system/notice'
|
||||
|
||||
definePage({ name: 'work-notices', style: { navigationBarTitleText: '通知公告' } })
|
||||
|
||||
const toast = useToast()
|
||||
const searchTitle = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<NoticeForm>({ notice_title: '', notice_content: '', notice_type: '1', status: 0, description: '' })
|
||||
|
||||
const NOTICE_TYPE_OPTIONS = [{ value: '1', label: '通知' }, { value: '2', label: '公告' }]
|
||||
const STATUS_OPTIONS = [{ value: 0, label: '草稿' }, { value: 1, label: '已发布' }, { value: 2, label: '已归档' }]
|
||||
|
||||
function statusLabel(s: number | string | undefined) {
|
||||
const n = Number(s)
|
||||
if (n === 1)
|
||||
return 'published'
|
||||
if (n === 2)
|
||||
return 'archived'
|
||||
return 'draft'
|
||||
}
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadNext } = useListPage<NoticeItem>({
|
||||
fetcher: p => NoticeAPI.getPage({ ...p, notice_title: searchTitle.value || undefined }),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function handlePageChange({ value }: { value: number }) {
|
||||
pageParams.value.page_no = value
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchTitle.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { notice_title: '', notice_content: '', notice_type: '1', status: 0, description: '' })
|
||||
}
|
||||
function openCreate() {
|
||||
formTitle.value = '新增公告'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
showForm.value = true
|
||||
}
|
||||
async function openEdit(id: number) {
|
||||
formTitle.value = '编辑公告'
|
||||
currentId.value = id
|
||||
try {
|
||||
const detail = await NoticeAPI.getDetail(id)
|
||||
Object.assign(formData, {
|
||||
notice_title: detail.notice_title || '',
|
||||
notice_content: detail.notice_content || '',
|
||||
notice_type: detail.notice_type || '1',
|
||||
status: detail.status ?? 0,
|
||||
description: detail.description || '',
|
||||
})
|
||||
showForm.value = true
|
||||
}
|
||||
catch { toast.error('获取详情失败') }
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await NoticeAPI.update(currentId.value, formData)
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await NoticeAPI.create(formData)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await NoticeAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchTitle" placeholder="搜索公告标题" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">
|
||||
+ 新增
|
||||
</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<wd-empty v-if="!loading && list.length === 0" tip="暂无公告" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="item in list" :key="item.id" center is-link @click="openEdit(item.id!)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="text-md font-medium">
|
||||
{{ item.notice_title }}
|
||||
</text><text class="text-muted mt-xs block text-xs">
|
||||
{{ item.description || '' }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<view class="flex items-center gap-xs">
|
||||
<StatusBadge :status="statusLabel(item.status)" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<wd-pagination
|
||||
:model-value="pageParams.page_no"
|
||||
:total="total"
|
||||
:page-size="pageParams.page_size"
|
||||
button-variant="plain"
|
||||
hide-if-one-page
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
<wd-popup v-model="showForm" position="bottom" round custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar :title="formTitle" left-arrow @click-left="showForm = false" />
|
||||
<wd-form :model="formData" class="mt-lg">
|
||||
<wd-form-item label="公告标题" prop="notice_title" border>
|
||||
<wd-input v-model="formData.notice_title" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="公告内容" prop="notice_content" border>
|
||||
<wd-textarea v-model="formData.notice_content" placeholder="请输入内容" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="公告类型" border>
|
||||
<wd-radio-group v-model="formData.notice_type">
|
||||
<wd-radio v-for="opt in NOTICE_TYPE_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="状态" border>
|
||||
<wd-radio-group v-model="formData.status">
|
||||
<wd-radio v-for="opt in STATUS_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="备注" border>
|
||||
<wd-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button block variant="plain" @click="showForm = false">
|
||||
取消
|
||||
</wd-button><wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,248 @@
|
||||
<script setup lang="ts">
|
||||
import type { TicketComment, TicketItem } from '@/api/module_system/ticket'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
|
||||
definePage({
|
||||
name: 'work-ticket-detail',
|
||||
style: { navigationBarTitleText: '工单详情' },
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const ticket = ref<TicketItem | null>(null)
|
||||
const comments = ref<TicketComment[]>([])
|
||||
const commentTotal = ref(0)
|
||||
const commentPage = reactive({ page_no: 1, page_size: 20 })
|
||||
const commentText = ref('')
|
||||
const submitting = ref(false)
|
||||
const ticketId = ref(0)
|
||||
|
||||
async function loadTicket() {
|
||||
if (!ticketId.value)
|
||||
return
|
||||
loading.value = true
|
||||
try {
|
||||
ticket.value = await TicketAPI.getDetail(ticketId.value)
|
||||
}
|
||||
catch {
|
||||
toast.error('加载工单详情失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadComments() {
|
||||
if (!ticketId.value)
|
||||
return
|
||||
try {
|
||||
const res = await TicketAPI.getComments(ticketId.value, { page_no: commentPage.page_no, page_size: commentPage.page_size })
|
||||
comments.value = res.list || []
|
||||
commentTotal.value = res.total || 0
|
||||
}
|
||||
catch (e) {
|
||||
console.error('加载评论失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function submitComment() {
|
||||
if (!commentText.value.trim())
|
||||
return toast.warning('请输入评论内容')
|
||||
submitting.value = true
|
||||
try {
|
||||
await TicketAPI.createComment(ticketId.value, { content: commentText.value.trim() })
|
||||
commentText.value = ''
|
||||
toast.success('评论成功')
|
||||
loadComments()
|
||||
}
|
||||
catch {
|
||||
toast.error('评论失败')
|
||||
}
|
||||
finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getTypeLabel(type?: string) {
|
||||
const map: Record<string, string> = { suggestion: '建议', bug: '缺陷', optimize: '优化', other: '其他' }
|
||||
return map[type || ''] || type || '其他'
|
||||
}
|
||||
function getTypeColor(type?: string) {
|
||||
const map: Record<string, string> = { suggestion: '#4F8CFF', bug: '#EF4444', optimize: '#F59E0B', other: '#6B7280' }
|
||||
return map[type || ''] || '#6B7280'
|
||||
}
|
||||
function getStatusLabel(status?: string | number) {
|
||||
const map: Record<string, string> = { 0: '待处理', 1: '处理中', 2: '已完成', 3: '已关闭' }
|
||||
return map[String(status ?? '')] || '未知'
|
||||
}
|
||||
function getStatusColor(status?: string | number) {
|
||||
const map: Record<string, string> = { 0: '#F59E0B', 1: '#4F8CFF', 2: '#10B981', 3: '#6B7280' }
|
||||
return map[String(status ?? '')] || '#6B7280'
|
||||
}
|
||||
function parseImages(images?: string): string[] {
|
||||
if (!images)
|
||||
return []
|
||||
try {
|
||||
return JSON.parse(images)
|
||||
}
|
||||
catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function previewImages(images: string[], current: number) {
|
||||
uni.previewImage({ urls: images, current })
|
||||
}
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
try {
|
||||
await Promise.all([loadTicket(), loadComments()])
|
||||
}
|
||||
finally {
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
})
|
||||
|
||||
onLoad((options) => {
|
||||
ticketId.value = Number(options?.id || 0)
|
||||
if (ticketId.value) {
|
||||
loadTicket()
|
||||
loadComments()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper" style="padding-bottom: 140rpx;">
|
||||
<SkeletonPage v-if="loading && !ticket" :rows="5" />
|
||||
|
||||
<template v-else-if="ticket">
|
||||
<!-- Ticket Header -->
|
||||
<view class="mx-3 mb-3 rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<text class="block text-4 font-bold wot-text-text-main">
|
||||
{{ ticket.title || '未命名工单' }}
|
||||
</text>
|
||||
<view class="mt-3 flex gap-2">
|
||||
<wd-tag size="small" round :bg-color="`${getTypeColor(ticket.ticket_type)}18`" :color="getTypeColor(ticket.ticket_type)">
|
||||
{{ getTypeLabel(ticket.ticket_type) }}
|
||||
</wd-tag>
|
||||
<wd-tag size="small" round :bg-color="`${getStatusColor(ticket.status)}18`" :color="getStatusColor(ticket.status)">
|
||||
{{ getStatusLabel(ticket.status) }}
|
||||
</wd-tag>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Ticket Info -->
|
||||
<view class="mx-3 mb-3">
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="创建时间" :value="ticket.created_time || '—'" />
|
||||
<wd-cell title="处理人" :value="ticket.assigned_by?.name || '未分配'" />
|
||||
<wd-cell title="更新时间" :value="ticket.updated_time || '—'" />
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- Ticket Content -->
|
||||
<view v-if="ticket.ticket_content || ticket.summary" class="mx-3 mb-3 rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<text class="mb-3 block text-3.5 font-bold wot-text-text-main">
|
||||
工单内容
|
||||
</text>
|
||||
<text class="block text-3 leading-relaxed wot-text-text-secondary">
|
||||
{{ ticket.summary || ticket.ticket_content }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Images -->
|
||||
<view v-if="parseImages(ticket.images).length > 0" class="mx-3 mb-3 rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<text class="mb-3 block text-3.5 font-bold wot-text-text-main">
|
||||
附件图片
|
||||
</text>
|
||||
<view class="flex flex-wrap gap-3">
|
||||
<image
|
||||
v-for="(img, idx) in parseImages(ticket.images)"
|
||||
:key="img"
|
||||
:src="img"
|
||||
class="h-[200rpx] w-[200rpx] rounded-lg wot-bg-filled-oppo"
|
||||
mode="aspectFill"
|
||||
lazy-load
|
||||
@click="previewImages(parseImages(ticket.images), idx)"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Reply -->
|
||||
<view v-if="ticket.reply" class="mx-3 mb-3 rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<text class="mb-3 block text-3.5 font-bold wot-text-text-main">
|
||||
处理回复
|
||||
</text>
|
||||
<view class="rounded-lg p-3" style="background: var(--primary-color-light, rgba(1, 77, 178, 0.06));">
|
||||
<text class="block text-3 leading-relaxed wot-text-text-secondary">
|
||||
{{ ticket.reply }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Comments -->
|
||||
<view class="mx-3 mb-3 rounded-2 p-4 wot-bg-filled-oppo">
|
||||
<text class="mb-3 block text-3.5 font-bold wot-text-text-main">
|
||||
评论 ({{ commentTotal }})
|
||||
</text>
|
||||
<wd-empty v-if="comments.length === 0" tip="暂无评论,快来抢沙发" />
|
||||
<view v-else class="flex flex-col gap-4">
|
||||
<view v-for="comment in comments" :key="comment.id" class="flex gap-3">
|
||||
<view class="h-8 w-8 flex shrink-0 items-center justify-center rounded-full" style="background: var(--primary-color);">
|
||||
<text class="text-3.5 font-semibold" style="color: #FFFFFF;">
|
||||
{{ (comment.username || '匿').charAt(0) }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="min-w-0 flex-1">
|
||||
<view class="flex items-center gap-3">
|
||||
<text class="text-3 font-semibold wot-text-text-main">
|
||||
{{ comment.username || '匿名用户' }}
|
||||
</text>
|
||||
<text class="text-2.5 wot-text-text-auxiliary">
|
||||
{{ comment.created_time || '' }}
|
||||
</text>
|
||||
</view>
|
||||
<text class="mt-1 block text-3 leading-relaxed wot-text-text-secondary">
|
||||
{{ comment.content }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="commentTotal > comments.length" class="flex items-center justify-center py-1" @click="commentPage.page_no++; loadComments()">
|
||||
<text class="text-3 wot-text-primary">
|
||||
加载更多
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<wd-empty v-else tip="工单不存在或已删除" />
|
||||
|
||||
<!-- Comment Input -->
|
||||
<view
|
||||
v-if="ticket"
|
||||
class="fixed inset-x-0 bottom-0 flex items-center gap-3 px-4 py-3"
|
||||
style="z-index: 100; background: var(--card-bg-color, #FFFFFF); border-top: 1rpx solid var(--border-color, #F0F0F0); padding-bottom: calc(12px + env(safe-area-inset-bottom));"
|
||||
>
|
||||
<wd-input
|
||||
v-model="commentText"
|
||||
placeholder="写下你的评论..."
|
||||
clearable
|
||||
class="flex-1"
|
||||
/>
|
||||
<wd-button
|
||||
size="small"
|
||||
type="primary"
|
||||
:loading="submitting"
|
||||
:disabled="!commentText.trim()"
|
||||
@click="submitComment"
|
||||
>
|
||||
发送
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,218 @@
|
||||
<script setup lang="ts">
|
||||
import type { TicketForm, TicketItem } from '@/api/module_system/ticket'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { TicketAPI } from '@/api/module_system/ticket'
|
||||
|
||||
definePage({ name: 'work-tickets', style: { navigationBarTitleText: '工单管理' } })
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const searchTitle = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const initForm: TicketForm = { title: '', ticket_content: '', ticket_type: '', status: undefined, description: '' }
|
||||
const formData = reactive<TicketForm>({ ...initForm })
|
||||
const showPickerType = ref(false)
|
||||
const showPickerStatus = ref(false)
|
||||
|
||||
const TYPE_OPTIONS = [{ value: 'suggestion', label: '建议' }, { value: 'bug', label: '缺陷' }, { value: 'optimize', label: '优化' }, { value: 'other', label: '其他' }]
|
||||
|
||||
const STATUS_OPTIONS = [{ value: 0, label: '待处理' }, { value: 1, label: '处理中' }, { value: 2, label: '已完成' }, { value: 3, label: '已关闭' }]
|
||||
|
||||
interface PickerConfirmEvent { value: Array<string | number>, selectedOptions: any[] }
|
||||
function handleTypeConfirm(e: PickerConfirmEvent) {
|
||||
const item = TYPE_OPTIONS.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
formData.ticket_type = item.value
|
||||
showPickerType.value = false
|
||||
}
|
||||
function handleStatusConfirm(e: PickerConfirmEvent) {
|
||||
const item = STATUS_OPTIONS.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
formData.status = item.value
|
||||
showPickerStatus.value = false
|
||||
}
|
||||
|
||||
const TICKET_STATUS_MAP: Record<number, string> = { 0: 'pending', 1: 'processing', 2: 'completed', 3: 'closed' }
|
||||
function ticketStatus(status: number | string | undefined) {
|
||||
return TICKET_STATUS_MAP[Number(status)] || String(status)
|
||||
}
|
||||
|
||||
function displayContent(item: TicketItem) {
|
||||
return item.ticket_content || ''
|
||||
}
|
||||
function displayType(item: TicketItem) {
|
||||
return item.ticket_type || ''
|
||||
}
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadNext } = useListPage<TicketItem>({
|
||||
fetcher: p => TicketAPI.getPage({ ...p, title: searchTitle.value || undefined }),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function handlePageChange({ value }: { value: number }) {
|
||||
pageParams.value.page_no = value
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchTitle.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function resetForm() {
|
||||
Object.assign(formData, { ...initForm })
|
||||
}
|
||||
function openCreate() {
|
||||
formTitle.value = '新增'
|
||||
currentId.value = undefined
|
||||
resetForm()
|
||||
showForm.value = true
|
||||
}
|
||||
function navigateToDetail(id: number) {
|
||||
router.push({ name: 'work-ticket-detail', query: { id: String(id) } })
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
if (currentId.value) {
|
||||
await TicketAPI.update(currentId.value, { ...formData })
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await TicketAPI.create({ ...formData })
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content: '确认删除?',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await TicketAPI.remove([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchTitle" placeholder="搜索标题" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
<view class="action-bar">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">
|
||||
+ 新增
|
||||
</wd-button>
|
||||
</view>
|
||||
<SkeletonPage v-if="loading" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<wd-empty v-if="!loading && list.length === 0" tip="暂无数据" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="item in list" :key="item.id" center is-link @click="navigateToDetail(item.id!)">
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="text-md font-medium">
|
||||
{{ item.title }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
<template #label>
|
||||
<text class="text-muted text-xs">
|
||||
{{ displayType(item) ? `${displayType(item)} · ` : '' }}{{ displayContent(item) }}
|
||||
</text>
|
||||
</template>
|
||||
<view class="flex items-center gap-xs">
|
||||
<StatusBadge :status="ticketStatus(item.status)" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</view>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<wd-pagination
|
||||
:model-value="pageParams.page_no"
|
||||
:total="total"
|
||||
:page-size="pageParams.page_size"
|
||||
button-variant="plain"
|
||||
hide-if-one-page
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</template>
|
||||
<wd-popup v-model="showForm" position="bottom" round custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar :title="formTitle" left-arrow @click-left="showForm = false" />
|
||||
<wd-form :model="formData" class="mt-lg">
|
||||
<wd-form-item label="工单标题">
|
||||
<wd-input v-model="formData.title" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="内容">
|
||||
<wd-textarea v-model="formData.ticket_content" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="类型">
|
||||
<view @click="showPickerType = true">
|
||||
<wd-cell title="类型" :value="TYPE_OPTIONS.find(o => o.value === formData.ticket_type)?.label || '请选择'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerType" :columns="[TYPE_OPTIONS]" @confirm="handleTypeConfirm" @cancel="showPickerType = false" />
|
||||
</wd-form-item>
|
||||
<wd-form-item v-if="currentId" label="状态">
|
||||
<view @click="showPickerStatus = true">
|
||||
<wd-cell title="状态" :value="STATUS_OPTIONS.find(o => o.value === formData.status)?.label || '请选择'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerStatus" :columns="[STATUS_OPTIONS]" @confirm="handleStatusConfirm" @cancel="showPickerStatus = false" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="备注">
|
||||
<wd-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button block variant="plain" @click="showForm = false">
|
||||
取消
|
||||
</wd-button><wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,227 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeptItem } from '@/api/module_system/dept'
|
||||
import type { UserForm } from '@/api/module_system/user'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { DeptAPI } from '@/api/module_system/dept'
|
||||
import { RoleAPI } from '@/api/module_system/role'
|
||||
import UserAPI from '@/api/module_system/user'
|
||||
|
||||
definePage({
|
||||
name: 'work-user-detail',
|
||||
style: { navigationBarTitleText: '用户详情' },
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const userId = ref<number>()
|
||||
|
||||
const user = ref<UserForm>({})
|
||||
const deptOptions = ref<{ value: number, label: string }[]>([])
|
||||
const roleOptions = ref<{ value: number, label: string }[]>([])
|
||||
const showPickerDept = ref(false)
|
||||
const showPickerRole = ref(false)
|
||||
|
||||
interface PickerConfirmEvent { value: Array<string | number>, selectedOptions: any[] }
|
||||
function handleDeptConfirm(e: PickerConfirmEvent) {
|
||||
const item = deptOptions.value.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
user.value.dept_id = item.value
|
||||
showPickerDept.value = false
|
||||
}
|
||||
function handleRoleConfirm(e: PickerConfirmEvent) {
|
||||
const item = roleOptions.value.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
user.value.role_ids = [item.value]
|
||||
showPickerRole.value = false
|
||||
}
|
||||
|
||||
const GENDER_OPTIONS = [
|
||||
{ value: 0, label: '男' },
|
||||
{ value: 1, label: '女' },
|
||||
{ value: 2, label: '未知' },
|
||||
]
|
||||
|
||||
function genderLabel(gender?: number) {
|
||||
return GENDER_OPTIONS.find(g => g.value === gender)?.label || '未知'
|
||||
}
|
||||
|
||||
function flattenDept(items: DeptItem[], prefix = ''): { value: number, label: string }[] {
|
||||
let r: { value: number, label: string }[] = []
|
||||
for (const item of items) {
|
||||
r.push({ value: item.id, label: prefix + item.name })
|
||||
if (item.children?.length)
|
||||
r = r.concat(flattenDept(item.children, `${prefix} `))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
try {
|
||||
const [deptTree, roleRes] = await Promise.all([
|
||||
DeptAPI.getTree().catch(() => [] as DeptItem[]),
|
||||
RoleAPI.getOptions().catch(() => []),
|
||||
])
|
||||
deptOptions.value = flattenDept(deptTree || [])
|
||||
roleOptions.value = (roleRes || []).map(r => ({ value: r.value, label: r.label }))
|
||||
}
|
||||
catch { /* silent */ }
|
||||
}
|
||||
|
||||
async function loadUser() {
|
||||
if (!userId.value)
|
||||
return
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await UserAPI.getUserDetail(userId.value)
|
||||
user.value = res
|
||||
}
|
||||
catch {
|
||||
toast.error('加载用户详情失败')
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
if (!user.value.id)
|
||||
return
|
||||
saving.value = true
|
||||
try {
|
||||
await UserAPI.updateUser(user.value)
|
||||
toast.success('保存成功')
|
||||
isEdit.value = false
|
||||
}
|
||||
catch {
|
||||
toast.error('保存失败')
|
||||
}
|
||||
finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleToggleStatus() {
|
||||
// 后端规范: 0=启用, 1=禁用
|
||||
const newStatus = user.value.status === 1 ? 0 : 1
|
||||
const action = newStatus === 0 ? '启用' : '禁用'
|
||||
uni.showModal({
|
||||
title: '确认操作',
|
||||
content: `确认${action}该用户吗?`,
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
user.value.status = newStatus
|
||||
await UserAPI.updateUser(user.value)
|
||||
toast.success(`${action}成功`)
|
||||
}
|
||||
catch {
|
||||
toast.error('操作失败')
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((query) => {
|
||||
userId.value = Number(query?.id)
|
||||
loadOptions()
|
||||
loadUser()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper py-3">
|
||||
<SkeletonPage v-if="loading" :rows="6" />
|
||||
|
||||
<template v-else>
|
||||
<!-- Avatar -->
|
||||
<view class="mx-3 mb-3 flex flex-col items-center gap-2 py-4">
|
||||
<wd-avatar
|
||||
size="80px"
|
||||
round
|
||||
:text="(user.name || user.username || '?').charAt(0)"
|
||||
bg-color="var(--primary-color)"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
<text class="text-4 font-bold wot-text-text-main">
|
||||
{{ user.name || user.username || '未知用户' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- View mode -->
|
||||
<view v-if="!isEdit" class="mx-3 mb-3">
|
||||
<wd-cell-group border custom-class="rounded-2! overflow-hidden">
|
||||
<wd-cell title="用户名" :value="user.username || '-'" />
|
||||
<wd-cell title="邮箱" :value="user.email || '-'" />
|
||||
<wd-cell title="手机号" :value="user.mobile || '-'" />
|
||||
<wd-cell title="性别" :value="genderLabel(user.gender)" />
|
||||
<wd-cell title="角色" :value="user.role_names?.join(', ') || roleOptions.find(r => user.role_ids?.includes(r.value))?.label || '-'" />
|
||||
<wd-cell title="部门" :value="user.dept_name || deptOptions.find(d => d.value === user.dept_id)?.label || '-'" />
|
||||
<wd-cell title="状态">
|
||||
<StatusBadge :status="user.status" />
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
|
||||
<!-- Edit mode -->
|
||||
<view v-else class="mx-3 mb-3">
|
||||
<view class="rounded-2 p-3 wot-bg-filled-oppo">
|
||||
<wd-form :model="user">
|
||||
<wd-form-item label="用户名" border>
|
||||
<wd-input v-model="user.username" placeholder="请输入用户名" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="姓名" border>
|
||||
<wd-input v-model="user.name" placeholder="请输入姓名" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="邮箱" border>
|
||||
<wd-input v-model="user.email" placeholder="请输入邮箱" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="手机号" border>
|
||||
<wd-input v-model="user.mobile" placeholder="请输入手机号" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="性别" border>
|
||||
<wd-radio-group v-model="user.gender">
|
||||
<wd-radio v-for="g in GENDER_OPTIONS" :key="g.value" :value="g.value">
|
||||
{{ g.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="部门" border>
|
||||
<view class="flex-1" @click="showPickerDept = true">
|
||||
<wd-cell :value="deptOptions.find(o => o.value === user.dept_id)?.label || '选择部门'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerDept" :columns="[deptOptions]" @confirm="handleDeptConfirm" @cancel="showPickerDept = false" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="角色" border>
|
||||
<view class="flex-1" @click="showPickerRole = true">
|
||||
<wd-cell :value="roleOptions.find(o => o.value === user.role_ids?.[0])?.label || '选择角色'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerRole" :columns="[roleOptions]" @confirm="handleRoleConfirm" @cancel="showPickerRole = false" />
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Actions -->
|
||||
<view v-if="isEdit" class="mx-3 flex flex-col gap-3">
|
||||
<wd-button type="primary" round block :loading="saving" @click="handleSave">
|
||||
保存修改
|
||||
</wd-button>
|
||||
<wd-button variant="plain" round block @click="isEdit = false">
|
||||
取消
|
||||
</wd-button>
|
||||
</view>
|
||||
<view v-else class="mx-3 flex flex-col gap-3">
|
||||
<wd-button type="primary" round block @click="isEdit = true">
|
||||
编辑资料
|
||||
</wd-button>
|
||||
<wd-button type="danger" plain round block @click="handleToggleStatus">
|
||||
{{ user.status === 1 ? '启用账号' : '禁用账号' }}
|
||||
</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
@@ -0,0 +1,315 @@
|
||||
<script setup lang="ts">
|
||||
import type { DeptItem } from '@/api/module_system/dept'
|
||||
import type { PositionItem } from '@/api/module_system/position'
|
||||
import type { RoleItem } from '@/api/module_system/role'
|
||||
import type { UserForm, UserInfo } from '@/api/module_system/user'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { ref } from 'vue'
|
||||
import { DeptAPI } from '@/api/module_system/dept'
|
||||
import { PositionAPI } from '@/api/module_system/position'
|
||||
import { RoleAPI } from '@/api/module_system/role'
|
||||
import UserAPI from '@/api/module_system/user'
|
||||
|
||||
definePage({ name: 'work-users', style: { navigationBarTitleText: '用户管理' } })
|
||||
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const searchName = ref('')
|
||||
const showForm = ref(false)
|
||||
const formTitle = ref('')
|
||||
const currentId = ref<number>()
|
||||
const formData = reactive<UserForm>({
|
||||
username: '',
|
||||
name: '',
|
||||
password: '',
|
||||
dept_id: undefined,
|
||||
role_ids: [],
|
||||
position_ids: [],
|
||||
email: '',
|
||||
mobile: '',
|
||||
gender: 2,
|
||||
status: 1,
|
||||
description: '',
|
||||
})
|
||||
|
||||
const GENDER_OPTIONS = [{ value: 0, label: '男' }, { value: 1, label: '女' }, { value: 2, label: '未知' }]
|
||||
const STATUS_OPTIONS = [{ value: 0, label: '正常' }, { value: 1, label: '禁用' }]
|
||||
|
||||
const deptOptions = ref<{ value: number, label: string }[]>([])
|
||||
const roleOptions = ref<{ value: number, label: string }[]>([])
|
||||
const positionOptions = ref<{ value: number, label: string }[]>([])
|
||||
const showPickerDept = ref(false)
|
||||
const showPickerRole = ref(false)
|
||||
const showPickerPosition = ref(false)
|
||||
|
||||
interface PickerConfirmEvent { value: Array<string | number>, selectedOptions: any[] }
|
||||
function handleDeptConfirm(e: PickerConfirmEvent) {
|
||||
const item = deptOptions.value.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
formData.dept_id = item.value
|
||||
showPickerDept.value = false
|
||||
}
|
||||
function handleRoleConfirm(e: PickerConfirmEvent) {
|
||||
const item = roleOptions.value.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
formData.role_ids = [item.value]
|
||||
showPickerRole.value = false
|
||||
}
|
||||
function handlePositionConfirm(e: PickerConfirmEvent) {
|
||||
const item = positionOptions.value.find(o => o.value === e.value[0])
|
||||
if (item)
|
||||
formData.position_ids = [item.value]
|
||||
showPickerPosition.value = false
|
||||
}
|
||||
|
||||
function flattenDept(items: DeptItem[], prefix = ''): { value: number, label: string }[] {
|
||||
let r: { value: number, label: string }[] = []
|
||||
for (const item of items) {
|
||||
r.push({ value: item.id, label: prefix + item.name })
|
||||
if (item.children?.length)
|
||||
r = r.concat(flattenDept(item.children, `${prefix} `))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
async function loadOptions() {
|
||||
try {
|
||||
const [depts, roles, positions] = await Promise.all([
|
||||
DeptAPI.getTree(),
|
||||
RoleAPI.getPage({ page_no: 1, page_size: 1000 }),
|
||||
PositionAPI.getPage({ page_no: 1, page_size: 1000 }),
|
||||
])
|
||||
deptOptions.value = flattenDept(depts || [])
|
||||
roleOptions.value = (roles.list || []).map((r: RoleItem) => ({ value: r.id, label: r.name || '' }))
|
||||
positionOptions.value = (positions.list || []).map((p: PositionItem) => ({ value: p.id, label: p.name || '' }))
|
||||
}
|
||||
catch {
|
||||
deptOptions.value = []
|
||||
roleOptions.value = []
|
||||
positionOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const { list, total, loading, pageParams, loadData, toFirst, loadNext } = useListPage<UserInfo>({
|
||||
fetcher: p => UserAPI.getUserPage({ ...p, name: searchName.value || undefined }),
|
||||
onError: () => toast.error('加载失败'),
|
||||
})
|
||||
|
||||
function handlePageChange({ value }: { value: number }) {
|
||||
pageParams.value.page_no = value
|
||||
loadData()
|
||||
}
|
||||
|
||||
function onSearch() {
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
function onReset() {
|
||||
searchName.value = ''
|
||||
toFirst()
|
||||
loadData()
|
||||
}
|
||||
|
||||
const initForm: UserForm = { username: '', name: '', password: '', dept_id: undefined, role_ids: [], position_ids: [], email: '', mobile: '', gender: 2, status: 1, description: '' }
|
||||
|
||||
function openCreate() {
|
||||
formTitle.value = '新增用户'
|
||||
currentId.value = undefined
|
||||
Object.assign(formData, { ...initForm })
|
||||
if (deptOptions.value.length === 0)
|
||||
loadOptions()
|
||||
showForm.value = true
|
||||
}
|
||||
function openEdit(id: number) {
|
||||
router.push({ name: 'work-user-detail', query: { id: String(id) } })
|
||||
}
|
||||
async function handleSubmit() {
|
||||
loading.value = true
|
||||
try {
|
||||
const payload = { ...formData }
|
||||
if (currentId.value) {
|
||||
await UserAPI.updateUser({ ...payload, id: currentId.value })
|
||||
toast.success('更新成功')
|
||||
}
|
||||
else {
|
||||
await UserAPI.addUser(payload)
|
||||
toast.success('创建成功')
|
||||
}
|
||||
showForm.value = false
|
||||
loadData()
|
||||
}
|
||||
catch { toast.error('操作失败') }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
function handleDelete(id: number) {
|
||||
uni.showModal({ title: '提示', content: '确认删除该用户?', success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
await UserAPI.deleteUser([id])
|
||||
toast.success('删除成功')
|
||||
loadData()
|
||||
}
|
||||
catch { toast.error('删除失败') }
|
||||
}
|
||||
} })
|
||||
}
|
||||
|
||||
// Avatar color generator
|
||||
const avatarColors = ['#014DB2', '#F59E0B', '#10B981', '#8B5CF6', '#EF4444', '#EC4899', '#06B6D4']
|
||||
function getAvatarColor(index: number) {
|
||||
return avatarColors[index % avatarColors.length]
|
||||
}
|
||||
function getAvatarInitial(name: string) {
|
||||
return (name || '?').charAt(0)
|
||||
}
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!loading.value)
|
||||
loadNext()
|
||||
})
|
||||
onPullDownRefresh(() => {
|
||||
loadData()
|
||||
})
|
||||
onLoad(() => loadData())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="page-wraper">
|
||||
<!-- Search bar -->
|
||||
<view class="search-bar">
|
||||
<view class="flex items-center gap-sm">
|
||||
<wd-input v-model="searchName" placeholder="搜索姓名或用户名" clearable class="flex-1" />
|
||||
<wd-button size="small" type="primary" variant="plain" @click="onSearch">
|
||||
搜索
|
||||
</wd-button>
|
||||
<wd-button size="small" variant="plain" @click="onReset">
|
||||
重置
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Meta bar -->
|
||||
<view class="action-bar">
|
||||
<text class="text-md text-muted font-bold">
|
||||
共 {{ total }} 条
|
||||
</text>
|
||||
<wd-button size="small" type="primary" @click="openCreate">
|
||||
+ 新增
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- List -->
|
||||
<SkeletonPage v-if="loading && list.length === 0" :rows="5" search />
|
||||
<template v-else>
|
||||
<view class="px-sm">
|
||||
<view class="admin-card">
|
||||
<wd-empty v-if="!loading && list.length === 0" tip="暂无用户" />
|
||||
<wd-cell-group v-else>
|
||||
<wd-cell v-for="(item, index) in list" :key="item.id" center @click="openEdit(item.id!)">
|
||||
<template #title>
|
||||
<view class="flex items-center gap-2">
|
||||
<wd-avatar
|
||||
size="40px"
|
||||
round
|
||||
:text="getAvatarInitial(item.name || item.username || '')"
|
||||
:bg-color="getAvatarColor(index)"
|
||||
color="#FFFFFF"
|
||||
/>
|
||||
<view class="min-w-0">
|
||||
<view class="truncate text-3.5 font-medium wot-text-text-main">
|
||||
{{ item.name || item.username }}
|
||||
</view>
|
||||
<view class="mt-1 truncate text-2.5 wot-text-text-auxiliary">
|
||||
{{ item.username }} · {{ item.email || '-' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
<template #default>
|
||||
<view class="flex items-center gap-2">
|
||||
<StatusBadge :status="item.status" />
|
||||
<wd-icon name="delete" size="18px" color="var(--danger-color)" @click.stop="handleDelete(item.id!)" />
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</wd-cell-group>
|
||||
</view>
|
||||
</view>
|
||||
<wd-pagination
|
||||
:model-value="pageParams.page_no"
|
||||
:total="total"
|
||||
:page-size="pageParams.page_size"
|
||||
button-variant="plain"
|
||||
hide-if-one-page
|
||||
@change="handlePageChange"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Form popup -->
|
||||
<wd-popup v-model="showForm" position="bottom" round custom-style="max-height: 80vh; overflow-y: auto;" @close="showForm = false">
|
||||
<view class="p-xl">
|
||||
<wd-navbar :title="formTitle" left-arrow @click-left="showForm = false" />
|
||||
<wd-form :model="formData" class="mt-lg">
|
||||
<wd-form-item label="用户名" border>
|
||||
<wd-input v-model="formData.username" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="姓名" border>
|
||||
<wd-input v-model="formData.name" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item v-if="!currentId" label="密码" border>
|
||||
<wd-input v-model="formData.password" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="部门" border>
|
||||
<view class="flex-1" @click="showPickerDept = true">
|
||||
<wd-cell :value="deptOptions.find(o => o.value === formData.dept_id)?.label || '请选择部门'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerDept" :columns="[deptOptions]" @confirm="handleDeptConfirm" @cancel="showPickerDept = false" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="角色" border>
|
||||
<view class="flex-1" @click="showPickerRole = true">
|
||||
<wd-cell :value="roleOptions.find(o => o.value === formData.role_ids?.[0])?.label || '请选择角色'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerRole" :columns="[roleOptions]" @confirm="handleRoleConfirm" @cancel="showPickerRole = false" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="岗位" border>
|
||||
<view class="flex-1" @click="showPickerPosition = true">
|
||||
<wd-cell :value="positionOptions.find(o => o.value === formData.position_ids?.[0])?.label || '请选择岗位'" is-link :border="false" />
|
||||
</view>
|
||||
<wd-picker :visible="showPickerPosition" :columns="[positionOptions]" @confirm="handlePositionConfirm" @cancel="showPickerPosition = false" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="性别" border>
|
||||
<wd-radio-group v-model="formData.gender">
|
||||
<wd-radio v-for="opt in GENDER_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="状态" border>
|
||||
<wd-radio-group v-model="formData.status">
|
||||
<wd-radio v-for="opt in STATUS_OPTIONS" :key="opt.value" :value="opt.value">
|
||||
{{ opt.label }}
|
||||
</wd-radio>
|
||||
</wd-radio-group>
|
||||
</wd-form-item>
|
||||
<wd-form-item label="邮箱" border>
|
||||
<wd-input v-model="formData.email" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="手机号" border>
|
||||
<wd-input v-model="formData.mobile" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
<wd-form-item label="备注" border>
|
||||
<wd-textarea v-model="formData.description" placeholder="请输入" />
|
||||
</wd-form-item>
|
||||
</wd-form>
|
||||
<view class="gap-md mt-xl flex">
|
||||
<wd-button variant="plain" block @click="showForm = false">
|
||||
取消
|
||||
</wd-button>
|
||||
<wd-button block type="primary" :loading="loading" @click="handleSubmit">
|
||||
保存
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
Reference in New Issue
Block a user