init: 橘视厅酒店版会员系统前端

This commit is contained in:
zhaogd
2026-07-26 12:54:46 +08:00
commit 690568861b
243 changed files with 42311 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
<template>
<div class="mod-api-jst-call-log">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item label="设备 MAC">
<el-input v-model="dataForm.mac" placeholder="MAC 模糊" clearable style="width: 160px"></el-input>
</el-form-item>
<el-form-item label="创建时间区间">
<el-date-picker
v-model="createdAtRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
:default-time="createdAtRangeDefaultTime"
unlink-panels
clearable
style="width: 380px"
/>
</el-form-item>
<el-form-item label="接口路径">
<el-input v-model="dataForm.apiPath" placeholder="路径关键字,后端 LIKE 匹配" clearable style="width: 220px"></el-input>
</el-form-item>
<el-form-item label="请求体">
<el-input v-model="dataForm.requestBody" placeholder="请求体关键字,后端 LIKE 匹配" clearable style="width: 240px"></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
</el-form-item>
</el-form>
<div class="table-page-wrap">
<el-table
:data="dataList"
border
v-loading="dataListLoading"
style="width: 100%"
>
<el-table-column
prop="mac"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="设备 MAC"
/>
<el-table-column
prop="deviceSn"
header-align="center"
align="center"
width="140"
:show-overflow-tooltip="true"
label="序列号"
/>
<el-table-column
prop="deviceUid"
header-align="center"
align="center"
width="120"
:show-overflow-tooltip="true"
label="设备 UID"
/>
<el-table-column
prop="createTime"
header-align="center"
align="center"
width="170"
label="创建时间"
/>
<el-table-column
prop="apiPath"
header-align="center"
align="left"
min-width="200"
:show-overflow-tooltip="true"
label="接口路径"
/>
<el-table-column
prop="requestBody"
header-align="center"
align="left"
min-width="220"
:show-overflow-tooltip="true"
label="请求体 requestBody"
/>
<el-table-column
prop="responseBody"
header-align="center"
align="left"
min-width="220"
:show-overflow-tooltip="true"
label="请求结果"
/>
<el-table-column
prop="errorMessage"
header-align="center"
align="left"
min-width="220"
:show-overflow-tooltip="true"
label="错误信息"
/>
<el-table-column
prop="storeOrgName"
header-align="center"
align="center"
min-width="140"
:show-overflow-tooltip="true"
label="组织名称"
/>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper"
>
</el-pagination>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { getApiJstCallLogList } from '@/api/sys'
import type { ApiJstCallLogInfo } from '@/api/sys'
import type { PageParams, PageResult } from '@/types/api'
interface DataForm {
mac: string
apiPath: string
requestBody: string
}
type JstLogListResponsePayload = {
data?: { page?: PageResult<ApiJstCallLogInfo> } & Partial<PageResult<ApiJstCallLogInfo>>
page?: PageResult<ApiJstCallLogInfo>
}
const dataForm = reactive<DataForm>({
mac: '',
apiPath: '',
requestBody: ''
})
/** 选日期后默认起始 00:00:00、结束 23:59:59,避免只选同一天时区间过窄 */
const createdAtRangeDefaultTime: [Date, Date] = [
new Date(2000, 0, 1, 0, 0, 0),
new Date(2000, 0, 1, 23, 59, 59)
]
const createdAtRange = ref<[string, string] | null>(null)
const dataList = ref<ApiJstCallLogInfo[]>([])
const pageIndex = ref<number>(1)
const pageSize = ref<number>(10)
const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false)
/**
* GET /sys/apiJstCallLog/list 查询参数:
* - mac(模糊由后端 LIKE)
* - beginTime / endTime:创建时间闭区间(与 audit-log 一致)
* - apiPath:路径模糊(由后端 LIKE)
* - requestBody:请求体模糊(由后端 LIKE)
*/
function buildListParams (): PageParams {
const p: PageParams = { page: pageIndex.value, limit: pageSize.value }
const t = (s: string | null | undefined): string => (s != null && String(s).trim() !== '' ? String(s).trim() : '')
const mac = t(dataForm.mac)
if (mac) p.mac = mac
const apiPath = t(dataForm.apiPath)
if (apiPath) p.apiPath = apiPath
const requestBody = t(dataForm.requestBody)
if (requestBody) p.requestBody = requestBody
const r = createdAtRange.value
if (Array.isArray(r) && r.length === 2 && r[0] && r[1]) {
p.beginTime = r[0]
p.endTime = r[1]
}
return p
}
function getDataList (): void {
dataListLoading.value = true
getApiJstCallLogList(buildListParams()).then((data) => {
if (data && data.code === 0) {
const raw = data as JstLogListResponsePayload
const page = raw.data?.page ?? raw.page ?? raw.data
if (page && (Array.isArray(page.list) || page.totalCount != null)) {
dataList.value = page.list || []
totalPage.value = page.totalCount || 0
} else {
dataList.value = []
totalPage.value = 0
}
} else {
dataList.value = []
totalPage.value = 0
}
dataListLoading.value = false
})
}
function sizeChangeHandle (val: number): void {
pageSize.value = val
pageIndex.value = 1
getDataList()
}
function currentChangeHandle (val: number): void {
pageIndex.value = val
getDataList()
}
onMounted(() => {
getDataList()
})
</script>

View File

