chore: initialize wygj monorepo
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="key键" prop="testKey">
|
||||
<el-input v-model="queryParams.testKey" placeholder="请输入key键" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="值" prop="value">
|
||||
<el-input v-model="queryParams.value" placeholder="请输入值" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['demo:demo:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['demo:demo:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['demo:demo:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()"
|
||||
>删除</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['demo:demo:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="demoList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="true" label="主键" align="center" prop="id" />
|
||||
<el-table-column label="部门id" align="center" prop="deptId" />
|
||||
<el-table-column label="用户id" align="center" prop="userId" />
|
||||
<el-table-column label="排序号" align="center" prop="orderNum" />
|
||||
<el-table-column label="key键" align="center" prop="testKey" />
|
||||
<el-table-column label="值" align="center" prop="value" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['demo:demo:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['demo:demo:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改测试单对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="demoFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="部门id" prop="deptId">
|
||||
<el-input v-model="form.deptId" placeholder="请输入部门id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户id" prop="userId">
|
||||
<el-input v-model="form.userId" placeholder="请输入用户id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="排序号" prop="orderNum">
|
||||
<el-input v-model="form.orderNum" placeholder="请输入排序号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="key键" prop="testKey">
|
||||
<el-input v-model="form.testKey" placeholder="请输入key键" />
|
||||
</el-form-item>
|
||||
<el-form-item label="值" prop="value">
|
||||
<el-input v-model="form.value" placeholder="请输入值" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Demo" lang="ts">
|
||||
import { listDemo, getDemo, delDemo, addDemo, updateDemo } from '@/api/demo/demo';
|
||||
import { DemoVO, DemoQuery, DemoForm } from '@/api/demo/demo/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const demoList = ref<DemoVO[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const demoFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: DemoForm = {
|
||||
id: undefined,
|
||||
deptId: undefined,
|
||||
userId: undefined,
|
||||
orderNum: undefined,
|
||||
testKey: undefined,
|
||||
value: undefined
|
||||
};
|
||||
const data = reactive<PageData<DemoForm, DemoQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
deptId: undefined,
|
||||
userId: undefined,
|
||||
orderNum: undefined,
|
||||
testKey: undefined,
|
||||
value: undefined
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
|
||||
deptId: [{ required: true, message: '部门id不能为空', trigger: 'blur' }],
|
||||
userId: [{ required: true, message: '用户id不能为空', trigger: 'blur' }],
|
||||
orderNum: [{ required: true, message: '排序号不能为空', trigger: 'blur' }],
|
||||
testKey: [{ required: true, message: 'key键不能为空', trigger: 'blur' }],
|
||||
value: [{ required: true, message: '值不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询测试单列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listDemo(queryParams.value);
|
||||
demoList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
demoFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: DemoVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加测试单';
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: DemoVO) => {
|
||||
reset();
|
||||
const _id = row?.id || ids.value[0];
|
||||
const res = await getDemo(_id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改测试单';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
demoFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.id) {
|
||||
await updateDemo(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addDemo(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('修改成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: DemoVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除测试单编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||
await delDemo(_ids);
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
await getList();
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'demo/demo/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`demo_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,259 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="树节点名" prop="treeName">
|
||||
<el-input v-model="queryParams.treeName" placeholder="请输入树节点名" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['demo:tree:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-table
|
||||
ref="treeTableRef"
|
||||
v-loading="loading"
|
||||
:data="treeList"
|
||||
row-key="id"
|
||||
border
|
||||
:default-expand-all="isExpandAll"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
>
|
||||
<el-table-column label="父id" align="center" prop="parentId" />
|
||||
<el-table-column label="部门id" align="center" prop="deptId" />
|
||||
<el-table-column label="用户id" align="center" prop="userId" />
|
||||
<el-table-column label="树节点名" align="center" prop="treeName" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['demo:tree:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="新增" placement="top">
|
||||
<el-button v-hasPermi="['demo:tree:add']" link type="primary" icon="Plus" @click="handleAdd(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['demo:tree:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<!-- 添加或修改测试树对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="treeFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="父id" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="treeOptions"
|
||||
:props="{ value: 'id', label: 'treeName', children: 'children' } as any"
|
||||
value-key="id"
|
||||
placeholder="请选择父id"
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门id" prop="deptId">
|
||||
<el-input v-model="form.deptId" placeholder="请输入部门id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户id" prop="userId">
|
||||
<el-input v-model="form.userId" placeholder="请输入用户id" />
|
||||
</el-form-item>
|
||||
<el-form-item label="值" prop="treeName">
|
||||
<el-input v-model="form.treeName" placeholder="请输入值" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Tree" lang="ts">
|
||||
import { listTree, getTree, delTree, addTree, updateTree } from '@/api/demo/tree';
|
||||
import { TreeVO, TreeQuery, TreeForm } from '@/api/demo/tree/types';
|
||||
|
||||
type TreeOption = {
|
||||
id: number;
|
||||
treeName: string;
|
||||
children?: TreeOption[];
|
||||
};
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const treeList = ref<TreeVO[]>([]);
|
||||
const treeOptions = ref<TreeOption[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const showSearch = ref(true);
|
||||
const isExpandAll = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const treeFormRef = ref<ElFormInstance>();
|
||||
const treeTableRef = ref<ElTableInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: TreeForm = {
|
||||
id: undefined,
|
||||
parentId: undefined,
|
||||
deptId: undefined,
|
||||
userId: undefined,
|
||||
treeName: undefined
|
||||
};
|
||||
|
||||
const data = reactive<PageData<TreeForm, TreeQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
parentId: undefined,
|
||||
deptId: undefined,
|
||||
userId: undefined,
|
||||
treeName: undefined
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
|
||||
parentId: [{ required: true, message: '父id不能为空', trigger: 'blur' }],
|
||||
deptId: [{ required: true, message: '部门id不能为空', trigger: 'blur' }],
|
||||
userId: [{ required: true, message: '用户id不能为空', trigger: 'blur' }],
|
||||
treeName: [{ required: true, message: '值不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询测试树列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listTree(queryParams.value);
|
||||
const data = proxy?.handleTree<TreeVO>(res.data, 'id', 'parentId');
|
||||
if (data) {
|
||||
treeList.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 查询测试树下拉树结构 */
|
||||
const getTreeselect = async () => {
|
||||
const res = await listTree();
|
||||
treeOptions.value = [];
|
||||
const data: TreeOption = { id: 0, treeName: '顶级节点', children: [] };
|
||||
data.children = proxy?.handleTree<TreeOption>(res.data, 'id', 'parentId');
|
||||
treeOptions.value.push(data);
|
||||
};
|
||||
|
||||
// 取消按钮
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
// 表单重置
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
treeFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = (row?: TreeVO) => {
|
||||
reset();
|
||||
getTreeselect();
|
||||
if (row && row.id) {
|
||||
form.value.parentId = row.id;
|
||||
} else {
|
||||
form.value.parentId = 0;
|
||||
}
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加测试树';
|
||||
};
|
||||
|
||||
/** 展开/折叠操作 */
|
||||
const handleToggleExpandAll = () => {
|
||||
isExpandAll.value = !isExpandAll.value;
|
||||
toggleExpandAll(treeList.value, isExpandAll.value);
|
||||
};
|
||||
|
||||
/** 展开/折叠操作 */
|
||||
const toggleExpandAll = (data: TreeVO[], status: boolean) => {
|
||||
data.forEach((item) => {
|
||||
treeTableRef.value?.toggleRowExpansion(item, status);
|
||||
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
|
||||
});
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row: TreeVO) => {
|
||||
reset();
|
||||
await getTreeselect();
|
||||
if (row) {
|
||||
form.value.parentId = row.id;
|
||||
}
|
||||
const res = await getTree(row.id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改测试树';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
treeFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.id) {
|
||||
await updateTree(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addTree(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row: TreeVO) => {
|
||||
await proxy?.$modal.confirm('是否确认删除测试树编号为"' + row.id + '"的数据项?');
|
||||
loading.value = true;
|
||||
await delTree(row.id).finally(() => (loading.value = false));
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="errPage-container">
|
||||
<el-button icon="arrow-left" class="pan-back-btn" @click="back"> 返回 </el-button>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<h1 class="text-jumbo text-ginormous">401错误!</h1>
|
||||
<h2>您没有访问权限!</h2>
|
||||
<h6>对不起,您没有访问权限,请不要进行非法操作!您可以返回主页面</h6>
|
||||
<ul class="list-unstyled">
|
||||
<li class="link-type">
|
||||
<router-link to="/"> 回首页 </router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream." />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import errImage from '@/assets/401_images/401.gif';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const errGif = ref(errImage + '?' + +new Date());
|
||||
|
||||
function back() {
|
||||
if (proxy?.$route.query.noGoBack) {
|
||||
proxy.$router.push({ path: '/' });
|
||||
} else {
|
||||
proxy?.$router.go(-1);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.errPage-container {
|
||||
width: 800px;
|
||||
max-width: 100%;
|
||||
margin: 100px auto;
|
||||
.pan-back-btn {
|
||||
background: #008489;
|
||||
color: #fff;
|
||||
border: none !important;
|
||||
}
|
||||
.pan-gif {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
}
|
||||
.pan-img {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
.text-jumbo {
|
||||
font-size: 60px;
|
||||
font-weight: 700;
|
||||
color: #484848;
|
||||
}
|
||||
.list-unstyled {
|
||||
font-size: 14px;
|
||||
li {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
a {
|
||||
color: #008489;
|
||||
text-decoration: none;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,223 @@
|
||||
<template>
|
||||
<div class="wscn-http404-container">
|
||||
<div class="wscn-http404">
|
||||
<div class="pic-404">
|
||||
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404" />
|
||||
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404" />
|
||||
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404" />
|
||||
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404" />
|
||||
</div>
|
||||
<div class="bullshit">
|
||||
<div class="bullshit__oops">404错误!</div>
|
||||
<div class="bullshit__headline">
|
||||
{{ message }}
|
||||
</div>
|
||||
<div class="bullshit__info">
|
||||
对不起,您正在寻找的页面不存在。尝试检查URL的错误,然后按浏览器上的刷新按钮或尝试在我们的应用程序中找到其他内容。
|
||||
</div>
|
||||
<router-link to="/index" class="bullshit__return-home"> 返回首页 </router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const message = computed(() => {
|
||||
return '找不到网页!';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.wscn-http404-container {
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
}
|
||||
.wscn-http404 {
|
||||
position: relative;
|
||||
width: 1200px;
|
||||
padding: 0 50px;
|
||||
overflow: hidden;
|
||||
.pic-404 {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 600px;
|
||||
overflow: hidden;
|
||||
&__parent {
|
||||
width: 100%;
|
||||
}
|
||||
&__child {
|
||||
position: absolute;
|
||||
&.left {
|
||||
width: 80px;
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
animation-name: cloudLeft;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
&.mid {
|
||||
width: 46px;
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
animation-name: cloudMid;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1.2s;
|
||||
}
|
||||
&.right {
|
||||
width: 62px;
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
animation-name: cloudRight;
|
||||
animation-duration: 2s;
|
||||
animation-timing-function: linear;
|
||||
animation-fill-mode: forwards;
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes cloudLeft {
|
||||
0% {
|
||||
top: 17px;
|
||||
left: 220px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 33px;
|
||||
left: 188px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 81px;
|
||||
left: 92px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 97px;
|
||||
left: 60px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudMid {
|
||||
0% {
|
||||
top: 10px;
|
||||
left: 420px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 40px;
|
||||
left: 360px;
|
||||
opacity: 1;
|
||||
}
|
||||
70% {
|
||||
top: 130px;
|
||||
left: 180px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 160px;
|
||||
left: 120px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes cloudRight {
|
||||
0% {
|
||||
top: 100px;
|
||||
left: 500px;
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
top: 120px;
|
||||
left: 460px;
|
||||
opacity: 1;
|
||||
}
|
||||
80% {
|
||||
top: 180px;
|
||||
left: 340px;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 200px;
|
||||
left: 300px;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.bullshit {
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
padding: 30px 0;
|
||||
overflow: hidden;
|
||||
&__oops {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
line-height: 40px;
|
||||
color: #1482f0;
|
||||
opacity: 0;
|
||||
margin-bottom: 20px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__headline {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
opacity: 0;
|
||||
margin-bottom: 10px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.1s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__info {
|
||||
font-size: 13px;
|
||||
line-height: 21px;
|
||||
color: grey;
|
||||
opacity: 0;
|
||||
margin-bottom: 30px;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.2s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
&__return-home {
|
||||
display: block;
|
||||
float: left;
|
||||
width: 110px;
|
||||
height: 36px;
|
||||
background: #1482f0;
|
||||
border-radius: 100px;
|
||||
text-align: center;
|
||||
color: #ffffff;
|
||||
opacity: 0;
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
cursor: pointer;
|
||||
animation-name: slideUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-delay: 0.3s;
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
@keyframes slideUp {
|
||||
0% {
|
||||
transform: translateY(60px);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,723 @@
|
||||
<template>
|
||||
<div class="demo-home">
|
||||
<section class="toolbar-meta">
|
||||
<span class="snapshot-time">数据截至:2026-07-02 10:00:00</span>
|
||||
<el-button text :icon="Refresh">刷新</el-button>
|
||||
</section>
|
||||
|
||||
<section class="hero-grid">
|
||||
<article v-for="item in heroEntries" :key="item.title" class="entry-card">
|
||||
<div class="entry-main">
|
||||
<div class="entry-icon">
|
||||
<el-icon><component :is="item.icon" /></el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h2>{{ item.title }}</h2>
|
||||
<p>{{ item.desc }}</p>
|
||||
<span class="status-pill" :class="`pill-${item.type}`">{{ item.status }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="entry-actions">
|
||||
<el-button type="danger" :disabled="!item.path" @click="goFeature(item.path)">进入功能</el-button>
|
||||
<el-button>查看样例</el-button>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="content-grid">
|
||||
<article class="panel progress-panel">
|
||||
<div class="panel-title">
|
||||
<h2>选 用 育 留 案例沉淀 流程进度</h2>
|
||||
</div>
|
||||
<div class="stage-track">
|
||||
<div v-for="stage in stages" :key="stage.name" class="stage-item" :class="stage.state">
|
||||
<div class="stage-node">
|
||||
<el-icon v-if="stage.state === 'done'"><Check /></el-icon>
|
||||
<span v-else>{{ stage.step }}</span>
|
||||
</div>
|
||||
<div class="stage-copy">
|
||||
<strong>{{ stage.name }}</strong>
|
||||
<span>{{ stage.desc }}</span>
|
||||
<span class="status-pill" :class="`pill-${stage.tagType}`">{{ stage.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel risk-panel">
|
||||
<div class="panel-title">
|
||||
<h2>演示待办与风险</h2>
|
||||
</div>
|
||||
<ul class="risk-list">
|
||||
<li v-for="risk in risks" :key="risk.name">
|
||||
<div class="risk-icon">
|
||||
<el-icon><component :is="risk.icon" /></el-icon>
|
||||
</div>
|
||||
<span class="risk-name">{{ risk.name }}</span>
|
||||
<span class="risk-owner">负责人:{{ risk.owner }}</span>
|
||||
<span class="status-pill risk-status" :class="`pill-${risk.type}`">{{ risk.status }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="bottom-grid">
|
||||
<article class="panel record-panel">
|
||||
<div class="panel-title compact">
|
||||
<h2>最近训练记录</h2>
|
||||
</div>
|
||||
<el-table :data="records" size="small" class="record-table">
|
||||
<el-table-column prop="name" label="人员" min-width="92" />
|
||||
<el-table-column prop="scene" label="场景" min-width="118" />
|
||||
<el-table-column prop="score" label="得分" width="68" />
|
||||
<el-table-column label="状态" width="88">
|
||||
<template #default="{ row }">
|
||||
<span class="status-dot" :class="row.state"></span>
|
||||
{{ row.status }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="time" label="时间" width="112" />
|
||||
</el-table>
|
||||
<div class="panel-link">
|
||||
查看更多 <el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel pipeline-panel">
|
||||
<div class="panel-title compact">
|
||||
<h2>案例沉淀 Pipeline</h2>
|
||||
</div>
|
||||
<div class="pipeline-steps">
|
||||
<div v-for="(step, index) in pipeline" :key="step.name" class="pipeline-step">
|
||||
<div class="pipeline-icon">
|
||||
<el-icon><component :is="step.icon" /></el-icon>
|
||||
</div>
|
||||
<strong>{{ step.name }}</strong>
|
||||
<span>{{ step.count }}</span>
|
||||
<el-icon v-if="index < pipeline.length - 1" class="pipeline-arrow"><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pipeline-summary">
|
||||
<span>今日新增:12 条</span>
|
||||
<span>待审核:2 条</span>
|
||||
<span>已入库:128 条</span>
|
||||
</div>
|
||||
<div class="panel-link">
|
||||
进入案例沉淀 <el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel metrics-panel">
|
||||
<div class="panel-title compact">
|
||||
<h2>验收指标</h2>
|
||||
</div>
|
||||
<div class="metric-grid">
|
||||
<div v-for="metric in metrics" :key="metric.label" class="metric-card">
|
||||
<el-icon><component :is="metric.icon" /></el-icon>
|
||||
<div>
|
||||
<span>{{ metric.label }}</span>
|
||||
<strong>{{ metric.value }}</strong>
|
||||
<small>{{ metric.target }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-link">
|
||||
查看指标详情 <el-icon><ArrowRight /></el-icon>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Index" lang="ts">
|
||||
import {
|
||||
ArrowRight,
|
||||
Check,
|
||||
Cpu,
|
||||
DataLine,
|
||||
DocumentChecked,
|
||||
Finished,
|
||||
FolderChecked,
|
||||
Headset,
|
||||
Histogram,
|
||||
Microphone,
|
||||
Notebook,
|
||||
Operation,
|
||||
Reading,
|
||||
Refresh,
|
||||
Switch,
|
||||
Tickets,
|
||||
TrendCharts,
|
||||
UserFilled,
|
||||
VideoCamera
|
||||
} from '@element-plus/icons-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const heroEntries = [
|
||||
{
|
||||
title: 'AI面试出题',
|
||||
desc: '基于岗位画像与 JD,一键生成面试题目',
|
||||
status: '可演示',
|
||||
type: 'success',
|
||||
icon: DocumentChecked,
|
||||
path: '/recruit/interview'
|
||||
},
|
||||
{
|
||||
title: '三角色对练',
|
||||
desc: '简历生成 + 面试对象 + 结果评估闭环',
|
||||
status: '可演示',
|
||||
type: 'success',
|
||||
icon: UserFilled,
|
||||
path: '/train/practice'
|
||||
},
|
||||
{
|
||||
title: '案例语音整理',
|
||||
desc: '语音上传,AI 整理为结构化案例',
|
||||
status: '样片兜底',
|
||||
type: 'warning',
|
||||
icon: Microphone,
|
||||
path: '/knowledge/cases'
|
||||
},
|
||||
{
|
||||
title: 'SOP知识库',
|
||||
desc: '知识库检索 + RAG问答,支持员工赋能',
|
||||
status: '可演示',
|
||||
type: 'success',
|
||||
icon: Reading,
|
||||
path: '/knowledge/sop'
|
||||
}
|
||||
] as const;
|
||||
|
||||
const goFeature = (path?: string) => {
|
||||
if (!path) return;
|
||||
router.push(path);
|
||||
};
|
||||
|
||||
const stages = [
|
||||
{ step: 1, name: '选:AI面试', desc: '题目与评估', label: '已完成', state: 'done', tagType: 'success' },
|
||||
{ step: 2, name: '用:岗位适配', desc: '与录用评估', label: '已完成', state: 'done', tagType: 'success' },
|
||||
{ step: 3, name: '育:三角色对练', desc: '实战陪练', label: '进行中', state: 'active', tagType: 'danger' },
|
||||
{ step: 4, name: '留:成长追踪', desc: '与能力提升', label: '待启动', state: 'idle', tagType: 'info' },
|
||||
{ step: 5, name: '案例沉淀', desc: '知识与复用', label: '待启动', state: 'idle', tagType: 'info' }
|
||||
] as const;
|
||||
|
||||
const risks = [
|
||||
{ name: '住宅 SOP 素材', owner: '培训组', status: '待确认', type: 'danger', icon: Tickets },
|
||||
{ name: '方言 ASR', owner: '技术组', status: '样片兜底', type: 'warning', icon: Headset },
|
||||
{ name: 'PII 出境评估', owner: '合规组', status: '评估中', type: 'warning', icon: Finished },
|
||||
{ name: '录屏兜底方案', owner: '产品组', status: '已准备', type: 'primary', icon: VideoCamera }
|
||||
] as const;
|
||||
|
||||
const records = [
|
||||
{ name: '张三', scene: '客服应对投诉', score: 86, status: '已完成', state: 'done', time: '07-02 09:32' },
|
||||
{ name: '李四', scene: '催缴沟通技巧', score: 78, status: '已完成', state: 'done', time: '07-02 09:12' },
|
||||
{ name: '王五', scene: '突发事件处理', score: 72, status: '进行中', state: 'active', time: '07-02 08:47' },
|
||||
{ name: '赵六', scene: '业主沟通谈判', score: 90, status: '已完成', state: 'done', time: '07-02 08:15' },
|
||||
{ name: '孙七', scene: '服务礼仪规范', score: 65, status: '未达标', state: 'danger', time: '07-01 18:33' }
|
||||
];
|
||||
|
||||
const pipeline = [
|
||||
{ name: '语音上传', count: '12 条', icon: FolderChecked },
|
||||
{ name: 'ASR 转写', count: '12 条', icon: Histogram },
|
||||
{ name: 'AI 整理', count: '10 条', icon: Cpu },
|
||||
{ name: '审核入库', count: '8 条', icon: Tickets },
|
||||
{ name: '样片展示', count: '5 条', icon: VideoCamera }
|
||||
] as const;
|
||||
|
||||
const metrics = [
|
||||
{ label: '端到端路径', value: '1 条', target: '可完整演示', icon: Switch },
|
||||
{ label: 'RAG 可用率', value: '95%', target: '目标 > 90%', icon: Operation },
|
||||
{ label: '评分一致率', value: '92%', target: '目标 > 85%', icon: TrendCharts },
|
||||
{ label: '试点人数', value: '120 人', target: '目标 > 100 人', icon: DataLine }
|
||||
] as const;
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.demo-home {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 22px;
|
||||
background: #f5f7fb;
|
||||
color: #162033;
|
||||
}
|
||||
|
||||
.toolbar-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
color: #657184;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: fit-content;
|
||||
min-height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pill-danger {
|
||||
color: #c80f1f;
|
||||
background: #ffe8e9;
|
||||
}
|
||||
|
||||
.pill-success {
|
||||
color: #15844a;
|
||||
background: #e5f8ee;
|
||||
}
|
||||
|
||||
.pill-warning {
|
||||
color: #c46b00;
|
||||
background: #fff2dc;
|
||||
}
|
||||
|
||||
.pill-primary {
|
||||
color: #1467bd;
|
||||
background: #e7f2ff;
|
||||
}
|
||||
|
||||
.pill-info {
|
||||
color: #64748b;
|
||||
background: #eef2f7;
|
||||
}
|
||||
|
||||
.hero-grid,
|
||||
.content-grid,
|
||||
.bottom-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hero-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(360px, 0.75fr);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.bottom-grid {
|
||||
grid-template-columns: minmax(350px, 1fr) minmax(420px, 1.1fr) minmax(330px, 0.9fr);
|
||||
}
|
||||
|
||||
.panel,
|
||||
.entry-card {
|
||||
border: 1px solid #e4e9f2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(21, 30, 45, 0.04);
|
||||
}
|
||||
|
||||
.entry-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
min-height: 188px;
|
||||
padding: 22px 22px 20px;
|
||||
}
|
||||
|
||||
.entry-main {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: flex-start;
|
||||
|
||||
h2 {
|
||||
margin: 2px 0 8px;
|
||||
font-size: 18px;
|
||||
line-height: 1.25;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
min-height: 40px;
|
||||
margin: 0 0 12px;
|
||||
color: #3f4b5f;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-icon {
|
||||
display: grid;
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
flex: 0 0 58px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #ff6b72, #d90f20);
|
||||
font-size: 32px;
|
||||
box-shadow: 0 10px 20px rgba(216, 15, 32, 0.22);
|
||||
}
|
||||
|
||||
.entry-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 22px;
|
||||
|
||||
.el-button {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 18px;
|
||||
|
||||
&.compact {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
line-height: 1.35;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-track {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.stage-item {
|
||||
position: relative;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 12px;
|
||||
min-height: 150px;
|
||||
text-align: center;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
right: -50%;
|
||||
width: 100%;
|
||||
height: 0;
|
||||
border-top: 2px dashed #c7ced8;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&.done:not(:last-child)::after {
|
||||
border-color: #31b367;
|
||||
}
|
||||
|
||||
&.active:not(:last-child)::after {
|
||||
border-color: #d70f20;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-node {
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
place-items: center;
|
||||
border: 2px solid #d3d9e3;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
color: #69758a;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stage-item.done .stage-node {
|
||||
border-color: #31b367;
|
||||
color: #fff;
|
||||
background: #31b367;
|
||||
}
|
||||
|
||||
.stage-item.active .stage-node {
|
||||
border-color: #d70f20;
|
||||
color: #fff;
|
||||
background: #d70f20;
|
||||
}
|
||||
|
||||
.stage-copy {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
|
||||
strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
span {
|
||||
min-height: 34px;
|
||||
color: #303b4c;
|
||||
font-size: 13px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
}
|
||||
|
||||
.risk-list {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
|
||||
li {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(110px, 1fr) minmax(120px, 0.8fr) 82px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 43px;
|
||||
border-bottom: 1px solid #edf0f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.risk-icon {
|
||||
color: #5d6878;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.risk-name {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.risk-owner {
|
||||
color: #637085;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.record-panel {
|
||||
:deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.record-table {
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-right: 6px;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
|
||||
&.done {
|
||||
background: #31b367;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #e6a23c;
|
||||
}
|
||||
|
||||
&.danger {
|
||||
background: #d70f20;
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
padding: 8px 0 18px;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
position: relative;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
|
||||
strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #0f7be8;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.pipeline-icon {
|
||||
display: grid;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
place-items: center;
|
||||
border: 1px solid #dce2ec;
|
||||
border-radius: 50%;
|
||||
background: #f8fafc;
|
||||
color: #1d2b3f;
|
||||
font-size: 30px;
|
||||
}
|
||||
|
||||
.pipeline-arrow {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
right: -18px;
|
||||
color: #aab3c2;
|
||||
}
|
||||
|
||||
.pipeline-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid #edf0f5;
|
||||
color: #526178;
|
||||
|
||||
span {
|
||||
position: relative;
|
||||
|
||||
&:not(:last-child)::after {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: -9px;
|
||||
width: 1px;
|
||||
height: 12px;
|
||||
background: #cbd3df;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 86px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e5eaf3;
|
||||
border-radius: 8px;
|
||||
|
||||
.el-icon {
|
||||
color: #d70f20;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
span,
|
||||
small {
|
||||
display: block;
|
||||
color: #5f6c80;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin: 2px 0;
|
||||
color: #142033;
|
||||
font-size: 24px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
margin-top: 14px;
|
||||
color: #d70f20;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
.hero-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.content-grid,
|
||||
.bottom-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.demo-home {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-grid,
|
||||
.stage-track,
|
||||
.pipeline-steps,
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.stage-item {
|
||||
justify-items: start;
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
min-height: auto;
|
||||
text-align: left;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.stage-copy {
|
||||
justify-items: start;
|
||||
}
|
||||
|
||||
.risk-list li {
|
||||
grid-template-columns: 28px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.risk-owner,
|
||||
.risk-status {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.pipeline-step {
|
||||
grid-template-columns: 56px minmax(0, 1fr);
|
||||
justify-items: start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pipeline-arrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.pipeline-summary span::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,586 @@
|
||||
<template>
|
||||
<div class="cases-page">
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>案例语音整理</h1>
|
||||
<p>语音上传或选择样例录音,完成 ASR 转写、AI 结构化整理、送审入库。</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<span class="status-pill" :class="apiPill.type">{{ apiPill.label }}</span>
|
||||
<span class="status-pill" :class="flowStatus.type">{{ flowStatus.label }}</span>
|
||||
<el-button @click="router.push('/index')">返回首页</el-button>
|
||||
<el-button type="danger" @click="selectSample">选择样例录音</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="case-grid">
|
||||
<article class="panel upload-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><Microphone /></el-icon>
|
||||
<h2>上传与转写</h2>
|
||||
</div>
|
||||
<div class="upload-box" @click="selectSample">
|
||||
<el-icon><FolderChecked /></el-icon>
|
||||
<strong>{{ sampleSelected ? displayFileName : '拖入语音 / 选择样例录音' }}</strong>
|
||||
<span>{{ sampleSelected ? '已加载样例录音并完成 ASR 转写。' : 'Demo 暂用样例转写文本,ASR 接入后替换。' }}</span>
|
||||
</div>
|
||||
<div v-if="sampleSelected" class="transcript-card">
|
||||
<span>ASR 转写结果</span>
|
||||
<p>{{ displayTranscript }}</p>
|
||||
</div>
|
||||
<div v-else class="empty-state">先选择样例录音,页面会展示转写结果和后续整理动作。</div>
|
||||
<div class="action-row">
|
||||
<el-button type="danger" @click="organizeCase">AI 整理</el-button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel summary-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><DocumentChecked /></el-icon>
|
||||
<h2>结构化案例稿</h2>
|
||||
</div>
|
||||
<div v-if="organized" class="summary-grid">
|
||||
<div v-for="item in summary" :key="item.label" class="summary-item">
|
||||
<strong>{{ item.label }}</strong>
|
||||
<p>{{ item.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">AI 整理后生成背景、处理动作、结果和可复用亮点。</div>
|
||||
<div v-if="organized" class="tag-row">
|
||||
<span v-for="tag in displayTags" :key="tag.text" class="status-pill" :class="tag.type">{{ tag.text }}</span>
|
||||
</div>
|
||||
<div class="action-row">
|
||||
<el-button :disabled="!organized" @click="submitReview">{{ reviewed ? '已送审' : '送审' }}</el-button>
|
||||
<el-button type="danger" :disabled="!organized" @click="storeCase">{{ stored ? '已入库' : '入库' }}</el-button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel pipeline-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
<h2>案例沉淀 Pipeline</h2>
|
||||
</div>
|
||||
<div class="pipeline-list">
|
||||
<div v-for="step in pipeline" :key="step.name" class="pipeline-item" :class="step.state">
|
||||
<span>{{ step.name }}</span>
|
||||
<strong>{{ step.count }}</strong>
|
||||
<small>{{ step.note }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sample-card" :class="{ active: sampleVisible }">
|
||||
<strong>视频样片</strong>
|
||||
<p>
|
||||
{{
|
||||
sampleVisible ? '已命中车库积水投诉处理样片,可用于演示案例沉淀后的培训复用。' : '案例转视频本期只展示预渲染样片,不现场调用视频生成。'
|
||||
}}
|
||||
</p>
|
||||
<el-button :icon="VideoCamera" @click="viewSample">查看样片</el-button>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel record-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
<h2>最近案例</h2>
|
||||
</div>
|
||||
<el-table :data="records" size="small" class="record-table">
|
||||
<el-table-column prop="title" label="标题" min-width="180" />
|
||||
<el-table-column prop="type" label="类型" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column prop="time" label="时间" width="130" />
|
||||
<el-table-column prop="owner" label="负责人" width="120" />
|
||||
<el-table-column prop="summary" label="摘要" min-width="300" />
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="KnowledgeCases" lang="ts">
|
||||
import { DocumentChecked, FolderChecked, Microphone, Tickets, VideoCamera } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { curateKnowledgeCase, organizeKnowledgeCase, uploadCaseAudio, type CaseRecord, type CaseSummaryItem, type CaseTag } from '@/api/aihr/case';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const seedCase = {
|
||||
fileName: '车库积水投诉_20260702.wav',
|
||||
transcript: '业主反映地下车库长期积水,担心车辆受损。管家先确认车位号与积水时间,安排工程人员排查排水沟,并在业主群同步处理进度。',
|
||||
tags: [
|
||||
{ text: '投诉处理', type: 'pill-success' },
|
||||
{ text: '车库', type: 'pill-primary' },
|
||||
{ text: '高情绪', type: 'pill-warning' }
|
||||
],
|
||||
summary: [
|
||||
{ label: '背景', text: '雨后车库积水,业主担心车辆与安全问题。' },
|
||||
{ label: '处理', text: '确认位置、派工排查、群内同步、次日复盘。' },
|
||||
{ label: '结果', text: '排水沟堵塞已清理,业主确认问题解决。' },
|
||||
{ label: '亮点', text: '响应快、过程透明、主动同步进度。' }
|
||||
]
|
||||
} as const;
|
||||
|
||||
const sampleSelected = ref(false);
|
||||
const organized = ref(false);
|
||||
const reviewed = ref(false);
|
||||
const stored = ref(false);
|
||||
const sampleVisible = ref(false);
|
||||
const caseId = ref('');
|
||||
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
|
||||
const apiFileName = ref('');
|
||||
const apiTranscript = ref('');
|
||||
const apiSummary = ref<CaseSummaryItem[] | null>(null);
|
||||
const apiTags = ref<CaseTag[] | null>(null);
|
||||
const apiRecord = ref<CaseRecord | null>(null);
|
||||
const apiSampleHint = ref('');
|
||||
|
||||
const displayFileName = computed(() => apiFileName.value || seedCase.fileName);
|
||||
const displayTranscript = computed(() => apiTranscript.value || seedCase.transcript);
|
||||
const displaySummary = computed(() => apiSummary.value ?? seedCase.summary);
|
||||
const displayTags = computed(() => apiTags.value ?? seedCase.tags);
|
||||
const summary = computed(() => (organized.value ? displaySummary.value : []));
|
||||
|
||||
const apiPill = computed(() => {
|
||||
if (apiMode.value === 'api') return { label: '后端 API seed', type: 'pill-primary' };
|
||||
if (apiMode.value === 'fallback') return { label: 'API 失败回退', type: 'pill-warning' };
|
||||
return { label: '本地 seed 演示', type: 'pill-primary' };
|
||||
});
|
||||
|
||||
const flowStatus = computed(() => {
|
||||
if (stored.value) return { label: '已完成闭环', type: 'pill-success' };
|
||||
if (reviewed.value) return { label: '已送审待入库', type: 'pill-primary' };
|
||||
if (organized.value) return { label: '已生成案例稿', type: 'pill-warning' };
|
||||
if (sampleSelected.value) return { label: '已完成 ASR', type: 'pill-primary' };
|
||||
return { label: '样片兜底', type: 'pill-warning' };
|
||||
});
|
||||
|
||||
const pipeline = computed(() => [
|
||||
{
|
||||
name: '语音上传',
|
||||
count: sampleSelected.value ? '1 条' : '0 条',
|
||||
note: sampleSelected.value ? '样例录音已选择' : '等待上传',
|
||||
state: sampleSelected.value ? 'done' : 'todo'
|
||||
},
|
||||
{
|
||||
name: 'ASR 转写',
|
||||
count: sampleSelected.value ? '1 条' : '0 条',
|
||||
note: sampleSelected.value ? '本地 seed 转写完成' : '等待语音',
|
||||
state: sampleSelected.value ? 'done' : 'todo'
|
||||
},
|
||||
{
|
||||
name: 'AI 整理',
|
||||
count: organized.value ? '1 条' : '0 条',
|
||||
note: organized.value ? '结构化案例稿已生成' : '等待整理',
|
||||
state: organized.value ? 'done' : sampleSelected.value ? 'active' : 'todo'
|
||||
},
|
||||
{
|
||||
name: '审核入库',
|
||||
count: stored.value ? '1 条' : reviewed.value ? '待入库' : '0 条',
|
||||
note: stored.value ? '已进入知识库' : reviewed.value ? '已送审' : '等待审核',
|
||||
state: stored.value ? 'done' : reviewed.value ? 'active' : 'todo'
|
||||
},
|
||||
{
|
||||
name: '样片展示',
|
||||
count: sampleVisible.value ? '1 条' : '5 条',
|
||||
note: sampleVisible.value ? '已打开预渲染样片' : '演示兜底',
|
||||
state: sampleVisible.value ? 'done' : 'todo'
|
||||
}
|
||||
]);
|
||||
|
||||
const baseRecords = [
|
||||
{
|
||||
title: '车库积水投诉处理',
|
||||
type: '投诉处理',
|
||||
status: '已入库',
|
||||
time: '07-01 16:30',
|
||||
owner: '培训组',
|
||||
summary: '确认车位、派工排水、群内同步进度,业主确认问题解决。'
|
||||
},
|
||||
{
|
||||
title: '催缴沟通争议化解',
|
||||
type: '催缴沟通',
|
||||
status: '待审核',
|
||||
time: '07-01 14:10',
|
||||
owner: '品质组',
|
||||
summary: '业主质疑服务质量,管家先回应服务问题再说明缴费安排。'
|
||||
},
|
||||
{
|
||||
title: '夜间噪音投诉跟进',
|
||||
type: '邻里纠纷',
|
||||
status: '整理中',
|
||||
time: '06-30 19:45',
|
||||
owner: '项目组',
|
||||
summary: '记录噪音时间、沟通责任方、约定复核节点。'
|
||||
}
|
||||
];
|
||||
|
||||
const seedRecord = {
|
||||
title: '车库积水投诉处理 · seed入库',
|
||||
type: '投诉处理',
|
||||
status: '已入库',
|
||||
time: '07-02 10:50',
|
||||
owner: '培训组',
|
||||
summary: 'ASR 转写后自动整理为案例稿,完成送审与入库,可用于三角色对练复用。'
|
||||
};
|
||||
|
||||
const records = ref([...baseRecords]);
|
||||
|
||||
async function selectSample() {
|
||||
sampleSelected.value = true;
|
||||
organized.value = false;
|
||||
reviewed.value = false;
|
||||
stored.value = false;
|
||||
sampleVisible.value = false;
|
||||
apiSummary.value = null;
|
||||
apiRecord.value = null;
|
||||
apiSampleHint.value = '';
|
||||
try {
|
||||
const { data } = await uploadCaseAudio({
|
||||
fileName: seedCase.fileName,
|
||||
projectExtOrgId: 'seed-project'
|
||||
});
|
||||
caseId.value = data.caseId;
|
||||
apiMode.value = 'api';
|
||||
apiFileName.value = data.fileName;
|
||||
apiTranscript.value = data.transcript;
|
||||
apiTags.value = data.tags;
|
||||
ElMessage.success('已通过后端 API 加载样例录音并完成 ASR 转写');
|
||||
} catch {
|
||||
caseId.value = 'case-garage-water';
|
||||
apiMode.value = 'fallback';
|
||||
apiFileName.value = '';
|
||||
apiTranscript.value = '';
|
||||
apiTags.value = null;
|
||||
ElMessage.warning('后端案例上传 API 不可用,已回退本地 seed 转写');
|
||||
}
|
||||
}
|
||||
|
||||
async function organizeCase() {
|
||||
if (!sampleSelected.value) {
|
||||
await selectSample();
|
||||
}
|
||||
if (apiMode.value === 'api') {
|
||||
try {
|
||||
const { data } = await organizeKnowledgeCase({ caseId: caseId.value });
|
||||
apiSummary.value = data.summary;
|
||||
apiTags.value = data.tags;
|
||||
organized.value = true;
|
||||
ElMessage.success('结构化案例稿已由后端 API 返回');
|
||||
return;
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
apiSummary.value = null;
|
||||
apiTags.value = null;
|
||||
ElMessage.warning('后端案例整理 API 不可用,已回退本地 seed 案例稿');
|
||||
}
|
||||
}
|
||||
organized.value = true;
|
||||
ElMessage.success('AI 已生成结构化案例稿');
|
||||
}
|
||||
|
||||
function submitReview() {
|
||||
if (!organized.value) {
|
||||
ElMessage.warning('请先完成 AI 整理');
|
||||
return;
|
||||
}
|
||||
reviewed.value = true;
|
||||
ElMessage.success('案例稿已送审');
|
||||
}
|
||||
|
||||
async function storeCase() {
|
||||
if (!organized.value) {
|
||||
ElMessage.warning('请先完成 AI 整理');
|
||||
return;
|
||||
}
|
||||
reviewed.value = true;
|
||||
stored.value = true;
|
||||
if (apiMode.value === 'api') {
|
||||
try {
|
||||
const { data } = await curateKnowledgeCase({
|
||||
caseId: caseId.value,
|
||||
criteria: '可复用培训案例',
|
||||
limit: 1
|
||||
});
|
||||
apiRecord.value = data.records?.[0] ?? null;
|
||||
apiSampleHint.value = data.sampleHint;
|
||||
if (apiRecord.value && !records.value.some((item) => item.title === apiRecord.value?.title)) {
|
||||
records.value.unshift(apiRecord.value);
|
||||
}
|
||||
ElMessage.success('案例已由后端 API 入库,可用于训练复用');
|
||||
return;
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
apiRecord.value = null;
|
||||
apiSampleHint.value = '';
|
||||
ElMessage.warning('后端案例入库 API 不可用,已回退本地 seed 入库');
|
||||
}
|
||||
}
|
||||
if (!records.value.some((item) => item.title === seedRecord.title)) {
|
||||
records.value.unshift(seedRecord);
|
||||
}
|
||||
ElMessage.success('案例已入库,可用于训练复用');
|
||||
}
|
||||
|
||||
function viewSample() {
|
||||
sampleVisible.value = true;
|
||||
ElMessage.info(apiSampleHint.value || '已打开预渲染样片兜底');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cases-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 22px;
|
||||
background: #f5f7fb;
|
||||
color: #162033;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #657184;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.head-actions,
|
||||
.action-row,
|
||||
.tag-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.case-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 0.9fr) minmax(420px, 1.25fr) minmax(300px, 0.85fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid #e4e9f2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(21, 30, 45, 0.04);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
color: #4971b6;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-box,
|
||||
.transcript-card,
|
||||
.summary-item,
|
||||
.pipeline-item,
|
||||
.sample-card {
|
||||
border: 1px solid #e5eaf3;
|
||||
border-radius: 8px;
|
||||
background: #f9fbfe;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
display: grid;
|
||||
min-height: 150px;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
color: #657184;
|
||||
cursor: pointer;
|
||||
|
||||
.el-icon {
|
||||
color: #d70f20;
|
||||
font-size: 38px;
|
||||
}
|
||||
|
||||
strong,
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.transcript-card,
|
||||
.sample-card {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
|
||||
span {
|
||||
color: #7a8597;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #344255;
|
||||
line-height: 1.65;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
margin-top: 12px;
|
||||
border: 1px dashed #d7dee9;
|
||||
border-radius: 8px;
|
||||
background: #fbfcff;
|
||||
color: #7a8597;
|
||||
line-height: 1.6;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
min-height: 112px;
|
||||
padding: 12px;
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #344255;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.tag-row,
|
||||
.action-row {
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.pipeline-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.pipeline-item {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 4px 10px;
|
||||
padding: 12px;
|
||||
|
||||
span {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #0f7be8;
|
||||
}
|
||||
|
||||
small {
|
||||
grid-column: 1 / -1;
|
||||
color: #657184;
|
||||
}
|
||||
|
||||
&.done {
|
||||
border-color: #cdeeda;
|
||||
background: #f2fbf6;
|
||||
|
||||
strong {
|
||||
color: #15844a;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #ffd6d9;
|
||||
background: #fff6f6;
|
||||
|
||||
strong {
|
||||
color: #d70f20;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sample-card {
|
||||
background: #fff8ed;
|
||||
|
||||
&.active {
|
||||
border-color: #ffd7a6;
|
||||
}
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pill-success {
|
||||
color: #15844a;
|
||||
background: #e5f8ee;
|
||||
}
|
||||
|
||||
.pill-warning {
|
||||
color: #c46b00;
|
||||
background: #fff2dc;
|
||||
}
|
||||
|
||||
.pill-primary {
|
||||
color: #1467bd;
|
||||
background: #e7f2ff;
|
||||
}
|
||||
|
||||
.record-panel {
|
||||
:deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.record-table {
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
.case-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cases-page {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,562 @@
|
||||
<template>
|
||||
<div class="sop-page">
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>SOP 知识库</h1>
|
||||
<p>住宅类 SOP 检索与 RAG 问答,回答必须引用可追溯 SOP 内容。</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<span class="status-pill" :class="apiPill.type">{{ apiPill.label }}</span>
|
||||
<span class="status-pill" :class="flowStatus.type">{{ flowStatus.label }}</span>
|
||||
<el-upload
|
||||
accept=".txt,.md,.markdown,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:on-change="handleDocSelected"
|
||||
>
|
||||
<el-button :loading="uploading">
|
||||
<el-icon><Upload /></el-icon>
|
||||
上传文档
|
||||
</el-button>
|
||||
</el-upload>
|
||||
<el-button @click="router.push('/index')">返回首页</el-button>
|
||||
<el-button type="danger" @click="generateTraining">生成训练题</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sop-grid">
|
||||
<article class="panel category-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><Reading /></el-icon>
|
||||
<h2>SOP 分类</h2>
|
||||
</div>
|
||||
<div class="category-list">
|
||||
<button v-for="item in categories" :key="item" :class="{ active: item === selectedCategory }" @click="selectCategory(item)">
|
||||
{{ item }}
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel qa-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><DocumentChecked /></el-icon>
|
||||
<h2>AI 问答</h2>
|
||||
</div>
|
||||
<div class="search-row">
|
||||
<el-input v-model="queryText" placeholder="输入员工问题" />
|
||||
<el-button type="danger" @click="runSearch">检索</el-button>
|
||||
</div>
|
||||
<div v-if="searched" class="answer-card">
|
||||
<strong>AI 回答</strong>
|
||||
<p>{{ answerText }}</p>
|
||||
<span>引用:{{ referenceText }}</span>
|
||||
</div>
|
||||
<div v-else class="empty-state">点击检索后展示命中文档、原文片段和带引用的 AI 回答。</div>
|
||||
<div v-if="searched" class="doc-list">
|
||||
<div v-for="doc in docs" :key="doc.title" class="doc-item">
|
||||
<div>
|
||||
<strong>{{ doc.title }}</strong>
|
||||
<span>{{ doc.desc }}</span>
|
||||
</div>
|
||||
<span class="status-pill" :class="doc.type">{{ doc.hit }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="trainingGenerated" class="training-card">
|
||||
<strong>训练题已生成</strong>
|
||||
<p v-for="item in training" :key="item">{{ item }}</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel detail-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
<h2>SOP 原文片段</h2>
|
||||
</div>
|
||||
<div v-if="searched" class="snippet-list">
|
||||
<div v-for="snippet in snippets" :key="snippet.title" class="snippet-item">
|
||||
<strong>{{ snippet.title }}</strong>
|
||||
<p>{{ snippet.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">待检索后展示 SOP 原文片段,便于验收回答来源。</div>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel record-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
<h2>知识库文档</h2>
|
||||
</div>
|
||||
<el-table :data="records" size="small" class="record-table">
|
||||
<el-table-column prop="title" label="文档" min-width="180" />
|
||||
<el-table-column prop="category" label="分类" width="140" />
|
||||
<el-table-column prop="version" label="版本" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column prop="owner" label="维护人" width="120" />
|
||||
<el-table-column prop="summary" label="摘要" min-width="300" />
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="KnowledgeSop" lang="ts">
|
||||
import { DocumentChecked, Reading, Tickets, Upload } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import type { UploadFile } from 'element-plus';
|
||||
import { computed, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { searchKnowledgeSop, uploadKnowledgeDoc, type SopRecord, type SopSearchResponse } from '@/api/aihr/sop';
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const categories = ['投诉处理 SOP', '催缴沟通 SOP', '报修跟进 SOP', '交付接待 SOP', '巡检记录 SOP'] as const;
|
||||
type Category = (typeof categories)[number];
|
||||
|
||||
const sopSeeds: Record<
|
||||
Category,
|
||||
{
|
||||
question: string;
|
||||
answer: string;
|
||||
reference: string;
|
||||
docs: { title: string; desc: string; hit: string; type: string }[];
|
||||
snippets: { title: string; text: string }[];
|
||||
training: string[];
|
||||
}
|
||||
> = {
|
||||
'投诉处理 SOP': {
|
||||
question: '业主投诉漏水,管家第一步怎么处理?',
|
||||
answer: '先安抚业主情绪并确认影响范围;同步记录房号、报修时间、现场照片;再创建工单并明确首次反馈时限。',
|
||||
reference: '投诉处理 SOP v1.0 第 2、3、5 条',
|
||||
docs: [
|
||||
{ title: '投诉处理 SOP v1.0', desc: '安抚、记录、派单、反馈时限', hit: '引用 3 段', type: 'pill-success' },
|
||||
{ title: '报修跟进 SOP v1.0', desc: '报修流转、工程派工、闭环确认', hit: '引用 2 段', type: 'pill-primary' },
|
||||
{ title: '高情绪沟通话术', desc: '先承接情绪,再进入事实确认', hit: '建议补充', type: 'pill-warning' }
|
||||
],
|
||||
snippets: [
|
||||
{ title: '第 2 条:先安抚', text: '接到投诉后先表达理解,避免争辩责任归属。' },
|
||||
{ title: '第 3 条:确认事实', text: '记录房号、联系人、发生时间、现场照片和影响范围。' },
|
||||
{ title: '第 5 条:明确反馈', text: '创建工单后告知首次反馈时限和责任人。' }
|
||||
],
|
||||
training: ['题 1:漏水投诉首响时限如何表述?', '题 2:哪些事实必须在工单中记录?']
|
||||
},
|
||||
'催缴沟通 SOP': {
|
||||
question: '业主说服务不到位所以拒缴,客服怎么回应?',
|
||||
answer: '先承接服务不满并记录具体事项,再区分服务整改与费用义务;给出整改跟进人和缴费安排,避免直接争辩。',
|
||||
reference: '催缴沟通 SOP v1.0 第 1、4、6 条',
|
||||
docs: [
|
||||
{ title: '催缴沟通 SOP v1.0', desc: '服务异议、费用说明、承诺跟进', hit: '引用 3 段', type: 'pill-success' },
|
||||
{ title: '客户异议话术库', desc: '先确认诉求,再进入费用说明', hit: '引用 1 段', type: 'pill-primary' }
|
||||
],
|
||||
snippets: [
|
||||
{ title: '第 1 条:先听诉求', text: '业主表达不满时,先记录具体服务问题,不以欠费事实压过诉求。' },
|
||||
{ title: '第 4 条:费用解释', text: '说明费用构成、账期和缴费渠道,避免承诺超出权限的减免。' },
|
||||
{ title: '第 6 条:跟进闭环', text: '对服务问题设置跟进责任人,并在约定时间内反馈处理进度。' }
|
||||
],
|
||||
training: ['题 1:服务异议与催缴说明如何分开表达?', '题 2:哪些减免承诺不能现场答复?']
|
||||
},
|
||||
'报修跟进 SOP': {
|
||||
question: '报修超过一天还没处理,管家怎么跟进?',
|
||||
answer: '先查询工单状态和责任人,再向工程组确认下一次到场时间;对业主同步原因、节点和复核方式。',
|
||||
reference: '报修跟进 SOP v1.0 第 2、5、7 条',
|
||||
docs: [
|
||||
{ title: '报修跟进 SOP v1.0', desc: '派工、到场、复核、关闭', hit: '引用 3 段', type: 'pill-success' },
|
||||
{ title: '工程派单规则', desc: '急修与普通维修分级', hit: '引用 2 段', type: 'pill-primary' }
|
||||
],
|
||||
snippets: [
|
||||
{ title: '第 2 条:查工单', text: '跟进前必须确认工单编号、责任班组、当前状态和超时原因。' },
|
||||
{ title: '第 5 条:给节点', text: '向业主说明下一次反馈或到场时间,避免只说尽快。' },
|
||||
{ title: '第 7 条:复核关闭', text: '处理完成后需向业主确认效果,再关闭工单。' }
|
||||
],
|
||||
training: ['题 1:报修超时必须先查哪些字段?', '题 2:如何避免用“尽快处理”替代明确节点?']
|
||||
},
|
||||
'交付接待 SOP': {
|
||||
question: '新业主交付当天资料不齐,接待岗怎么处理?',
|
||||
answer: '先核验身份和已有资料,列出缺失项并给补交方式;不影响可办理事项先行办理,明确后续补齐节点。',
|
||||
reference: '交付接待 SOP v1.0 第 1、3、6 条',
|
||||
docs: [{ title: '交付接待 SOP v1.0', desc: '身份核验、资料补齐、流程引导', hit: '引用 3 段', type: 'pill-success' }],
|
||||
snippets: [
|
||||
{ title: '第 1 条:身份核验', text: '接待前核验业主身份、房号和交付批次。' },
|
||||
{ title: '第 3 条:缺项清单', text: '资料不齐时提供缺项清单和补交渠道。' },
|
||||
{ title: '第 6 条:后续节点', text: '对暂无法完成事项给出下一次办理节点。' }
|
||||
],
|
||||
training: ['题 1:资料不齐时如何不阻塞可办理事项?', '题 2:缺项清单应包含哪些内容?']
|
||||
},
|
||||
'巡检记录 SOP': {
|
||||
question: '巡检发现消防通道堆物,记录要包含什么?',
|
||||
answer: '记录位置、照片、发现时间、风险等级和责任人;同步整改时限,并在复查后补充关闭证据。',
|
||||
reference: '巡检记录 SOP v1.0 第 2、4、8 条',
|
||||
docs: [{ title: '巡检记录 SOP v1.0', desc: '发现、记录、派单、复查', hit: '引用 3 段', type: 'pill-success' }],
|
||||
snippets: [
|
||||
{ title: '第 2 条:定位证据', text: '巡检问题需记录楼栋、楼层、点位和现场照片。' },
|
||||
{ title: '第 4 条:责任与时限', text: '派单时注明责任人、整改要求和完成时限。' },
|
||||
{ title: '第 8 条:复查关闭', text: '复查通过后上传关闭照片,并保留处理记录。' }
|
||||
],
|
||||
training: ['题 1:消防通道堆物属于哪类巡检风险?', '题 2:复查关闭必须补哪些证据?']
|
||||
}
|
||||
};
|
||||
|
||||
const selectedCategory = ref<Category>('投诉处理 SOP');
|
||||
const queryText = ref(sopSeeds[selectedCategory.value].question);
|
||||
const searched = ref(false);
|
||||
const trainingGenerated = ref(false);
|
||||
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
|
||||
const apiResult = ref<SopSearchResponse | null>(null);
|
||||
const uploading = ref(false);
|
||||
const uploadedRecords = ref<SopRecord[]>([]);
|
||||
|
||||
const activeSeed = computed(() => sopSeeds[selectedCategory.value]);
|
||||
const answerText = computed(() => apiResult.value?.answer ?? activeSeed.value.answer);
|
||||
const referenceText = computed(() => apiResult.value?.reference ?? activeSeed.value.reference);
|
||||
const docs = computed(() => apiResult.value?.docs ?? activeSeed.value.docs);
|
||||
const snippets = computed(() => apiResult.value?.snippets ?? activeSeed.value.snippets);
|
||||
const training = computed(() => apiResult.value?.training ?? activeSeed.value.training);
|
||||
const apiPill = computed(() => {
|
||||
if (apiMode.value === 'api') return { label: '后端 API', type: 'pill-primary' };
|
||||
if (apiMode.value === 'fallback') return { label: 'API 失败回退', type: 'pill-warning' };
|
||||
return { label: '本地 seed 演示', type: 'pill-primary' };
|
||||
});
|
||||
const flowStatus = computed(() => {
|
||||
if (trainingGenerated.value) return { label: '已完成闭环', type: 'pill-success' };
|
||||
if (searched.value) return { label: `已命中 ${snippets.value.length} 段`, type: 'pill-primary' };
|
||||
return { label: '可引用 SOP 内容', type: 'pill-success' };
|
||||
});
|
||||
|
||||
const seedRecords = [
|
||||
{
|
||||
title: '投诉处理 SOP v1.0',
|
||||
category: '投诉处理',
|
||||
version: 'v1.0',
|
||||
status: '已入库',
|
||||
owner: '品质组',
|
||||
summary: '业主投诉的接待、记录、派单、反馈和闭环确认。'
|
||||
},
|
||||
{
|
||||
title: '催缴沟通 SOP v1.0',
|
||||
category: '催缴沟通',
|
||||
version: 'v1.0',
|
||||
status: '已入库',
|
||||
owner: '财务组',
|
||||
summary: '缴费提醒、费用争议回应、服务问题承接。'
|
||||
},
|
||||
{
|
||||
title: '报修跟进 SOP v1.0',
|
||||
category: '报修跟进',
|
||||
version: 'v1.0',
|
||||
status: '待审核',
|
||||
owner: '工程组',
|
||||
summary: '报修接单、派工、现场反馈、复核关闭。'
|
||||
}
|
||||
];
|
||||
|
||||
const records = computed(() => [...uploadedRecords.value, ...seedRecords]);
|
||||
|
||||
function selectCategory(item: Category) {
|
||||
selectedCategory.value = item;
|
||||
queryText.value = sopSeeds[item].question;
|
||||
searched.value = false;
|
||||
trainingGenerated.value = false;
|
||||
apiMode.value = 'seed';
|
||||
apiResult.value = null;
|
||||
}
|
||||
|
||||
async function runSearch() {
|
||||
trainingGenerated.value = false;
|
||||
try {
|
||||
const { data } = await searchKnowledgeSop({
|
||||
queryText: queryText.value,
|
||||
category: selectedCategory.value,
|
||||
limit: 3
|
||||
});
|
||||
apiResult.value = data;
|
||||
apiMode.value = 'api';
|
||||
searched.value = true;
|
||||
ElMessage.success(`已通过后端 API 命中 ${data.snippets.length} 段 SOP 原文`);
|
||||
} catch {
|
||||
apiResult.value = null;
|
||||
apiMode.value = 'fallback';
|
||||
searched.value = true;
|
||||
ElMessage.warning('后端 SOP 检索 API 不可用,已回退本地 seed 知识库');
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDocSelected(uploadFile: UploadFile) {
|
||||
const raw = uploadFile.raw;
|
||||
if (!raw) return;
|
||||
if (!/\.(txt|md|markdown|pdf|doc|docx|xls|xlsx|ppt|pptx)$/i.test(raw.name)) {
|
||||
ElMessage.warning('仅支持 txt/md/markdown/pdf/doc/docx/xls/xlsx/ppt/pptx 文件');
|
||||
return;
|
||||
}
|
||||
if (raw.size > 100 * 1024 * 1024) {
|
||||
ElMessage.warning('文件不能超过 100MB');
|
||||
return;
|
||||
}
|
||||
|
||||
uploading.value = true;
|
||||
try {
|
||||
const form = new FormData();
|
||||
form.append('file', raw);
|
||||
form.append('category', selectedCategory.value);
|
||||
const { data } = await uploadKnowledgeDoc(form);
|
||||
uploadedRecords.value.unshift({
|
||||
title: data.fileName,
|
||||
category: data.category.replace(' SOP', ''),
|
||||
version: '上传',
|
||||
status: '已入库',
|
||||
owner: 'MinIO',
|
||||
summary: `OSS #${data.ossId},已解析 ${data.fragments} 个片段`
|
||||
});
|
||||
queryText.value = data.snippets[0]?.text.slice(0, 40) || queryText.value;
|
||||
ElMessage.success(`已入库 ${data.fragments} 个片段`);
|
||||
await runSearch();
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function generateTraining() {
|
||||
if (!searched.value) {
|
||||
await runSearch();
|
||||
}
|
||||
trainingGenerated.value = true;
|
||||
ElMessage.success('已基于引用片段生成训练题');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sop-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 22px;
|
||||
background: #f5f7fb;
|
||||
color: #162033;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #657184;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.sop-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 240px minmax(460px, 1.2fr) minmax(320px, 0.85fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid #e4e9f2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(21, 30, 45, 0.04);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
color: #4971b6;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.category-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
|
||||
button {
|
||||
min-height: 38px;
|
||||
border: 1px solid #e5eaf3;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #405066;
|
||||
text-align: left;
|
||||
padding: 0 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
color: #c80f1f;
|
||||
background: #fff0f1;
|
||||
border-color: #ffd6d9;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 90px;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.answer-card,
|
||||
.doc-item,
|
||||
.snippet-item,
|
||||
.empty-state,
|
||||
.training-card {
|
||||
border: 1px solid #e5eaf3;
|
||||
border-radius: 8px;
|
||||
background: #f9fbfe;
|
||||
}
|
||||
|
||||
.answer-card {
|
||||
padding: 14px;
|
||||
border-left: 4px solid #d70f20;
|
||||
background: #fff6f6;
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #657184;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.training-card {
|
||||
padding: 14px;
|
||||
color: #657184;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.training-card {
|
||||
margin-top: 12px;
|
||||
border-color: #cdeeda;
|
||||
background: #f2fbf6;
|
||||
|
||||
strong {
|
||||
color: #15844a;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0 0;
|
||||
color: #344255;
|
||||
}
|
||||
}
|
||||
|
||||
.doc-list,
|
||||
.snippet-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.doc-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
|
||||
strong,
|
||||
span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
div > span {
|
||||
margin-top: 4px;
|
||||
color: #657184;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.snippet-item {
|
||||
padding: 12px;
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #46566e;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.pill-success {
|
||||
color: #15844a;
|
||||
background: #e5f8ee;
|
||||
}
|
||||
|
||||
.pill-warning {
|
||||
color: #c46b00;
|
||||
background: #fff2dc;
|
||||
}
|
||||
|
||||
.pill-primary {
|
||||
color: #1467bd;
|
||||
background: #e7f2ff;
|
||||
}
|
||||
|
||||
.record-panel {
|
||||
:deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.record-table {
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
.sop-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.sop-page {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,378 @@
|
||||
<template>
|
||||
<div class="login">
|
||||
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
|
||||
<div class="title-box">
|
||||
<h3 class="title">{{ title }}</h3>
|
||||
<lang-select />
|
||||
</div>
|
||||
<el-form-item v-if="tenantEnabled" prop="tenantId">
|
||||
<el-select v-model="loginForm.tenantId" filterable :placeholder="proxy.$t('login.selectPlaceholder')" style="width: 100%">
|
||||
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"></el-option>
|
||||
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" type="text" size="large" auto-complete="off" :placeholder="proxy.$t('login.username')">
|
||||
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
:placeholder="proxy.$t('login.password')"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="captchaEnabled" prop="code">
|
||||
<el-input
|
||||
v-model="loginForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
:placeholder="proxy.$t('login.code')"
|
||||
style="width: 63%"
|
||||
@keyup.enter="handleLogin"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
<div class="login-code">
|
||||
<img :src="codeUrl" class="login-code-img" @click="getCode" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-checkbox v-model="loginForm.rememberMe" style="margin: 0 0 25px 0">{{ proxy.$t('login.rememberPassword') }}</el-checkbox>
|
||||
<el-form-item style="float: right">
|
||||
<el-button circle :title="proxy.$t('login.social.wechat')" @click="doSocialLogin('wechat')">
|
||||
<svg-icon icon-class="wechat" />
|
||||
</el-button>
|
||||
<el-button circle :title="proxy.$t('login.social.maxkey')" @click="doSocialLogin('maxkey')">
|
||||
<svg-icon icon-class="maxkey" />
|
||||
</el-button>
|
||||
<el-button circle :title="proxy.$t('login.social.topiam')" @click="doSocialLogin('topiam')">
|
||||
<svg-icon icon-class="topiam" />
|
||||
</el-button>
|
||||
<el-button circle :title="proxy.$t('login.social.gitee')" @click="doSocialLogin('gitee')">
|
||||
<svg-icon icon-class="gitee" />
|
||||
</el-button>
|
||||
<el-button circle :title="proxy.$t('login.social.github')" @click="doSocialLogin('github')">
|
||||
<svg-icon icon-class="github" />
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 100%">
|
||||
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleLogin">
|
||||
<span v-if="!loading">{{ proxy.$t('login.login') }}</span>
|
||||
<span v-else>{{ proxy.$t('login.logging') }}</span>
|
||||
</el-button>
|
||||
<div v-if="register" style="float: right">
|
||||
<router-link class="link-type" :to="'/register'">{{ proxy.$t('login.switchRegisterPage') }}</router-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 底部 -->
|
||||
<div class="el-login-footer">
|
||||
<span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCodeImg, getTenantList } from '@/api/login';
|
||||
import { authRouterUrl } from '@/api/system/social/auth';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { LoginData, TenantVO } from '@/api/types';
|
||||
import { to } from 'await-to-js';
|
||||
import { HttpStatus } from '@/enums/RespEnum';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE;
|
||||
const userStore = useUserStore();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
|
||||
const loginForm = ref<LoginData>({
|
||||
tenantId: '000000',
|
||||
username: 'admin',
|
||||
password: 'admin123',
|
||||
rememberMe: false,
|
||||
code: '',
|
||||
uuid: ''
|
||||
} as LoginData);
|
||||
|
||||
const loginRules: ElFormRules = {
|
||||
tenantId: [{ required: true, trigger: 'blur', message: t('login.rule.tenantId.required') }],
|
||||
username: [{ required: true, trigger: 'blur', message: t('login.rule.username.required') }],
|
||||
password: [{ required: true, trigger: 'blur', message: t('login.rule.password.required') }],
|
||||
code: [{ required: true, trigger: 'change', message: t('login.rule.code.required') }]
|
||||
};
|
||||
|
||||
const codeUrl = ref('');
|
||||
const loading = ref(false);
|
||||
// 验证码开关
|
||||
const captchaEnabled = ref(true);
|
||||
// 租户开关
|
||||
const tenantEnabled = ref(true);
|
||||
|
||||
// 注册开关
|
||||
const register = ref(false);
|
||||
const redirect = ref('/');
|
||||
const loginRef = ref<ElFormInstance>();
|
||||
// 租户列表
|
||||
const tenantList = ref<TenantVO[]>([]);
|
||||
|
||||
watch(
|
||||
() => router.currentRoute.value,
|
||||
(newRoute: any) => {
|
||||
redirect.value = newRoute.query && newRoute.query.redirect && decodeURIComponent(newRoute.query.redirect);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const handleLogin = () => {
|
||||
loginRef.value?.validate(async (valid: boolean, fields: any) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
// 勾选了需要记住密码设置在 localStorage 中设置记住用户名和密码
|
||||
if (loginForm.value.rememberMe) {
|
||||
localStorage.setItem('tenantId', String(loginForm.value.tenantId));
|
||||
localStorage.setItem('username', String(loginForm.value.username));
|
||||
localStorage.setItem('password', String(loginForm.value.password));
|
||||
localStorage.setItem('rememberMe', String(loginForm.value.rememberMe));
|
||||
} else {
|
||||
// 否则移除
|
||||
localStorage.removeItem('tenantId');
|
||||
localStorage.removeItem('username');
|
||||
localStorage.removeItem('password');
|
||||
localStorage.removeItem('rememberMe');
|
||||
}
|
||||
// 调用action的登录方法
|
||||
const [err] = await to(userStore.login(loginForm.value));
|
||||
if (!err) {
|
||||
const redirectUrl = redirect.value || '/';
|
||||
await router.push(redirectUrl);
|
||||
loading.value = false;
|
||||
} else {
|
||||
loading.value = false;
|
||||
// 重新获取验证码
|
||||
if (captchaEnabled.value) {
|
||||
await getCode();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('error submit!', fields);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取验证码
|
||||
*/
|
||||
const getCode = async () => {
|
||||
const res = await getCodeImg();
|
||||
const { data } = res;
|
||||
captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
|
||||
if (captchaEnabled.value) {
|
||||
// 刷新验证码时清空输入框
|
||||
loginForm.value.code = '';
|
||||
codeUrl.value = 'data:image/gif;base64,' + data.img;
|
||||
loginForm.value.uuid = data.uuid;
|
||||
}
|
||||
};
|
||||
|
||||
const getLoginData = () => {
|
||||
const tenantId = localStorage.getItem('tenantId');
|
||||
const username = localStorage.getItem('username');
|
||||
const password = localStorage.getItem('password');
|
||||
const rememberMe = localStorage.getItem('rememberMe');
|
||||
loginForm.value = {
|
||||
tenantId: tenantId === null ? String(loginForm.value.tenantId) : tenantId,
|
||||
username: username === null ? String(loginForm.value.username) : username,
|
||||
password: password === null ? String(loginForm.value.password) : String(password),
|
||||
rememberMe: rememberMe === null ? false : Boolean(rememberMe)
|
||||
} as LoginData;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取租户列表
|
||||
*/
|
||||
const initTenantList = async () => {
|
||||
const { data } = await getTenantList(false);
|
||||
tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
|
||||
if (tenantEnabled.value) {
|
||||
tenantList.value = data.voList;
|
||||
if (tenantList.value != null && tenantList.value.length !== 0) {
|
||||
loginForm.value.tenantId = tenantList.value[0].tenantId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 第三方登录
|
||||
* @param type
|
||||
*/
|
||||
const doSocialLogin = (type: string) => {
|
||||
authRouterUrl(type, loginForm.value.tenantId).then((res: any) => {
|
||||
if (res.code === HttpStatus.SUCCESS) {
|
||||
// 获取授权地址跳转
|
||||
window.location.href = res.data;
|
||||
} else {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getCode();
|
||||
initTenantList();
|
||||
getLoginData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-image: url('../assets/images/login-background.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.title-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.title {
|
||||
margin: 0px auto 26px auto;
|
||||
text-align: center;
|
||||
color: var(--el-text-color-primary);
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
:deep(.lang-select--style) {
|
||||
line-height: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.login-form {
|
||||
border-radius: var(--app-radius-lg);
|
||||
background: rgba(255, 255, 255, 0.94);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
width: min(420px, 90vw);
|
||||
padding: 32px 30px 12px 30px;
|
||||
z-index: 1;
|
||||
box-shadow: var(--app-shadow-lg);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
.el-input {
|
||||
height: 40px;
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
height: 39px;
|
||||
width: 14px;
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.login-tip {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__wrapper) {
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__wrapper.is-focus) {
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.login-form :deep(.el-button--primary) {
|
||||
border-radius: var(--app-radius-md);
|
||||
box-shadow: 0 8px 20px rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.login-form :deep(.el-button.is-circle) {
|
||||
background: rgba(15, 23, 42, 0.04);
|
||||
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
|
||||
.login-form :deep(.el-button.is-circle:hover) {
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
border-color: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.login-code {
|
||||
width: calc(37% - 10px);
|
||||
height: 40px;
|
||||
float: right;
|
||||
margin-left: 10px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--app-radius-sm);
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.el-login-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-family: Arial, serif;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.login-code-img {
|
||||
height: 40px;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
:global(html.dark) {
|
||||
.login-form {
|
||||
background: rgba(17, 24, 39, 0.9);
|
||||
border-color: rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
|
||||
.login-form :deep(.el-input__wrapper) {
|
||||
background-color: rgba(17, 24, 39, 0.7);
|
||||
}
|
||||
|
||||
.login-form :deep(.el-button.is-circle) {
|
||||
background: rgba(148, 163, 184, 0.12);
|
||||
border-color: rgba(148, 163, 184, 0.25);
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.el-login-footer {
|
||||
color: rgba(226, 232, 240, 0.65);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<i-frame v-model:src="url"></i-frame>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const url = ref(import.meta.env.VITE_APP_MONITOR_ADMIN);
|
||||
</script>
|
||||
+192
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="24" class="card-box">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<Monitor style="width: 1em; height: 1em; vertical-align: middle" />
|
||||
<span style="vertical-align: middle">基本信息</span>
|
||||
</template>
|
||||
|
||||
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||
<table style="width: 100%">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">Redis版本</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.redis_version }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">运行模式</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.redis_mode === 'standalone' ? '单机' : '集群' }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">端口</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.tcp_port }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">客户端数</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.connected_clients }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">运行时间(天)</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.uptime_in_days }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">使用内存</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.used_memory_human }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">使用CPU</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ parseFloat(cache.info.used_cpu_user_children).toFixed(2) }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">内存配置</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.maxmemory_human }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">AOF是否开启</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.aof_enabled === '0' ? '否' : '是' }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">RDB是否成功</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">{{ cache.info.rdb_last_bgsave_status }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">Key数量</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.dbSize" class="cell">{{ cache.dbSize }}</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div class="cell">网络入口/出口</div>
|
||||
</td>
|
||||
<td class="el-table__cell is-leaf">
|
||||
<div v-if="cache.info" class="cell">
|
||||
{{ cache.info.instantaneous_input_kbps }}kps/{{ cache.info.instantaneous_output_kbps }}kps
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="card-box">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<PieChart style="width: 1em; height: 1em; vertical-align: middle" />
|
||||
<span style="vertical-align: middle">命令统计</span>
|
||||
</template>
|
||||
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||
<div ref="commandstats" style="height: 420px" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12" class="card-box">
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<Odometer style="width: 1em; height: 1em; vertical-align: middle" /> <span style="vertical-align: middle">内存信息</span>
|
||||
</template>
|
||||
<div class="el-table el-table--enable-row-hover el-table--medium">
|
||||
<div ref="usedmemory" style="height: 420px" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Cache" lang="ts">
|
||||
import { getCache } from '@/api/monitor/cache';
|
||||
import * as echarts from 'echarts';
|
||||
import { CacheVO } from '@/api/monitor/cache/types';
|
||||
|
||||
const cache = ref<Partial<CacheVO>>({});
|
||||
const commandstats = ref();
|
||||
const usedmemory = ref();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const getList = async () => {
|
||||
proxy?.$modal.loading('正在加载缓存监控数据,请稍候!');
|
||||
const res = await getCache();
|
||||
proxy?.$modal.closeLoading();
|
||||
cache.value = res.data;
|
||||
const commandstatsIntance = echarts.init(commandstats.value, 'macarons');
|
||||
commandstatsIntance.setOption({
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b} : {c} ({d}%)'
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '命令',
|
||||
type: 'pie',
|
||||
roseType: 'radius',
|
||||
radius: [15, 95],
|
||||
center: ['50%', '38%'],
|
||||
data: res.data.commandStats,
|
||||
animationEasing: 'cubicInOut',
|
||||
animationDuration: 1000
|
||||
}
|
||||
]
|
||||
});
|
||||
const usedmemoryInstance = echarts.init(usedmemory.value, 'macarons');
|
||||
usedmemoryInstance.setOption({
|
||||
tooltip: {
|
||||
formatter: '{b} <br/>{a} : ' + cache.value.info.used_memory_human
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: '峰值',
|
||||
type: 'gauge',
|
||||
min: 0,
|
||||
max: 1000,
|
||||
detail: {
|
||||
formatter: cache.value.info.used_memory_human
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: parseFloat(cache.value.info.used_memory_human),
|
||||
name: '内存消耗'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
window.addEventListener('resize', () => {
|
||||
commandstatsIntance.resize();
|
||||
usedmemoryInstance.resize();
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,209 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="登录地址" prop="ipaddr">
|
||||
<el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="登录状态" clearable>
|
||||
<el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="登录时间" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['monitor:logininfor:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['monitor:logininfor:remove']" type="danger" plain icon="Delete" @click="handleClean">清空</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['monitor:logininfor:unlock']" type="primary" plain icon="Unlock" :disabled="single" @click="handleUnlock">
|
||||
解锁
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['monitor:logininfor:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="loginInfoTableRef"
|
||||
v-loading="loading"
|
||||
:data="loginInfoList"
|
||||
:default-sort="defaultSort"
|
||||
border
|
||||
@selection-change="handleSelectionChange"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="访问编号" align="center" prop="infoId" />
|
||||
<el-table-column
|
||||
label="用户名称"
|
||||
align="center"
|
||||
prop="userName"
|
||||
:show-overflow-tooltip="true"
|
||||
sortable="custom"
|
||||
:sort-orders="['descending', 'ascending']"
|
||||
/>
|
||||
<el-table-column label="客户端" align="center" prop="clientKey" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="设备类型" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="地址" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="登录状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_common_status" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="描述" align="center" prop="msg" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="访问时间" align="center" prop="loginTime" sortable="custom" :sort-orders="['descending', 'ascending']" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.loginTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Logininfor" lang="ts">
|
||||
import { list, delLoginInfo, cleanLoginInfo, unlockLoginInfo } from '@/api/monitor/loginInfo';
|
||||
import { LoginInfoQuery, LoginInfoVO } from '@/api/monitor/loginInfo/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
|
||||
const { sys_common_status } = toRefs<any>(proxy?.useDict('sys_common_status'));
|
||||
|
||||
const loginInfoList = ref<LoginInfoVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<number | string>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const selectName = ref<Array<string>>([]);
|
||||
const total = ref(0);
|
||||
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||
const defaultSort = ref<any>({ prop: 'loginTime', order: 'descending' });
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const loginInfoTableRef = ref<ElTableInstance>();
|
||||
// 查询参数
|
||||
const queryParams = ref<LoginInfoQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
ipaddr: '',
|
||||
userName: '',
|
||||
status: '',
|
||||
orderByColumn: defaultSort.value.prop,
|
||||
isAsc: defaultSort.value.order
|
||||
});
|
||||
|
||||
/** 查询登录日志列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await list(proxy?.addDateRange(queryParams.value, dateRange.value));
|
||||
loginInfoList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = ['', ''];
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.pageNum = 1;
|
||||
loginInfoTableRef.value?.sort(defaultSort.value.prop, defaultSort.value.order);
|
||||
};
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: LoginInfoVO[]) => {
|
||||
ids.value = selection.map((item) => item.infoId);
|
||||
multiple.value = !selection.length;
|
||||
single.value = selection.length != 1;
|
||||
selectName.value = selection.map((item) => item.userName);
|
||||
};
|
||||
/** 排序触发事件 */
|
||||
const handleSortChange = (column: any) => {
|
||||
queryParams.value.orderByColumn = column.prop;
|
||||
queryParams.value.isAsc = column.order;
|
||||
getList();
|
||||
};
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: LoginInfoVO) => {
|
||||
const infoIds = row?.infoId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除访问编号为"' + infoIds + '"的数据项?');
|
||||
await delLoginInfo(infoIds);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
/** 清空按钮操作 */
|
||||
const handleClean = async () => {
|
||||
await proxy?.$modal.confirm('是否确认清空所有登录日志数据项?');
|
||||
await cleanLoginInfo();
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('清空成功');
|
||||
};
|
||||
/** 解锁按钮操作 */
|
||||
const handleUnlock = async () => {
|
||||
const username = selectName.value;
|
||||
await proxy?.$modal.confirm('是否确认解锁用户"' + username + '"数据项?');
|
||||
await unlockLoginInfo(username);
|
||||
proxy?.$modal.msgSuccess('用户' + username + '解锁成功');
|
||||
};
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'monitor/logininfor/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`logininfor_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<div class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="登录地址" prop="ipaddr">
|
||||
<el-input v-model="queryParams.ipaddr" placeholder="请输入登录地址" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
<el-card shadow="hover">
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
border
|
||||
:data="onlineList.slice((queryParams.pageNum - 1) * queryParams.pageSize, queryParams.pageNum * queryParams.pageSize)"
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column label="序号" width="50" type="index" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="会话编号" align="center" prop="tokenId" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="登录名称" align="center" prop="userName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="客户端" align="center" prop="clientKey" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="设备类型" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="所属部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="登录时间" align="center" prop="loginTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.loginTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="强退" placement="top">
|
||||
<el-button v-hasPermi="['monitor:online:forceLogout']" link type="primary" icon="Delete" @click="handleForceLogout(scope.row)">
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Online" lang="ts">
|
||||
import { forceLogout, list as initData } from '@/api/monitor/online';
|
||||
import { OnlineQuery, OnlineVO } from '@/api/monitor/online/types';
|
||||
import api from '@/api/system/user';
|
||||
import { to } from 'await-to-js';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
|
||||
|
||||
const onlineList = ref<OnlineVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const queryParams = ref<OnlineQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
ipaddr: '',
|
||||
userName: ''
|
||||
});
|
||||
|
||||
/** 查询登录日志列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await initData(queryParams.value);
|
||||
onlineList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
/** 强退按钮操作 */
|
||||
const handleForceLogout = async (row: OnlineVO) => {
|
||||
const [err] = await to(proxy?.$modal.confirm('是否确认强退名称为"' + row.userName + '"的用户?') as any);
|
||||
if (!err) {
|
||||
await forceLogout(row.tokenId);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="操作地址" prop="operIp">
|
||||
<el-input v-model="queryParams.operIp" placeholder="请输入操作地址" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="系统模块" prop="title">
|
||||
<el-input v-model="queryParams.title" placeholder="请输入系统模块" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人员" prop="operName">
|
||||
<el-input v-model="queryParams.operName" placeholder="请输入操作人员" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="businessType">
|
||||
<el-select v-model="queryParams.businessType" placeholder="操作类型" clearable>
|
||||
<el-option v-for="dict in sys_oper_type" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="操作状态" clearable>
|
||||
<el-option v-for="dict in sys_common_status" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作时间" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['monitor:operlog:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['monitor:operlog:remove']" type="danger" plain icon="WarnTriangleFilled" @click="handleClean">清空</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['monitor:operlog:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="operLogTableRef"
|
||||
v-loading="loading"
|
||||
:data="operlogList"
|
||||
border
|
||||
:default-sort="defaultSort"
|
||||
@selection-change="handleSelectionChange"
|
||||
@sort-change="handleSortChange"
|
||||
>
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column label="日志编号" align="center" prop="operId" />
|
||||
<el-table-column label="系统模块" align="center" prop="title" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作类型" align="center" prop="businessType">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_oper_type" :value="scope.row.businessType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="操作人员"
|
||||
align="center"
|
||||
width="110"
|
||||
prop="operName"
|
||||
:show-overflow-tooltip="true"
|
||||
sortable="custom"
|
||||
:sort-orders="['descending', 'ascending']"
|
||||
/>
|
||||
<el-table-column label="部门" align="center" prop="deptName" width="130" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作地址" align="center" prop="operIp" width="130" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_common_status" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作日期" align="center" prop="operTime" width="180" sortable="custom" :sort-orders="['descending', 'ascending']">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.operTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="消耗时间"
|
||||
align="center"
|
||||
prop="costTime"
|
||||
width="110"
|
||||
:show-overflow-tooltip="true"
|
||||
sortable="custom"
|
||||
:sort-orders="['descending', 'ascending']"
|
||||
>
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.costTime }}毫秒</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="详细" placement="top">
|
||||
<el-button v-hasPermi="['monitor:operlog:query']" link type="primary" icon="View" @click="handleView(scope.row)"> </el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 操作日志详细 -->
|
||||
<OperInfoDialog ref="operInfoDialogRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Operlog" lang="ts">
|
||||
import { list, delOperlog, cleanOperlog } from '@/api/monitor/operlog';
|
||||
import { OperLogForm, OperLogQuery, OperLogVO } from '@/api/monitor/operlog/types';
|
||||
import OperInfoDialog from './oper-info-dialog.vue';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_oper_type, sys_common_status } = toRefs<any>(proxy?.useDict('sys_oper_type', 'sys_common_status'));
|
||||
|
||||
const operlogList = ref<OperLogVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<number | string>>([]);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||
const defaultSort = ref<any>({ prop: 'operTime', order: 'descending' });
|
||||
|
||||
const operLogTableRef = ref<ElTableInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const data = reactive<PageData<OperLogForm, OperLogQuery>>({
|
||||
form: {
|
||||
operId: undefined,
|
||||
tenantId: undefined,
|
||||
title: '',
|
||||
businessType: 0,
|
||||
businessTypes: undefined,
|
||||
method: '',
|
||||
requestMethod: '',
|
||||
operatorType: 0,
|
||||
operName: '',
|
||||
deptName: '',
|
||||
operUrl: '',
|
||||
operIp: '',
|
||||
operLocation: '',
|
||||
operParam: '',
|
||||
jsonResult: '',
|
||||
status: 0,
|
||||
errorMsg: '',
|
||||
operTime: '',
|
||||
costTime: 0
|
||||
},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
operIp: '',
|
||||
title: '',
|
||||
operName: '',
|
||||
businessType: '',
|
||||
status: '',
|
||||
orderByColumn: defaultSort.value.prop,
|
||||
isAsc: defaultSort.value.order
|
||||
},
|
||||
rules: {}
|
||||
});
|
||||
|
||||
const { queryParams, form } = toRefs(data);
|
||||
|
||||
/** 查询登录日志 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await list(proxy?.addDateRange(queryParams.value, dateRange.value));
|
||||
operlogList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
/** 操作日志类型字典翻译 */
|
||||
const typeFormat = (row: OperLogForm) => {
|
||||
return proxy?.selectDictLabel(sys_oper_type.value, row.businessType);
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = ['', ''];
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.pageNum = 1;
|
||||
operLogTableRef.value?.sort(defaultSort.value.prop, defaultSort.value.order);
|
||||
};
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: OperLogVO[]) => {
|
||||
ids.value = selection.map((item) => item.operId);
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
/** 排序触发事件 */
|
||||
const handleSortChange = (column: any) => {
|
||||
queryParams.value.orderByColumn = column.prop;
|
||||
queryParams.value.isAsc = column.order;
|
||||
getList();
|
||||
};
|
||||
|
||||
const operInfoDialogRef = ref<InstanceType<typeof OperInfoDialog>>();
|
||||
/** 详细按钮操作 */
|
||||
const handleView = (row: OperLogVO) => {
|
||||
operInfoDialogRef.value.openDialog(row);
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: OperLogVO) => {
|
||||
const operIds = row?.operId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除日志编号为"' + operIds + '"的数据项?');
|
||||
await delOperlog(operIds);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
/** 清空按钮操作 */
|
||||
const handleClean = async () => {
|
||||
await proxy?.$modal.confirm('是否确认清空所有操作日志数据项?');
|
||||
await cleanOperlog();
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('清空成功');
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'monitor/operlog/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`config_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,111 @@
|
||||
<template>
|
||||
<el-dialog v-model="open" title="操作日志详细" width="700px" append-to-body close-on-click-modal @closed="info = null">
|
||||
<el-descriptions v-if="info" :column="1" border>
|
||||
<el-descriptions-item label="操作状态">
|
||||
<template #default>
|
||||
<el-tag v-if="info.status === 0" type="success">正常</el-tag>
|
||||
<el-tag v-else-if="info.status === 1" type="danger">失败</el-tag>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="登录信息">
|
||||
<template #default> {{ info.operName }} / {{ info.deptName }} / {{ info.operIp }} / {{ info.operLocation }} </template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求信息">
|
||||
<template #default> {{ info.requestMethod }} {{ info.operUrl }} </template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作模块">
|
||||
<template #default> {{ info.title }} / {{ typeFormat(info) }} </template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作方法">
|
||||
<template #default>
|
||||
{{ info.method }}
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="请求参数">
|
||||
<template #default>
|
||||
<div class="max-h-300px overflow-y-auto">
|
||||
<VueJsonPretty :data="formatToJsonObject(info.operParam)" />
|
||||
</div>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="返回参数">
|
||||
<template #default>
|
||||
<div class="max-h-300px overflow-y-auto">
|
||||
<VueJsonPretty :data="formatToJsonObject(info.jsonResult)" />
|
||||
</div>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="消耗时间">
|
||||
<template #default>
|
||||
<span> {{ info.costTime }}ms </span>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间">
|
||||
<template #default> {{ proxy.parseTime(info.operTime) }}</template>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item v-if="info.status === 1" label="异常信息">
|
||||
<template #default>
|
||||
<span class="text-danger"> {{ info.errorMsg }}</span>
|
||||
</template>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { OperLogForm } from '@/api/monitor/operlog/types';
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
|
||||
const open = ref(false);
|
||||
const info = ref<OperLogForm | null>(null);
|
||||
function openDialog(row: OperLogForm) {
|
||||
info.value = row;
|
||||
open.value = true;
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
open.value = false;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
closeDialog
|
||||
});
|
||||
|
||||
/**
|
||||
* json转为对象
|
||||
* @param data 原始数据
|
||||
*/
|
||||
function formatToJsonObject(data: string) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字典信息
|
||||
*/
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_oper_type } = toRefs<any>(proxy?.useDict('sys_oper_type'));
|
||||
const typeFormat = (row: OperLogForm) => {
|
||||
return proxy?.selectDictLabel(sys_oper_type.value, row.businessType);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/**
|
||||
label宽度固定
|
||||
*/
|
||||
:deep(.el-descriptions__label) {
|
||||
min-width: 100px;
|
||||
}
|
||||
/**
|
||||
文字超过 换行显示
|
||||
*/
|
||||
:deep(.el-descriptions__content) {
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<i-frame v-model:src="url"></i-frame>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const url = ref(import.meta.env.VITE_APP_SNAILJOB_ADMIN);
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { params, query } = route;
|
||||
const { path } = params;
|
||||
|
||||
router.replace({ path: '/' + path, query });
|
||||
</script>
|
||||
@@ -0,0 +1,263 @@
|
||||
<template>
|
||||
<div class="register">
|
||||
<el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
|
||||
<div class="title-box">
|
||||
<h3 class="title">{{ title }}</h3>
|
||||
<lang-select />
|
||||
</div>
|
||||
<el-form-item v-if="tenantEnabled" prop="tenantId">
|
||||
<el-select v-model="registerForm.tenantId" filterable :placeholder="proxy.$t('register.selectPlaceholder')" style="width: 100%">
|
||||
<el-option v-for="item in tenantList" :key="item.tenantId" :label="item.companyName" :value="item.tenantId"> </el-option>
|
||||
<template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="registerForm.username" type="text" size="large" auto-complete="off" :placeholder="proxy.$t('register.username')">
|
||||
<template #prefix><svg-icon icon-class="user" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input
|
||||
v-model="registerForm.password"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
:placeholder="proxy.$t('register.password')"
|
||||
@keyup.enter="handleRegister"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item prop="confirmPassword">
|
||||
<el-input
|
||||
v-model="registerForm.confirmPassword"
|
||||
type="password"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
:placeholder="proxy.$t('register.confirmPassword')"
|
||||
@keyup.enter="handleRegister"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="password" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="captchaEnabled" prop="code">
|
||||
<el-input
|
||||
v-model="registerForm.code"
|
||||
size="large"
|
||||
auto-complete="off"
|
||||
:placeholder="proxy.$t('register.code')"
|
||||
style="width: 63%"
|
||||
@keyup.enter="handleRegister"
|
||||
>
|
||||
<template #prefix><svg-icon icon-class="validCode" class="el-input__icon input-icon" /></template>
|
||||
</el-input>
|
||||
<div class="register-code">
|
||||
<img :src="codeUrl" class="register-code-img" @click="getCode" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item style="width: 100%">
|
||||
<el-button :loading="loading" size="large" type="primary" style="width: 100%" @click.prevent="handleRegister">
|
||||
<span v-if="!loading">{{ proxy.$t('register.register') }}</span>
|
||||
<span v-else>{{ proxy.$t('register.registering') }}</span>
|
||||
</el-button>
|
||||
<div style="float: right">
|
||||
<router-link class="link-type" :to="'/login'">{{ proxy.$t('register.switchLoginPage') }}</router-link>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 底部 -->
|
||||
<div class="el-register-footer">
|
||||
<span>Copyright © 2018-2026 疯狂的狮子Li All Rights Reserved.</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getCodeImg, register, getTenantList } from '@/api/login';
|
||||
import { RegisterForm, TenantVO } from '@/api/types';
|
||||
import { to } from 'await-to-js';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const title = import.meta.env.VITE_APP_TITLE;
|
||||
const router = useRouter();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const registerForm = ref<RegisterForm>({
|
||||
tenantId: '',
|
||||
username: '',
|
||||
password: '',
|
||||
confirmPassword: '',
|
||||
code: '',
|
||||
uuid: '',
|
||||
userType: 'sys_user'
|
||||
});
|
||||
|
||||
// 租户开关
|
||||
const tenantEnabled = ref(true);
|
||||
|
||||
const equalToPassword = (rule: any, value: string, callback: any) => {
|
||||
if (registerForm.value.password !== value) {
|
||||
callback(new Error(t('register.rule.confirmPassword.equalToPassword')));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
|
||||
const registerRules: ElFormRules = {
|
||||
tenantId: [{ required: true, trigger: 'blur', message: t('register.rule.tenantId.required') }],
|
||||
username: [
|
||||
{ required: true, trigger: 'blur', message: t('register.rule.username.required') },
|
||||
{ min: 2, max: 20, message: t('register.rule.username.length', { min: 2, max: 20 }), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, trigger: 'blur', message: t('register.rule.password.required') },
|
||||
{ min: 5, max: 20, message: t('register.rule.password.length', { min: 5, max: 20 }), trigger: 'blur' },
|
||||
{ pattern: /^[^<>"'|\\]+$/, message: t('register.rule.password.pattern', { strings: '< > " \' \\ |' }), trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, trigger: 'blur', message: t('register.rule.confirmPassword.required') },
|
||||
{ required: true, validator: equalToPassword, trigger: 'blur' }
|
||||
],
|
||||
code: [{ required: true, trigger: 'change', message: t('register.rule.code.required') }]
|
||||
};
|
||||
const codeUrl = ref('');
|
||||
const loading = ref(false);
|
||||
const captchaEnabled = ref(true);
|
||||
const registerRef = ref<ElFormInstance>();
|
||||
// 租户列表
|
||||
const tenantList = ref<TenantVO[]>([]);
|
||||
|
||||
const handleRegister = () => {
|
||||
registerRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
const [err] = await to(register(registerForm.value));
|
||||
if (!err) {
|
||||
const username = registerForm.value.username;
|
||||
await ElMessageBox.alert('<span style="color: red; ">' + t('register.registerSuccess', { username }) + '</font>', '系统提示', {
|
||||
app: undefined,
|
||||
dangerouslyUseHTMLString: true,
|
||||
type: 'success'
|
||||
});
|
||||
await router.push('/login');
|
||||
} else {
|
||||
loading.value = false;
|
||||
if (captchaEnabled.value) {
|
||||
getCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getCode = async () => {
|
||||
const res = await getCodeImg();
|
||||
const { data } = res;
|
||||
captchaEnabled.value = data.captchaEnabled === undefined ? true : data.captchaEnabled;
|
||||
if (captchaEnabled.value) {
|
||||
codeUrl.value = 'data:image/gif;base64,' + data.img;
|
||||
registerForm.value.uuid = data.uuid;
|
||||
}
|
||||
};
|
||||
|
||||
const initTenantList = async () => {
|
||||
const { data } = await getTenantList(false);
|
||||
tenantEnabled.value = data.tenantEnabled === undefined ? true : data.tenantEnabled;
|
||||
if (tenantEnabled.value) {
|
||||
tenantList.value = data.voList;
|
||||
if (tenantList.value != null && tenantList.value.length !== 0) {
|
||||
registerForm.value.tenantId = tenantList.value[0].tenantId;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getCode();
|
||||
initTenantList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.register {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
background-image: url('../assets/images/login-background.jpg');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.title-box {
|
||||
display: flex;
|
||||
|
||||
.title {
|
||||
margin: 0px auto 30px auto;
|
||||
text-align: center;
|
||||
color: #707070;
|
||||
}
|
||||
|
||||
:deep(.lang-select--style) {
|
||||
line-height: 0;
|
||||
color: #7483a3;
|
||||
}
|
||||
}
|
||||
|
||||
.register-form {
|
||||
border-radius: 6px;
|
||||
background: #ffffff;
|
||||
width: 400px;
|
||||
padding: 25px 25px 5px 25px;
|
||||
|
||||
.el-input {
|
||||
height: 40px;
|
||||
|
||||
input {
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
height: 39px;
|
||||
width: 14px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.register-tip {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
color: #bfbfbf;
|
||||
}
|
||||
|
||||
.register-code {
|
||||
width: 33%;
|
||||
height: 40px;
|
||||
float: right;
|
||||
|
||||
img {
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.el-register-footer {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-family: Arial, serif;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.register-code-img {
|
||||
height: 40px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,316 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="search">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="85px">
|
||||
<el-form-item label="客户端key" prop="clientKey">
|
||||
<el-input v-model="queryParams.clientKey" placeholder="请输入客户端key" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户端秘钥" prop="clientSecret">
|
||||
<el-input v-model="queryParams.clientSecret" placeholder="请输入客户端秘钥" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="状态" clearable>
|
||||
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:client:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:client:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">
|
||||
修改
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:client:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:client:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="clientList" border @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="true" label="id" align="center" prop="id" />
|
||||
<el-table-column label="客户端id" align="center" prop="clientId" />
|
||||
<el-table-column label="客户端key" align="center" prop="clientKey" />
|
||||
<el-table-column label="客户端秘钥" align="center" prop="clientSecret" />
|
||||
<el-table-column label="授权类型" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_grant_type" :value="scope.row.grantTypeList" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="设备类型" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Token活跃超时时间" align="center" prop="activeTimeout" />
|
||||
<el-table-column label="Token固定超时时间" align="center" prop="timeout" />
|
||||
<el-table-column key="status" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:client:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:client:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改客户端管理对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="clientFormRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="客户端key" prop="clientKey">
|
||||
<el-input v-model="form.clientKey" :disabled="form.id != null" placeholder="请输入客户端key" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户端秘钥" prop="clientSecret">
|
||||
<el-input v-model="form.clientSecret" :disabled="form.id != null" placeholder="请输入客户端秘钥" />
|
||||
</el-form-item>
|
||||
<el-form-item label="授权类型" prop="grantTypeList">
|
||||
<el-select v-model="form.grantTypeList" multiple placeholder="请输入授权类型">
|
||||
<el-option v-for="dict in sys_grant_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="设备类型" prop="deviceType">
|
||||
<el-select v-model="form.deviceType" placeholder="请输入设备类型">
|
||||
<el-option v-for="dict in sys_device_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item prop="activeTimeout" label-width="auto">
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="指定时间无操作则过期(单位:秒),默认30分钟(1800秒)" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
Token活跃超时时间
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="form.activeTimeout" placeholder="请输入Token活跃超时时间" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="timeout" label-width="auto">
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="指定时间必定过期(单位:秒),默认七天(604800秒)" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
Token固定超时时间
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="form.timeout" placeholder="请输入Token固定超时时间" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Client" lang="ts">
|
||||
import { listClient, getClient, delClient, addClient, updateClient, changeStatus } from '@/api/system/client';
|
||||
import { ClientVO, ClientQuery, ClientForm } from '@/api/system/client/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
|
||||
const { sys_grant_type } = toRefs<any>(proxy?.useDict('sys_grant_type'));
|
||||
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
|
||||
|
||||
const clientList = ref<ClientVO[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const clientFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: ClientForm = {
|
||||
id: undefined,
|
||||
clientId: undefined,
|
||||
clientKey: undefined,
|
||||
clientSecret: undefined,
|
||||
grantTypeList: undefined,
|
||||
deviceType: undefined,
|
||||
activeTimeout: undefined,
|
||||
timeout: undefined,
|
||||
status: undefined
|
||||
};
|
||||
const data = reactive<PageData<ClientForm, ClientQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
clientId: undefined,
|
||||
clientKey: undefined,
|
||||
clientSecret: undefined,
|
||||
grantType: undefined,
|
||||
deviceType: undefined,
|
||||
activeTimeout: undefined,
|
||||
timeout: undefined,
|
||||
status: undefined
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: 'id不能为空', trigger: 'blur' }],
|
||||
clientId: [{ required: true, message: '客户端id不能为空', trigger: 'blur' }],
|
||||
clientKey: [{ required: true, message: '客户端key不能为空', trigger: 'blur' }],
|
||||
clientSecret: [{ required: true, message: '客户端秘钥不能为空', trigger: 'blur' }],
|
||||
grantTypeList: [{ required: true, message: '授权类型不能为空', trigger: 'change' }],
|
||||
deviceType: [{ required: true, message: '设备类型不能为空', trigger: 'change' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询客户端管理列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listClient(queryParams.value);
|
||||
clientList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
clientFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: ClientVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加客户端管理';
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: ClientVO) => {
|
||||
reset();
|
||||
const _id = row?.id || ids.value[0];
|
||||
const res = await getClient(_id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改客户端管理';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
clientFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.id) {
|
||||
await updateClient(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addClient(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('修改成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: ClientVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除客户端管理编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||
await delClient(_ids);
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
await getList();
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'system/client/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`client_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
/** 状态修改 */
|
||||
const handleStatusChange = async (row: ClientVO) => {
|
||||
const text = row.status === '0' ? '启用' : '停用';
|
||||
try {
|
||||
await proxy?.$modal.confirm('确认要"' + text + '"吗?');
|
||||
await changeStatus(row.clientId, row.status);
|
||||
proxy?.$modal.msgSuccess(text + '成功');
|
||||
} catch (err) {
|
||||
row.status = row.status === '0' ? '1' : '0';
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="参数名称" prop="configName">
|
||||
<el-input v-model="queryParams.configName" placeholder="请输入参数名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键名" prop="configKey">
|
||||
<el-input v-model="queryParams.configKey" placeholder="请输入参数键名" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="系统内置" prop="configType">
|
||||
<el-select v-model="queryParams.configType" placeholder="系统内置" clearable>
|
||||
<el-option v-for="dict in sys_yes_no" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:config:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:config:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">
|
||||
修改
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:config:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:config:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:config:remove']" type="danger" plain icon="Refresh" @click="handleRefreshCache">刷新缓存</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="configList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="参数主键" align="center" prop="configId" />
|
||||
<el-table-column label="参数名称" align="center" prop="configName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="参数键名" align="center" prop="configKey" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="参数键值" align="center" prop="configValue" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="系统内置" align="center" prop="configType">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_yes_no" :value="scope.row.configType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="150" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:config:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:config:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
|
||||
<!-- 添加或修改参数配置对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="configFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="参数名称" prop="configName">
|
||||
<el-input v-model="form.configName" placeholder="请输入参数名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键名" prop="configKey">
|
||||
<el-input v-model="form.configKey" placeholder="请输入参数键名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="参数键值" prop="configValue">
|
||||
<el-input v-model="form.configValue" type="textarea" placeholder="请输入参数键值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="系统内置" prop="configType">
|
||||
<el-radio-group v-model="form.configType">
|
||||
<el-radio v-for="dict in sys_yes_no" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Config" lang="ts">
|
||||
import { listConfig, getConfig, delConfig, addConfig, updateConfig, refreshCache } from '@/api/system/config';
|
||||
import { ConfigForm, ConfigQuery, ConfigVO } from '@/api/system/config/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_yes_no } = toRefs<any>(proxy?.useDict('sys_yes_no'));
|
||||
|
||||
const configList = ref<ConfigVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<number | string>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const configFormRef = ref<ElFormInstance>();
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
const initFormData: ConfigForm = {
|
||||
configId: undefined,
|
||||
configName: '',
|
||||
configKey: '',
|
||||
configValue: '',
|
||||
configType: 'Y',
|
||||
remark: ''
|
||||
};
|
||||
const data = reactive<PageData<ConfigForm, ConfigQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
configName: '',
|
||||
configKey: '',
|
||||
configType: ''
|
||||
},
|
||||
rules: {
|
||||
configName: [{ required: true, message: '参数名称不能为空', trigger: 'blur' }],
|
||||
configKey: [{ required: true, message: '参数键名不能为空', trigger: 'blur' }],
|
||||
configValue: [{ required: true, message: '参数键值不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询参数列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listConfig(proxy?.addDateRange(queryParams.value, dateRange.value));
|
||||
configList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
configFormRef.value?.resetFields();
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = ['', ''];
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: ConfigVO[]) => {
|
||||
ids.value = selection.map((item) => item.configId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加参数';
|
||||
};
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: ConfigVO) => {
|
||||
reset();
|
||||
const configId = row?.configId || ids.value[0];
|
||||
const res = await getConfig(configId);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改参数';
|
||||
};
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
configFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
form.value.configId ? await updateConfig(form.value) : await addConfig(form.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: ConfigVO) => {
|
||||
const configIds = row?.configId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除参数编号为"' + configIds + '"的数据项?');
|
||||
await delConfig(configIds);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'system/config/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`config_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
/** 刷新缓存按钮操作 */
|
||||
const handleRefreshCache = async () => {
|
||||
await refreshCache();
|
||||
proxy?.$modal.msgSuccess('刷新缓存成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,320 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="部门名称" prop="deptName">
|
||||
<el-input v-model="queryParams.deptName" placeholder="请输入部门名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类别编码" prop="deptCategory">
|
||||
<el-input v-model="queryParams.deptCategory" placeholder="请输入类别编码" clearable style="width: 240px" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="部门状态" clearable>
|
||||
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:dept:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增 </el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="deptTableRef"
|
||||
v-loading="loading"
|
||||
:data="deptList"
|
||||
row-key="deptId"
|
||||
border
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="isExpandAll"
|
||||
>
|
||||
<el-table-column prop="deptName" label="部门名称" width="260"></el-table-column>
|
||||
<el-table-column prop="deptCategory" align="center" label="类别编码" width="200"></el-table-column>
|
||||
<el-table-column prop="orderNum" align="center" label="排序" width="200"></el-table-column>
|
||||
<el-table-column prop="status" align="center" label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="200">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:dept:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="新增" placement="top">
|
||||
<el-button v-hasPermi="['system:dept:add']" link type="primary" icon="Plus" @click="handleAdd(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:dept:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-body width="600px">
|
||||
<el-form ref="deptFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-row>
|
||||
<el-col v-if="form.parentId !== 0" :span="24">
|
||||
<el-form-item label="上级部门" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="deptOptions"
|
||||
:props="{ value: 'deptId', label: 'deptName', children: 'children' } as any"
|
||||
value-key="deptId"
|
||||
placeholder="选择上级部门"
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门名称" prop="deptName">
|
||||
<el-input v-model="form.deptName" placeholder="请输入部门名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="类别编码" prop="deptCategory">
|
||||
<el-input v-model="form.deptCategory" placeholder="请输入类别编码" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="显示排序" prop="orderNum">
|
||||
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="负责人" prop="leader">
|
||||
<el-select v-model="form.leader" placeholder="请选择负责人">
|
||||
<el-option v-for="item in deptUserList" :key="item.userId" :label="item.userName" :value="item.userId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话" prop="phone">
|
||||
<el-input v-model="form.phone" placeholder="请输入联系电话" maxlength="11" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="部门状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Dept" lang="ts">
|
||||
import { listDept, getDept, delDept, addDept, updateDept, listDeptExcludeChild } from '@/api/system/dept';
|
||||
import { DeptForm, DeptQuery, DeptVO } from '@/api/system/dept/types';
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
import { listUserByDeptId } from '@/api/system/user';
|
||||
|
||||
interface DeptOptionsType {
|
||||
deptId: number | string;
|
||||
deptName: string;
|
||||
children: DeptOptionsType[];
|
||||
}
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
|
||||
|
||||
const deptList = ref<DeptVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const deptOptions = ref<DeptOptionsType[]>([]);
|
||||
const isExpandAll = ref(true);
|
||||
const deptUserList = ref<UserVO[]>([]);
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const deptTableRef = ref<ElTableInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const deptFormRef = ref<ElFormInstance>();
|
||||
|
||||
const initFormData: DeptForm = {
|
||||
deptId: undefined,
|
||||
parentId: undefined,
|
||||
deptName: undefined,
|
||||
deptCategory: undefined,
|
||||
orderNum: 0,
|
||||
leader: undefined,
|
||||
phone: undefined,
|
||||
email: undefined,
|
||||
status: '0'
|
||||
};
|
||||
const initData: PageData<DeptForm, DeptQuery> = {
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
deptName: undefined,
|
||||
deptCategory: undefined,
|
||||
status: undefined
|
||||
},
|
||||
rules: {
|
||||
parentId: [{ required: true, message: '上级部门不能为空', trigger: 'blur' }],
|
||||
deptName: [{ required: true, message: '部门名称不能为空', trigger: 'blur' }],
|
||||
orderNum: [{ required: true, message: '显示排序不能为空', trigger: 'blur' }],
|
||||
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
|
||||
phone: [{ pattern: /^1[3456789][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }]
|
||||
}
|
||||
};
|
||||
const data = reactive<PageData<DeptForm, DeptQuery>>(initData);
|
||||
|
||||
const { queryParams, form, rules } = toRefs<PageData<DeptForm, DeptQuery>>(data);
|
||||
|
||||
/** 查询菜单列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listDept(queryParams.value);
|
||||
const data = proxy?.handleTree<DeptVO>(res.data, 'deptId');
|
||||
if (data) {
|
||||
deptList.value = data;
|
||||
}
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 查询当前部门的所有用户 */
|
||||
async function getDeptAllUser(deptId: any) {
|
||||
if (deptId !== null && deptId !== '' && deptId !== undefined) {
|
||||
const res = await listUserByDeptId(deptId);
|
||||
deptUserList.value = res.data;
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
deptFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 展开/折叠操作 */
|
||||
const handleToggleExpandAll = () => {
|
||||
isExpandAll.value = !isExpandAll.value;
|
||||
toggleExpandAll(deptList.value, isExpandAll.value);
|
||||
};
|
||||
/** 展开/折叠所有 */
|
||||
const toggleExpandAll = (data: DeptVO[], status: boolean) => {
|
||||
data.forEach((item) => {
|
||||
deptTableRef.value?.toggleRowExpansion(item, status);
|
||||
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
|
||||
});
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = async (row?: DeptVO) => {
|
||||
reset();
|
||||
const res = await listDept();
|
||||
const data = proxy?.handleTree<DeptOptionsType>(res.data, 'deptId');
|
||||
if (data) {
|
||||
deptOptions.value = data;
|
||||
if (row && row.deptId) {
|
||||
form.value.parentId = row?.deptId;
|
||||
}
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加部门';
|
||||
}
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row: DeptVO) => {
|
||||
reset();
|
||||
//查询当前部门所有用户
|
||||
getDeptAllUser(row.deptId);
|
||||
const res = await getDept(row.deptId);
|
||||
form.value = res.data;
|
||||
const response = await listDeptExcludeChild(row.deptId);
|
||||
const data = proxy?.handleTree<DeptOptionsType>(response.data, 'deptId');
|
||||
if (data) {
|
||||
deptOptions.value = data;
|
||||
if (data.length === 0) {
|
||||
const noResultsOptions: DeptOptionsType = {
|
||||
deptId: res.data.parentId,
|
||||
deptName: res.data.parentName,
|
||||
children: []
|
||||
};
|
||||
deptOptions.value.push(noResultsOptions);
|
||||
}
|
||||
}
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改部门';
|
||||
};
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
deptFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
form.value.deptId ? await updateDept(form.value) : await addDept(form.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row: DeptVO) => {
|
||||
await proxy?.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?');
|
||||
await delDept(row.deptId);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,676 @@
|
||||
<template>
|
||||
<div class="p-2 dict-page">
|
||||
<el-row :gutter="16" class="dict-grid">
|
||||
<!-- 字典类型 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card shadow="hover" class="dict-card">
|
||||
<template #header>
|
||||
<div class="dict-card__header">
|
||||
<div class="dict-card__title">字典管理</div>
|
||||
<right-toolbar v-model:show-search="showTypeSearch" @query-table="getTypeList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="showTypeSearch" class="dict-form-scroll">
|
||||
<el-form ref="typeQueryFormRef" :model="typeQueryParams" :inline="true">
|
||||
<el-form-item label="字典名称" prop="dictName">
|
||||
<el-input v-model="typeQueryParams.dictName" placeholder="请输入字典名称" clearable @keyup.enter="handleTypeQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="字典类型" prop="dictType">
|
||||
<el-input v-model="typeQueryParams.dictType" placeholder="请输入字典类型" clearable @keyup.enter="handleTypeQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleTypeQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="handleTypeResetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="dict-actions">
|
||||
<el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" @click="handleTypeAdd">新增</el-button>
|
||||
<el-button v-hasPermi="['system:dict:edit']" type="success" plain icon="Edit" :disabled="typeSingle" @click="handleTypeUpdate()"
|
||||
>修改</el-button
|
||||
>
|
||||
<el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Delete" :disabled="typeMultiple" @click="handleTypeDelete()"
|
||||
>删除</el-button
|
||||
>
|
||||
<el-button v-hasPermi="['system:dict:export']" type="warning" plain icon="Download" @click="handleTypeExport">导出</el-button>
|
||||
<el-button v-hasPermi="['system:dict:remove']" type="danger" plain icon="Refresh" @click="handleRefreshCache">刷新缓存</el-button>
|
||||
</div>
|
||||
|
||||
<div class="dict-table-wrap">
|
||||
<el-table
|
||||
ref="typeTableRef"
|
||||
v-loading="typeLoading"
|
||||
border
|
||||
:data="typeList"
|
||||
highlight-current-row
|
||||
@row-click="handleTypeRowClick"
|
||||
@selection-change="handleTypeSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="字典编号" align="center" prop="dictId" />
|
||||
<el-table-column label="字典名称" align="center" prop="dictName" width="120" />
|
||||
<el-table-column label="字典类型" align="center" prop="dictType" width="160">
|
||||
<template #default="scope">
|
||||
<span class="link-type" @click.stop="handleTypeRowClick(scope.row)">{{ scope.row.dictType }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center" prop="remark" width="160" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" align="center" width="120" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:dict:edit']" link type="primary" icon="Edit" @click="handleTypeUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:dict:remove']" link type="primary" icon="Delete" @click="handleTypeDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<pagination
|
||||
v-show="typeTotal > 0"
|
||||
v-model:page="typeQueryParams.pageNum"
|
||||
v-model:limit="typeQueryParams.pageSize"
|
||||
:total="typeTotal"
|
||||
@pagination="getTypeList"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
|
||||
<!-- 字典数据 -->
|
||||
<el-col :xs="24" :lg="12">
|
||||
<el-card shadow="hover" class="dict-card">
|
||||
<template #header>
|
||||
<div class="dict-card__header">
|
||||
<div class="dict-card__title">
|
||||
字典数据
|
||||
<span class="dict-card__subtitle">{{ currentDictLabel }}</span>
|
||||
</div>
|
||||
<right-toolbar v-model:show-search="showDataSearch" @query-table="getDataList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="showDataSearch" class="dict-form-scroll">
|
||||
<el-form ref="dataQueryFormRef" :model="dataQueryParams" :inline="true">
|
||||
<el-form-item label="字典标签" prop="dictLabel">
|
||||
<el-input
|
||||
v-model="dataQueryParams.dictLabel"
|
||||
placeholder="请输入字典标签"
|
||||
clearable
|
||||
:disabled="!hasCurrentDict"
|
||||
@keyup.enter="handleDataQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" :disabled="!hasCurrentDict" @click="handleDataQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" :disabled="!hasCurrentDict" @click="handleDataResetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="dict-actions">
|
||||
<el-button v-hasPermi="['system:dict:add']" type="primary" plain icon="Plus" :disabled="!hasCurrentDict" @click="handleDataAdd"
|
||||
>新增</el-button
|
||||
>
|
||||
<el-button
|
||||
v-hasPermi="['system:dict:edit']"
|
||||
type="success"
|
||||
plain
|
||||
icon="Edit"
|
||||
:disabled="dataSingle || !hasCurrentDict"
|
||||
@click="handleDataUpdate()"
|
||||
>修改</el-button
|
||||
>
|
||||
<el-button
|
||||
v-hasPermi="['system:dict:remove']"
|
||||
type="danger"
|
||||
plain
|
||||
icon="Delete"
|
||||
:disabled="dataMultiple || !hasCurrentDict"
|
||||
@click="handleDataDelete()"
|
||||
>删除</el-button
|
||||
>
|
||||
<el-button v-hasPermi="['system:dict:export']" type="warning" plain icon="Download" :disabled="!hasCurrentDict" @click="handleDataExport"
|
||||
>导出</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="dict-table-wrap">
|
||||
<el-table v-loading="dataLoading" border :data="dataList" @selection-change="handleDataSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="字典编码" align="center" prop="dictCode" />
|
||||
<el-table-column label="字典标签" align="center" prop="dictLabel" width="80">
|
||||
<template #default="scope">
|
||||
<span
|
||||
v-if="
|
||||
(scope.row.listClass === '' || scope.row.listClass === 'default') && (scope.row.cssClass === '' || scope.row.cssClass == null)
|
||||
"
|
||||
>{{ scope.row.dictLabel }}</span
|
||||
>
|
||||
<el-tag
|
||||
v-else
|
||||
:type="scope.row.listClass === 'primary' || scope.row.listClass === 'default' ? 'primary' : scope.row.listClass"
|
||||
:class="scope.row.cssClass"
|
||||
>{{ scope.row.dictLabel }}</el-tag
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字典键值" align="center" prop="dictValue" width="80" />
|
||||
<el-table-column label="字典排序" align="center" prop="dictSort" width="80" />
|
||||
<el-table-column label="备注" align="center" prop="remark" width="100" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" align="center" width="120" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:dict:edit']" link type="primary" icon="Edit" @click="handleDataUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:dict:remove']" link type="primary" icon="Delete" @click="handleDataDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
|
||||
<pagination
|
||||
v-show="dataTotal > 0"
|
||||
v-model:page="dataQueryParams.pageNum"
|
||||
v-model:limit="dataQueryParams.pageSize"
|
||||
:total="dataTotal"
|
||||
@pagination="getDataList"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 字典类型对话框 -->
|
||||
<el-dialog v-model="typeDialog.visible" :title="typeDialog.title" width="500px" append-to-body>
|
||||
<el-form ref="typeFormRef" :model="typeForm" :rules="typeRules" label-width="100px">
|
||||
<el-form-item label="字典名称" prop="dictName">
|
||||
<el-input v-model="typeForm.dictName" placeholder="请输入字典名称" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="dictType">
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="数据存储中的Key值,如:sys_user_sex" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
字典类型
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="typeForm.dictType" placeholder="请输入字典类型" maxlength="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="typeForm.remark" type="textarea" placeholder="请输入内容"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitTypeForm">确 定</el-button>
|
||||
<el-button @click="cancelType">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 字典数据对话框 -->
|
||||
<el-dialog v-model="dataDialog.visible" :title="dataDialog.title" width="500px" append-to-body>
|
||||
<el-form ref="dataFormRef" :model="dataForm" :rules="dataRules" label-width="80px">
|
||||
<el-form-item label="字典类型">
|
||||
<el-input v-model="dataForm.dictType" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据标签" prop="dictLabel">
|
||||
<el-input v-model="dataForm.dictLabel" placeholder="请输入数据标签" />
|
||||
</el-form-item>
|
||||
<el-form-item label="数据键值" prop="dictValue">
|
||||
<el-input v-model="dataForm.dictValue" placeholder="请输入数据键值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="样式属性" prop="cssClass">
|
||||
<el-input v-model="dataForm.cssClass" placeholder="请输入样式属性" />
|
||||
</el-form-item>
|
||||
<el-form-item label="显示排序" prop="dictSort">
|
||||
<el-input-number v-model="dataForm.dictSort" controls-position="right" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="回显样式" prop="listClass">
|
||||
<el-select v-model="dataForm.listClass">
|
||||
<el-option
|
||||
v-for="item in listClassOptions"
|
||||
:key="item.value"
|
||||
:label="item.label + '(' + item.value + ')'"
|
||||
:value="item.value"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="dataForm.remark" type="textarea" placeholder="请输入内容"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitDataForm">确 定</el-button>
|
||||
<el-button @click="cancelData">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Dict" lang="ts">
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { listType, getType, delType, addType, updateType, refreshCache } from '@/api/system/dict/type';
|
||||
import { listData, getData, delData, addData, updateData } from '@/api/system/dict/data';
|
||||
import { DictTypeForm, DictTypeQuery, DictTypeVO } from '@/api/system/dict/type/types';
|
||||
import { DictDataForm, DictDataQuery, DictDataVO } from '@/api/system/dict/data/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const typeList = ref<DictTypeVO[]>([]);
|
||||
const typeLoading = ref(true);
|
||||
const showTypeSearch = ref(true);
|
||||
const typeIds = ref<Array<number | string>>([]);
|
||||
const typeSingle = ref(true);
|
||||
const typeMultiple = ref(true);
|
||||
const typeTotal = ref(0);
|
||||
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||
|
||||
const typeFormRef = ref<ElFormInstance>();
|
||||
const typeQueryFormRef = ref<ElFormInstance>();
|
||||
const typeTableRef = ref<ElTableInstance>();
|
||||
|
||||
const typeDialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const typeInitFormData: DictTypeForm = {
|
||||
dictId: undefined,
|
||||
dictName: '',
|
||||
dictType: '',
|
||||
remark: ''
|
||||
};
|
||||
|
||||
const typeState = reactive<PageData<DictTypeForm, DictTypeQuery>>({
|
||||
form: { ...typeInitFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
dictName: '',
|
||||
dictType: ''
|
||||
},
|
||||
rules: {
|
||||
dictName: [{ required: true, message: '字典名称不能为空', trigger: 'blur' }],
|
||||
dictType: [{ required: true, message: '字典类型不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams: typeQueryParams, form: typeForm, rules: typeRules } = toRefs(typeState);
|
||||
|
||||
const currentDict = ref<DictTypeVO | null>(null);
|
||||
const hasCurrentDict = computed(() => !!currentDict.value);
|
||||
const currentDictLabel = computed(() => {
|
||||
if (!currentDict.value) return '请先选择字典';
|
||||
return `${currentDict.value.dictName} / ${currentDict.value.dictType}`;
|
||||
});
|
||||
|
||||
const dataList = ref<DictDataVO[]>([]);
|
||||
const dataLoading = ref(false);
|
||||
const showDataSearch = ref(true);
|
||||
const dataIds = ref<Array<string | number>>([]);
|
||||
const dataSingle = ref(true);
|
||||
const dataMultiple = ref(true);
|
||||
const dataTotal = ref(0);
|
||||
|
||||
const dataFormRef = ref<ElFormInstance>();
|
||||
const dataQueryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dataDialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const listClassOptions = ref<Array<{ value: string; label: string }>>([
|
||||
{ value: 'default', label: '默认' },
|
||||
{ value: 'primary', label: '主要' },
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'info', label: '信息' },
|
||||
{ value: 'warning', label: '警告' },
|
||||
{ value: 'danger', label: '危险' }
|
||||
]);
|
||||
|
||||
const dataInitFormData: DictDataForm = {
|
||||
dictCode: undefined,
|
||||
dictLabel: '',
|
||||
dictValue: '',
|
||||
cssClass: '',
|
||||
listClass: 'primary',
|
||||
dictSort: 0,
|
||||
remark: ''
|
||||
};
|
||||
|
||||
const dataState = reactive<PageData<DictDataForm, DictDataQuery>>({
|
||||
form: { ...dataInitFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
dictName: '',
|
||||
dictType: '',
|
||||
dictLabel: ''
|
||||
},
|
||||
rules: {
|
||||
dictLabel: [{ required: true, message: '数据标签不能为空', trigger: 'blur' }],
|
||||
dictValue: [{ required: true, message: '数据键值不能为空', trigger: 'blur' }],
|
||||
dictSort: [{ required: true, message: '数据顺序不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams: dataQueryParams, form: dataForm, rules: dataRules } = toRefs(dataState);
|
||||
|
||||
const getTypeList = () => {
|
||||
typeLoading.value = true;
|
||||
listType(proxy?.addDateRange(typeQueryParams.value, dateRange.value)).then((res) => {
|
||||
typeList.value = res.rows;
|
||||
typeTotal.value = res.total;
|
||||
typeLoading.value = false;
|
||||
ensureCurrentType();
|
||||
});
|
||||
};
|
||||
|
||||
const ensureCurrentType = () => {
|
||||
if (!typeList.value.length) {
|
||||
currentDict.value = null;
|
||||
dataQueryParams.value.dictType = '';
|
||||
dataList.value = [];
|
||||
dataTotal.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const current = currentDict.value && typeList.value.find((item) => item.dictId === currentDict.value?.dictId);
|
||||
const nextRow = current || typeList.value[0];
|
||||
setCurrentType(nextRow);
|
||||
};
|
||||
|
||||
const setCurrentType = (row: DictTypeVO) => {
|
||||
currentDict.value = row;
|
||||
dataQueryParams.value.dictType = row.dictType;
|
||||
dataQueryParams.value.pageNum = 1;
|
||||
dataQueryParams.value.dictLabel = '';
|
||||
getDataList();
|
||||
nextTick(() => typeTableRef.value?.setCurrentRow(row));
|
||||
};
|
||||
|
||||
const handleTypeRowClick = (row: DictTypeVO) => {
|
||||
setCurrentType(row);
|
||||
};
|
||||
|
||||
const cancelType = () => {
|
||||
resetTypeForm();
|
||||
typeDialog.visible = false;
|
||||
};
|
||||
|
||||
const resetTypeForm = () => {
|
||||
typeForm.value = { ...typeInitFormData };
|
||||
typeFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
const handleTypeQuery = () => {
|
||||
typeQueryParams.value.pageNum = 1;
|
||||
getTypeList();
|
||||
};
|
||||
|
||||
const handleTypeResetQuery = () => {
|
||||
dateRange.value = ['', ''];
|
||||
typeQueryFormRef.value?.resetFields();
|
||||
handleTypeQuery();
|
||||
};
|
||||
|
||||
const handleTypeAdd = () => {
|
||||
resetTypeForm();
|
||||
typeDialog.visible = true;
|
||||
typeDialog.title = '添加字典类型';
|
||||
};
|
||||
|
||||
const handleTypeSelectionChange = (selection: DictTypeVO[]) => {
|
||||
typeIds.value = selection.map((item) => item.dictId);
|
||||
typeSingle.value = selection.length != 1;
|
||||
typeMultiple.value = !selection.length;
|
||||
};
|
||||
|
||||
const handleTypeUpdate = async (row?: DictTypeVO) => {
|
||||
resetTypeForm();
|
||||
const dictId = row?.dictId || typeIds.value[0];
|
||||
const res = await getType(dictId);
|
||||
Object.assign(typeForm.value, res.data);
|
||||
typeDialog.visible = true;
|
||||
typeDialog.title = '修改字典类型';
|
||||
};
|
||||
|
||||
const submitTypeForm = () => {
|
||||
typeFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
typeForm.value.dictId ? await updateType(typeForm.value) : await addType(typeForm.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
typeDialog.visible = false;
|
||||
getTypeList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleTypeDelete = async (row?: DictTypeVO) => {
|
||||
const dictIds = row?.dictId || typeIds.value;
|
||||
await proxy?.$modal.confirm('是否确认删除字典编号为"' + dictIds + '"的数据项?');
|
||||
await delType(dictIds);
|
||||
getTypeList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
const handleTypeExport = () => {
|
||||
proxy?.download(
|
||||
'system/dict/type/export',
|
||||
{
|
||||
...typeQueryParams.value
|
||||
},
|
||||
`dict_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
const handleRefreshCache = async () => {
|
||||
await refreshCache();
|
||||
proxy?.$modal.msgSuccess('刷新成功');
|
||||
useDictStore().cleanDict();
|
||||
};
|
||||
|
||||
const getDataList = async () => {
|
||||
if (!currentDict.value) {
|
||||
dataList.value = [];
|
||||
dataTotal.value = 0;
|
||||
dataLoading.value = false;
|
||||
return;
|
||||
}
|
||||
dataLoading.value = true;
|
||||
const res = await listData(dataQueryParams.value);
|
||||
dataList.value = res.rows;
|
||||
dataTotal.value = res.total;
|
||||
dataLoading.value = false;
|
||||
};
|
||||
|
||||
const cancelData = () => {
|
||||
dataDialog.visible = false;
|
||||
resetDataForm();
|
||||
};
|
||||
|
||||
const resetDataForm = () => {
|
||||
dataForm.value = { ...dataInitFormData };
|
||||
dataFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
const handleDataQuery = () => {
|
||||
if (!currentDict.value) return;
|
||||
dataQueryParams.value.pageNum = 1;
|
||||
getDataList();
|
||||
};
|
||||
|
||||
const handleDataResetQuery = () => {
|
||||
dataQueryFormRef.value?.resetFields();
|
||||
dataQueryParams.value.dictLabel = '';
|
||||
handleDataQuery();
|
||||
};
|
||||
|
||||
const handleDataAdd = () => {
|
||||
if (!currentDict.value) {
|
||||
proxy?.$modal.msgWarning('请先选择字典');
|
||||
return;
|
||||
}
|
||||
resetDataForm();
|
||||
dataForm.value.dictType = currentDict.value.dictType;
|
||||
dataDialog.visible = true;
|
||||
dataDialog.title = '添加字典数据';
|
||||
};
|
||||
|
||||
const handleDataSelectionChange = (selection: DictDataVO[]) => {
|
||||
dataIds.value = selection.map((item) => item.dictCode);
|
||||
dataSingle.value = selection.length != 1;
|
||||
dataMultiple.value = !selection.length;
|
||||
};
|
||||
|
||||
const handleDataUpdate = async (row?: DictDataVO) => {
|
||||
if (!currentDict.value) {
|
||||
proxy?.$modal.msgWarning('请先选择字典');
|
||||
return;
|
||||
}
|
||||
resetDataForm();
|
||||
const dictCode = row?.dictCode || dataIds.value[0];
|
||||
const res = await getData(dictCode);
|
||||
Object.assign(dataForm.value, res.data);
|
||||
dataDialog.visible = true;
|
||||
dataDialog.title = '修改字典数据';
|
||||
};
|
||||
|
||||
const submitDataForm = () => {
|
||||
dataFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
dataForm.value.dictCode ? await updateData(dataForm.value) : await addData(dataForm.value);
|
||||
useDictStore().removeDict(dataQueryParams.value.dictType);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dataDialog.visible = false;
|
||||
await getDataList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDataDelete = async (row?: DictDataVO) => {
|
||||
if (!currentDict.value) {
|
||||
proxy?.$modal.msgWarning('请先选择字典');
|
||||
return;
|
||||
}
|
||||
const dictCodes = row?.dictCode || dataIds.value;
|
||||
await proxy?.$modal.confirm('是否确认删除字典编码为"' + dictCodes + '"的数据项?');
|
||||
await delData(dictCodes);
|
||||
await getDataList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
useDictStore().removeDict(dataQueryParams.value.dictType);
|
||||
};
|
||||
|
||||
const handleDataExport = () => {
|
||||
if (!currentDict.value) {
|
||||
proxy?.$modal.msgWarning('请先选择字典');
|
||||
return;
|
||||
}
|
||||
proxy?.download(
|
||||
'system/dict/data/export',
|
||||
{
|
||||
...dataQueryParams.value
|
||||
},
|
||||
`dict_data_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getTypeList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dict-grid {
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.dict-card__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.dict-card__title {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dict-card__subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.dict-form-scroll {
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 6px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.dict-form-scroll :deep(.el-form) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
column-gap: 12px;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
.dict-form-scroll :deep(.el-form-item) {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dict-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin: 8px 0 12px;
|
||||
}
|
||||
|
||||
.dict-actions :deep(.el-button) {
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
}
|
||||
|
||||
.dict-actions :deep(.el-button + .el-button) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.dict-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,540 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="菜单名称" prop="menuName">
|
||||
<el-input v-model="queryParams.menuName" placeholder="请输入菜单名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="菜单状态" clearable>
|
||||
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:menu:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:menu:remove']" type="danger" plain icon="Delete" @click="handleCascadeDelete" :loading="deleteLoading">级联删除</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
ref="menuTableRef"
|
||||
v-loading="loading"
|
||||
:data="menuList"
|
||||
row-key="menuId"
|
||||
border
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
:default-expand-all="false"
|
||||
lazy
|
||||
:load="getChildrenList"
|
||||
:expand-change="expandMenuHandle"
|
||||
>
|
||||
<el-table-column prop="menuName" label="菜单名称" :show-overflow-tooltip="true" width="160"></el-table-column>
|
||||
<el-table-column prop="icon" label="图标" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<svg-icon :icon-class="scope.row.icon" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="orderNum" label="排序" width="60"></el-table-column>
|
||||
<el-table-column prop="perms" label="权限标识" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="component" label="组件路径" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="80">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:menu:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="新增" placement="top">
|
||||
<el-button v-hasPermi="['system:menu:add']" link type="primary" icon="Plus" @click="handleAdd(scope.row)" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:menu:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" destroy-on-close append-to-bod width="750px">
|
||||
<el-form ref="menuFormRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="上级菜单">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="menuOptions"
|
||||
:props="{ value: 'menuId', label: 'menuName', children: 'children' } as any"
|
||||
value-key="menuId"
|
||||
placeholder="选择上级菜单"
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="菜单类型" prop="menuType">
|
||||
<el-radio-group v-model="form.menuType">
|
||||
<el-radio value="M">目录</el-radio>
|
||||
<el-radio value="C">菜单</el-radio>
|
||||
<el-radio value="F">按钮</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.menuType !== 'F'" :span="24">
|
||||
<el-form-item label="菜单图标" prop="icon">
|
||||
<!-- 图标选择器 -->
|
||||
<icon-select v-model="form.icon" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="菜单名称" prop="menuName">
|
||||
<el-input v-model="form.menuName" placeholder="请输入菜单名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="显示排序" prop="orderNum">
|
||||
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.menuType !== 'F'" :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="选择是外链则路由地址需要以`http(s)://`开头" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon> </el-tooltip
|
||||
>是否外链
|
||||
</span>
|
||||
</template>
|
||||
<el-radio-group v-model="form.isFrame">
|
||||
<el-radio value="0">是</el-radio>
|
||||
<el-radio value="1">否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.menuType !== 'F'" :span="12">
|
||||
<el-form-item prop="path">
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="访问的路由地址,如:`user`,如外网地址需内链访问则以`http(s)://`开头" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
路由地址
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="form.path" placeholder="请输入路由地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.menuType === 'C'" :span="12">
|
||||
<el-form-item prop="component">
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="访问的组件路径,如:`system/user/index`,默认在`views`目录下" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
组件路径
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="form.component" placeholder="请输入组件路径" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.menuType !== 'M'" :span="12">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.perms" placeholder="请输入权限标识" maxlength="100" />
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="控制器中定义的权限字符,如:@SaCheckPermission('system:user:list')" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
权限字符
|
||||
</span>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.menuType === 'C'" :span="12">
|
||||
<el-form-item>
|
||||
<el-input v-model="form.queryParam" placeholder="请输入路由参数" maxlength="255" />
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content='访问路由的默认传递参数,如:`{"id": 1, "name": "ry"}`' placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
路由参数
|
||||
</span>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.menuType === 'C'" :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="选择是则会被`keep-alive`缓存,需要匹配组件的`name`和地址保持一致" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
是否缓存
|
||||
</span>
|
||||
</template>
|
||||
<el-radio-group v-model="form.isCache">
|
||||
<el-radio value="0">缓存</el-radio>
|
||||
<el-radio value="1">不缓存</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.menuType !== 'F'" :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="选择隐藏则路由将不会出现在侧边栏,但仍然可以访问" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
显示状态
|
||||
</span>
|
||||
</template>
|
||||
<el-radio-group v-model="form.visible">
|
||||
<el-radio v-for="dict in sys_show_hide" :key="dict.value" :value="dict.value">{{ dict.label }} </el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="选择停用则路由将不会出现在侧边栏,也不能被访问" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
菜单状态
|
||||
</span>
|
||||
</template>
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col v-if="form.visible !== '0'" :span="12">
|
||||
<el-form-item label="激活路径" prop="form.remark">
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="隐藏菜单填写默认激活路由,比如激活父菜单的路由 /system/user" placement="top">
|
||||
<el-icon>
|
||||
<question-filled />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
激活路由
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="form.remark" placeholder="请输入激活路径" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="deleteDialog.visible" :title="deleteDialog.title" destroy-on-close append-to-bod width="750px">
|
||||
<el-tree
|
||||
ref="menuTreeRef"
|
||||
class="tree-border"
|
||||
:data="menuOptions"
|
||||
show-checkbox
|
||||
node-key="menuId"
|
||||
:check-strictly="false"
|
||||
empty-text="加载中,请稍候"
|
||||
:default-expanded-keys="[0]"
|
||||
:props="{ value: 'menuId', label: 'menuName', children: 'children' } as any"
|
||||
/>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitDeleteForm" :loading="deleteLoading">确 定</el-button>
|
||||
<el-button @click="cancelCascade">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Menu" lang="ts">
|
||||
import { addMenu, cascadeDelMenu, delMenu, getMenu, listMenu, updateMenu } from '@/api/system/menu';
|
||||
import { MenuForm, MenuQuery, MenuVO } from '@/api/system/menu/types';
|
||||
import { MenuTypeEnum } from '@/enums/MenuTypeEnum';
|
||||
|
||||
interface MenuOptionsType {
|
||||
menuId: number;
|
||||
menuName: string;
|
||||
children: MenuOptionsType[] | undefined;
|
||||
}
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_show_hide', 'sys_normal_disable'));
|
||||
|
||||
const menuList = ref<MenuVO[]>([]);
|
||||
const menuChildrenListMap = ref({});
|
||||
const menuExpandMap = ref({});
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const menuOptions = ref<MenuOptionsType[]>([]);
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const menuFormRef = ref<ElFormInstance>();
|
||||
const initFormData = {
|
||||
path: '',
|
||||
menuId: undefined,
|
||||
parentId: 0,
|
||||
menuName: '',
|
||||
icon: '',
|
||||
menuType: MenuTypeEnum.M,
|
||||
orderNum: 1,
|
||||
isFrame: '1',
|
||||
isCache: '0',
|
||||
visible: '0',
|
||||
status: '0'
|
||||
};
|
||||
const data = reactive<PageData<MenuForm, MenuQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
menuName: undefined,
|
||||
status: undefined
|
||||
},
|
||||
rules: {
|
||||
menuName: [{ required: true, message: '菜单名称不能为空', trigger: 'blur' }],
|
||||
orderNum: [{ required: true, message: '菜单顺序不能为空', trigger: 'blur' }],
|
||||
path: [{ required: true, message: '路由地址不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const menuTableRef = ref<ElTableInstance>();
|
||||
|
||||
const { queryParams, form, rules } = toRefs<PageData<MenuForm, MenuQuery>>(data);
|
||||
|
||||
/** 获取子菜单列表 */
|
||||
const getChildrenList = async (row: any, treeNode: unknown, resolve: (data: any[]) => void) => {
|
||||
menuExpandMap.value[row.menuId] = { row, treeNode, resolve };
|
||||
const children = menuChildrenListMap.value[row.menuId] || [];
|
||||
// 菜单的子菜单清空后关闭展开
|
||||
if (children.length == 0) {
|
||||
// fix: 处理当菜单只有一个子菜单并被删除,需要将父菜单的展开状态关闭
|
||||
menuTableRef.value?.updateKeyChildren(row.menuId, children);
|
||||
}
|
||||
resolve(children);
|
||||
};
|
||||
|
||||
/** 收起菜单时从menuExpandMap中删除对应菜单id数据 */
|
||||
const expandMenuHandle = async (row: any, expanded: boolean) => {
|
||||
if (!expanded) {
|
||||
menuExpandMap.value[row.menuId] = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/** 刷新展开的菜单数据 */
|
||||
const refreshLoadTree = (parentId: string | number) => {
|
||||
if (menuExpandMap.value[parentId]) {
|
||||
const { row, treeNode, resolve } = menuExpandMap.value[parentId];
|
||||
if (row) {
|
||||
getChildrenList(row, treeNode, resolve);
|
||||
if (row.parentId) {
|
||||
const grandpaMenu = menuExpandMap.value[row.parentId];
|
||||
getChildrenList(grandpaMenu.row, grandpaMenu.treeNode, grandpaMenu.resolve);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/** 重新加载所有已展开的菜单的数据 */
|
||||
const refreshAllExpandMenuData = () => {
|
||||
for (const menuId in menuExpandMap.value) {
|
||||
refreshLoadTree(menuId);
|
||||
}
|
||||
};
|
||||
|
||||
/** 查询菜单列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listMenu(queryParams.value);
|
||||
|
||||
const tempMap = {};
|
||||
// 存储 父菜单:子菜单列表
|
||||
for (const menu of res.data) {
|
||||
const parentId = menu.parentId;
|
||||
if (!tempMap[parentId]) {
|
||||
tempMap[parentId] = [];
|
||||
}
|
||||
tempMap[parentId].push(menu);
|
||||
}
|
||||
// 创建一个当前所有 menuId 的 Set,用于查找父菜单是否存在于当前数据中
|
||||
const menuIdSet = new Set();
|
||||
// 设置有没有子菜单
|
||||
for (const menu of res.data) {
|
||||
menu['hasChildren'] = tempMap[menu.menuId]?.length > 0;
|
||||
menuIdSet.add(menu.menuId);
|
||||
}
|
||||
menuChildrenListMap.value = tempMap;
|
||||
// 找出所有父ID不在当前菜单ID集合中的菜单项,作为新的顶层菜单
|
||||
menuList.value = res.data.filter((menu) => !menuIdSet.has(menu.parentId));
|
||||
// 根据新数据重新加载子菜单数据
|
||||
refreshAllExpandMenuData();
|
||||
loading.value = false;
|
||||
};
|
||||
/** 查询菜单下拉树结构 */
|
||||
const getTreeselect = async () => {
|
||||
menuOptions.value = [];
|
||||
const response = await listMenu();
|
||||
const menu: MenuOptionsType = { menuId: 0, menuName: '主类目', children: [] };
|
||||
menu.children = proxy?.handleTree<MenuOptionsType>(response.data, 'menuId');
|
||||
menuOptions.value.push(menu);
|
||||
};
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
menuFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = (row?: MenuVO) => {
|
||||
reset();
|
||||
getTreeselect();
|
||||
row && row.menuId ? (form.value.parentId = row.menuId) : (form.value.parentId = 0);
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加菜单';
|
||||
};
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row: MenuVO) => {
|
||||
reset();
|
||||
await getTreeselect();
|
||||
if (row.menuId) {
|
||||
const { data } = await getMenu(row.menuId);
|
||||
form.value = data;
|
||||
}
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改菜单';
|
||||
};
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
menuFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
form.value.menuId ? await updateMenu(form.value) : await addMenu(form.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row: MenuVO) => {
|
||||
await proxy?.$modal.confirm('是否确认删除名称为"' + row.menuName + '"的数据项?');
|
||||
await delMenu(row.menuId);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
const deleteLoading = ref<boolean>(false);
|
||||
const menuTreeRef = ref<ElTreeInstance>();
|
||||
|
||||
const deleteDialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: '级联删除菜单'
|
||||
});
|
||||
|
||||
/** 级联删除按钮操作 */
|
||||
const handleCascadeDelete = () => {
|
||||
menuTreeRef.value?.setCheckedKeys([]);
|
||||
getTreeselect();
|
||||
deleteDialog.visible = true;
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancelCascade = () => {
|
||||
menuTreeRef.value?.setCheckedKeys([]);
|
||||
deleteDialog.visible = false;
|
||||
};
|
||||
|
||||
/** 删除提交按钮 */
|
||||
const submitDeleteForm = async () => {
|
||||
const menuIds = menuTreeRef.value?.getCheckedKeys();
|
||||
if (menuIds.length < 0) {
|
||||
proxy?.$modal.msgWarning('请选择要删除的菜单');
|
||||
return;
|
||||
}
|
||||
|
||||
deleteLoading.value = true;
|
||||
await cascadeDelMenu(menuIds).finally(() => (deleteLoading.value = false));
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
deleteDialog.visible = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.tree-border {
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,915 @@
|
||||
<template>
|
||||
<div class="model-config-page">
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>模型配置</h1>
|
||||
<p>统一维护供应商 Key、Base URL 与可用模型,供 RAG、面试出题、对练调用。</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<el-button type="danger" :icon="Plus" @click="openProviderDialog()">新增供应商</el-button>
|
||||
<el-button type="danger" :icon="Plus" @click="openModelDialog()">新增模型</el-button>
|
||||
<el-button :icon="Connection" :loading="testing" @click="runConnectionTest">测试连接</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="model-layout">
|
||||
<aside class="panel provider-panel">
|
||||
<div class="panel-title">
|
||||
<h2>供应商</h2>
|
||||
</div>
|
||||
<el-input v-model="keyword" class="provider-search" placeholder="搜索供应商名称或编码" :prefix-icon="Search" clearable />
|
||||
|
||||
<div v-loading="loading" class="provider-list">
|
||||
<button
|
||||
v-for="provider in filteredProviders"
|
||||
:key="provider.providerCode"
|
||||
class="provider-item"
|
||||
:class="{ active: provider.providerCode === selectedProviderCode }"
|
||||
@click="selectedProviderCode = provider.providerCode"
|
||||
>
|
||||
<span class="provider-logo" :class="`logo-${provider.providerCode}`">{{ provider.logo }}</span>
|
||||
<span class="provider-text">
|
||||
<strong>{{ provider.providerName }}</strong>
|
||||
<small>{{ provider.providerCode }}</small>
|
||||
</span>
|
||||
<span class="status-tag" :class="providerTag(provider).className">{{ providerTag(provider).label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer>共 {{ providers.length }} 个供应商</footer>
|
||||
</aside>
|
||||
|
||||
<main class="detail-stack">
|
||||
<section class="panel detail-panel">
|
||||
<div class="panel-title with-actions">
|
||||
<h2>供应商详情</h2>
|
||||
<div>
|
||||
<el-button :icon="EditPen" @click="openProviderDialog(activeProvider)">编辑</el-button>
|
||||
<el-button type="danger" plain :icon="Connection" :loading="testing" @click="runConnectionTest">测试</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid">
|
||||
<div class="field-list">
|
||||
<div class="field-row">
|
||||
<span>供应商名称</span>
|
||||
<strong>{{ activeProvider.providerName }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>供应商编码</span>
|
||||
<strong>{{ activeProvider.providerCode }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>Base URL</span>
|
||||
<strong>{{ activeProvider.apiHost }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>API Key</span>
|
||||
<strong>
|
||||
{{ keyText }}
|
||||
<el-button link :icon="showKey ? View : Hide" @click="showKey = !showKey" />
|
||||
</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field-list side">
|
||||
<div class="field-row">
|
||||
<span>状态</span>
|
||||
<el-switch
|
||||
:model-value="activeProvider.status === '0'"
|
||||
active-color="#e60012"
|
||||
inactive-color="#d7dee8"
|
||||
@change="(value) => toggleProviderStatus(Boolean(value))"
|
||||
/>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>最近测试时间</span>
|
||||
<strong>{{ lastTestAt }}</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>请求方式</span>
|
||||
<strong>OpenAI Compatible</strong>
|
||||
</div>
|
||||
<div class="field-row">
|
||||
<span>备注</span>
|
||||
<strong>{{ activeProvider.providerDesc || `${activeProvider.providerName} OpenAI 兼容接口` }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel model-panel">
|
||||
<div class="panel-title">
|
||||
<h2>模型列表</h2>
|
||||
</div>
|
||||
<el-table :data="modelRows" class="model-table" size="large">
|
||||
<el-table-column prop="category" label="类型" width="130" />
|
||||
<el-table-column prop="modelName" label="模型名" min-width="190" />
|
||||
<el-table-column prop="modelDescribe" label="用途" min-width="280" />
|
||||
<el-table-column label="维度" width="110">
|
||||
<template #default="{ row }">{{ row.modelDimension || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.enabled"
|
||||
active-color="#e60012"
|
||||
inactive-color="#d7dee8"
|
||||
@change="(value) => toggleModelEnabled(row, Boolean(value))"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="danger" @click="openModelDialog(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
|
||||
<div class="tip-bar">
|
||||
<el-icon><InfoFilled /></el-icon>
|
||||
<span>调用优先级:模型配置覆盖供应商默认值;未配置时回退 seed,接口不返回 API Key。</span>
|
||||
</div>
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="providerDialogVisible" :title="providerForm.id > 0 ? '编辑供应商' : '新增供应商'" width="560px">
|
||||
<el-form label-width="104px">
|
||||
<el-form-item label="供应商名称" required>
|
||||
<el-input v-model="providerForm.providerName" placeholder="DeepSeek" />
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商编码" required>
|
||||
<el-input v-model="providerForm.providerCode" placeholder="deepseek" :disabled="providerForm.id > 0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="Base URL">
|
||||
<el-input v-model="providerForm.apiHost" placeholder="https://api.deepseek.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="API Key">
|
||||
<el-input v-model="providerForm.apiKey" type="password" show-password placeholder="留空则保留原密钥" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input v-model="providerForm.providerDesc" type="textarea" :rows="2" placeholder="OpenAI 兼容接口" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-switch v-model="providerEnabled" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="providerDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="saving" @click="submitProvider">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="modelDialogVisible" :title="modelForm.id > 0 ? '编辑模型' : '新增模型'" width="560px">
|
||||
<el-form label-width="104px">
|
||||
<el-form-item label="模型类型" required>
|
||||
<el-select v-model="modelForm.category">
|
||||
<el-option label="对话 chat" value="chat" />
|
||||
<el-option label="向量 vector" value="vector" />
|
||||
<el-option label="重排 rerank" value="rerank" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名称" required>
|
||||
<el-input v-model="modelForm.modelName" placeholder="deepseek-chat" />
|
||||
</el-form-item>
|
||||
<el-form-item label="供应商" required>
|
||||
<el-select v-model="modelForm.providerCode">
|
||||
<el-option v-for="provider in providers" :key="provider.providerCode" :label="provider.providerName" :value="provider.providerCode" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="用途">
|
||||
<el-input v-model="modelForm.modelDescribe" placeholder="用于 Demo 对话与 RAG 生成" />
|
||||
</el-form-item>
|
||||
<el-form-item label="维度">
|
||||
<el-input-number v-model="modelForm.modelDimension" :min="0" :controls="false" placeholder="向量模型填写" />
|
||||
</el-form-item>
|
||||
<el-form-item label="覆盖 URL">
|
||||
<el-input v-model="modelForm.apiHost" placeholder="留空则使用供应商 Base URL" />
|
||||
</el-form-item>
|
||||
<el-form-item label="覆盖 Key">
|
||||
<el-input v-model="modelForm.apiKey" type="password" show-password placeholder="留空则使用供应商 Key 或保留原值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="modelForm.enabled" active-text="启用" inactive-text="停用" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="modelDialogVisible = false">取消</el-button>
|
||||
<el-button type="danger" :loading="saving" @click="submitModel">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="SystemModelConfig" lang="ts">
|
||||
import { Connection, EditPen, Hide, InfoFilled, Plus, Search, View } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
createModelConfig,
|
||||
createModelProvider,
|
||||
getModelConfigs,
|
||||
getModelProviders,
|
||||
testModelChat,
|
||||
updateModelConfig,
|
||||
updateModelConfigEnabled,
|
||||
updateModelProvider,
|
||||
updateModelProviderStatus,
|
||||
type ModelConfig,
|
||||
type ModelConfigPayload,
|
||||
type ModelProvider,
|
||||
type ModelProviderPayload
|
||||
} from '@/api/aihr/model';
|
||||
|
||||
type ProviderView = ModelProvider & { logo: string };
|
||||
type ProviderFormState = ModelProviderPayload & {
|
||||
id: number;
|
||||
originalProviderCode: string;
|
||||
apiHost: string;
|
||||
apiKey: string;
|
||||
providerDesc: string;
|
||||
sortOrder: number;
|
||||
};
|
||||
type ModelFormState = ModelConfigPayload & {
|
||||
id: number;
|
||||
apiHost: string;
|
||||
apiKey: string;
|
||||
modelDescribe: string;
|
||||
};
|
||||
|
||||
const seedProviders: ProviderView[] = [
|
||||
{
|
||||
id: -2,
|
||||
providerName: 'DeepSeek',
|
||||
providerCode: 'deepseek',
|
||||
providerDesc: 'DeepSeek 官方 API',
|
||||
apiHost: 'https://api.deepseek.com',
|
||||
status: '0',
|
||||
sortOrder: 0,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: 'DS'
|
||||
},
|
||||
{
|
||||
id: -3,
|
||||
providerName: '智谱AI',
|
||||
providerCode: 'zhipu',
|
||||
providerDesc: '智谱 AI 大模型服务',
|
||||
apiHost: 'https://open.bigmodel.cn',
|
||||
status: '0',
|
||||
sortOrder: 1,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: '智'
|
||||
},
|
||||
{
|
||||
id: -4,
|
||||
providerName: '阿里云百炼',
|
||||
providerCode: 'qianwen',
|
||||
providerDesc: '阿里云百炼大模型服务',
|
||||
apiHost: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||
status: '0',
|
||||
sortOrder: 2,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: '百'
|
||||
},
|
||||
{
|
||||
id: -1,
|
||||
providerName: 'OpenAI兼容接口',
|
||||
providerCode: 'custom_api',
|
||||
providerDesc: '自定义 OpenAI 兼容接口',
|
||||
apiHost: '自定义',
|
||||
status: '1',
|
||||
sortOrder: 3,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: 'AI'
|
||||
},
|
||||
{
|
||||
id: -5,
|
||||
providerName: 'Ollama(本地)',
|
||||
providerCode: 'ollama',
|
||||
providerDesc: '本地 Ollama 服务',
|
||||
apiHost: 'http://127.0.0.1:11434/v1',
|
||||
status: '0',
|
||||
sortOrder: 4,
|
||||
source: 'seed',
|
||||
configured: false,
|
||||
logo: 'OL'
|
||||
}
|
||||
];
|
||||
|
||||
const seedModels: ModelConfig[] = [
|
||||
{
|
||||
id: -1,
|
||||
category: 'chat',
|
||||
modelName: 'deepseek-chat',
|
||||
providerCode: 'deepseek',
|
||||
modelDescribe: '用于 Demo 对话与 RAG 生成',
|
||||
modelShow: 'Y',
|
||||
apiHost: '',
|
||||
enabled: true,
|
||||
apiKeyConfigured: false,
|
||||
configured: true
|
||||
},
|
||||
{
|
||||
id: -2,
|
||||
category: 'chat',
|
||||
modelName: 'deepseek-reasoner',
|
||||
providerCode: 'deepseek',
|
||||
modelDescribe: '用于复杂评分解释',
|
||||
modelShow: 'Y',
|
||||
apiHost: '',
|
||||
enabled: true,
|
||||
apiKeyConfigured: false,
|
||||
configured: true
|
||||
},
|
||||
{
|
||||
id: -3,
|
||||
category: 'vector',
|
||||
modelName: 'embedding-3',
|
||||
providerCode: 'zhipu',
|
||||
modelDescribe: '用于知识片段量化',
|
||||
modelDimension: 2048,
|
||||
modelShow: 'N',
|
||||
apiHost: '',
|
||||
enabled: false,
|
||||
apiKeyConfigured: false,
|
||||
configured: false
|
||||
},
|
||||
{
|
||||
id: -4,
|
||||
category: 'rerank',
|
||||
modelName: 'rerank',
|
||||
providerCode: 'zhipu',
|
||||
modelDescribe: '用于召回片段重排',
|
||||
modelShow: 'N',
|
||||
apiHost: '',
|
||||
enabled: false,
|
||||
apiKeyConfigured: false,
|
||||
configured: false
|
||||
}
|
||||
];
|
||||
|
||||
const providers = ref<ProviderView[]>(seedProviders);
|
||||
const modelRows = ref<ModelConfig[]>(seedModels);
|
||||
const selectedProviderCode = ref('deepseek');
|
||||
const keyword = ref('');
|
||||
const loading = ref(false);
|
||||
const testing = ref(false);
|
||||
const saving = ref(false);
|
||||
const showKey = ref(false);
|
||||
const providerDialogVisible = ref(false);
|
||||
const modelDialogVisible = ref(false);
|
||||
const lastTestAt = ref('未测试');
|
||||
|
||||
const providerForm = reactive<ProviderFormState>({
|
||||
id: 0,
|
||||
originalProviderCode: '',
|
||||
providerName: '',
|
||||
providerCode: '',
|
||||
providerDesc: '',
|
||||
apiHost: '',
|
||||
apiKey: '',
|
||||
status: '0',
|
||||
sortOrder: 0
|
||||
});
|
||||
|
||||
const modelForm = reactive<ModelFormState>({
|
||||
id: 0,
|
||||
category: 'chat',
|
||||
modelName: '',
|
||||
providerCode: '',
|
||||
modelDescribe: '',
|
||||
modelDimension: undefined,
|
||||
modelShow: 'Y',
|
||||
apiHost: '',
|
||||
apiKey: '',
|
||||
enabled: true
|
||||
});
|
||||
|
||||
const filteredProviders = computed(() => {
|
||||
const word = keyword.value.trim().toLowerCase();
|
||||
if (!word) return providers.value;
|
||||
return providers.value.filter((item) => `${item.providerName} ${item.providerCode}`.toLowerCase().includes(word));
|
||||
});
|
||||
|
||||
const activeProvider = computed<ProviderView>(
|
||||
() => providers.value.find((item) => item.providerCode === selectedProviderCode.value) ?? providers.value[0] ?? seedProviders[0]
|
||||
);
|
||||
const activeChatModel = computed(() => modelRows.value.find((item) => item.category === 'chat' && item.enabled) ?? modelRows.value[0]);
|
||||
const keyText = computed(() => {
|
||||
if (!activeProvider.value.configured) return '未配置';
|
||||
return showKey.value ? '已配置,接口不回传密钥' : 'sk-************';
|
||||
});
|
||||
const providerEnabled = computed({
|
||||
get: () => providerForm.status === '0',
|
||||
set: (value: boolean) => {
|
||||
providerForm.status = value ? '0' : '1';
|
||||
}
|
||||
});
|
||||
|
||||
const providerTag = (provider: ProviderView) => {
|
||||
if (provider.providerCode.includes('ollama')) return { label: '本地', className: 'neutral' };
|
||||
if (provider.status !== '0') return { label: '未启用', className: 'warning' };
|
||||
if (!provider.configured) return { label: '未配置', className: 'warning' };
|
||||
return { label: '已启用', className: 'success' };
|
||||
};
|
||||
|
||||
const normalizeProviders = (items: ModelProvider[]): ProviderView[] => {
|
||||
if (!items.length) return seedProviders;
|
||||
const seedMap = new Map(seedProviders.map((item) => [item.providerCode, item]));
|
||||
return items.map((item) => {
|
||||
const seed = seedMap.get(item.providerCode);
|
||||
return {
|
||||
...item,
|
||||
logo: seed?.logo ?? item.providerName.slice(0, 2).toUpperCase()
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const loadData = async (preferredProviderCode = selectedProviderCode.value) => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [providerResult, configResult] = await Promise.all([getModelProviders(), getModelConfigs()]);
|
||||
providers.value = normalizeProviders(providerResult.data ?? []);
|
||||
modelRows.value = configResult.data?.length ? configResult.data : seedModels;
|
||||
selectedProviderCode.value =
|
||||
providers.value.find((item) => item.providerCode === preferredProviderCode)?.providerCode ?? providers.value[0]?.providerCode ?? 'deepseek';
|
||||
} catch {
|
||||
providers.value = seedProviders;
|
||||
modelRows.value = seedModels;
|
||||
selectedProviderCode.value = preferredProviderCode || 'deepseek';
|
||||
ElMessage.warning('模型配置 API 不可用,已展示本地 seed 原型数据');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const runConnectionTest = async () => {
|
||||
testing.value = true;
|
||||
try {
|
||||
const result = await testModelChat({
|
||||
model: activeChatModel.value?.modelName,
|
||||
prompt: '请用一句话说明模型配置已连接。'
|
||||
});
|
||||
lastTestAt.value = new Date().toLocaleString('zh-CN', { hour12: false });
|
||||
if (result.data?.configured) {
|
||||
ElMessage.success(`连接成功:${result.data.modelName}`);
|
||||
} else {
|
||||
ElMessage.warning('后端返回 seed fallback,请先在数据库配置供应商 URL 和 Key');
|
||||
}
|
||||
} catch {
|
||||
ElMessage.warning('模型测试接口不可用,当前仅展示页面原型数据');
|
||||
} finally {
|
||||
testing.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openProviderDialog = (provider?: ProviderView) => {
|
||||
const source = provider ?? undefined;
|
||||
Object.assign(providerForm, {
|
||||
id: source && source.id > 0 ? source.id : 0,
|
||||
originalProviderCode: source && source.id > 0 ? source.providerCode : '',
|
||||
providerName: source?.providerName ?? '',
|
||||
providerCode: source?.providerCode ?? '',
|
||||
providerDesc: source?.providerDesc ?? '',
|
||||
apiHost: source?.apiHost === '自定义' ? '' : (source?.apiHost ?? ''),
|
||||
apiKey: '',
|
||||
status: source?.status ?? '0',
|
||||
sortOrder: source?.sortOrder ?? providers.value.length
|
||||
});
|
||||
providerDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const providerPayload = (): ModelProviderPayload | undefined => {
|
||||
const providerName = providerForm.providerName.trim();
|
||||
const providerCode = providerForm.providerCode.trim();
|
||||
if (!providerName || !providerCode) {
|
||||
ElMessage.warning('请填写供应商名称和编码');
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
providerName,
|
||||
providerCode,
|
||||
providerDesc: providerForm.providerDesc.trim(),
|
||||
apiHost: providerForm.apiHost.trim(),
|
||||
apiKey: providerForm.apiKey.trim(),
|
||||
status: providerForm.status,
|
||||
sortOrder: providerForm.sortOrder
|
||||
};
|
||||
};
|
||||
|
||||
const submitProvider = async () => {
|
||||
const payload = providerPayload();
|
||||
if (!payload) return;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (providerForm.id > 0) {
|
||||
await updateModelProvider(providerForm.originalProviderCode || providerForm.providerCode, payload);
|
||||
} else {
|
||||
await createModelProvider(payload);
|
||||
}
|
||||
providerDialogVisible.value = false;
|
||||
await loadData(payload.providerCode);
|
||||
ElMessage.success('供应商配置已保存');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleProviderStatus = async (enabled: boolean) => {
|
||||
const provider = activeProvider.value;
|
||||
if (provider.id < 0) {
|
||||
ElMessage.warning('seed 供应商请先保存后再启停');
|
||||
openProviderDialog(provider);
|
||||
return;
|
||||
}
|
||||
|
||||
const status = enabled ? '0' : '1';
|
||||
try {
|
||||
await updateModelProviderStatus(provider.providerCode, status);
|
||||
await loadData(provider.providerCode);
|
||||
ElMessage.success(enabled ? '供应商已启用' : '供应商已停用');
|
||||
} catch {
|
||||
await loadData(provider.providerCode);
|
||||
}
|
||||
};
|
||||
|
||||
const openModelDialog = (model?: ModelConfig) => {
|
||||
Object.assign(modelForm, {
|
||||
id: model && model.id > 0 ? model.id : 0,
|
||||
category: model?.category ?? 'chat',
|
||||
modelName: model?.modelName ?? '',
|
||||
providerCode: model?.providerCode ?? selectedProviderCode.value,
|
||||
modelDescribe: model?.modelDescribe ?? '',
|
||||
modelDimension: model?.modelDimension,
|
||||
modelShow: model?.modelShow ?? 'Y',
|
||||
apiHost: model?.apiHost ?? '',
|
||||
apiKey: '',
|
||||
enabled: model?.enabled ?? true
|
||||
});
|
||||
modelDialogVisible.value = true;
|
||||
};
|
||||
|
||||
const modelPayload = (): ModelConfigPayload | undefined => {
|
||||
const modelName = modelForm.modelName.trim();
|
||||
const providerCode = modelForm.providerCode.trim();
|
||||
if (!modelName || !providerCode) {
|
||||
ElMessage.warning('请填写模型名称和供应商');
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
category: modelForm.category,
|
||||
modelName,
|
||||
providerCode,
|
||||
modelDescribe: modelForm.modelDescribe.trim(),
|
||||
modelDimension: modelForm.modelDimension && modelForm.modelDimension > 0 ? modelForm.modelDimension : undefined,
|
||||
modelShow: modelForm.modelShow,
|
||||
apiHost: modelForm.apiHost.trim(),
|
||||
apiKey: modelForm.apiKey.trim(),
|
||||
enabled: modelForm.enabled
|
||||
};
|
||||
};
|
||||
|
||||
const submitModel = async () => {
|
||||
const payload = modelPayload();
|
||||
if (!payload) return;
|
||||
|
||||
saving.value = true;
|
||||
try {
|
||||
if (modelForm.id > 0) {
|
||||
await updateModelConfig(modelForm.id, payload);
|
||||
} else {
|
||||
await createModelConfig(payload);
|
||||
}
|
||||
modelDialogVisible.value = false;
|
||||
await loadData(payload.providerCode);
|
||||
ElMessage.success('模型配置已保存');
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleModelEnabled = async (row: ModelConfig, enabled: boolean) => {
|
||||
if (row.id < 0) {
|
||||
row.enabled = !enabled;
|
||||
ElMessage.warning('seed 模型请先保存后再启停');
|
||||
openModelDialog(row);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateModelConfigEnabled(row.id, enabled);
|
||||
row.enabled = enabled;
|
||||
ElMessage.success(enabled ? '模型已启用' : '模型已停用');
|
||||
} catch {
|
||||
row.enabled = !enabled;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadData);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.model-config-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 28px 32px 34px;
|
||||
background: #f4f7fb;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 12px;
|
||||
font-size: 30px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #5f6f86;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.head-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-top: 2px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.model-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 330px) minmax(0, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 24px rgb(15 23 42 / 5%);
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22px 24px 14px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
font-weight: 800;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-panel {
|
||||
overflow: hidden;
|
||||
|
||||
footer {
|
||||
padding: 18px 24px 22px;
|
||||
color: #64748b;
|
||||
border-top: 1px solid #edf1f6;
|
||||
}
|
||||
}
|
||||
|
||||
.provider-search {
|
||||
width: calc(100% - 40px);
|
||||
margin: 4px 20px 18px;
|
||||
}
|
||||
|
||||
.provider-list {
|
||||
min-height: 420px;
|
||||
}
|
||||
|
||||
.provider-item {
|
||||
display: grid;
|
||||
grid-template-columns: 44px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
min-height: 82px;
|
||||
padding: 14px 22px;
|
||||
border: 0;
|
||||
border-top: 1px solid #edf1f6;
|
||||
border-left: 4px solid transparent;
|
||||
background: #fff;
|
||||
color: #172033;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
|
||||
&.active {
|
||||
border-left-color: #e60012;
|
||||
background: linear-gradient(90deg, #fff1f2 0%, #fff 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.provider-logo {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
background: #eef4ff;
|
||||
color: #2264d1;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.logo-zhipu {
|
||||
background: #f3e8ff;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
.logo-qianwen {
|
||||
background: #e0f2fe;
|
||||
color: #0891b2;
|
||||
}
|
||||
|
||||
.logo-custom_api {
|
||||
background: #f1f5f9;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.logo-ollama {
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
.provider-text {
|
||||
min-width: 0;
|
||||
|
||||
strong,
|
||||
small {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 16px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
small {
|
||||
margin-top: 5px;
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 52px;
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
|
||||
&.success {
|
||||
color: #17834d;
|
||||
background: #dcfce7;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
color: #c2410c;
|
||||
background: #ffedd5;
|
||||
}
|
||||
|
||||
&.neutral {
|
||||
color: #475569;
|
||||
background: #eef2f7;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.detail-panel,
|
||||
.model-panel {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.with-actions {
|
||||
padding-bottom: 18px;
|
||||
border-bottom: 1px solid #edf1f6;
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(280px, 0.72fr);
|
||||
gap: 34px;
|
||||
padding: 22px 24px 6px;
|
||||
}
|
||||
|
||||
.field-list {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
|
||||
&.side {
|
||||
padding-left: 32px;
|
||||
border-left: 1px solid #edf1f6;
|
||||
}
|
||||
}
|
||||
|
||||
.field-row {
|
||||
display: grid;
|
||||
grid-template-columns: 116px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
span {
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
color: #25324a;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.model-table {
|
||||
width: calc(100% - 40px);
|
||||
margin: 4px 20px 0;
|
||||
border: 1px solid #edf1f6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tip-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 54px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 8px;
|
||||
background: #fff7f7;
|
||||
color: #8a2f33;
|
||||
}
|
||||
|
||||
:deep(.el-button--danger) {
|
||||
--el-button-bg-color: #e60012;
|
||||
--el-button-border-color: #e60012;
|
||||
--el-button-hover-bg-color: #f0202f;
|
||||
--el-button-hover-border-color: #f0202f;
|
||||
}
|
||||
|
||||
:deep(.el-table th.el-table__cell) {
|
||||
background: #f8fafc;
|
||||
color: #334155;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.page-head,
|
||||
.head-actions {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-layout,
|
||||
.detail-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.field-list.side {
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="公告标题" prop="noticeTitle">
|
||||
<el-input v-model="queryParams.noticeTitle" placeholder="请输入公告标题" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人员" prop="createByName">
|
||||
<el-input v-model="queryParams.createByName" placeholder="请输入操作人员" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="noticeType">
|
||||
<el-select v-model="queryParams.noticeType" placeholder="公告类型" clearable>
|
||||
<el-option v-for="dict in sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:notice:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:notice:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
|
||||
>修改</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:notice:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="noticeList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="序号" align="center" prop="noticeId" width="100" />
|
||||
<el-table-column label="公告标题" align="center" prop="noticeTitle" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="公告类型" align="center" prop="noticeType" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_notice_type" :value="scope.row.noticeType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_notice_status" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建者" align="center" prop="createByName" width="100" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="100">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:notice:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:notice:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改公告对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="780px" append-to-body>
|
||||
<el-form ref="noticeFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="公告标题" prop="noticeTitle">
|
||||
<el-input v-model="form.noticeTitle" placeholder="请输入公告标题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="公告类型" prop="noticeType">
|
||||
<el-select v-model="form.noticeType" placeholder="请选择">
|
||||
<el-option v-for="dict in sys_notice_type" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_notice_status" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="内容">
|
||||
<editor v-model="form.noticeContent" :min-height="192" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Notice" lang="ts">
|
||||
import { listNotice, getNotice, delNotice, addNotice, updateNotice } from '@/api/system/notice';
|
||||
import { NoticeForm, NoticeQuery, NoticeVO } from '@/api/system/notice/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_notice_status, sys_notice_type } = toRefs<any>(proxy?.useDict('sys_notice_status', 'sys_notice_type'));
|
||||
|
||||
const noticeList = ref<NoticeVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const noticeFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: NoticeForm = {
|
||||
noticeId: undefined,
|
||||
noticeTitle: '',
|
||||
noticeType: '',
|
||||
noticeContent: '',
|
||||
status: '0',
|
||||
remark: '',
|
||||
createByName: ''
|
||||
};
|
||||
const data = reactive<PageData<NoticeForm, NoticeQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
noticeTitle: '',
|
||||
createByName: '',
|
||||
status: '',
|
||||
noticeType: ''
|
||||
},
|
||||
rules: {
|
||||
noticeTitle: [{ required: true, message: '公告标题不能为空', trigger: 'blur' }],
|
||||
noticeType: [{ required: true, message: '公告类型不能为空', trigger: 'change' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询公告列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listNotice(queryParams.value);
|
||||
noticeList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
noticeFormRef.value?.resetFields();
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: NoticeVO[]) => {
|
||||
ids.value = selection.map((item) => item.noticeId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加公告';
|
||||
};
|
||||
/**修改按钮操作 */
|
||||
const handleUpdate = async (row?: NoticeVO) => {
|
||||
reset();
|
||||
const noticeId = row?.noticeId || ids.value[0];
|
||||
const { data } = await getNotice(noticeId);
|
||||
Object.assign(form.value, data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改公告';
|
||||
};
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
noticeFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
form.value.noticeId ? await updateNotice(form.value) : await addNotice(form.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: NoticeVO) => {
|
||||
const noticeIds = row?.noticeId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除公告编号为"' + noticeIds + '"的数据项?');
|
||||
await delNotice(noticeIds);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="配置key" prop="configKey">
|
||||
<el-input v-model="queryParams.configKey" placeholder="配置key" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="桶名称" prop="bucketName">
|
||||
<el-input v-model="queryParams.bucketName" placeholder="请输入桶名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否默认" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择状态" clearable>
|
||||
<el-option key="0" label="是" value="0" />
|
||||
<el-option key="1" label="否" value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:ossConfig:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:ossConfig:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
|
||||
>修改</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:ossConfig:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="ossConfigList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="columns[0].visible" label="主建" align="center" prop="ossConfigId" />
|
||||
<el-table-column v-if="columns[1].visible" label="配置key" align="center" prop="configKey" />
|
||||
<el-table-column v-if="columns[2].visible" label="访问站点" align="center" prop="endpoint" width="200" />
|
||||
<el-table-column v-if="columns[3].visible" label="自定义域名" align="center" prop="domain" width="200" />
|
||||
<el-table-column v-if="columns[4].visible" label="桶名称" align="center" prop="bucketName" />
|
||||
<el-table-column v-if="columns[5].visible" label="前缀" align="center" prop="prefix" />
|
||||
<el-table-column v-if="columns[6].visible" label="域" align="center" prop="region" />
|
||||
<el-table-column v-if="columns[7].visible" label="桶权限类型" align="center" prop="accessPolicy">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.accessPolicy === '0'" type="warning">private</el-tag>
|
||||
<el-tag v-if="scope.row.accessPolicy === '1'" type="success">public</el-tag>
|
||||
<el-tag v-if="scope.row.accessPolicy === '2'" type="info">custom</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="columns[8].visible" label="是否默认" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" align="center" width="150" class-name="small-padding">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:ossConfig:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:ossConfig:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改对象存储配置对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="800px" append-to-body>
|
||||
<el-form ref="ossConfigFormRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="配置key" prop="configKey">
|
||||
<el-input v-model="form.configKey" placeholder="请输入配置key" />
|
||||
</el-form-item>
|
||||
<el-form-item label="访问站点" prop="endpoint">
|
||||
<el-input v-model="form.endpoint" placeholder="请输入访问站点">
|
||||
<template #prefix>
|
||||
<span style="color: #999">{{ protocol }}</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="自定义域名" prop="domain">
|
||||
<el-input v-model="form.domain" placeholder="请输入自定义域名">
|
||||
<template #prefix>
|
||||
<span style="color: #999">{{ protocol }}</span>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="accessKey" prop="accessKey">
|
||||
<el-input v-model="form.accessKey" placeholder="请输入accessKey" />
|
||||
</el-form-item>
|
||||
<el-form-item label="secretKey" prop="secretKey">
|
||||
<el-input v-model="form.secretKey" placeholder="请输入秘钥" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="桶名称" prop="bucketName">
|
||||
<el-input v-model="form.bucketName" placeholder="请输入桶名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="前缀" prop="prefix">
|
||||
<el-input v-model="form.prefix" placeholder="请输入前缀" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否HTTPS">
|
||||
<el-radio-group v-model="form.isHttps">
|
||||
<el-radio v-for="dict in sys_yes_no" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="桶权限类型">
|
||||
<el-radio-group v-model="form.accessPolicy">
|
||||
<el-radio value="0">private</el-radio>
|
||||
<el-radio value="1">public</el-radio>
|
||||
<el-radio value="2">custom</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="域" prop="region">
|
||||
<el-input v-model="form.region" placeholder="请输入域" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="OssConfig" lang="ts">
|
||||
import { listOssConfig, getOssConfig, delOssConfig, addOssConfig, updateOssConfig, changeOssConfigStatus } from '@/api/system/ossConfig';
|
||||
import { OssConfigForm, OssConfigQuery, OssConfigVO } from '@/api/system/ossConfig/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_yes_no } = toRefs<any>(proxy?.useDict('sys_yes_no'));
|
||||
|
||||
const ossConfigList = ref<OssConfigVO[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<number | string>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const ossConfigFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
// 列显隐信息
|
||||
const columns = ref<FieldOption[]>([
|
||||
{ key: 0, label: `主建`, visible: false },
|
||||
{ key: 1, label: `配置key`, visible: true },
|
||||
{ key: 2, label: `访问站点`, visible: true },
|
||||
{ key: 3, label: `自定义域名`, visible: true },
|
||||
{ key: 4, label: `桶名称`, visible: true },
|
||||
{ key: 5, label: `前缀`, visible: true },
|
||||
{ key: 6, label: `域`, visible: true },
|
||||
{ key: 7, label: `桶权限类型`, visible: true },
|
||||
{ key: 8, label: `状态`, visible: true }
|
||||
]);
|
||||
|
||||
const initFormData: OssConfigForm = {
|
||||
ossConfigId: undefined,
|
||||
configKey: '',
|
||||
accessKey: '',
|
||||
secretKey: '',
|
||||
bucketName: '',
|
||||
prefix: '',
|
||||
endpoint: '',
|
||||
domain: '',
|
||||
isHttps: 'N',
|
||||
accessPolicy: '1',
|
||||
region: '',
|
||||
status: '1',
|
||||
remark: ''
|
||||
};
|
||||
const data = reactive<PageData<OssConfigForm, OssConfigQuery>>({
|
||||
form: { ...initFormData },
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
configKey: '',
|
||||
bucketName: '',
|
||||
status: ''
|
||||
},
|
||||
rules: {
|
||||
configKey: [{ required: true, message: 'configKey不能为空', trigger: 'blur' }],
|
||||
accessKey: [
|
||||
{ required: true, message: 'accessKey不能为空', trigger: 'blur' },
|
||||
{
|
||||
min: 2,
|
||||
max: 200,
|
||||
message: 'accessKey长度必须介于 2 和 100 之间',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
secretKey: [
|
||||
{ required: true, message: 'secretKey不能为空', trigger: 'blur' },
|
||||
{
|
||||
min: 2,
|
||||
max: 100,
|
||||
message: 'secretKey长度必须介于 2 和 100 之间',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
bucketName: [
|
||||
{ required: true, message: 'bucketName不能为空', trigger: 'blur' },
|
||||
{
|
||||
min: 2,
|
||||
max: 100,
|
||||
message: 'bucketName长度必须介于 2 和 100 之间',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
endpoint: [
|
||||
{ required: true, message: 'endpoint不能为空', trigger: 'blur' },
|
||||
{
|
||||
min: 2,
|
||||
max: 100,
|
||||
message: 'endpoint名称长度必须介于 2 和 100 之间',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
accessPolicy: [{ required: true, message: 'accessPolicy不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
const protocol = computed(() => (form.value.isHttps === 'Y' ? 'https://' : 'http://'));
|
||||
|
||||
/** 查询对象存储配置列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listOssConfig(queryParams.value);
|
||||
ossConfigList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
dialog.visible = false;
|
||||
reset();
|
||||
};
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
ossConfigFormRef.value?.resetFields();
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
/** 选择条数 */
|
||||
const handleSelectionChange = (selection: OssConfigVO[]) => {
|
||||
ids.value = selection.map((item) => item.ossConfigId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加对象存储配置';
|
||||
};
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: OssConfigVO) => {
|
||||
reset();
|
||||
const ossConfigId = row?.ossConfigId || ids.value[0];
|
||||
const res = await getOssConfig(ossConfigId);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改对象存储配置';
|
||||
};
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
ossConfigFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.ossConfigId) {
|
||||
await updateOssConfig(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addOssConfig(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('新增成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 状态修改 */
|
||||
const handleStatusChange = async (row: OssConfigVO) => {
|
||||
const text = row.status === '0' ? '启用' : '停用';
|
||||
try {
|
||||
await proxy?.$modal.confirm('确认要"' + text + '""' + row.configKey + '"配置吗?');
|
||||
await changeOssConfigStatus(row.ossConfigId, row.status, row.configKey);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess(text + '成功');
|
||||
} catch {
|
||||
return;
|
||||
} finally {
|
||||
row.status = row.status === '0' ? '1' : '0';
|
||||
}
|
||||
};
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: OssConfigVO) => {
|
||||
const ossConfigIds = row?.ossConfigId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除OSS配置编号为"' + ossConfigIds + '"的数据项?');
|
||||
loading.value = true;
|
||||
await delOssConfig(ossConfigIds).finally(() => (loading.value = false));
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,333 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="文件名" prop="fileName">
|
||||
<el-input v-model="queryParams.fileName" placeholder="请输入文件名" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="原名" prop="originalName">
|
||||
<el-input v-model="queryParams.originalName" placeholder="请输入原名" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文件后缀" prop="fileSuffix">
|
||||
<el-input v-model="queryParams.fileSuffix" placeholder="请输入文件后缀" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRangeCreateTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="服务商" prop="service">
|
||||
<el-input v-model="queryParams.service" placeholder="请输入服务商" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:oss:upload']" type="primary" plain icon="Upload" @click="handleFile">上传文件</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:oss:upload']" type="primary" plain icon="Upload" @click="handleImage">上传图片</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:oss:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button
|
||||
v-hasPermi="['system:oss:edit']"
|
||||
:type="previewListResource ? 'danger' : 'warning'"
|
||||
plain
|
||||
@click="handlePreviewListResource(!previewListResource)"
|
||||
>预览开关 : {{ previewListResource ? '禁用' : '启用' }}</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:ossConfig:list']" type="info" plain icon="Operation" @click="handleOssConfig">配置管理</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
v-if="showTable"
|
||||
v-loading="loading"
|
||||
:data="ossList"
|
||||
border
|
||||
:header-cell-class-name="handleHeaderClass"
|
||||
@selection-change="handleSelectionChange"
|
||||
@header-click="handleHeaderCLick"
|
||||
>
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="对象存储主键" align="center" prop="ossId" />
|
||||
<el-table-column label="文件名" align="center" prop="fileName" />
|
||||
<el-table-column label="原名" align="center" prop="originalName" />
|
||||
<el-table-column label="文件后缀" align="center" prop="fileSuffix" />
|
||||
<el-table-column label="文件展示" align="center" prop="url">
|
||||
<template #default="scope">
|
||||
<ImagePreview
|
||||
v-if="previewListResource && checkFileSuffix(scope.row.fileSuffix)"
|
||||
:width="100"
|
||||
:height="100"
|
||||
:src="scope.row.url"
|
||||
:preview-src-list="[scope.row.url]"
|
||||
/>
|
||||
<span v-if="!checkFileSuffix(scope.row.fileSuffix) || !previewListResource" v-text="scope.row.url" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180" sortable="custom">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="上传人" align="center" prop="createByName" />
|
||||
<el-table-column label="服务商" align="center" prop="service" sortable="custom" />
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="下载" placement="top">
|
||||
<el-button v-hasPermi="['system:oss:download']" link type="primary" icon="Download" @click="handleDownload(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:oss:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改OSS对象存储对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="ossFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="文件名">
|
||||
<fileUpload v-if="type === 0" v-model="form.file" />
|
||||
<imageUpload v-if="type === 1" v-model="form.file" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Oss" lang="ts">
|
||||
import { listOss, delOss } from '@/api/system/oss';
|
||||
import ImagePreview from '@/components/ImagePreview/index.vue';
|
||||
import { OssForm, OssQuery, OssVO } from '@/api/system/oss/types';
|
||||
|
||||
const router = useRouter();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const ossList = ref<OssVO[]>([]);
|
||||
const showTable = ref(true);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const type = ref(0);
|
||||
const previewListResource = ref(true);
|
||||
const dateRangeCreateTime = ref<[DateModelType, DateModelType]>(['', '']);
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
// 默认排序
|
||||
const defaultSort = ref({ prop: 'createTime', order: 'ascending' });
|
||||
|
||||
const ossFormRef = ref<ElFormInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const initFormData = {
|
||||
file: undefined
|
||||
};
|
||||
const data = reactive<PageData<OssForm, OssQuery>>({
|
||||
form: { ...initFormData },
|
||||
// 查询参数
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
fileName: '',
|
||||
originalName: '',
|
||||
fileSuffix: '',
|
||||
createTime: '',
|
||||
service: '',
|
||||
orderByColumn: defaultSort.value.prop,
|
||||
isAsc: defaultSort.value.order
|
||||
},
|
||||
rules: {
|
||||
file: [{ required: true, message: '文件不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询OSS对象存储列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await proxy?.getConfigKey('sys.oss.previewListResource');
|
||||
previewListResource.value = res?.data === undefined ? true : res.data === 'true';
|
||||
const response = await listOss(proxy?.addDateRange(queryParams.value, dateRangeCreateTime.value, 'CreateTime'));
|
||||
ossList.value = response.rows;
|
||||
total.value = response.total;
|
||||
loading.value = false;
|
||||
showTable.value = true;
|
||||
};
|
||||
function checkFileSuffix(fileSuffix: string | string[]) {
|
||||
const arr = ['.png', '.jpg', '.jpeg'];
|
||||
const suffixArray = Array.isArray(fileSuffix) ? fileSuffix : [fileSuffix];
|
||||
return suffixArray.some((suffix) => arr.includes(suffix.toLowerCase()));
|
||||
}
|
||||
/** 取消按钮 */
|
||||
function cancel() {
|
||||
dialog.visible = false;
|
||||
reset();
|
||||
}
|
||||
/** 表单重置 */
|
||||
function reset() {
|
||||
form.value = { ...initFormData };
|
||||
ossFormRef.value?.resetFields();
|
||||
}
|
||||
/** 搜索按钮操作 */
|
||||
function handleQuery() {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
}
|
||||
/** 重置按钮操作 */
|
||||
function resetQuery() {
|
||||
showTable.value = false;
|
||||
dateRangeCreateTime.value = ['', ''];
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.orderByColumn = defaultSort.value.prop;
|
||||
queryParams.value.isAsc = defaultSort.value.order;
|
||||
handleQuery();
|
||||
}
|
||||
/** 选择条数 */
|
||||
function handleSelectionChange(selection: OssVO[]) {
|
||||
ids.value = selection.map((item) => item.ossId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
}
|
||||
/** 设置列的排序为我们自定义的排序 */
|
||||
const handleHeaderClass = ({ column }: any): any => {
|
||||
column.order = column.multiOrder;
|
||||
};
|
||||
/** 点击表头进行排序 */
|
||||
const handleHeaderCLick = (column: any) => {
|
||||
if (column.sortable !== 'custom') {
|
||||
return;
|
||||
}
|
||||
switch (column.multiOrder) {
|
||||
case 'descending':
|
||||
column.multiOrder = 'ascending';
|
||||
break;
|
||||
case 'ascending':
|
||||
column.multiOrder = '';
|
||||
break;
|
||||
default:
|
||||
column.multiOrder = 'descending';
|
||||
break;
|
||||
}
|
||||
handleOrderChange(column.property, column.multiOrder);
|
||||
};
|
||||
const handleOrderChange = (prop: string, order: string) => {
|
||||
const orderByArr = queryParams.value.orderByColumn ? queryParams.value.orderByColumn.split(',') : [];
|
||||
const isAscArr = queryParams.value.isAsc ? queryParams.value.isAsc.split(',') : [];
|
||||
const propIndex = orderByArr.indexOf(prop);
|
||||
if (propIndex !== -1) {
|
||||
if (order) {
|
||||
//排序里已存在 只修改排序
|
||||
isAscArr[propIndex] = order;
|
||||
} else {
|
||||
//如果order为null 则删除排序字段和属性
|
||||
isAscArr.splice(propIndex, 1); //删除排序
|
||||
orderByArr.splice(propIndex, 1); //删除属性
|
||||
}
|
||||
} else {
|
||||
//排序里不存在则新增排序
|
||||
orderByArr.push(prop);
|
||||
isAscArr.push(order);
|
||||
}
|
||||
//合并排序
|
||||
queryParams.value.orderByColumn = orderByArr.join(',');
|
||||
queryParams.value.isAsc = isAscArr.join(',');
|
||||
getList();
|
||||
};
|
||||
/** 任务日志列表查询 */
|
||||
const handleOssConfig = () => {
|
||||
router.push('/system/oss-config/index');
|
||||
};
|
||||
/** 文件按钮操作 */
|
||||
const handleFile = () => {
|
||||
reset();
|
||||
type.value = 0;
|
||||
dialog.visible = true;
|
||||
dialog.title = '上传文件';
|
||||
};
|
||||
/** 图片按钮操作 */
|
||||
const handleImage = () => {
|
||||
reset();
|
||||
type.value = 1;
|
||||
dialog.visible = true;
|
||||
dialog.title = '上传图片';
|
||||
};
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
dialog.visible = false;
|
||||
getList();
|
||||
};
|
||||
/** 下载按钮操作 */
|
||||
const handleDownload = (row: OssVO) => {
|
||||
proxy?.$download.oss(row.ossId);
|
||||
};
|
||||
/** 预览开关按钮 */
|
||||
const handlePreviewListResource = async (preview: boolean) => {
|
||||
const text = preview ? '启用' : '停用';
|
||||
try {
|
||||
await proxy?.$modal.confirm('确认要"' + text + '""预览列表图片"配置吗?');
|
||||
await proxy?.updateConfigByKey('sys.oss.previewListResource', preview);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess(text + '成功');
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
};
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: OssVO) => {
|
||||
const ossIds = row?.ossId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除OSS对象存储编号为"' + ossIds + '"的数据项?');
|
||||
loading.value = true;
|
||||
await delOss(ossIds).finally(() => (loading.value = false));
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,361 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-row :gutter="20">
|
||||
<!-- 部门树 -->
|
||||
<el-col :lg="4" :xs="24" style="">
|
||||
<el-card shadow="hover">
|
||||
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
class="mt-2"
|
||||
node-key="id"
|
||||
:data="deptOptions"
|
||||
:props="{ label: 'label', children: 'children' } as any"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="filterNode"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="20" :xs="24">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="岗位编码" prop="postCode">
|
||||
<el-input v-model="queryParams.postCode" placeholder="请输入岗位编码" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类别编码" prop="postCategory">
|
||||
<el-input
|
||||
v-model="queryParams.postCategory"
|
||||
placeholder="请输入类别编码"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位名称" prop="postName">
|
||||
<el-input v-model="queryParams.postName" placeholder="请输入岗位名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="deptId">
|
||||
<el-tree-select
|
||||
v-model="queryParams.deptId"
|
||||
:data="deptOptions"
|
||||
:props="{ value: 'id', label: 'label', children: 'children' } as any"
|
||||
value-key="id"
|
||||
placeholder="请选择部门"
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="岗位状态" clearable>
|
||||
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:post:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:post:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
|
||||
>修改</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:post:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:post:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-table v-loading="loading" border :data="postList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="岗位编号" align="center" prop="postId" />
|
||||
<el-table-column label="岗位编码" align="center" prop="postCode" />
|
||||
<el-table-column label="类别编码" align="center" prop="postCategory" />
|
||||
<el-table-column label="岗位名称" align="center" prop="postName" />
|
||||
<el-table-column label="部门" align="center" prop="deptName" />
|
||||
<el-table-column label="排序" align="center" prop="postSort" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:post:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:post:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<!-- 添加或修改岗位对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="postFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="岗位名称" prop="postName">
|
||||
<el-input v-model="form.postName" placeholder="请输入岗位名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" prop="deptId">
|
||||
<el-tree-select
|
||||
v-model="form.deptId"
|
||||
:data="deptOptions"
|
||||
:props="{ value: 'id', label: 'label', children: 'children' } as any"
|
||||
value-key="id"
|
||||
placeholder="请选择部门"
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位编码" prop="postCode">
|
||||
<el-input v-model="form.postCode" placeholder="请输入编码名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="类别编码" prop="postCategory">
|
||||
<el-input v-model="form.postCategory" placeholder="请输入类别编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位顺序" prop="postSort">
|
||||
<el-input-number v-model="form.postSort" controls-position="right" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="岗位状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Post" lang="ts">
|
||||
import { listPost, addPost, delPost, getPost, updatePost, deptTreeSelect } from '@/api/system/post';
|
||||
import { PostForm, PostQuery, PostVO } from '@/api/system/post/types';
|
||||
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
|
||||
|
||||
const postList = ref<PostVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<number | string>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const deptName = ref('');
|
||||
const deptOptions = ref<DeptTreeVO[]>([]);
|
||||
const deptTreeRef = ref<ElTreeInstance>();
|
||||
const postFormRef = ref<ElFormInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: PostForm = {
|
||||
postId: undefined,
|
||||
deptId: undefined,
|
||||
postCode: '',
|
||||
postName: '',
|
||||
postCategory: '',
|
||||
postSort: 0,
|
||||
status: '0',
|
||||
remark: ''
|
||||
};
|
||||
|
||||
const data = reactive<PageData<PostForm, PostQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
deptId: undefined,
|
||||
belongDeptId: undefined,
|
||||
postCode: '',
|
||||
postName: '',
|
||||
postCategory: '',
|
||||
status: ''
|
||||
},
|
||||
rules: {
|
||||
postName: [{ required: true, message: '岗位名称不能为空', trigger: 'blur' }],
|
||||
postCode: [{ required: true, message: '岗位编码不能为空', trigger: 'blur' }],
|
||||
deptId: [{ required: true, message: '部门不能为空', trigger: 'blur' }],
|
||||
postSort: [{ required: true, message: '岗位顺序不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs<PageData<PostForm, PostQuery>>(data);
|
||||
|
||||
/** 通过条件过滤节点 */
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true;
|
||||
return data.label.indexOf(value) !== -1;
|
||||
};
|
||||
|
||||
/** 根据名称筛选部门树 */
|
||||
watchEffect(
|
||||
() => {
|
||||
deptTreeRef.value?.filter(deptName.value);
|
||||
},
|
||||
{
|
||||
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
|
||||
}
|
||||
);
|
||||
|
||||
/** 查询部门下拉树结构 */
|
||||
const getTreeSelect = async () => {
|
||||
const res = await deptTreeSelect();
|
||||
deptOptions.value = res.data;
|
||||
};
|
||||
|
||||
/** 节点单击事件 */
|
||||
const handleNodeClick = (data: DeptVO) => {
|
||||
queryParams.value.belongDeptId = data.id;
|
||||
queryParams.value.deptId = undefined;
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 查询岗位列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listPost(queryParams.value);
|
||||
postList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
postFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
if (queryParams.value.deptId) {
|
||||
queryParams.value.belongDeptId = undefined;
|
||||
}
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.deptId = undefined;
|
||||
deptTreeRef.value?.setCurrentKey(undefined);
|
||||
/** 清空左边部门树选中值 */
|
||||
queryParams.value.belongDeptId = undefined;
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: PostVO[]) => {
|
||||
ids.value = selection.map((item) => item.postId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加岗位';
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: PostVO) => {
|
||||
reset();
|
||||
const postId = row?.postId || ids.value[0];
|
||||
const res = await getPost(postId);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改岗位';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
postFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
form.value.postId ? await updatePost(form.value) : await addPost(form.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: PostVO) => {
|
||||
const postIds = row?.postId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除岗位编号为"' + postIds + '"的数据项?');
|
||||
await delPost(postIds);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'system/post/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`post_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getTreeSelect(); // 初始化部门数据
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="search">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="phonenumber">
|
||||
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="openSelectUser">添加用户</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:role:remove']" type="danger" plain icon="CircleClose" :disabled="multiple" @click="cancelAuthUserAll">
|
||||
批量取消授权
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="Close" @click="handleClose">关闭</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" :search="true" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="取消授权" placement="top">
|
||||
<el-button v-hasPermi="['system:role:remove']" link type="primary" icon="CircleClose" @click="cancelAuthUser(scope.row)"> </el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
<select-user ref="selectRef" :role-id="queryParams.roleId" @ok="handleQuery" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="AuthUser" lang="ts">
|
||||
import { allocatedUserList, authUserCancel, authUserCancelAll } from '@/api/system/role';
|
||||
import { UserQuery } from '@/api/system/user/types';
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
import SelectUser from './selectUser.vue';
|
||||
import { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
|
||||
|
||||
const userList = ref<UserVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const userIds = ref<Array<string | number>>([]);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const selectRef = ref<InstanceType<typeof SelectUser>>();
|
||||
|
||||
const queryParams = reactive<UserQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
roleId: route.params.roleId as string,
|
||||
userName: undefined,
|
||||
phonenumber: undefined
|
||||
});
|
||||
|
||||
/** 查询授权用户列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await allocatedUserList(queryParams);
|
||||
userList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
// 返回按钮
|
||||
const handleClose = () => {
|
||||
const obj: RouteLocationNormalized = {
|
||||
path: '/system/role',
|
||||
fullPath: '',
|
||||
hash: '',
|
||||
matched: [],
|
||||
meta: undefined,
|
||||
name: undefined,
|
||||
params: undefined,
|
||||
query: undefined,
|
||||
redirectedFrom: undefined
|
||||
};
|
||||
proxy?.$tab.closeOpenPage(obj);
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: UserVO[]) => {
|
||||
userIds.value = selection.map((item) => item.userId);
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
/** 打开授权用户表弹窗 */
|
||||
const openSelectUser = () => {
|
||||
selectRef.value?.show();
|
||||
};
|
||||
/** 取消授权按钮操作 */
|
||||
const cancelAuthUser = async (row: UserVO) => {
|
||||
await proxy?.$modal.confirm('确认要取消该用户"' + row.userName + '"角色吗?');
|
||||
await authUserCancel({ userId: row.userId, roleId: queryParams.roleId });
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('取消授权成功');
|
||||
};
|
||||
/** 批量取消授权按钮操作 */
|
||||
const cancelAuthUserAll = async () => {
|
||||
const roleId = queryParams.roleId;
|
||||
const uIds = userIds.value.join(',');
|
||||
await proxy?.$modal.confirm('是否取消选中用户授权数据项?');
|
||||
await authUserCancelAll({ roleId: roleId, userIds: uIds });
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('取消授权成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,503 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="queryParams.roleName" placeholder="请输入角色名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="权限字符" prop="roleKey">
|
||||
<el-input v-model="queryParams.roleKey" placeholder="请输入权限字符" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="角色状态" clearable>
|
||||
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:role:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:role:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:role:remove']" type="danger" plain :disabled="ids.length === 0" @click="handleDelete()">删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:role:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table ref="roleTableRef" border v-loading="loading" :data="roleList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="角色编号" prop="roleId" width="120" />
|
||||
<el-table-column label="角色名称" prop="roleName" :show-overflow-tooltip="true" width="150" />
|
||||
<el-table-column label="权限字符" prop="roleKey" :show-overflow-tooltip="true" width="200" />
|
||||
<el-table-column label="显示顺序" prop="roleSort" width="100" />
|
||||
<el-table-column label="状态" align="center" width="100">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column fixed="right" label="操作" width="180">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="scope.row.roleId !== 1" content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.roleId !== 1" content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:role:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.roleId !== 1" content="数据权限" placement="top">
|
||||
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="CircleCheck" @click="handleDataScope(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.roleId !== 1" content="分配用户" placement="top">
|
||||
<el-button v-hasPermi="['system:role:edit']" link type="primary" icon="User" @click="handleAuthUser(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-if="total > 0"
|
||||
v-model:total="total"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="roleFormRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="角色名称" prop="roleName">
|
||||
<el-input v-model="form.roleName" placeholder="请输入角色名称" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="roleKey">
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="控制器中定义的权限字符,如:@SaCheckRole('admin')" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
权限字符
|
||||
</span>
|
||||
</template>
|
||||
<el-input v-model="form.roleKey" placeholder="请输入权限字符" />
|
||||
</el-form-item>
|
||||
<el-form-item label="角色顺序" prop="roleSort">
|
||||
<el-input-number v-model="form.roleSort" controls-position="right" :min="0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="菜单权限">
|
||||
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
|
||||
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选</el-checkbox>
|
||||
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动</el-checkbox>
|
||||
<el-tree
|
||||
ref="menuRef"
|
||||
class="tree-border"
|
||||
:data="menuOptions"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:check-strictly="!form.menuCheckStrictly"
|
||||
empty-text="加载中,请稍候"
|
||||
:props="{ label: 'label', children: 'children' } as any"
|
||||
></el-tree>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分配角色数据权限对话框 -->
|
||||
<el-dialog v-model="openDataScope" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="dataScopeRef" :model="form" label-width="80px">
|
||||
<el-form-item label="角色名称">
|
||||
<el-input v-model="form.roleName" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="权限字符">
|
||||
<el-input v-model="form.roleKey" :disabled="true" />
|
||||
</el-form-item>
|
||||
<el-form-item label="权限范围">
|
||||
<el-select v-model="form.dataScope" @change="dataScopeSelectChange">
|
||||
<el-option v-for="item in dataScopeOptions" :key="item.value" :label="item.label" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item v-show="form.dataScope === '2'" label="数据权限">
|
||||
<el-checkbox v-model="deptExpand" @change="handleCheckedTreeExpand($event, 'dept')">展开/折叠</el-checkbox>
|
||||
<el-checkbox v-model="deptNodeAll" @change="handleCheckedTreeNodeAll($event, 'dept')">全选/全不选</el-checkbox>
|
||||
<el-checkbox v-model="form.deptCheckStrictly" @change="handleCheckedTreeConnect($event, 'dept')">父子联动</el-checkbox>
|
||||
<el-tree
|
||||
ref="deptRef"
|
||||
class="tree-border"
|
||||
:data="deptOptions"
|
||||
show-checkbox
|
||||
default-expand-all
|
||||
node-key="id"
|
||||
:check-strictly="!form.deptCheckStrictly"
|
||||
empty-text="加载中,请稍候"
|
||||
:props="{ label: 'label', children: 'children' } as any"
|
||||
></el-tree>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitDataScope">确 定</el-button>
|
||||
<el-button @click="cancelDataScope">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Role" lang="ts">
|
||||
import { addRole, changeRoleStatus, dataScope, delRole, getRole, listRole, updateRole, deptTreeSelect } from '@/api/system/role';
|
||||
import { roleMenuTreeselect, treeselect as menuTreeselect } from '@/api/system/menu/index';
|
||||
import { RoleVO, RoleForm, RoleQuery, DeptTreeOption } from '@/api/system/role/types';
|
||||
import { MenuTreeOption, RoleMenuTree } from '@/api/system/menu/types';
|
||||
|
||||
const router = useRouter();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
|
||||
|
||||
const roleList = ref<RoleVO[]>();
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||
const menuOptions = ref<MenuTreeOption[]>([]);
|
||||
const menuExpand = ref(false);
|
||||
const menuNodeAll = ref(false);
|
||||
const deptExpand = ref(true);
|
||||
const deptNodeAll = ref(false);
|
||||
const deptOptions = ref<DeptTreeOption[]>([]);
|
||||
const openDataScope = ref(false);
|
||||
|
||||
/** 数据范围选项*/
|
||||
const dataScopeOptions = ref([
|
||||
{ value: '1', label: '全部数据权限' },
|
||||
{ value: '2', label: '自定数据权限' },
|
||||
{ value: '3', label: '本部门数据权限' },
|
||||
{ value: '4', label: '本部门及以下数据权限' },
|
||||
{ value: '5', label: '仅本人数据权限' },
|
||||
{ value: '6', label: '部门及以下或本人数据权限' }
|
||||
]);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const roleFormRef = ref<ElFormInstance>();
|
||||
const dataScopeRef = ref<ElFormInstance>();
|
||||
const menuRef = ref<ElTreeInstance>();
|
||||
const deptRef = ref<ElTreeInstance>();
|
||||
|
||||
const initForm: RoleForm = {
|
||||
roleId: undefined,
|
||||
roleSort: 1,
|
||||
status: '0',
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
menuCheckStrictly: true,
|
||||
deptCheckStrictly: true,
|
||||
remark: '',
|
||||
dataScope: '1',
|
||||
menuIds: [],
|
||||
deptIds: []
|
||||
};
|
||||
|
||||
const data = reactive<PageData<RoleForm, RoleQuery>>({
|
||||
form: { ...initForm },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
roleName: '',
|
||||
roleKey: '',
|
||||
status: ''
|
||||
},
|
||||
rules: {
|
||||
roleName: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }],
|
||||
roleKey: [{ required: true, message: '权限字符不能为空', trigger: 'blur' }],
|
||||
roleSort: [{ required: true, message: '角色顺序不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
const { form, queryParams, rules } = toRefs(data);
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
/**
|
||||
* 查询角色列表
|
||||
*/
|
||||
const getList = () => {
|
||||
loading.value = true;
|
||||
listRole(proxy?.addDateRange(queryParams.value, dateRange.value)).then((res) => {
|
||||
roleList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 搜索按钮操作
|
||||
*/
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = ['', ''];
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
/**删除按钮操作 */
|
||||
const handleDelete = async (row?: RoleVO) => {
|
||||
const roleids = row?.roleId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除角色编号为' + roleids + '数据项目');
|
||||
await delRole(roleids);
|
||||
getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'system/role/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`role_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: RoleVO[]) => {
|
||||
ids.value = selection.map((item: RoleVO) => item.roleId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 角色状态修改 */
|
||||
const handleStatusChange = async (row: RoleVO) => {
|
||||
const text = row.status === '0' ? '启用' : '停用';
|
||||
try {
|
||||
await proxy?.$modal.confirm('确认要"' + text + '""' + row.roleName + '"角色吗?');
|
||||
await changeRoleStatus(row.roleId, row.status);
|
||||
proxy?.$modal.msgSuccess(text + '成功');
|
||||
} catch {
|
||||
row.status = row.status === '0' ? '1' : '0';
|
||||
}
|
||||
};
|
||||
|
||||
/** 分配用户 */
|
||||
const handleAuthUser = (row: RoleVO) => {
|
||||
router.push('/system/role-auth/user/' + row.roleId);
|
||||
};
|
||||
|
||||
/** 查询菜单树结构 */
|
||||
const getMenuTreeselect = async () => {
|
||||
const res = await menuTreeselect();
|
||||
menuOptions.value = res.data;
|
||||
};
|
||||
/** 所有部门节点数据 */
|
||||
const getDeptAllCheckedKeys = (): any => {
|
||||
// 目前被选中的部门节点
|
||||
const checkedKeys = deptRef.value?.getCheckedKeys();
|
||||
// 半选中的部门节点
|
||||
const halfCheckedKeys = deptRef.value?.getHalfCheckedKeys();
|
||||
if (halfCheckedKeys) {
|
||||
checkedKeys?.unshift(...halfCheckedKeys);
|
||||
}
|
||||
return checkedKeys;
|
||||
};
|
||||
/** 重置新增的表单以及其他数据 */
|
||||
const reset = () => {
|
||||
menuRef.value?.setCheckedKeys([]);
|
||||
menuExpand.value = false;
|
||||
menuNodeAll.value = false;
|
||||
deptExpand.value = true;
|
||||
deptNodeAll.value = false;
|
||||
form.value = { ...initForm };
|
||||
roleFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 添加角色 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
getMenuTreeselect();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加角色';
|
||||
};
|
||||
/** 修改角色 */
|
||||
const handleUpdate = async (row?: RoleVO) => {
|
||||
reset();
|
||||
const roleId = row?.roleId || ids.value[0];
|
||||
const { data } = await getRole(roleId);
|
||||
Object.assign(form.value, data);
|
||||
form.value.roleSort = Number(form.value.roleSort);
|
||||
const res = await getRoleMenuTreeselect(roleId);
|
||||
dialog.title = '修改角色';
|
||||
dialog.visible = true;
|
||||
res.checkedKeys.forEach((v) => {
|
||||
nextTick(() => {
|
||||
menuRef.value?.setChecked(v, true, false);
|
||||
});
|
||||
});
|
||||
};
|
||||
/** 根据角色ID查询菜单树结构 */
|
||||
const getRoleMenuTreeselect = (roleId: string | number) => {
|
||||
return roleMenuTreeselect(roleId).then((res): RoleMenuTree => {
|
||||
menuOptions.value = res.data.menus;
|
||||
return res.data;
|
||||
});
|
||||
};
|
||||
/** 根据角色ID查询部门树结构 */
|
||||
const getRoleDeptTreeSelect = async (roleId: string | number) => {
|
||||
const res = await deptTreeSelect(roleId);
|
||||
deptOptions.value = res.data.depts;
|
||||
return res.data;
|
||||
};
|
||||
/** 树权限(展开/折叠)*/
|
||||
const handleCheckedTreeExpand = (value: boolean, type: string) => {
|
||||
if (type == 'menu') {
|
||||
const treeList = menuOptions.value;
|
||||
for (let i = 0; i < treeList.length; i++) {
|
||||
if (menuRef.value) {
|
||||
menuRef.value.store.nodesMap[treeList[i].id].expanded = value;
|
||||
}
|
||||
}
|
||||
} else if (type == 'dept') {
|
||||
const treeList = deptOptions.value;
|
||||
for (let i = 0; i < treeList.length; i++) {
|
||||
if (deptRef.value) {
|
||||
deptRef.value.store.nodesMap[treeList[i].id].expanded = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
/** 树权限(全选/全不选) */
|
||||
const handleCheckedTreeNodeAll = (value: any, type: string) => {
|
||||
if (type == 'menu') {
|
||||
menuRef.value?.setCheckedNodes(value ? (menuOptions.value as any) : []);
|
||||
} else if (type == 'dept') {
|
||||
deptRef.value?.setCheckedNodes(value ? (deptOptions.value as any) : []);
|
||||
}
|
||||
};
|
||||
/** 树权限(父子联动) */
|
||||
const handleCheckedTreeConnect = (value: any, type: string) => {
|
||||
if (type == 'menu') {
|
||||
form.value.menuCheckStrictly = value;
|
||||
} else if (type == 'dept') {
|
||||
form.value.deptCheckStrictly = value;
|
||||
}
|
||||
};
|
||||
/** 所有菜单节点数据 */
|
||||
const getMenuAllCheckedKeys = (): any => {
|
||||
// 目前被选中的菜单节点
|
||||
const checkedKeys = menuRef.value?.getCheckedKeys();
|
||||
// 半选中的菜单节点
|
||||
const halfCheckedKeys = menuRef.value?.getHalfCheckedKeys();
|
||||
if (halfCheckedKeys) {
|
||||
checkedKeys?.unshift(...halfCheckedKeys);
|
||||
}
|
||||
return checkedKeys;
|
||||
};
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
roleFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
form.value.menuIds = getMenuAllCheckedKeys();
|
||||
form.value.roleId ? await updateRole(form.value) : await addRole(form.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
/** 选择角色权限范围触发 */
|
||||
const dataScopeSelectChange = (value: string) => {
|
||||
if (value !== '2') {
|
||||
deptRef.value?.setCheckedKeys([]);
|
||||
}
|
||||
};
|
||||
/** 分配数据权限操作 */
|
||||
const handleDataScope = async (row: RoleVO) => {
|
||||
const response = await getRole(row.roleId);
|
||||
Object.assign(form.value, response.data);
|
||||
const res = await getRoleDeptTreeSelect(row.roleId);
|
||||
openDataScope.value = true;
|
||||
dialog.title = '分配数据权限';
|
||||
await nextTick(() => {
|
||||
deptRef.value?.setCheckedKeys(res.checkedKeys);
|
||||
});
|
||||
};
|
||||
/** 提交按钮(数据权限) */
|
||||
const submitDataScope = async () => {
|
||||
if (form.value.roleId) {
|
||||
form.value.deptIds = getDeptAllCheckedKeys();
|
||||
await dataScope(form.value);
|
||||
proxy?.$modal.msgSuccess('修改成功');
|
||||
openDataScope.value = false;
|
||||
getList();
|
||||
}
|
||||
};
|
||||
/** 取消按钮(数据权限)*/
|
||||
const cancelDataScope = () => {
|
||||
dataScopeRef.value?.resetFields();
|
||||
form.value = { ...initForm };
|
||||
openDataScope.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<el-row>
|
||||
<el-dialog v-model="visible" title="选择用户" width="800px" top="5vh" append-to-body>
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="phonenumber">
|
||||
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-row>
|
||||
<el-table ref="tableRef" border :data="userList" height="260px" @row-click="clickRow" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column label="用户名称" prop="userName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="用户昵称" prop="nickName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="邮箱" prop="email" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="手机" prop="phonenumber" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_normal_disable" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-if="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-row>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="handleSelectUser">确 定</el-button>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<script setup name="SelectUser" lang="ts">
|
||||
import { authUserSelectAll, unallocatedUserList } from '@/api/system/role';
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
import { UserQuery } from '@/api/system/user/types';
|
||||
|
||||
const props = defineProps({
|
||||
roleId: {
|
||||
type: [Number, String],
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_normal_disable'));
|
||||
|
||||
const userList = ref<UserVO[]>([]);
|
||||
const visible = ref(false);
|
||||
const total = ref(0);
|
||||
const userIds = ref<Array<string | number>>([]);
|
||||
|
||||
const queryParams = reactive<UserQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
roleId: undefined,
|
||||
userName: undefined,
|
||||
phonenumber: undefined
|
||||
});
|
||||
|
||||
const tableRef = ref<ElTableInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const show = () => {
|
||||
queryParams.roleId = props.roleId;
|
||||
getList();
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* 选择行
|
||||
*/
|
||||
const clickRow = (row: any) => {
|
||||
// ele的bug
|
||||
tableRef.value?.toggleRowSelection(row, false);
|
||||
};
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: UserVO[]) => {
|
||||
userIds.value = selection.map((item: UserVO) => item.userId);
|
||||
};
|
||||
|
||||
/** 查询数据 */
|
||||
const getList = async () => {
|
||||
const res = await unallocatedUserList(queryParams);
|
||||
userList.value = res.rows;
|
||||
total.value = res.total;
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
getList();
|
||||
};
|
||||
|
||||
const emit = defineEmits(['ok']);
|
||||
/**选择授权用户操作 */
|
||||
const handleSelectUser = async () => {
|
||||
const roleId = queryParams.roleId;
|
||||
const ids = userIds.value.join(',');
|
||||
if (ids == '') {
|
||||
proxy?.$modal.msgError('请选择要分配的用户');
|
||||
return;
|
||||
}
|
||||
await authUserSelectAll({ roleId, userIds: ids });
|
||||
proxy?.$modal.msgSuccess('分配成功');
|
||||
emit('ok');
|
||||
visible.value = false;
|
||||
};
|
||||
// 暴露
|
||||
defineExpose({
|
||||
show
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="租户编号" prop="tenantId">
|
||||
<el-input v-model="queryParams.tenantId" placeholder="请输入租户编号" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contactUserName">
|
||||
<el-input v-model="queryParams.contactUserName" placeholder="请输入联系人" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="contactPhone">
|
||||
<el-input v-model="queryParams.contactPhone" placeholder="请输入联系电话" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="企业名称" prop="companyName">
|
||||
<el-input v-model="queryParams.companyName" placeholder="请输入企业名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:tenant:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:tenant:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()"
|
||||
>修改</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:tenant:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:tenant:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-if="userId === 1" type="success" plain icon="Refresh" @click="handleSyncTenantDict">同步租户字典</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-if="userId === 1" type="success" plain icon="Refresh" @click="handleSyncTenantConfig">同步租户参数配置</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="tenantList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="id" align="center" prop="id" />
|
||||
<el-table-column label="租户编号" align="center" prop="tenantId" />
|
||||
<el-table-column label="联系人" align="center" prop="contactUserName" />
|
||||
<el-table-column label="联系电话" align="center" prop="contactPhone" />
|
||||
<el-table-column label="企业名称" align="center" prop="companyName" />
|
||||
<el-table-column label="社会信用代码" align="center" prop="licenseNumber" />
|
||||
<el-table-column label="过期时间" align="center" prop="expireTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.expireTime, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="租户状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column width="150" label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:tenant:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="同步套餐" placement="top">
|
||||
<el-button v-hasPermi="['system:tenant:edit']" link type="primary" icon="Refresh" @click="handleSyncTenantPackage(scope.row)">
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:tenant:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改租户对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="tenantFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="企业名称" prop="companyName">
|
||||
<el-input v-model="form.companyName" placeholder="请输入企业名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系人" prop="contactUserName">
|
||||
<el-input v-model="form.contactUserName" placeholder="请输入联系人" />
|
||||
</el-form-item>
|
||||
<el-form-item label="联系电话" prop="contactPhone">
|
||||
<el-input v-model="form.contactPhone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!form.id" label="用户名" prop="username">
|
||||
<el-input v-model="form.username" placeholder="请输入系统用户名" maxlength="30" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="!form.id" label="用户密码" prop="password">
|
||||
<el-input v-model="form.password" type="password" placeholder="请输入系统用户密码" maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="租户套餐" prop="packageId">
|
||||
<el-select v-model="form.packageId" :disabled="!!form.tenantId" placeholder="请选择租户套餐" clearable style="width: 100%">
|
||||
<el-option v-for="item in packageList" :key="item.packageId" :label="item.packageName" :value="item.packageId" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="过期时间" prop="expireTime">
|
||||
<el-date-picker v-model="form.expireTime" clearable type="datetime" value-format="YYYY-MM-DD HH:mm:ss" placeholder="请选择过期时间">
|
||||
</el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户数量" prop="accountCount">
|
||||
<el-input v-model="form.accountCount" placeholder="请输入用户数量" />
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定域名" prop="domain">
|
||||
<el-input v-model="form.domain" placeholder="请输入绑定域名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="企业地址" prop="address">
|
||||
<el-input v-model="form.address" placeholder="请输入企业地址" />
|
||||
</el-form-item>
|
||||
<el-form-item label="企业代码" prop="licenseNumber">
|
||||
<el-input v-model="form.licenseNumber" placeholder="请输入统一社会信用代码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="企业简介" prop="intro">
|
||||
<el-input v-model="form.intro" type="textarea" placeholder="请输入企业简介" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Tenant" lang="ts">
|
||||
import {
|
||||
listTenant,
|
||||
getTenant,
|
||||
delTenant,
|
||||
addTenant,
|
||||
updateTenant,
|
||||
changeTenantStatus,
|
||||
syncTenantPackage,
|
||||
syncTenantDict,
|
||||
syncTenantConfig
|
||||
} from '@/api/system/tenant';
|
||||
import { selectTenantPackage } from '@/api/system/tenantPackage';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { TenantForm, TenantQuery, TenantVO } from '@/api/system/tenant/types';
|
||||
import { TenantPkgVO } from '@/api/system/tenantPackage/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const userStore = useUserStore();
|
||||
const userId = ref(userStore.userId);
|
||||
const tenantList = ref<TenantVO[]>([]);
|
||||
const packageList = ref<TenantPkgVO[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const tenantFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: TenantForm = {
|
||||
id: undefined,
|
||||
tenantId: undefined,
|
||||
contactUserName: '',
|
||||
contactPhone: '',
|
||||
username: '',
|
||||
password: '',
|
||||
companyName: '',
|
||||
licenseNumber: '',
|
||||
domain: '',
|
||||
address: '',
|
||||
intro: '',
|
||||
remark: '',
|
||||
packageId: '',
|
||||
expireTime: '',
|
||||
accountCount: 0,
|
||||
status: '0'
|
||||
};
|
||||
const data = reactive<PageData<TenantForm, TenantQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
tenantId: '',
|
||||
contactUserName: '',
|
||||
contactPhone: '',
|
||||
companyName: ''
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: 'id不能为空', trigger: 'blur' }],
|
||||
tenantId: [{ required: true, message: '租户编号不能为空', trigger: 'blur' }],
|
||||
contactUserName: [{ required: true, message: '联系人不能为空', trigger: 'blur' }],
|
||||
contactPhone: [{ required: true, message: '联系电话不能为空', trigger: 'blur' }],
|
||||
companyName: [{ required: true, message: '企业名称不能为空', trigger: 'blur' }],
|
||||
username: [
|
||||
{ required: true, message: '用户名不能为空', trigger: 'blur' },
|
||||
{ min: 2, max: 20, message: '用户名称长度必须介于 2 和 20 之间', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '密码不能为空', trigger: 'blur' },
|
||||
{ min: 5, max: 20, message: '用户密码长度必须介于 5 和 20 之间', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询所有租户套餐 */
|
||||
const getTenantPackage = async () => {
|
||||
const res = await selectTenantPackage();
|
||||
packageList.value = res.data;
|
||||
};
|
||||
|
||||
/** 查询租户列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listTenant(queryParams.value);
|
||||
tenantList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
// 租户套餐状态修改
|
||||
const handleStatusChange = async (row: TenantVO) => {
|
||||
const text = row.status === '0' ? '启用' : '停用';
|
||||
try {
|
||||
await proxy?.$modal.confirm('确认要"' + text + '""' + row.companyName + '"租户吗?');
|
||||
await changeTenantStatus(row.id, row.tenantId, row.status);
|
||||
proxy?.$modal.msgSuccess(text + '成功');
|
||||
} catch {
|
||||
row.status = row.status === '0' ? '1' : '0';
|
||||
}
|
||||
};
|
||||
|
||||
// 取消按钮
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
// 表单重置
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
tenantFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: TenantVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
getTenantPackage();
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加租户';
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: TenantVO) => {
|
||||
reset();
|
||||
await getTenantPackage();
|
||||
const _id = row?.id || ids.value[0];
|
||||
const res = await getTenant(_id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改租户';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
tenantFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.id) {
|
||||
await updateTenant(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addTenant(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: TenantVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除租户编号为"' + _ids + '"的数据项?');
|
||||
loading.value = true;
|
||||
await delTenant(_ids).finally(() => (loading.value = false));
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
/** 同步租户套餐按钮操作 */
|
||||
const handleSyncTenantPackage = async (row: TenantVO) => {
|
||||
try {
|
||||
await proxy?.$modal.confirm('是否确认同步租户套餐租户编号为"' + row.tenantId + '"的数据项?');
|
||||
loading.value = true;
|
||||
await syncTenantPackage(row.tenantId, row.packageId);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('同步成功');
|
||||
} catch {
|
||||
return;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'system/tenant/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`tenant_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
/**同步租户字典*/
|
||||
const handleSyncTenantDict = async () => {
|
||||
await proxy?.$modal.confirm('确认要同步所有租户字典吗?');
|
||||
const res = await syncTenantDict();
|
||||
proxy?.$modal.msgSuccess(res.msg);
|
||||
};
|
||||
|
||||
/**同步租户参数配置*/
|
||||
const handleSyncTenantConfig = async () => {
|
||||
await proxy?.$modal.confirm('确认要同步所有租户参数配置吗?');
|
||||
const res = await syncTenantConfig();
|
||||
proxy?.$modal.msgSuccess(res.msg);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="套餐名称" prop="packageName">
|
||||
<el-input v-model="queryParams.packageName" placeholder="请输入套餐名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:tenantPackage:add']" type="primary" plain icon="Plus" @click="handleAdd"> 新增 </el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:tenantPackage:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()">
|
||||
修改
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:tenantPackage:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['system:tenantPackage:export']" type="warning" plain icon="Download" @click="handleExport">导出 </el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="tenantPackageList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="租户套餐id" align="center" prop="packageId" />
|
||||
<el-table-column label="套餐名称" align="center" prop="packageName" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @click="handleStatusChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:tenantPackage:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:tenantPackage:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
|
||||
<!-- 添加或修改租户套餐对话框 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="500px" append-to-body>
|
||||
<el-form ref="tenantPackageFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="套餐名称" prop="packageName">
|
||||
<el-input v-model="form.packageName" placeholder="请输入套餐名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="关联菜单">
|
||||
<el-checkbox v-model="menuExpand" @change="handleCheckedTreeExpand($event, 'menu')">展开/折叠</el-checkbox>
|
||||
<el-checkbox v-model="menuNodeAll" @change="handleCheckedTreeNodeAll($event, 'menu')">全选/全不选 </el-checkbox>
|
||||
<el-checkbox v-model="form.menuCheckStrictly" @change="handleCheckedTreeConnect($event, 'menu')">父子联动 </el-checkbox>
|
||||
<el-tree
|
||||
ref="menuTreeRef"
|
||||
class="tree-border"
|
||||
:data="menuOptions"
|
||||
show-checkbox
|
||||
node-key="id"
|
||||
:check-strictly="!form.menuCheckStrictly"
|
||||
empty-text="加载中,请稍候"
|
||||
:props="{ label: 'label', children: 'children' } as any"
|
||||
></el-tree>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="TenantPackage" lang="ts">
|
||||
import {
|
||||
listTenantPackage,
|
||||
getTenantPackage,
|
||||
delTenantPackage,
|
||||
addTenantPackage,
|
||||
updateTenantPackage,
|
||||
changePackageStatus
|
||||
} from '@/api/system/tenantPackage';
|
||||
import { tenantPackageMenuTreeselect } from '@/api/system/menu';
|
||||
import { TenantPkgForm, TenantPkgQuery, TenantPkgVO } from '@/api/system/tenantPackage/types';
|
||||
import { MenuTreeOption } from '@/api/system/menu/types';
|
||||
import to from 'await-to-js';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const tenantPackageList = ref<TenantPkgVO[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const menuExpand = ref(false);
|
||||
const menuNodeAll = ref(false);
|
||||
const menuOptions = ref<MenuTreeOption[]>([]);
|
||||
|
||||
const menuTreeRef = ref<ElTreeInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const tenantPackageFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: TenantPkgForm = {
|
||||
packageId: undefined,
|
||||
packageName: '',
|
||||
menuIds: '',
|
||||
remark: '',
|
||||
menuCheckStrictly: true
|
||||
};
|
||||
const data = reactive<PageData<TenantPkgForm, TenantPkgQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
packageName: ''
|
||||
},
|
||||
rules: {
|
||||
packageId: [{ required: true, message: '租户套餐id不能为空', trigger: 'blur' }],
|
||||
packageName: [{ required: true, message: '套餐名称不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
// 所有菜单节点数据
|
||||
const getMenuAllCheckedKeys = (): any => {
|
||||
// 目前被选中的菜单节点
|
||||
const checkedKeys = menuTreeRef.value?.getCheckedKeys();
|
||||
// 半选中的菜单节点
|
||||
const halfCheckedKeys = menuTreeRef.value?.getHalfCheckedKeys();
|
||||
if (halfCheckedKeys) {
|
||||
checkedKeys?.unshift(...halfCheckedKeys);
|
||||
}
|
||||
return checkedKeys;
|
||||
};
|
||||
|
||||
/** 根据租户套餐ID查询菜单树结构 */
|
||||
const getPackageMenuTreeselect = async (packageId: string | number) => {
|
||||
const res = await tenantPackageMenuTreeselect(packageId);
|
||||
menuOptions.value = res.data.menus;
|
||||
return Promise.resolve(res);
|
||||
};
|
||||
|
||||
/** 查询租户套餐列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listTenantPackage(queryParams.value);
|
||||
tenantPackageList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
// 租户套餐状态修改
|
||||
const handleStatusChange = async (row: TenantPkgVO) => {
|
||||
const text = row.status === '0' ? '启用' : '停用';
|
||||
const [err] = await to(proxy?.$modal.confirm('确认要"' + text + '""' + row.packageName + '"套餐吗?') as Promise<any>);
|
||||
if (err) {
|
||||
row.status = row.status === '0' ? '1' : '0';
|
||||
} else {
|
||||
await changePackageStatus(row.packageId, row.status);
|
||||
proxy?.$modal.msgSuccess(text + '成功');
|
||||
}
|
||||
};
|
||||
|
||||
// 取消按钮
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
// 表单重置
|
||||
const reset = () => {
|
||||
menuTreeRef.value?.setCheckedKeys([]);
|
||||
menuExpand.value = false;
|
||||
menuNodeAll.value = false;
|
||||
form.value = { ...initFormData };
|
||||
tenantPackageFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: TenantPkgVO[]) => {
|
||||
ids.value = selection.map((item) => item.packageId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
// 树权限(展开/折叠)
|
||||
const handleCheckedTreeExpand = (value: CheckboxValueType, type: string) => {
|
||||
if (type == 'menu') {
|
||||
const treeList = menuOptions.value;
|
||||
for (let i = 0; i < treeList.length; i++) {
|
||||
if (menuTreeRef.value) {
|
||||
menuTreeRef.value.store.nodesMap[treeList[i].id].expanded = value as boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 树权限(全选/全不选)
|
||||
const handleCheckedTreeNodeAll = (value: CheckboxValueType, type: string) => {
|
||||
if (type == 'menu') {
|
||||
menuTreeRef.value?.setCheckedNodes(value ? (menuOptions.value as any) : []);
|
||||
}
|
||||
};
|
||||
|
||||
// 树权限(父子联动)
|
||||
const handleCheckedTreeConnect = (value: CheckboxValueType, type: string) => {
|
||||
if (type == 'menu') {
|
||||
form.value.menuCheckStrictly = value as boolean;
|
||||
}
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = async () => {
|
||||
reset();
|
||||
await getPackageMenuTreeselect(0);
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加租户套餐';
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: TenantPkgVO) => {
|
||||
reset();
|
||||
const _packageId = row?.packageId || ids.value[0];
|
||||
const response = await getTenantPackage(_packageId);
|
||||
form.value = response.data;
|
||||
const res = await getPackageMenuTreeselect(_packageId);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改租户套餐';
|
||||
res.data.checkedKeys.forEach((v) => {
|
||||
nextTick(() => {
|
||||
menuTreeRef.value?.setChecked(v, true, false);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
tenantPackageFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
form.value.menuIds = getMenuAllCheckedKeys();
|
||||
if (form.value.packageId != null) {
|
||||
await updateTenantPackage(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addTenantPackage(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: TenantPkgVO) => {
|
||||
const _packageIds = row?.packageId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除租户套餐编号为"' + _packageIds + '"的数据项?').finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
await delTenantPackage(_packageIds);
|
||||
loading.value = true;
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'system/tenant/package/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`tenantPackage_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<div class="panel">
|
||||
<h4 class="panel-title">基本信息</h4>
|
||||
<el-form :model="form" :inline="true">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="2.5">
|
||||
<el-form-item label="用户昵称" prop="nickName">
|
||||
<el-input v-model="form.nickName" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="2.5">
|
||||
<el-form-item label="登录账号" prop="userName">
|
||||
<el-input v-model="form.userName" disabled />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h4 class="panel-title">角色信息</h4>
|
||||
<div>
|
||||
<el-table
|
||||
ref="tableRef"
|
||||
v-loading="loading"
|
||||
border
|
||||
:row-key="getRowKey"
|
||||
:data="roles.slice((pageNum - 1) * pageSize, pageNum * pageSize)"
|
||||
@row-click="clickRow"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column label="序号" width="55" type="index" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column>
|
||||
<el-table-column label="角色编号" align="center" prop="roleId" />
|
||||
<el-table-column label="角色名称" align="center" prop="roleName" />
|
||||
<el-table-column label="权限字符" align="center" prop="roleKey" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.createTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" v-model:page="pageNum" v-model:limit="pageSize" :total="total" />
|
||||
<div style="text-align: center; margin-left: -120px; margin-top: 30px">
|
||||
<el-button type="primary" @click="submitForm()">提交</el-button>
|
||||
<el-button @click="close()">返回</el-button>
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="AuthRole" lang="ts">
|
||||
import { RoleVO } from '@/api/system/role/types';
|
||||
import { getAuthRole, updateAuthRole } from '@/api/system/user';
|
||||
import { UserForm } from '@/api/system/user/types';
|
||||
import { RouteLocationNormalized } from 'vue-router';
|
||||
import { parseTime } from '@/utils/ruoyi';
|
||||
|
||||
const route = useRoute();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const loading = ref(true);
|
||||
const total = ref(0);
|
||||
const pageNum = ref(1);
|
||||
const pageSize = ref(10);
|
||||
const roleIds = ref<Array<string | number>>([]);
|
||||
const roles = ref<RoleVO[]>([]);
|
||||
const form = ref<Partial<UserForm>>({
|
||||
nickName: undefined,
|
||||
userName: '',
|
||||
userId: undefined
|
||||
});
|
||||
|
||||
const tableRef = ref<ElTableInstance>();
|
||||
|
||||
/** 单击选中行数据 */
|
||||
const clickRow = (row: RoleVO) => {
|
||||
if (checkSelectable(row)) {
|
||||
row.flag = !row.flag;
|
||||
tableRef.value?.toggleRowSelection(row, row.flag);
|
||||
}
|
||||
};
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: RoleVO[]) => {
|
||||
roleIds.value = selection.map((item) => item.roleId);
|
||||
};
|
||||
/** 保存选中的数据编号 */
|
||||
const getRowKey = (row: RoleVO): string => {
|
||||
return String(row.roleId);
|
||||
};
|
||||
/** 检查角色状态 */
|
||||
const checkSelectable = (row: RoleVO): boolean => {
|
||||
return row.status === '0';
|
||||
};
|
||||
/** 关闭按钮 */
|
||||
const close = () => {
|
||||
const obj: RouteLocationNormalized = {
|
||||
fullPath: '',
|
||||
hash: '',
|
||||
matched: [],
|
||||
meta: undefined,
|
||||
name: undefined,
|
||||
params: undefined,
|
||||
query: undefined,
|
||||
redirectedFrom: undefined,
|
||||
path: '/system/user'
|
||||
};
|
||||
proxy?.$tab.closeOpenPage(obj);
|
||||
};
|
||||
/** 提交按钮 */
|
||||
const submitForm = async () => {
|
||||
const userId = form.value.userId;
|
||||
const rIds = roleIds.value.join(',');
|
||||
await updateAuthRole({ userId: userId as string, roleIds: rIds });
|
||||
proxy?.$modal.msgSuccess('授权成功');
|
||||
close();
|
||||
};
|
||||
|
||||
const getList = async () => {
|
||||
const userId = route.params && route.params.userId;
|
||||
if (userId) {
|
||||
loading.value = true;
|
||||
const res = await getAuthRole(userId as string);
|
||||
Object.assign(form.value, res.data.user);
|
||||
Object.assign(roles.value, res.data.roles);
|
||||
total.value = roles.value.length;
|
||||
await nextTick(() => {
|
||||
roles.value.forEach((row) => {
|
||||
if (row?.flag) {
|
||||
tableRef.value?.toggleRowSelection(row, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,679 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-row :gutter="20">
|
||||
<!-- 部门树 -->
|
||||
<el-col :lg="4" :xs="24" style="">
|
||||
<el-card shadow="hover">
|
||||
<el-input v-model="deptName" placeholder="请输入部门名称" prefix-icon="Search" clearable />
|
||||
<el-tree
|
||||
ref="deptTreeRef"
|
||||
class="mt-2"
|
||||
node-key="id"
|
||||
:data="deptOptions"
|
||||
:props="{ label: 'label', children: 'children' } as any"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="filterNode"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="20" :xs="24">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="用户名称" prop="userName">
|
||||
<el-input v-model="queryParams.userName" placeholder="请输入用户名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称" prop="nickName">
|
||||
<el-input v-model="queryParams.nickName" placeholder="请输入用户昵称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="phonenumber">
|
||||
<el-input v-model="queryParams.phonenumber" placeholder="请输入手机号码" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" placeholder="用户状态" clearable>
|
||||
<el-option v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.label" :value="dict.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-has-permi="['system:user:add']" type="primary" plain icon="Plus" @click="handleAdd()">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-has-permi="['system:user:edit']" type="success" plain :disabled="single" icon="Edit" @click="handleUpdate()">
|
||||
修改
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-has-permi="['system:user:remove']" type="danger" plain :disabled="multiple" icon="Delete" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown class="mt-[1px]">
|
||||
<el-button plain type="info">
|
||||
更多
|
||||
<el-icon class="el-icon--right"><arrow-down /></el-icon
|
||||
></el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Download" @click="importTemplate">下载模板</el-dropdown-item>
|
||||
<!-- 注意 由于el-dropdown-item标签是延迟加载的 所以v-has-permi自定义标签不生效 需要使用v-if调用方法执行 -->
|
||||
<el-dropdown-item v-if="checkPermi(['system:user:import'])" icon="Top" @click="handleImport">导入数据</el-dropdown-item>
|
||||
<el-dropdown-item v-if="checkPermi(['system:user:export'])" icon="Download" @click="handleExport">导出数据</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" :columns="columns" :search="true" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="userList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="50" align="center" />
|
||||
<el-table-column v-if="columns[0].visible" key="userId" label="用户编号" align="center" prop="userId" />
|
||||
<el-table-column v-if="columns[1].visible" key="userName" label="用户名称" align="center" prop="userName" :show-overflow-tooltip="true" />
|
||||
<el-table-column v-if="columns[2].visible" key="nickName" label="用户昵称" align="center" prop="nickName" :show-overflow-tooltip="true" />
|
||||
<el-table-column v-if="columns[3].visible" key="deptName" label="部门" align="center" prop="deptName" :show-overflow-tooltip="true" />
|
||||
<el-table-column v-if="columns[4].visible" key="phonenumber" label="手机号码" align="center" prop="phonenumber" width="120" />
|
||||
<el-table-column v-if="columns[5].visible" key="status" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<el-switch v-model="scope.row.status" active-value="0" inactive-value="1" @change="handleStatusChange(scope.row)"></el-switch>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column v-if="columns[6].visible" label="创建时间" align="center" prop="createTime" width="160">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.createTime }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" fixed="right" width="180" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip v-if="scope.row.userId !== 1" content="修改" placement="top">
|
||||
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="Edit" @click="handleUpdate(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="scope.row.userId !== 1" content="删除" placement="top">
|
||||
<el-button v-hasPermi="['system:user:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip v-if="scope.row.userId !== 1" content="重置密码" placement="top">
|
||||
<el-button v-hasPermi="['system:user:resetPwd']" link type="primary" icon="Key" @click="handleResetPwd(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-tooltip v-if="scope.row.userId !== 1" content="分配角色" placement="top">
|
||||
<el-button v-hasPermi="['system:user:edit']" link type="primary" icon="CircleCheck" @click="handleAuthRole(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 添加或修改用户配置对话框 -->
|
||||
<el-dialog ref="formDialogRef" v-model="dialog.visible" :title="dialog.title" width="600px" append-to-body @close="closeDialog">
|
||||
<el-form ref="userFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户昵称" prop="nickName">
|
||||
<el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||
<el-form-item label="归属部门" prop="deptId">
|
||||
<el-tree-select
|
||||
v-model="form.deptId"
|
||||
:data="enabledDeptOptions"
|
||||
:props="{ value: 'id', label: 'label', children: 'children' } as any"
|
||||
value-key="id"
|
||||
placeholder="请选择归属部门"
|
||||
check-strictly
|
||||
@change="handleDeptChange"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机号码" prop="phonenumber">
|
||||
<el-input v-model="form.phonenumber" placeholder="请输入手机号码" maxlength="11" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="form.email" placeholder="请输入邮箱" maxlength="50" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
|
||||
<el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
|
||||
<el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" show-password />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="用户性别">
|
||||
<el-select v-model="form.sex" placeholder="请选择">
|
||||
<el-option v-for="dict in sys_user_sex" :key="dict.value" :label="dict.label" :value="dict.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :value="dict.value">{{ dict.label }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||
<el-form-item label="岗位">
|
||||
<el-select v-model="form.postIds" multiple placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in postOptions"
|
||||
:key="item.postId"
|
||||
:label="item.postName"
|
||||
:value="item.postId"
|
||||
:disabled="item.status == '1'"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="form.userId == null || form.userId != useUserStore().userId">
|
||||
<el-form-item label="角色" prop="roleIds">
|
||||
<el-select v-model="form.roleIds" filterable multiple placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in roleOptions"
|
||||
:key="item.roleId"
|
||||
:label="item.roleName"
|
||||
:value="item.roleId"
|
||||
:disabled="item.status == '1'"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" placeholder="请输入内容"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel()">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 用户导入对话框 -->
|
||||
<el-dialog v-model="upload.open" :title="upload.title" width="400px" append-to-body>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:limit="1"
|
||||
accept=".xlsx, .xls"
|
||||
:headers="upload.headers"
|
||||
:action="upload.url + '?updateSupport=' + upload.updateSupport"
|
||||
:disabled="upload.isUploading"
|
||||
:on-progress="handleFileUploadProgress"
|
||||
:on-success="handleFileSuccess"
|
||||
:auto-upload="false"
|
||||
drag
|
||||
>
|
||||
<el-icon class="el-icon--upload">
|
||||
<i-ep-upload-filled />
|
||||
</el-icon>
|
||||
<div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
||||
<template #tip>
|
||||
<div class="text-center el-upload__tip">
|
||||
<div class="el-upload__tip"><el-checkbox v-model="upload.updateSupport" />是否更新已经存在的用户数据</div>
|
||||
<span>仅允许导入xls、xlsx格式文件。</span>
|
||||
<el-link type="primary" :underline="false" style="font-size: 12px; vertical-align: baseline" @click="importTemplate">下载模板</el-link>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="submitFileForm">确 定</el-button>
|
||||
<el-button @click="upload.open = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="User" lang="ts">
|
||||
import api from '@/api/system/user';
|
||||
import { UserForm, UserQuery, UserVO } from '@/api/system/user/types';
|
||||
import { DeptTreeVO, DeptVO } from '@/api/system/dept/types';
|
||||
import { RoleVO } from '@/api/system/role/types';
|
||||
import { PostVO } from '@/api/system/post/types';
|
||||
import { globalHeaders } from '@/utils/request';
|
||||
import { to } from 'await-to-js';
|
||||
import { optionselect } from '@/api/system/post';
|
||||
import { checkPermi } from '@/utils/permission';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const router = useRouter();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_normal_disable, sys_user_sex } = toRefs<any>(proxy?.useDict('sys_normal_disable', 'sys_user_sex'));
|
||||
const userList = ref<UserVO[]>();
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<number | string>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||
const deptName = ref('');
|
||||
const deptOptions = ref<DeptTreeVO[]>([]);
|
||||
const enabledDeptOptions = ref<DeptTreeVO[]>([]);
|
||||
const initPassword = ref<string>('');
|
||||
const postOptions = ref<PostVO[]>([]);
|
||||
const roleOptions = ref<RoleVO[]>([]);
|
||||
/*** 用户导入参数 */
|
||||
const upload = reactive<ImportOption>({
|
||||
// 是否显示弹出层(用户导入)
|
||||
open: false,
|
||||
// 弹出层标题(用户导入)
|
||||
title: '',
|
||||
// 是否禁用上传
|
||||
isUploading: false,
|
||||
// 是否更新已经存在的用户数据
|
||||
updateSupport: 0,
|
||||
// 设置上传的请求头部
|
||||
headers: globalHeaders(),
|
||||
// 上传的地址
|
||||
url: import.meta.env.VITE_APP_BASE_API + '/system/user/importData'
|
||||
});
|
||||
// 列显隐信息
|
||||
const columns = ref<FieldOption[]>([
|
||||
{ key: 0, label: `用户编号`, visible: false, children: [] },
|
||||
{ key: 1, label: `用户名称`, visible: true, children: [] },
|
||||
{ key: 2, label: `用户昵称`, visible: true, children: [] },
|
||||
{ key: 3, label: `部门`, visible: true, children: [] },
|
||||
{ key: 4, label: `手机号码`, visible: true, children: [] },
|
||||
{ key: 5, label: `状态`, visible: true, children: [] },
|
||||
{ key: 6, label: `创建时间`, visible: true, children: [] }
|
||||
]);
|
||||
|
||||
const deptTreeRef = ref<ElTreeInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const userFormRef = ref<ElFormInstance>();
|
||||
const uploadRef = ref<ElUploadInstance>();
|
||||
const formDialogRef = ref<ElDialogInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: UserForm = {
|
||||
userId: undefined,
|
||||
deptId: undefined,
|
||||
userName: '',
|
||||
nickName: undefined,
|
||||
password: '',
|
||||
phonenumber: undefined,
|
||||
email: undefined,
|
||||
sex: undefined,
|
||||
status: '0',
|
||||
remark: '',
|
||||
postIds: [],
|
||||
roleIds: []
|
||||
};
|
||||
|
||||
const initData: PageData<UserForm, UserQuery> = {
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
userName: '',
|
||||
phonenumber: '',
|
||||
status: '',
|
||||
deptId: '',
|
||||
roleId: ''
|
||||
},
|
||||
rules: {
|
||||
userName: [
|
||||
{ required: true, message: '用户名称不能为空', trigger: 'blur' },
|
||||
{
|
||||
min: 2,
|
||||
max: 20,
|
||||
message: '用户名称长度必须介于 2 和 20 之间',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
|
||||
password: [
|
||||
{ required: true, message: '用户密码不能为空', trigger: 'blur' },
|
||||
{
|
||||
min: 5,
|
||||
max: 20,
|
||||
message: '用户密码长度必须介于 5 和 20 之间',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\ |', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{
|
||||
type: 'email',
|
||||
message: '请输入正确的邮箱地址',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
phonenumber: [
|
||||
{
|
||||
pattern: /^1[3456789][0-9]\d{8}$/,
|
||||
message: '请输入正确的手机号码',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
roleIds: [{ required: true, message: '用户角色不能为空', trigger: 'blur' }]
|
||||
}
|
||||
};
|
||||
const data = reactive<PageData<UserForm, UserQuery>>(initData);
|
||||
|
||||
const { queryParams, form, rules } = toRefs<PageData<UserForm, UserQuery>>(data);
|
||||
|
||||
/** 通过条件过滤节点 */
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true;
|
||||
return data.label.indexOf(value) !== -1;
|
||||
};
|
||||
/** 根据名称筛选部门树 */
|
||||
watchEffect(
|
||||
() => {
|
||||
deptTreeRef.value?.filter(deptName.value);
|
||||
},
|
||||
{
|
||||
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
|
||||
}
|
||||
);
|
||||
|
||||
/** 查询用户列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await api.listUser(proxy?.addDateRange(queryParams.value, dateRange.value));
|
||||
loading.value = false;
|
||||
userList.value = res.rows;
|
||||
total.value = res.total;
|
||||
};
|
||||
|
||||
/** 查询部门下拉树结构 */
|
||||
const getDeptTree = async () => {
|
||||
const res = await api.deptTreeSelect();
|
||||
deptOptions.value = res.data;
|
||||
enabledDeptOptions.value = filterDisabledDept(res.data);
|
||||
};
|
||||
|
||||
/** 过滤禁用的部门 */
|
||||
const filterDisabledDept = (deptList: DeptTreeVO[]) => {
|
||||
return deptList.filter((dept) => {
|
||||
if (dept.disabled) {
|
||||
return false;
|
||||
}
|
||||
if (dept.children && dept.children.length) {
|
||||
dept.children = filterDisabledDept(dept.children);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
/** 节点单击事件 */
|
||||
const handleNodeClick = (data: DeptVO) => {
|
||||
queryParams.value.deptId = data.id;
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = ['', ''];
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.deptId = undefined;
|
||||
deptTreeRef.value?.setCurrentKey(undefined);
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: UserVO) => {
|
||||
const userIds = row?.userId || ids.value;
|
||||
const [err] = await to(proxy?.$modal.confirm('是否确认删除用户编号为"' + userIds + '"的数据项?') as any);
|
||||
if (!err) {
|
||||
await api.delUser(userIds);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
}
|
||||
};
|
||||
|
||||
/** 用户状态修改 */
|
||||
const handleStatusChange = async (row: UserVO) => {
|
||||
const text = row.status === '0' ? '启用' : '停用';
|
||||
try {
|
||||
await proxy?.$modal.confirm('确认要"' + text + '""' + row.userName + '"用户吗?');
|
||||
await api.changeUserStatus(row.userId, row.status);
|
||||
proxy?.$modal.msgSuccess(text + '成功');
|
||||
} catch (err) {
|
||||
row.status = row.status === '0' ? '1' : '0';
|
||||
}
|
||||
};
|
||||
/** 跳转角色分配 */
|
||||
const handleAuthRole = (row: UserVO) => {
|
||||
const userId = row.userId;
|
||||
router.push('/system/user-auth/role/' + userId);
|
||||
};
|
||||
|
||||
/** 重置密码按钮操作 */
|
||||
const handleResetPwd = async (row: UserVO) => {
|
||||
const [err, res] = await to(
|
||||
ElMessageBox.prompt('请输入"' + row.userName + '"的新密码', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
closeOnClickModal: false,
|
||||
inputPattern: /^.{5,20}$/,
|
||||
inputErrorMessage: '用户密码长度必须介于 5 和 20 之间',
|
||||
inputValidator: (value) => {
|
||||
if (/<|>|"|'|\||\\/.test(value)) {
|
||||
return '不能包含非法字符:< > " \' \\ |';
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
if (!err && res) {
|
||||
await api.resetUserPwd(row.userId, res.value);
|
||||
proxy?.$modal.msgSuccess('修改成功,新密码是:' + res.value);
|
||||
}
|
||||
};
|
||||
|
||||
/** 选择条数 */
|
||||
const handleSelectionChange = (selection: UserVO[]) => {
|
||||
ids.value = selection.map((item) => item.userId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 导入按钮操作 */
|
||||
const handleImport = () => {
|
||||
upload.title = '用户导入';
|
||||
upload.open = true;
|
||||
};
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'system/user/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`user_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
/** 下载模板操作 */
|
||||
const importTemplate = () => {
|
||||
proxy?.download('system/user/importTemplate', {}, `user_template_${new Date().getTime()}.xlsx`);
|
||||
};
|
||||
|
||||
/**文件上传中处理 */
|
||||
const handleFileUploadProgress = () => {
|
||||
upload.isUploading = true;
|
||||
};
|
||||
/** 文件上传成功处理 */
|
||||
const handleFileSuccess = (response: any, file: UploadFile) => {
|
||||
upload.open = false;
|
||||
upload.isUploading = false;
|
||||
uploadRef.value?.handleRemove(file);
|
||||
ElMessageBox.alert("<div style='overflow: auto;overflow-x: hidden;max-height: 70vh;padding: 10px 20px 0;'>" + response.msg + '</div>', '导入结果', {
|
||||
dangerouslyUseHTMLString: true
|
||||
});
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 提交上传文件 */
|
||||
function submitFileForm() {
|
||||
uploadRef.value?.submit();
|
||||
}
|
||||
|
||||
/** 重置操作表单 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
userFormRef.value?.resetFields();
|
||||
};
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
dialog.visible = false;
|
||||
reset();
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = async () => {
|
||||
reset();
|
||||
const { data } = await api.getUser();
|
||||
dialog.visible = true;
|
||||
dialog.title = '新增用户';
|
||||
postOptions.value = data.posts;
|
||||
roleOptions.value = data.roles;
|
||||
form.value.password = initPassword.value.toString();
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: UserForm) => {
|
||||
reset();
|
||||
const userId = row?.userId || ids.value[0];
|
||||
const { data } = await api.getUser(userId);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改用户';
|
||||
Object.assign(form.value, data.user);
|
||||
postOptions.value = data.posts;
|
||||
roleOptions.value = Array.from(
|
||||
new Map([...data.roles, ...data.user.roles].map(role => [role.roleId, role])).values()
|
||||
);
|
||||
form.value.postIds = data.postIds;
|
||||
form.value.roleIds = data.roleIds;
|
||||
form.value.password = '';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
userFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
if (form.value.userId) {
|
||||
// 自己编辑自己的情况下 不允许编辑角色部门岗位
|
||||
if (form.value.userId == useUserStore().userId) {
|
||||
form.value.roleIds = null;
|
||||
form.value.deptId = null;
|
||||
form.value.postIds = null;
|
||||
}
|
||||
await api.updateUser(form.value);
|
||||
} else {
|
||||
await api.addUser(form.value);
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 关闭用户弹窗
|
||||
*/
|
||||
const closeDialog = () => {
|
||||
dialog.visible = false;
|
||||
resetForm();
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置表单
|
||||
*/
|
||||
const resetForm = () => {
|
||||
userFormRef.value?.resetFields();
|
||||
userFormRef.value?.clearValidate();
|
||||
|
||||
form.value.id = undefined;
|
||||
form.value.status = '1';
|
||||
};
|
||||
onMounted(() => {
|
||||
getDeptTree(); // 初始化部门数据
|
||||
getList(); // 初始化列表数据
|
||||
proxy?.getConfigKey('sys.user.initPassword').then((response) => {
|
||||
initPassword.value = response.data;
|
||||
});
|
||||
});
|
||||
|
||||
async function handleDeptChange(value: number | string) {
|
||||
const response = await optionselect(value);
|
||||
postOptions.value = response.data;
|
||||
form.value.postIds = [];
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6" :xs="24">
|
||||
<el-card class="box-card">
|
||||
<template #header>
|
||||
<div class="clearfix">
|
||||
<span>个人信息</span>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<div class="text-center">
|
||||
<userAvatar />
|
||||
</div>
|
||||
<ul class="list-group list-group-striped">
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="user" />用户名称
|
||||
<div class="pull-right">{{ state.user.userName }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="phone" />手机号码
|
||||
<div class="pull-right">{{ state.user.phonenumber }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="email" />用户邮箱
|
||||
<div class="pull-right">{{ state.user.email }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="tree" />所属部门
|
||||
<div v-if="state.user.deptName" class="pull-right">{{ state.user.deptName }} / {{ state.postGroup }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="peoples" />所属角色
|
||||
<div class="pull-right">{{ state.roleGroup }}</div>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<svg-icon icon-class="date" />创建日期
|
||||
<div class="pull-right">{{ state.user.createTime }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="18" :xs="24">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="clearfix">
|
||||
<span>基本资料</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="基本资料" name="userinfo">
|
||||
<userInfo :user="userForm" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="修改密码" name="resetPwd">
|
||||
<resetPwd />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="第三方应用" name="thirdParty">
|
||||
<thirdParty :auths="state.auths" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="在线设备" name="onlineDevice">
|
||||
<onlineDevice :devices="state.devices" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Profile" lang="ts">
|
||||
import UserAvatar from './userAvatar.vue';
|
||||
import UserInfo from './userInfo.vue';
|
||||
import ResetPwd from './resetPwd.vue';
|
||||
import ThirdParty from './thirdParty.vue';
|
||||
import OnlineDevice from './onlineDevice.vue';
|
||||
import { getAuthList } from '@/api/system/social/auth';
|
||||
import { getUserProfile } from '@/api/system/user';
|
||||
import { getOnline } from '@/api/monitor/online';
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
|
||||
const activeTab = ref('userinfo');
|
||||
interface State {
|
||||
user: Partial<UserVO>;
|
||||
roleGroup: string;
|
||||
postGroup: string;
|
||||
auths: any;
|
||||
devices: any;
|
||||
}
|
||||
const state = ref<State>({
|
||||
user: {},
|
||||
roleGroup: '',
|
||||
postGroup: '',
|
||||
auths: [],
|
||||
devices: []
|
||||
});
|
||||
|
||||
const userForm = ref({});
|
||||
|
||||
const getUser = async () => {
|
||||
const res = await getUserProfile();
|
||||
state.value.user = res.data.user;
|
||||
userForm.value = { ...res.data.user };
|
||||
state.value.roleGroup = res.data.roleGroup;
|
||||
state.value.postGroup = res.data.postGroup;
|
||||
};
|
||||
|
||||
const getAuths = async () => {
|
||||
const res = await getAuthList();
|
||||
state.value.auths = res.data;
|
||||
};
|
||||
const getOnlines = async () => {
|
||||
const res = await getOnline();
|
||||
state.value.devices = res.rows;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getUser();
|
||||
getAuths();
|
||||
getOnlines();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table :data="devices" border style="width: 100%; height: 100%; font-size: 14px">
|
||||
<el-table-column label="设备类型" align="center">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="sys_device_type" :value="scope.row.deviceType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="主机" align="center" prop="ipaddr" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="登录地点" align="center" prop="loginLocation" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="操作系统" align="center" prop="os" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="浏览器" align="center" prop="browser" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="登录时间" align="center" prop="loginTime" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.loginTime) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button link type="primary" icon="Delete" @click="handldDelOnline(scope.row)"> </el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Online" lang="ts">
|
||||
import { delOnline } from '@/api/monitor/online';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_device_type } = toRefs<any>(proxy?.useDict('sys_device_type'));
|
||||
|
||||
const props = defineProps({
|
||||
devices: propTypes.any.isRequired
|
||||
});
|
||||
const devices = computed(() => props.devices);
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handldDelOnline = (row: any) => {
|
||||
ElMessageBox.confirm('删除设备后,在该设备登录需要重新进行验证')
|
||||
.then(() => {
|
||||
return delOnline(row.tokenId);
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (res.code === 200) {
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
proxy?.$tab.refreshPage();
|
||||
} else {
|
||||
proxy?.$modal.msgError(res.msg);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<el-form ref="pwdRef" :model="user" :rules="rules" label-width="80px">
|
||||
<el-form-item label="旧密码" prop="oldPassword">
|
||||
<el-input v-model="user.oldPassword" placeholder="请输入旧密码" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="newPassword">
|
||||
<el-input v-model="user.newPassword" placeholder="请输入新密码" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认密码" prop="confirmPassword">
|
||||
<el-input v-model="user.confirmPassword" placeholder="请确认新密码" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submit">保存</el-button>
|
||||
<el-button type="danger" @click="close">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { updateUserPwd } from '@/api/system/user';
|
||||
import type { ResetPwdForm } from '@/api/system/user/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const pwdRef = ref<ElFormInstance>();
|
||||
const user = ref<ResetPwdForm>({
|
||||
oldPassword: '',
|
||||
newPassword: '',
|
||||
confirmPassword: ''
|
||||
});
|
||||
|
||||
const equalToPassword = (rule: any, value: string, callback: any) => {
|
||||
if (user.value.newPassword !== value) {
|
||||
callback(new Error('两次输入的密码不一致'));
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
};
|
||||
const rules = ref({
|
||||
oldPassword: [{ required: true, message: '旧密码不能为空', trigger: 'blur' }],
|
||||
newPassword: [
|
||||
{ required: true, message: '新密码不能为空', trigger: 'blur' },
|
||||
{
|
||||
min: 6,
|
||||
max: 20,
|
||||
message: '长度在 6 到 20 个字符',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{ pattern: /^[^<>"'|\\]+$/, message: '不能包含非法字符:< > " \' \\ |', trigger: 'blur' }
|
||||
],
|
||||
confirmPassword: [
|
||||
{ required: true, message: '确认密码不能为空', trigger: 'blur' },
|
||||
{
|
||||
required: true,
|
||||
validator: equalToPassword,
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
/** 提交按钮 */
|
||||
const submit = () => {
|
||||
pwdRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
await updateUserPwd(user.value.oldPassword, user.value.newPassword);
|
||||
proxy?.$modal.msgSuccess('修改成功');
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 关闭按钮 */
|
||||
const close = () => {
|
||||
proxy?.$tab.closePage();
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-table :data="auths" border style="width: 100%; height: 100%; font-size: 14px">
|
||||
<el-table-column label="序号" width="50" type="index" />
|
||||
<el-table-column label="绑定账号平台" width="140" align="center" prop="source" show-overflow-tooltip />
|
||||
<el-table-column label="头像" width="120" align="center" prop="avatar">
|
||||
<template #default="scope">
|
||||
<img :src="scope.row.avatar" style="width: 45px; height: 45px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="系统账号" width="180" align="center" prop="userName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="绑定时间" width="180" align="center" prop="createTime" />
|
||||
<el-table-column label="操作" width="80" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="text" @click="unlockAuth(scope.row)">解绑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div id="git-user-binding">
|
||||
<h4 class="provider-desc">你可以绑定以下第三方帐号</h4>
|
||||
<div id="authlist" class="user-bind">
|
||||
<a class="third-app" href="#" title="使用 微信 账号授权登录" @click="authUrl('wechat')">
|
||||
<div class="git-other-login-icon">
|
||||
<svg-icon icon-class="wechat" />
|
||||
</div>
|
||||
<span class="app-name">WeiXin</span>
|
||||
</a>
|
||||
<a class="third-app" href="#" title="使用 MaxKey 账号授权登录" @click="authUrl('maxkey')">
|
||||
<div class="git-other-login-icon">
|
||||
<svg-icon icon-class="maxkey" />
|
||||
</div>
|
||||
<span class="app-name">MaxKey</span>
|
||||
</a>
|
||||
<a class="third-app" href="#" title="使用 TopIam 账号授权登录" @click="authUrl('topiam')">
|
||||
<div class="git-other-login-icon">
|
||||
<svg-icon icon-class="topiam" />
|
||||
</div>
|
||||
<span class="app-name">TopIam</span>
|
||||
</a>
|
||||
<a class="third-app" href="#" title="使用 Gitee 账号授权登录" @click="authUrl('gitee')">
|
||||
<div class="git-other-login-icon">
|
||||
<svg-icon icon-class="gitee" />
|
||||
</div>
|
||||
<span class="app-name">Gitee</span>
|
||||
</a>
|
||||
<a class="third-app" href="#" title="使用 GitHub 账号授权登录" @click="authUrl('github')">
|
||||
<div class="git-other-login-icon">
|
||||
<svg-icon icon-class="github" />
|
||||
</div>
|
||||
<span class="app-name">Github</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { authUnlock, authRouterUrl } from '@/api/system/social/auth';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const props = defineProps({
|
||||
auths: propTypes.any.isRequired
|
||||
});
|
||||
const auths = computed(() => props.auths);
|
||||
|
||||
const unlockAuth = (row: any) => {
|
||||
ElMessageBox.confirm('您确定要解除"' + row.source + '"的账号绑定吗?')
|
||||
.then(() => {
|
||||
return authUnlock(row.id);
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (res.code === 200) {
|
||||
proxy?.$modal.msgSuccess('解绑成功');
|
||||
proxy?.$tab.refreshPage();
|
||||
} else {
|
||||
proxy?.$modal.msgError(res.msg);
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
const authUrl = (source: string) => {
|
||||
authRouterUrl(source, useUserStore().tenantId).then((res: any) => {
|
||||
if (res.code === 200) {
|
||||
window.location.href = res.data;
|
||||
} else {
|
||||
proxy?.$modal.msgError(res.msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-bind .third-app {
|
||||
display: -webkit-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-box-direction: normal;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
-webkit-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
align-items: center;
|
||||
min-width: 80px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.user-bind {
|
||||
font-size: 1rem;
|
||||
text-align: start;
|
||||
height: 50px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.git-other-login-icon > img {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: #005980;
|
||||
}
|
||||
|
||||
.provider-desc {
|
||||
font-family:
|
||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Liberation Sans',
|
||||
'PingFang SC', 'Microsoft YaHei', 'Hiragino Sans GB', 'Wenquanyi Micro Hei', 'WenQuanYi Zen Hei', 'ST Heiti', SimHei, SimSun,
|
||||
'WenQuanYi Zen Hei Sharp', sans-serif;
|
||||
font-size: 1.071rem;
|
||||
}
|
||||
|
||||
td > img {
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,182 @@
|
||||
<template>
|
||||
<div class="user-info-head" @click="editCropper()">
|
||||
<img :src="options.img" title="点击上传头像" class="img-circle img-lg" />
|
||||
<el-dialog v-model="open" :title="title" width="800px" append-to-body @opened="modalOpened" @close="closeDialog">
|
||||
<el-row>
|
||||
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
||||
<vue-cropper
|
||||
v-if="visible"
|
||||
ref="cropper"
|
||||
:img="options.img"
|
||||
:info="true"
|
||||
:auto-crop="options.autoCrop"
|
||||
:auto-crop-width="options.autoCropWidth"
|
||||
:auto-crop-height="options.autoCropHeight"
|
||||
:fixed-box="options.fixedBox"
|
||||
:output-type="options.outputType"
|
||||
@real-time="realTime"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :xs="24" :md="12" :style="{ height: '350px' }">
|
||||
<div class="avatar-upload-preview">
|
||||
<img :src="options.previews.url" :style="options.previews.img" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<br />
|
||||
<el-row>
|
||||
<el-col :lg="2" :md="2">
|
||||
<el-upload action="#" :http-request="requestUpload" :show-file-list="false" :before-upload="beforeUpload">
|
||||
<el-button>
|
||||
选择
|
||||
<el-icon class="el-icon--right">
|
||||
<Upload />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 2 }" :md="2">
|
||||
<el-button icon="Plus" @click="changeScale(1)"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button icon="Minus" @click="changeScale(-1)"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button icon="RefreshLeft" @click="rotateLeft()"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 1, offset: 1 }" :md="2">
|
||||
<el-button icon="RefreshRight" @click="rotateRight()"></el-button>
|
||||
</el-col>
|
||||
<el-col :lg="{ span: 2, offset: 6 }" :md="2">
|
||||
<el-button type="primary" @click="uploadImg()">提 交</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import 'vue-cropper/dist/index.css';
|
||||
import { VueCropper } from 'vue-cropper';
|
||||
import { uploadAvatar } from '@/api/system/user';
|
||||
import { useUserStore } from '@/store/modules/user';
|
||||
import { UploadRawFile } from 'element-plus';
|
||||
|
||||
interface Options {
|
||||
img: string | any; // 裁剪图片的地址
|
||||
autoCrop: boolean; // 是否默认生成截图框
|
||||
autoCropWidth: number; // 默认生成截图框宽度
|
||||
autoCropHeight: number; // 默认生成截图框高度
|
||||
fixedBox: boolean; // 固定截图框大小 不允许改变
|
||||
fileName: string;
|
||||
previews: any; // 预览数据
|
||||
outputType: string;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const open = ref(false);
|
||||
const visible = ref(false);
|
||||
const title = ref('修改头像');
|
||||
|
||||
const cropper = ref<any>({});
|
||||
//图片裁剪数据
|
||||
const options = reactive<Options>({
|
||||
img: userStore.avatar,
|
||||
autoCrop: true,
|
||||
autoCropWidth: 200,
|
||||
autoCropHeight: 200,
|
||||
fixedBox: true,
|
||||
outputType: 'png',
|
||||
fileName: '',
|
||||
previews: {},
|
||||
visible: false
|
||||
});
|
||||
|
||||
/** 编辑头像 */
|
||||
const editCropper = () => {
|
||||
open.value = true;
|
||||
};
|
||||
/** 打开弹出层结束时的回调 */
|
||||
const modalOpened = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
/** 覆盖默认上传行为 */
|
||||
const requestUpload = (): any => {};
|
||||
/** 向左旋转 */
|
||||
const rotateLeft = () => {
|
||||
cropper.value.rotateLeft();
|
||||
};
|
||||
/** 向右旋转 */
|
||||
const rotateRight = () => {
|
||||
cropper.value.rotateRight();
|
||||
};
|
||||
/** 图片缩放 */
|
||||
const changeScale = (num: number) => {
|
||||
num = num || 1;
|
||||
cropper.value.changeScale(num);
|
||||
};
|
||||
/** 上传预处理 */
|
||||
const beforeUpload = (file: UploadRawFile): any => {
|
||||
if (file.type.indexOf('image/') == -1) {
|
||||
proxy?.$modal.msgError('文件格式错误,请上传图片类型,如:JPG,PNG后缀的文件。');
|
||||
} else {
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = () => {
|
||||
options.img = reader.result;
|
||||
options.fileName = file.name;
|
||||
};
|
||||
}
|
||||
};
|
||||
/** 上传图片 */
|
||||
const uploadImg = async () => {
|
||||
cropper.value.getCropBlob(async (data: any) => {
|
||||
const formData = new FormData();
|
||||
formData.append('avatarfile', data, options.fileName);
|
||||
const res = await uploadAvatar(formData);
|
||||
open.value = false;
|
||||
options.img = res.data.imgUrl;
|
||||
userStore.setAvatar(options.img);
|
||||
proxy?.$modal.msgSuccess('修改成功');
|
||||
visible.value = false;
|
||||
});
|
||||
};
|
||||
/** 实时预览 */
|
||||
const realTime = (data: any) => {
|
||||
options.previews = data;
|
||||
};
|
||||
/** 关闭窗口 */
|
||||
const closeDialog = () => {
|
||||
options.img = userStore.avatar;
|
||||
options.visible = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.user-info-head {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.user-info-head:hover:after {
|
||||
content: '+';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
color: #eee;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
font-size: 24px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
cursor: pointer;
|
||||
line-height: 110px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<el-form ref="userRef" :model="userForm" :rules="rules" label-width="80px">
|
||||
<el-form-item label="用户昵称" prop="nickName">
|
||||
<el-input v-model="userForm.nickName" maxlength="30" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号码" prop="phonenumber">
|
||||
<el-input v-model="userForm.phonenumber" maxlength="11" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="userForm.email" maxlength="50" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别">
|
||||
<el-radio-group v-model="userForm.sex">
|
||||
<el-radio value="0">男</el-radio>
|
||||
<el-radio value="1">女</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submit">保存</el-button>
|
||||
<el-button type="danger" @click="close">关闭</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { updateUserProfile } from '@/api/system/user';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
|
||||
const props = defineProps({
|
||||
user: propTypes.any.isRequired
|
||||
});
|
||||
const userForm = computed(() => props.user);
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const userRef = ref<ElFormInstance>();
|
||||
const rule: ElFormRules = {
|
||||
nickName: [{ required: true, message: '用户昵称不能为空', trigger: 'blur' }],
|
||||
email: [
|
||||
{ required: true, message: '邮箱地址不能为空', trigger: 'blur' },
|
||||
{
|
||||
type: 'email',
|
||||
message: '请输入正确的邮箱地址',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
phonenumber: [
|
||||
{
|
||||
required: true,
|
||||
message: '手机号码不能为空',
|
||||
trigger: 'blur'
|
||||
},
|
||||
{ pattern: /^1[3456789][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
|
||||
]
|
||||
};
|
||||
const rules = ref<ElFormRules>(rule);
|
||||
|
||||
/** 提交按钮 */
|
||||
const submit = () => {
|
||||
userRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
await updateUserProfile(props.user);
|
||||
proxy?.$modal.msgSuccess('修改成功');
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 关闭按钮 */
|
||||
const close = () => {
|
||||
proxy?.$tab.closePage();
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<el-form ref="basicInfoForm" :model="infoForm" :rules="rules" label-width="150px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="表名称" prop="tableName">
|
||||
<el-input v-model="infoForm.tableName" placeholder="请输入仓库名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="表描述" prop="tableComment">
|
||||
<el-input v-model="infoForm.tableComment" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="实体类名称" prop="className">
|
||||
<el-input v-model="infoForm.className" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="作者" prop="functionAuthor">
|
||||
<el-input v-model="infoForm.functionAuthor" placeholder="请输入" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="infoForm.remark" type="textarea" :rows="3"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
|
||||
const prop = defineProps({
|
||||
info: propTypes.any.def({})
|
||||
});
|
||||
|
||||
const infoForm = computed(() => prop.info);
|
||||
|
||||
// 表单校验
|
||||
const rules = ref({
|
||||
tableName: [{ required: true, message: '请输入表名称', trigger: 'blur' }],
|
||||
tableComment: [{ required: true, message: '请输入表描述', trigger: 'blur' }],
|
||||
className: [{ required: true, message: '请输入实体类名称', trigger: 'blur' }],
|
||||
functionAuthor: [{ required: true, message: '请输入作者', trigger: 'blur' }]
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane label="基本信息" name="basic">
|
||||
<basic-info-form ref="basicInfo" :info="info" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="字段信息" name="columnInfo">
|
||||
<el-table ref="dragTable" border :data="columns" row-key="columnId" :max-height="tableHeight">
|
||||
<el-table-column label="序号" type="index" min-width="5%" />
|
||||
<el-table-column label="字段列名" prop="columnName" min-width="10%" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="字段描述" min-width="10%">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.columnComment"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物理类型" prop="columnType" min-width="10%" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="Java类型" min-width="11%">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.javaType">
|
||||
<el-option label="Long" value="Long" />
|
||||
<el-option label="String" value="String" />
|
||||
<el-option label="Integer" value="Integer" />
|
||||
<el-option label="Double" value="Double" />
|
||||
<el-option label="BigDecimal" value="BigDecimal" />
|
||||
<el-option label="Date" value="Date" />
|
||||
<el-option label="Boolean" value="Boolean" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="java属性" min-width="10%">
|
||||
<template #default="scope">
|
||||
<el-input v-model="scope.row.javaField"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="插入" min-width="5%">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.isInsert" true-value="1" false-value="0"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="编辑" min-width="5%">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.isEdit" true-value="1" false-value="0"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="列表" min-width="5%">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.isList" true-value="1" false-value="0"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="查询" min-width="5%">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.isQuery" true-value="1" false-value="0"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="查询方式" min-width="10%">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.queryType">
|
||||
<el-option label="=" value="EQ" />
|
||||
<el-option label="!=" value="NE" />
|
||||
<el-option label=">" value="GT" />
|
||||
<el-option label=">=" value="GE" />
|
||||
<el-option label="<" value="LT" />
|
||||
<el-option label="<=" value="LE" />
|
||||
<el-option label="LIKE" value="LIKE" />
|
||||
<el-option label="BETWEEN" value="BETWEEN" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="必填" min-width="5%">
|
||||
<template #default="scope">
|
||||
<el-checkbox v-model="scope.row.isRequired" true-value="1" false-value="0"></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="显示类型" min-width="12%">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.htmlType">
|
||||
<el-option label="文本框" value="input" />
|
||||
<el-option label="文本域" value="textarea" />
|
||||
<el-option label="下拉框" value="select" />
|
||||
<el-option label="单选框" value="radio" />
|
||||
<el-option label="复选框" value="checkbox" />
|
||||
<el-option label="日期控件" value="datetime" />
|
||||
<el-option label="图片上传" value="imageUpload" />
|
||||
<el-option label="文件上传" value="fileUpload" />
|
||||
<el-option label="富文本控件" value="editor" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="字典类型" min-width="12%">
|
||||
<template #default="scope">
|
||||
<el-select v-model="scope.row.dictType" clearable filterable placeholder="请选择" value-on-clear="">
|
||||
<el-option v-for="dict in dictOptions" :key="dict.dictType" :label="dict.dictName" :value="dict.dictType">
|
||||
<span style="float: left">{{ dict.dictName }}</span>
|
||||
<span style="float: right; color: #8492a6; font-size: 13px">{{ dict.dictType }}</span>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="生成信息" name="genInfo">
|
||||
<gen-info-form ref="genInfo" :info="info" :tables="tables" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form label-width="100px">
|
||||
<div style="text-align: center; margin-left: -100px; margin-top: 10px">
|
||||
<el-button type="primary" @click="submitForm()">提交</el-button>
|
||||
<el-button @click="close()">返回</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup name="GenEdit" lang="ts">
|
||||
import { getGenTable, updateGenTable } from '@/api/tool/gen';
|
||||
import { DbColumnVO, DbTableVO } from '@/api/tool/gen/types';
|
||||
import { optionselect as getDictOptionselect } from '@/api/system/dict/type';
|
||||
import { DictTypeVO } from '@/api/system/dict/type/types';
|
||||
import BasicInfoForm from './basicInfoForm.vue';
|
||||
import GenInfoForm from './genInfoForm.vue';
|
||||
import { RouteLocationNormalized } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const activeName = ref('columnInfo');
|
||||
const tableHeight = ref(document.documentElement.scrollHeight - 245 + 'px');
|
||||
const tables = ref<DbTableVO[]>([]);
|
||||
const columns = ref<DbColumnVO[]>([]);
|
||||
const dictOptions = ref<DictTypeVO[]>([]);
|
||||
const info = ref<Partial<DbTableVO>>({});
|
||||
|
||||
const basicInfo = ref<InstanceType<typeof BasicInfoForm>>();
|
||||
const genInfo = ref<InstanceType<typeof GenInfoForm>>();
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
const basicForm = basicInfo.value?.$refs.basicInfoForm;
|
||||
const genForm = genInfo.value?.$refs.genInfoForm;
|
||||
|
||||
Promise.all([basicForm, genForm].map(getFormPromise)).then(async (res) => {
|
||||
const validateResult = res.every((item) => !!item);
|
||||
if (validateResult) {
|
||||
const genTable: any = Object.assign({}, info.value);
|
||||
genTable.columns = columns.value;
|
||||
genTable.params = {
|
||||
treeCode: info.value?.treeCode,
|
||||
treeName: info.value.treeName,
|
||||
treeParentCode: info.value.treeParentCode,
|
||||
parentMenuId: info.value.parentMenuId
|
||||
};
|
||||
const response = await updateGenTable(genTable);
|
||||
proxy?.$modal.msgSuccess(response.msg);
|
||||
if (response.code === 200) {
|
||||
close();
|
||||
}
|
||||
} else {
|
||||
proxy?.$modal.msgError('表单校验未通过,请重新检查提交内容');
|
||||
}
|
||||
});
|
||||
};
|
||||
const getFormPromise = (form: any) => {
|
||||
return new Promise((resolve) => {
|
||||
form.validate((res: any) => {
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
};
|
||||
const close = () => {
|
||||
const obj: RouteLocationNormalized = {
|
||||
path: '/tool/gen',
|
||||
fullPath: '',
|
||||
hash: '',
|
||||
matched: [],
|
||||
meta: undefined,
|
||||
name: undefined,
|
||||
params: undefined,
|
||||
redirectedFrom: undefined,
|
||||
query: { t: Date.now().toString(), pageNum: route.query.pageNum }
|
||||
};
|
||||
proxy?.$tab.closeOpenPage(obj);
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const tableId = route.params && (route.params.tableId as string);
|
||||
if (tableId) {
|
||||
// 获取表详细信息
|
||||
const res = await getGenTable(tableId);
|
||||
columns.value = res.data.rows;
|
||||
info.value = res.data.info;
|
||||
tables.value = res.data.tables;
|
||||
/** 查询字典下拉列表 */
|
||||
const response = await getDictOptionselect();
|
||||
dictOptions.value = response.data;
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<el-form ref="genInfoForm" :model="infoForm" :rules="rules" label-width="150px">
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="tplCategory">
|
||||
<template #label>生成模板</template>
|
||||
<el-select v-model="infoForm.tplCategory" @change="tplSelectChange">
|
||||
<el-option label="单表(增删改查)" value="crud" />
|
||||
<el-option label="树表(增删改查)" value="tree" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="packageName">
|
||||
<template #label>
|
||||
生成包路径
|
||||
<el-tooltip content="生成在哪个java包下,例如 com.ruoyi.system" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model="infoForm.packageName" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="moduleName">
|
||||
<template #label>
|
||||
生成模块名
|
||||
<el-tooltip content="可理解为子系统名,例如 system" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model="infoForm.moduleName" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="businessName">
|
||||
<template #label>
|
||||
生成业务名
|
||||
<el-tooltip content="可理解为功能英文名,例如 user" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model="infoForm.businessName" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item prop="functionName">
|
||||
<template #label>
|
||||
生成功能名
|
||||
<el-tooltip content="用作类描述,例如 用户" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-input v-model="infoForm.functionName" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
上级菜单
|
||||
<el-tooltip content="分配到指定菜单下,例如 系统管理" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-tree-select
|
||||
v-model="infoForm.parentMenuId"
|
||||
:data="menuOptions"
|
||||
:props="{ value: 'menuId', label: 'menuName', children: 'children' } as any"
|
||||
value-key="menuId"
|
||||
node-key="menuId"
|
||||
placeholder="选择上级菜单"
|
||||
check-strictly
|
||||
filterable
|
||||
clearable
|
||||
highlight-current
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
|
||||
<template v-if="info.tplCategory == 'tree'">
|
||||
<h4 class="form-header">其他信息</h4>
|
||||
<el-row v-show="info.tplCategory == 'tree'">
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
树编码字段
|
||||
<el-tooltip content="树显示的编码字段名, 如:dept_id" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-select v-model="infoForm.treeCode" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="(column, index) in info.columns"
|
||||
:key="index"
|
||||
:label="column.columnName + ':' + column.columnComment"
|
||||
:value="column.columnName"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
树父编码字段
|
||||
<el-tooltip content="树显示的父编码字段名, 如:parent_Id" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-select v-model="infoForm.treeParentCode" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="(column, index) in infoForm.columns"
|
||||
:key="index"
|
||||
:label="column.columnName + ':' + column.columnComment"
|
||||
:value="column.columnName"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
树名称字段
|
||||
<el-tooltip content="树节点的显示名称字段名, 如:dept_name" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-select v-model="infoForm.treeName" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="(column, index) in info.columns"
|
||||
:key="index"
|
||||
:label="column.columnName + ':' + column.columnComment"
|
||||
:value="column.columnName"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<template v-if="info.tplCategory == 'sub'">
|
||||
<h4 class="form-header">关联信息</h4>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
关联子表的表名
|
||||
<el-tooltip content="关联子表的表名, 如:sys_user" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-select v-model="infoForm.subTableName" placeholder="请选择" @change="subSelectChange">
|
||||
<el-option v-for="(t, index) in table" :key="index" :label="t.tableName + ':' + t.tableComment" :value="t.tableName"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
子表关联的外键名
|
||||
<el-tooltip content="子表关联的外键名, 如:user_id" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-select v-model="infoForm.subTableFkName" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="(column, index) in subColumns"
|
||||
:key="index"
|
||||
:label="column.columnName + ':' + column.columnComment"
|
||||
:value="column.columnName"
|
||||
></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listMenu } from '@/api/system/menu';
|
||||
import { propTypes } from '@/utils/propTypes';
|
||||
|
||||
interface MenuOptionsType {
|
||||
menuId: number | string;
|
||||
menuName: string;
|
||||
children?: MenuOptionsType[];
|
||||
}
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const subColumns = ref<any>([]);
|
||||
const menuOptions = ref<Array<MenuOptionsType>>([]);
|
||||
|
||||
const props = defineProps({
|
||||
info: propTypes.any.isRequired,
|
||||
tables: propTypes.any.isRequired
|
||||
});
|
||||
|
||||
const infoForm = computed(() => props.info);
|
||||
|
||||
const table = computed(() => props.tables);
|
||||
|
||||
// 表单校验
|
||||
const rules = ref({
|
||||
tplCategory: [{ required: true, message: '请选择生成模板', trigger: 'blur' }],
|
||||
packageName: [{ required: true, message: '请输入生成包路径', trigger: 'blur' }],
|
||||
moduleName: [{ required: true, message: '请输入生成模块名', trigger: 'blur' }],
|
||||
businessName: [{ required: true, message: '请输入生成业务名', trigger: 'blur' }],
|
||||
functionName: [{ required: true, message: '请输入生成功能名', trigger: 'blur' }]
|
||||
});
|
||||
const subSelectChange = () => {
|
||||
infoForm.value.subTableFkName = '';
|
||||
};
|
||||
const tplSelectChange = (value: string) => {
|
||||
if (value !== 'sub') {
|
||||
infoForm.value.subTableName = '';
|
||||
infoForm.value.subTableFkName = '';
|
||||
}
|
||||
};
|
||||
const setSubTableColumns = (value: string) => {
|
||||
table.value.forEach((item: any) => {
|
||||
const name = item.tableName;
|
||||
if (value === name) {
|
||||
subColumns.value = item.columns;
|
||||
return;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 查询菜单下拉树结构 */
|
||||
const getMenuTreeselect = async () => {
|
||||
const res = await listMenu();
|
||||
const data = proxy?.handleTree<MenuOptionsType>(res.data, 'menuId');
|
||||
|
||||
if (data) {
|
||||
menuOptions.value = data;
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.info.subTableName,
|
||||
(val) => {
|
||||
setSubTableColumns(val);
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getMenuTreeselect();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<!-- 导入表 -->
|
||||
<el-dialog v-model="visible" title="导入表" width="1100px" top="5vh" append-to-body>
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="数据源" prop="dataName">
|
||||
<el-select v-model="queryParams.dataName" filterable placeholder="请选择/输入数据源名称">
|
||||
<el-option v-for="item in dataNameList" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="表名称" prop="tableName">
|
||||
<el-input v-model="queryParams.tableName" placeholder="请输入表名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="表描述" prop="tableComment">
|
||||
<el-input v-model="queryParams.tableComment" placeholder="请输入表描述" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-row>
|
||||
<el-table ref="tableRef" border :data="dbTableList" height="260px" @row-click="clickRow" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55"></el-table-column>
|
||||
<el-table-column prop="tableName" label="表名称" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="tableComment" label="表描述" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间"></el-table-column>
|
||||
<el-table-column prop="updateTime" label="更新时间"></el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-row>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button type="primary" @click="handleImportTable">确 定</el-button>
|
||||
<el-button @click="visible = false">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { listDbTable, importTable, getDataNames } from '@/api/tool/gen';
|
||||
import { DbTableQuery, DbTableVO } from '@/api/tool/gen/types';
|
||||
|
||||
const total = ref(0);
|
||||
const visible = ref(false);
|
||||
const tables = ref<Array<string>>([]);
|
||||
const dbTableList = ref<Array<DbTableVO>>([]);
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const tableRef = ref<ElTableInstance>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const queryParams = reactive<DbTableQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
dataName: '',
|
||||
tableName: '',
|
||||
tableComment: ''
|
||||
});
|
||||
const dataNameList = ref<Array<string>>([]);
|
||||
|
||||
const emit = defineEmits(['ok']);
|
||||
|
||||
/** 查询参数列表 */
|
||||
const show = (dataName: string) => {
|
||||
getDataNames().then((res) => {
|
||||
if (res.code == 200) {
|
||||
dataNameList.value = res.data;
|
||||
if (dataName) {
|
||||
queryParams.dataName = dataName;
|
||||
} else {
|
||||
queryParams.dataName = dataNameList.value[0];
|
||||
}
|
||||
getList();
|
||||
visible.value = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 单击选择行 */
|
||||
const clickRow = (row: DbTableVO) => {
|
||||
// ele bug
|
||||
tableRef.value?.toggleRowSelection(row, false);
|
||||
};
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: DbTableVO[]) => {
|
||||
tables.value = selection.map((item) => item.tableName);
|
||||
};
|
||||
/** 查询表数据 */
|
||||
const getList = async () => {
|
||||
const res = await listDbTable(queryParams);
|
||||
dbTableList.value = res.rows;
|
||||
total.value = res.total;
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
/** 导入按钮操作 */
|
||||
const handleImportTable = async () => {
|
||||
const tableNames = tables.value.join(',');
|
||||
if (tableNames == '') {
|
||||
proxy?.$modal.msgError('请选择要导入的表');
|
||||
return;
|
||||
}
|
||||
const res = await importTable({ tables: tableNames, dataName: queryParams.dataName });
|
||||
proxy?.$modal.msgSuccess(res.msg);
|
||||
if (res.code === 200) {
|
||||
visible.value = false;
|
||||
emit('ok');
|
||||
}
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
show
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="数据源" prop="dataName">
|
||||
<el-select v-model="queryParams.dataName" filterable clearable placeholder="请选择/输入数据源名称">
|
||||
<el-option key="" label="全部" value="" />
|
||||
<el-option v-for="item in dataNameList" :key="item" :label="item" :value="item"> </el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="表名称" prop="tableName">
|
||||
<el-input v-model="queryParams.tableName" placeholder="请输入表名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="表描述" prop="tableComment">
|
||||
<el-input v-model="queryParams.tableComment" placeholder="请输入表描述" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" style="width: 308px">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
value-format="YYYY-MM-DD"
|
||||
type="daterange"
|
||||
range-separator="-"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
></el-date-picker>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['tool:gen:code']" type="primary" plain icon="Download" @click="handleGenTable()">生成</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['tool:gen:import']" type="info" plain icon="Upload" @click="openImportTable">导入</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['tool:gen:edit']" type="success" plain icon="Edit" :disabled="single" @click="handleEditTable()">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['tool:gen:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()">
|
||||
删除
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="tableList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" align="center" width="55"></el-table-column>
|
||||
<el-table-column label="序号" type="index" width="50" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数据源" align="center" prop="dataName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="表名称" align="center" prop="tableName" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="表描述" align="center" prop="tableComment" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="实体" align="center" prop="className" :show-overflow-tooltip="true" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="160" />
|
||||
<el-table-column label="更新时间" align="center" prop="updateTime" width="160" />
|
||||
<el-table-column label="操作" align="center" width="330" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="预览" placement="top">
|
||||
<el-button v-hasPermi="['tool:gen:preview']" link type="primary" icon="View" @click="handlePreview(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="编辑" placement="top">
|
||||
<el-button v-hasPermi="['tool:gen:edit']" link type="primary" icon="Edit" @click="handleEditTable(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button v-hasPermi="['tool:gen:remove']" link type="primary" icon="Delete" @click="handleDelete(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="同步" placement="top">
|
||||
<el-button v-hasPermi="['tool:gen:edit']" link type="primary" icon="Refresh" @click="handleSynchDb(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="生成代码" placement="top">
|
||||
<el-button v-hasPermi="['tool:gen:code']" link type="primary" icon="Download" @click="handleGenTable(scope.row)"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
|
||||
<!-- 预览界面 -->
|
||||
<el-dialog v-model="dialog.visible" :title="dialog.title" width="80%" top="5vh" append-to-body class="scrollbar">
|
||||
<el-tabs v-model="preview.activeName">
|
||||
<el-tab-pane
|
||||
v-for="(value, key) in preview.data"
|
||||
:key="value"
|
||||
:label="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
|
||||
:name="key.substring(key.lastIndexOf('/') + 1, key.indexOf('.vm'))"
|
||||
>
|
||||
<el-link v-copyText="value" v-copyText:callback="copyTextSuccess" :underline="false" icon="DocumentCopy" style="float: right">
|
||||
复制
|
||||
</el-link>
|
||||
<highlightjs :code="value" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
<import-table ref="importRef" @ok="handleQuery" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Gen" lang="ts">
|
||||
import { delTable, getDataNames, listTable, previewTable, synchDb } from '@/api/tool/gen';
|
||||
import { TableQuery, TableVO } from '@/api/tool/gen/types';
|
||||
import router from '@/router';
|
||||
import ImportTable from './importTable.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const tableList = ref<TableVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const dateRange = ref<[DateModelType, DateModelType]>(['', '']);
|
||||
const uniqueId = ref('');
|
||||
const dataNameList = ref<Array<string>>([]);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const importRef = ref<InstanceType<typeof ImportTable>>();
|
||||
|
||||
const queryParams = ref<TableQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
tableName: '',
|
||||
tableComment: '',
|
||||
dataName: ''
|
||||
});
|
||||
|
||||
const preview = ref<{
|
||||
data: Record<string, string>;
|
||||
activeName: string;
|
||||
}>({
|
||||
data: {},
|
||||
activeName: 'domain.java'
|
||||
});
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: '代码预览'
|
||||
});
|
||||
|
||||
/** 查询多数据源名称 */
|
||||
const getDataNameList = async () => {
|
||||
const res = await getDataNames();
|
||||
dataNameList.value = res.data;
|
||||
};
|
||||
|
||||
/** 查询表集合 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listTable(proxy?.addDateRange(queryParams.value, dateRange.value));
|
||||
tableList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
/** 生成代码操作 */
|
||||
const handleGenTable = async (row?: TableVO) => {
|
||||
const tbIds = row?.tableId || ids.value;
|
||||
if (tbIds == '') {
|
||||
proxy?.$modal.msgError('请选择要生成的数据');
|
||||
return;
|
||||
}
|
||||
proxy?.$download.zip('/tool/gen/batchGenCode?tableIdStr=' + tbIds, 'ruoyi.zip');
|
||||
};
|
||||
/** 同步数据库操作 */
|
||||
const handleSynchDb = async (row: TableVO) => {
|
||||
const tableId = row.tableId;
|
||||
await proxy?.$modal.confirm('确认要强制同步"' + row.tableName + '"表结构吗?');
|
||||
await synchDb(tableId);
|
||||
proxy?.$modal.msgSuccess('同步成功');
|
||||
};
|
||||
/** 打开导入表弹窗 */
|
||||
const openImportTable = () => {
|
||||
importRef.value?.show(queryParams.value.dataName);
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
dateRange.value = ['', ''];
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
/** 预览按钮 */
|
||||
const handlePreview = async (row: TableVO) => {
|
||||
const res = await previewTable(row.tableId);
|
||||
preview.value.data = res.data;
|
||||
dialog.visible = true;
|
||||
preview.value.activeName = 'domain.java';
|
||||
};
|
||||
/** 复制代码成功 */
|
||||
const copyTextSuccess = () => {
|
||||
proxy?.$modal.msgSuccess('复制成功');
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: TableVO[]) => {
|
||||
ids.value = selection.map((item) => item.tableId);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
/** 修改按钮操作 */
|
||||
const handleEditTable = (row?: TableVO) => {
|
||||
const tableId = row?.tableId || ids.value[0];
|
||||
router.push({ path: '/tool/gen-edit/index/' + tableId, query: { pageNum: queryParams.value.pageNum } });
|
||||
};
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: TableVO) => {
|
||||
const tableIds = row?.tableId || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除表编号为"' + tableIds + '"的数据项?');
|
||||
await delTable(tableIds);
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
const time = route.query.t;
|
||||
if (time != null && time != uniqueId.value) {
|
||||
uniqueId.value = time as string;
|
||||
queryParams.value.pageNum = Number(route.query.pageNum);
|
||||
dateRange.value = ['', ''];
|
||||
queryFormRef.value?.resetFields();
|
||||
}
|
||||
getList();
|
||||
getDataNameList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-tab-pane {
|
||||
background-color: #282c34;
|
||||
.el-link {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,888 @@
|
||||
<template>
|
||||
<div class="practice-page">
|
||||
<section class="page-head">
|
||||
<div>
|
||||
<h1>三角色对练</h1>
|
||||
<p>AI 业主发起情境,学员应答,AI 教练给方向提示,AI 考官完成评分沉淀。</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<span class="status-pill" :class="apiPill.className">{{ apiPill.text }}</span>
|
||||
<span class="status-pill" :class="flowPill.className">{{ flowPill.text }}</span>
|
||||
<el-button @click="router.push('/index')">返回首页</el-button>
|
||||
<el-button type="danger" :icon="Refresh" @click="resetPractice">重置演示</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="flow-strip">
|
||||
<button v-for="step in flowSteps" :key="step.title" type="button" class="flow-step" :class="{ active: step.active, done: step.done }">
|
||||
<span>{{ step.index }}</span>
|
||||
<strong>{{ step.title }}</strong>
|
||||
<small>{{ step.desc }}</small>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="practice-grid">
|
||||
<article class="panel config-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
<h2>场景配置</h2>
|
||||
</div>
|
||||
|
||||
<div class="select-card">
|
||||
<span>训练场景</span>
|
||||
<el-select v-model="scenarioId" class="scenario-select" @change="resetPractice">
|
||||
<el-option v-for="scenario in scenarios" :key="scenario.id" :label="scenario.name" :value="scenario.id" />
|
||||
</el-select>
|
||||
</div>
|
||||
|
||||
<div class="field-list">
|
||||
<div v-for="item in scenarioProfile" :key="item.label" class="field-item">
|
||||
<span>{{ item.label }}</span>
|
||||
<strong>{{ item.value }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trust-card">
|
||||
<div>
|
||||
<span>业主信任度</span>
|
||||
<strong>{{ trustScore }}%</strong>
|
||||
</div>
|
||||
<div class="meter"><i :style="{ width: `${trustScore}%` }"></i></div>
|
||||
</div>
|
||||
|
||||
<div class="action-row config-actions">
|
||||
<el-button type="danger" :disabled="started" :icon="Microphone" @click="startPractice">开始对练</el-button>
|
||||
<el-button :icon="Tickets" @click="showGoal = !showGoal">{{ showGoal ? '收起目标' : '查看目标' }}</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="showGoal" class="goal-card">
|
||||
<strong>训练目标</strong>
|
||||
<p>{{ selectedScenario.goal }}</p>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="panel chat-panel">
|
||||
<div class="panel-title title-with-extra">
|
||||
<div>
|
||||
<el-icon><Microphone /></el-icon>
|
||||
<h2>对练实时过程</h2>
|
||||
</div>
|
||||
<span>{{ completedRounds }}/{{ selectedScenario.rounds.length }} 轮</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!started" class="empty-state">
|
||||
<strong>等待开始对练</strong>
|
||||
<p>点击“开始对练”,AI 业主会抛出第一轮投诉情境。</p>
|
||||
<el-button type="danger" :icon="Microphone" @click="startPractice">开始对练</el-button>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="chat-list">
|
||||
<div v-for="turn in conversation" :key="turn.id" class="bubble" :class="turn.role">
|
||||
<strong>{{ turn.name }}</strong>
|
||||
<p>{{ turn.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="reply-box">
|
||||
<div class="reply-head">
|
||||
<span>学员下一句</span>
|
||||
<el-button text type="danger" :disabled="finished" @click="fillSeedReply">填入 seed 回复</el-button>
|
||||
</div>
|
||||
<el-input
|
||||
v-model="draftReply"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
resize="none"
|
||||
:disabled="finished"
|
||||
placeholder="输入学员回复,或点击“填入 seed 回复”推进演示"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="action-row">
|
||||
<el-button :disabled="finished" @click="continueRound">继续一轮</el-button>
|
||||
<el-button type="danger" :disabled="!canScore" @click="finishScore">结束并评分</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</article>
|
||||
|
||||
<article class="panel score-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
<h2>考官评分</h2>
|
||||
</div>
|
||||
|
||||
<div v-if="!scored" class="empty-state score-empty">
|
||||
<strong>等待评分</strong>
|
||||
<p>完成两轮对练后点击“结束并评分”,生成考官评分、导师改写和训练记录。</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<div class="score-grid">
|
||||
<div v-for="score in displayScores" :key="score.label" class="score-item">
|
||||
<span>{{ score.label }}</span>
|
||||
<strong>{{ score.value }}</strong>
|
||||
<small>{{ score.note }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="coach-card">
|
||||
<strong>导师改写</strong>
|
||||
<p>{{ displayRewrite }}</p>
|
||||
</div>
|
||||
</template>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="panel record-panel">
|
||||
<div class="panel-title">
|
||||
<el-icon><Tickets /></el-icon>
|
||||
<h2>最近对练记录</h2>
|
||||
</div>
|
||||
<el-table :data="records" size="small" class="record-table">
|
||||
<el-table-column prop="time" label="时间" width="128" />
|
||||
<el-table-column prop="trainee" label="学员" width="120" />
|
||||
<el-table-column prop="scene" label="场景" min-width="150" />
|
||||
<el-table-column prop="score" label="得分" width="80" />
|
||||
<el-table-column prop="status" label="状态" width="120" />
|
||||
<el-table-column prop="summary" label="AI 摘要" min-width="260" />
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="TrainPractice" lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { Microphone, Refresh, Tickets, TrendCharts, UserFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { finishPracticeSession, startPracticeSession, submitPracticeTurn } from '@/api/aihr/practice';
|
||||
|
||||
type ChatRole = 'customer' | 'trainee' | 'coach';
|
||||
|
||||
type ChatTurn = {
|
||||
id: string;
|
||||
role: ChatRole;
|
||||
name: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
type PracticeRound = {
|
||||
customer: string;
|
||||
sampleReply: string;
|
||||
coachHint: string;
|
||||
};
|
||||
|
||||
type ScenarioSeed = {
|
||||
id: string;
|
||||
name: string;
|
||||
trainee: string;
|
||||
customer: string;
|
||||
project: string;
|
||||
strategy: string;
|
||||
goal: string;
|
||||
trustStart: number;
|
||||
trustEnd: number;
|
||||
rounds: PracticeRound[];
|
||||
scores: Array<{
|
||||
label: string;
|
||||
value: number | string;
|
||||
note: string;
|
||||
}>;
|
||||
total: number;
|
||||
rewrite: string;
|
||||
summary: string;
|
||||
};
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const scenarios: ScenarioSeed[] = [
|
||||
{
|
||||
id: 'complaint-water',
|
||||
name: '客服应对投诉',
|
||||
trainee: '张三',
|
||||
customer: '高情绪、追问进度、质疑服务',
|
||||
project: '住宅 / 车库积水',
|
||||
strategy: '仅在漏关键步骤时提示',
|
||||
goal: '训练学员先安抚、再确认诉求,并给出责任人和首次反馈时间。',
|
||||
trustStart: 68,
|
||||
trustEnd: 82,
|
||||
rounds: [
|
||||
{
|
||||
customer: '你们说今天处理,到现在都没人联系我,这个车库还怎么停?',
|
||||
sampleReply: '我理解您现在最关心的是谁负责和什么时候处理。我先确认工单和现场照片,10 分钟内给您明确责任人和到场时间。',
|
||||
coachHint: '这轮命中“安抚 + 责任人 + 首次反馈时间”,可以继续追问处理承诺。'
|
||||
},
|
||||
{
|
||||
customer: '我不要流程,我要知道如果今天还处理不好怎么办。',
|
||||
sampleReply: '如果今天现场不能完成处理,我会同步主管升级,并在 18 点前给您替代方案和下一次确认时间,避免您继续空等。',
|
||||
coachHint: '补上了升级路径和兜底承诺,结束后可进入评分。'
|
||||
}
|
||||
],
|
||||
scores: [
|
||||
{ label: '合规', value: 84, note: '有升级路径' },
|
||||
{ label: '沟通', value: 88, note: '承诺清楚' },
|
||||
{ label: '情绪', value: 82, note: '先承接诉求' },
|
||||
{ label: '错题', value: '1条', note: '补费用争议' }
|
||||
],
|
||||
total: 86,
|
||||
rewrite: '建议表达为:“我先确认责任人和到场时间,10 分钟内回复您;如果今天无法完成,我同步主管给您替代方案和下一次反馈时间。”',
|
||||
summary: '能先安抚并明确责任人与反馈时限,升级兜底表达比上一轮更完整。'
|
||||
},
|
||||
{
|
||||
id: 'fee-collection',
|
||||
name: '催缴沟通技巧',
|
||||
trainee: '李四',
|
||||
customer: '拒绝缴费、质疑服务、要求先整改',
|
||||
project: '住宅 / 物业费催缴',
|
||||
strategy: '提示服务改进与缴费并行',
|
||||
goal: '训练学员把费用沟通和服务整改拆开处理,避免激化争议。',
|
||||
trustStart: 61,
|
||||
trustEnd: 76,
|
||||
rounds: [
|
||||
{
|
||||
customer: '服务做成这样还让我缴费?你先把问题解决了再说。',
|
||||
sampleReply: '您提到的服务问题我会逐项记录并跟进,缴费和整改可以并行处理。我先确认您最不满意的三件事,并约定今天给您整改反馈。',
|
||||
coachHint: '表达方向正确,但要补“争议项复核”和“责任人”。'
|
||||
},
|
||||
{
|
||||
customer: '那我怎么知道你不是随便记一下?',
|
||||
sampleReply: '我会把问题登记到回访记录里,由客服主管复核,今天 17 点前给您第一次处理反馈,后续每个节点都会同步。',
|
||||
coachHint: '补上主管复核和节点同步,可以进入评分。'
|
||||
}
|
||||
],
|
||||
scores: [
|
||||
{ label: '合规', value: 80, note: '复核明确' },
|
||||
{ label: '沟通', value: 83, note: '拆分得当' },
|
||||
{ label: '情绪', value: 78, note: '安抚一般' },
|
||||
{ label: '错题', value: '2条', note: '补开场安抚' }
|
||||
],
|
||||
total: 81,
|
||||
rewrite: '建议先承接情绪:“您觉得服务不到位,我先把问题负责跟到底。”再说明缴费和整改并行,最后给出主管复核与反馈时间。',
|
||||
summary: '费用争议拆分较清楚,但开场情绪承接不足。'
|
||||
}
|
||||
];
|
||||
|
||||
const baseRecords = [
|
||||
{ time: '07-02 09:32', trainee: '张三', scene: '客服应对投诉', score: 86, status: '已完成', summary: '能先安抚并确认诉求,但反馈时限需要更明确。' },
|
||||
{ time: '07-02 09:12', trainee: '李四', scene: '催缴沟通技巧', score: 78, status: '已完成', summary: '费用争议回应完整,服务改进承诺偏弱。' },
|
||||
{ time: '07-02 08:47', trainee: '王五', scene: '突发事件处理', score: 72, status: '进行中', summary: '多方协调表达不够清晰,建议继续一轮。' }
|
||||
];
|
||||
|
||||
const scenarioId = ref(scenarios[0].id);
|
||||
const started = ref(false);
|
||||
const finished = ref(false);
|
||||
const scored = ref(false);
|
||||
const showGoal = ref(false);
|
||||
const activeRound = ref(0);
|
||||
const draftReply = ref('');
|
||||
const sessionId = ref('');
|
||||
const apiMode = ref<'seed' | 'api' | 'fallback'>('seed');
|
||||
const apiScores = ref<ScenarioSeed['scores'] | null>(null);
|
||||
const apiTotal = ref<number | null>(null);
|
||||
const apiRewrite = ref('');
|
||||
const apiSummary = ref('');
|
||||
const apiTrust = ref<number | null>(null);
|
||||
const conversation = ref<ChatTurn[]>([]);
|
||||
const records = ref([...baseRecords]);
|
||||
|
||||
const selectedScenario = computed(() => scenarios.find((scenario) => scenario.id === scenarioId.value) ?? scenarios[0]);
|
||||
const completedRounds = computed(() => conversation.value.filter((turn) => turn.role === 'trainee').length);
|
||||
const canScore = computed(() => finished.value && !scored.value);
|
||||
const displayScores = computed(() => apiScores.value ?? selectedScenario.value.scores);
|
||||
const displayTotal = computed(() => apiTotal.value ?? selectedScenario.value.total);
|
||||
const displayRewrite = computed(() => apiRewrite.value || selectedScenario.value.rewrite);
|
||||
const displaySummary = computed(() => apiSummary.value || selectedScenario.value.summary);
|
||||
const trustScore = computed(() => {
|
||||
if (apiTrust.value !== null) {
|
||||
return apiTrust.value;
|
||||
}
|
||||
if (scored.value) {
|
||||
return selectedScenario.value.trustEnd;
|
||||
}
|
||||
if (!started.value) {
|
||||
return selectedScenario.value.trustStart;
|
||||
}
|
||||
return selectedScenario.value.trustStart + completedRounds.value * 7;
|
||||
});
|
||||
const apiPill = computed(() => {
|
||||
if (apiMode.value === 'api') {
|
||||
return { text: '后端 API seed', className: 'pill-info' };
|
||||
}
|
||||
if (apiMode.value === 'fallback') {
|
||||
return { text: 'API 失败回退', className: 'pill-warning' };
|
||||
}
|
||||
return { text: '本地 seed 演示', className: 'pill-info' };
|
||||
});
|
||||
const flowPill = computed(() => {
|
||||
if (!started.value) {
|
||||
return { text: '待开始对练', className: 'pill-warning' };
|
||||
}
|
||||
if (!scored.value) {
|
||||
return { text: '对练进行中', className: 'pill-warning' };
|
||||
}
|
||||
return { text: '已完成闭环', className: 'pill-success' };
|
||||
});
|
||||
const flowSteps = computed(() => [
|
||||
{ index: '1', title: '选场景', desc: selectedScenario.value.name, active: !started.value, done: started.value || scored.value },
|
||||
{
|
||||
index: '2',
|
||||
title: '三角色对话',
|
||||
desc: `${completedRounds.value}/${selectedScenario.value.rounds.length} 轮`,
|
||||
active: started.value && !finished.value,
|
||||
done: finished.value
|
||||
},
|
||||
{ index: '3', title: '评分沉淀', desc: scored.value ? `${displayTotal.value} 分` : '等待评分', active: scored.value, done: scored.value }
|
||||
]);
|
||||
const scenarioProfile = computed(() => [
|
||||
{ label: '训练场景', value: selectedScenario.value.name },
|
||||
{ label: 'AI 业主', value: selectedScenario.value.customer },
|
||||
{ label: '项目类型', value: selectedScenario.value.project },
|
||||
{ label: '教练策略', value: selectedScenario.value.strategy },
|
||||
{ label: '学员', value: selectedScenario.value.trainee }
|
||||
]);
|
||||
|
||||
const appendTurn = (role: ChatRole, text: string) => {
|
||||
const nameMap = { customer: 'AI 业主', trainee: '学员', coach: 'AI 教练' };
|
||||
conversation.value.push({ id: `${role}-${conversation.value.length}`, role, name: nameMap[role], text });
|
||||
};
|
||||
|
||||
const resetPractice = () => {
|
||||
started.value = false;
|
||||
finished.value = false;
|
||||
scored.value = false;
|
||||
showGoal.value = false;
|
||||
activeRound.value = 0;
|
||||
draftReply.value = '';
|
||||
sessionId.value = '';
|
||||
apiMode.value = 'seed';
|
||||
apiScores.value = null;
|
||||
apiTotal.value = null;
|
||||
apiRewrite.value = '';
|
||||
apiSummary.value = '';
|
||||
apiTrust.value = null;
|
||||
conversation.value = [];
|
||||
records.value = [...baseRecords];
|
||||
};
|
||||
|
||||
const startPractice = async () => {
|
||||
resetPractice();
|
||||
const fallbackCustomer = selectedScenario.value.rounds[0].customer;
|
||||
try {
|
||||
const { data } = await startPracticeSession({
|
||||
extPartyId: selectedScenario.value.trainee,
|
||||
scenarioId: selectedScenario.value.id,
|
||||
mode: 'seed'
|
||||
});
|
||||
sessionId.value = data.sessionId;
|
||||
apiMode.value = 'api';
|
||||
apiTrust.value = data.trust;
|
||||
appendTurn('customer', data.customerText || fallbackCustomer);
|
||||
ElMessage.success(`已通过后端 API 进入 ${selectedScenario.value.name} seed 对练`);
|
||||
} catch {
|
||||
sessionId.value = '';
|
||||
apiMode.value = 'fallback';
|
||||
appendTurn('customer', fallbackCustomer);
|
||||
ElMessage.warning('后端对练 API 不可用,已回退本地 seed 对练');
|
||||
}
|
||||
started.value = true;
|
||||
};
|
||||
|
||||
const fillSeedReply = () => {
|
||||
draftReply.value = selectedScenario.value.rounds[activeRound.value].sampleReply;
|
||||
};
|
||||
|
||||
const continueRound = async () => {
|
||||
const round = selectedScenario.value.rounds[activeRound.value];
|
||||
if (!draftReply.value.trim()) {
|
||||
ElMessage.warning('请先输入学员回复');
|
||||
return;
|
||||
}
|
||||
const traineeText = draftReply.value.trim();
|
||||
appendTurn('trainee', traineeText);
|
||||
draftReply.value = '';
|
||||
|
||||
if (apiMode.value === 'api' && sessionId.value) {
|
||||
try {
|
||||
const { data } = await submitPracticeTurn({
|
||||
sessionId: sessionId.value,
|
||||
roundIndex: activeRound.value,
|
||||
traineeText
|
||||
});
|
||||
appendTurn('coach', data.coachHint || round.coachHint);
|
||||
apiTrust.value = data.trust;
|
||||
activeRound.value = Math.min(data.roundIndex ?? activeRound.value + 1, selectedScenario.value.rounds.length);
|
||||
if (data.finished || activeRound.value >= selectedScenario.value.rounds.length) {
|
||||
finished.value = true;
|
||||
ElMessage.success('后端 API 已完成两轮对练,可以结束并评分');
|
||||
return;
|
||||
}
|
||||
appendTurn('customer', data.customerText || selectedScenario.value.rounds[activeRound.value].customer);
|
||||
return;
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
ElMessage.warning('后端回合 API 不可用,已回退本地 seed 推进');
|
||||
}
|
||||
}
|
||||
|
||||
appendTurn('coach', round.coachHint);
|
||||
activeRound.value += 1;
|
||||
if (activeRound.value >= selectedScenario.value.rounds.length) {
|
||||
finished.value = true;
|
||||
ElMessage.success('两轮对练已完成,可以结束并评分');
|
||||
return;
|
||||
}
|
||||
appendTurn('customer', selectedScenario.value.rounds[activeRound.value].customer);
|
||||
};
|
||||
|
||||
const finishScore = async () => {
|
||||
let nextRecord = {
|
||||
time: '07-02 10:42',
|
||||
trainee: selectedScenario.value.trainee,
|
||||
scene: selectedScenario.value.name,
|
||||
score: displayTotal.value,
|
||||
status: '已完成',
|
||||
summary: displaySummary.value
|
||||
};
|
||||
|
||||
if (apiMode.value === 'api' && sessionId.value) {
|
||||
try {
|
||||
const { data } = await finishPracticeSession({ sessionId: sessionId.value });
|
||||
apiScores.value = data.score;
|
||||
apiTotal.value = data.totalScore;
|
||||
apiRewrite.value = data.mentorRewrite;
|
||||
apiSummary.value = data.aiComment;
|
||||
apiTrust.value = data.trustCurve?.[data.trustCurve.length - 1] ?? selectedScenario.value.trustEnd;
|
||||
nextRecord = data.records?.[0] ?? { ...nextRecord, score: data.totalScore, summary: data.aiComment };
|
||||
ElMessage.success('三角色对练评分已由后端 API 返回');
|
||||
} catch {
|
||||
apiMode.value = 'fallback';
|
||||
apiScores.value = null;
|
||||
apiTotal.value = null;
|
||||
apiRewrite.value = '';
|
||||
apiSummary.value = '';
|
||||
ElMessage.warning('后端评分 API 不可用,已回退本地 seed 评分');
|
||||
}
|
||||
} else {
|
||||
apiScores.value = null;
|
||||
apiTotal.value = null;
|
||||
apiRewrite.value = '';
|
||||
apiSummary.value = '';
|
||||
}
|
||||
|
||||
scored.value = true;
|
||||
records.value = [nextRecord, ...baseRecords];
|
||||
if (apiMode.value !== 'api') {
|
||||
ElMessage.success('考官评分已生成,并写入本地对练记录');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.practice-page {
|
||||
min-height: calc(100vh - 84px);
|
||||
padding: 22px;
|
||||
background: #f5f7fb;
|
||||
color: #162033;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #657184;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.head-actions,
|
||||
.action-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.flow-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.flow-step {
|
||||
display: grid;
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
gap: 4px 10px;
|
||||
align-items: center;
|
||||
min-height: 70px;
|
||||
padding: 12px;
|
||||
border: 1px solid #e4e9f2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
text-align: left;
|
||||
cursor: default;
|
||||
|
||||
span {
|
||||
display: grid;
|
||||
grid-row: span 2;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #eef4ff;
|
||||
color: #356bb4;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
strong {
|
||||
color: #22314a;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
small {
|
||||
color: #7a8597;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.flow-step.active {
|
||||
border-color: #f5a1a8;
|
||||
box-shadow: 0 8px 20px rgba(215, 15, 32, 0.08);
|
||||
}
|
||||
|
||||
.flow-step.done span {
|
||||
color: #15844a;
|
||||
background: #e5f8ee;
|
||||
}
|
||||
|
||||
.practice-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(280px, 0.9fr) minmax(440px, 1.35fr) minmax(300px, 0.9fr);
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid #e4e9f2;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
box-shadow: 0 10px 24px rgba(21, 30, 45, 0.04);
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
color: #4971b6;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.title-with-extra {
|
||||
justify-content: space-between;
|
||||
|
||||
div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: #657184;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.select-card,
|
||||
.field-item,
|
||||
.reply-box,
|
||||
.coach-card,
|
||||
.trust-card,
|
||||
.goal-card {
|
||||
padding: 12px;
|
||||
border: 1px solid #e5eaf3;
|
||||
border-radius: 8px;
|
||||
background: #f9fbfe;
|
||||
}
|
||||
|
||||
.select-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
span {
|
||||
color: #7a8597;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.scenario-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.field-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.field-item {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
|
||||
span {
|
||||
color: #7a8597;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.trust-card,
|
||||
.goal-card {
|
||||
margin-top: 12px;
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
color: #46566e;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.trust-card div:first-child {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.config-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.meter {
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #ecf1f7;
|
||||
overflow: hidden;
|
||||
|
||||
i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
background: linear-gradient(90deg, #f65f61, #31b367);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
max-width: 88%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
color: #344255;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
&.customer {
|
||||
background: #f1f5fb;
|
||||
}
|
||||
|
||||
&.trainee {
|
||||
justify-self: end;
|
||||
background: #ffe9ea;
|
||||
}
|
||||
|
||||
&.coach {
|
||||
border: 1px dashed #ffbe73;
|
||||
background: #fff8ed;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-box {
|
||||
margin-top: 12px;
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
line-height: 1.7;
|
||||
}
|
||||
}
|
||||
|
||||
.reply-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
span {
|
||||
color: #7a8597;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 10px;
|
||||
min-height: 320px;
|
||||
padding: 24px;
|
||||
border: 1px dashed #dbe3ef;
|
||||
border-radius: 8px;
|
||||
background: #fbfcff;
|
||||
text-align: center;
|
||||
|
||||
strong {
|
||||
color: #22314a;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
max-width: 360px;
|
||||
margin: 0;
|
||||
color: #657184;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.score-empty {
|
||||
min-height: 360px;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
justify-content: flex-end;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.score-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.score-item {
|
||||
padding: 12px;
|
||||
border: 1px solid #e5eaf3;
|
||||
border-radius: 8px;
|
||||
|
||||
span,
|
||||
small {
|
||||
display: block;
|
||||
color: #647084;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
margin: 4px 0;
|
||||
color: #d70f20;
|
||||
font-size: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.coach-card {
|
||||
margin-top: 12px;
|
||||
background: #fff8ed;
|
||||
|
||||
p {
|
||||
margin: 6px 0 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
}
|
||||
|
||||
.status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pill-success {
|
||||
color: #15844a;
|
||||
background: #e5f8ee;
|
||||
}
|
||||
|
||||
.pill-warning {
|
||||
color: #c46b00;
|
||||
background: #fff2dc;
|
||||
}
|
||||
|
||||
.pill-info {
|
||||
color: #4971b6;
|
||||
background: #edf4ff;
|
||||
}
|
||||
|
||||
.record-panel {
|
||||
:deep(.el-table__inner-wrapper::before) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.record-table {
|
||||
border: 1px solid #edf0f5;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@media (max-width: 1360px) {
|
||||
.practice-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.practice-page {
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.page-head {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flow-strip {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.score-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-row {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,254 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="search">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="分类名称" prop="categoryName">
|
||||
<el-input v-model="queryParams.categoryName" placeholder="请输入分类名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd()" v-hasPermi="['workflow:category:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="Sort" @click="handleToggleExpandAll">展开/折叠</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-table
|
||||
ref="categoryTableRef"
|
||||
v-loading="loading"
|
||||
:data="categoryList"
|
||||
row-key="categoryId"
|
||||
border
|
||||
:default-expand-all="isExpandAll"
|
||||
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
|
||||
>
|
||||
<el-table-column label="分类名称" prop="categoryName" width="260" />
|
||||
<el-table-column label="显示顺序" align="center" prop="orderNum" width="200" />
|
||||
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
|
||||
<el-table-column label="操作" fixed="right" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['workflow:category:edit']" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="新增" placement="top">
|
||||
<el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['workflow:category:add']" />
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['workflow:category:remove']" />
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
<el-dialog :title="dialog.title" v-model="dialog.visible" width="500px" append-to-body>
|
||||
<el-form ref="categoryFormRef" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="上级分类" prop="parentId">
|
||||
<el-tree-select
|
||||
v-model="form.parentId"
|
||||
:data="categoryOptions"
|
||||
:props="{ value: 'categoryId', label: 'categoryName', children: 'children' } as any"
|
||||
value-key="categoryId"
|
||||
placeholder="请选择上级分类"
|
||||
check-strictly
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分类名称" prop="categoryName">
|
||||
<el-input v-model="form.categoryName" placeholder="请输入分类名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="排序" prop="orderNum">
|
||||
<el-input-number v-model="form.orderNum" controls-position="right" :min="0" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Category" lang="ts">
|
||||
import { listCategory, getCategory, delCategory, addCategory, updateCategory } from '@/api/workflow/category';
|
||||
import { CategoryVO, CategoryQuery, CategoryForm } from '@/api/workflow/category/types';
|
||||
|
||||
type CategoryOption = {
|
||||
categoryId: number;
|
||||
categoryName: string;
|
||||
children?: CategoryOption[];
|
||||
};
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const categoryList = ref<CategoryVO[]>([]);
|
||||
const categoryOptions = ref<CategoryOption[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const showSearch = ref(true);
|
||||
const isExpandAll = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const categoryFormRef = ref<ElFormInstance>();
|
||||
const categoryTableRef = ref<ElTableInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: CategoryForm = {
|
||||
categoryId: undefined,
|
||||
categoryName: '',
|
||||
parentId: undefined,
|
||||
orderNum: 0
|
||||
};
|
||||
|
||||
const data = reactive<PageData<CategoryForm, CategoryQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
categoryName: undefined
|
||||
},
|
||||
rules: {
|
||||
categoryId: [{ required: true, message: '流程分类ID不能为空', trigger: 'blur' }],
|
||||
parentId: [{ required: true, message: '请选择上级分类', trigger: 'change' }],
|
||||
categoryName: [{ required: true, message: '请输入分类名称', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询流程分类列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listCategory(queryParams.value);
|
||||
const data = proxy?.handleTree<CategoryVO>(res.data, 'categoryId', 'parentId');
|
||||
if (data) {
|
||||
categoryList.value = data;
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/** 查询流程分类下拉树结构 */
|
||||
const getTreeselect = async () => {
|
||||
const res = await listCategory();
|
||||
categoryOptions.value = [];
|
||||
// 处理树形数据
|
||||
const data = proxy?.handleTree<CategoryOption>(res.data, 'categoryId', 'parentId');
|
||||
if (data) {
|
||||
categoryOptions.value = data; // 将处理后的树形数据赋值
|
||||
}
|
||||
};
|
||||
|
||||
// 取消按钮
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
};
|
||||
|
||||
// 表单重置
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
categoryFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = (row?: CategoryVO) => {
|
||||
reset();
|
||||
getTreeselect();
|
||||
if (row?.categoryId) {
|
||||
form.value.parentId = row.categoryId;
|
||||
} else {
|
||||
form.value.parentId = undefined;
|
||||
}
|
||||
dialog.visible = true;
|
||||
dialog.title = '添加流程分类';
|
||||
};
|
||||
|
||||
/** 展开/折叠操作 */
|
||||
const handleToggleExpandAll = () => {
|
||||
isExpandAll.value = !isExpandAll.value;
|
||||
toggleExpandAll(categoryList.value, isExpandAll.value);
|
||||
};
|
||||
|
||||
/** 展开/折叠操作 */
|
||||
const toggleExpandAll = (data: CategoryVO[], status: boolean) => {
|
||||
data.forEach((item) => {
|
||||
categoryTableRef.value?.toggleRowExpansion(item, status);
|
||||
if (item.children && item.children.length > 0) toggleExpandAll(item.children, status);
|
||||
});
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row: CategoryVO) => {
|
||||
reset();
|
||||
await getTreeselect();
|
||||
if (row != null) {
|
||||
form.value.parentId = row.parentId;
|
||||
}
|
||||
const res = await getCategory(row.categoryId);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = '修改流程分类';
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
categoryFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.categoryId) {
|
||||
await updateCategory(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
await addCategory(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
dialog.visible = false;
|
||||
getList();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row: CategoryVO) => {
|
||||
await proxy?.$modal.confirm('是否确认删除"' + row.categoryName + '"的分类?');
|
||||
loading.value = true;
|
||||
await delCategory(row.categoryId).finally(() => (loading.value = false));
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="search">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="请假天数" prop="startLeaveDays">
|
||||
<el-input v-model="queryParams.startLeaveDays" placeholder="请输入请假天数" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="endLeaveDays"> 至 </el-form-item>
|
||||
<el-form-item prop="endLeaveDays">
|
||||
<el-input v-model="queryParams.endLeaveDays" placeholder="请输入请假天数" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:leave:add']" type="primary" plain icon="Plus" @click="handleAdd">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:leave:export']" type="warning" plain icon="Download" @click="handleExport">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="leaveList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column v-if="false" label="主键" align="center" prop="id" />
|
||||
<el-table-column label="请假类型" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag>{{ options.find((e) => e.value === scope.row.leaveType)?.label }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="开始时间" align="center" prop="startDate">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.startDate, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结束时间" align="center" prop="endDate">
|
||||
<template #default="scope">
|
||||
<span>{{ proxy.parseTime(scope.row.endDate, '{y}-{m}-{d}') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="请假天数" align="center" prop="leaveDays" />
|
||||
<el-table-column label="请假原因" align="center" prop="remark" />
|
||||
<el-table-column align="center" label="流程状态" min-width="70">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_business_status" :value="scope.row.status"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="162">
|
||||
<template #default="scope">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
|
||||
<el-button v-hasPermi="['workflow:leave:edit']" size="small" type="primary" icon="Edit" @click="handleUpdate(scope.row)"
|
||||
>修改</el-button
|
||||
>
|
||||
</el-col>
|
||||
<el-col :span="1.5" v-if="scope.row.status === 'draft' || scope.row.status === 'cancel' || scope.row.status === 'back'">
|
||||
<el-button v-hasPermi="['workflow:leave:remove']" size="small" type="primary" icon="Delete" @click="handleDelete(scope.row)"
|
||||
>删除</el-button
|
||||
>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5" v-if="scope.row.status === 'waiting'">
|
||||
<el-button size="small" type="primary" icon="Notification" @click="handleCancelProcessApply(scope.row.id)">撤销</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" :total="total" @pagination="getList" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Leave" lang="ts">
|
||||
import { delLeave, listLeave } from '@/api/workflow/leave';
|
||||
import { cancelProcessApply } from '@/api/workflow/instance';
|
||||
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
|
||||
const leaveList = ref<LeaveVO[]>([]);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
const options = [
|
||||
{
|
||||
value: '1',
|
||||
label: '事假'
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '调休'
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
label: '病假'
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
label: '婚假'
|
||||
}
|
||||
];
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
|
||||
const data = reactive<PageData<LeaveForm, LeaveQuery>>({
|
||||
form: {},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
startLeaveDays: undefined,
|
||||
endLeaveDays: undefined
|
||||
},
|
||||
rules: {}
|
||||
});
|
||||
|
||||
const { queryParams } = toRefs(data);
|
||||
|
||||
/** 查询请假列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listLeave(queryParams.value);
|
||||
leaveList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
};
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: LeaveVO[]) => {
|
||||
ids.value = selection.map((item) => item.id);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.push({
|
||||
path: `/workflow/leaveEdit/index`,
|
||||
query: {
|
||||
type: 'add'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = (row?: LeaveVO) => {
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.push({
|
||||
path: `/workflow/leaveEdit/index`,
|
||||
query: {
|
||||
id: row.id,
|
||||
type: 'update'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 查看按钮操作 */
|
||||
const handleView = (row?: LeaveVO) => {
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.push({
|
||||
path: `/workflow/leaveEdit/index`,
|
||||
query: {
|
||||
id: row.id,
|
||||
type: 'view'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: LeaveVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除请假编号为"' + _ids + '"的数据项?').finally(() => (loading.value = false));
|
||||
await delLeave(_ids);
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
await getList();
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = () => {
|
||||
proxy?.download(
|
||||
'workflow/leave/export',
|
||||
{
|
||||
...queryParams.value
|
||||
},
|
||||
`leave_${new Date().getTime()}.xlsx`
|
||||
);
|
||||
};
|
||||
|
||||
/** 撤销按钮操作 */
|
||||
const handleCancelProcessApply = async (id: string) => {
|
||||
await proxy?.$modal.confirm('是否确认撤销当前单据?');
|
||||
loading.value = true;
|
||||
const data = {
|
||||
businessId: id,
|
||||
message: '申请人撤销流程!'
|
||||
};
|
||||
await cancelProcessApply(data).finally(() => (loading.value = false));
|
||||
await getList();
|
||||
proxy?.$modal.msgSuccess('撤销成功');
|
||||
};
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,286 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-card shadow="never">
|
||||
<!-- mode用于直接后端发起流程 不同接口实现方式可查看具体后端代码 -->
|
||||
<!-- 默认前端发起 前端发起更多样性 比如可以选审批人 选抄送人 上传附件等等 后端发起需要用户自行编写代码传这些参数 -->
|
||||
<approvalButton
|
||||
@submitForm="submitForm"
|
||||
@approvalVerifyOpen="approvalVerifyOpen"
|
||||
@handleApprovalRecord="handleApprovalRecord"
|
||||
:buttonLoading="buttonLoading"
|
||||
:id="form.id"
|
||||
:status="form.status"
|
||||
:pageType="routeParams.type"
|
||||
:mode="false"
|
||||
/>
|
||||
</el-card>
|
||||
<el-card shadow="never" style="height: 78vh; overflow-y: auto">
|
||||
<el-form ref="leaveFormRef" v-loading="loading" :disabled="routeParams.type === 'view'" :model="form" :rules="rules" label-width="80px">
|
||||
<el-form-item label="流程定义" v-if="routeParams.type === 'add'">
|
||||
<el-select v-model="flowCode" placeholder="选择流程定义" style="width: 100%">
|
||||
<el-option v-for="item in flowCodeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假类型" prop="leaveType">
|
||||
<el-select v-model="form.leaveType" placeholder="请选择请假类型" style="width: 100%">
|
||||
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假时间" required>
|
||||
<el-date-picker
|
||||
v-model="leaveTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
range-separator="To"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
:default-time="[new Date(2000, 1, 1, 0, 0, 0), new Date(2000, 1, 1, 23, 59, 59)]"
|
||||
@change="changeLeaveTime()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="请假天数" prop="leaveDays">
|
||||
<el-input v-model="form.leaveDays" disabled type="number" placeholder="请输入请假天数" />
|
||||
</el-form-item>
|
||||
<el-form-item label="请假原因" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入请假原因" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<!-- 提交组件 -->
|
||||
<submitVerify ref="submitVerifyRef" :task-variables="taskVariables" @submit-callback="submitCallback" />
|
||||
<!-- 审批记录 -->
|
||||
<approvalRecord ref="approvalRecordRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Leave" lang="ts">
|
||||
import { addLeave, getLeave, submitAndFlowStart, updateLeave } from '@/api/workflow/leave';
|
||||
import { LeaveForm, LeaveQuery, LeaveVO } from '@/api/workflow/leave/types';
|
||||
import { startWorkFlow } from '@/api/workflow/task';
|
||||
import SubmitVerify from '@/components/Process/submitVerify.vue';
|
||||
import ApprovalRecord from '@/components/Process/approvalRecord.vue';
|
||||
import ApprovalButton from '@/components/Process/approvalButton.vue';
|
||||
import { AxiosResponse } from 'axios';
|
||||
import { StartProcessBo } from '@/api/workflow/workflowCommon/types';
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const leaveTime = ref<Array<string>>([]);
|
||||
//路由参数
|
||||
const routeParams = ref<Record<string, any>>({});
|
||||
const options = [
|
||||
{
|
||||
value: '1',
|
||||
label: '事假'
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: '调休'
|
||||
},
|
||||
{
|
||||
value: '3',
|
||||
label: '病假'
|
||||
},
|
||||
{
|
||||
value: '4',
|
||||
label: '婚假'
|
||||
}
|
||||
];
|
||||
const flowCodeOptions = [
|
||||
{
|
||||
value: 'leave1',
|
||||
label: '请假申请-普通'
|
||||
},
|
||||
{
|
||||
value: 'leave2',
|
||||
label: '请假申请-排他网关'
|
||||
},
|
||||
{
|
||||
value: 'leave3',
|
||||
label: '请假申请-并行网关'
|
||||
},
|
||||
{
|
||||
value: 'leave4',
|
||||
label: '请假申请-会签'
|
||||
},
|
||||
{
|
||||
value: 'leave5',
|
||||
label: '请假申请-并行会签网关'
|
||||
},
|
||||
{
|
||||
value: 'leave6',
|
||||
label: '请假申请-排他并行会签'
|
||||
}
|
||||
];
|
||||
// 自定义流程可不选择 直接填写flowCode 例如 'leave1'
|
||||
const flowCode = ref<string>('leave1');
|
||||
|
||||
//提交组件
|
||||
const submitVerifyRef = ref<InstanceType<typeof SubmitVerify>>();
|
||||
//审批记录组件
|
||||
const approvalRecordRef = ref<InstanceType<typeof ApprovalRecord>>();
|
||||
|
||||
const leaveFormRef = ref<ElFormInstance>();
|
||||
|
||||
const submitFormData = ref<StartProcessBo>({
|
||||
businessId: '',
|
||||
flowCode: '',
|
||||
variables: {},
|
||||
bizExt: {}
|
||||
});
|
||||
const taskVariables = ref<Record<string, any>>({});
|
||||
const bizExt = ref<Record<string, any>>({});
|
||||
|
||||
const initFormData: LeaveForm = {
|
||||
id: undefined,
|
||||
leaveType: undefined,
|
||||
startDate: undefined,
|
||||
endDate: undefined,
|
||||
leaveDays: undefined,
|
||||
remark: undefined,
|
||||
status: undefined
|
||||
};
|
||||
const data = reactive<PageData<LeaveForm, LeaveQuery>>({
|
||||
form: { ...initFormData },
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
startLeaveDays: undefined,
|
||||
endLeaveDays: undefined
|
||||
},
|
||||
rules: {
|
||||
id: [{ required: true, message: '主键不能为空', trigger: 'blur' }],
|
||||
leaveType: [{ required: true, message: '请假类型不能为空', trigger: 'blur' }],
|
||||
leaveTime: [{ required: true, message: '请假时间不能为空', trigger: 'blur' }],
|
||||
leaveDays: [{ required: true, message: '请假天数不能为空', trigger: 'blur' }]
|
||||
}
|
||||
});
|
||||
|
||||
const { form, rules } = toRefs(data);
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
leaveTime.value = [];
|
||||
leaveFormRef.value?.resetFields();
|
||||
};
|
||||
|
||||
const changeLeaveTime = () => {
|
||||
const startDate = new Date(leaveTime.value[0]).getTime();
|
||||
const endDate = new Date(leaveTime.value[1]).getTime();
|
||||
const diffInMilliseconds = endDate - startDate;
|
||||
form.value.leaveDays = Math.floor(diffInMilliseconds / (1000 * 60 * 60 * 24)) + 1;
|
||||
};
|
||||
/** 获取详情 */
|
||||
const getInfo = () => {
|
||||
loading.value = true;
|
||||
buttonLoading.value = false;
|
||||
nextTick(async () => {
|
||||
const res = await getLeave(routeParams.value.id);
|
||||
Object.assign(form.value, res.data);
|
||||
leaveTime.value = [];
|
||||
leaveTime.value.push(form.value.startDate);
|
||||
leaveTime.value.push(form.value.endDate);
|
||||
loading.value = false;
|
||||
buttonLoading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = (status: string, mode: boolean) => {
|
||||
if (leaveTime.value.length === 0) {
|
||||
proxy?.$modal.msgError('请假时间不能为空');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
leaveFormRef.value?.validate(async (valid: boolean) => {
|
||||
form.value.startDate = leaveTime.value[0];
|
||||
form.value.endDate = leaveTime.value[1];
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
// 设置后端发起和不等于草稿状态 直接走流程发起
|
||||
if (mode && status != 'draft') {
|
||||
const res = await submitAndFlowStart(form.value).finally(() => (buttonLoading.value = false));
|
||||
form.value = res.data;
|
||||
buttonLoading.value = false;
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.go(-1);
|
||||
} else {
|
||||
let res;
|
||||
if (form.value.id) {
|
||||
res = await updateLeave(form.value).finally(() => (buttonLoading.value = false));
|
||||
} else {
|
||||
res = await addLeave(form.value).finally(() => (buttonLoading.value = false));
|
||||
}
|
||||
form.value = res.data;
|
||||
if (status === 'draft') {
|
||||
buttonLoading.value = false;
|
||||
proxy?.$modal.msgSuccess('暂存成功');
|
||||
proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.go(-1);
|
||||
} else {
|
||||
await handleStartWorkFlow(res.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
buttonLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
//提交申请
|
||||
const handleStartWorkFlow = async (data: LeaveForm) => {
|
||||
try {
|
||||
submitFormData.value.flowCode = flowCode.value;
|
||||
submitFormData.value.businessId = data.id;
|
||||
//流程变量
|
||||
taskVariables.value = {
|
||||
// leave2/6 使用的流程变量
|
||||
leaveDays: data.leaveDays,
|
||||
// leave4/5 使用的流程变量
|
||||
userList: ['1', '3', '4']
|
||||
};
|
||||
//流程实例业务扩展字段
|
||||
bizExt.value = {
|
||||
businessTitle: '请假申请',
|
||||
businessCode: data.applyCode
|
||||
};
|
||||
submitFormData.value.variables = taskVariables.value;
|
||||
submitFormData.value.bizExt = bizExt.value;
|
||||
const resp = await startWorkFlow(submitFormData.value);
|
||||
if (submitVerifyRef.value) {
|
||||
buttonLoading.value = false;
|
||||
submitVerifyRef.value.openDialog(resp.data.taskId);
|
||||
}
|
||||
} finally {
|
||||
buttonLoading.value = false;
|
||||
}
|
||||
};
|
||||
//审批记录
|
||||
const handleApprovalRecord = () => {
|
||||
approvalRecordRef.value.init(form.value.id);
|
||||
};
|
||||
//提交回调
|
||||
const submitCallback = async () => {
|
||||
await proxy.$tab.closePage(proxy.$route);
|
||||
proxy.$router.go(-1);
|
||||
};
|
||||
//审批
|
||||
const approvalVerifyOpen = async () => {
|
||||
submitVerifyRef.value.openDialog(routeParams.value.taskId);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(async () => {
|
||||
routeParams.value = proxy.$route.query;
|
||||
reset();
|
||||
loading.value = false;
|
||||
if (routeParams.value.type === 'update' || routeParams.value.type === 'view' || routeParams.value.type === 'approval') {
|
||||
getInfo();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div ref="container" class="w-full h-[calc(100vh-88px)]">
|
||||
<iframe ref="iframe" :src="iframeUrl" frameborder="0" height="100%" style="height: 100%; width: inherit"></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="WarmFlow" lang="ts">
|
||||
const { proxy } = getCurrentInstance();
|
||||
import { onMounted } from 'vue';
|
||||
import { getToken } from '@/utils/auth';
|
||||
|
||||
// definitionId为需要查询的流程定义id,
|
||||
// disabled为是否可编辑, 例如:查看的时候不可编辑,不可保存
|
||||
const iframeUrl = ref('');
|
||||
const baseUrl = import.meta.env.VITE_APP_BASE_API;
|
||||
const iframeLoaded = () => {
|
||||
// iframe监听组件内设计器保存事件
|
||||
window.onmessage = (event) => {
|
||||
switch (event.data.method) {
|
||||
case 'close':
|
||||
close();
|
||||
break;
|
||||
}
|
||||
};
|
||||
};
|
||||
const open = async (definitionId, disabled) => {
|
||||
const url = baseUrl + `/warm-flow-ui/index.html?id=${definitionId}&onlyDesignShow=true`;
|
||||
iframeUrl.value = url + '&Authorization=Bearer ' + getToken() + '&clientid=' + import.meta.env.VITE_APP_CLIENT_ID;
|
||||
};
|
||||
/** 关闭按钮 */
|
||||
function close() {
|
||||
const obj = { path: '/workflow/processDefinition', query: { activeName: proxy.$route.query.activeName } };
|
||||
proxy.$tab.closeOpenPage(obj);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
iframeLoaded();
|
||||
open(proxy.$route.query.definitionId, proxy.$route.query.disabled);
|
||||
});
|
||||
/**
|
||||
* 对外暴露子组件方法
|
||||
*/
|
||||
defineExpose({
|
||||
open
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,563 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-row :gutter="20">
|
||||
<!-- 流程分类树 -->
|
||||
<el-col :lg="4" :xs="24" style="">
|
||||
<el-card shadow="hover">
|
||||
<el-input v-model="categoryName" placeholder="请输入流程分类名" prefix-icon="Search" clearable />
|
||||
<el-tree
|
||||
ref="categoryTreeRef"
|
||||
class="mt-2"
|
||||
node-key="id"
|
||||
:data="categoryOptions"
|
||||
:props="{ label: 'label', children: 'children' } as any"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="filterNode"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick"
|
||||
></el-tree>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="20" :xs="24">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form v-show="showSearch" ref="queryFormRef" :model="queryParams" :inline="true" label-width="120px">
|
||||
<el-form-item label="流程定义名称" prop="flowName">
|
||||
<el-input v-model="queryParams.flowName" placeholder="请输入流程定义名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义编码" prop="flowCode">
|
||||
<el-input v-model="queryParams.flowCode" placeholder="请输入流程定义编码" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:add']" type="primary" icon="Plus" @click="handleAdd()">添加</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:edit']" type="success" icon="Edit" :disabled="single" @click="handleUpdate()">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:remove']" type="danger" icon="Delete" :disabled="multiple" @click="handleDelete()">删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:import']" type="primary" icon="UploadFilled" @click="uploadDialog.visible = true">部署流程文件</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:export']" type="warning" icon="Download" :disabled="single" @click="handleExportDef">导出</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
|
||||
<el-tab-pane label="已发布" name="0"></el-tab-pane>
|
||||
<el-tab-pane label="未发布" name="1"></el-tab-pane>
|
||||
<el-table v-loading="loading" border :data="processDefinitionList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column align="center" prop="id" label="主键" v-if="false"></el-table-column>
|
||||
<el-table-column align="center" prop="flowName" label="流程定义名称" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column align="center" prop="flowCode" label="标识KEY" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column align="center" prop="categoryName" label="流程分类" :show-overflow-tooltip="true"></el-table-column>
|
||||
<el-table-column align="center" prop="version" label="版本号" width="80">
|
||||
<template #default="scope"> v{{ scope.row.version }}.0</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="activityStatus" label="激活状态" width="130">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-hasPermi="['workflow:definition:active']"
|
||||
v-model="scope.row.activityStatus"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="(status) => handleProcessDefState(scope.row, status)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="isPublish" label="发布状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.isPublish == 0" type="danger">未发布</el-tag>
|
||||
<el-tag v-else-if="scope.row.isPublish == 1" type="success">已发布</el-tag>
|
||||
<el-tag v-else type="danger">失效</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" align="center" width="170" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:remove']" link type="primary" size="small" icon="Delete" @click="handleDelete(scope.row)">删除流程</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:copy']" link type="primary" size="small" icon="CopyDocument" @click="handleCopyDef(scope.row)">复制流程</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:query']" link type="primary" v-if="scope.row.isPublish === 0" icon="Pointer" size="small" @click="design(scope.row)"
|
||||
>流程设计</el-button
|
||||
>
|
||||
<el-button v-hasPermi="['workflow:definition:query']" link type="primary" v-else icon="View" size="small" @click="designView(scope.row)">查看流程</el-button>
|
||||
</el-col>
|
||||
<el-col v-if="scope.row.isPublish !== 1" :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:definition:publish']" link type="primary" size="small" icon="CircleCheck" @click="handlePublish(scope.row)">发布流程</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="getPageList"
|
||||
/>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 部署文件 -->
|
||||
<el-dialog v-if="uploadDialog.visible" v-model="uploadDialog.visible" :title="uploadDialog.title" width="30%">
|
||||
<div v-loading="uploadDialogLoading">
|
||||
<div class="mb5">
|
||||
<el-text class="mx-1" size="large"><span class="text-danger">*</span>请选择部署流程分类:</el-text>
|
||||
<el-tree-select
|
||||
v-model="selectCategory"
|
||||
:data="categoryOptions"
|
||||
:props="{ value: 'id', label: 'label', children: 'children' } as any"
|
||||
filterable
|
||||
value-key="id"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
style="width: 240px"
|
||||
/>
|
||||
</div>
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
drag
|
||||
multiple
|
||||
accept="application/json,application/text"
|
||||
:before-upload="handlerBeforeUpload"
|
||||
:http-request="handlerImportDefinition"
|
||||
>
|
||||
<el-icon class="UploadFilled"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text"><em>点击上传,选择JSON流程文件</em></div>
|
||||
<div class="el-upload__text">仅支持json格式文件</div>
|
||||
<div class="el-upload__text">PS:如若部署请部署从本项目模型管理导出的数据</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增/编辑流程定义 -->
|
||||
<el-dialog v-model="modelDialog.visible" :title="modelDialog.title" width="650px" append-to-body :close-on-click-modal="false">
|
||||
<template #footer>
|
||||
<el-form ref="defFormRef" :model="form" :rules="rules" label-width="120px">
|
||||
<el-form-item label="流程类别" prop="category">
|
||||
<el-tree-select
|
||||
v-model="form.category"
|
||||
:data="categoryOptions"
|
||||
:props="{ value: 'id', label: 'label', children: 'children' } as any"
|
||||
filterable
|
||||
value-key="id"
|
||||
:render-after-expand="false"
|
||||
check-strictly
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="流程编码" prop="flowCode">
|
||||
<el-input v-model="form.flowCode" placeholder="请输入流程编码" maxlength="40" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程名称" prop="flowName">
|
||||
<el-input v-model="form.flowName" placeholder="请输入流程名称" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="设计器模式" prop="modelValue">
|
||||
<el-radio-group v-model="form.modelValue" :disabled="!!form.id">
|
||||
<el-radio value="CLASSICS" size="large" border>经典模式</el-radio>
|
||||
<el-radio value="MIMIC" size="large" border>仿钉钉模式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="流程配置">
|
||||
<el-checkbox v-model="autoPass" label="下一节点执行人是当前任务处理人自动审批" />
|
||||
</el-form-item>
|
||||
<el-form-item label="是否动态表单" prop="formCustom">
|
||||
<el-radio-group v-model="form.formCustom">
|
||||
<el-radio value="Y" size="large" border disabled>是</el-radio>
|
||||
<el-radio value="N" size="large" border>否</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="表单路径" prop="formPath">
|
||||
<el-input v-model="form.formPath" placeholder="请输入表单路径" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="modelDialog.visible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="processDefinition" lang="ts">
|
||||
import { listDefinition, deleteDefinition, active, importDef, unPublishList, publish, add, edit, getInfo, copy } from '@/api/workflow/definition';
|
||||
import { categoryTree } from '@/api/workflow/category';
|
||||
import { CategoryTreeVO } from '@/api/workflow/category/types';
|
||||
import { FlowDefinitionQuery, FlowDefinitionVo, FlowDefinitionForm } from '@/api/workflow/definition/types';
|
||||
import { UploadRequestOptions, TabsPaneContext } from 'element-plus';
|
||||
import { ElMessageBoxOptions } from 'element-plus/es/components/message-box/src/message-box.type';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const categoryTreeRef = ref<ElTreeInstance>();
|
||||
|
||||
const loading = ref(true);
|
||||
const ids = ref<Array<any>>([]);
|
||||
const flowCodeList = ref<Array<any>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const total = ref(0);
|
||||
const uploadDialogLoading = ref(false);
|
||||
const processDefinitionList = ref<FlowDefinitionVo[]>([]);
|
||||
const categoryOptions = ref<CategoryTreeVO[]>([]);
|
||||
const categoryName = ref('');
|
||||
const autoPass = ref(false);
|
||||
/** 部署文件分类选择 */
|
||||
const selectCategory = ref();
|
||||
const defFormRef = ref<ElFormInstance>();
|
||||
const activeName = ref('0');
|
||||
const uploadDialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: '部署流程文件'
|
||||
});
|
||||
|
||||
const processDefinitionDialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: '历史版本'
|
||||
});
|
||||
|
||||
const modelDialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
// 查询参数
|
||||
const queryParams = ref<FlowDefinitionQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
flowName: undefined,
|
||||
flowCode: undefined,
|
||||
category: undefined
|
||||
});
|
||||
const rules = {
|
||||
category: [{ required: true, message: '分类名称不能为空', trigger: 'blur' }],
|
||||
flowName: [{ required: true, message: '流程定义名称不能为空', trigger: 'blur' }],
|
||||
formCustom: [{ required: true, message: '请选择是否动态表单', trigger: 'change' }],
|
||||
modelValue: [{ required: true, message: '设计器模式不能为空', trigger: 'change' }],
|
||||
flowCode: [{ required: true, message: '流程定义编码不能为空', trigger: 'blur' }]
|
||||
};
|
||||
const initFormData: FlowDefinitionForm = {
|
||||
id: '',
|
||||
flowName: '',
|
||||
flowCode: '',
|
||||
category: '',
|
||||
ext: '',
|
||||
formPath: '',
|
||||
formCustom: '',
|
||||
modelValue: ''
|
||||
};
|
||||
//流程定义参数
|
||||
const form = ref<FlowDefinitionForm>({
|
||||
id: '',
|
||||
flowName: '',
|
||||
flowCode: '',
|
||||
category: '',
|
||||
ext: '',
|
||||
formPath: '',
|
||||
formCustom: '',
|
||||
modelValue: ''
|
||||
});
|
||||
onMounted(() => {
|
||||
getPageList();
|
||||
getTreeselect();
|
||||
});
|
||||
|
||||
/** 节点单击事件 */
|
||||
const handleNodeClick = (data: CategoryTreeVO) => {
|
||||
queryParams.value.category = data.id;
|
||||
if (data.id === '0') {
|
||||
queryParams.value.category = '';
|
||||
}
|
||||
handleQuery();
|
||||
};
|
||||
/** 通过条件过滤节点 */
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true;
|
||||
return data.categoryName.indexOf(value) !== -1;
|
||||
};
|
||||
/** 根据名称筛选部门树 */
|
||||
watchEffect(
|
||||
() => {
|
||||
categoryTreeRef.value.filter(categoryName.value);
|
||||
},
|
||||
{
|
||||
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
|
||||
}
|
||||
);
|
||||
|
||||
/** 查询流程分类下拉树结构 */
|
||||
const getTreeselect = async () => {
|
||||
const res = await categoryTree();
|
||||
categoryOptions.value = res.data;
|
||||
};
|
||||
const handleClick = (tab: TabsPaneContext, event: Event) => {
|
||||
// v-model处理有延迟 需要手动处理
|
||||
activeName.value = tab.index;
|
||||
handleQuery();
|
||||
};
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
if (activeName.value === '0') {
|
||||
getList();
|
||||
} else {
|
||||
getUnPublishList();
|
||||
}
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.category = '';
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.pageSize = 10;
|
||||
handleQuery();
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: any) => {
|
||||
ids.value = selection.map((item: any) => item.id);
|
||||
flowCodeList.value = selection.map((item: any) => item.flowCode);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
//分页
|
||||
const getPageList = async () => {
|
||||
const query = proxy.$route.query;
|
||||
if (query.activeName) {
|
||||
activeName.value = query.activeName;
|
||||
proxy.$route.query.activeName = '';
|
||||
}
|
||||
if (activeName.value === '0') {
|
||||
getList();
|
||||
} else {
|
||||
getUnPublishList();
|
||||
}
|
||||
};
|
||||
//分页
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const resp = await listDefinition(queryParams.value);
|
||||
processDefinitionList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
};
|
||||
//查询未发布的流程定义列表
|
||||
const getUnPublishList = async () => {
|
||||
loading.value = true;
|
||||
const resp = await unPublishList(queryParams.value);
|
||||
processDefinitionList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: FlowDefinitionVo) => {
|
||||
const id = row?.id || ids.value;
|
||||
const defList = processDefinitionList.value.filter((x) => id.indexOf(x.id) != -1).map((x) => x.flowCode);
|
||||
await proxy?.$modal.confirm('是否确认删除流程定义编码为【' + defList + '】的数据项?');
|
||||
loading.value = true;
|
||||
await deleteDefinition(id).finally(() => (loading.value = false));
|
||||
await handleQuery();
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
/** 发布流程定义 */
|
||||
const handlePublish = async (row?: FlowDefinitionVo) => {
|
||||
await proxy?.$modal.confirm(
|
||||
'是否确认发布流程定义编码为【' + row.flowCode + '】版本为【' + row.version + '】的数据项?,发布后会将已发布流程定义改为失效!'
|
||||
);
|
||||
loading.value = true;
|
||||
await publish(row.id).finally(() => (loading.value = false));
|
||||
processDefinitionDialog.visible = false;
|
||||
activeName.value = '0';
|
||||
await handleQuery();
|
||||
proxy?.$modal.msgSuccess('发布成功');
|
||||
};
|
||||
/** 挂起/激活 */
|
||||
const handleProcessDefState = async (row: FlowDefinitionVo, status: number | string | boolean) => {
|
||||
let msg: string;
|
||||
if (status === 0) {
|
||||
msg = `暂停后,此流程下的所有任务都不允许往后流转,您确定挂起【${row.flowName || row.flowCode}】吗?`;
|
||||
} else {
|
||||
msg = `启动后,此流程下的所有任务都允许往后流转,您确定激活【${row.flowName || row.flowCode}】吗?`;
|
||||
}
|
||||
try {
|
||||
loading.value = true;
|
||||
await proxy?.$modal.confirm(msg);
|
||||
await active(row.id, !!status);
|
||||
await handleQuery();
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
} catch (error) {
|
||||
row.activityStatus = status === 0 ? 1 : 0;
|
||||
console.error(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
//上传文件前的钩子
|
||||
const handlerBeforeUpload = () => {
|
||||
if (selectCategory.value === 'ALL') {
|
||||
proxy?.$modal.msgError('顶级节点不可作为分类!');
|
||||
return false;
|
||||
}
|
||||
if (!selectCategory.value) {
|
||||
proxy?.$modal.msgError('请选择左侧要上传的分类!');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
//部署文件
|
||||
const handlerImportDefinition = (data: UploadRequestOptions): XMLHttpRequest => {
|
||||
const formData = new FormData();
|
||||
uploadDialogLoading.value = true;
|
||||
formData.append('file', data.file);
|
||||
formData.append('category', selectCategory.value);
|
||||
importDef(formData)
|
||||
.then(() => {
|
||||
uploadDialog.visible = false;
|
||||
proxy?.$modal.msgSuccess('部署成功');
|
||||
activeName.value = '1';
|
||||
handleQuery();
|
||||
})
|
||||
.finally(() => {
|
||||
uploadDialogLoading.value = false;
|
||||
});
|
||||
return;
|
||||
};
|
||||
/**
|
||||
* 设计流程
|
||||
* @param row
|
||||
*/
|
||||
const design = async (row: FlowDefinitionVo) => {
|
||||
proxy.$router.push({
|
||||
path: `/workflow/design/index`,
|
||||
query: {
|
||||
definitionId: row.id,
|
||||
disabled: false,
|
||||
activeName: activeName.value
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 查看流程
|
||||
* @param row
|
||||
*/
|
||||
const designView = async (row: FlowDefinitionVo) => {
|
||||
proxy.$router.push({
|
||||
path: `/workflow/design/index`,
|
||||
query: {
|
||||
definitionId: row.id,
|
||||
disabled: true,
|
||||
activeName: activeName.value
|
||||
}
|
||||
});
|
||||
};
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = { ...initFormData };
|
||||
defFormRef.value?.resetFields();
|
||||
};
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
const handleAdd = async () => {
|
||||
reset();
|
||||
if (queryParams.value.category != '') {
|
||||
form.value.category = queryParams.value.category;
|
||||
}
|
||||
form.value.modelValue = 'CLASSICS';
|
||||
form.value.formCustom = 'N';
|
||||
modelDialog.visible = true;
|
||||
modelDialog.title = '新增流程';
|
||||
};
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: FlowDefinitionVo) => {
|
||||
reset();
|
||||
const id = row?.id || ids.value[0];
|
||||
const res = await getInfo(id);
|
||||
Object.assign(form.value, res.data);
|
||||
autoPass.value = false;
|
||||
if (form.value.ext != null && form.value.ext != '') {
|
||||
const extJson = JSON.parse(form.value.ext);
|
||||
if (extJson.autoPass != null && extJson.autoPass != '') {
|
||||
autoPass.value = extJson.autoPass;
|
||||
}
|
||||
}
|
||||
modelDialog.visible = true;
|
||||
modelDialog.title = '修改流程';
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
defFormRef.value.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
loading.value = true;
|
||||
const ext = {};
|
||||
ext.autoPass = autoPass.value;
|
||||
form.value.ext = JSON.stringify(ext);
|
||||
if (form.value.id) {
|
||||
await edit(form.value).finally(() => (loading.value = false));
|
||||
} else {
|
||||
await add(form.value).finally(() => (loading.value = false));
|
||||
activeName.value = '1';
|
||||
}
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
modelDialog.visible = false;
|
||||
handleQuery();
|
||||
}
|
||||
});
|
||||
};
|
||||
//复制
|
||||
const handleCopyDef = async (row: FlowDefinitionVo) => {
|
||||
ElMessageBox.confirm(`是否确认复制【${row.flowCode}】版本为【${row.version}】的流程定义!`, '提示', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
} as ElMessageBoxOptions).then(() => {
|
||||
loading.value = true;
|
||||
copy(row.id)
|
||||
.then((resp) => {
|
||||
if (resp.code === 200) {
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
activeName.value = '1';
|
||||
handleQuery();
|
||||
}
|
||||
})
|
||||
.finally(() => (loading.value = false));
|
||||
});
|
||||
};
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExportDef = () => {
|
||||
proxy?.download(`/workflow/definition/exportDef/${ids.value[0]}`, {}, `${flowCodeList.value[0]}.json`);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,478 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-row :gutter="20">
|
||||
<!-- 流程分类树 -->
|
||||
<el-col :lg="4" :xs="24" style="">
|
||||
<el-card shadow="hover">
|
||||
<el-input v-model="categoryName" placeholder="请输入流程分类名" prefix-icon="Search" clearable />
|
||||
<el-tree
|
||||
ref="categoryTreeRef"
|
||||
class="mt-2"
|
||||
node-key="id"
|
||||
:data="categoryOptions"
|
||||
:props="{ label: 'label', children: 'children' } as any"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="filterNode"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick"
|
||||
></el-tree>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="20" :xs="24">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form v-show="showSearch" ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item>
|
||||
<el-badge :value="userSelectCount" :max="10" class="item">
|
||||
<el-button type="primary" @click="openUserSelect">选择申请人</el-button>
|
||||
</el-badge>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务名称" prop="nodeName">
|
||||
<el-input v-model="queryParams.nodeName" placeholder="请输入任务名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义名称" label-width="100" prop="flowName">
|
||||
<el-input v-model="queryParams.flowName" placeholder="请输入流程定义名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义编码" label-width="100" prop="flowCode">
|
||||
<el-input v-model="queryParams.flowCode" placeholder="请输入流程定义编码" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:instance:remove']" type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete">删除</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-tabs v-model="tab" @tab-click="changeTab">
|
||||
<el-tab-pane name="running" label="运行中"></el-tab-pane>
|
||||
<el-tab-pane name="finish" label="已完成"></el-tab-pane>
|
||||
<el-table v-loading="loading" border :data="processInstanceList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column align="center" type="index" label="序号" width="60"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessCode" align="center" label="业务编码"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessTitle" align="center" label="业务标题"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" width="120" label="流程定义名称">
|
||||
<template #default="scope">
|
||||
<span>{{ scope.row.flowName }}v{{ scope.row.version }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="flowCode" width="120" label="流程定义编码"></el-table-column>
|
||||
<el-table-column align="center" prop="categoryName" label="流程分类"></el-table-column>
|
||||
<el-table-column align="center" prop="nodeName" label="任务名称"></el-table-column>
|
||||
<el-table-column align="center" prop="createByName" :show-overflow-tooltip="true" label="申请人"></el-table-column>
|
||||
<el-table-column align="center" prop="version" label="版本号" width="90">
|
||||
<template #default="scope"> v{{ scope.row.version }}.0</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tab === 'running'" v-hasPermi="['workflow:instance:active']" align="center" prop="isSuspended" label="状态" min-width="70">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="!scope.row.isSuspended" type="success">激活</el-tag>
|
||||
<el-tag v-else type="danger">挂起</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流程状态" min-width="80">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="createTime" label="启动时间" width="160"></el-table-column>
|
||||
<el-table-column v-if="tab === 'finish'" align="center" prop="updateTime" label="结束时间" width="160"></el-table-column>
|
||||
<el-table-column label="操作" align="center" :width="165" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-row v-if="tab === 'running'" :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-popover :ref="`popoverRef${scope.$index}`" trigger="click" placement="left" :width="300">
|
||||
<el-input v-model="deleteReason" resize="none" :rows="3" type="textarea" placeholder="请输入作废原因" />
|
||||
<div style="text-align: right; margin: 5px 0px 0px 0px">
|
||||
<el-button size="small" text @click="cancelPopover(scope.$index)">取消</el-button>
|
||||
<el-button size="small" type="primary" @click="handleInvalid(scope.row)">确认</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button type="danger" size="small" icon="CircleClose">作废</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:instance:remove']" type="danger" size="small" icon="Delete" @click="handleDelete(scope.row)">删除 </el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:instance:query']" type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button v-hasPermi="['workflow:instance:variableQuery']" type="primary" size="small" icon="Document" @click="handleInstanceVariable(scope.row)"> 变量 </el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="handleQuery"
|
||||
/>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-dialog v-if="processDefinitionDialog.visible" v-model="processDefinitionDialog.visible" :title="processDefinitionDialog.title" width="70%">
|
||||
<el-table v-loading="loading" :data="processDefinitionHistoryList">
|
||||
<el-table-column fixed align="center" type="index" label="序号" width="60"></el-table-column>
|
||||
<el-table-column fixed align="center" prop="name" label="流程定义名称"></el-table-column>
|
||||
<el-table-column fixed align="center" prop="nodeName" label="任务名称"></el-table-column>
|
||||
<el-table-column align="center" prop="key" label="标识Key"></el-table-column>
|
||||
<el-table-column align="center" prop="version" label="版本号" width="90">
|
||||
<template #default="scope"> v{{ scope.row.version }}.0</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-hasPermi="['workflow:instance:active']" align="center" prop="suspensionState" label="状态" min-width="70">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.suspensionState == 1" type="success">激活</el-tag>
|
||||
<el-tag v-else type="danger">挂起</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="deploymentTime" label="部署时间" :show-overflow-tooltip="true"></el-table-column>
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
<!-- 流程变量开始 -->
|
||||
<el-dialog v-model="variableVisible" v-if="variableVisible" draggable title="流程变量" width="60%" :close-on-click-modal="false">
|
||||
<el-card v-loading="variableLoading">
|
||||
<template #header>
|
||||
<div class="clearfix">
|
||||
<span
|
||||
>流程定义名称:<el-tag>{{ processDefinitionName }}</el-tag></span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<div class="max-h-500px overflow-y-auto">
|
||||
<VueJsonPretty :data="formatToJsonObject(variables)" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card v-loading="variableLoading">
|
||||
<el-form ref="ruleFormRef" :model="form" :inline="true" :rules="rules" label-width="120px">
|
||||
<el-form-item label="变量KEY" prop="key">
|
||||
<el-input v-model="form.key" placeholder="请输入变量KEY" />
|
||||
</el-form-item>
|
||||
<el-form-item label="变量值" prop="value">
|
||||
<el-input v-model="form.value" placeholder="请输入变量值" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button v-hasPermi="['workflow:instance:variable']" type="primary" @click="handleVariable(ruleFormRef)">确认</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-dialog>
|
||||
<!-- 流程变量结束 -->
|
||||
|
||||
<!-- 申请人 -->
|
||||
<UserSelect ref="userSelectRef" :multiple="true" :data="selectUserIds" @confirm-call-back="userSelectCallBack"></UserSelect>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
pageByRunning,
|
||||
pageByFinish,
|
||||
deleteByInstanceIds,
|
||||
deleteHisByInstanceIds,
|
||||
instanceVariable,
|
||||
invalid,
|
||||
updateVariable
|
||||
} from '@/api/workflow/instance';
|
||||
import { categoryTree } from '@/api/workflow/category';
|
||||
import { CategoryTreeVO } from '@/api/workflow/category/types';
|
||||
import { FlowInstanceQuery, FlowInstanceVO } from '@/api/workflow/instance/types';
|
||||
import workflowCommon from '@/api/workflow/workflowCommon';
|
||||
import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
import UserSelect from '@/components/UserSelect/index.vue';
|
||||
import { ElForm, FormInstance } from 'element-plus';
|
||||
//审批记录组件
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const categoryTreeRef = ref<ElTreeInstance>();
|
||||
import { ref } from 'vue';
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
const form = ref<Record<string, any>>({
|
||||
instanceId: undefined,
|
||||
key: undefined,
|
||||
value: undefined
|
||||
});
|
||||
const ruleFormRef = ref<FormInstance>();
|
||||
const userSelectRef = ref<InstanceType<typeof UserSelect>>();
|
||||
// 遮罩层
|
||||
const loading = ref(true);
|
||||
// 选中数组
|
||||
const ids = ref<Array<any>>([]);
|
||||
// 选中实例id数组
|
||||
const instanceIds = ref<Array<number | string>>([]);
|
||||
// 非单个禁用
|
||||
const single = ref(true);
|
||||
// 非多个禁用
|
||||
const multiple = ref(true);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 实例id
|
||||
const instanceId = ref(undefined);
|
||||
|
||||
// 流程变量是否显示
|
||||
const variableVisible = ref(false);
|
||||
const variableLoading = ref(true);
|
||||
const variables = ref<string>('');
|
||||
//流程定义名称
|
||||
const processDefinitionName = ref();
|
||||
// 模型定义表格数据
|
||||
const processInstanceList = ref<FlowInstanceVO[]>([]);
|
||||
const processDefinitionHistoryList = ref<Array<any>>([]);
|
||||
const categoryOptions = ref<CategoryOption[]>([]);
|
||||
const categoryName = ref('');
|
||||
|
||||
const processDefinitionDialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: '流程定义'
|
||||
});
|
||||
|
||||
type CategoryOption = {
|
||||
id: string;
|
||||
categoryName: string;
|
||||
children?: CategoryOption[];
|
||||
};
|
||||
|
||||
const tab = ref('running');
|
||||
// 作废原因
|
||||
const deleteReason = ref('');
|
||||
|
||||
//申请人id
|
||||
const selectUserIds = ref<Array<number | string>>([]);
|
||||
//申请人选择数量
|
||||
const userSelectCount = ref(0);
|
||||
// 查询参数
|
||||
const queryParams = ref<FlowInstanceQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
nodeName: undefined,
|
||||
flowName: undefined,
|
||||
flowCode: undefined,
|
||||
createByIds: [],
|
||||
category: undefined
|
||||
});
|
||||
|
||||
/** 节点单击事件 */
|
||||
const handleNodeClick = (data: CategoryTreeVO) => {
|
||||
queryParams.value.category = data.id;
|
||||
if (data.id === '0') {
|
||||
queryParams.value.category = '';
|
||||
}
|
||||
handleQuery();
|
||||
};
|
||||
/** 通过条件过滤节点 */
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true;
|
||||
return data.categoryName.indexOf(value) !== -1;
|
||||
};
|
||||
/** 根据名称筛选部门树 */
|
||||
watchEffect(
|
||||
() => {
|
||||
categoryTreeRef.value.filter(categoryName.value);
|
||||
},
|
||||
{
|
||||
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
|
||||
}
|
||||
);
|
||||
|
||||
/** 查询流程分类下拉树结构 */
|
||||
const getTreeselect = async () => {
|
||||
const res = await categoryTree();
|
||||
categoryOptions.value = res.data;
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
if ('running' === tab.value) {
|
||||
getProcessInstanceRunningList();
|
||||
} else {
|
||||
getProcessInstanceFinishList();
|
||||
}
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.category = '';
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.pageSize = 10;
|
||||
queryParams.value.createByIds = [];
|
||||
userSelectCount.value = 0;
|
||||
handleQuery();
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: FlowInstanceVO[]) => {
|
||||
ids.value = selection.map((item: any) => item.id);
|
||||
instanceIds.value = selection.map((item: FlowInstanceVO) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
//分页
|
||||
const getProcessInstanceRunningList = () => {
|
||||
loading.value = true;
|
||||
pageByRunning(queryParams.value).then((resp) => {
|
||||
processInstanceList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
//分页
|
||||
const getProcessInstanceFinishList = () => {
|
||||
loading.value = true;
|
||||
pageByFinish(queryParams.value).then((resp) => {
|
||||
processInstanceList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row: FlowInstanceVO) => {
|
||||
const instanceIdList = row.id || instanceIds.value;
|
||||
await proxy?.$modal.confirm('是否确认删除?');
|
||||
loading.value = true;
|
||||
if ('running' === tab.value) {
|
||||
await deleteByInstanceIds(instanceIdList).finally(() => (loading.value = false));
|
||||
getProcessInstanceRunningList();
|
||||
} else {
|
||||
await deleteHisByInstanceIds(instanceIdList).finally(() => (loading.value = false));
|
||||
getProcessInstanceFinishList();
|
||||
}
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
const changeTab = async (data: string) => {
|
||||
processInstanceList.value = [];
|
||||
queryParams.value.pageNum = 1;
|
||||
if ('running' === data.paneName) {
|
||||
getProcessInstanceRunningList();
|
||||
} else {
|
||||
getProcessInstanceFinishList();
|
||||
}
|
||||
};
|
||||
/** 作废按钮操作 */
|
||||
const handleInvalid = async (row: FlowInstanceVO) => {
|
||||
await proxy?.$modal.confirm('是否确认作废?');
|
||||
loading.value = true;
|
||||
if ('running' === tab.value) {
|
||||
const param = {
|
||||
id: row.id,
|
||||
comment: deleteReason.value
|
||||
};
|
||||
await invalid(param).finally(() => (loading.value = false));
|
||||
getProcessInstanceRunningList();
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
}
|
||||
};
|
||||
const cancelPopover = async (index: any) => {
|
||||
(proxy?.$refs[`popoverRef${index}`] as any).hide(); //关闭弹窗
|
||||
};
|
||||
/** 查看按钮操作 */
|
||||
const handleView = (row) => {
|
||||
const routerJumpVo = reactive<RouterJumpVo>({
|
||||
businessId: row.businessId,
|
||||
taskId: row.id,
|
||||
type: 'view',
|
||||
formCustom: row.formCustom,
|
||||
formPath: row.formPath
|
||||
});
|
||||
workflowCommon.routerJump(routerJumpVo, proxy);
|
||||
};
|
||||
|
||||
//查询流程变量
|
||||
const handleInstanceVariable = async (row: FlowInstanceVO) => {
|
||||
instanceId.value = row.id;
|
||||
variableLoading.value = true;
|
||||
variableVisible.value = true;
|
||||
processDefinitionName.value = row.flowName;
|
||||
const data = await instanceVariable(row.id);
|
||||
variables.value = data.data.variable;
|
||||
variableLoading.value = false;
|
||||
form.value.instanceId = undefined;
|
||||
form.value.key = undefined;
|
||||
form.value.value = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* json转为对象
|
||||
* @param data 原始数据
|
||||
*/
|
||||
function formatToJsonObject(data: string) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (error) {
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
//打开申请人选择
|
||||
const openUserSelect = () => {
|
||||
userSelectRef.value.open();
|
||||
};
|
||||
//确认选择申请人
|
||||
const userSelectCallBack = (data: UserVO[]) => {
|
||||
userSelectCount.value = 0;
|
||||
selectUserIds.value = [];
|
||||
queryParams.value.createByIds = [];
|
||||
|
||||
if (data && data.length > 0) {
|
||||
userSelectCount.value = data.length;
|
||||
selectUserIds.value = data.map((item) => item.userId);
|
||||
queryParams.value.createByIds = selectUserIds.value;
|
||||
}
|
||||
};
|
||||
const rules = reactive<Record<string, any>>({
|
||||
key: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入KEY',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入变量值',
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const handleVariable = async (formEl: FormInstance | undefined) => {
|
||||
await formEl.validate(async (valid, fields) => {
|
||||
if (valid) {
|
||||
form.value.instanceId = instanceId.value;
|
||||
await proxy?.$modal.confirm('是否确认提交?');
|
||||
await updateVariable(form.value);
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
const data = await instanceVariable(instanceId.value);
|
||||
variables.value = data.data.variable;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getProcessInstanceRunningList();
|
||||
getTreeselect();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="组件名称" prop="componentName">
|
||||
<el-input v-model="queryParams.componentName" placeholder="请输入组件名称" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="方法名" prop="methodName">
|
||||
<el-input v-model="queryParams.methodName" placeholder="请输入方法名" clearable @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['workflow:spel:add']">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate()" v-hasPermi="['workflow:spel:edit']">修改</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete()" v-hasPermi="['workflow:spel:remove']">删除</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="spelList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column label="序号" type="index" width="60" align="center">
|
||||
<template #default="scope">
|
||||
<span>{{ (queryParams.pageNum - 1) * queryParams.pageSize + scope.$index + 1 }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="组件名称" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.componentName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="方法名称" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.methodName || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="参数名称" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.methodParams || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="SPEL表达式" align="center" prop="viewSpel" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="scope.row.status === '0'">正常</el-tag>
|
||||
<el-tag v-else>停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="备注" align="center">
|
||||
<template #default="scope">
|
||||
{{ scope.row.remark || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['workflow:spel:edit']"></el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="top">
|
||||
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['workflow:spel:remove']"></el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
|
||||
</el-card>
|
||||
<!-- 添加或修改流程spel表达式定义对话框 -->
|
||||
<el-dialog :title="dialog.title" v-model="dialog.visible" width="550px" append-to-body>
|
||||
<el-form ref="spelFormRef" :model="form" :rules="rules" label-width="100px">
|
||||
<!-- 组件名称 -->
|
||||
<el-form-item label="组件名称" prop="componentName">
|
||||
<el-input v-model="form.componentName" placeholder="请输入组件名称" @input="updateViewSpel" />
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="注册到Spring容器中的组件名,如:spelRuleComponent" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
组件名称
|
||||
</span>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item label="方法名称" prop="methodName">
|
||||
<el-input v-model="form.methodName" placeholder="请输入方法名称" @input="updateViewSpel" />
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="组件中的方法名称,如:selectDeptLeaderById" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
方法名称
|
||||
</span>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<el-form-item label="方法参数" prop="methodParams">
|
||||
<el-input v-model="form.methodParams" placeholder="请输入方法参数" @input="updateViewSpel" />
|
||||
<template #label>
|
||||
<span>
|
||||
<el-tooltip content="方法参数,如:deptId, 多个使用 ',' 分隔,单参数变量仅支持单个方法参数" placement="top">
|
||||
<el-icon><question-filled /></el-icon>
|
||||
</el-tooltip>
|
||||
方法参数
|
||||
</span>
|
||||
</template>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 改为只读文本展示 -->
|
||||
<el-form-item label="SPEL表达式">
|
||||
<span class="preview-box">
|
||||
{{ form.viewSpel || '例如:#{@组件名.方法名(#方法参数)} 或 ${方法参数}' }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="form.status">
|
||||
<el-radio v-for="dict in sys_normal_disable" :key="dict.value" :label="dict.value">
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="cancel">取 消</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Spel" lang="ts">
|
||||
import { listSpel, getSpel, delSpel, addSpel, updateSpel } from '@/api/workflow/spel';
|
||||
import { SpelVO, SpelQuery, SpelForm } from '@/api/workflow/spel/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { sys_show_hide, sys_normal_disable } = toRefs<any>(proxy?.useDict('sys_show_hide', 'sys_normal_disable'));
|
||||
|
||||
const spelList = ref<SpelVO[]>([]);
|
||||
const buttonLoading = ref(false);
|
||||
const loading = ref(true);
|
||||
const showSearch = ref(true);
|
||||
const ids = ref<Array<string | number>>([]);
|
||||
const single = ref(true);
|
||||
const multiple = ref(true);
|
||||
const total = ref(0);
|
||||
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const spelFormRef = ref<ElFormInstance>();
|
||||
|
||||
const dialog = reactive<DialogOption>({
|
||||
visible: false,
|
||||
title: ''
|
||||
});
|
||||
|
||||
const initFormData: SpelForm = {
|
||||
id: undefined,
|
||||
componentName: undefined,
|
||||
methodName: undefined,
|
||||
methodParams: undefined,
|
||||
viewSpel: undefined,
|
||||
status: '0',
|
||||
remark: undefined,
|
||||
}
|
||||
const data = reactive<PageData<SpelForm, SpelQuery>>({
|
||||
form: {...initFormData},
|
||||
queryParams: {
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
componentName: undefined,
|
||||
methodName: undefined,
|
||||
methodParams: undefined,
|
||||
viewSpel: undefined,
|
||||
status: '0',
|
||||
params: {
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
status: [
|
||||
{ required: true, message: "状态不能为空", trigger: "change" }
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
const { queryParams, form, rules } = toRefs(data);
|
||||
|
||||
/** 查询流程spel表达式定义列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true;
|
||||
const res = await listSpel(queryParams.value);
|
||||
spelList.value = res.rows;
|
||||
total.value = res.total;
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
/** 取消按钮 */
|
||||
const cancel = () => {
|
||||
reset();
|
||||
dialog.visible = false;
|
||||
}
|
||||
|
||||
/** 表单重置 */
|
||||
const reset = () => {
|
||||
form.value = {...initFormData};
|
||||
spelFormRef.value?.resetFields();
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.value.pageNum = 1;
|
||||
getList();
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
handleQuery();
|
||||
}
|
||||
|
||||
/** 多选框选中数据 */
|
||||
const handleSelectionChange = (selection: SpelVO[]) => {
|
||||
ids.value = selection.map(item => item.id);
|
||||
single.value = selection.length != 1;
|
||||
multiple.value = !selection.length;
|
||||
}
|
||||
|
||||
/** 新增按钮操作 */
|
||||
const handleAdd = () => {
|
||||
reset();
|
||||
dialog.visible = true;
|
||||
dialog.title = "添加流程spel表达式定义";
|
||||
}
|
||||
|
||||
/** 修改按钮操作 */
|
||||
const handleUpdate = async (row?: SpelVO) => {
|
||||
reset();
|
||||
const _id = row?.id || ids.value[0]
|
||||
const res = await getSpel(_id);
|
||||
Object.assign(form.value, res.data);
|
||||
dialog.visible = true;
|
||||
dialog.title = "修改流程spel表达式定义";
|
||||
}
|
||||
|
||||
/** 提交按钮 */
|
||||
const submitForm = () => {
|
||||
spelFormRef.value?.validate(async (valid: boolean) => {
|
||||
if (valid) {
|
||||
buttonLoading.value = true;
|
||||
if (form.value.id) {
|
||||
await updateSpel(form.value).finally(() => buttonLoading.value = false);
|
||||
} else {
|
||||
await addSpel(form.value).finally(() => buttonLoading.value = false);
|
||||
}
|
||||
proxy?.$modal.msgSuccess("操作成功");
|
||||
dialog.visible = false;
|
||||
await getList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row?: SpelVO) => {
|
||||
const _ids = row?.id || ids.value;
|
||||
await proxy?.$modal.confirm('是否确认删除流程spel表达式定义编号为"' + _ids + '"的数据项?').finally(() => loading.value = false);
|
||||
await delSpel(_ids);
|
||||
proxy?.$modal.msgSuccess("删除成功");
|
||||
await getList();
|
||||
}
|
||||
|
||||
/** 控制是否显示 viewSpel 输入框 */
|
||||
const showViewSpelInput = ref(false);
|
||||
|
||||
/** 更新 spel 预览值并决定是否显示输入框 */
|
||||
const updateViewSpel = () => {
|
||||
const comp = (form.value.componentName || '').trim();
|
||||
const method = (form.value.methodName || '').trim();
|
||||
const paramStr = (form.value.methodParams || '').trim();
|
||||
|
||||
if (!comp && !method && !paramStr) {
|
||||
form.value.viewSpel = '';
|
||||
return;
|
||||
}
|
||||
|
||||
// 替换变量值:只有参数存在,组件和方法都不存在
|
||||
if (!comp && !method && paramStr) {
|
||||
const paramList = paramStr.split(',')
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0);
|
||||
|
||||
if (paramList.length === 1) {
|
||||
form.value.viewSpel = `\${${paramList[0]}}`;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果缺少组件或方法,提示填写
|
||||
if (!comp || !method) {
|
||||
form.value.viewSpel = '请填写组件名称和方法名';
|
||||
return;
|
||||
}
|
||||
|
||||
let paramList = [];
|
||||
|
||||
if (paramStr) {
|
||||
// 分割并过滤掉空参数
|
||||
paramList = paramStr.split(',')
|
||||
.map(p => p.trim())
|
||||
.filter(p => p.length > 0);
|
||||
}
|
||||
|
||||
const paramPart = paramList.length > 0
|
||||
? '(' + paramList.map(p => `#${p}`).join(',') + ')'
|
||||
: '()';
|
||||
|
||||
form.value.viewSpel = `#{@${comp}.${method}${paramPart}}`;
|
||||
};
|
||||
|
||||
/** 监听所有字段变化 */
|
||||
watch(
|
||||
() => [form.value.componentName, form.value.methodName, form.value.methodParams],
|
||||
updateViewSpel
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.preview-box {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
background-color: #f5f7fa;
|
||||
border-radius: 4px;
|
||||
color: #333;
|
||||
font-family: monospace; /* 等宽字体更清晰 */
|
||||
white-space: nowrap; /* 禁止换行 */
|
||||
overflow-x: auto; /* 超出宽度时显示水平滚动条 */
|
||||
min-height: 36px; /* 与 el-input 高度对齐 */
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form v-show="showSearch" ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item>
|
||||
<el-badge :value="userSelectCount" :max="10" class="item">
|
||||
<el-button type="primary" @click="openUserSelect">选择申请人</el-button>
|
||||
</el-badge>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务名称" prop="nodeName">
|
||||
<el-input v-model="queryParams.nodeName" placeholder="请输入任务名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义名称" label-width="100" prop="flowName">
|
||||
<el-input v-model="queryParams.flowName" placeholder="请输入流程定义名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5" v-if="tab === 'waiting'">
|
||||
<el-button class="todo-action-btn todo-action-btn--primary" type="primary" plain icon="Edit" :disabled="multiple" @click="handleUserOpen()"
|
||||
>修改办理人
|
||||
</el-button>
|
||||
<el-button class="todo-action-btn todo-action-btn--warning" type="warning" plain icon="Bell" :disabled="multiple" @click="handleUrgeTaskOpen()"
|
||||
>催办
|
||||
</el-button>
|
||||
</el-col>
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
<el-tabs v-model="tab" @tab-click="changeTab">
|
||||
<el-tab-pane name="waiting" label="待办任务"> </el-tab-pane>
|
||||
<el-tab-pane name="finish" label="已办任务"> </el-tab-pane>
|
||||
<el-table v-loading="loading" border :data="taskList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column align="center" type="index" label="序号" width="60"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessCode" align="center" label="业务编码"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessTitle" align="center" label="业务标题"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="flowName" align="center" width="120" label="流程定义名称"></el-table-column>
|
||||
<el-table-column align="center" prop="flowCode" width="120" label="流程定义编码"></el-table-column>
|
||||
<el-table-column align="center" prop="categoryName" label="流程分类"></el-table-column>
|
||||
<el-table-column align="center" prop="version" label="版本号" width="90">
|
||||
<template #default="scope"> v{{ scope.row.version }}.0</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="nodeName" :show-overflow-tooltip="true" label="任务名称"></el-table-column>
|
||||
<el-table-column align="center" prop="createByName" :show-overflow-tooltip="true" label="申请人"></el-table-column>
|
||||
<el-table-column align="center" label="办理人">
|
||||
<template #default="scope">
|
||||
<template v-if="tab === 'waiting'">
|
||||
<template v-if="scope.row.assigneeNames">
|
||||
<el-tag v-for="(name, index) in scope.row.assigneeNames.split(',')" :key="index" type="success">
|
||||
{{ name }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag type="success"> 无</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag type="success"> {{ scope.row.approveName }}</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流程状态" prop="flowStatus" min-width="70">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tab === 'finish'" align="center" label="任务状态" prop="flowTaskStatus" min-width="70">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_task_status" :value="scope.row.flowTaskStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="createTime" label="创建时间" width="160"></el-table-column>
|
||||
<el-table-column label="操作" align="center" :width="tab === 'finish' ? '88' : '188'">
|
||||
<template #default="scope">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5" v-if="tab === 'waiting' || tab === 'finish'">
|
||||
<el-button type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5" v-if="tab === 'waiting'">
|
||||
<el-button type="primary" size="small" icon="Setting" @click="handleMeddle(scope.row)">流程干预 </el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="handleQuery"
|
||||
/>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
<!-- 选人组件 -->
|
||||
<UserSelect ref="userSelectRef" :multiple="userMultiple" @confirm-call-back="submitCallback"></UserSelect>
|
||||
<!-- 流程干预组件 -->
|
||||
<processMeddle ref="processMeddleRef" @submitCallback="getWaitingList"></processMeddle>
|
||||
<!-- 申请人 -->
|
||||
<UserSelect ref="applyUserSelectRef" :multiple="true" :data="selectUserIds" @confirm-call-back="userSelectCallBack"></UserSelect>
|
||||
<!-- 流程干预组件 -->
|
||||
<messageType ref="messageTypeRef" @submitCallback="handleUserTask"></messageType>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pageByAllTaskWait, pageByAllTaskFinish, updateAssignee, urgeTask } from '@/api/workflow/task';
|
||||
import UserSelect from '@/components/UserSelect';
|
||||
import { TaskQuery } from '@/api/workflow/task/types';
|
||||
import workflowCommon from '@/api/workflow/workflowCommon';
|
||||
import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
|
||||
import processMeddle from '@/components/Process/processMeddle';
|
||||
import messageType from '@/components/Process/MessageType';
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
import { TabsPaneContext } from 'element-plus';
|
||||
//选人组件
|
||||
const userSelectRef = ref<InstanceType<typeof UserSelect>>();
|
||||
//流程干预组件
|
||||
const processMeddleRef = ref<InstanceType<typeof processMeddle>>();
|
||||
//选人组件
|
||||
const applyUserSelectRef = ref<InstanceType<typeof UserSelect>>();
|
||||
//消息组件
|
||||
const messageTypeRef = ref<InstanceType<typeof messageType>>();
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
|
||||
const { wf_task_status } = toRefs<any>(proxy?.useDict('wf_task_status'));
|
||||
// 遮罩层
|
||||
const loading = ref(true);
|
||||
// 选中数组
|
||||
const ids = ref<Array<any>>([]);
|
||||
// 非单个禁用
|
||||
const single = ref(true);
|
||||
// 非多个禁用
|
||||
const multiple = ref(true);
|
||||
const userMultiple = ref(false);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 模型定义表格数据
|
||||
const taskList = ref([]);
|
||||
const buttonType = ref('');
|
||||
//申请人id
|
||||
const selectUserIds = ref<Array<number | string>>([]);
|
||||
//申请人选择数量
|
||||
const userSelectCount = ref(0);
|
||||
// 查询参数
|
||||
const queryParams = ref<TaskQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
nodeName: undefined,
|
||||
flowName: undefined,
|
||||
flowCode: undefined,
|
||||
createByIds: []
|
||||
});
|
||||
const tab = ref('waiting');
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
if ('waiting' === tab.value) {
|
||||
getWaitingList();
|
||||
} else {
|
||||
getFinishList();
|
||||
}
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.pageSize = 10;
|
||||
queryParams.value.createByIds = [];
|
||||
userSelectCount.value = 0;
|
||||
selectUserIds.value = [];
|
||||
handleQuery();
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: any) => {
|
||||
ids.value = selection.map((item: any) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
const changeTab = async (data: TabsPaneContext) => {
|
||||
taskList.value = [];
|
||||
queryParams.value.pageNum = 1;
|
||||
if ('waiting' === data.paneName) {
|
||||
getWaitingList();
|
||||
} else {
|
||||
getFinishList();
|
||||
}
|
||||
};
|
||||
//分页
|
||||
const getWaitingList = () => {
|
||||
loading.value = true;
|
||||
pageByAllTaskWait(queryParams.value).then((resp) => {
|
||||
taskList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
const getFinishList = () => {
|
||||
loading.value = true;
|
||||
pageByAllTaskFinish(queryParams.value).then((resp) => {
|
||||
taskList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
// 打开催办
|
||||
const handleUrgeTaskOpen = () => {
|
||||
messageTypeRef.value.open();
|
||||
};
|
||||
//打开修改选人
|
||||
const handleUserOpen = () => {
|
||||
userSelectRef.value.open();
|
||||
};
|
||||
|
||||
//打开修改选人
|
||||
const handleUserTask = async (data) => {
|
||||
await proxy?.$modal.confirm('是否确认提交?');
|
||||
data.taskIdList = ids.value;
|
||||
await urgeTask(data);
|
||||
messageTypeRef.value.close();
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
handleQuery();
|
||||
};
|
||||
//修改办理人
|
||||
const submitCallback = async (data) => {
|
||||
if (data && data.length > 0) {
|
||||
await proxy?.$modal.confirm('是否确认提交?');
|
||||
loading.value = true;
|
||||
await updateAssignee(ids.value, data[0].userId);
|
||||
handleQuery();
|
||||
proxy?.$modal.msgSuccess('操作成功');
|
||||
} else {
|
||||
proxy?.$modal.msgWarning('请选择用户!');
|
||||
}
|
||||
};
|
||||
/** 查看按钮操作 */
|
||||
const handleView = (row) => {
|
||||
const routerJumpVo = reactive<RouterJumpVo>({
|
||||
businessId: row.businessId,
|
||||
taskId: row.id,
|
||||
type: 'view',
|
||||
formCustom: row.formCustom,
|
||||
formPath: row.formPath
|
||||
});
|
||||
workflowCommon.routerJump(routerJumpVo, proxy);
|
||||
};
|
||||
const handleMeddle = (row) => {
|
||||
processMeddleRef.value.open(row.id);
|
||||
};
|
||||
//打开申请人选择
|
||||
const openUserSelect = () => {
|
||||
applyUserSelectRef.value.open();
|
||||
};
|
||||
//确认选择申请人
|
||||
const userSelectCallBack = (data: UserVO[]) => {
|
||||
userSelectCount.value = 0;
|
||||
selectUserIds.value = [];
|
||||
queryParams.value.createByIds = [];
|
||||
|
||||
if (data && data.length > 0) {
|
||||
userSelectCount.value = data.length;
|
||||
selectUserIds.value = data.map((item) => item.userId);
|
||||
queryParams.value.createByIds = selectUserIds.value;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
getWaitingList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.todo-action-btn {
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border-radius: var(--app-radius-md);
|
||||
font-weight: 600;
|
||||
--el-button-bg-color: var(--el-fill-color-lighter);
|
||||
--el-button-border-color: var(--el-border-color-light);
|
||||
--el-button-text-color: var(--el-text-color-regular);
|
||||
--el-button-hover-bg-color: var(--el-fill-color-light);
|
||||
--el-button-hover-border-color: var(--el-border-color);
|
||||
}
|
||||
|
||||
.todo-action-btn--primary {
|
||||
--el-button-text-color: var(--el-color-primary);
|
||||
--el-button-border-color: var(--el-color-primary-light-5);
|
||||
--el-button-bg-color: var(--el-color-primary-light-9);
|
||||
--el-button-hover-bg-color: var(--el-color-primary-light-8);
|
||||
--el-button-hover-border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.todo-action-btn--warning {
|
||||
--el-button-text-color: var(--el-color-warning);
|
||||
--el-button-border-color: var(--el-color-warning-light-5);
|
||||
--el-button-bg-color: var(--el-color-warning-light-9);
|
||||
--el-button-hover-bg-color: var(--el-color-warning-light-8);
|
||||
--el-button-hover-border-color: var(--el-color-warning-light-5);
|
||||
}
|
||||
|
||||
:global(html.dark) {
|
||||
.todo-action-btn {
|
||||
--el-button-bg-color: rgba(148, 163, 184, 0.12);
|
||||
--el-button-border-color: rgba(148, 163, 184, 0.3);
|
||||
--el-button-text-color: var(--el-text-color-regular);
|
||||
--el-button-hover-bg-color: rgba(148, 163, 184, 0.2);
|
||||
--el-button-hover-border-color: rgba(148, 163, 184, 0.45);
|
||||
}
|
||||
|
||||
.todo-action-btn--primary {
|
||||
--el-button-bg-color: rgba(99, 113, 150, 0.14);
|
||||
--el-button-border-color: rgba(99, 113, 150, 0.45);
|
||||
--el-button-text-color: #c4cfdd;
|
||||
--el-button-hover-bg-color: rgba(99, 113, 150, 0.22);
|
||||
--el-button-hover-border-color: rgba(99, 113, 150, 0.65);
|
||||
}
|
||||
|
||||
.todo-action-btn--warning {
|
||||
--el-button-bg-color: rgba(214, 149, 59, 0.16);
|
||||
--el-button-border-color: rgba(214, 149, 59, 0.55);
|
||||
--el-button-text-color: #ffe6bb;
|
||||
--el-button-hover-bg-color: rgba(214, 149, 59, 0.24);
|
||||
--el-button-hover-border-color: rgba(214, 149, 59, 0.75);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<el-row :gutter="20">
|
||||
<!-- 流程分类树 -->
|
||||
<el-col :lg="4" :xs="24" style="">
|
||||
<el-card shadow="hover">
|
||||
<el-input v-model="categoryName" placeholder="请输入流程分类名" prefix-icon="Search" clearable />
|
||||
<el-tree
|
||||
ref="categoryTreeRef"
|
||||
class="mt-2"
|
||||
node-key="id"
|
||||
:data="categoryOptions"
|
||||
:props="{ label: 'label', children: 'children' } as any"
|
||||
:expand-on-click-node="false"
|
||||
:filter-node-method="filterNode"
|
||||
highlight-current
|
||||
default-expand-all
|
||||
@node-click="handleNodeClick"
|
||||
></el-tree>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :lg="20" :xs="24">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form v-show="showSearch" ref="queryFormRef" :model="queryParams" :inline="true" label-width="120px">
|
||||
<el-form-item label="流程定义编码" prop="flowCode">
|
||||
<el-input v-model="queryParams.flowCode" placeholder="请输入流程定义编码" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="processInstanceList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column align="center" type="index" label="序号" width="60"></el-table-column>
|
||||
<el-table-column v-if="false" align="center" prop="id" label="id"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="flowName" align="center" label="流程定义名称"> </el-table-column>
|
||||
<el-table-column align="center" prop="flowCode" label="流程定义编码"></el-table-column>
|
||||
<el-table-column align="center" prop="categoryName" label="流程分类"></el-table-column>
|
||||
<el-table-column align="center" prop="version" label="版本号" width="90">
|
||||
<template #default="scope"> v{{ scope.row.version }}.0</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="tab === 'running'" align="center" prop="isSuspended" label="状态" min-width="70">
|
||||
<template #default="scope">
|
||||
<el-tag v-if="!scope.row.isSuspended" type="success">激活</el-tag>
|
||||
<el-tag v-else type="danger">挂起</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流程状态" min-width="70">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="createTime" label="启动时间" width="160"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="162">
|
||||
<template #default="scope">
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5" v-if="scope.row.flowStatus === 'draft' || scope.row.flowStatus === 'cancel' || scope.row.flowStatus === 'back'">
|
||||
<el-button type="primary" size="small" icon="Edit" @click="handleOpen(scope.row, 'update')">编辑</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5" v-if="scope.row.flowStatus === 'draft' || scope.row.flowStatus === 'cancel' || scope.row.flowStatus === 'back'">
|
||||
<el-button type="primary" size="small" icon="Delete" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="primary" size="small" icon="View" @click="handleOpen(scope.row, 'view')">查看</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5" v-if="scope.row.flowStatus === 'waiting'">
|
||||
<el-button type="primary" size="small" icon="Notification" @click="handleCancelProcessApply(scope.row.businessId)"
|
||||
>撤销</el-button
|
||||
>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<!-- 提交组件 -->
|
||||
<submitVerify ref="submitVerifyRef" @submit-callback="getList" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pageByCurrent, deleteByInstanceIds, cancelProcessApply } from '@/api/workflow/instance';
|
||||
import { categoryTree } from '@/api/workflow/category';
|
||||
import { CategoryTreeVO } from '@/api/workflow/category/types';
|
||||
import { FlowInstanceQuery, FlowInstanceVO } from '@/api/workflow/instance/types';
|
||||
import workflowCommon from '@/api/workflow/workflowCommon';
|
||||
import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const categoryTreeRef = ref<ElTreeInstance>();
|
||||
|
||||
// 遮罩层
|
||||
const loading = ref(true);
|
||||
// 选中数组
|
||||
const businessIds = ref<Array<number | string>>([]);
|
||||
const instanceIds = ref<Array<number | string>>([]);
|
||||
// 非单个禁用
|
||||
const single = ref(true);
|
||||
// 非多个禁用
|
||||
const multiple = ref(true);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 模型定义表格数据
|
||||
const processInstanceList = ref<FlowInstanceVO[]>([]);
|
||||
|
||||
const categoryOptions = ref<CategoryTreeVO[]>([]);
|
||||
const categoryName = ref('');
|
||||
|
||||
const tab = ref('running');
|
||||
// 查询参数
|
||||
const queryParams = ref<FlowInstanceQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
flowCode: undefined,
|
||||
category: undefined
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
getList();
|
||||
getTreeselect();
|
||||
});
|
||||
|
||||
/** 节点单击事件 */
|
||||
const handleNodeClick = (data: CategoryTreeVO) => {
|
||||
queryParams.value.category = data.id;
|
||||
if (data.id === '0') {
|
||||
queryParams.value.category = '';
|
||||
}
|
||||
handleQuery();
|
||||
};
|
||||
/** 通过条件过滤节点 */
|
||||
const filterNode = (value: string, data: any) => {
|
||||
if (!value) return true;
|
||||
return data.categoryName.indexOf(value) !== -1;
|
||||
};
|
||||
/** 根据名称筛选部门树 */
|
||||
watchEffect(
|
||||
() => {
|
||||
categoryTreeRef.value.filter(categoryName.value);
|
||||
},
|
||||
{
|
||||
flush: 'post' // watchEffect会在DOM挂载或者更新之前就会触发,此属性控制在DOM元素更新后运行
|
||||
}
|
||||
);
|
||||
|
||||
/** 查询流程分类下拉树结构 */
|
||||
const getTreeselect = async () => {
|
||||
const res = await categoryTree();
|
||||
categoryOptions.value = res.data;
|
||||
};
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.category = '';
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.pageSize = 10;
|
||||
handleQuery();
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: FlowInstanceVO[]) => {
|
||||
businessIds.value = selection.map((item: any) => item.businessId);
|
||||
instanceIds.value = selection.map((item: FlowInstanceVO) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
//分页
|
||||
const getList = () => {
|
||||
loading.value = true;
|
||||
pageByCurrent(queryParams.value).then((resp) => {
|
||||
processInstanceList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (row: FlowInstanceVO) => {
|
||||
const instanceIdList = row.id || instanceIds.value;
|
||||
await proxy?.$modal.confirm('是否确认删除?');
|
||||
loading.value = true;
|
||||
if ('running' === tab.value) {
|
||||
await deleteByInstanceIds(instanceIdList).finally(() => (loading.value = false));
|
||||
getList();
|
||||
}
|
||||
proxy?.$modal.msgSuccess('删除成功');
|
||||
};
|
||||
|
||||
/** 撤销按钮操作 */
|
||||
const handleCancelProcessApply = async (businessId: string) => {
|
||||
await proxy?.$modal.confirm('是否确认撤销当前单据?');
|
||||
loading.value = true;
|
||||
if ('running' === tab.value) {
|
||||
const data = {
|
||||
businessId: businessId,
|
||||
message: '申请人撤销流程!'
|
||||
};
|
||||
await cancelProcessApply(data).finally(() => (loading.value = false));
|
||||
getList();
|
||||
}
|
||||
proxy?.$modal.msgSuccess('撤销成功');
|
||||
};
|
||||
|
||||
//办理
|
||||
const handleOpen = async (row, type) => {
|
||||
const routerJumpVo = reactive<RouterJumpVo>({
|
||||
businessId: row.businessId,
|
||||
taskId: row.id,
|
||||
type: type,
|
||||
formCustom: row.formCustom,
|
||||
formPath: row.formPath
|
||||
});
|
||||
workflowCommon.routerJump(routerJumpVo, proxy);
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form v-show="showSearch" ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item label="任务名称" prop="nodeName">
|
||||
<el-input v-model="queryParams.nodeName" placeholder="请输入任务名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义名称" label-width="100" prop="flowName">
|
||||
<el-input v-model="queryParams.flowName" placeholder="请输入流程定义名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义编码" label-width="100" prop="flowCode">
|
||||
<el-input v-model="queryParams.flowCode" placeholder="请输入流程定义编码" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="taskList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column align="center" type="index" label="序号" width="60"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessCode" align="center" label="业务编码"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessTitle" align="center" label="业务标题"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="flowName" align="center" label="流程定义名称"></el-table-column>
|
||||
<el-table-column align="center" prop="flowCode" label="流程定义编码"></el-table-column>
|
||||
<el-table-column align="center" prop="categoryName" label="流程分类"></el-table-column>
|
||||
<el-table-column align="center" prop="version" label="版本号" width="90">
|
||||
<template #default="scope"> v{{ scope.row.version }}.0</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="nodeName" label="任务名称"></el-table-column>
|
||||
<el-table-column align="center" label="流程状态" min-width="70">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="handleQuery"
|
||||
/>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pageByTaskCopy } from '@/api/workflow/task';
|
||||
import { TaskQuery } from '@/api/workflow/task/types';
|
||||
import workflowCommon from '@/api/workflow/workflowCommon';
|
||||
import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
|
||||
//审批记录组件
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
|
||||
// 遮罩层
|
||||
const loading = ref(true);
|
||||
// 选中数组
|
||||
const ids = ref<Array<any>>([]);
|
||||
// 非单个禁用
|
||||
const single = ref(true);
|
||||
// 非多个禁用
|
||||
const multiple = ref(true);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 模型定义表格数据
|
||||
const taskList = ref([]);
|
||||
// 查询参数
|
||||
const queryParams = ref<TaskQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
nodeName: undefined,
|
||||
flowName: undefined,
|
||||
flowCode: undefined
|
||||
});
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getTaskCopyList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.pageSize = 10;
|
||||
handleQuery();
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: any) => {
|
||||
ids.value = selection.map((item: any) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
//分页
|
||||
const getTaskCopyList = () => {
|
||||
loading.value = true;
|
||||
pageByTaskCopy(queryParams.value).then((resp) => {
|
||||
taskList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
/** 查看按钮操作 */
|
||||
const handleView = (row) => {
|
||||
const routerJumpVo = reactive<RouterJumpVo>({
|
||||
businessId: row.businessId,
|
||||
taskId: row.id,
|
||||
type: 'view',
|
||||
formCustom: row.formCustom,
|
||||
formPath: row.formPath
|
||||
});
|
||||
workflowCommon.routerJump(routerJumpVo, proxy);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getTaskCopyList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form v-show="showSearch" ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item>
|
||||
<el-badge :value="userSelectCount" :max="10" class="item">
|
||||
<el-button type="primary" @click="openUserSelect">选择申请人</el-button>
|
||||
</el-badge>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务名称" prop="nodeName">
|
||||
<el-input v-model="queryParams.nodeName" placeholder="请输入任务名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义名称" label-width="100" prop="flowName">
|
||||
<el-input v-model="queryParams.flowName" placeholder="请输入流程定义名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义编码" label-width="100" prop="flowCode">
|
||||
<el-input v-model="queryParams.flowCode" placeholder="请输入流程定义编码" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="taskList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column align="center" type="index" label="序号" width="60"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessCode" align="center" label="业务编码"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessTitle" align="center" label="业务标题"></el-table-column>
|
||||
<el-table-column align="center" prop="flowName" width="120" label="流程定义名称"></el-table-column>
|
||||
<el-table-column align="center" prop="flowCode" width="120" label="流程定义编码"></el-table-column>
|
||||
<el-table-column align="center" prop="categoryName" label="流程分类"></el-table-column>
|
||||
<el-table-column align="center" prop="version" label="版本号" width="90">
|
||||
<template #default="scope"> v{{ scope.row.version }}.0</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="nodeName" :show-overflow-tooltip="true" label="任务名称"></el-table-column>
|
||||
<el-table-column align="center" prop="createByName" :show-overflow-tooltip="true" label="申请人"></el-table-column>
|
||||
<el-table-column align="center" prop="approverName" label="办理人">
|
||||
<template #default="scope">
|
||||
<el-tag type="success">
|
||||
{{ scope.row.approveName || '无' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流程状态" prop="flowStatus" min-width="80">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="任务状态" prop="flowTaskStatus" min-width="80">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_task_status" :value="scope.row.flowTaskStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="createTime" label="创建时间" :show-overflow-tooltip="true" width="150"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" icon="View" @click="handleView(scope.row)">查看</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="handleQuery"
|
||||
/>
|
||||
</el-card>
|
||||
<!-- 申请人 -->
|
||||
<UserSelect ref="userSelectRef" :multiple="true" :data="selectUserIds" @confirm-call-back="userSelectCallBack"></UserSelect>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pageByTaskFinish } from '@/api/workflow/task';
|
||||
import { TaskQuery, FlowTaskVO } from '@/api/workflow/task/types';
|
||||
import workflowCommon from '@/api/workflow/workflowCommon';
|
||||
import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
|
||||
//审批记录组件
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
|
||||
const { wf_task_status } = toRefs<any>(proxy?.useDict('wf_task_status'));
|
||||
import UserSelect from '@/components/UserSelect';
|
||||
import { ref } from 'vue';
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
|
||||
const userSelectRef = ref<InstanceType<typeof UserSelect>>();
|
||||
// 遮罩层
|
||||
const loading = ref(true);
|
||||
// 选中数组
|
||||
const ids = ref<Array<any>>([]);
|
||||
// 非单个禁用
|
||||
const single = ref(true);
|
||||
// 非多个禁用
|
||||
const multiple = ref(true);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 模型定义表格数据
|
||||
const taskList = ref([]);
|
||||
// 查询参数
|
||||
const queryParams = ref<TaskQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
nodeName: undefined,
|
||||
flowName: undefined,
|
||||
flowCode: undefined,
|
||||
createByIds: []
|
||||
});
|
||||
//申请人id
|
||||
const selectUserIds = ref<Array<number | string>>([]);
|
||||
//申请人选择数量
|
||||
const userSelectCount = ref(0);
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getFinishList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.pageSize = 10;
|
||||
queryParams.value.createByIds = [];
|
||||
userSelectCount.value = 0;
|
||||
selectUserIds.value = [];
|
||||
handleQuery();
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: any) => {
|
||||
ids.value = selection.map((item: any) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
const getFinishList = () => {
|
||||
loading.value = true;
|
||||
pageByTaskFinish(queryParams.value).then((resp) => {
|
||||
taskList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
/** 查看按钮操作 */
|
||||
const handleView = (row: FlowTaskVO) => {
|
||||
const routerJumpVo = reactive<RouterJumpVo>({
|
||||
businessId: row.businessId,
|
||||
taskId: row.id,
|
||||
type: 'view',
|
||||
formCustom: row.formCustom,
|
||||
formPath: row.formPath
|
||||
});
|
||||
workflowCommon.routerJump(routerJumpVo, proxy);
|
||||
};
|
||||
//打开申请人选择
|
||||
const openUserSelect = () => {
|
||||
userSelectRef.value.open();
|
||||
};
|
||||
//确认选择申请人
|
||||
const userSelectCallBack = (data: UserVO[]) => {
|
||||
userSelectCount.value = 0;
|
||||
selectUserIds.value = [];
|
||||
queryParams.value.createByIds = [];
|
||||
|
||||
if (data && data.length > 0) {
|
||||
userSelectCount.value = data.length;
|
||||
selectUserIds.value = data.map((item) => item.userId);
|
||||
queryParams.value.createByIds = selectUserIds.value;
|
||||
}
|
||||
};
|
||||
onMounted(() => {
|
||||
getFinishList();
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<div class="p-2">
|
||||
<transition :enter-active-class="proxy?.animate.searchAnimate.enter" :leave-active-class="proxy?.animate.searchAnimate.leave">
|
||||
<div v-show="showSearch" class="mb-[10px]">
|
||||
<el-card shadow="hover">
|
||||
<el-form v-show="showSearch" ref="queryFormRef" :model="queryParams" :inline="true">
|
||||
<el-form-item>
|
||||
<el-badge :value="userSelectCount" :max="10" class="item">
|
||||
<el-button type="primary" @click="openUserSelect">选择申请人</el-button>
|
||||
</el-badge>
|
||||
</el-form-item>
|
||||
<el-form-item label="任务名称" prop="nodeName">
|
||||
<el-input v-model="queryParams.nodeName" placeholder="请输入任务名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义名称" label-width="100" prop="flowName">
|
||||
<el-input v-model="queryParams.flowName" placeholder="请输入流程定义名称" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item label="流程定义编码" label-width="100" prop="flowCode">
|
||||
<el-input v-model="queryParams.flowCode" placeholder="请输入流程定义编码" @keyup.enter="handleQuery" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
|
||||
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</transition>
|
||||
<el-card shadow="hover">
|
||||
<template #header>
|
||||
<el-row :gutter="10" class="mb8">
|
||||
<right-toolbar v-model:show-search="showSearch" @query-table="handleQuery"></right-toolbar>
|
||||
</el-row>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" border :data="taskList" @selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" align="center" />
|
||||
<el-table-column align="center" type="index" label="序号" width="60"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessCode" align="center" label="业务编码"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="businessTitle" align="center" label="业务标题"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" prop="flowName" align="center" label="流程定义名称"></el-table-column>
|
||||
<el-table-column align="center" prop="flowCode" label="流程定义编码"></el-table-column>
|
||||
<el-table-column align="center" prop="categoryName" label="流程分类"></el-table-column>
|
||||
<el-table-column align="center" prop="nodeName" label="任务名称"></el-table-column>
|
||||
<el-table-column align="center" prop="createByName" label="申请人"></el-table-column>
|
||||
<el-table-column align="center" label="办理人">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.assigneeNames">
|
||||
<el-tag v-for="(name, index) in scope.row.assigneeNames.split(',')" :key="index" type="success">
|
||||
{{ name }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-tag type="success"> 无</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="流程状态" prop="flowStatusName" min-width="70">
|
||||
<template #default="scope">
|
||||
<dict-tag :options="wf_business_status" :value="scope.row.flowStatus"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" prop="createTime" label="创建时间" width="160"></el-table-column>
|
||||
<el-table-column label="操作" align="center" width="200">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" icon="Edit" @click="handleOpen(scope.row)">办理</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<pagination
|
||||
v-show="total > 0"
|
||||
v-model:page="queryParams.pageNum"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
:total="total"
|
||||
@pagination="handleQuery"
|
||||
/>
|
||||
</el-card>
|
||||
<!-- 申请人 -->
|
||||
<UserSelect ref="userSelectRef" :multiple="true" :data="selectUserIds" @confirm-call-back="userSelectCallBack"></UserSelect>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { pageByTaskWait } from '@/api/workflow/task';
|
||||
import { TaskQuery, FlowTaskVO } from '@/api/workflow/task/types';
|
||||
import workflowCommon from '@/api/workflow/workflowCommon';
|
||||
import { RouterJumpVo } from '@/api/workflow/workflowCommon/types';
|
||||
|
||||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||||
const { wf_business_status } = toRefs<any>(proxy?.useDict('wf_business_status'));
|
||||
import UserSelect from '@/components/UserSelect';
|
||||
import { ref } from 'vue';
|
||||
import { UserVO } from '@/api/system/user/types';
|
||||
|
||||
const userSelectRef = ref<InstanceType<typeof UserSelect>>();
|
||||
//提交组件
|
||||
const queryFormRef = ref<ElFormInstance>();
|
||||
// 遮罩层
|
||||
const loading = ref(true);
|
||||
// 选中数组
|
||||
const ids = ref<Array<any>>([]);
|
||||
// 非单个禁用
|
||||
const single = ref(true);
|
||||
// 非多个禁用
|
||||
const multiple = ref(true);
|
||||
// 显示搜索条件
|
||||
const showSearch = ref(true);
|
||||
// 总条数
|
||||
const total = ref(0);
|
||||
// 模型定义表格数据
|
||||
const taskList = ref([]);
|
||||
|
||||
//申请人id
|
||||
const selectUserIds = ref<Array<number | string>>([]);
|
||||
//申请人选择数量
|
||||
const userSelectCount = ref(0);
|
||||
// 查询参数
|
||||
const queryParams = ref<TaskQuery>({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
nodeName: undefined,
|
||||
flowName: undefined,
|
||||
flowCode: undefined,
|
||||
createByIds: []
|
||||
});
|
||||
onMounted(() => {
|
||||
getWaitingList();
|
||||
});
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
getWaitingList();
|
||||
};
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value?.resetFields();
|
||||
queryParams.value.pageNum = 1;
|
||||
queryParams.value.pageSize = 10;
|
||||
queryParams.value.createByIds = [];
|
||||
userSelectCount.value = 0;
|
||||
selectUserIds.value = [];
|
||||
handleQuery();
|
||||
};
|
||||
// 多选框选中数据
|
||||
const handleSelectionChange = (selection: any) => {
|
||||
ids.value = selection.map((item: any) => item.id);
|
||||
single.value = selection.length !== 1;
|
||||
multiple.value = !selection.length;
|
||||
};
|
||||
//分页
|
||||
const getWaitingList = () => {
|
||||
loading.value = true;
|
||||
pageByTaskWait(queryParams.value).then((resp) => {
|
||||
taskList.value = resp.rows;
|
||||
total.value = resp.total;
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
//办理
|
||||
const handleOpen = async (row: FlowTaskVO) => {
|
||||
const routerJumpVo = reactive<RouterJumpVo>({
|
||||
businessId: row.businessId,
|
||||
taskId: row.id,
|
||||
type: 'approval',
|
||||
formCustom: row.formCustom,
|
||||
formPath: row.formPath
|
||||
});
|
||||
workflowCommon.routerJump(routerJumpVo, proxy);
|
||||
};
|
||||
//打开申请人选择
|
||||
const openUserSelect = () => {
|
||||
userSelectRef.value.open();
|
||||
};
|
||||
//确认选择申请人
|
||||
const userSelectCallBack = (data: UserVO[]) => {
|
||||
userSelectCount.value = 0;
|
||||
selectUserIds.value = [];
|
||||
queryParams.value.createByIds = [];
|
||||
|
||||
if (data && data.length > 0) {
|
||||
userSelectCount.value = data.length;
|
||||
selectUserIds.value = data.map((item) => item.userId);
|
||||
queryParams.value.createByIds = selectUserIds.value;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
Reference in New Issue
Block a user