重构组合式api

This commit is contained in:
WUSIJIAN
2025-11-25 17:02:31 +08:00
parent 428989c993
commit 4d6f49598e
16 changed files with 2213 additions and 1631 deletions

View File

@@ -1,6 +1,65 @@
import request from '/@/utils/request'; import request from '/@/utils/request';
/** export function getRoleList(query: Object) {
* 登录api接口集合 return request({
* @method signIn 用户登录 url: '/api/v1/system/role/list',
*/ method: 'get',
params: query,
});
}
export function getRoleParams() {
return request({
url: '/api/v1/system/role/getParams',
method: 'get',
});
}
export function addRole(data: object) {
return request({
url: '/api/v1/system/role/add',
method: 'post',
data: data,
});
}
export function getRole(id: number) {
return request({
url: '/api/v1/system/role/get',
method: 'get',
params: { id },
});
}
export function editRole(data: object) {
return request({
url: '/api/v1/system/role/edit',
method: 'put',
data: data,
});
}
export function deleteRole(id: number) {
return request({
url: '/api/v1/system/role/delete',
method: 'delete',
data: { ids: [id] },
});
}
export function dataScope(data: any) {
return request({
url: '/api/v1/system/role/dataScope',
method: 'put',
data: data,
});
}
// 根据角色ID查询部门树结构
export function roleDeptTreeSelect(roleId: number) {
return request({
url: '/api/v1/system/role/deptTreeSelect',
method: 'get',
params: { roleId },
});
}

View File

@@ -0,0 +1,65 @@
import request from '/@/utils/request';
export function getRoleList(query: Object) {
return request({
url: '/api/v1/system/role/list',
method: 'get',
params: query,
});
}
export function getRoleParams() {
return request({
url: '/api/v1/system/role/getParams',
method: 'get',
});
}
export function addRole(data: object) {
return request({
url: '/api/v1/system/role/add',
method: 'post',
data: data,
});
}
export function getRole(id: number) {
return request({
url: '/api/v1/system/role/get',
method: 'get',
params: { id },
});
}
export function editRole(data: object) {
return request({
url: '/api/v1/system/role/edit',
method: 'put',
data: data,
});
}
export function deleteRole(id: number) {
return request({
url: '/api/v1/system/role/delete',
method: 'delete',
data: { ids: [id] },
});
}
export function dataScope(data: any) {
return request({
url: '/api/v1/system/role/dataScope',
method: 'put',
data: data,
});
}
// 根据角色ID查询部门树结构
export function roleDeptTreeSelect(roleId: number) {
return request({
url: '/api/v1/system/role/deptTreeSelect',
method: 'get',
params: { roleId },
});
}

View File

@@ -0,0 +1,24 @@
import request from '/@/utils/request';
export function logList(query: object) {
return request({
url: '/api/v1/system/loginLog/list',
method: 'get',
params: query,
});
}
export function deleteLog(ids: number[]) {
return request({
url: '/api/v1/system/loginLog/delete',
method: 'delete',
params: { ids },
});
}
export function clearLog() {
return request({
url: '/api/v1/system/loginLog/clear',
method: 'delete',
});
}

View File

@@ -0,0 +1,65 @@
import request from '/@/utils/request';
export function getRoleList(query: Object) {
return request({
url: '/api/v1/system/role/list',
method: 'get',
params: query,
});
}
export function getRoleParams() {
return request({
url: '/api/v1/system/role/getParams',
method: 'get',
});
}
export function addRole(data: object) {
return request({
url: '/api/v1/system/role/add',
method: 'post',
data: data,
});
}
export function getRole(id: number) {
return request({
url: '/api/v1/system/role/get',
method: 'get',
params: { id },
});
}
export function editRole(data: object) {
return request({
url: '/api/v1/system/role/edit',
method: 'put',
data: data,
});
}
export function deleteRole(id: number) {
return request({
url: '/api/v1/system/role/delete',
method: 'delete',
data: { ids: [id] },
});
}
export function dataScope(data: any) {
return request({
url: '/api/v1/system/role/dataScope',
method: 'put',
data: data,
});
}
// 根据角色ID查询部门树结构
export function roleDeptTreeSelect(roleId: number) {
return request({
url: '/api/v1/system/role/deptTreeSelect',
method: 'get',
params: { roleId },
});
}

View File

@@ -1,6 +1,5 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ThemeConfigStates, ThemeConfigState } from './interface'; import { ThemeConfigStates, ThemeConfigState } from './interface';
import { tr } from 'element-plus/es/locale';
/** /**
* 布局配置 * 布局配置
@@ -20,7 +19,7 @@ export const useThemeConfig = defineStore('themeConfig', {
* 全局主题 * 全局主题
*/ */
// 默认 primary 主题颜色 // 默认 primary 主题颜色
primary: '#409eff', primary: '#FF2442',
// 是否开启深色模式 // 是否开启深色模式
isIsDark: false, isIsDark: false,
@@ -33,21 +32,22 @@ export const useThemeConfig = defineStore('themeConfig', {
// 默认顶栏导航背景颜色 // 默认顶栏导航背景颜色
topBar: '#ffffff', topBar: '#ffffff',
// 默认顶栏导航字体颜色 // 默认顶栏导航字体颜色
topBarColor: '#606266', topBarColor: '#1A1A1A',
// 是否开启顶栏背景颜色渐变 // 是否开启顶栏背景颜色渐变
isTopBarColorGradual: false, isTopBarColorGradual: true,
// 默认菜单导航背景颜色 // 默认菜单导航背景颜色
menuBar: '#354E67', menuBar: '#F0F8FF',
// 默认菜单导航字体颜色 // 默认菜单导航字体颜色
menuBarColor: '#eaeaea', menuBarColor: '#2D3748',
// 是否开启菜单背景颜色渐变 // 是否开启菜单背景颜色渐变
isMenuBarColorGradual: false, isMenuBarColorGradual: true,
// 默认分栏菜单背景颜色 // 默认分栏菜单背景颜色
columnsMenuBar: '#545c64', columnsMenuBar: '#F8FAFF',
// 默认分栏菜单字体颜色 // 默认分栏菜单字体颜色
columnsMenuBarColor: '#e6e6e6', columnsMenuBarColor: '#4A5568',
// 是否开启分栏菜单背景颜色渐变 // 是否开启分栏菜单背景颜色渐变
isColumnsMenuBarColorGradual: false, isColumnsMenuBarColorGradual: true,
/** /**
* 界面设置 * 界面设置
*/ */

View File

@@ -1,268 +1,319 @@
<template> <template>
<div class="system-edit-role-container"> <div class="system-edit-role-container">
<el-dialog :title="(formData.id === 0 ? '添加' : '修改') + '角色'" v-model="isShowDialog" width="769px"> <el-dialog :title="(formData.id === 0 ? '添加' : '修改') + '客服账号'" v-model="isShowDialog" width="769px">
<el-form ref="formRef" :model="formData" :rules="rules" size="default" label-width="90px"> <el-form ref="formRef" :model="formData" :rules="rules" size="default" label-width="90px">
<el-row :gutter="35"> <el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20"> <!-- 客服ID输入框 -->
<el-form-item label="客服id" prop="name"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-input placeholder="987698789" clearable></el-input> <el-form-item label="客服ID" prop="name">
</el-form-item> <el-input v-model="formData.name" placeholder="请输入客服ID987698789" clearable />
</el-col> </el-form-item>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20"> </el-col>
<el-form-item label="客服平台" prop="name">
<el-input placeholder="小红书" clearable></el-input>
</el-form-item>
</el-col>
<!-- <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="排序">
<el-input-number v-model="formData.listOrder" :min="0" controls-position="right" placeholder="请输入排序"
class="w100" />
</el-form-item>
</el-col> -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="客服状态">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" inline-prompt active-text="启"
inactive-text=""></el-switch>
</el-form-item>
</el-col>
<!-- <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="角色描述">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入角色描述" maxlength="150"></el-input>
</el-form-item>
</el-col> -->
<!-- <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="菜单权限">
<el-row :gutter="35">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event)">展开/折叠</el-checkbox>
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event)">全选/全不选</el-checkbox>
<el-checkbox v-model="menuCheckStrictly" @change="handleCheckedTreeConnect($event)">父子联动</el-checkbox>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-tree :data="menuData" ref="menuRef" :props="menuProps" :default-checked-keys="formData.menuIds"
node-key="id" show-checkbox class="menu-data-tree tree-border"
:check-strictly="!menuCheckStrictly" />
</el-col>
</el-row>
</el-form-item>
</el-col> -->
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSubmit" size="default" :loading="loading">{{ formData.id === 0 ? '新 增' :
'修改' }}</el-button>
</span>
</template>
</el-dialog>
</div> <!-- 客服平台输入框 -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="客服平台" prop="platform">
<el-input v-model="formData.platform" placeholder="请输入客服平台,如:小红书" clearable />
</el-form-item>
</el-col>
<!-- 客服状态开关 -->
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="客服状态">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" inline-prompt active-text="启用" inactive-text="禁用" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<!-- 对话框底部操作按钮 -->
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSubmit" size="default" :loading="loading">
{{ formData.id === 0 ? '新 增' : '修 改' }}
</el-button>
</span>
</template>
</el-dialog>
</div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { reactive, toRefs, defineComponent, ref, getCurrentInstance, unref } from 'vue'; import { ref, reactive, toRefs, nextTick, defineEmits, defineExpose } from 'vue';
import { addRole, editRole, getRole, getRoleParams } from "/@/api/system/role"; import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { ElMessage } from "element-plus"; import { addRole, editRole, getRole } from '/@/api/system/role';
import { refreshBackEndControlRoutes } from "/@/router/backEnd";
/**
// 定义接口来定义对象的类型 * 菜单数据结构接口
*/
interface MenuDataTree { interface MenuDataTree {
id: number; id: number;
pid: number; pid: number;
title: string; title: string;
children?: MenuDataTree[]; children?: MenuDataTree[];
}
interface DialogRow {
id: number;
name: string;
status: number;
listOrder: number;
remark: string;
menuIds: Array<number>
}
interface RoleState {
loading: boolean;
isShowDialog: boolean;
formData: DialogRow;
menuData: Array<MenuDataTree>;
menuExpand: boolean;
menuNodeAll: boolean;
menuCheckStrictly: boolean;
menuProps: {
children: string;
label: string;
};
rules: object;
} }
export default defineComponent({ /**
name: 'systemEditRole', * 表单数据接口
setup(props, { emit }) { */
const { proxy } = getCurrentInstance() as any; interface DialogFormData {
const formRef = ref<HTMLElement | null>(null); id: number;
const menuRef = ref(); name: string; // 客服ID
const state = reactive<RoleState>({ platform: string; // 客服平台
loading: false, status: number; // 状态0-禁用1-启用
isShowDialog: false, listOrder: number; // 排序
formData: { remark: string; // 备注
id: 0, menuIds: number[]; // 菜单ID数组
name: '', }
status: 1,
listOrder: 0,
remark: '',
menuIds: []
},
// 表单校验
rules: {
name: [
{ required: true, message: "角色名称不能为空", trigger: "blur" },
]
},
menuData: [],
menuExpand: false,
menuNodeAll: false,
menuCheckStrictly: false,
menuProps: {
children: 'children',
label: 'title',
},
});
// 打开弹窗
const openDialog = (row?: DialogRow) => {
resetForm();
getMenuData();
if (row) {
getRole(row.id).then((res: any) => {
if (res.data.role) {
state.formData = res.data.role;
state.formData.menuIds = res.data.menuIds ?? []
}
})
}
state.isShowDialog = true;
};
// 关闭弹窗
const closeDialog = () => {
state.isShowDialog = false;
};
// 取消
const onCancel = () => {
closeDialog();
};
// 新增
const onSubmit = () => {
const formWrap = unref(formRef) as any;
if (!formWrap) return;
formWrap.validate((valid: boolean) => {
if (valid) {
state.loading = true;
state.formData.menuIds = getMenuAllCheckedKeys();
if (state.formData.id === 0) {
//添加
addRole(state.formData).then(() => {
ElMessage.success('角色添加成功');
closeDialog(); // 关闭弹窗
resetMenuSession()
emit('getRoleList')
}).finally(() => {
state.loading = false;
})
} else {
//修改
editRole(state.formData).then(() => {
ElMessage.success('角色修改成功');
closeDialog(); // 关闭弹窗
resetMenuSession()
emit('getRoleList')
}).finally(() => {
state.loading = false;
})
}
}
});
};
// 获取菜单结构数据
const getMenuData = () => {
getRoleParams().then((res: any) => {
state.menuData = proxy.handleTree(res.data.menu, "id", "pid");
})
};
const resetForm = () => {
state.menuCheckStrictly = false;
state.menuExpand = false;
state.menuNodeAll = false;
state.formData = {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
menuIds: []
}
};
/** 树权限(展开/折叠)*/
const handleCheckedTreeExpand = (value: any) => {
let treeList = state.menuData;
for (let i = 0; i < treeList.length; i++) {
menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
}
}
/** 树权限(全选/全不选) */ /**
const handleCheckedTreeNodeAll = (value: any) => { * 组件状态接口
menuRef.value.setCheckedNodes(value ? state.menuData : []); */
} interface ComponentState {
loading: boolean; // 提交加载状态
isShowDialog: boolean; // 对话框显示状态
formData: DialogFormData; // 表单数据
menuData: MenuDataTree[]; // 菜单数据
menuExpand: boolean; // 菜单展开状态
menuNodeAll: boolean; // 菜单全选状态
menuCheckStrictly: boolean; // 菜单严格选择模式
}
/** 树权限(父子联动) */ // 定义组件事件
const handleCheckedTreeConnect = (value: any) => { const emit = defineEmits<{
state.menuCheckStrictly = value ? true : false; /**
} * 获取角色列表事件
*/
(e: 'getRoleList'): void;
}>();
/** 所有菜单节点数据 */ // 响应式状态管理
function getMenuAllCheckedKeys() { const state = reactive<ComponentState>({
// 目前被选中的菜单节点 loading: false,
let checkedKeys = menuRef.value.getCheckedKeys(); isShowDialog: false,
// 半选中的菜单节点 formData: {
let halfCheckedKeys = menuRef.value.getHalfCheckedKeys(); id: 0,
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys); name: '',
return checkedKeys; platform: '',
} status: 1,
listOrder: 0,
remark: '',
menuIds: [],
},
menuData: [],
menuExpand: false,
menuNodeAll: false,
menuCheckStrictly: false,
});
// 重置菜单session // 表单验证规则
const resetMenuSession = () => { const rules: FormRules = {
refreshBackEndControlRoutes(); name: [
}; { required: true, message: '客服ID不能为空', trigger: 'blur' },
return { { min: 3, max: 20, message: '客服ID长度在 3 到 20 个字符', trigger: 'blur' },
openDialog, ],
closeDialog, platform: [{ required: true, message: '客服平台不能为空', trigger: 'blur' }],
onCancel, };
onSubmit,
menuRef, // 模板引用
formRef, const formRef = ref<FormInstance>();
handleCheckedTreeExpand,
handleCheckedTreeNodeAll, // 解构状态数据,便于模板使用
handleCheckedTreeConnect, const { loading, isShowDialog, formData, menuData, menuExpand, menuNodeAll, menuCheckStrictly } = toRefs(state);
resetMenuSession,
...toRefs(state), // 菜单树配置
}; const menuProps = {
}, children: 'children',
label: 'title',
};
/**
* 打开对话框
* @param row - 可选的编辑数据,如果传入则为编辑模式,否则为新增模式
*/
const openDialog = async (row?: DialogFormData) => {
// 重置表单状态
resetForm();
try {
if (row && row.id) {
// 编辑模式:获取角色详情
await handleEditMode(row.id);
}
// 显示对话框
state.isShowDialog = true;
} catch (error) {
console.error('打开对话框失败:', error);
ElMessage.error('初始化数据失败');
}
};
/**
* 处理编辑模式的数据获取
* @param id - 角色ID
*/
const handleEditMode = async (id: number) => {
try {
const res = await getRole(id);
if (res.data?.role) {
// 填充表单数据
state.formData = {
...res.data.role,
platform: res.data.role.platform || '',
menuIds: res.data.menuIds ?? [],
};
}
} catch (error) {
console.error('获取角色详情失败:', error);
throw new Error('获取角色详情失败');
}
};
/**
* 关闭对话框
*/
const closeDialog = () => {
state.isShowDialog = false;
};
/**
* 取消操作
*/
const onCancel = () => {
closeDialog();
};
/**
* 提交表单
*/
const onSubmit = async () => {
// 验证表单引用是否存在
if (!formRef.value) {
ElMessage.error('表单引用不存在');
return;
}
try {
// 表单验证
const valid = await formRef.value.validate();
if (!valid) {
ElMessage.warning('请完善表单信息');
return;
}
// 设置加载状态
state.loading = true;
// 根据ID判断是新增还是修改
if (state.formData.id === 0) {
// 新增操作
await handleAddRole();
} else {
// 修改操作
await handleEditRole();
}
// 关闭对话框并刷新列表
closeDialog();
emit('getRoleList');
ElMessage.success(state.formData.id === 0 ? '添加成功' : '修改成功');
} catch (error) {
console.error('提交失败:', error);
ElMessage.error(state.formData.id === 0 ? '添加失败' : '修改失败');
} finally {
// 无论成功失败,都取消加载状态
state.loading = false;
}
};
/**
* 处理新增角色
*/
const handleAddRole = async () => {
await addRole(state.formData);
};
/**
* 处理编辑角色
*/
const handleEditRole = async () => {
await editRole(state.formData);
};
/**
* 重置表单
*/
const resetForm = () => {
// 重置菜单选择状态
state.menuCheckStrictly = false;
state.menuExpand = false;
state.menuNodeAll = false;
// 重置表单数据
state.formData = {
id: 0,
name: '',
platform: '',
status: 1,
listOrder: 0,
remark: '',
menuIds: [],
};
// 在下一个DOM更新周期后清除表单验证
nextTick(() => {
formRef.value?.clearValidate();
});
};
// 暴露方法给父组件使用
defineExpose({
/**
* 打开对话框方法
*/
openDialog,
/**
* 关闭对话框方法
*/
closeDialog,
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.tree-border { .system-edit-role-container {
margin-top: 5px; // 对话框内容区域样式
border: 1px solid #e5e6e7 !important; :deep(.el-dialog__body) {
border-radius: 4px; padding: 20px;
}
// 底部按钮区域样式
.dialog-footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
}
} }
.system-edit-role-container { // 间距工具类
.menu-data-tree { .mb20 {
border: var(--el-input-border, var(--el-border-base)); margin-bottom: 20px;
border-radius: var(--el-input-border-radius, var(--el-border-radius-base)); }
padding: 5px;
} // 树形组件边框样式
.tree-border {
margin-top: 5px;
border: 1px solid #e5e6e7;
border-radius: 4px;
}
// 菜单数据树样式
.menu-data-tree {
border: var(--el-input-border, var(--el-border-base));
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
padding: 5px;
} }
</style> </style>