@@ -0,0 +1,211 @@
<template>
<div class="mod-audit-log">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item label="操作者">
<el-input v-model="dataForm.actorName" placeholder="按用户名模糊" clearable style="width: 180px"></el-input>
</el-form-item>
<el-form-item label="操作行为">
<el-input v-model="dataForm.head" placeholder="按操作行为模糊" clearable style="width: 200px"></el-input>
</el-form-item>
<el-form-item label="接口">
<el-input v-model="dataForm.api" placeholder="类#方法 等" clearable style="width: 220px"></el-input>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="createdAtRange"
type="datetimerange"
range-separator="至"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DD HH:mm:ss"
clearable
style="width: 360px"
/>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
</el-form-item>
</el-form>
<div class="table-page-wrap">
<el-table
:data="dataList"
border
v-loading="dataListLoading"
style="width: 100%">
<el-table-column
prop="id"
header-align="center"
align="center"
width="80"
label="ID">
</el-table-column>
<el-table-column
prop="name"
header-align="center"
align="center"
width="100"
label="操作者">
</el-table-column>
<el-table-column
prop="head"
header-align="center"
align="center"
min-width="160"
:show-overflow-tooltip="true"
label="操作行为">
</el-table-column>
<el-table-column
prop="createdAt"
header-align="center"
align="center"
width="170"
label="创建时间">
</el-table-column>
<el-table-column
prop="api"
header-align="center"
align="center"
min-width="200"
:show-overflow-tooltip="true"
label="接口">
</el-table-column>
<el-table-column
prop="param"
header-align="center"
align="left"
min-width="200"
:show-overflow-tooltip="true"
label="参数">
</el-table-column>
<el-table-column
prop="paramDesc"
header-align="center"
align="left"
min-width="200"
:show-overflow-tooltip="true"
label="参数描述">
</el-table-column>
<el-table-column
prop="result"
header-align="center"
align="center"
width="100"
:show-overflow-tooltip="true"
label="结果">
</el-table-column>
<el-table-column
prop="ip"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="IP">
</el-table-column>
<el-table-column
prop="deviceInfo"
header-align="center"
align="left"
min-width="180"
:show-overflow-tooltip="true"
label="客户端">
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { getAuditLogList } from '@/api/sys'
import type { AuditLogInfo } from '@/api/sys'
import type { PageParams, PageResult } from '@/types/api'
interface DataForm {
actorName: string
head: string
api: string
}
/** 列表接口可能返回根级 page,或包在 data / data.page 下 */
type AuditListResponsePayload = {
data?: { page?: PageResult<AuditLogInfo> } & Partial<PageResult<AuditLogInfo>>
page?: PageResult<AuditLogInfo>
}
const dataForm = reactive<DataForm>({
actorName: '',
head: '',
api: ''
})
const createdAtRange = ref<[string, string] | null>(null)
const dataList = ref<AuditLogInfo[]>([])
const pageIndex = ref<number>(1)
const pageSize = ref<number>(10)
const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false)
function buildListParams (): PageParams {
const p: PageParams = { page: pageIndex.value, limit: pageSize.value }
const t = (s: string | null | undefined): string => (s != null && String(s).trim() !== '' ? String(s).trim() : '')
const actor = t(dataForm.actorName)
if (actor) p.actorName = actor
const head = t(dataForm.head)
if (head) p.head = head
const api = t(dataForm.api)
if (api) p.api = api
const r = createdAtRange.value
if (Array.isArray(r) && r.length === 2 && r[0] && r[1]) {
p.beginTime = r[0]
p.endTime = r[1]
}
return p
}
function getDataList (): void {
dataListLoading.value = true
getAuditLogList(buildListParams()).then((data) => {
if (data && data.code === 0) {
// 兼容:{ code, page };或 { code, data: { page } };或 data 本体即分页字段
const raw = data as AuditListResponsePayload
const page = raw.data?.page ?? raw.page ?? raw.data
if (page && (Array.isArray(page.list) || page.totalCount != null)) {
dataList.value = page.list || []
totalPage.value = page.totalCount || 0
} else {
dataList.value = []
totalPage.value = 0
}
} else {
dataList.value = []
totalPage.value = 0
}
dataListLoading.value = false
})
}
function sizeChangeHandle (val: number): void {
pageSize.value = val
pageIndex.value = 1
getDataList()
}
function currentChangeHandle (val: number): void {
pageIndex.value = val
getDataList()
}
onMounted(() => {
getDataList()
})
</script>

View File

@@ -0,0 +1,108 @@
<template>
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
v-model="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataFormRef" @keyup.enter="dataFormSubmit()" label-width="80px">
<el-form-item label="参数名" prop="paramKey">
<el-input v-model="dataForm.paramKey" placeholder="参数名"></el-input>
</el-form-item>
<el-form-item label="参数值" prop="paramValue">
<el-input v-model="dataForm.paramValue" placeholder="参数值"></el-input>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="dataForm.remark" placeholder="备注"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, nextTick, useTemplateRef } from 'vue'
import { getConfigInfo, saveConfig } from '@/api/sys'
import { ElMessage, type FormInstance, type FormItemRule } from 'element-plus'
const emit = defineEmits<{
refreshDataList: []
}>()
const visible = ref<boolean>(false)
interface DataForm {
id: number | null
paramKey: string
paramValue: string
remark: string
}
const dataForm = reactive<DataForm>({
id: null,
paramKey: '',
paramValue: '',
remark: ''
})
const dataRule: Record<string, FormItemRule[]> = {
paramKey: [
{ required: true, message: '参数名不能为空', trigger: 'blur' }
],
paramValue: [
{ required: true, message: '参数值不能为空', trigger: 'blur' }
]
}
const dataFormRef = useTemplateRef<FormInstance>('dataFormRef')
function init (id?: number): void {
dataForm.id = id || null
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
if (dataForm.id) {
getConfigInfo(dataForm.id).then((data) => {
if (data && data.code === 0 && data.data) {
const config = data.data
dataForm.paramKey = config.paramKey
dataForm.paramValue = config.paramValue
dataForm.remark = config.remark || ''
}
})
}
})
}
// 表单提交
function dataFormSubmit (): void {
dataFormRef.value?.validate((valid) => {
if (valid) {
saveConfig({
id: dataForm.id || undefined,
paramKey: dataForm.paramKey,
paramValue: dataForm.paramValue,
remark: dataForm.remark
}).then((data) => {
if (data && data.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
}
})
} else {
ElMessage.error(data.msg)
}
})
}
})
}
defineExpose({ init })
</script>

View File

