Files
medical-mall/pages/mall/admin/setting/auth/role.uvue

273 lines
10 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="admin-page-container">
<view class="page-card">
<!-- 搜索栏 -->
<view class="search-wrap">
<view class="search-item">
<text class="label">状态:</text>
<uni-data-select v-model="statusValue" :localdata="statusOptions" style="width: 120px;" @change="handleQuery" />
</view>
<view class="search-item">
<text class="label">身份昵称:</text>
<input class="input" placeholder="请输入身份昵称" v-model="searchKey" @confirm="handleQuery" />
</view>
<button class="btn btn-primary" @click="handleQuery">查询</button>
</view>
<view class="action-wrap">
<button class="btn btn-primary" @click="onAdd">添加身份</button>
</view>
<!-- 表格区域 -->
<view class="table-wrap border-shadow">
<view class="table-header">
<view class="th" style="flex: 1;">序号</view>
<view class="th" style="flex: 3;">身份昵称</view>
<view class="th" style="flex: 2;">状态</view>
<view class="th" style="flex: 2;">操作</view>
</view>
<view class="table-body">
<view v-if="loading" class="loading-box">
<text>加载中...</text>
</view>
<view v-else-if="roleList.length === 0" class="no-data">
<text class="no-data-text">暂无数据</text>
</view>
<view v-else v-for="(item, index) in roleList" :key="item.id" class="tr">
<view class="td" style="flex: 1;"><text class="td-txt">{{ (page - 1) * pageSize + index + 1 }}</text></view>
<view class="td" style="flex: 3;"><text class="td-txt">{{ item.name }} ({{ item.code }})</text></view>
<view class="td" style="flex: 2;">
<switch :checked="item.is_active" color="#1890ff" scale="0.7" @change="onToggleStatus(item)" />
</view>
<view class="td" style="flex: 2;">
<text class="action-btn" @click="onEdit(item)">编辑</text>
<view class="divider"></view>
<text class="action-btn danger" @click="onDelete(item)">删除</text>
</view>
</view>
</view>
</view>
<!-- 分页栏 -->
<view class="pagination-footer">
<text class="total-txt">共 {{ total }} 条</text>
<view class="page-btns">
<text :class="['p-btn', page <= 1 ? 'disabled' : '']" @click="prevPage"> < </text>
<text class="p-btn active">{{ page }}</text>
<text :class="['p-btn', roleList.length < pageSize ? 'disabled' : '']" @click="nextPage"> > </text>
</view>
</view>
</view>
<!-- 添加/编辑 弹窗 -->
<view v-if="showModal" class="modal-overlay" @click="closeModal">
<view class="modal-card" @click.stop>
<view class="modal-header">
<text class="modal-title">{{ isEdit ? '编辑身份' : '添加身份' }}</text>
<text class="close-btn" @click="closeModal">×</text>
</view>
<view class="modal-body">
<view class="form-item">
<text class="f-label">身份名称:</text>
<input class="f-input" v-model="form.name" placeholder="请输入身份名称" />
</view>
<view class="form-item">
<text class="f-label">身份编码:</text>
<input class="f-input" v-model="form.code" placeholder="如: super_admin" :disabled="isEdit" />
</view>
<view class="form-item">
<text class="f-label">描述:</text>
<textarea class="f-textarea" v-model="form.description" placeholder="请输入备注描述" />
</view>
</view>
<view class="modal-footer">
<button class="btn" @click="closeModal">取消</button>
<button class="btn btn-primary" @click="handleSave">保存</button>
</view>
</view>
</view>
</view>
</template>
<script setup lang="uts">
import { ref, reactive, onMounted } from 'vue'
import { fetchRolePage, saveRole, deleteRole, type AdminRole } from '@/services/admin/authService.uts'
const roleList = ref<AdminRole[]>([])
const loading = ref(false)
const total = ref(0)
const page = ref(1)
const pageSize = 15
const searchKey = ref('')
const statusValue = ref('all')
const statusOptions = [
{ value: 'all', text: '所有' },
{ value: '1', text: '启用' },
{ value: '0', text: '禁用' }
]
// 弹窗表单状态
const showModal = ref(false)
const isEdit = ref(false)
const form = reactive({
id: '' as string | null,
name: '',
code: '',
description: '',
is_active: true
})
onMounted(() => {
loadData()
})
async function loadData() {
loading.value = true
try {
const res = await fetchRolePage(page.value, pageSize, searchKey.value || null)
roleList.value = res.items
total.value = res.total
} catch (e) {
uni.showToast({ title: '加载失败', icon: 'none' })
} finally {
loading.value = false
}
}
function handleQuery() {
page.value = 1
loadData()
}
function onAdd() {
isEdit.value = false
form.id = null
form.name = ''
form.code = ''
form.description = ''
form.is_active = true
showModal.value = true
}
function onEdit(item : AdminRole) {
isEdit.value = true
form.id = item.id
form.name = item.name
form.code = item.code
form.description = item.description || ''
form.is_active = item.is_active
showModal.value = true
}
function closeModal() {
showModal.value = false
}
async function handleSave() {
if (!form.name || !form.code) {
uni.showToast({ title: '请完善必要信息', icon: 'none' })
return
}
loading.value = true
try {
const resId = await saveRole(form)
if (resId != null) {
uni.showToast({ title: '保存成功' })
closeModal()
loadData()
} else {
uni.showToast({ title: '保存失败', icon: 'none' })
}
} finally {
loading.value = false
}
}
async function onDelete(item : AdminRole) {
uni.showModal({
title: '删除确认',
content: `确定要删除角色 "${item.name}" 吗?\n\n⚠ 警告:该操作将同时解除所有关联管理员的身份并清空该角色的权限配置!`,
confirmText: '确认删除',
confirmColor: '#ff4d4f',
success: async (res) => {
if (res.confirm) {
try {
const ok = await deleteRole(item.id)
if (ok) {
uni.showToast({ title: '删除成功' })
loadData()
}
} catch (e: any) {
const errMsg = e?.message || '删除失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
}
}
})
}
async function onToggleStatus(item : AdminRole) {
const nextStatus = !item.is_active
const ok = await saveRole({ ...item, is_active: nextStatus })
if (ok != null) {
item.is_active = nextStatus
uni.showToast({ title: '状态已更新' })
}
}
function prevPage() { if (page.value > 1) { page.value--; loadData(); } }
function nextPage() { if (roleList.value.length >= pageSize) { page.value++; loadData(); } }
</script>
<style scoped lang="scss">
.admin-page-container { padding: 24px; background-color: #f5f7f9; min-height: 100vh; }
.page-card { background-color: #fff; border-radius: 4px; padding: 24px; }
.search-wrap { display: flex; flex-direction: row; align-items: center; padding-bottom: 24px; border-bottom: 1px solid #f0f0f0; margin-bottom: 24px; }
.search-item { display: flex; flex-direction: row; align-items: center; margin-right: 24px; }
.label { font-size: 14px; color: #606266; margin-right: 8px; }
.input { width: 200px; height: 32px; border: 1px solid #dcdfe6; border-radius: 4px; padding: 0 12px; font-size: 14px; }
.btn { height: 32px; padding: 0 20px; border-radius: 4px; border: none; cursor: pointer; display: flex; align-items: center; justify-content: center; }
.btn-primary { background-color: #1890ff; color: #fff; font-size: 14px; }
.action-wrap { margin-bottom: 20px; }
.table-wrap { border: 1px solid #f0f0f0; border-radius: 4px; }
.table-header { display: flex; flex-direction: row; background-color: #f8f8f9; }
.th { padding: 12px 10px; font-size: 14px; font-weight: bold; color: #515a6e; border-bottom: 1px solid #f0f0f0; text-align: center; }
.tr { display: flex; flex-direction: row; border-bottom: 1px solid #f0f0f0; min-height: 54px; align-items: center; }
.td { padding: 10px; display: flex; align-items: center; justify-content: center; }
.td-txt { font-size: 13px; color: #606266; }
.action-btn { color: #1890ff; font-size: 13px; cursor: pointer; }
.danger { color: #ff4d4f; }
.divider { width: 1px; height: 12px; background-color: #e8e8e8; margin: 0 8px; }
.no-data { padding: 60px 0; text-align: center; }
.no-data-text { font-size: 14px; color: #c5c8ce; }
.loading-box { padding: 60px 0; text-align: center; color: #1890ff; }
.pagination-footer { margin-top: 24px; display: flex; flex-direction: row; align-items: center; justify-content: flex-end; gap: 12px; }
.total-txt { font-size: 13px; color: #999; }
.page-btns { display: flex; flex-direction: row; gap: 8px; }
.p-btn { width: 30px; height: 30px; border: 1px solid #dcdee2; display: flex; align-items: center; justify-content: center; border-radius: 4px; cursor: pointer; font-size: 13px; }
.p-btn.active { background-color: #1890ff; color: #fff; border-color: #1890ff; }
.p-btn.disabled { opacity: 0.5; cursor: not-allowed; }
/* 弹窗样式 */
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: rgba(0,0,0,0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; }
.modal-card { width: 500px; background-color: #fff; border-radius: 8px; overflow: hidden; }
.modal-header { padding: 16px 20px; border-bottom: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: space-between; align-items: center; }
.modal-title { font-size: 16px; font-weight: bold; color: #333; }
.close-btn { font-size: 24px; color: #999; cursor: pointer; }
.modal-body { padding: 24px; }
.form-item { margin-bottom: 20px; display: flex; flex-direction: row; align-items: flex-start; }
.f-label { width: 90px; font-size: 14px; color: #666; text-align: right; padding-top: 6px; }
.f-input { flex: 1; border: 1px solid #dcdfe6; height: 36px; padding: 0 12px; border-radius: 4px; font-size: 14px; }
.f-textarea { flex: 1; border: 1px solid #dcdfe6; height: 80px; padding: 8px 12px; border-radius: 4px; font-size: 14px; }
.modal-footer { padding: 16px 24px; border-top: 1px solid #f0f0f0; display: flex; flex-direction: row; justify-content: flex-end; gap: 12px; }
</style>