Files
medical-mall/pages/mall/admin/user/label.uvue

673 lines
14 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="user-label-page">
<view class="content-card">
<!-- 搜索与操作栏 -->
<view class="action-bar">
<view class="filter-item">
<input class="filter-input" v-model="searchText" placeholder="搜索标签名称/备注" @confirm="handleSearch" />
</view>
<button class="btn primary small" @click="onAddLabel">添加标签</button>
<button class="btn ghost small" @click="handleSearch">查询</button>
<button class="btn ghost small" @click="handleReset">重置</button>
</view>
<!-- 标签列表表格 -->
<view class="table-container">
<!-- 表头 -->
<view class="table-header">
<view class="col col-id"><text>ID</text></view>
<view class="col col-name"><text>标签名称</text></view>
<view class="col col-remark"><text>备注</text></view>
<view class="col col-status"><text>状态</text></view>
<view class="col col-ops"><text>操作</text></view>
</view>
<!-- 表格内容 -->
<view class="table-body">
<view v-if="loading" class="table-loading"><text>加载中...</text></view>
<view v-else-if="labelList.length === 0" class="table-empty"><text>暂无数据</text></view>
<view v-else v-for="(label, index) in labelList" :key="label.id" class="table-row">
<view class="col col-id"><text>{{ label.id }}</text></view>
<view class="col col-name">
<text :style="{ color: label.color || '#666' }">{{ label.name }}</text>
</view>
<view class="col col-remark"><text>{{ label.remark || '-' }}</text></view>
<view class="col col-status">
<view :class="['switch-box', label.status === 1 ? 'active' : '']" @click="onToggleStatus(index)">
<view class="switch-dot"></view>
</view>
</view>
<view class="col col-ops">
<text class="op-link" @click="onEditLabel(label)">修改</text>
<view class="op-divider">|</view>
<text class="op-link text-danger" @click="onDeleteLabel(label.id)">删除</text>
</view>
</view>
</view>
</view>
<!-- 分页区域 -->
<view class="pagination-row">
<text class="total-text">共 {{ total }} 条</text>
<view class="page-btns">
<view :class="['page-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"><text></text></view>
<view class="page-btn active"><text>{{ page }}</text></view>
<view :class="['page-btn', page >= totalPages ? 'disabled' : '']" @click="nextPage"><text></text></view>
</view>
<view class="page-jump">
<text>前往</text>
<input class="jump-input" v-model="jumpPage" @confirm="goToJumpPage" />
<text>页</text>
</view>
</view>
</view>
<!-- 添加/修改标签弹窗 -->
<view class="modal-mask" v-if="showModal" @click="closeModal">
<view class="modal-content" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ modalTitle }}</text>
<text class="modal-close" @click="closeModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<view class="label-box">
<text class="required">*</text>
<text class="label">标签名称:</text>
</view>
<input
class="form-input"
v-model="formData.name"
placeholder="请输入标签名称"
/>
</view>
<view class="form-item" style="margin-top: 20px;">
<view class="label-box">
<text class="label">标签颜色:</text>
</view>
<input
class="form-input"
v-model="formData.color"
placeholder="例如: #1890ff"
/>
</view>
<view class="form-item" style="margin-top: 20px;">
<view class="label-box">
<text class="label">备注说明:</text>
</view>
<textarea
class="form-textarea"
v-model="formData.remark"
placeholder="请输入备注说明"
/>
</view>
<view class="form-item" style="margin-top: 20px;">
<view class="label-box">
<text class="label">状态:</text>
</view>
<view :class="['switch-box', formData.status === 1 ? 'active' : '']" @click="formData.status = (formData.status === 1 ? 0 : 1)">
<view class="switch-dot"></view>
</view>
</view>
</view>
<view class="modal-footer">
<button class="btn ghost" @click="closeModal">取消</button>
<button class="btn primary" @click="submitForm">确定</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, computed, onMounted } from 'vue'
import {
fetchAdminUserLabelPage,
saveAdminUserLabel,
deleteAdminUserLabel,
setAdminUserLabelStatus,
type AdminUserLabel
} from '@/services/admin/userLabelService.uts'
const searchText = ref('')
const total = ref(0)
const labelList = ref<AdminUserLabel[]>([])
const loading = ref(false)
const page = ref(1)
const pageSize = ref(15)
const jumpPage = ref('')
const totalPages = computed((): number => {
if (pageSize.value <= 0) return 1
const pages = Math.ceil(total.value / pageSize.value)
return pages <= 0 ? 1 : pages
})
const loadLabelList = async () => {
loading.value = true
try {
const res = await fetchAdminUserLabelPage(page.value, pageSize.value, {
search: searchText.value || null,
status: null,
includeDeleted: false
})
labelList.value = res.items
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
onMounted(() => {
loadLabelList()
})
function handleSearch() {
page.value = 1
loadLabelList()
}
function handleReset() {
searchText.value = ''
page.value = 1
loadLabelList()
}
function prevPage() {
if (page.value > 1) {
page.value--
loadLabelList()
}
}
function nextPage() {
if (page.value < totalPages.value) {
page.value++
loadLabelList()
}
}
function goToJumpPage() {
const targetPage = parseInt(jumpPage.value)
if (!isNaN(targetPage) && targetPage >= 1 && targetPage <= totalPages.value) {
page.value = targetPage
loadLabelList()
jumpPage.value = ''
} else {
uni.showToast({ title: '请输入有效的页码', icon: 'none' })
}
}
// 弹窗状态
const showModal = ref(false)
const isEdit = ref(false)
const modalTitle = computed(() => isEdit.value ? '修改标签' : '添加标签')
// 表单数据
const formData = reactive({
id: '',
name: '',
color: '',
remark: '',
status: 1
})
function onAddLabel() {
isEdit.value = false
formData.id = ''
formData.name = ''
formData.color = ''
formData.remark = ''
formData.status = 1
showModal.value = true
}
function onEditLabel(label: AdminUserLabel) {
isEdit.value = true
formData.id = label.id
formData.name = label.name
formData.color = label.color ?? ''
formData.remark = label.remark ?? ''
formData.status = label.status
showModal.value = true
}
function onDeleteLabel(id: string) {
uni.showModal({
title: '提示',
content: '确定要删除该标签吗?',
success: async (res) => {
if (res.confirm) {
try {
const ok = await deleteAdminUserLabel(id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadLabelList()
} else {
uni.showToast({ title: '删除失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
}
async function onToggleStatus(index: number) {
const item = labelList.value[index]
const targetStatus = item.status === 1 ? 0 : 1
try {
const ok = await setAdminUserLabelStatus(item.id, targetStatus)
if (ok) {
item.status = targetStatus
uni.showToast({ title: '状态更新成功' })
} else {
uni.showToast({ title: '状态更新失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '状态更新失败', icon: 'none' })
}
}
function closeModal() {
showModal.value = false
}
async function submitForm() {
if (!formData.name) {
uni.showToast({ title: '请输入标签名称', icon: 'none' })
return
}
try {
const resId = await saveAdminUserLabel({
id: isEdit.value ? formData.id : null,
name: formData.name,
color: formData.color || null,
remark: formData.remark || null,
status: formData.status
})
if (resId != null) {
uni.showToast({ title: isEdit.value ? '修改成功' : '添加成功' })
closeModal()
loadLabelList()
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
} catch (e) {
uni.showToast({ title: '保存失败', icon: 'none' })
}
}
</script>
<style scoped lang="scss">
.user-label-page {
padding: 16px;
background-color: #f0f2f5;
min-height: 100vh;
}
.content-card {
background: #fff;
border-radius: 4px;
padding: 24px;
}
.action-bar {
margin-bottom: 20px;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
}
.filter-item {
display: flex;
flex-direction: row;
align-items: center;
}
.filter-input {
width: 200px;
height: 32px;
padding: 0 12px;
border: 1px solid #d9d9d9;
border-radius: 2px;
font-size: 14px;
background-color: #fff;
}
/* 按钮样式 */
.btn {
height: 32px;
line-height: 32px;
padding: 0 15px;
font-size: 14px;
border-radius: 2px;
border: 1px solid #d9d9d9;
background: #fff;
color: #666;
cursor: pointer;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: auto;
margin: 0;
}
.btn.primary {
background-color: #1890ff;
border-color: #1890ff;
color: #fff;
}
.btn.ghost {
background-color: #fff;
border-color: #d9d9d9;
color: #666;
}
.btn.small {
height: 32px;
padding: 0 12px;
}
/* 表格样式 */
.table-container {
border: 1px solid #f0f0f0;
border-radius: 2px;
overflow: hidden;
}
.table-header {
display: flex;
flex-direction: row;
background-color: #f8faff;
border-bottom: 1px solid #f0f0f0;
}
.table-header .col {
padding: 12px 16px;
font-weight: 500;
color: #333;
font-size: 14px;
}
.table-row {
display: flex;
flex-direction: row;
border-bottom: 1px solid #f0f0f0;
}
.table-row:last-child {
border-bottom: none;
}
.table-row .col {
padding: 12px 16px;
color: #666;
font-size: 14px;
display: flex;
flex-direction: row;
align-items: center;
}
.col-id {
width: 100px;
}
.col-name {
flex: 1;
}
.col-remark {
flex: 1.5;
}
.col-status {
width: 120px;
display: flex;
justify-content: center;
}
.col-ops {
width: 150px;
display: flex;
flex-direction: row;
justify-content: flex-end;
}
.table-loading,
.table-empty {
padding: 24px;
text-align: center;
color: #999;
font-size: 14px;
}
.op-link {
color: #1890ff;
cursor: pointer;
font-size: 14px;
}
.op-link:hover {
text-decoration: underline;
}
.op-divider {
margin: 0 8px;
color: #1890ff;
font-size: 12px;
opacity: 0.5;
}
.text-danger {
color: #ff4d4f;
}
/* Switch 开关 */
.switch-box {
width: 44px;
height: 22px;
background-color: #dcdfe6;
border-radius: 11px;
position: relative;
transition: background-color 0.3s;
cursor: pointer;
}
.switch-box.active {
background-color: #1890ff;
}
.switch-dot {
width: 18px;
height: 18px;
background-color: #fff;
border-radius: 9px;
position: absolute;
top: 2px;
left: 2px;
transition: transform 0.3s;
}
.switch-box.active .switch-dot {
transform: translateX(22px);
}
/* 分页样式 */
.pagination-row {
margin-top: 24px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
color: #666;
font-size: 14px;
}
.total-text {
margin-right: 16px;
}
.page-btns {
display: flex;
flex-direction: row;
margin-right: 16px;
}
.page-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #d9d9d9;
border-radius: 2px;
margin-right: 8px;
cursor: pointer;
&.active {
background: #1890ff;
border-color: #1890ff;
color: #fff;
}
&.disabled {
color: #ccc;
cursor: not-allowed;
}
}
.page-jump {
display: flex;
flex-direction: row;
align-items: center;
.jump-input {
width: 48px;
height: 32px;
border: 1px solid #d9d9d9;
border-radius: 2px;
margin: 0 8px;
text-align: center;
font-size: 14px;
}
}
/* 弹窗样式 */
.modal-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-content {
width: 520px;
background: #fff;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.modal-header {
padding: 16px 24px;
border-bottom: 1px solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-title {
font-size: 16px;
font-weight: 500;
color: #333;
}
.modal-close {
font-size: 20px;
color: #999;
cursor: pointer;
&:hover {
color: #666;
}
}
.modal-body {
padding: 40px 24px;
}
.form-item {
display: flex;
align-items: center;
}
.label-box {
width: 100px;
display: flex;
justify-content: flex-end;
align-items: center;
}
.required {
color: #ff4d4f;
margin-right: 4px;
}
.label {
color: #333;
font-size: 14px;
}
.form-input {
flex: 1;
height: 32px;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 2px;
font-size: 14px;
&:focus {
border-color: #40a9ff;
outline: none;
}
}
.form-textarea {
flex: 1;
height: 80px;
padding: 4px 11px;
border: 1px solid #d9d9d9;
border-radius: 2px;
font-size: 14px;
&:focus {
border-color: #40a9ff;
outline: none;
}
}
.modal-footer {
padding: 10px 16px;
border-top: 1px solid #f0f0f0;
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>