@@ -0,0 +1,315 @@
<template>
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
v-model="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataFormRef" @keyup.enter="dataFormSubmit()" label-width="80px">
<el-form-item label="类型" prop="type">
<el-radio-group v-model="dataForm.type">
<el-radio v-for="(type, index) in dataForm.typeList" :value="index" :key="index">{{ type }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="dataForm.typeList[dataForm.type] + '名称'" prop="name">
<el-input v-model="dataForm.name" :placeholder="dataForm.typeList[dataForm.type] + '名称'"></el-input>
</el-form-item>
<el-form-item label="上级菜单" prop="parentName">
<el-popover
ref="menuListPopoverRef"
placement="bottom-start"
trigger="click"
:width="300">
<template #reference>
<el-input v-model="dataForm.parentName" readonly placeholder="点击选择上级菜单" class="menu-list__input"></el-input>
</template>
<el-tree
:data="menuList"
:props="menuListTreeProps"
node-key="id"
ref="menuListTreeRef"
@current-change="menuListTreeCurrentChangeHandle"
:default-expand-all="true"
:highlight-current="true"
:expand-on-click-node="false">
</el-tree>
</el-popover>
</el-form-item>
<el-form-item v-if="dataForm.type === 1" label="菜单路由" prop="url">
<el-input v-model="dataForm.url" placeholder="菜单路由"></el-input>
</el-form-item>
<el-form-item v-if="dataForm.type !== 0" label="授权标识" prop="perms">
<el-input v-model="dataForm.perms" placeholder="多个用逗号分隔, 如: user:list,user:create"></el-input>
</el-form-item>
<el-form-item v-if="dataForm.type !== 2" label="排序号" prop="orderNum">
<el-input-number v-model="dataForm.orderNum" controls-position="right" :min="0" label="排序号"></el-input-number>
</el-form-item>
<el-form-item v-if="dataForm.type !== 2" label="是否展示" prop="show">
<el-radio-group v-model="dataForm.show">
<el-radio :value="10">展示</el-radio>
<el-radio :value="20">隐藏</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="dataForm.type !== 2" label="菜单图标" prop="icon">
<el-row>
<el-col :span="22">
<el-popover
ref="iconListPopoverRef"
placement="bottom-start"
trigger="click"
popper-class="mod-menu__icon-popover"
:width="458">
<template #reference>
<el-input :model-value="''" readonly placeholder="点击选择菜单图标" class="icon-list__input">
<template v-if="dataForm.icon && iconMap[dataForm.icon as keyof typeof iconMap]" #prefix>
<el-icon><component :is="iconMap[dataForm.icon as keyof typeof iconMap]" /></el-icon>
</template>
</el-input>
</template>
<div class="mod-menu__icon-inner">
<div class="mod-menu__icon-list">
<el-button
v-for="(iconName, index) in iconList"
:key="index"
@click="iconActiveHandle(iconName)"
:class="{ 'is-active': iconName === dataForm.icon }">
<el-icon :size="18">
<component :is="iconMap[iconName as keyof typeof iconMap]" />
</el-icon>
</el-button>
</div>
</div>
</el-popover>
</el-col>
<el-col :span="2" class="icon-list__tips">
<el-tooltip placement="top" effect="light">
<template #content>
使用 Element Plus Icons,保存的值为图标组件名
</template>
<span class="icon-list__tips-icon">?</span>
</el-tooltip>
</el-col>
</el-row>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, nextTick, useTemplateRef } from 'vue'
import { getMenuSelect, getMenuInfo, saveMenu } from '@/api/sys'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { treeDataTranslate } from '@/utils'
import { ElMessage, type FormInstance, type FormItemRule, type TreeInstance } from 'element-plus'
import type { MenuInfo } from '@/api/sys'
// Element Plus Icons 图标名列表(用于菜单图标选择)
const iconNames = Object.keys(ElementPlusIconsVue).filter(
key => key !== 'default' && (typeof ElementPlusIconsVue[key as keyof typeof ElementPlusIconsVue] === 'object' || typeof ElementPlusIconsVue[key as keyof typeof ElementPlusIconsVue] === 'function') && ElementPlusIconsVue[key as keyof typeof ElementPlusIconsVue] != null
)
const iconList = iconNames
const iconMap = ElementPlusIconsVue
const emit = defineEmits<{
refreshDataList: []
}>()
const visible = ref<boolean>(false)
interface DataForm {
id: number | null
type: number
typeList: string[]
name: string
parentId: number
parentName: string
url: string
perms: string
orderNum: number
icon: string
show: number
}
const dataForm = reactive<DataForm>({
id: null,
type: 1,
typeList: ['目录', '菜单', '按钮'],
name: '',
parentId: 0,
parentName: '',
url: '',
perms: '',
orderNum: 0,
icon: '',
show: 10
})
const validateUrl: FormItemRule['validator'] = (_rule, value, callback) => {
if (dataForm.type === 1 && !/\S/.test(value as string)) {
callback(new Error('菜单URL不能为空'))
} else {
callback()
}
}
const dataRule: Record<string, FormItemRule[]> = {
name: [
{ required: true, message: '菜单名称不能为空', trigger: 'blur' }
],
parentName: [
{ required: true, message: '上级菜单不能为空', trigger: 'change' }
],
url: [
{ validator: validateUrl, trigger: 'blur' }
]
}
const menuList = ref<any[]>([])
const menuListTreeProps = {
label: 'name',
children: 'children'
}
const dataFormRef = useTemplateRef<FormInstance>('dataFormRef')
const menuListTreeRef = useTemplateRef<TreeInstance>('menuListTreeRef')
function init (id?: number): void {
dataForm.id = id || null
getMenuSelect().then((data) => {
if (!data || data.code !== 0) return
const body = data as unknown as { data?: { menuList?: MenuInfo[] }; menuList?: MenuInfo[] }
const rawList = body.data?.menuList ?? body.menuList ?? []
menuList.value = treeDataTranslate(rawList, 'id')
}).then(() => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
})
}).then(() => {
if (!dataForm.id) {
// 新增
menuListTreeSetCurrentNode()
} else {
// 修改
getMenuInfo(dataForm.id).then((data) => {
if (!data || data.code !== 0) return
const body = data as unknown as { data?: MenuInfo; menu?: MenuInfo }
const menu = body.data ?? body.menu
if (menu && typeof menu === 'object') {
dataForm.id = menu.id
dataForm.type = menu.type ?? 1
dataForm.name = menu.name
dataForm.parentId = menu.parentId ?? 0
dataForm.url = menu.url || ''
dataForm.perms = menu.perms || ''
dataForm.orderNum = menu.orderNum ?? 0
dataForm.icon = menu.icon || ''
const rawShow = menu.show != null ? menu.show : (menu as any).isShow
dataForm.show = rawShow != null ? Number(rawShow) : 10
menuListTreeSetCurrentNode()
}
})
}
})
}
// 菜单树选中
function menuListTreeCurrentChangeHandle (data: MenuInfo): void {
dataForm.parentId = data.id
dataForm.parentName = data.name
}
// 菜单树设置当前选中节点
function menuListTreeSetCurrentNode (): void {
menuListTreeRef.value?.setCurrentKey(dataForm.parentId)
const currentNode = menuListTreeRef.value?.getCurrentNode()
dataForm.parentName = (currentNode as MenuInfo | undefined)?.name || ''
}
// 图标选中
function iconActiveHandle (iconName: string): void {
dataForm.icon = iconName
}
// 表单提交
function dataFormSubmit (): void {
dataFormRef.value?.validate((valid) => {
if (valid) {
saveMenu({
id: dataForm.id || undefined,
type: dataForm.type,
name: dataForm.name,
parentId: dataForm.parentId,
url: dataForm.url,
perms: dataForm.perms,
orderNum: dataForm.orderNum,
icon: dataForm.icon,
show: dataForm.show
}).then((data) => {
if (data && data.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
}
})
} else {
ElMessage.error(data.msg)
}
})
}
})
}
defineExpose({ init })
</script>
<style lang="scss">
.mod-menu {
.menu-list__input,
.icon-list__input {
> .el-input__inner {
cursor: pointer;
}
}
&__icon-popover {
width: 458px;
overflow: hidden;
}
&__icon-inner {
width: 478px;
max-height: 258px;
overflow-x: hidden;
overflow-y: auto;
}
&__icon-list {
width: 458px;
padding: 0;
margin: -8px 0 0 -8px;
> .el-button {
padding: 8px;
margin: 8px 0 0 8px;
display: inline-flex;
align-items: center;
gap: 6px;
> .el-icon {
flex-shrink: 0;
}
}
}
.icon-list__tips {
font-size: 18px;
text-align: center;
color: #e6a23c;
cursor: pointer;
.icon-list__tips-icon { font-weight: bold; }
}
}
</style>