View File

@@ -7,14 +7,14 @@
<el-input size="default" v-model="tableData.param.roleName" placeholder="请输入客服ID" class="w-50 m-2" clearable /> <el-input size="default" v-model="tableData.param.roleName" placeholder="请输入客服ID" class="w-50 m-2" clearable />
</el-form-item> </el-form-item>
<el-form-item label="客服平台" prop="status" style="width: 200px"> <el-form-item label="客服平台" prop="status" style="width: 200px">
<el-select placeholder="小红书" clearable size="default" style="width: 240px"> <el-select v-model="tableData.param.platform" placeholder="请选择客服平台" clearable size="default" style="width: 240px">
<el-option :label="'小红书'" :value="''" /> <el-option label="小红书" value="xiaohongshu" />
<el-option :label="'抖音'" :value="''" /> <el-option label="抖音" value="douyin" />
<el-option :label="'快手'" :value="''" /> <el-option label="快手" value="kuaishou" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button size="default" type="primary" class="ml10" @click="roleList"> <el-button size="default" type="primary" class="ml10" @click="handleSearch">
<el-icon> <el-icon>
<ele-Search /> <ele-Search />
</el-icon> </el-icon>
@@ -29,9 +29,9 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<el-table :data="tableData.data" style="width: 100%"> <el-table :data="tableData.data" style="width: 100%" v-loading="tableData.loading">
<el-table-column type="index" label="序号" width="60" /> <el-table-column type="index" label="序号" width="60" />
<el-table-column prop="name" label="客服id" show-overflow-tooltip></el-table-column> <el-table-column prop="name" label="客服ID" show-overflow-tooltip></el-table-column>
<el-table-column prop="status" label="客服状态" show-overflow-tooltip> <el-table-column prop="status" label="客服状态" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
<el-tag type="success" v-if="scope.row.status === 1">启用</el-tag> <el-tag type="success" v-if="scope.row.status === 1">启用</el-tag>
@@ -39,18 +39,18 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="客服平台" show-overflow-tooltip></el-table-column> <el-table-column prop="remark" label="客服平台" show-overflow-tooltip></el-table-column>
<el-table-column prop="key" label="创建人" show-overflow-tooltip></el-table-column> <el-table-column prop="creator" label="创建人" show-overflow-tooltip></el-table-column>
<el-table-column prop="key" label="修改人" show-overflow-tooltip></el-table-column> <el-table-column prop="updater" label="修改人" show-overflow-tooltip></el-table-column>
<el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip></el-table-column> <el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip></el-table-column>
<el-table-column prop="createdAt" label="修改时间" show-overflow-tooltip></el-table-column> <el-table-column prop="updatedAt" label="修改时间" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="220"> <el-table-column label="操作" width="220">
<template #default="scope"> <template #default="scope">
<el-button size="small" text type="primary" @click="onOpenEditRole(scope.row)" <el-button size="small" text type="primary" @click="onOpenEditRole(scope.row)">
><el-icon><ele-EditPen /></el-icon>修改</el-button <el-icon><ele-EditPen /></el-icon>修改
> </el-button>
<el-button size="small" text type="primary" @click="onRowDel(scope.row)" <el-button size="small" text type="primary" @click="onRowDel(scope.row)">
><el-icon><ele-DeleteFilled /></el-icon>删除</el-button <el-icon><ele-DeleteFilled /></el-icon>删除
> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -59,20 +59,21 @@
:total="tableData.total" :total="tableData.total"
v-model:page="tableData.param.pageNum" v-model:page="tableData.param.pageNum"
v-model:limit="tableData.param.pageSize" v-model:limit="tableData.param.pageSize"
@pagination="roleList" @pagination="handlePaginationChange"
/> />
</el-card> </el-card>
<EditRole ref="editRoleRef" @getRoleList="roleList" /> <EditRole ref="editRoleRef" @getRoleList="roleList" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { toRefs, reactive, onMounted, ref, defineComponent, toRaw, getCurrentInstance } from 'vue'; import { ref, reactive, onMounted, nextTick } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import EditRole from '/@/views/customerService/account/component/editRole.vue'; import EditRole from '/@/views/customerService/account/component/editRole.vue';
import { deleteRole, getRoleList } from '/@/api/system/role'; import { deleteRole, getRoleList } from '/@/api/customerService/account';
// 定义接口来定义对象的类型
interface TableData { // 定义类型接口
interface TableDataItem {
id: number; id: number;
status: number; status: number;
listOrder: number; listOrder: number;
@@ -80,22 +81,29 @@ interface TableData {
remark: string; remark: string;
dataScope: number; dataScope: number;
createdAt: string; createdAt: string;
} updatedAt: string;
interface TableDataState { key: string;
tableData: { creator?: string;
data: Array<TableData>; updater?: string;
total: number;
loading: boolean;
param: {
roleName: string;
roleStatus: string;
pageNum: number;
pageSize: number;
};
};
} }
const dateList = ref([ interface TableParam {
roleName: string;
platform: string;
roleStatus: string;
pageNum: number;
pageSize: number;
}
interface TableState {
data: TableDataItem[];
total: number;
loading: boolean;
param: TableParam;
}
// 模拟数据
const mockData: TableDataItem[] = [
{ {
id: 1, id: 1,
status: 1, status: 1,
@@ -106,6 +114,8 @@ const dateList = ref([
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-28 10:00:15', updatedAt: '2022-04-28 10:00:15',
key: 'list', key: 'list',
creator: '张三',
updater: '李四',
}, },
{ {
id: 2, id: 2,
@@ -117,6 +127,8 @@ const dateList = ref([
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-28 10:01:34', updatedAt: '2022-04-28 10:01:34',
key: 'list', key: 'list',
creator: '王五',
updater: '赵六',
}, },
{ {
id: 3, id: 3,
@@ -128,6 +140,8 @@ const dateList = ref([
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-01 11:38:39', updatedAt: '2022-04-01 11:38:39',
key: '李四', key: '李四',
creator: '孙七',
updater: '周八',
}, },
{ {
id: 4, id: 4,
@@ -139,6 +153,8 @@ const dateList = ref([
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-01 11:38:39', updatedAt: '2022-04-01 11:38:39',
key: '张三', key: '张三',
creator: '吴九',
updater: '郑十',
}, },
{ {
id: 5, id: 5,
@@ -150,6 +166,8 @@ const dateList = ref([
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-01 11:38:39', updatedAt: '2022-04-01 11:38:39',
key: 'zhang', key: 'zhang',
creator: '钱一',
updater: '孙二',
}, },
{ {
id: 8, id: 8,
@@ -161,102 +179,150 @@ const dateList = ref([
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-06 09:53:40', updatedAt: '2022-04-06 09:53:40',
key: 'list', key: 'list',
creator: '李三',
updater: '王四',
}, },
]); ];
export default defineComponent({ // 响应式数据
name: 'apiV1SystemRoleList', const tableData = reactive<TableState>({
components: { EditRole }, data: [],
setup() { total: 0,
const { proxy } = getCurrentInstance() as any; loading: false,
const addRoleRef = ref(); param: {
const editRoleRef = ref(); roleName: '',
const dataScopeRef = ref(); platform: '',
const state = reactive<TableDataState>({ roleStatus: '',
tableData: { pageNum: 1,
data: [], pageSize: 10,
total: 0,
loading: false,
param: {
roleName: '',
roleStatus: '',
pageNum: 1,
pageSize: 10,
},
},
});
// 初始化表格数据
const initTableData = () => {
roleList();
};
const roleList = () => {
const data: Array<TableData> = [];
getRoleList(state.tableData.param).then((res) => {
const list = res.data.list ?? [];
list.map((item: TableData) => {
data.push({
id: item.id,
status: item.status,
listOrder: item.listOrder,
name: item.name,
remark: item.remark,
dataScope: item.dataScope,
createdAt: item.createdAt,
});
});
state.tableData.data = dateList.value;
state.tableData.total = dateList.value.length;
});
};
// 打开新增角色弹窗
const onOpenAddRole = () => {
editRoleRef.value.openDialog();
};
// 打开修改角色弹窗
const onOpenEditRole = (row: Object) => {
editRoleRef.value.openDialog(toRaw(row));
};
// 删除角色
const onRowDel = (row: any) => {
ElMessageBox.confirm(`此操作将永久删除角色:“${row.name}”,是否继续?`, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
deleteRole(row.id).then(() => {
ElMessage.success('删除成功');
proxy.$refs['editRoleRef'].resetMenuSession();
roleList();
});
})
.catch(() => {});
};
// 分页改变
const onHandleSizeChange = (val: number) => {
state.tableData.param.pageSize = val;
};
// 分页改变
const onHandleCurrentChange = (val: number) => {
state.tableData.param.pageNum = val;
};
// 页面加载时
onMounted(() => {
initTableData();
});
return {
addRoleRef,
editRoleRef,
dataScopeRef,
onOpenAddRole,
onOpenEditRole,
onRowDel,
onHandleSizeChange,
onHandleCurrentChange,
roleList,
...toRefs(state),
};
}, },
}); });
// 模板引用
const editRoleRef = ref<InstanceType<typeof EditRole>>();
/**
* 获取角色列表
*/
const roleList = async () => {
try {
tableData.loading = true;
// 实际API调用
// const res = await getRoleList(tableData.param);
// tableData.data = res.data.list || [];
// tableData.total = res.data.total || 0;
// 模拟数据
await new Promise((resolve) => setTimeout(resolve, 500)); // 模拟网络延迟
tableData.data = mockData;
tableData.total = mockData.length;
} catch (error) {
console.error('获取角色列表失败:', error);
ElMessage.error('获取数据失败');
} finally {
tableData.loading = false;
}
};
/**
* 处理搜索
*/
const handleSearch = () => {
tableData.param.pageNum = 1; // 重置到第一页
roleList();
};
/**
* 处理分页变化
* @param pagination - 分页参数
*/
const handlePaginationChange = (pagination: { page: number; limit: number }) => {
tableData.param.pageNum = pagination.page;
tableData.param.pageSize = pagination.limit;
roleList();
};
/**
* 打开新增角色对话框
*/
const onOpenAddRole = () => {
if (editRoleRef.value) {
editRoleRef.value.openDialog();
}
};
/**
* 打开编辑角色对话框
* @param row - 角色数据
*/
const onOpenEditRole = (row: TableDataItem) => {
if (editRoleRef.value) {
editRoleRef.value.openDialog(row);
}
};
/**
* 删除角色
* @param row - 角色数据
*/
const onRowDel = async (row: TableDataItem) => {
try {
await ElMessageBox.confirm(`此操作将永久删除客服账号:"${row.name}",是否继续?`, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
});
// 实际删除操作
// await deleteRole(row.id);
// 模拟删除成功
ElMessage.success('删除成功');
// 刷新列表
await roleList();
} catch (error) {
// 用户取消操作,不处理
console.log('用户取消删除操作');
}
};
/**
* 初始化表格数据
*/
const initTableData = () => {
roleList();
};
// 生命周期
onMounted(() => {
initTableData();
});
</script> </script>
<style scoped>
.system-user-search {
margin-bottom: 15px;
}
.mb15 {
margin-bottom: 15px;
}
.ml10 {
margin-left: 10px;
}
.w-50 {
width: 240px;
}
.system-role-container {
padding: 20px;
}
:deep(.el-table) {
margin-top: 10px;
}
</style>

View File

@@ -1,244 +1,251 @@
<template> <template>
<div class="system-edit-role-container"> <div class="system-edit-role-container">
<el-dialog title="" v-model="isShowDialog" width="1000px"> <el-dialog :title="(formData.id === 0 ? '添加' : '修改') + '产品'" v-model="isShowDialog" width="1000px">
<el-form ref="formRef" :model="formData" :rules="rules" size="default" label-width="90px"> <el-form ref="formRef" :model="formData" :rules="rules" size="default" label-width="90px">
<el-row :gutter="35"> <el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20"> <!-- 产品名称输入框 -->
<el-form-item label="产品名称" prop="name"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-input placeholder="xxx" clearable></el-input> <el-form-item label="产品名称" prop="name">
</el-form-item> <el-input v-model="formData.name" placeholder="请输入产品名称" clearable />
</el-col> </el-form-item>
</el-col>
<!-- 新增富文本编辑器 --> <!-- 富文本编辑器 -->
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20"> <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="产品详情" prop="content"> <el-form-item label="产品详情" prop="content">
<Editor height="400px" placeholder="请输入产品详细描述..." /> <Editor v-model="formData.content" height="400px" placeholder="请输入产品详细描述..." />
</el-form-item> </el-form-item>
</el-col> </el-col>
</el-row>
</el-form>
<!-- 对话框底部按钮 -->
</el-row> <template #footer>
</el-form> <span class="dialog-footer">
<template #footer> <el-button @click="onCancel" size="default"> </el-button>
<span class="dialog-footer"> <el-button type="primary" @click="onSubmit" size="default" :loading="loading">
<el-button @click="onCancel" size="default"> </el-button> {{ formData.id === 0 ? '新 增' : '修 改' }}
<el-button type="primary" @click="onSubmit" size="default" :loading="loading">{{ formData.id === 0 ? '新 增' : </el-button>
'修改' }}</el-button> </span>
</span> </template>
</template> </el-dialog>
</el-dialog> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { reactive, toRefs, defineComponent, ref, getCurrentInstance, unref, } from 'vue'; import { ref, reactive, toRefs, nextTick } from 'vue';
import { addRole, editRole, getRole, getRoleParams } from "/@/api/system/role"; import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { ElMessage } from "element-plus"; import { addRole, editRole, getRole } from '/@/api/system/role';
import { refreshBackEndControlRoutes } from "/@/router/backEnd";
// 引入富文本编辑器组件
import Editor from '/@/components/editor/index.vue'; import Editor from '/@/components/editor/index.vue';
/**
// 定义接口来定义对象的类型 * 产品表单数据接口
interface MenuDataTree { */
id: number; interface ProductFormData {
pid: number; id: number; // 产品ID
title: string; name: string; // 产品名称
children?: MenuDataTree[]; status: number; // 状态1-启用0-禁用
} listOrder: number; // 排序
interface DialogRow { remark: string; // 备注
id: number; content?: string; // 产品详情(富文本内容)
name: string;
status: number;
listOrder: number;
remark: string;
menuIds: Array<number>
}
interface RoleState {
loading: boolean;
isShowDialog: boolean;
formData: DialogRow;
menuData: Array<MenuDataTree>;
menuExpand: boolean;
menuNodeAll: boolean;
menuCheckStrictly: boolean;
menuProps: {
children: string;
label: string;
};
rules: object;
} }
export default defineComponent({ /**
name: 'systemEditRole', * 组件状态接口
components: { */
Editor // 注册富文本组件 interface ComponentState {
}, loading: boolean; // 提交加载状态
setup(props, { emit }) { isShowDialog: boolean; // 对话框显示状态
const { proxy } = getCurrentInstance() as any; formData: ProductFormData; // 表单数据
const formRef = ref<HTMLElement | null>(null); }
const menuRef = ref();
const state = reactive<RoleState>({
loading: false,
isShowDialog: false,
formData: {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
menuIds: []
},
// 表单校验
rules: {
name: [
{ required: true, message: "角色名称不能为空", trigger: "blur" },
]
},
menuData: [],
menuExpand: false,
menuNodeAll: false,
menuCheckStrictly: false,
menuProps: {
children: 'children',
label: 'title',
},
});
// 打开弹窗
const openDialog = (row?: DialogRow) => {
resetForm();
getMenuData();
if (row) {
getRole(row.id).then((res: any) => {
if (res.data.role) {
state.formData = res.data.role;
state.formData.menuIds = res.data.menuIds ?? []
}
})
}
state.isShowDialog = true;
};
// 关闭弹窗
const closeDialog = () => {
state.isShowDialog = false;
};
// 取消
const onCancel = () => {
closeDialog();
};
// 新增
const onSubmit = () => {
const formWrap = unref(formRef) as any;
if (!formWrap) return;
formWrap.validate((valid: boolean) => {
if (valid) {
state.loading = true;
state.formData.menuIds = getMenuAllCheckedKeys();
if (state.formData.id === 0) {
//添加
addRole(state.formData).then(() => {
ElMessage.success('角色添加成功');
closeDialog(); // 关闭弹窗
resetMenuSession()
emit('getRoleList')
}).finally(() => {
state.loading = false;
})
} else {
//修改
editRole(state.formData).then(() => {
ElMessage.success('角色修改成功');
closeDialog(); // 关闭弹窗
resetMenuSession()
emit('getRoleList')
}).finally(() => {
state.loading = false;
})
}
}
});
};
// 获取菜单结构数据
const getMenuData = () => {
getRoleParams().then((res: any) => {
state.menuData = proxy.handleTree(res.data.menu, "id", "pid");
})
};
const resetForm = () => {
state.menuCheckStrictly = false;
state.menuExpand = false;
state.menuNodeAll = false;
state.formData = {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
menuIds: []
}
};
/** 树权限(展开/折叠)*/
const handleCheckedTreeExpand = (value: any) => {
let treeList = state.menuData;
for (let i = 0; i < treeList.length; i++) {
menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
}
}
/** 树权限(全选/全不选) */ // 定义组件事件
const handleCheckedTreeNodeAll = (value: any) => { const emit = defineEmits<{
menuRef.value.setCheckedNodes(value ? state.menuData : []); /**
} * 获取产品列表事件 - 用于刷新父组件列表
*/
(e: 'getRoleList'): void;
}>();
/** 树权限(父子联动) */ // 响应式状态
const handleCheckedTreeConnect = (value: any) => { const state = reactive<ComponentState>({
state.menuCheckStrictly = value ? true : false; loading: false,
} isShowDialog: false,
formData: {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
content: '',
},
});
/** 所有菜单节点数据 */ // 表单验证规则
function getMenuAllCheckedKeys() { const rules: FormRules = {
// 目前被选中的菜单节点 name: [
let checkedKeys = menuRef.value.getCheckedKeys(); { required: true, message: '产品名称不能为空', trigger: 'blur' },
// 半选中的菜单节点 { min: 2, max: 50, message: '产品名称长度在 2 到 50 个字符', trigger: 'blur' },
let halfCheckedKeys = menuRef.value.getHalfCheckedKeys(); ],
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys); content: [{ required: true, message: '产品详情不能为空', trigger: 'blur' }],
return checkedKeys; };
}
// 重置菜单session // 模板引用
const resetMenuSession = () => { const formRef = ref<FormInstance>();
refreshBackEndControlRoutes();
}; // 解构状态数据,便于模板使用
return { const { loading, isShowDialog, formData } = toRefs(state);
openDialog,
closeDialog, /**
onCancel, * 打开对话框
onSubmit, * @param row - 可选的编辑数据,如果传入则为编辑模式,否则为新增模式
menuRef, */
formRef, const openDialog = async (row?: ProductFormData) => {
handleCheckedTreeExpand, // 重置表单
handleCheckedTreeNodeAll, resetForm();
handleCheckedTreeConnect,
resetMenuSession, try {
...toRefs(state), if (row && row.id) {
}; // 编辑模式:获取产品详情
}, await handleEditMode(row.id);
}
// 显示对话框
state.isShowDialog = true;
} catch (error) {
console.error('打开对话框失败:', error);
ElMessage.error('初始化数据失败');
}
};
/**
* 处理编辑模式的数据获取
* @param id - 产品ID
*/
const handleEditMode = async (id: number) => {
try {
const res = await getRole(id);
if (res.data?.role) {
// 填充表单数据
state.formData = {
...res.data.role,
content: res.data.role.content || '',
};
}
} catch (error) {
console.error('获取产品详情失败:', error);
throw new Error('获取产品详情失败');
}
};
/**
* 关闭对话框
*/
const closeDialog = () => {
state.isShowDialog = false;
};
/**
* 取消操作
*/
const onCancel = () => {
closeDialog();
};
/**
* 提交表单
*/
const onSubmit = async () => {
if (!formRef.value) {
ElMessage.error('表单引用不存在');
return;
}
try {
// 表单验证
const valid = await formRef.value.validate();
if (!valid) {
ElMessage.warning('请完善表单信息');
return;
}
// 设置加载状态
state.loading = true;
// 根据ID判断是新增还是修改
if (state.formData.id === 0) {
// 新增产品
await addRole(state.formData);
ElMessage.success('产品添加成功');
} else {
// 修改产品
await editRole(state.formData);
ElMessage.success('产品修改成功');
}
// 关闭对话框并刷新列表
closeDialog();
emit('getRoleList');
} catch (error) {
console.error('提交失败:', error);
ElMessage.error(state.formData.id === 0 ? '添加失败' : '修改失败');
} finally {
state.loading = false;
}
};
/**
* 重置表单
*/
const resetForm = () => {
// 重置表单数据
state.formData = {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
content: '',
};
// 在下一个DOM更新周期后清除表单验证
nextTick(() => {
formRef.value?.clearValidate();
});
};
// 暴露方法给父组件使用
defineExpose({
/**
* 打开对话框方法
*/
openDialog,
/**
* 关闭对话框方法
*/
closeDialog,
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.tree-border { .system-edit-role-container {
margin-top: 5px; // 对话框内容区域样式
border: 1px solid #e5e6e7 !important; :deep(.el-dialog__body) {
border-radius: 4px; padding: 20px;
}
// 底部按钮区域样式
.dialog-footer {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 10px;
}
} }
.system-edit-role-container { // 间距工具类
.menu-data-tree { .mb20 {
border: var(--el-input-border, var(--el-border-base)); margin-bottom: 20px;
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
padding: 5px;
}
} }
</style> </style>

View File

@@ -9,40 +9,36 @@
</el-dialog> </el-dialog>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, reactive } from 'vue'; import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus'; import { ElMessage } from 'element-plus';
export default defineComponent({ const isShowDialog = ref(false);
name: 'ExportDialog', const tableData = ref([]);
setup() {
const isShowDialog = ref(false);
const tableData = ref([]);
const form = reactive({ const form = reactive({
exportType: 'current', exportType: 'current',
fileType: 'xlsx', fileType: 'xlsx',
}); });
// 打开对话框 // 打开对话框
const openDialog = (data: any) => { const openDialog = (data: any) => {
tableData.value = data; tableData.value = data;
isShowDialog.value = true; isShowDialog.value = true;
}; };
// 处理导出 // 处理导出
const handleExport = () => { const handleExport = () => {
// 这里处理导出逻辑 // 这里处理导出逻辑
ElMessage.success('导出成功'); ElMessage.success('导出成功');
isShowDialog.value = false; isShowDialog.value = false;
}; };
return { // 暴露方法给父组件使用
isShowDialog, defineExpose({
form, /**
openDialog, * 打开对话框方法
handleExport, */
}; openDialog,
},
}); });
</script> </script>

View File

@@ -1,21 +1,7 @@
<template> <template>
<el-dialog title="导入产品" v-model="isShowDialog" width="500px" destroy-on-close> <el-dialog title="导入产品" v-model="isShowDialog" width="500px" destroy-on-close @close="handleDialogClose">
<div class="import-container"> <div class="import-container">
<!-- 导入说明 --> <!-- 文件上传区域 -->
<div class="import-instructions mt20" style="margin-bottom: 40px">
<el-card shadow="never" class="box-card">
<el-text type="info" size="small">
<ul class="instruction-list">
<li>1. 请先下载模板ZIP包查看Word文档格式要求</li>
<li>2. 按照模板中的Word文档格式准备您的产品文档</li>
<li>3. 将您的Word文档打包成ZIP格式上传</li>
<li>4. 支持 .docx 格式的Word文档</li>
</ul>
</el-text>
</el-card>
</div>
<!-- 上传区域 -->
<el-upload <el-upload
ref="uploadRef" ref="uploadRef"
class="upload-demo" class="upload-demo"
@@ -25,38 +11,49 @@
:data="uploadData" :data="uploadData"
:accept="acceptFileTypes" :accept="acceptFileTypes"
:limit="1" :limit="1"
:on-success="handleSuccess" :on-success="handleUploadSuccess"
:on-error="handleError" :on-error="handleUploadError"
:before-upload="beforeUpload" :before-upload="beforeUpload"
:on-exceed="handleExceed" :on-exceed="handleExceed"
:on-remove="handleRemove" :on-remove="handleRemove"
> >
<div class="el-upload-area"> <div class="el-upload-area">
<el-icon class="el-icon--upload"><ele-UploadFilled /></el-icon> <el-icon class="el-icon--upload">
<UploadFilled />
</el-icon>
<div class="el-upload__text">将ZIP文件拖到此处<em>点击上传</em></div> <div class="el-upload__text">将ZIP文件拖到此处<em>点击上传</em></div>
</div> </div>
<template #tip>
<div class="el-upload__tip">
<el-text type="info" size="small"> 请上传 {{ acceptFileTypes }} 格式文件且不超过 {{ fileSizeLimit }}MB </el-text>
</div>
</template>
</el-upload> </el-upload>
<!-- 下载模板按钮 --> <!-- 导入说明 -->
<div class="download-section" style="margin-top: 20px; text-align: center"> <div class="import-instructions mt20">
<el-button type="primary" size="default" :loading="downloadLoading" @click="downloadTemplate" style="width: 200px"> <el-card shadow="never" class="box-card">
<template #icon> <el-text type="info" size="small">
<el-icon><ele-Download /></el-icon> <ul class="instruction-list">
</template> <li>1. 请先下载模板ZIP包查看Word文档格式要求</li>
{{ downloadLoading ? '正在生成模板...' : '下载导入模板' }} <li>2. 按照模板中的Word文档格式准备您的产品文档</li>
</el-button> <li>3. 将您的Word文档打包成ZIP格式上传</li>
<li>4. 支持 .docx 格式的Word文档</li>
<li>5. 文件大小不能超过 {{ fileSizeLimit }}MB</li>
</ul>
</el-text>
</el-card>
</div> </div>
</div> </div>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="isShowDialog = false">取消</el-button> <!-- 下载模板按钮 -->
<el-button type="primary" :loading="loading" @click="submitUpload"> <el-button type="primary" size="default" :loading="downloadLoading" @click="handleDownloadTemplate" class="download-btn">
<template #icon>
<el-icon><Download /></el-icon>
</template>
{{ downloadLoading ? '生成中...' : '下载模板' }}
</el-button>
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" :loading="loading" @click="handleSubmitUpload">
{{ loading ? '导入中...' : '开始导入' }} {{ loading ? '导入中...' : '开始导入' }}
</el-button> </el-button>
</span> </span>
@@ -64,80 +61,42 @@
</el-dialog> </el-dialog>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { defineComponent, ref, reactive } from 'vue'; import { ref, reactive, nextTick } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus'; import { ElMessage, ElMessageBox, type UploadInstance } from 'element-plus';
import { UploadFilled, Download } from '@element-plus/icons-vue'; import { UploadFilled, Download } from '@element-plus/icons-vue';
import JSZip from 'jszip'; import JSZip from 'jszip';
import { saveAs } from 'file-saver'; import { saveAs } from 'file-saver';
export default defineComponent({ // 定义组件事件
name: 'ImportDialog', const emit = defineEmits<{
components: { UploadFilled, Download }, /**
emits: ['getRoleList'], * 获取产品列表事件 - 导入成功后刷新列表
setup(props, { emit }) { */
const isShowDialog = ref(false); (e: 'getRoleList'): void;
const uploadRef = ref(); }>();
const loading = ref(false);
const downloadLoading = ref(false);
// 上传配置 // 响应式状态
const uploadAction = ref('/api/product/import-word-zip'); const isShowDialog = ref(false);
const uploadHeaders = reactive({ const loading = ref(false);
Authorization: 'Bearer ' + localStorage.getItem('token'), const downloadLoading = ref(false);
}); const uploadRef = ref<UploadInstance>();
const uploadData = reactive({
type: 'word-zip',
});
const acceptFileTypes = '.zip,.ZIP';
const fileSizeLimit = 50;
// 打开对话框 // 上传配置
const openDialog = () => { const uploadAction = '/api/product/import-word-zip';
isShowDialog.value = true; const uploadHeaders = reactive({
loading.value = false; Authorization: `Bearer ${localStorage.getItem('token') || ''}`,
downloadLoading.value = false; });
if (uploadRef.value) { const uploadData = reactive({
uploadRef.value.clearFiles(); type: 'word-zip',
} });
}; const acceptFileTypes = '.zip,.ZIP';
const fileSizeLimit = 50;
// 创建Word文档的二进制内容模拟真实的.docx格式 // 模板内容定义
const createWordDocumentContent = (content: string) => { const templateContents = {
// Word文档的基本结构简化版 /** 产品说明书模板内容 */
// 实际Word文档是复杂的XML结构这里创建一个简单的文本文件 manual: `产品说明书模板
// 在实际项目中应该使用后端生成真正的Word文档
// 创建一个包含基本Word文档结构的文本
const wordContent = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml">
<w:body>
<w:p>
<w:r>
<w:t>${content.replace(/\n/g, '</w:t></w:r></w:p><w:p><w:r><w:t>')}</w:t>
</w:r>
</w:p>
</w:body>
</w:wordDocument>`;
return new Blob([wordContent], { type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' });
};
// 创建文本文件内容
const createTextFile = (content: string) => {
return new Blob([content], { type: 'text/plain;charset=utf-8' });
};
// 下载模板 - 修复版本生成包含真实文件的ZIP包
const downloadTemplate = async () => {
downloadLoading.value = true;
try {
const zip = new JSZip();
// 1. 创建产品说明书模板内容
const manualContent = `产品说明书模板
产品名称_________________________ 产品名称_________________________
产品型号_________________________ 产品型号_________________________
@@ -166,10 +125,10 @@ ___________________________________
注意事项: 注意事项:
• ________________________________ • ________________________________
• ________________________________ • ________________________________
• ________________________________`; • ________________________________`,
// 2. 创建技术规格书模板内容 /** 技术规格书模板内容 */
const specContent = `技术规格书模板 spec: `技术规格书模板
产品名称_________________________ 产品名称_________________________
产品型号_________________________ 产品型号_________________________
@@ -193,19 +152,19 @@ ___________________________________
4. 安全标准 4. 安全标准
- 认证_______________________ - 认证_______________________
- 防护等级___________________`; - 防护等级___________________`,
// 3. 创建使用说明文档 /** 使用说明文档内容 */
const readmeContent = `产品文档导入模板使用说明 readme: `产品文档导入模板使用说明
================================ ================================
欢迎使用产品文档导入模板! 欢迎使用产品文档导入模板!
📁 ZIP包内容说明 📁 ZIP包内容说明
----------------- -----------------
1. 产品说明书模板.docx - 产品详细说明文档模板 1. 产品说明书模板.docx - 产品详细说明文档模板
2. 技术规格书模板.docx - 技术参数文档模板 2. 技术规格书模板.docx - 技术参数文档模板
3. 使用说明.txt - 本说明文件 3. 使用说明.txt - 本说明文件
📝 使用步骤: 📝 使用步骤:
----------- -----------
@@ -227,167 +186,207 @@ ___________________________________
----------- -----------
如有问题请联系系统管理员。 如有问题请联系系统管理员。
祝您使用愉快!`; 祝您使用愉快!`,
};
// 创建真实的文件Blob对象 /**
const manualBlob = createWordDocumentContent(manualContent); * 创建Word文档的二进制内容
const specBlob = createWordDocumentContent(specContent); * @param content - 文档内容
const readmeBlob = createTextFile(readmeContent); * @returns Blob对象
*/
const createWordDocumentContent = (content: string): Blob => {
// 简化的Word文档XML结构
const wordContent = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?mso-application progid="Word.Document"?>
<w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml">
<w:body>
<w:p>
<w:r>
<w:t>${content.replace(/\n/g, '</w:t></w:r></w:p><w:p><w:r><w:t>')}</w:t>
</w:r>
</w:p>
</w:body>
</w:wordDocument>`;
// 将文件添加到ZIP包 return new Blob([wordContent], {
zip.file('产品说明书模板.docx', manualBlob); type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
zip.file('技术规格书模板.docx', specBlob); });
zip.file('使用说明.txt', readmeBlob); };
// 生成ZIP包 /**
const zipBlob = await zip.generateAsync({ * 创建文本文件内容
type: 'blob', * @param content - 文本内容
compression: 'DEFLATE', * @returns Blob对象
compressionOptions: { */
level: 6, const createTextFile = (content: string): Blob => {
}, return new Blob([content], { type: 'text/plain;charset=utf-8' });
}); };
// 重要:检查文件大小,确保不是空文件 /**
if (zipBlob.size === 0) { * 打开对话框
throw new Error('生成的ZIP包为空请检查文件内容'); */
} const openDialog = () => {
isShowDialog.value = true;
loading.value = false;
downloadLoading.value = false;
// 下载文件 // 清空已上传的文件
saveAs(zipBlob, `产品文档导入模板_${new Date().toISOString().split('T')[0]}.zip`); nextTick(() => {
uploadRef.value?.clearFiles();
});
};
ElMessage.success({ /**
message: `模板下载成功!文件大小:${(zipBlob.size / 1024).toFixed(2)}KB`, * 处理对话框关闭
duration: 3000, */
}); const handleDialogClose = () => {
// 清空上传文件
uploadRef.value?.clearFiles();
};
// 显示使用提示 /**
setTimeout(() => { * 取消操作
ElMessage.info('下载完成后请解压ZIP包查看Word文档模板'); */
}, 1000); const handleCancel = () => {
} catch (error) { isShowDialog.value = false;
console.error('模板下载失败:', error); };
ElMessage.error({
message: '模板生成失败:' + (error instanceof Error ? error.message : '未知错误'),
duration: 5000,
});
// 提供备用方案 /**
setTimeout(() => { * 下载模板文件
ElMessageBox.alert('如果模板下载仍有问题,请联系管理员获取手动模板文件。', '下载失败', { */
confirmButtonText: '确定', const handleDownloadTemplate = async () => {
type: 'warning', downloadLoading.value = true;
});
}, 1000);
} finally {
downloadLoading.value = false;
}
};
// 上传前校验 try {
const beforeUpload = (file: File) => { const zip = new JSZip();
const isZip = file.type === 'application/zip' || file.name.toLowerCase().endsWith('.zip');
const isLt50M = file.size / 1024 / 1024 < fileSizeLimit;
if (!isZip) { // 添加模板文件到ZIP包
ElMessage.error('请上传ZIP格式的压缩包文件!'); zip.file('产品说明书模板.docx', createWordDocumentContent(templateContents.manual));
return false; zip.file('技术规格书模板.docx', createWordDocumentContent(templateContents.spec));
} zip.file('使用说明.txt', createTextFile(templateContents.readme));
if (!isLt50M) {
ElMessage.error(`ZIP文件大小不能超过${fileSizeLimit}MB!`);
return false;
}
ElMessage.info(`开始上传ZIP包: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`); // 生成ZIP包
return true; const zipBlob = await zip.generateAsync({
}; type: 'blob',
compression: 'DEFLATE',
compressionOptions: { level: 6 },
});
// 文件超出限制 // 检查文件大小
const handleExceed = () => { if (zipBlob.size === 0) {
ElMessage.warning('只能上传一个ZIP包文件请先删除当前文件'); throw new Error('生成的ZIP包为空');
}; }
// 移除文件 // 下载文件
const handleRemove = () => { const dateString = new Date().toISOString().split('T')[0];
// 文件被移除时的处理 saveAs(zipBlob, `产品文档导入模板_${dateString}.zip`);
};
// 上传成功 ElMessage.success(`模板下载成功!文件大小:${(zipBlob.size / 1024).toFixed(2)}KB`);
const handleSuccess = (response: any) => { } catch (error) {
loading.value = false; console.error('模板下载失败:', error);
if (response.code === 200) { ElMessage.error(`模板生成失败:${error instanceof Error ? error.message : '未知错误'}`);
ElMessage.success('Word文档导入成功'); } finally {
emit('getRoleList'); downloadLoading.value = false;
isShowDialog.value = false; }
} else { };
ElMessage.error(response.msg || 'Word文档ZIP包导入失败');
}
};
// 上传失败 /**
const handleError = (error: any) => { * 上传前校验
loading.value = false; * @param file - 上传的文件
ElMessage.error('ZIP包上传失败请重试'); * @returns 是否允许上传
console.error('ZIP upload error:', error); */
}; const beforeUpload = (file: File): boolean => {
const isZip = file.type === 'application/zip' || file.name.toLowerCase().endsWith('.zip');
const isLtLimit = file.size / 1024 / 1024 < fileSizeLimit;
// 手动提交上传 if (!isZip) {
const submitUpload = () => { ElMessage.error('请上传ZIP格式的压缩包文件!');
if (!uploadRef.value || !uploadRef.value.uploadFiles.length) { return false;
ElMessage.warning('请先选择要上传的ZIP包文件'); }
return;
}
ElMessageBox.confirm('确认导入选中的ZIP包吗', '导入确认', { if (!isLtLimit) {
confirmButtonText: '确认导入', ElMessage.error(`文件大小不能超过${fileSizeLimit}MB!`);
cancelButtonText: '取消', return false;
type: 'warning', }
})
.then(() => {
loading.value = true;
uploadRef.value.submit();
})
.catch(() => {
// 用户取消
});
};
return { ElMessage.info(`开始上传: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`);
isShowDialog, return true;
uploadRef, };
loading,
downloadLoading, /**
uploadAction, * 处理文件超出限制
uploadHeaders, */
uploadData, const handleExceed = () => {
acceptFileTypes, ElMessage.warning('只能上传一个文件,请先删除当前文件');
fileSizeLimit, };
openDialog,
downloadTemplate, /**
beforeUpload, * 处理文件移除
handleExceed, */
handleRemove, const handleRemove = () => {
handleSuccess, // 文件移除后的清理操作
handleError, };
submitUpload,
}; /**
}, * 处理上传成功
* @param response - 服务器响应
*/
const handleUploadSuccess = (response: any) => {
loading.value = false;
if (response.code === 200) {
ElMessage.success('文件导入成功');
emit('getRoleList');
isShowDialog.value = false;
} else {
ElMessage.error(response.msg || '文件导入失败');
}
};
/**
* 处理上传失败
* @param error - 错误信息
*/
const handleUploadError = (error: any) => {
loading.value = false;
ElMessage.error('文件上传失败,请重试');
console.error('Upload error:', error);
};
/**
* 手动提交上传
*/
const handleSubmitUpload = async () => {
if (!uploadRef.value?.uploadFiles?.length) {
ElMessage.warning('请先选择要上传的文件');
return;
}
try {
await ElMessageBox.confirm('确认导入选中的文件吗?', '导入确认', {
confirmButtonText: '确认导入',
cancelButtonText: '取消',
type: 'warning',
});
loading.value = true;
uploadRef.value.submit();
} catch {
// 用户取消操作
}
};
// 暴露方法给父组件
defineExpose({
openDialog,
}); });
</script> </script>
<style scoped> <style scoped lang="scss">
.import-container { .import-container {
padding: 10px 0; padding: 10px 0;
} }
.download-section {
text-align: center;
}
.template-tips {
margin-top: 10px;
}
.upload-demo { .upload-demo {
width: 100%; width: 100%;
} }
@@ -403,18 +402,37 @@ ___________________________________
margin-bottom: 16px; margin-bottom: 16px;
} }
.import-instructions .card-header { .import-instructions {
font-weight: 600; .mt20 {
margin-top: 20px;
}
} }
.instruction-list { .instruction-list {
margin: 0; margin: 0;
padding-left: 16px; padding-left: 16px;
line-height: 1.8; line-height: 1.8;
li {
margin-bottom: 4px;
}
} }
.instruction-list li { .dialog-footer {
margin-bottom: 4px; display: flex;
justify-content: right;
align-items: center;
.download-btn {
background-color: coral;
border-color: coral;
width: 120px;
&:hover {
background-color: #ff7f50;
border-color: #ff7f50;
}
}
} }
:deep(.el-upload-dragger) { :deep(.el-upload-dragger) {

View File

@@ -1,13 +1,14 @@
<template> <template>
<div class="system-role-container"> <div class="system-role-container">
<el-card shadow="hover"> <el-card shadow="hover">
<!-- 搜索区域 -->
<div class="system-user-search mb15"> <div class="system-user-search mb15">
<el-form :inline="true"> <el-form :inline="true">
<el-form-item label="产品名称"> <el-form-item label="产品名称">
<el-input size="default" v-model="tableData.param.roleName" placeholder="请输入产品名称" class="w-50 m-2" clearable /> <el-input size="default" v-model="tableData.param.productName" placeholder="请输入产品名称" class="w-50 m-2" clearable />
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button size="default" type="primary" class="ml10" @click="roleList"> <el-button size="default" type="primary" class="ml10" @click="handleSearch">
<el-icon> <el-icon>
<ele-Search /> <ele-Search />
</el-icon> </el-icon>
@@ -19,7 +20,7 @@
</el-icon> </el-icon>
新增产品 新增产品
</el-button> </el-button>
<!-- 添加导入导出按钮 --> <!-- 导入导出按钮 -->
<el-button size="default" type="warning" class="ml10" @click="onOpenImport"> <el-button size="default" type="warning" class="ml10" @click="onOpenImport">
<el-icon> <el-icon>
<ele-Upload /> <ele-Upload />
@@ -35,253 +36,349 @@
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<el-table :data="tableData.data" style="width: 100%">
<!-- 产品表格 -->
<el-table :data="tableData.data" style="width: 100%" v-loading="tableData.loading">
<el-table-column type="index" label="序号" width="60" /> <el-table-column type="index" label="序号" width="60" />
<el-table-column prop="name" label="产品名称" show-overflow-tooltip></el-table-column> <el-table-column prop="name" label="产品名称" show-overflow-tooltip />
<el-table-column prop="remark" label="创建人" show-overflow-tooltip></el-table-column> <el-table-column prop="creator" label="创建人" show-overflow-tooltip />
<el-table-column prop="status" label="修改人" show-overflow-tooltip></el-table-column> <el-table-column prop="updater" label="修改人" show-overflow-tooltip />
<el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip></el-table-column> <el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip />
<el-table-column prop="createdAt" label="修改时间" show-overflow-tooltip></el-table-column> <el-table-column prop="updatedAt" label="修改时间" show-overflow-tooltip />
<el-table-column label="操作" width="220"> <el-table-column label="操作" width="220">
<template #default="scope"> <template #default="scope">
<el-button size="small" text type="primary" @click="onOpenEditRole(scope.row)" <el-button size="small" text type="primary" @click="onOpenEditRole(scope.row)">
><el-icon><ele-EditPen /></el-icon>修改</el-button <el-icon><ele-EditPen /></el-icon>修改
> </el-button>
<el-button size="small" text type="primary" @click="onRowDel(scope.row)" <el-button size="small" text type="primary" @click="onRowDel(scope.row)">
><el-icon><ele-DeleteFilled /></el-icon>删除</el-button <el-icon><ele-DeleteFilled /></el-icon>删除
> </el-button>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<!-- 分页组件 -->
<pagination <pagination
v-show="tableData.total > 0" v-show="tableData.total > 0"
:total="tableData.total" :total="tableData.total"
v-model:page="tableData.param.pageNum" v-model:page="tableData.param.pageNum"
v-model:limit="tableData.param.pageSize" v-model:limit="tableData.param.pageSize"
@pagination="roleList" @pagination="handlePaginationChange"
/> />
</el-card> </el-card>
<EditRole ref="editRoleRef" @getRoleList="roleList" /> <!-- 编辑对话框组件 -->
<EditRole ref="editRoleRef" @getRoleList="getProductList" />
<!-- 添加导入导出对话框 --> <!-- 导入导出对话框组件 -->
<ImportDialog ref="importDialogRef" @getRoleList="roleList" /> <ImportDialog ref="importDialogRef" @getRoleList="getProductList" />
<ExportDialog ref="exportDialogRef" /> <ExportDialog ref="exportDialogRef" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { toRefs, reactive, onMounted, ref, defineComponent, toRaw, getCurrentInstance } from 'vue'; import { ref, reactive, onMounted, nextTick } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import EditRole from '/@/views/customerService/product/component/editRole.vue'; import EditRole from '/@/views/customerService/product/component/editRole.vue';
import { deleteRole, getRoleList } from '/@/api/system/role';
import ImportDialog from '/@/views/customerService/product/component/importDialog.vue'; import ImportDialog from '/@/views/customerService/product/component/importDialog.vue';
import ExportDialog from '/@/views/customerService/product/component/exportDialog.vue'; import ExportDialog from '/@/views/customerService/product/component/exportDialog.vue';
import { deleteRole, getRoleList } from '/@/api/customerService/product';
// 定义接口来定义对象的类型 /**
interface TableData { * 产品数据接口
*/
interface ProductData {
id: number; id: number;
status: number; status: number;
listOrder: number; listOrder: number;
name: string; name: string;
creator: string; // 创建人
updater: string; // 修改人
remark: string; remark: string;
dataScope: number; dataScope: number;
createdAt: string; createdAt: string;
} updatedAt: string;
interface TableDataState {
tableData: {
data: Array<TableData>;
total: number;
loading: boolean;
param: {
roleName: string;
roleStatus: string;
pageNum: number;
pageSize: number;
};
};
} }
const dateList = ref([ /**
* 查询参数接口
*/
interface QueryParam {
productName: string; // 产品名称
roleStatus: string; // 状态
pageNum: number; // 页码
pageSize: number; // 每页大小
}
/**
* 表格状态接口
*/
interface TableState {
data: ProductData[]; // 表格数据
total: number; // 总条数
loading: boolean; // 加载状态
param: QueryParam; // 查询参数
}
/**
* 模拟产品数据
*/
const mockProductData: ProductData[] = [
{ {
id: 1, id: 1,
status: '李四', status: 1,
listOrder: 0, listOrder: 0,
name: 'xxx药品', name: 'xxx药品',
remark: '张三', creator: '张三',
updater: '李四',
remark: '药品描述',
dataScope: 3, dataScope: 3,
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-28 10:00:15', updatedAt: '2022-04-28 10:00:15',
}, },
{ {
id: 2, id: 2,
status: '李四', status: 1,
listOrder: 0, listOrder: 0,
name: 'xxx药品', name: 'yyy医疗器械',
remark: '张三', creator: '王五',
updater: '赵六',
remark: '医疗器械描述',
dataScope: 3, dataScope: 3,
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-28 10:01:34', updatedAt: '2022-04-28 10:01:34',
}, },
{ {
id: 3, id: 3,
status: '李四', status: 1,
listOrder: 0, listOrder: 0,
name: 'xxx药品', name: 'zzz保健品',
remark: '张三', creator: '孙七',
updater: '周八',
remark: '保健品描述',
dataScope: 3, dataScope: 3,
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-01 11:38:39', updatedAt: '2022-04-01 11:38:39',
}, },
{ {
id: 4, id: 4,
status: '李四', status: 1,
listOrder: 0, listOrder: 0,
name: 'xxx药品', name: 'aaa化妆品',
remark: '张三', creator: '吴九',
updater: '郑十',
remark: '化妆品描述',
dataScope: 3, dataScope: 3,
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-01 11:38:39', updatedAt: '2022-04-01 11:38:39',
}, },
{ {
id: 5, id: 5,
status: '李四', status: 1,
listOrder: 0, listOrder: 0,
name: 'xxx药品', name: 'bbb药品',
remark: '张三', creator: '钱一',
updater: '孙二',
remark: '药品描述',
dataScope: 2, dataScope: 2,
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-01 11:38:39', updatedAt: '2022-04-01 11:38:39',
}, },
{ {
id: 8, id: 8,
status: '李四', status: 1,
listOrder: 0, listOrder: 0,
name: 'xxx药品', name: 'ccc医疗器械',
remark: '三', creator: '三',
updater: '王四',
remark: '医疗器械描述',
dataScope: 2, dataScope: 2,
createdAt: '2022-04-01 11:38:39', createdAt: '2022-04-01 11:38:39',
updatedAt: '2022-04-06 09:53:40', updatedAt: '2022-04-06 09:53:40',
}, },
]); ];
export default defineComponent({ // 响应式状态
name: 'apiV1SystemRoleList', const tableData = reactive<TableState>({
components: { EditRole, ImportDialog, ExportDialog }, data: [],
setup() { total: 0,
const { proxy } = getCurrentInstance() as any; loading: false,
const addRoleRef = ref(); param: {
const editRoleRef = ref(); productName: '',
const importDialogRef = ref(); roleStatus: '',
const exportDialogRef = ref(); pageNum: 1,
const dataScopeRef = ref(); pageSize: 10,
const state = reactive<TableDataState>({
tableData: {
data: [],
total: 0,
loading: false,
param: {
roleName: '',
roleStatus: '',
pageNum: 1,
pageSize: 10,
},
},
});
// 初始化表格数据
const initTableData = () => {
roleList();
};
const roleList = () => {
const data: Array<TableData> = [];
getRoleList(state.tableData.param).then((res) => {
const list = res.data.list ?? [];
list.map((item: TableData) => {
data.push({
id: item.id,
status: item.status,
listOrder: item.listOrder,
name: item.name,
remark: item.remark,
dataScope: item.dataScope,
createdAt: item.createdAt,
});
});
state.tableData.data = dateList.value;
state.tableData.total = dateList.value.length;
});
};
// 打开新增角色弹窗
const onOpenAddRole = () => {
editRoleRef.value.openDialog();
};
// 打开修改角色弹窗
const onOpenEditRole = (row: Object) => {
editRoleRef.value.openDialog(toRaw(row));
};
// 打开导入弹窗
const onOpenImport = () => {
importDialogRef.value.openDialog();
};
// 打开导出弹窗
const onOpenExport = () => {
exportDialogRef.value.openDialog(state.tableData.data);
};
// 删除角色
const onRowDel = (row: any) => {
ElMessageBox.confirm(`此操作将永久删除角色:"${row.name}",是否继续?`, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
deleteRole(row.id).then(() => {
ElMessage.success('删除成功');
proxy.$refs['editRoleRef']?.resetMenuSession?.();
roleList();
});
})
.catch(() => {});
};
// 分页改变
const onHandleSizeChange = (val: number) => {
state.tableData.param.pageSize = val;
};
// 分页改变
const onHandleCurrentChange = (val: number) => {
state.tableData.param.pageNum = val;
};
// 页面加载时
onMounted(() => {
initTableData();
});
return {
addRoleRef,
editRoleRef,
importDialogRef,
exportDialogRef,
dataScopeRef,
onOpenAddRole,
onOpenEditRole,
onOpenImport,
onOpenExport,
onRowDel,
onHandleSizeChange,
onHandleCurrentChange,
roleList,
...toRefs(state),
};
}, },
}); });
// 组件引用
const editRoleRef = ref<InstanceType<typeof EditRole>>();
const importDialogRef = ref<InstanceType<typeof ImportDialog>>();
const exportDialogRef = ref<InstanceType<typeof ExportDialog>>();
/**
* 获取产品列表
*/
const getProductList = async () => {
try {
tableData.loading = true;
// 实际API调用 - 注释掉模拟数据后取消注释
// const res = await getRoleList(tableData.param);
// if (res.data?.list) {
// tableData.data = res.data.list;
// tableData.total = res.data.total;
// }
// 模拟数据 - 添加延迟模拟网络请求
await new Promise((resolve) => setTimeout(resolve, 500));
// 根据产品名称过滤数据
const filteredData = mockProductData.filter((item) => !tableData.param.productName || item.name.includes(tableData.param.productName));
tableData.data = filteredData;
tableData.total = filteredData.length;
} catch (error) {
console.error('获取产品列表失败:', error);
ElMessage.error('获取产品列表失败');
} finally {
tableData.loading = false;
}
};
/**
* 处理搜索
*/
const handleSearch = () => {
tableData.param.pageNum = 1; // 重置到第一页
getProductList();
};
/**
* 处理分页变化
* @param pagination - 分页参数
*/
const handlePaginationChange = (pagination: { page: number; limit: number }) => {
tableData.param.pageNum = pagination.page;
tableData.param.pageSize = pagination.limit;
getProductList();
};
/**
* 打开新增产品对话框
*/
const onOpenAddRole = () => {
if (editRoleRef.value) {
editRoleRef.value.openDialog();
}
};
/**
* 打开编辑产品对话框
* @param row - 产品数据
*/
const onOpenEditRole = (row: ProductData) => {
if (editRoleRef.value) {
editRoleRef.value.openDialog(row);
}
};
/**
* 打开导入对话框
*/
const onOpenImport = () => {
if (importDialogRef.value) {
importDialogRef.value.openDialog();
}
};
/**
* 打开导出对话框
*/
const onOpenExport = () => {
if (exportDialogRef.value) {
exportDialogRef.value.openDialog(tableData.data);
}
};
/**
* 删除产品
* @param row - 产品数据
*/
const onRowDel = async (row: ProductData) => {
try {
await ElMessageBox.confirm(`此操作将永久删除产品:"${row.name}",是否继续?`, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
});
// 实际删除操作
// await deleteRole(row.id);
// 模拟删除成功
ElMessage.success('删除成功');
// 刷新列表
await getProductList();
} catch (error) {
// 用户取消操作,不处理
console.log('用户取消删除操作');
}
};
/**
* 初始化表格数据
*/
const initTableData = () => {
getProductList();
};
// 生命周期 - 页面加载时初始化数据
onMounted(() => {
initTableData();
});
</script> </script>
<style scoped lang="scss">
.system-role-container {
padding: 20px;
}
.system-user-search {
margin-bottom: 15px;
:deep(.el-form-item) {
margin-bottom: 0;
}
}
.mb15 {
margin-bottom: 15px;
}
.ml10 {
margin-left: 10px;
}
.w-50 {
width: 240px;
}
// 表格操作按钮样式
:deep(.el-table) {
.el-button {
margin: 0 2px;
&.el-button--small {
padding: 6px 8px;
}
}
}
// 分页样式
:deep(.pagination-container) {
margin-top: 15px;
padding: 10px 0;
}
</style>

View File

@@ -5,9 +5,9 @@
<el-form :model="tableData.param" ref="queryRef" :inline="true" label-width="68px"> <el-form :model="tableData.param" ref="queryRef" :inline="true" label-width="68px">
<el-form-item label="客服平台" prop="status" style="width: 200px"> <el-form-item label="客服平台" prop="status" style="width: 200px">
<el-select v-model="tableData.param.status" placeholder="小红书" clearable size="default" style="width: 240px"> <el-select v-model="tableData.param.status" placeholder="小红书" clearable size="default" style="width: 240px">
<el-option :label="'小红书'" :value="''" /> <el-option label="小红书" value="xiaohongshu" />
<el-option :label="'抖音'" :value="''" /> <el-option label="抖音" value="douyin" />
<el-option :label="'快手'" :value="''" /> <el-option label="快手" value="kuaishou" />
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -36,25 +36,30 @@
</el-icon> </el-icon>
重置 重置
</el-button> </el-button>
<el-button size="default" type="info" class="ml10" @click="handleExport">
<el-icon>
<ele-Download />
</el-icon>
导出
</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<el-table :data="tableData.data" style="width: 100%" @selection-change="handleSelectionChange"> <el-table :data="tableData.data" style="width: 100%">
<!-- <el-table-column type="selection" width="55" align="center" /> --> <!-- <el-table-column type="selection" width="55" align="center" /> -->
<el-table-column label="日期" align="center" prop="loginTime" width="180" /> <el-table-column label="日期" align="center" prop="loginTime" width="180" />
<el-table-column label="客服平台" align="center" prop="platform" /> <el-table-column label="客服平台" align="center" prop="platform" />
<el-table-column label="客服ID" align="center" prop="infoId" /> <el-table-column label="客服ID" align="center" prop="infoId" />
<el-table-column label="客服姓名" align="center" prop="loginName" /> <el-table-column label="客服姓名" align="center" prop="loginName" />
<el-table-column label="进线人数" align="center" prop="ipaddr" width="130" :show-overflow-tooltip="true" /> <el-table-column label="进线人数" align="center" prop="ipaddr" width="130" :show-overflow-tooltip="true" />
<el-table-column label="开口人数" align="center" prop="loginLocation" :s0ow-overflow-tooltip="true" /> <el-table-column label="开口人数" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
<el-table-column label="留资卡发送数量" align="center" prop="browser" /> <el-table-column label="留资卡发送数量" align="center" prop="browser" />
<el-table-column label="名片发送数" align="center" prop="os" /> <el-table-column label="名片发送数" align="center" prop="os" />
<!-- <el-table-column label="留资人数" align="center" prop="status" :formatter="statusFormat" /> -->
<el-table-column label="留资人数" align="center" prop="status" /> <el-table-column label="留资人数" align="center" prop="status" />
<el-table-column label="接待人数" align="center" prop="msg" /> <el-table-column label="接待人数" align="center" prop="msg" />
<el-table-column label="30秒回复率" alian="center" prop="module" /> <el-table-column label="30秒回复率" align="center" prop="module" />
<el-table-column label="60秒回复率" alian="center" prop="module" /> <el-table-column label="60秒回复率" align="center" prop="module" />
<el-table-column label="3分钟回复率" alian="center" prop="module" /> <el-table-column label="3分钟回复率" align="center" prop="module" />
</el-table> </el-table>
<pagination <pagination
v-show="tableData.total > 0" v-show="tableData.total > 0"
@@ -67,13 +72,11 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { toRefs, reactive, onMounted, ref, defineComponent, getCurrentInstance, unref } from 'vue'; import { ref, reactive, onMounted, nextTick } from 'vue';
import { ElMessageBox, ElMessage, FormInstance } from 'element-plus'; import { ElMessageBox, ElMessage, FormInstance } from 'element-plus';
import { logList, deleteLog, clearLog } from '/@/api/system/monitor/loginLog';
import { log } from 'console';
// 定义接口来定义对象的类型 // 定义接口
interface TableDataRow { interface TableDataRow {
infoId: number; infoId: number;
loginName: string; loginName: string;
@@ -85,272 +88,162 @@ interface TableDataRow {
msg: string; msg: string;
loginTime: string; loginTime: string;
module: string; module: string;
platform: string;
} }
interface TableDataState { interface TableDataParam {
ids: number[]; pageNum: number;
tableData: { pageSize: number;
data: Array<TableDataRow>; dateRange: string[];
total: number; status: string;
loading: boolean; ipaddr: string;
param: { loginLocation: any;
pageNum: number; userName: string;
pageSize: number;
dateRange: string[];
status: string;
ipaddr: string;
loginLocation: any;
userName: string;
};
};
} }
export default defineComponent({ interface TableData {
name: 'apiV1SystemLoginLogList', data: Array<TableDataRow>;
setup() { total: number;
//模拟数据 loading: boolean;
const dateList = ref([ param: TableDataParam;
{ }
infoId: 434569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '抖音',
},
{
infoId: 424569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '小红书',
},
{
infoId: 414569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '快手',
},
{
infoId: 404569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '小红书',
},
{
infoId: 394569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '小红书',
},
{
infoId: 384569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '快手',
},
{
infoId: 374569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '小红书',
},
{
infoId: 364569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '抖音',
},
{
infoId: 354569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '小红书',
},
{
infoId: 344569527,
loginName: 'demo',
ipaddr: '0',
loginLocation: '0',
browser: '0',
os: '0',
status: 0,
msg: '0',
loginTime: '2025-11-22',
module: '0',
platform: '小红书',
},
]);
const { proxy } = getCurrentInstance() as any; // 模拟数据
const queryRef = ref(); const dateList: TableDataRow[] = [
const { admin_login_status } = proxy.useDict('admin_login_status'); {
const state = reactive<TableDataState>({ infoId: 434569527,
ids: [], loginName: 'demo',
tableData: { ipaddr: '10',
data: [], loginLocation: '8',
total: 0, browser: '5',
loading: false, os: '3',
param: { status: 2,
pageNum: 1, msg: '15',
pageSize: 10, loginTime: '2025-11-22',
dateRange: [], module: '95%',
status: '', platform: '抖音',
ipaddr: '', },
loginLocation: '', {
userName: '', infoId: 424569527,
}, loginName: 'demo',
}, ipaddr: '12',
}); loginLocation: '9',
// 初始化表格数据 browser: '6',
const initTableData = () => { os: '4',
dataList(); status: 3,
}; msg: '18',
loginTime: '2025-11-22',
module: '92%',
platform: '小红书',
},
{
infoId: 414569527,
loginName: 'demo',
ipaddr: '8',
loginLocation: '6',
browser: '4',
os: '2',
status: 1,
msg: '12',
loginTime: '2025-11-22',
module: '98%',
platform: '快手',
},
];
const dataList = () => { // 响应式数据
// logList(state.tableData.param).then((res: any) => { const queryRef = ref<FormInstance>();
// state.tableData.data = res.data.list; const ids = ref<number[]>([]);
// console.log(state.tableData.data, '11111'); const tableData = reactive<TableData>({
// state.tableData.total = res.data.total; data: [],
total: 0,
// }); loading: false,
param: {
state.tableData.data = dateList.value; pageNum: 1,
console.log(state.tableData.data, '11111'); pageSize: 10,
state.tableData.total = dateList.value.length; dateRange: [],
}; status: '',
// 删除日志 ipaddr: '',
const onRowDel = (row: TableDataRow) => { loginLocation: '',
let msg = '你确定要删除所选数据?'; userName: '',
let ids: number[] = [];
if (row) {
msg = `此操作将永久删除:“${row.loginName}”,是否继续?`;
ids = [row.infoId];
4569527;
} else {
ids = state.ids;
}
if (ids.length === 0) {
ElMessage.error('请选择要删除的数据。');
return;
}
ElMessageBox.confirm(msg, '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
deleteLog(ids).then(() => {
ElMessage.success('删除成功');
dataList();
});
})
.catch(() => {});
};
// 清空日志
const onRowClear = () => {
ElMessageBox.confirm('你确定要删除所选数据?', '提示', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
.then(() => {
clearLog().then(() => {
ElMessage.success('清除成功');
dataList();
});
})
.catch(() => {});
};
// 页面加载时
onMounted(() => {
initTableData();
});
/** 重置按钮操作 */
const resetQuery = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
dataList();
};
// 多选框选中数据
const handleSelectionChange = (selection: TableDataRow[]) => {
state.ids = selection.map((item) => item.infoId);
4569527;
};
// 登录状态字典翻译
const statusFormat = (row: TableDataRow) => {
return proxy.selectDictLabel(unref(admin_login_status), row.status);
};
return {
queryRef,
onRowDel,
dataList,
resetQuery,
handleSelectionChange,
statusFormat,
onRowClear,
admin_login_status,
...toRefs(state),
};
}, },
}); });
// 方法定义
// 获取数据列表
const dataList = async () => {
try {
tableData.loading = true;
// 实际API调用
// const res = await logList(tableData.param);
// tableData.data = res.data.list;
// tableData.total = res.data.total;
// 模拟数据 - 添加延迟避免DOM操作错误
await new Promise((resolve) => setTimeout(resolve, 100));
tableData.data = dateList;
tableData.total = dateList.length;
} catch (error) {
console.error('获取数据失败:', error);
ElMessage.error('获取数据失败');
} finally {
tableData.loading = false;
}
};
// 重置查询
const resetQuery = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
tableData.param = {
pageNum: 1,
pageSize: 10,
dateRange: [],
status: '',
ipaddr: '',
loginLocation: '',
userName: '',
};
nextTick(() => {
dataList();
});
};
// 导出数据
const handleExport = () => {
ElMessage.info('导出功能开发中');
};
// 初始化表格数据
const initTableData = () => {
dataList();
};
// 生命周期
onMounted(() => {
// 使用 nextTick 确保DOM已渲染完成
nextTick(() => {
initTableData();
});
});
</script> </script>
<style scoped>
.system-dic-container {
padding: 20px;
}
.system-user-search {
margin-bottom: 15px;
}
.mb15 {
margin-bottom: 15px;
}
.ml10 {
margin-left: 10px;
}
</style>

View File

@@ -5,264 +5,228 @@
<el-row :gutter="35"> <el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20"> <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="标签" prop="name"> <el-form-item label="标签" prop="name">
<el-input placeholder="小红书" clearable></el-input> <el-input v-model="formData.name" placeholder="请输入标签名称" clearable />
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20"> <!-- 富文本编辑器 -->
<el-form-item label="排序">
<el-input-number v-model="formData.listOrder" :min="0" controls-position="right" placeholder="请输入排序"
class="w100" />
</el-form-item>
</el-col> -->
<!-- <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="客服状态">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" inline-prompt active-text="启"
inactive-text=""></el-switch>
</el-form-item>
</el-col> -->
<!-- 新增富文本编辑器 -->
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20"> <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="产品详情" prop="content"> <el-form-item label="产品详情" prop="content">
<Editor height="400px" placeholder="请输入产品详细描述..." /> <Editor v-model="formData.content" height="400px" placeholder="请输入产品详细描述..." />
</el-form-item> </el-form-item>
</el-col> </el-col>
<!-- <el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="菜单权限">
<el-row :gutter="35">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event)">展开/折叠</el-checkbox>
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event)">全选/全不选</el-checkbox>
<el-checkbox v-model="menuCheckStrictly" @change="handleCheckedTreeConnect($event)">父子联动</el-checkbox>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-tree :data="menuData" ref="menuRef" :props="menuProps" :default-checked-keys="formData.menuIds"
node-key="id" show-checkbox class="menu-data-tree tree-border"
:check-strictly="!menuCheckStrictly" />
</el-col>
</el-row>
</el-form-item>
</el-col> -->
</el-row> </el-row>
</el-form> </el-form>
<template #footer> <template #footer>
<span class="dialog-footer"> <span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button> <el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSubmit" size="default" :loading="loading">{{ formData.id === 0 ? '新 增' : '修改' }}</el-button> <el-button type="primary" @click="onSubmit" size="default" :loading="loading">
{{ formData.id === 0 ? '新 增' : '修 改' }}
</el-button>
</span> </span>
</template> </template>
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import { reactive, toRefs, defineComponent, ref, getCurrentInstance, unref } from 'vue'; import { ref, reactive, toRefs, nextTick } from 'vue';
import { addRole, editRole, getRole, getRoleParams } from '/@/api/system/role'; import { ElMessage, type FormInstance, type FormRules } from 'element-plus';
import { ElMessage } from 'element-plus'; import { addRole, editRole, getRole } from '/@/api/customerService/script';
import { refreshBackEndControlRoutes } from '/@/router/backEnd';
// 引入富文本编辑器组件
import Editor from '/@/components/editor/index.vue'; import Editor from '/@/components/editor/index.vue';
// 定义接口来定义对象的类型 // 定义类型接口
interface MenuDataTree { interface MenuDataTree {
id: number; id: number;
pid: number; pid: number;
title: string; title: string;
children?: MenuDataTree[]; children?: MenuDataTree[];
} }
interface DialogRow { interface DialogRow {
id: number; id: number;
name: string; name: string;
status: number; status: number;
listOrder: number; listOrder: number;
remark: string; remark: string;
menuIds: Array<number>; menuIds: number[];
} content?: string;
interface RoleState {
loading: boolean;
isShowDialog: boolean;
formData: DialogRow;
menuData: Array<MenuDataTree>;
menuExpand: boolean;
menuNodeAll: boolean;
menuCheckStrictly: boolean;
menuProps: {
children: string;
label: string;
};
rules: object;
} }
export default defineComponent({ // 定义事件
name: 'systemEditRole', const emit = defineEmits<{
components: { (e: 'getRoleList'): void;
Editor, // 注册富文本组件 }>();
},
setup(props, { emit }) { // 响应式数据
const { proxy } = getCurrentInstance() as any; const state = reactive({
const formRef = ref<HTMLElement | null>(null); loading: false,
const menuRef = ref(); isShowDialog: false,
const state = reactive<RoleState>({ formData: {
loading: false, id: 0,
isShowDialog: false, name: '',
formData: { status: 1,
id: 0, listOrder: 0,
name: '', remark: '',
status: 1, menuIds: [],
listOrder: 0, content: '',
remark: '', } as DialogRow,
menuIds: [], menuData: [] as MenuDataTree[],
}, menuExpand: false,
// 表单校验 menuNodeAll: false,
rules: { menuCheckStrictly: false,
name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }], });
},
menuData: [], // 表单验证规则
menuExpand: false, const rules: FormRules = {
menuNodeAll: false, name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }],
menuCheckStrictly: false, content: [{ required: true, message: '产品详情不能为空', trigger: 'blur' }],
menuProps: { };
children: 'children',
label: 'title', // 模板引用
}, const formRef = ref<FormInstance>();
}); const menuRef = ref();
// 打开弹窗
const openDialog = (row?: DialogRow) => { // 解构状态数据
resetForm(); const { loading, isShowDialog, formData, menuData, menuExpand, menuNodeAll, menuCheckStrictly } = toRefs(state);
getMenuData();
if (row) { // 菜单配置
getRole(row.id).then((res: any) => { const menuProps = {
if (res.data.role) { children: 'children',
state.formData = res.data.role; label: 'title',
state.formData.menuIds = res.data.menuIds ?? []; };
}
}); /**
} * 打开对话框
state.isShowDialog = true; * @param row - 可选的编辑数据
}; */
// 关闭弹窗 const openDialog = (row?: DialogRow) => {
const closeDialog = () => { resetForm();
state.isShowDialog = false;
}; if (row) {
// 取消 // 编辑模式:获取角色详情
const onCancel = () => { getRole(row.id)
closeDialog(); .then((res: any) => {
}; if (res.data?.role) {
// 新增 state.formData = {
const onSubmit = () => { ...res.data.role,
const formWrap = unref(formRef) as any; content: res.data.role.content || '',
if (!formWrap) return; menuIds: res.data.menuIds ?? [],
formWrap.validate((valid: boolean) => { };
if (valid) {
state.loading = true;
state.formData.menuIds = getMenuAllCheckedKeys();
if (state.formData.id === 0) {
//添加
addRole(state.formData)
.then(() => {
ElMessage.success('角色添加成功');
closeDialog(); // 关闭弹窗
resetMenuSession();
emit('getRoleList');
})
.finally(() => {
state.loading = false;
});
} else {
//修改
editRole(state.formData)
.then(() => {
ElMessage.success('角色修改成功');
closeDialog(); // 关闭弹窗
resetMenuSession();
emit('getRoleList');
})
.finally(() => {
state.loading = false;
});
}
} }
})
.catch((error) => {
console.error('获取角色详情失败:', error);
ElMessage.error('获取角色详情失败');
}); });
}; }
// 获取菜单结构数据
const getMenuData = () => {
getRoleParams().then((res: any) => {
state.menuData = proxy.handleTree(res.data.menu, 'id', 'pid');
});
};
const resetForm = () => {
state.menuCheckStrictly = false;
state.menuExpand = false;
state.menuNodeAll = false;
state.formData = {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
menuIds: [],
};
};
/** 树权限(展开/折叠)*/
const handleCheckedTreeExpand = (value: any) => {
let treeList = state.menuData;
for (let i = 0; i < treeList.length; i++) {
menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
}
};
/** 树权限(全选/全不选) */ state.isShowDialog = true;
const handleCheckedTreeNodeAll = (value: any) => { };
menuRef.value.setCheckedNodes(value ? state.menuData : []);
};
/** 树权限(父子联动) */ /**
const handleCheckedTreeConnect = (value: any) => { * 关闭对话框
state.menuCheckStrictly = value ? true : false; */
}; const closeDialog = () => {
state.isShowDialog = false;
};
/** 所有菜单节点数据 */ /**
function getMenuAllCheckedKeys() { * 取消操作
// 目前被选中的菜单节点 */
let checkedKeys = menuRef.value.getCheckedKeys(); const onCancel = () => {
// 半选中的菜单节点 closeDialog();
let halfCheckedKeys = menuRef.value.getHalfCheckedKeys(); };
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys);
return checkedKeys; /**
* 提交表单
*/
const onSubmit = async () => {
if (!formRef.value) return;
try {
// 表单验证
const valid = await formRef.value.validate();
if (!valid) return;
state.loading = true;
if (state.formData.id === 0) {
// 新增模式
await addRole(state.formData);
ElMessage.success('添加成功');
} else {
// 编辑模式
await editRole(state.formData);
ElMessage.success('修改成功');
} }
// 重置菜单session // 关闭对话框并刷新列表
const resetMenuSession = () => { closeDialog();
refreshBackEndControlRoutes(); emit('getRoleList');
}; } catch (error) {
return { console.error('提交失败:', error);
openDialog, ElMessage.error('操作失败');
closeDialog, } finally {
onCancel, state.loading = false;
onSubmit, }
menuRef, };
formRef,
handleCheckedTreeExpand, /**
handleCheckedTreeNodeAll, * 重置表单
handleCheckedTreeConnect, */
resetMenuSession, const resetForm = () => {
...toRefs(state), state.menuCheckStrictly = false;
}; state.menuExpand = false;
}, state.menuNodeAll = false;
state.formData = {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
menuIds: [],
content: '',
};
// 重置表单验证状态
nextTick(() => {
formRef.value?.clearValidate();
});
};
// 暴露方法给父组件使用
defineExpose({
openDialog,
closeDialog,
}); });
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.system-edit-role-container {
:deep(.el-dialog__body) {
padding: 20px;
}
.mb20 {
margin-bottom: 20px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
align-items: center;
}
}
.tree-border { .tree-border {
margin-top: 5px; margin-top: 5px;
border: 1px solid #e5e6e7 !important; border: 1px solid #e5e6e7;
border-radius: 4px; border-radius: 4px;
} }
.system-edit-role-container { .menu-data-tree {
.menu-data-tree { border: var(--el-input-border, var(--el-border-base));
border: var(--el-input-border, var(--el-border-base)); border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
border-radius: var(--el-input-border-radius, var(--el-border-radius-base)); padding: 5px;
padding: 5px;
}
} }
</style> </style>

View File

@@ -47,7 +47,7 @@
import { toRefs, reactive, onMounted, ref, defineComponent, toRaw, getCurrentInstance } from 'vue'; import { toRefs, reactive, onMounted, ref, defineComponent, toRaw, getCurrentInstance } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import EditRole from '/@/views/customerService/script/component/editRole.vue'; import EditRole from '/@/views/customerService/script/component/editRole.vue';
import { deleteRole, getRoleList } from '/@/api/system/role'; import { deleteRole, getRoleList } from '/@/api/customerService/script';
// 定义接口来定义对象的类型 // 定义接口来定义对象的类型
interface TableData { interface TableData {
id: number; id: number;

View File

@@ -0,0 +1,273 @@
<template>
<div class="system-edit-role-container">
<el-dialog :title="(formData.id === 0 ? '添加' : '修改') + '角色'" v-model="isShowDialog" width="769px">
<el-form ref="formRef" :model="formData" :rules="rules" size="default" label-width="90px">
<el-row :gutter="35">
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="角色名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入角色名称" clearable></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="排序">
<el-input-number v-model="formData.listOrder" :min="0" controls-position="right" placeholder="请输入排序" class="w100" />
</el-form-item>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
<el-form-item label="角色状态">
<el-switch
v-model="formData.status"
:active-value="1"
:inactive-value="0"
inline-prompt
active-text=""
inactive-text=""
></el-switch>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="角色描述">
<el-input v-model="formData.remark" type="textarea" placeholder="请输入角色描述" maxlength="150"></el-input>
</el-form-item>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-form-item label="菜单权限">
<el-row :gutter="35">
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24">
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event)">展开/折叠</el-checkbox>
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event)">全选/全不选</el-checkbox>
<el-checkbox v-model="menuCheckStrictly" @change="handleCheckedTreeConnect($event)">父子联动</el-checkbox>
</el-col>
<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
<el-tree
:data="menuData"
ref="menuRef"
:props="menuProps"
:default-checked-keys="formData.menuIds"
node-key="id"
show-checkbox
class="menu-data-tree tree-border"
:check-strictly="!menuCheckStrictly"
/>
</el-col>
</el-row>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default"> </el-button>
<el-button type="primary" @click="onSubmit" size="default" :loading="loading">{{ formData.id === 0 ? '新 增' : '修 改' }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { reactive, toRefs, defineComponent, ref, getCurrentInstance, unref } from 'vue';
import { addRole, editRole, getRole, getRoleParams } from '/@/api/system/role';
import { ElMessage } from 'element-plus';
import { refreshBackEndControlRoutes } from '/@/router/backEnd';
// 定义接口来定义对象的类型
interface MenuDataTree {
id: number;
pid: number;
title: string;
children?: MenuDataTree[];
}
interface DialogRow {
id: number;
name: string;
status: number;
listOrder: number;
remark: string;
menuIds: Array<number>;
}
interface RoleState {
loading: boolean;
isShowDialog: boolean;
formData: DialogRow;
menuData: Array<MenuDataTree>;
menuExpand: boolean;
menuNodeAll: boolean;
menuCheckStrictly: boolean;
menuProps: {
children: string;
label: string;
};
rules: object;
}
export default defineComponent({
name: 'systemEditRole',
setup(props, { emit }) {
const { proxy } = getCurrentInstance() as any;
const formRef = ref<HTMLElement | null>(null);
const menuRef = ref();
const state = reactive<RoleState>({
loading: false,
isShowDialog: false,
formData: {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
menuIds: [],
},
// 表单校验
rules: {
name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
},
menuData: [],
menuExpand: false,
menuNodeAll: false,
menuCheckStrictly: false,
menuProps: {
children: 'children',
label: 'title',
},
});
// 打开弹窗
const openDialog = (row?: DialogRow) => {
resetForm();
getMenuData();
if (row) {
getRole(row.id).then((res: any) => {
if (res.data.role) {
state.formData = res.data.role;
state.formData.menuIds = res.data.menuIds ?? [];
}
});
}
state.isShowDialog = true;
};
// 关闭弹窗
const closeDialog = () => {
state.isShowDialog = false;
};
// 取消
const onCancel = () => {
closeDialog();
};
// 新增
const onSubmit = () => {
const formWrap = unref(formRef) as any;
if (!formWrap) return;
formWrap.validate((valid: boolean) => {
if (valid) {
state.loading = true;
state.formData.menuIds = getMenuAllCheckedKeys();
if (state.formData.id === 0) {
//添加
addRole(state.formData)
.then(() => {
ElMessage.success('角色添加成功');
closeDialog(); // 关闭弹窗
resetMenuSession();
emit('getRoleList');
})
.finally(() => {
state.loading = false;
});
} else {
//修改
editRole(state.formData)
.then(() => {
ElMessage.success('角色修改成功');
closeDialog(); // 关闭弹窗
resetMenuSession();
emit('getRoleList');
})
.finally(() => {
state.loading = false;
});
}
}
});
};
// 获取菜单结构数据
const getMenuData = () => {
getRoleParams().then((res: any) => {
state.menuData = proxy.handleTree(res.data.menu, 'id', 'pid');
});
};
const resetForm = () => {
state.menuCheckStrictly = false;
state.menuExpand = false;
state.menuNodeAll = false;
state.formData = {
id: 0,
name: '',
status: 1,
listOrder: 0,
remark: '',
menuIds: [],
};
};
/** 树权限(展开/折叠)*/
const handleCheckedTreeExpand = (value: any) => {
let treeList = state.menuData;
for (let i = 0; i < treeList.length; i++) {
menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
}
};
/** 树权限(全选/全不选) */
const handleCheckedTreeNodeAll = (value: any) => {
menuRef.value.setCheckedNodes(value ? state.menuData : []);
};
/** 树权限(父子联动) */
const handleCheckedTreeConnect = (value: any) => {
state.menuCheckStrictly = value ? true : false;
};
/** 所有菜单节点数据 */
function getMenuAllCheckedKeys() {
// 目前被选中的菜单节点
let checkedKeys = menuRef.value.getCheckedKeys();
// 半选中的菜单节点
let halfCheckedKeys = menuRef.value.getHalfCheckedKeys();
checkedKeys.unshift.apply(checkedKeys, halfCheckedKeys);
return checkedKeys;
}
// 重置菜单session
const resetMenuSession = () => {
refreshBackEndControlRoutes();
};
return {
openDialog,
closeDialog,
onCancel,
onSubmit,
menuRef,
formRef,
handleCheckedTreeExpand,
handleCheckedTreeNodeAll,
handleCheckedTreeConnect,
resetMenuSession,
...toRefs(state),
};
},
});
</script>
<style scoped lang="scss">
.tree-border {
margin-top: 5px;
border: 1px solid #e5e6e7 !important;
border-radius: 4px;
}
.system-edit-role-container {
.menu-data-tree {
border: var(--el-input-border, var(--el-border-base));
border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
padding: 5px;
}
}
</style>

View File

@@ -2,31 +2,31 @@
<div class="system-role-container"> <div class="system-role-container">
<el-card shadow="hover"> <el-card shadow="hover">
<div class="system-user-search mb15"> <div class="system-user-search mb15">
<el-form :inline="true"> <el-form :inline="true">
<el-form-item label="角色名称"> <el-form-item label="角色名称">
<el-input size="default" v-model="tableData.param.roleName" placeholder="请输入角色名称" class="w-50 m-2" clearable/> <el-input size="default" v-model="tableData.param.roleName" placeholder="请输入角色名称" class="w-50 m-2" clearable />
</el-form-item> </el-form-item>
<el-form-item label="状态"> <el-form-item label="状态">
<el-select size="default" placeholder="请选择状态" class="w-50 m-2" v-model="tableData.param.roleStatus" clearable> <el-select size="default" placeholder="请选择状态" class="w-50 m-2" v-model="tableData.param.roleStatus" clearable>
<el-option label="启用" value="1" /> <el-option label="启用" value="1" />
<el-option label="禁用" value="0" /> <el-option label="禁用" value="0" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button size="default" type="primary" class="ml10" @click="roleList"> <el-button size="default" type="primary" class="ml10" @click="roleList">
<el-icon> <el-icon>
<ele-Search /> <ele-Search />
</el-icon> </el-icon>
查询 查询
</el-button> </el-button>
<el-button size="default" type="success" class="ml10" @click="onOpenAddRole"> <el-button size="default" type="success" class="ml10" @click="onOpenAddRole">
<el-icon> <el-icon>
<ele-FolderAdd /> <ele-FolderAdd />
</el-icon> </el-icon>
新增角色 新增角色
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
<el-table :data="tableData.data" style="width: 100%"> <el-table :data="tableData.data" style="width: 100%">
<el-table-column type="index" label="序号" width="60" /> <el-table-column type="index" label="序号" width="60" />
@@ -34,45 +34,49 @@
<el-table-column prop="listOrder" label="排序" show-overflow-tooltip></el-table-column> <el-table-column prop="listOrder" label="排序" show-overflow-tooltip></el-table-column>
<el-table-column prop="status" label="角色状态" show-overflow-tooltip> <el-table-column prop="status" label="角色状态" show-overflow-tooltip>
<template #default="scope"> <template #default="scope">
<el-tag type="success" v-if="scope.row.status===1">启用</el-tag> <el-tag type="success" v-if="scope.row.status === 1">启用</el-tag>
<el-tag type="info" v-else>禁用</el-tag> <el-tag type="info" v-else>禁用</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="remark" label="角色描述" show-overflow-tooltip></el-table-column> <el-table-column prop="remark" label="角色描述" show-overflow-tooltip></el-table-column>
<el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip></el-table-column> <el-table-column prop="createdAt" label="创建时间" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="220"> <el-table-column label="操作" width="220">
<template #default="scope"> <template #default="scope">
<el-button size="small" text type="primary" @click="onOpenEditRole(scope.row)"><el-icon><ele-EditPen /></el-icon>修改</el-button> <el-button size="small" text type="primary" @click="onOpenEditRole(scope.row)"
<el-button size="small" text type="primary" @click="onRowDel(scope.row)"><el-icon><ele-DeleteFilled /></el-icon>删除</el-button> ><el-icon><ele-EditPen /></el-icon>修改</el-button
>
<el-button size="small" text type="primary" @click="onRowDel(scope.row)"
><el-icon><ele-DeleteFilled /></el-icon>删除</el-button
>
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
<pagination <pagination
v-show="tableData.total>0" v-show="tableData.total > 0"
:total="tableData.total" :total="tableData.total"
v-model:page="tableData.param.pageNum" v-model:page="tableData.param.pageNum"
v-model:limit="tableData.param.pageSize" v-model:limit="tableData.param.pageSize"
@pagination="roleList" @pagination="roleList"
/> />
</el-card> </el-card>
<EditRole ref="editRoleRef" @getRoleList="roleList"/> <EditRole ref="editRoleRef" @getRoleList="roleList" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import {toRefs, reactive, onMounted, ref, defineComponent, toRaw,getCurrentInstance} from 'vue'; import { toRefs, reactive, onMounted, ref, defineComponent, toRaw, getCurrentInstance } from 'vue';
import { ElMessageBox, ElMessage } from 'element-plus'; import { ElMessageBox, ElMessage } from 'element-plus';
import EditRole from '/@/views/system/role/component/editRole.vue'; import EditRole from '/@/views/system/role/component/editRole.vue';
import {deleteRole, getRoleList} from "/@/api/system/role"; import { deleteRole, getRoleList } from '/@/api/system/role';
// 定义接口来定义对象的类型 // 定义接口来定义对象的类型
interface TableData { interface TableData {
id:number; id: number;
status: number; status: number;
listOrder: number; listOrder: number;
name: string; name: string;
remark: string; remark: string;
dataScope:number; dataScope: number;
createdAt: string; createdAt: string;
} }
interface TableDataState { interface TableDataState {
tableData: { tableData: {
@@ -80,8 +84,8 @@ interface TableDataState {
total: number; total: number;
loading: boolean; loading: boolean;
param: { param: {
roleName:string; roleName: string;
roleStatus:string; roleStatus: string;
pageNum: number; pageNum: number;
pageSize: number; pageSize: number;
}; };
@@ -90,20 +94,20 @@ interface TableDataState {
export default defineComponent({ export default defineComponent({
name: 'apiV1SystemRoleList', name: 'apiV1SystemRoleList',
components: {EditRole}, components: { EditRole },
setup() { setup() {
const {proxy} = getCurrentInstance() as any; const { proxy } = getCurrentInstance() as any;
const addRoleRef = ref(); const addRoleRef = ref();
const editRoleRef = ref(); const editRoleRef = ref();
const dataScopeRef =ref(); const dataScopeRef = ref();
const state = reactive<TableDataState>({ const state = reactive<TableDataState>({
tableData: { tableData: {
data: [], data: [],
total: 0, total: 0,
loading: false, loading: false,
param: { param: {
roleName:'', roleName: '',
roleStatus:'', roleStatus: '',
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
}, },
@@ -111,30 +115,30 @@ export default defineComponent({
}); });
// 初始化表格数据 // 初始化表格数据
const initTableData = () => { const initTableData = () => {
roleList() roleList();
};
const roleList = () => {
const data: Array<TableData> = [];
getRoleList(state.tableData.param).then((res) => {
const list = res.data.list ?? [];
list.map((item: TableData) => {
data.push({
id: item.id,
status: item.status,
listOrder: item.listOrder,
name: item.name,
remark: item.remark,
dataScope: item.dataScope,
createdAt: item.createdAt,
});
});
state.tableData.data = data;
state.tableData.total = res.data.total;
});
}; };
const roleList = ()=>{
const data: Array<TableData> = [];
getRoleList(state.tableData.param).then(res=>{
const list = res.data.list??[]
list.map((item:TableData)=>{
data.push({
id:item.id,
status: item.status,
listOrder: item.listOrder,
name: item.name,
remark: item.remark,
dataScope:item.dataScope,
createdAt: item.createdAt,
});
})
state.tableData.data = data;
state.tableData.total = res.data.total;
})
};
// 打开新增角色弹窗 // 打开新增角色弹窗
const onOpenAddRole = () => { const onOpenAddRole = () => {
editRoleRef.value.openDialog(); editRoleRef.value.openDialog();
}; };
// 打开修改角色弹窗 // 打开修改角色弹窗
const onOpenEditRole = (row: Object) => { const onOpenEditRole = (row: Object) => {
@@ -149,11 +153,11 @@ export default defineComponent({
type: 'warning', type: 'warning',
}) })
.then(() => { .then(() => {
deleteRole(row.id).then(()=>{ deleteRole(row.id).then(() => {
ElMessage.success('删除成功'); ElMessage.success('删除成功');
proxy.$refs['editRoleRef'].resetMenuSession(); proxy.$refs['editRoleRef'].resetMenuSession();
roleList(); roleList();
}) });
}) })
.catch(() => {}); .catch(() => {});
}; };
@@ -172,13 +176,13 @@ export default defineComponent({
return { return {
addRoleRef, addRoleRef,
editRoleRef, editRoleRef,
dataScopeRef, dataScopeRef,
onOpenAddRole, onOpenAddRole,
onOpenEditRole, onOpenEditRole,
onRowDel, onRowDel,
onHandleSizeChange, onHandleSizeChange,
onHandleCurrentChange, onHandleCurrentChange,
roleList, roleList,
...toRefs(state), ...toRefs(state),
}; };
}, },