View File

@@ -0,0 +1,158 @@
<template>
<el-dialog
:title="!dataForm.id ? '新增' : '修改'"
:close-on-click-modal="false"
v-model="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataFormRef" @keyup.enter="dataFormSubmit()" label-width="80px">
<el-form-item label="角色名称" prop="roleName">
<el-input v-model="dataForm.roleName" placeholder="角色名称"></el-input>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="dataForm.remark" placeholder="备注"></el-input>
</el-form-item>
<el-form-item size="small" label="授权">
<el-tree
:data="menuList"
:props="menuListTreeProps"
node-key="id"
ref="menuListTreeRef"
:default-expand-all="true"
show-checkbox>
</el-tree>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, nextTick, useTemplateRef } from 'vue'
import { getMenuList, getRoleInfo, saveRole } from '@/api/sys'
import { ElMessage, type FormInstance, type FormItemRule, type TreeInstance } from 'element-plus'
import { treeDataTranslate } from '@/utils'
import type { RoleInfo } from '@/api/sys'
const emit = defineEmits<{
refreshDataList: []
}>()
const visible = ref<boolean>(false)
const menuList = ref<any[]>([])
const menuListTreeProps = {
label: 'name',
children: 'children'
}
interface DataForm {
id: number | null
roleName: string
remark: string
}
const dataForm = reactive<DataForm>({
id: null,
roleName: '',
remark: ''
})
const dataRule: Record<string, FormItemRule[]> = {
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' }
]
}
const tempKey = -666666 // 临时key, 用于解决tree半选中状态项不能传给后台接口问题. # 待优化
const dataFormRef = useTemplateRef<FormInstance>('dataFormRef')
const menuListTreeRef = useTemplateRef<TreeInstance>('menuListTreeRef')
function init (id?: number): void {
dataForm.id = id || null
getMenuList().then((resp) => {
if (!resp || resp.code !== 0) return
const r = resp as unknown as Record<string, any>
const dRaw = r.data
const d =
dRaw != null &&
typeof dRaw === 'object' &&
!Array.isArray(dRaw)
? (dRaw as Record<string, any>)
: ({} as Record<string, any>)
let list: any[] = []
if (Array.isArray(d.menuList)) list = d.menuList
else if (Array.isArray(r.menuList)) list = r.menuList
else if (d.page?.list) list = d.page.list
else if (Array.isArray(d.list)) list = d.list
else if (Array.isArray(r.list)) list = r.list
else {
const bucket = d.page ?? d
list = Array.isArray(bucket) ? bucket : bucket?.list ?? []
}
menuList.value = treeDataTranslate(list, 'id', 'parentId')
}).then(() => {
visible.value = true
nextTick(() => {
dataFormRef.value?.resetFields()
menuListTreeRef.value?.setCheckedKeys([])
})
}).then(() => {
if (dataForm.id) {
getRoleInfo(dataForm.id).then((body) => {
if (!body || body.code !== 0) return
const env = body as unknown as {
data?: RoleInfo & { menuIdList?: number[] }
role?: RoleInfo & { menuIdList?: number[] }
}
const rolePayload = env.data ?? env.role
if (!rolePayload || typeof rolePayload !== 'object') return
const role = rolePayload as RoleInfo & { menuIdList: number[] }
dataForm.roleName = role.roleName || ''
dataForm.remark = role.remark || ''
const menuIdList = Array.isArray(role.menuIdList) ? role.menuIdList : []
const idx = menuIdList.indexOf(tempKey)
if (idx !== -1) {
menuIdList.splice(idx, menuIdList.length - idx)
}
menuIdList.forEach((mid: number) => {
menuListTreeRef.value?.setChecked(mid, true, false)
})
})
}
})
}
function dataFormSubmit (): void {
dataFormRef.value?.validate((valid) => {
if (valid) {
const checkedKeys = (menuListTreeRef.value?.getCheckedKeys() || []) as number[]
const halfCheckedKeys = (menuListTreeRef.value?.getHalfCheckedKeys() || []) as number[]
saveRole({
id: dataForm.id || undefined,
roleName: dataForm.roleName,
remark: dataForm.remark,
menuIdList: [...checkedKeys, ...halfCheckedKeys].filter((id: number) => id !== tempKey)
}).then((data) => {
if (data && data.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
}
})
} else {
ElMessage.error(data.msg)
}
})
}
})
}
defineExpose({ init })
</script>

View File

@@ -0,0 +1,332 @@
<template>
<el-dialog
:title="!dataForm.id ? '添加用户' : '修改'"
:close-on-click-modal="false"
v-model="visible">
<el-form :model="dataForm" :rules="dataRule" ref="dataFormRef" @keyup.enter="dataFormSubmit()" label-width="100px">
<template v-if="!dataForm.id">
<el-form-item label="归属方式" prop="bindScopeType">
<el-radio-group v-model="dataForm.bindScopeType" @change="onBindScopeChange">
<el-radio value="channel_agent">渠道 / 代理商</el-radio>
<el-radio value="store">门店</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
v-if="dataForm.bindScopeType === 'channel_agent'"
label="所属组织"
prop="selectedOrgId">
<org-tree-select
v-model="dataForm.selectedOrgId"
:hide-node-types="orgTreeHideTypes"
height="320px"
@change="onOrgTreeChange" />
</el-form-item>
<el-form-item v-else label="所属门店" prop="storeOrgId">
<store-select
v-model="dataForm.storeOrgId"
width="100%"
@change="onStoreSelectChange" />
</el-form-item>
</template>
<el-form-item label="用户名" prop="userName">
<el-input v-model="dataForm.userName" placeholder="登录帐号"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" :class="{ 'is-required': !dataForm.id }">
<el-input
v-model="dataForm.password"
type="password"
:placeholder="dataForm.id ? '不改请留空' : '密码'"
autocomplete="new-password"></el-input>
<div v-if="!dataForm.id" class="form-tip" style="margin-top: 4px;">最短8位,且须同时包含小写、大写、数字、特殊字符4种</div>
<div v-else class="form-tip" style="margin-top: 4px;">不修改密码请留空</div>
</el-form-item>
<el-form-item label="确认密码" prop="comfirmPassword" :class="{ 'is-required': !dataForm.id }">
<el-input
v-model="dataForm.comfirmPassword"
type="password"
:placeholder="dataForm.id ? '不改请留空' : '确认密码'"
autocomplete="new-password"></el-input>
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="dataForm.email" placeholder="选填"></el-input>
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input v-model="dataForm.mobile" placeholder="选填"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="dataFormSubmit()">确定</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { reactive, ref, nextTick } from 'vue'
import { getUserInfo, saveUser, type UserInfo } from '@/api/sys'
import { isEmail, isMobile } from '@/utils/validate'
import { ElMessage, type FormInstance, type FormItemRule } from 'element-plus'
import OrgTreeSelect from '@/components/org-tree-select/index.vue'
import StoreSelect from '@/components/store-select/index.vue'
const emit = defineEmits<{
refreshDataList: []
}>()
interface OrgTreePayload {
orgId: string | number | null
channelOrgId?: string | number | null
agentOrgId?: string | number | null
}
/** 组织树仅展示渠道/代理商,不包含门店与平台根 */
const orgTreeHideTypes = ['PLATFORM', 'STORE']
const visible = ref<boolean>(false)
const dataFormRef = ref<FormInstance | null>(null)
const orgBindings = reactive<OrgTreePayload>({
orgId: null,
channelOrgId: null,
agentOrgId: null
})
interface DataForm {
id: number
bindScopeType: 'channel_agent' | 'store'
selectedOrgId: string | number | undefined
storeOrgId: string | number | undefined
userName: string
password: string
comfirmPassword: string
salt: string
email: string
mobile: string
status: number
}
const dataForm = reactive<DataForm>({
id: 0,
bindScopeType: 'channel_agent',
selectedOrgId: undefined,
storeOrgId: undefined,
userName: '',
password: '',
comfirmPassword: '',
salt: '',
email: '',
mobile: '',
status: 1
})
function onOrgTreeChange (payload: OrgTreePayload): void {
orgBindings.orgId = payload.orgId
orgBindings.channelOrgId = payload.channelOrgId
orgBindings.agentOrgId = payload.agentOrgId
nextTick(() => {
dataFormRef.value?.validateField('selectedOrgId')
})
}
function onBindScopeChange (): void {
dataForm.selectedOrgId = undefined
dataForm.storeOrgId = undefined
orgBindings.orgId = null
orgBindings.channelOrgId = null
orgBindings.agentOrgId = null
nextTick(() => {
dataFormRef.value?.clearValidate(['selectedOrgId', 'storeOrgId'])
})
}
function onStoreSelectChange (): void {
nextTick(() => {
dataFormRef.value?.validateField('storeOrgId')
})
}
function validatePasswordStrength (password: string): { valid: boolean; message?: string } {
if (!password || password.length < 8) return { valid: false, message: '密码长度不能少于8位' }
const hasLower = /[a-z]/.test(password)
const hasUpper = /[A-Z]/.test(password)
const hasDigit = /[0-9]/.test(password)
const hasSpecial = /[^A-Za-z0-9]/.test(password)
if (!hasLower) return { valid: false, message: '密码须包含小写字母' }
if (!hasUpper) return { valid: false, message: '密码须包含大写字母' }
if (!hasDigit) return { valid: false, message: '密码须包含数字' }
if (!hasSpecial) return { valid: false, message: '密码须包含特殊字符' }
return { valid: true }
}
const validatePassword: FormItemRule['validator'] = (_rule, value, callback) => {
const v = typeof value === 'string' ? value.trim() : ''
if (!dataForm.id && !v) {
callback(new Error('密码不能为空'))
} else if (!dataForm.id && v) {
const r = validatePasswordStrength(v)
if (!r.valid) return callback(new Error(r.message))
callback()
} else if (dataForm.id && v) {
const r = validatePasswordStrength(v)
if (!r.valid) return callback(new Error(r.message))
callback()
} else {
callback()
}
}
const validateComfirmPassword: FormItemRule['validator'] = (_rule, value, callback) => {
const confirm = typeof value === 'string' ? value.trim() : ''
const pwd = typeof dataForm.password === 'string' ? dataForm.password.trim() : ''
if (!dataForm.id && !confirm) {
callback(new Error('确认密码不能为空'))
} else if (!pwd && confirm) {
callback(new Error('请先填写密码'))
} else if (pwd !== confirm) {
callback(new Error('确认密码与密码输入不一致'))
} else {
callback()
}
}
const validateEmail: FormItemRule['validator'] = (_rule, value, callback) => {
if (!value || !String(value).trim()) {
return callback()
}
if (!isEmail(value as string)) {
callback(new Error('邮箱格式错误'))
} else {
callback()
}
}
const validateMobile: FormItemRule['validator'] = (_rule, value, callback) => {
if (!value || !String(value).trim()) {
return callback()
}
if (!isMobile(value as string)) {
callback(new Error('手机号格式错误'))
} else {
callback()
}
}
const validateChannelAgentOrg: FormItemRule['validator'] = (_rule, value, callback) => {
if (dataForm.id || dataForm.bindScopeType !== 'channel_agent') return callback()
if (value == null || value === '') {
return callback(new Error('请选择渠道或代理商'))
}
callback()
}
const validateStoreOrg: FormItemRule['validator'] = (_rule, value, callback) => {
if (dataForm.id || dataForm.bindScopeType !== 'store') return callback()
if (value == null || value === '') {
return callback(new Error('请选择门店'))
}
callback()
}
const dataRule: Record<string, FormItemRule[]> = {
selectedOrgId: [{ validator: validateChannelAgentOrg, trigger: 'change' }],
storeOrgId: [{ validator: validateStoreOrg, trigger: 'change' }],
userName: [{ required: true, message: '用户名不能为空', trigger: 'blur' }],
password: [{ validator: validatePassword, trigger: 'blur' }],
comfirmPassword: [{ validator: validateComfirmPassword, trigger: 'blur' }],
email: [{ validator: validateEmail, trigger: 'blur' }],
mobile: [{ validator: validateMobile, trigger: 'blur' }]
}
function init (id?: number | string | null): void {
dataForm.id = id ? Number(id) : 0
visible.value = true
nextTick(() => {
if (dataFormRef.value && dataFormRef.value.clearValidate) {
dataFormRef.value.clearValidate()
}
dataForm.bindScopeType = 'channel_agent'
dataForm.selectedOrgId = undefined
dataForm.storeOrgId = undefined
orgBindings.orgId = null
orgBindings.channelOrgId = null
orgBindings.agentOrgId = null
dataForm.userName = ''
dataForm.password = ''
dataForm.comfirmPassword = ''
dataForm.salt = ''
dataForm.email = ''
dataForm.mobile = ''
dataForm.status = 1
})
if (id !== null && id !== undefined && id !== '') {
getUserInfo(Number(id)).then((body) => {
if (!body || body.code !== 0) return
const env = body as unknown as { data?: UserInfo; user?: UserInfo }
const user = env.data ?? env.user
if (!user || typeof user !== 'object') return
dataForm.id = user.id
dataForm.userName = user.username || ''
dataForm.salt = user.salt || ''
dataForm.email = user.email || ''
dataForm.mobile = user.mobile || ''
dataForm.status = user.status != null ? Number(user.status) : 1
dataForm.password = ''
dataForm.comfirmPassword = ''
})
}
}
function dataFormSubmit (): void {
if (!dataFormRef.value) return
dataFormRef.value.validate((valid) => {
if (!valid) return
const pwdTrim = typeof dataForm.password === 'string' ? dataForm.password.trim() : ''
const payload: Record<string, any> = {
id: dataForm.id || undefined,
username: dataForm.userName,
salt: dataForm.salt,
email: dataForm.email,
mobile: dataForm.mobile,
status: dataForm.status
}
if (dataForm.id) {
if (pwdTrim) payload.password = pwdTrim
} else {
payload.password = pwdTrim
}
if (!dataForm.id) {
if (dataForm.bindScopeType === 'channel_agent') {
payload.orgId = orgBindings.orgId ?? dataForm.selectedOrgId
if (orgBindings.channelOrgId != null) {
payload.channelOrgId = orgBindings.channelOrgId
}
if (orgBindings.agentOrgId) {
payload.agentOrgId = orgBindings.agentOrgId
}
} else {
payload.orgId = dataForm.storeOrgId
}
}
saveUser(payload).then((res) => {
if (res && res.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
visible.value = false
emit('refreshDataList')
}
})
} else {
ElMessage.error(res?.msg || '请求失败')
}
})
})
}
defineExpose({ init })
</script>

168
src/views/modules/sys/config.vue Executable file
View File

@@ -0,0 +1,168 @@
<template>
<div class="mod-config">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item>
<el-input v-model="dataForm.paramKey" placeholder="参数名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-button type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>
</el-form-item>
</el-form>
<div class="table-page-wrap">
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%;">
<el-table-column
type="selection"
header-align="center"
align="center"
width="50">
</el-table-column>
<el-table-column
prop="id"
header-align="center"
align="center"
width="80"
label="ID">
</el-table-column>
<el-table-column
prop="paramKey"
header-align="center"
align="center"
label="参数名">
</el-table-column>
<el-table-column
prop="paramValue"
header-align="center"
align="center"
label="参数值">
</el-table-column>
<el-table-column
prop="remark"
header-align="center"
align="center"
label="备注">
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作">
<template #default="scope">
<el-button link type="primary" size="small" @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
<el-button link type="danger" size="small" @click="deleteHandle(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdateRef" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onActivated, nextTick, useTemplateRef } from 'vue'
import AddOrUpdate from './components/config-add-or-update.vue'
import { getConfigList, deleteConfig } from '@/api/sys'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { ConfigInfo } from '@/api/sys'
interface DataForm {
paramKey: string
}
const dataForm = reactive<DataForm>({
paramKey: ''
})
const dataList = ref<ConfigInfo[]>([])
const pageIndex = ref<number>(1)
const pageSize = ref<number>(10)
const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false)
const dataListSelections = ref<ConfigInfo[]>([])
const addOrUpdateVisible = ref<boolean>(false)
const addOrUpdateRef = useTemplateRef<InstanceType<typeof AddOrUpdate>>('addOrUpdateRef')
function getDataList (): void {
dataListLoading.value = true
getConfigList({
page: pageIndex.value,
limit: pageSize.value,
paramKey: dataForm.paramKey
}).then((data) => {
if (data && data.code === 0 && data.data) {
const page = data.data.page || data.data
dataList.value = page.list || []
totalPage.value = page.totalCount || 0
} else {
dataList.value = []
totalPage.value = 0
}
dataListLoading.value = false
})
}
function sizeChangeHandle (val: number): void {
pageSize.value = val
pageIndex.value = 1
getDataList()
}
function currentChangeHandle (val: number): void {
pageIndex.value = val
getDataList()
}
function selectionChangeHandle (val: ConfigInfo[]): void {
dataListSelections.value = val
}
async function addOrUpdateHandle (id?: number): Promise<void> {
addOrUpdateVisible.value = true
await nextTick()
addOrUpdateRef.value?.init(id)
}
function deleteHandle (id?: number): void {
const ids = id ? [id] : dataListSelections.value.map(item => item.id)
ElMessageBox.confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteConfig(ids as number[]).then((data) => {
if (data && data.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
getDataList()
}
})
} else {
ElMessage.error(data.msg)
}
})
}).catch(() => {})
}
onActivated(() => {
getDataList()
})
</script>

138
src/views/modules/sys/log.vue Executable file
View File

@@ -0,0 +1,138 @@
<template>
<div class="mod-log">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item>
<el-input v-model="dataForm.key" placeholder="用户名/用户操作" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
</el-form-item>
</el-form>
<div class="table-page-wrap">
<el-table
:data="dataList"
border
v-loading="dataListLoading"
style="width: 100%">
<el-table-column
prop="id"
header-align="center"
align="center"
width="80"
label="ID">
</el-table-column>
<el-table-column
prop="username"
header-align="center"
align="center"
label="用户名">
</el-table-column>
<el-table-column
prop="operation"
header-align="center"
align="center"
label="用户操作">
</el-table-column>
<el-table-column
prop="method"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="请求方法">
</el-table-column>
<el-table-column
prop="params"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="请求参数">
</el-table-column>
<el-table-column
prop="time"
header-align="center"
align="center"
label="执行时长(毫秒)">
</el-table-column>
<el-table-column
prop="ip"
header-align="center"
align="center"
width="150"
label="IP地址">
</el-table-column>
<el-table-column
prop="createDate"
header-align="center"
align="center"
width="180"
label="创建时间">
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { getLogList } from '@/api/sys'
import type { LogInfo } from '@/api/sys'
interface DataForm {
key: string
}
const dataForm = reactive<DataForm>({
key: ''
})
const dataList = ref<LogInfo[]>([])
const pageIndex = ref<number>(1)
const pageSize = ref<number>(10)
const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false)
function getDataList (): void {
dataListLoading.value = true
getLogList({
page: pageIndex.value,
limit: pageSize.value,
key: dataForm.key
}).then((data) => {
if (data && data.code === 0 && data.data) {
const page = data.data.page || data.data
dataList.value = page.list || []
totalPage.value = page.totalCount || 0
} else {
dataList.value = []
totalPage.value = 0
}
dataListLoading.value = false
})
}
function sizeChangeHandle (val: number): void {
pageSize.value = val
pageIndex.value = 1
getDataList()
}
function currentChangeHandle (val: number): void {
pageIndex.value = val
getDataList()
}
onMounted(() => {
getDataList()
})
</script>

165
src/views/modules/sys/menu.vue Executable file
View File

@@ -0,0 +1,165 @@
<template>
<div class="mod-menu">
<el-form :inline="true" :model="dataForm">
<el-form-item>
<el-button type="primary" @click="addOrUpdateHandle()">新增</el-button>
</el-form-item>
</el-form>
<el-table
:data="dataList"
row-key="id"
border
style="width: 100%; ">
<el-table-column
prop="name"
header-align="center"
min-width="150"
label="名称" >
</el-table-column>
<el-table-column
prop="parentName"
header-align="center"
align="center"
width="120"
label="上级菜单">
</el-table-column>
<el-table-column
header-align="center"
align="center"
label="图标">
<template #default="scope">
<el-icon v-if="scope.row.icon && iconMap[scope.row.icon as keyof typeof iconMap]">
<component :is="iconMap[scope.row.icon as keyof typeof iconMap]" />
</el-icon>
<i v-else-if="scope.row.icon" :class="'el-icon-' + scope.row.icon"></i>
</template>
</el-table-column>
<el-table-column
prop="type"
header-align="center"
align="center"
label="类型">
<template #default="scope">
<el-tag v-if="scope.row.type === 0" size="small">目录</el-tag>
<el-tag v-else-if="scope.row.type === 1" size="small" type="success">菜单</el-tag>
<el-tag v-else-if="scope.row.type === 2" size="small" type="info">按钮</el-tag>
</template>
</el-table-column>
<el-table-column
prop="orderNum"
header-align="center"
align="center"
label="排序号">
</el-table-column>
<el-table-column
prop="show"
header-align="center"
align="center"
width="90"
label="是否展示">
<template #default="scope">
<el-tag v-if="scope.row.show == '10'" size="small" type="success">展示</el-tag>
<el-tag v-else size="small" type="info">隐藏</el-tag>
</template>
</el-table-column>
<el-table-column
prop="url"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="菜单URL">
</el-table-column>
<el-table-column
prop="perms"
header-align="center"
align="center"
width="150"
:show-overflow-tooltip="true"
label="授权标识">
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作">
<template #default="scope">
<el-button link type="primary" size="small" @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
<el-button link type="danger" size="small" @click="deleteHandle(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdateRef" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, nextTick, useTemplateRef } from 'vue'
import AddOrUpdate from './components/menu-add-or-update.vue'
import { getMenuList, deleteMenu } from '@/api/sys'
import { treeDataTranslate } from '@/utils'
import { ElMessage, ElMessageBox } from 'element-plus'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const iconMap = ElementPlusIconsVue
const dataForm = reactive<Record<string, any>>({})
const dataList = ref<any[]>([])
const dataListLoading = ref<boolean>(false)
const addOrUpdateVisible = ref<boolean>(false)
const addOrUpdateRef = useTemplateRef<InstanceType<typeof AddOrUpdate>>('addOrUpdateRef')
function getDataList (): void {
dataListLoading.value = true
getMenuList().then((data) => {
if (data && data.code === 0 && data.data) {
const d = data.data as Record<string, any>
let list: any[] = []
if (Array.isArray(d.menuList)) list = d.menuList
else if (d.page?.list) list = d.page.list
else if (Array.isArray(d.list)) list = d.list
else {
const bucket = d.page ?? d
list = Array.isArray(bucket) ? bucket : bucket?.list ?? []
}
dataList.value = treeDataTranslate(list, 'id')
}
dataListLoading.value = false
})
}
async function addOrUpdateHandle (id?: number): Promise<void> {
addOrUpdateVisible.value = true
await nextTick()
addOrUpdateRef.value?.init(id)
}
function deleteHandle (id: number): void {
ElMessageBox.confirm(`确定对[id=${id}]进行[删除]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteMenu(id).then((data) => {
if (data && data.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
getDataList()
}
})
} else {
ElMessage.error(data.msg)
}
})
}).catch(() => {})
}
onMounted(() => {
getDataList()
})
</script>

194
src/views/modules/sys/role.vue Executable file
View File

@@ -0,0 +1,194 @@
<template>
<div class="mod-role">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item>
<el-input v-model="dataForm.roleName" placeholder="角色名称" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<el-button type="primary" @click="addOrUpdateHandle()">新增</el-button>
<el-tooltip content="选中角色中存在子集时不允许删除" :disabled="!(dataListSelections.length > 0 && dataListSelections.some(item => idsWithChildren.has(item.id)))">
<span class="pl-3">
<el-button type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0 || dataListSelections.some(item => idsWithChildren.has(item.id))">批量删除</el-button>
</span>
</el-tooltip>
</el-form-item>
</el-form>
<div class="table-page-wrap">
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%;">
<el-table-column
type="selection"
header-align="center"
align="center"
width="50">
</el-table-column>
<el-table-column
prop="id"
header-align="center"
align="center"
width="80"
label="ID">
</el-table-column>
<el-table-column
prop="roleName"
header-align="center"
align="center"
label="角色名称">
</el-table-column>
<el-table-column
prop="remark"
header-align="center"
align="center"
label="备注">
</el-table-column>
<el-table-column
prop="createdAt"
header-align="center"
align="center"
width="180"
label="创建时间">
</el-table-column>
<el-table-column
prop="updatedAt"
header-align="center"
align="center"
width="180"
label="更新时间">
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作">
<template #default="scope">
<el-button link type="primary" size="small" @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
<el-tooltip content="存在子集时不允许删除" :disabled="!idsWithChildren.has(scope.row.id)">
<span>
<el-button link type="danger" size="small" :disabled="idsWithChildren.has(scope.row.id)" @click="deleteHandle(scope.row.id)">删除</el-button>
</span>
</el-tooltip>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
<!-- 弹窗, 新增 / 修改 -->
<add-or-update v-if="addOrUpdateVisible" ref="addOrUpdateRef" @refreshDataList="getDataList"></add-or-update>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, nextTick, useTemplateRef } from 'vue'
import AddOrUpdate from './components/role-add-or-update.vue'
import { getRoleList, deleteRole } from '@/api/sys'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { RoleInfo } from '@/api/sys'
interface DataForm {
roleName: string
}
const dataForm = reactive<DataForm>({
roleName: ''
})
const dataList = ref<RoleInfo[]>([])
const pageIndex = ref<number>(1)
const pageSize = ref<number>(10)
const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false)
const dataListSelections = ref<RoleInfo[]>([])
const addOrUpdateVisible = ref<boolean>(false)
const addOrUpdateRef = useTemplateRef<InstanceType<typeof AddOrUpdate>>('addOrUpdateRef')
const idsWithChildren = ref<Set<number | string>>(new Set())
function getDataList (): void {
dataListLoading.value = true
getRoleList({
page: pageIndex.value,
limit: pageSize.value,
roleName: dataForm.roleName
}).then((data) => {
if (data && data.code === 0 && data.data) {
const page = data.data.page || data.data
dataList.value = page.list || []
totalPage.value = page.totalCount || 0
const list = dataList.value || []
const parentIds = list.map(r => r.parentId).filter(p => p != null && p !== '')
idsWithChildren.value = new Set(parentIds)
} else {
dataList.value = []
totalPage.value = 0
idsWithChildren.value = new Set()
}
dataListLoading.value = false
})
}
function sizeChangeHandle (val: number): void {
pageSize.value = val
pageIndex.value = 1
getDataList()
}
function currentChangeHandle (val: number): void {
pageIndex.value = val
getDataList()
}
function selectionChangeHandle (val: RoleInfo[]): void {
dataListSelections.value = val
}
async function addOrUpdateHandle (id?: number): Promise<void> {
addOrUpdateVisible.value = true
await nextTick()
addOrUpdateRef.value?.init(id)
}
function deleteHandle (id?: number): void {
const ids = id ? [id] : dataListSelections.value.map(item => item.id)
const withChildren = ids.filter(id => idsWithChildren.value.has(id))
if (withChildren.length > 0) {
ElMessage.warning('以下角色存在子集,不允许删除:' + withChildren.join('、'))
return
}
ElMessageBox.confirm(`确定对[id=${ids.join(',')}]进行[${id ? '删除' : '批量删除'}]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteRole(ids as number[]).then((data) => {
if (data && data.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
getDataList()
}
})
} else {
ElMessage.error(data.msg)
}
})
}).catch(() => {})
}
onMounted(() => {
getDataList()
})
</script>

185
src/views/modules/sys/user.vue Executable file
View File

@@ -0,0 +1,185 @@
<template>
<div class="mod-user">
<el-form :inline="true" :model="dataForm" @keyup.enter="getDataList()">
<el-form-item>
<el-input v-model="dataForm.userName" placeholder="用户名" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="addHandle()">添加用户</el-button>
</el-form-item>
</el-form>
<div class="table-page-wrap">
<el-table
:data="dataList"
border
v-loading="dataListLoading"
style="width: 100%;">
<el-table-column
prop="index"
header-align="center"
align="center"
width="80"
label="序号">
</el-table-column>
<el-table-column
prop="username"
header-align="center"
align="center"
label="用户名">
</el-table-column>
<el-table-column
prop="orgName"
header-align="center"
align="center"
label="所属组织">
</el-table-column>
<el-table-column
prop="mobile"
header-align="center"
align="center"
label="电话">
</el-table-column>
<el-table-column
prop="status"
header-align="center"
align="center"
label="状态">
<template #default="scope">
<el-tag v-if="scope.row.status === 0" size="small" type="danger">禁用</el-tag>
<el-tag v-else size="small">启用</el-tag>
</template>
</el-table-column>
<el-table-column
header-align="center"
align="center"
width="180"
label="创建时间">
<template #default="scope">
{{ scope.row.createdAt || '' }}
</template>
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作">
<template #default="scope">
<el-button link type="primary" size="small" @click="addOrUpdateHandle(scope.row.id)">修改</el-button>
<el-button link type="danger" size="small" @click="deleteHandle(scope.row.id)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[10, 20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper">
</el-pagination>
</div>
<!-- 弹窗, 新增 / 修改 -->
<AddOrUpdate
v-show="addOrUpdateVisible"
ref="addOrUpdateRef"
@refreshDataList="getDataList"></AddOrUpdate>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, nextTick, useTemplateRef } from 'vue'
import AddOrUpdate from './components/user-add-or-update.vue'
import { getUserList, deleteUser } from '@/api/sys'
import { ElMessage, ElMessageBox } from 'element-plus'
import type { UserInfo } from '@/api/sys'
interface DataForm {
userName: string
}
const dataForm = reactive<DataForm>({
userName: ''
})
const dataList = ref<UserInfo[]>([])
const pageIndex = ref<number>(1)
const pageSize = ref<number>(10)
const totalPage = ref<number>(0)
const dataListLoading = ref<boolean>(false)
const addOrUpdateVisible = ref<boolean>(false)
const addOrUpdateRef = useTemplateRef<InstanceType<typeof AddOrUpdate>>('addOrUpdateRef')
function getDataList (): void {
dataListLoading.value = true
getUserList({
page: pageIndex.value,
limit: pageSize.value,
username: dataForm.userName
}).then((data) => {
if (data && data.code === 0 && data.data) {
const page = data.data.page || data.data
dataList.value = page.list || []
totalPage.value = page.totalCount || 0
} else {
dataList.value = []
totalPage.value = 0
}
dataListLoading.value = false
})
}
function sizeChangeHandle (val: number): void {
pageSize.value = val
pageIndex.value = 1
getDataList()
}
function currentChangeHandle (val: number): void {
pageIndex.value = val
getDataList()
}
async function addOrUpdateHandle (id: number): Promise<void> {
addOrUpdateVisible.value = true
await nextTick()
addOrUpdateRef.value?.init(id)
}
async function addHandle (): Promise<void> {
addOrUpdateVisible.value = true
await nextTick()
addOrUpdateRef.value?.init(null)
}
function deleteHandle (id: number): void {
ElMessageBox.confirm(`确定对[id=${id}]进行[删除]操作?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteUser([id]).then((data) => {
if (data && data.code === 0) {
ElMessage.success({
message: '操作成功',
duration: 1500,
onClose: () => {
getDataList()
}
})
} else {
ElMessage.error(data.msg)
}
})
}).catch(() => {})
}
onMounted(() => {
getDataList()
})
</script>