This commit is contained in:
孟帅
2022-11-24 23:37:34 +08:00
parent 4ffe54b6ac
commit 29bda0dcdd
1487 changed files with 97869 additions and 96539 deletions

View File

@@ -0,0 +1,105 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="关于">
{{ name }} 是一个基于 goframe2vue3vite2TypeScriptuinapp
的中后台解决方案它可以帮助你快速搭建企业级中后台项目相信不管是从新技术使用还是其他方面都能帮助到你持续更新中
</n-card>
</div>
<n-card
:bordered="false"
title="项目信息"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-descriptions bordered label-placement="left" class="py-2">
<n-descriptions-item label="版本">
<n-tag type="info"> 2.0.1 </n-tag>
</n-descriptions-item>
<n-descriptions-item label="最后编译时间">
<n-tag type="info"> {{ lastBuildTime }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="文档地址">
<div class="flex items-center">
<a href="https://github.com/bufanyun/hotgo" class="py-2" target="_blank"
>查看文档地址</a
>
</div>
</n-descriptions-item>
<n-descriptions-item label="预览地址">
<div class="flex items-center">
<a href="https://hotgo.facms.cn/admin" class="py-2" target="_blank"
>查看预览地址</a
>
</div>
</n-descriptions-item>
<n-descriptions-item label="Github">
<div class="flex items-center">
<a href="https://github.com/bufanyun/hotgo" class="py-2" target="_blank"
>查看Github地址</a
>
</div>
</n-descriptions-item>
<n-descriptions-item label="QQ交流群">
<div class="flex items-center">
<a href="https://jq.qq.com/?_wv=1027&k=izJg29Cx" class="py-2" target="_blank"
>点击链接加入群聊HotGo 交流群</a
>
</div>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
title="开发环境依赖"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-descriptions bordered label-placement="left" class="py-2">
<n-descriptions-item v-for="item in devSchema" :key="item.field" :label="item.field">
{{ item.label }}
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
title="生产环境依赖"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-descriptions bordered label-placement="left" class="py-2">
<n-descriptions-item v-for="item in schema" :key="item.field" :label="item.field">
{{ item.label }}
</n-descriptions-item>
</n-descriptions>
</n-card>
</div>
</template>
<script lang="ts" setup>
export interface schemaItem {
field: string;
label: string;
}
const { pkg, lastBuildTime } = __APP_INFO__;
const { dependencies, devDependencies, name } = pkg;
const schema: schemaItem[] = [];
const devSchema: schemaItem[] = [];
Object.keys(dependencies).forEach((key) => {
schema.push({ field: key, label: dependencies[key] });
});
Object.keys(devDependencies).forEach((key) => {
devSchema.push({ field: key, label: devDependencies[key] });
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,90 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
key: 'id',
},
{
title: '应用',
key: 'appId',
},
{
title: '会员ID',
key: 'memberId',
},
{
title: '驱动',
key: 'drive',
render(row) {
return row.drive;
},
},
{
title: '上传类型',
key: 'kind',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.kind == 'images' ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.kind,
}
);
},
},
{
title: '文件',
key: 'fileUrl',
width: 80,
render(row) {
return h(NAvatar, {
size: 40,
src: row.fileUrl,
});
},
},
{
title: '本地路径',
key: 'path',
},
{
title: '扩展名',
key: 'ext',
},
{
title: '文件大小',
key: 'sizeFormat',
},
{
title: '状态',
key: 'status',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '隐藏'),
}
);
},
},
{
title: '上传时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,394 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="附件管理">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<UploadOutlined />
</n-icon>
</template>
上传附件
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
style="width: 60%"
title="上传附件"
>
<n-upload
multiple
directory-dnd
:action="`${uploadUrl}/admin/upload/image`"
:headers="uploadHeaders"
:data="{ type: 0 }"
@before-upload="beforeUpload"
@finish="finish"
name="file"
:max="20"
:default-file-list="fileList"
list-type="image"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<archive-icon />
</n-icon>
</div>
<n-text style="font-size: 16px"> 点击或者拖动文件到该区域来上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0"> 单次最多允许20个文件</n-p>
</n-upload-dragger>
</n-upload>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { UploadFileInfo, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, List, Status } from '@/api/apply/attachment';
import { columns } from './columns';
import { DeleteOutlined, UploadOutlined } from '@vicons/antd';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
import { useGlobSetting } from '@/hooks/setting';
import { useUserStoreWidthOut } from '@/store/modules/user';
import componentSetting from '@/settings/componentSetting';
import { ResultEnum } from '@/enums/httpEnum';
const useUserStore = useUserStoreWidthOut();
const globSetting = useGlobSetting();
const { uploadUrl } = globSetting;
const uploadHeaders = reactive({
Authorization: useUserStore.token,
});
const fileList = ref<UploadFileInfo[]>([
// {
// id: 'c',
// name: '图片.png',
// status: 'finished',
// url: 'https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg',
// },
]);
const driveOptions = [
{
value: 'local',
label: '本地',
},
].map((s) => {
return s;
});
const params = ref({
pageSize: 10,
title: '',
content: '',
status: null,
});
const rules = {
title: {
// required: true,
trigger: ['blur', 'input'],
message: '请输入标题',
},
};
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '用户ID',
componentProps: {
placeholder: '请输入用户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入用户ID', trigger: ['blur'] }],
},
{
field: 'drive',
component: 'NSelect',
label: '驱动',
defaultValue: null,
componentProps: {
placeholder: '请选择驱动',
options: driveOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref({});
const formRef = ref({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const resetFormParams = {
basicLogo: '',
id: 0,
title: '',
name: '',
type: 1,
receiver: '',
remark: '',
sort: 0,
status: 1,
created_at: '',
updated_at: '',
};
let formParams = ref(resetFormParams);
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
fileList.value = [];
}
const loadDataTable = async (res) => {
return await List({ ...params.value, ...res, ...searchFormRef.value.formModel });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
Edit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
reloadTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
//上传之前
function beforeUpload({ file }) {
return true;
}
//上传结束
function finish({ event: Event }) {
const res = eval('(' + Event.target.response + ')');
const infoField = componentSetting.upload.apiSetting.infoField;
const { code } = res;
const msg = res.msg || res.message || '上传失败';
const result = res[infoField];
//成功
if (code === ResultEnum.SUCCESS) {
fileList.value.push({
id: result.id,
name: result.name,
status: 'finished',
type: result.naiveType,
});
message.success('上传' + result.name + '成功');
reloadTable();
} else {
message.error(msg);
}
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,74 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
key: 'id',
},
{
title: '公告标题',
key: 'title',
render(row) {
return row.title;
},
},
{
title: '公告类型',
key: 'type',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.type == 1 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.type == 1 ? '通知' : '公告'),
}
);
},
},
{
title: '公告内容',
key: 'content',
},
{
title: '备注',
key: 'remark',
},
{
title: '排序',
key: 'sort',
},
{
title: '公告状态',
key: 'status',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '隐藏'),
}
);
},
},
{
title: '已读人数',
key: 'receiveNum',
},
{
title: '发布时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,376 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="公告管理">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
新建
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="公告标题" path="title">
<n-input placeholder="请输入公告标题" v-model:value="formParams.title" />
</n-form-item>
<n-form-item label="公告类型" path="type">
<n-radio-group v-model:value="formParams.type" name="type">
<n-radio-button
v-for="type in typeOptions"
:key="type.value"
:value="type.value"
:label="type.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="公告内容" path="content">
<n-input type="textarea" placeholder="请输入内容" v-model:value="formParams.content" />
</n-form-item>
<n-form-item label="接收人" path="receiver">
<n-input
type="textarea"
placeholder="多个用户ID用,隔开 不填则全部接收"
v-model:value="formParams.receiver"
/>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, List, Status } from '@/api/apply/notice';
import { columns } from './columns';
import { DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
const typeOptions = [
{
value: 1,
label: '通知',
},
{
value: 2,
label: '公告',
},
].map((s) => {
return s;
});
const params = ref({
pageSize: 10,
title: '',
content: '',
status: null,
});
const rules = {
title: {
// required: true,
trigger: ['blur', 'input'],
message: '请输入标题',
},
};
const schemas: FormSchema[] = [
{
field: 'title',
component: 'NInput',
label: '公告标题',
componentProps: {
placeholder: '请输入公告标题',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入公告标题', trigger: ['blur'] }],
},
{
field: 'content',
component: 'NInput',
label: '内容',
componentProps: {
placeholder: '请输入内容关键词',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref({});
const formRef = ref({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const resetFormParams = {
id: 0,
title: '',
name: '',
type: 1,
receiver: '',
remark: '',
sort: 0,
status: 1,
created_at: '',
updated_at: '',
};
let formParams = ref(resetFormParams);
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
}
const loadDataTable = async (res) => {
return await List({ ...params.value, ...res, ...searchFormRef.value.formModel });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
Edit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
reloadTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,71 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
key: 'id',
},
{
title: '地区名称',
key: 'title',
},
{
title: '父ID',
key: 'pid',
render(row) {
return row.pid;
},
},
{
title: '拼音',
key: 'pinyin',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'success',
bordered: false,
},
{
default: () => row.pinyin,
}
);
},
},
{
title: '经度',
key: 'lng',
},
{
title: '维度',
key: 'lat',
},
{
title: '状态',
key: 'status',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '隐藏'),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,376 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="省市区">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
新建
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="公告标题" path="title">
<n-input placeholder="请输入公告标题" v-model:value="formParams.title" />
</n-form-item>
<n-form-item label="公告类型" path="type">
<n-radio-group v-model:value="formParams.type" name="type">
<n-radio-button
v-for="type in typeOptions"
:key="type.value"
:value="type.value"
:label="type.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="公告内容" path="content">
<n-input type="textarea" placeholder="请输入内容" v-model:value="formParams.content" />
</n-form-item>
<n-form-item label="接收人" path="receiver">
<n-input
type="textarea"
placeholder="多个用户ID用,隔开 不填则全部接收"
v-model:value="formParams.receiver"
/>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, List, Status } from '@/api/apply/provinces';
import { columns } from './columns';
import { DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
const typeOptions = [
{
value: 1,
label: '通知',
},
{
value: 2,
label: '公告',
},
].map((s) => {
return s;
});
const params = ref({
pageSize: 10,
title: '',
content: '',
status: null,
});
const rules = {
title: {
// required: true,
trigger: ['blur', 'input'],
message: '请输入标题',
},
};
const schemas: FormSchema[] = [
{
field: 'title',
component: 'NInput',
label: '公告标题',
componentProps: {
placeholder: '请输入公告标题',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入公告标题', trigger: ['blur'] }],
},
{
field: 'content',
component: 'NInput',
label: '内容',
componentProps: {
placeholder: '请输入内容关键词',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref({});
const formRef = ref({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const resetFormParams = {
id: 0,
title: '',
name: '',
type: 1,
receiver: '',
remark: '',
sort: 0,
status: 1,
created_at: '',
updated_at: '',
};
let formParams = ref(resetFormParams);
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
}
const loadDataTable = async (res) => {
return await List({ ...params.value, ...res, ...searchFormRef.value.formModel });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
Edit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
reloadTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,162 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="拖拽"> 常用于卡片事项预约流程计划等 </n-card>
</div>
<n-alert title="花式拖拽演示" type="info" class="mt-4">
每个卡片都可以上下拖拽顺序另外不同卡片也可以拖拽过去拖拽过来都不在话下呢快试试O(_)O哈哈~
</n-alert>
<n-grid
cols="1 s:2 m:3 l:4 xl:4 2xl:4"
class="mt-4 proCard"
responsive="screen"
:x-gap="12"
:y-gap="8"
>
<n-grid-item>
<NCard
title="需求池"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="demandList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="info">需求</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="开发中"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="exploitList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="warning">开发中</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="已完成"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="completeList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="error">已完成</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="已验收"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<Draggable
class="draggable-ul"
animation="300"
:list="approvedList"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="success">已验收</n-tag><span class="ml-2">{{ element.name }}</span>
</div>
</template>
</Draggable>
</NCard>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import Draggable from 'vuedraggable';
const demandList = reactive([
{ name: '预约表单页面,能填写预约相关信息', id: 1 },
{ name: '促销活动页面,包含促销广告展示', id: 2 },
{ name: '商品列表,需要一个到货提醒功能', id: 3 },
{ name: '商品需要一个评价功能', id: 4 },
{ name: '商品图片需要提供放大镜', id: 5 },
{ name: '订单需要提供删除到回收站', id: 6 },
{ name: '用户头像上传,需要支持裁剪', id: 7 },
{ name: '据说Vue3.2发布了setup啥时候支持', id: 8 },
]);
const exploitList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
const completeList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
const approvedList = reactive([{ name: '商品图片需要提供放大镜', id: 5 }]);
</script>
<style lang="less" scoped>
.draggable-ul {
width: 100%;
overflow: hidden;
margin-top: -16px;
.draggable-li {
width: 100%;
padding: 16px 10px;
color: #333;
border-bottom: 1px solid #efeff5;
}
}
</style>

View File

@@ -0,0 +1,172 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单"> 基础表单用于向用户收集表单信息 </n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<div class="BasicForm">
<BasicForm
submitButtonText="提交预约"
layout="horizontal"
:gridProps="{ cols: 1 }"
:schemas="schemas"
@submit="handleSubmit"
@reset="handleReset"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</div>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { BasicForm } from '@/components/Form/index';
import { useMessage } from 'naive-ui';
const schemas = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
defaultValue: 1183135260000,
componentProps: {
type: 'date',
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
//插槽
slot: 'statusSlot',
},
];
const message = useMessage();
function handleSubmit(values: Recordable) {
console.log(values);
message.success(JSON.stringify(values));
}
function handleReset(values: Recordable) {
console.log(values);
}
</script>
<style lang="less" scoped>
.BasicForm {
width: 550px;
margin: 0 auto;
overflow: hidden;
padding-top: 20px;
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单"> useForm 表单用于向用户收集表单信息 </n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<div class="BasicForm">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</div>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { useMessage } from 'naive-ui';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
giProps: {
span: 1,
},
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
giProps: {
//span: 24,
},
defaultValue: 1183135260000,
componentProps: {
type: 'date',
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
giProps: {
//span: 24,
},
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
giProps: {
//span: 24,
},
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
giProps: {
//span: 24,
},
//插槽
slot: 'statusSlot',
},
];
const message = useMessage();
const [register, {}] = useForm({
gridProps: { cols: 1 },
collapsedRows: 3,
labelWidth: 120,
layout: 'horizontal',
submitButtonText: '提交预约',
schemas,
});
function handleSubmit(values: Recordable) {
console.log(values);
message.success(JSON.stringify(values));
}
function handleReset(values: Recordable) {
console.log(values);
}
</script>
<style lang="less" scoped>
.BasicForm {
width: 550px;
margin: 0 auto;
overflow: hidden;
padding-top: 20px;
}
</style>

View File

@@ -0,0 +1,306 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="模态框">
模态框用于向用户收集或展示信息Modal 采用 Dialog 预设扩展拖拽效果
<br />
以下是 useModal
方式ref方式也支持使用方式和其他组件一致modalRef.value.closeModal()
</n-card>
</div>
<n-card :bordered="false" class="proCard mt-4">
<n-alert title="Modal嵌套Form" type="info">
使用 useModal 进行弹窗展示和操作并演示了在Modal内和Form组件组合使用方法
</n-alert>
<n-divider />
<n-space>
<n-button type="primary" @click="showModal">打开Modal嵌套Form例子</n-button>
</n-space>
<n-divider />
<n-alert title="个性化轻量级" type="info">
使用 useModal 进行弹窗展示和操作自定义配置实现轻量级效果更多配置请参考文档
</n-alert>
<n-divider />
<n-space>
<n-button type="primary" @click="showLightModal">轻量级确认</n-button>
</n-space>
<n-divider />
<n-alert title="提示" type="info">
组件暴露了setProps 方法用于修改组件内部
Props比如标题具体参考UI框架文档DialogReactive Properties
</n-alert>
</n-card>
<basicModal @register="modalRegister" ref="modalRef" class="basicModal" @on-ok="okModal">
<template #default>
<BasicForm @register="register" @reset="handleReset" class="basicForm">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
</template>
</basicModal>
<basicModal
@register="lightModalRegister"
class="basicModalLight"
ref="modalRef"
@on-ok="lightOkModal"
>
<template #default>
<p class="text-gray-500" style="padding-left: 35px">一些对话框内容</p>
</template>
</basicModal>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, reactive, toRefs } from 'vue';
import { useMessage } from 'naive-ui';
import { basicModal, useModal } from '@/components/Modal';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '姓名',
labelMessage: '这是一个提示',
giProps: {
span: 1,
},
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
giProps: {
//span: 24,
},
defaultValue: 1183135260000,
componentProps: {
type: 'date',
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
giProps: {
//span: 24,
},
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
giProps: {
//span: 24,
},
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
giProps: {
//span: 24,
},
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
giProps: {
//span: 24,
},
//插槽
slot: 'statusSlot',
},
];
export default defineComponent({
components: { basicModal, BasicForm },
setup() {
const modalRef: any = ref(null);
const message = useMessage();
const [modalRegister, { openModal, closeModal, setSubLoading }] = useModal({
title: '新增预约',
});
const [
lightModalRegister,
{
openModal: lightOpenModal,
closeModal: lightCloseModal,
setSubLoading: lightSetSubLoading,
},
] = useModal({
title: '确认对话框',
showIcon: true,
type: 'warning',
closable: false,
maskClosable: true,
});
const [register, { submit }] = useForm({
gridProps: { cols: 1 },
collapsedRows: 3,
labelWidth: 120,
layout: 'horizontal',
submitButtonText: '提交预约',
showActionButtonGroup: false,
schemas,
});
const state = reactive({
formValue: {
name: '小马哥',
},
});
async function okModal() {
const formRes = await submit();
if (formRes) {
closeModal();
message.success('提交成功');
} else {
message.error('验证失败,请填写完整信息');
setSubLoading(false);
}
}
function lightOkModal() {
lightCloseModal();
lightSetSubLoading(false);
}
function showLightModal() {
lightOpenModal();
}
function showModal() {
openModal();
}
function handleReset(values: Recordable) {
console.log(values);
}
return {
...toRefs(state),
modalRef,
register,
modalRegister,
lightModalRegister,
handleReset,
showModal,
okModal,
lightOkModal,
showLightModal,
};
},
});
</script>
<style lang="less">
.basicForm {
padding-top: 20px;
}
.n-dialog.basicModal {
width: 640px;
}
.n-dialog.basicModalLight {
width: 416px;
padding-top: 26px;
}
</style>

View File

@@ -0,0 +1,114 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="富文本">
富文本用于展示图文信息比如商品详情文章详情等...
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<QuillEditor
ref="quillEditor"
:options="options"
v-model:content="myContent"
style="height: 350px"
@ready="readyQuill"
class="quillEditor"
/>
<template #footer>
<n-space>
<n-button @click="addText">增加文本</n-button>
<n-button @click="addImg">增加图片</n-button>
<n-button @click="getHtml">获取HTML</n-button>
</n-space>
</template>
</n-card>
<n-card :bordered="false" class="mt-4 proCard" title="HTML 内容">
<n-input
v-model:value="myContentHtml"
type="textarea"
placeholder="html"
:autosize="{
minRows: 3,
maxRows: 6,
}"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { QuillEditor } from '@vueup/vue-quill';
import '@vueup/vue-quill/dist/vue-quill.snow.css';
const quillEditor = ref();
const myContent = ref(
'<h4>Naive Ui Admin 是一个基于 vue3,vite2,TypeScript 的中后台解决方案</h4>'
);
const myContentHtml = ref(
'<h4>Naive Ui Admin 是一个基于 vue3,vite2,TypeScript 的中后台解决方案</h4>'
);
const options = reactive({
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'], // toggled buttons
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }], // custom button values
[{ list: 'ordered' }, { list: 'bullet' }],
[{ script: 'sub' }, { script: 'super' }], // superscript/subscript
[{ indent: '-1' }, { indent: '+1' }], // outdent/indent
[{ direction: 'rtl' }], // text direction
[{ size: ['small', false, 'large', 'huge'] }], // custom dropdown
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }], // dropdown with defaults from theme
[{ font: [] }],
[{ align: [] }],
['clean'],
['image'],
],
},
theme: 'snow',
placeholder: '输入您喜欢的内容吧!',
});
function readyQuill() {
console.log('Quill准备好了');
}
function getHtml() {
myContentHtml.value = getHtmlVal();
}
function addText() {
const html = getHtmlVal() + '新增加的内容';
quillEditor.value.setHTML(html);
}
function addImg() {
const html =
getHtmlVal() +
'<img style="width:100px" src="https://www.baidu.com/img/flexible/logo/pc/result.png"/>';
quillEditor.value.setHTML(html);
}
function getHtmlVal() {
return quillEditor.value.getHTML();
}
</script>
<style lang="less">
.ql-toolbar.ql-snow {
border-top: none;
border-left: none;
border-right: none;
border-bottom: 1px solid #eee;
margin-top: -10px;
}
.ql-container.ql-snow {
border: none;
}
</style>

View File

@@ -0,0 +1,83 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
key: 'name',
editComponent: 'NInput',
// 默认必填校验
editRule: true,
edit: true,
width: 200,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
editComponent: 'NSelect',
editComponentProps: {
options: [
{
label: '广东省',
value: 1,
},
{
label: '浙江省',
value: 2,
},
],
},
edit: true,
width: 200,
ellipsis: false,
},
{
title: '开始日期',
key: 'beginTime',
edit: true,
width: 160,
editComponent: 'NDatePicker',
editComponentProps: {
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
},
ellipsis: false,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
},
];

View File

@@ -0,0 +1,113 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:scroll-x="1360"
@update:checked-row-keys="onCheckedRow"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { reactive, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './basicColumns';
import { useDialog, useMessage } from 'naive-ui';
import { DeleteOutlined, EditOutlined } from '@vicons/antd';
const message = useMessage();
const dialog = useDialog();
const actionRef = ref();
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
});
const actionColumn = reactive({
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction as any, {
style: 'text',
actions: createActions(record),
});
},
});
function createActions(record) {
return [
{
label: '删除',
type: 'error',
// 配置 color 会覆盖 type
color: 'red',
icon: DeleteOutlined,
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
type: 'primary',
icon: EditOutlined,
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
];
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function handleDelete(record) {
console.log(record);
dialog.info({
title: '提示',
content: `您想删除${record.name}`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('删除成功');
},
onNegativeClick: () => {},
});
}
function handleEdit(record) {
console.log(record);
message.success('您点击了编辑按钮');
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,72 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
key: 'name',
width: 100,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
width: 150,
},
{
title: '开始日期',
key: 'beginTime',
width: 160,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '状态',
key: 'status',
width: 100,
render(row) {
return h(
NTag,
{
type: row.status ? 'success' : 'error',
},
{
default: () => (row.status ? '启用' : '禁用'),
}
);
},
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
},
];

View File

@@ -0,0 +1,59 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
@edit-end="editEnd"
@edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1360"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { BasicTable } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './CellColumns';
const actionRef = ref();
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
});
function onEditChange({ column, value, record }) {
if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,114 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicTable
title="表格列表"
titleTooltip="这是一个提示"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@edit-end="editEnd"
@edit-change="onEditChange"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1590"
>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { reactive, ref, h } from 'vue';
import { BasicTable, TableAction } from '@/components/Table';
import { getTableList } from '@/api/table/list';
import { columns } from './rowColumns';
const actionRef = ref();
const currentEditKeyRef = ref('');
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
});
const actionColumn = reactive({
width: 150,
title: '操作',
key: 'action',
fixed: 'right',
align: 'center',
render(record) {
return h(TableAction, {
style: 'button',
actions: createActions(record),
});
},
});
function handleEdit(record) {
currentEditKeyRef.value = record.key;
record.onEdit?.(true);
}
function handleCancel(record) {
currentEditKeyRef.value = '';
record.onEdit?.(false, false);
}
function onEditChange({ column, value, record }) {
if (column.key === 'id') {
record.editValueRefs.name4.value = `${value}`;
}
console.log(column, value, record);
}
async function handleSave(record) {
const pass = await record.onEdit?.(false, true);
if (pass) {
currentEditKeyRef.value = '';
}
}
function createActions(record) {
if (!record.editable) {
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
];
} else {
return [
{
label: '保存',
onClick: handleSave.bind(null, record),
},
{
label: '取消',
onClick: handleCancel.bind(null, record),
},
];
}
}
const loadDataTable = async (res) => {
return await getTableList({ ...params, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
console.log(actionRef.value);
actionRef.value.reload();
}
function editEnd({ record, index, key, value }) {
console.log(value);
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,97 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '编码',
key: 'no',
width: 100,
},
{
title: '名称',
key: 'name',
editComponent: 'NInput',
editRow: true,
// 默认必填校验
editRule: true,
edit: true,
width: 200,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
editRow: true,
editComponent: 'NSelect',
editComponentProps: {
options: [
{
label: '广东省',
value: 1,
},
{
label: '浙江省',
value: 2,
},
],
},
edit: true,
width: 200,
ellipsis: false,
},
{
title: '开始日期',
key: 'beginTime',
editRow: true,
edit: true,
width: 240,
editComponent: 'NDatePicker',
editComponentProps: {
type: 'datetime',
format: 'yyyy-MM-dd HH:mm:ss',
valueFormat: 'yyyy-MM-dd HH:mm:ss',
},
ellipsis: false,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '状态',
key: 'status',
editRow: true,
edit: true,
width: 100,
editComponent: 'NSwitch',
editValueMap: (value) => {
return value ? '启用' : '禁用';
},
},
{
title: '创建时间',
key: 'date',
width: 160,
},
{
title: '停留时间',
key: 'time',
width: 80,
},
];

View File

@@ -0,0 +1,111 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="上传图片"> 上传图片用于向用户收集图片信息 </n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<n-grid cols="2 s:1 m:3 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item offset="0 s:0 m:1 l:1 xl:1 2xl:1">
<n-form
:label-width="80"
:model="formValue"
:rules="rules"
label-placement="left"
ref="formRef"
class="py-8"
>
<n-form-item label="预约姓名" path="name">
<n-input v-model:value="formValue.name" placeholder="输入姓名" />
</n-form-item>
<n-form-item label="预约号码" path="mobile">
<n-input placeholder="电话号码" v-model:value="formValue.mobile" />
</n-form-item>
<n-form-item label="病例图片" path="images">
<BasicUpload
:action="`${uploadUrl}/v1.0/files`"
:headers="uploadHeaders"
:data="{ type: 0 }"
name="files"
:width="100"
:height="100"
@uploadChange="uploadChange"
v-model:value="formValue.images"
helpText="单个文件不超过2MB最多只能上传10个文件"
/>
</n-form-item>
<div style="margin-left: 80px">
<n-space>
<n-button type="primary" @click="formSubmit">提交预约</n-button>
<n-button @click="resetForm">重置</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, unref, reactive } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicUpload } from '@/components/Upload';
import { useGlobSetting } from '@/hooks/setting';
const globSetting = useGlobSetting();
const rules = {
name: {
required: true,
message: '请输入预约姓名',
trigger: 'blur',
},
remark: {
required: true,
message: '请输入预约备注',
trigger: 'blur',
},
images: {
required: true,
type: 'array',
message: '请上传病例图片',
trigger: 'change',
},
};
const formRef: any = ref(null);
const message = useMessage();
const { uploadUrl } = globSetting;
const formValue = reactive({
name: '',
mobile: '',
//图片列表 通常查看和编辑使用 绝对路径 | 相对路径都可以
images: ['https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png'],
});
const uploadHeaders = reactive({
platform: 'miniPrograms',
timestamp: new Date().getTime(),
token: 'Q6fFCuhc1vkKn5JNFWaCLf6gRAc5n0LQHd08dSnG4qo=',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
formRef.value.restoreValidation();
}
function uploadChange(list: string[]) {
formValue.images = unref(list);
}
</script>

View File

@@ -0,0 +1,111 @@
<template>
<div ref="chartRef" :style="{ height, width }"></div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, Ref } from 'vue';
import { useECharts } from '@/hooks/web/useECharts';
import { basicProps } from './props';
export default defineComponent({
props: basicProps,
setup() {
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
onMounted(() => {
setOptions({
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
width: 1,
color: '#019680',
},
},
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [
'6:00',
'7:00',
'8:00',
'9:00',
'10:00',
'11:00',
'12:00',
'13:00',
'14:00',
'15:00',
'16:00',
'17:00',
'18:00',
'19:00',
'20:00',
'21:00',
'22:00',
'23:00',
],
splitLine: {
show: true,
lineStyle: {
width: 1,
type: 'solid',
color: 'rgba(226,226,226,0.5)',
},
},
axisTick: {
show: false,
},
},
yAxis: [
{
type: 'value',
max: 80000,
splitNumber: 4,
axisTick: {
show: false,
},
splitArea: {
show: true,
areaStyle: {
color: ['rgba(255,255,255,0.2)', 'rgba(226,226,226,0.2)'],
},
},
},
],
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true },
series: [
{
smooth: true,
data: [
111, 222, 4000, 18000, 33333, 55555, 66666, 33333, 14000, 36000, 66666, 44444,
22222, 11111, 4000, 2000, 500, 333, 222, 111,
],
type: 'line',
areaStyle: {},
itemStyle: {
color: '#5ab1ef',
},
},
{
smooth: true,
data: [
33, 66, 88, 333, 3333, 5000, 18000, 3000, 1200, 13000, 22000, 11000, 2221, 1201,
390, 198, 60, 30, 22, 11,
],
type: 'line',
areaStyle: {},
itemStyle: {
color: '#019680',
},
},
],
});
});
return { chartRef };
},
});
</script>

View File

@@ -0,0 +1,25 @@
import {
CaretUpOutlined,
CaretDownOutlined,
UsergroupAddOutlined,
BarChartOutlined,
ShoppingCartOutlined,
AccountBookOutlined,
CreditCardOutlined,
MailOutlined,
TagsOutlined,
SettingOutlined,
} from '@vicons/antd';
export default {
CaretUpOutlined,
CaretDownOutlined,
UsergroupAddOutlined,
BarChartOutlined,
ShoppingCartOutlined,
AccountBookOutlined,
CreditCardOutlined,
MailOutlined,
TagsOutlined,
SettingOutlined,
};

View File

@@ -0,0 +1,30 @@
<template>
<div class="mt-4">
<NRow :gutter="24">
<NCol :span="24">
<n-card content-style="padding: 0;" :bordered="false">
<n-tabs type="line" size="large" :tabs-padding="20" pane-style="padding: 20px;">
<n-tab-pane name="流量消耗趋势">
<FluxTrend />
</n-tab-pane>
<n-tab-pane name="客户端访问量">
<VisitAmount />
</n-tab-pane>
</n-tabs>
</n-card>
</NCol>
</NRow>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import FluxTrend from './FluxTrend.vue';
import VisitAmount from './VisitAmount.vue';
export default defineComponent({
components: { FluxTrend, VisitAmount },
setup() {
return {};
},
});
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div ref="chartRef" :style="{ height, width }"></div>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref, Ref } from 'vue';
import { useECharts } from '@/hooks/web/useECharts';
import { basicProps } from './props';
export default defineComponent({
props: basicProps,
setup() {
const chartRef = ref<HTMLDivElement | null>(null);
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
onMounted(() => {
setOptions({
tooltip: {
trigger: 'axis',
axisPointer: {
lineStyle: {
width: 1,
color: '#019680',
},
},
},
grid: { left: '1%', right: '1%', top: '2 %', bottom: 0, containLabel: true },
xAxis: {
type: 'category',
data: [
'1月',
'2月',
'3月',
'4月',
'5月',
'6月',
'7月',
'8月',
'9月',
'10月',
'11月',
'12月',
],
},
yAxis: {
type: 'value',
max: 8000,
splitNumber: 4,
},
series: [
{
data: [3000, 2000, 3333, 5000, 3200, 4200, 3200, 2100, 3000, 5100, 6000, 3200, 4800],
type: 'bar',
barMaxWidth: 80,
},
],
});
});
return { chartRef };
},
});
</script>

View File

@@ -0,0 +1,17 @@
import { PropType } from 'vue';
export interface BasicProps {
width: string;
height: string;
}
export const basicProps = {
width: {
type: String as PropType<string>,
default: '100%',
},
height: {
type: String as PropType<string>,
default: '280px',
},
};

View File

@@ -0,0 +1,337 @@
<template>
<div class="console">
<!--数据卡片-->
<n-grid cols="1 s:2 m:3 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12" :y-gap="8">
<n-grid-item>
<NCard
title="卡板量"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="success"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else :startVal="1" :endVal="visits.dayVisits" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
日同比
<CountTo :startVal="1" suffix="%" :endVal="visits.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
周同比
<CountTo :startVal="1" suffix="%" :endVal="visits.decline" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" text :repeat="2" />
<template v-else>
<div class="text-sn"> 总卡板量 </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="visits.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="激活卡板"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo
v-else
prefix="¥"
:startVal="1"
:endVal="saleroom.weekSaleroom"
class="text-3xl"
/>
</div>
<div class="py-2 px-2 flex justify-between">
<div class="text-sn flex-1">
<n-progress
type="line"
:percentage="saleroom.degree"
:indicator-placement="'inside'"
processing
/>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总激活卡板 </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="saleroom.amount" /> <!-- prefix="¥"-->
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="代理商"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="warning"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else :startVal="1" :endVal="orderLarge.weekLarge" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
日同比
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
周同比
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.rise" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总代理商量 </div>
<div class="text-sn">
<CountTo :startVal="1" suffix="%" :endVal="orderLarge.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
title="提现佣金"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="error"></n-tag>
</template>
<div class="py-1 px-1 flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<CountTo v-else prefix="¥" :startVal="1" :endVal="volume.weekLarge" class="text-3xl" />
</div>
<div class="py-1 px-1 flex justify-between">
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
月同比
<CountTo :startVal="1" suffix="%" :endVal="volume.rise" />
<n-icon size="12" color="#00ff6f">
<CaretUpOutlined />
</n-icon>
</template>
</div>
<div class="text-sn">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
月同比
<CountTo :startVal="1" suffix="%" :endVal="volume.decline" />
<n-icon size="12" color="#ffde66">
<CaretDownOutlined />
</n-icon>
</template>
</div>
</div>
<template #footer>
<div class="flex justify-between">
<n-skeleton v-if="loading" :width="100" size="medium" />
<template v-else>
<div class="text-sn"> 总提现额 </div>
<div class="text-sn">
<CountTo prefix="¥" :startVal="1" :endVal="volume.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
</n-grid>
<!--导航卡片-->
<div class="mt-4">
<n-grid cols="1 s:2 m:3 l:8 xl:8 2xl:8" responsive="screen" :x-gap="16" :y-gap="8">
<n-grid-item v-for="(item, index) in iconList" :key="index">
<NCard content-style="padding-top: 0;" size="small" :bordered="false">
<template #footer>
<n-skeleton v-if="loading" size="medium" />
<div class="cursor-pointer" v-else>
<p class="flex justify-center">
<span>
<n-icon :size="item.size" class="flex-1" :color="item.color">
<component :is="item.icon" v-on="item.eventObject || {}" />
</n-icon>
</span>
</p>
<p class="flex justify-center"
><span>{{ item.title }}</span></p
>
</div>
</template>
</NCard>
</n-grid-item>
</n-grid>
</div>
<!--访问量 | 流量趋势-->
<VisiTab />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { getConsoleInfo } from '@/api/dashboard/console';
import VisiTab from './components/VisiTab.vue';
import { CountTo } from '@/components/CountTo/index';
import {
CaretUpOutlined,
CaretDownOutlined,
UsergroupAddOutlined,
BarChartOutlined,
ShoppingCartOutlined,
AccountBookOutlined,
CreditCardOutlined,
MailOutlined,
TagsOutlined,
SettingOutlined,
} from '@vicons/antd';
const loading = ref(true);
const visits = ref<any>({});
const saleroom = ref<any>({});
const orderLarge = ref<any>({});
const volume = ref({});
// 图标列表
const iconList = [
{
icon: UsergroupAddOutlined,
size: '32',
title: '用户',
color: '#69c0ff',
eventObject: {
click: () => {},
},
},
{
icon: BarChartOutlined,
size: '32',
title: '分析',
color: '#69c0ff',
eventObject: {
click: () => {},
},
},
{
icon: ShoppingCartOutlined,
size: '32',
title: '商品',
color: '#ff9c6e',
eventObject: {
click: () => {},
},
},
{
icon: AccountBookOutlined,
size: '32',
title: '订单',
color: '#b37feb',
eventObject: {
click: () => {},
},
},
{
icon: CreditCardOutlined,
size: '32',
title: '票据',
color: '#ffd666',
eventObject: {
click: () => {},
},
},
{
icon: MailOutlined,
size: '32',
title: '消息',
color: '#5cdbd3',
eventObject: {
click: () => {},
},
},
{
icon: TagsOutlined,
size: '32',
title: '标签',
color: '#ff85c0',
eventObject: {
click: () => {},
},
},
{
icon: SettingOutlined,
size: '32',
title: '配置',
color: '#ffc069',
eventObject: {
click: () => {},
},
},
];
onMounted(async () => {
const data = await getConsoleInfo();
console.log('data:'+JSON.stringify(data))
visits.value = data.visits;
saleroom.value = data.saleroom;
orderLarge.value = data.orderLarge;
volume.value = data.volume;
loading.value = false;
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,7 @@
<template>
<div>监控台</div>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,328 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="工作台">
<n-grid cols="2 s:1 m:1 l:2 xl:2 2xl:2" responsive="screen">
<n-gi>
<div class="flex items-center">
<div>
<n-avatar circle :size="64" :src="schoolboy" />
</div>
<div>
<p class="px-4 text-xl">早安{{ username }}开始您一天的工作吧</p>
<p class="px-4 text-gray-400">今日阴转大雨15 - 25出门记得带伞哦</p>
</div>
</div>
</n-gi>
<n-gi>
<div class="flex justify-end w-full">
<div class="flex flex-1 flex-col justify-center text-right">
<span class="text-secondary">项目数</span>
<span class="text-2xl">16</span>
</div>
<div class="flex flex-1 flex-col justify-center text-right">
<span class="text-secondary">待办</span>
<span class="text-2xl">3/15</span>
</div>
<div class="flex flex-1 flex-col justify-center text-right">
<span class="text-secondary">消息</span>
<span class="text-2xl">35</span>
</div>
</div>
</n-gi>
</n-grid>
</n-card>
</div>
<n-grid class="mt-4" cols="2 s:1 m:1 l:2 xl:2 2xl:2" responsive="screen" :x-gap="12" :y-gap="9">
<n-gi>
<n-card
:segmented="{ content: true }"
content-style="padding: 0;"
:bordered="false"
size="small"
title="项目"
>
<div class="flex flex-wrap project-card">
<n-card
size="small"
class="cursor-pointer project-card-item ms:w-1/2 md:w-1/3"
hoverable
>
<div class="flex">
<span>
<n-icon size="30">
<GithubOutlined />
</n-icon>
</span>
<span class="text-lg ml-4">Github</span>
</div>
<div class="flex mt-2 h-10 text-gray-400">
是一个面向开源及私有软件项目的托管平台
</div>
<div class="flex mt-2 h-10 text-gray-400"> 开源君2021-07-04</div>
</n-card>
<n-card
size="small"
class="cursor-pointer project-card-item ms:w-1/2 md:w-1/3"
hoverable
>
<div class="flex">
<span>
<n-icon size="30" color="#42b983">
<LogoVue />
</n-icon>
</span>
<span class="text-lg ml-4">Vue</span>
</div>
<div class="flex mt-2 h-10 text-gray-400"> 渐进式 JavaScript 框架</div>
<div class="flex mt-2 h-10 text-gray-400"> 学不动也要学2021-07-04</div>
</n-card>
<n-card
size="small"
class="cursor-pointer project-card-item ms:w-1/2 md:w-1/3"
hoverable
>
<div class="flex">
<span>
<n-icon size="30" color="#e44c27">
<Html5Outlined />
</n-icon>
</span>
<span class="text-lg ml-4">Html5</span>
</div>
<div class="flex mt-2 h-10 text-gray-400"> HTML5是互联网的下一代标准</div>
<div class="flex mt-2 h-10 text-gray-400"> 撸码也是一种艺术 2021-04-01</div>
</n-card>
<n-card
size="small"
class="cursor-pointer project-card-item ms:w-1/2 md:w-1/3"
hoverable
>
<div class="flex">
<span>
<n-icon size="30" color="#dd0031">
<LogoAngular />
</n-icon>
</span>
<span class="text-lg ml-4">Angular</span>
</div>
<div class="flex mt-2 h-10 text-gray-400"> 现代 Web 开发平台百万粉丝热捧</div>
<div class="flex mt-2 h-10 text-gray-400"> 铁粉君 2021-07-04</div>
</n-card>
<n-card
size="small"
class="cursor-pointer project-card-item ms:w-1/2 md:w-1/3"
hoverable
>
<div class="flex">
<span>
<n-icon size="30" color="#61dafb">
<LogoReact />
</n-icon>
</span>
<span class="text-lg ml-4">React</span>
</div>
<div class="flex mt-2 h-10 text-gray-400"> 用于构建用户界面的 JavaScript </div>
<div class="flex mt-2 h-10 text-gray-400"> 技术牛 2021-07-04</div>
</n-card>
<n-card
size="small"
class="cursor-pointer project-card-item ms:w-1/2 md:w-1/3"
hoverable
>
<div class="flex">
<span>
<n-icon size="30">
<LogoJavascript />
</n-icon>
</span>
<span class="text-lg ml-4">Js</span>
</div>
<div class="flex mt-2 h-10 text-gray-400"> 路是走出来的而不是空想出来的</div>
<div class="flex mt-2 h-10 text-gray-400"> 架构组 2021-07-04</div>
</n-card>
</div>
</n-card>
<n-card
:segmented="{ content: true }"
content-style="padding-top: 0;padding-bottom: 0;"
:bordered="false"
size="small"
title="动态"
class="mt-4"
>
<template #header-extra><a href="javascript:;">更多</a></template>
<n-list>
<n-list-item>
<template #prefix>
<n-avatar circle :size="40" :src="schoolboy" />
</template>
<n-thing :title="username + ',刚才把工作台页面随便写了一些,凑合能看了!'">
<template #description
><p class="text-xs text-gray-500">2021-07-04 22:37:16</p></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #prefix>
<n-avatar circle :size="40" :src="schoolboy" />
</template>
<n-thing :title="username + ' 在 开源组 创建了项目 hotGo'">
<template #description
><p class="text-xs text-gray-500">2021-07-04 09:37:16</p></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #prefix>
<n-avatar circle :size="40" :src="schoolboy" />
</template>
<n-thing title="@It界风清扬向hotGo提交了一个bug抽时间看看吧">
<template #description
><p class="text-xs text-gray-500">2021-07-04 22:37:16</p></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #prefix>
<n-avatar circle :size="40" :src="schoolboy" />
</template>
<n-thing title="技术部那几位童鞋,再次警告,不要摸鱼,不要摸鱼,不要摸鱼啦!">
<template #description
><p class="text-xs text-gray-500">2021-07-04 09:37:16</p></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #prefix>
<n-avatar circle :size="40" :src="schoolboy" />
</template>
<n-thing title="上班不摸鱼,和咸鱼有什么区别(这话真不是我说的哈)!">
<template #description
><p class="text-xs text-gray-500">2021-07-04 20:37:16</p></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #prefix>
<n-avatar circle :size="40" :src="schoolboy" />
</template>
<n-thing title="页面切换其实也支持缓存,只是加了过度效果,看起来像是重新渲染了">
<template #description>
<p class="text-gray-400">
<n-input type="text" placeholder="不信,输点文字试试" />
</p>
</template>
</n-thing>
</n-list-item>
</n-list>
</n-card>
</n-gi>
<n-gi>
<n-card
:segmented="{ content: true }"
content-style="padding: 0;"
:bordered="false"
size="small"
title="快捷操作"
>
<div class="flex flex-wrap project-card">
<n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500">
<span class="text-center">
<n-icon size="30" color="#68c755">
<DashboardOutlined />
</n-icon>
</span>
<span class="text-lx text-center">主控台</span>
</div>
</n-card>
<n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500">
<span class="text-center">
<n-icon size="30" color="#fab251">
<ProfileOutlined />
</n-icon>
</span>
<span class="text-lx text-center">列表</span>
</div>
</n-card>
<n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500">
<span class="text-center">
<n-icon size="30" color="#1890ff">
<FileProtectOutlined />
</n-icon>
</span>
<span class="text-lx text-center">表单</span>
</div>
</n-card>
<n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500">
<span class="text-center">
<n-icon size="30" color="#f06b96">
<ApartmentOutlined />
</n-icon>
</span>
<span class="text-lx text-center">权限管理</span>
</div>
</n-card>
<n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500">
<span class="text-center">
<n-icon size="30" color="#7238d1">
<SettingOutlined />
</n-icon>
</span>
<span class="text-lx text-center">系统管理</span>
</div>
</n-card>
<n-card size="small" class="cursor-pointer project-card-item" hoverable>
<div class="flex flex-col justify-center text-gray-500">
<span class="text-center">
<n-icon size="30" color="">
<DashboardOutlined />
</n-icon>
</span>
<span class="text-lx text-center">主控台</span>
</div>
</n-card>
</div>
</n-card>
<n-card :segmented="{ content: true }" :bordered="false" size="small" class="mt-4">
<img src="~@/assets/images/Business.svg" class="w-full" />
</n-card>
</n-gi>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import schoolboy from '@/assets/images/schoolboy.png';
import { useUserStore } from '@/store/modules/user';
import {
GithubOutlined,
DashboardOutlined,
ProfileOutlined,
FileProtectOutlined,
SettingOutlined,
ApartmentOutlined,
Html5Outlined,
} from '@vicons/antd';
import { LogoVue, LogoAngular, LogoReact, LogoJavascript } from '@vicons/ionicons5';
const userStore = useUserStore();
const username = userStore.info.username;
</script>
<style lang="less" scoped>
.project-card {
margin-right: -6px;
&-item {
margin: -1px;
width: 33.333333%;
}
}
</style>

View File

@@ -0,0 +1,39 @@
<template>
<div class="flex flex-col justify-center page-container">
<div class="text-center">
<h1 class="text-base">代码生成敬请期待</h1>
<n-button type="info" @click="goHome">回到首页</n-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router = useRouter();
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
border-radius: 4px;
padding: 50px 0;
height: 60vh;
.text-center {
h1 {
color: #666;
padding: 20px 0;
}
}
img {
width: 350px;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div class="flex flex-col justify-center page-container">
<div class="text-center">
<img src="~@/assets/images/exception/403.svg" alt="" />
</div>
<div class="text-center">
<h1 class="text-base text-gray-500">抱歉你无权访问该页面</h1>
<n-button type="info" @click="goHome">回到首页</n-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router = useRouter();
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
border-radius: 4px;
padding: 50px 0;
height: 100vh;
.text-center {
h1 {
color: #666;
padding: 20px 0;
}
}
img {
width: 350px;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div class="flex flex-col justify-center page-container">
<div class="text-center">
<img src="~@/assets/images/exception/404.svg" alt="" />
</div>
<div class="text-center">
<h1 class="text-base text-gray-500">抱歉你访问的页面不存在</h1>
<n-button type="info" @click="goHome">回到首页</n-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router = useRouter();
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
border-radius: 4px;
padding: 50px 0;
height: 100vh;
.text-center {
h1 {
color: #666;
padding: 20px 0;
}
}
img {
width: 350px;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<div class="flex flex-col justify-center page-container">
<div class="text-center">
<img src="~@/assets/images/exception/500.svg" alt="" />
</div>
<div class="text-center">
<h1 class="text-base text-gray-500">抱歉服务器出错了</h1>
<n-button type="info" @click="goHome">回到首页</n-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router = useRouter();
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.page-container {
width: 100%;
border-radius: 4px;
padding: 50px 0;
height: 100vh;
.text-center {
h1 {
color: #666;
padding: 20px 0;
}
}
img {
width: 350px;
margin: 0 auto;
}
}
</style>

View File

@@ -0,0 +1,194 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础表单">
表单页用于向用户收集或验证信息基础表单常见于数据项较少的表单场景表单域标签也可支持响应式
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<n-grid cols="1 s:1 m:3 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item offset="0 s:0 m:1 l:1 xl:1 2xl:1">
<n-form
:label-width="80"
:model="formValue"
:rules="rules"
label-placement="left"
ref="formRef"
class="py-8"
>
<n-form-item label="预约姓名1" path="name">
<n-input v-model:value="formValue.name" placeholder="输入姓名" />
</n-form-item>
<n-form-item label="预约号码" path="mobile">
<n-input placeholder="电话号码" v-model:value="formValue.mobile" />
</n-form-item>
<n-form-item label="预约时间" path="datetime">
<n-date-picker type="datetime" v-model:value="formValue.datetime" />
</n-form-item>
<n-form-item label="预约医生" path="doctor">
<n-select
placeholder="请选择预约医生"
:options="doctorList"
v-model:value="formValue.doctor"
/>
</n-form-item>
<n-form-item label="预约事项" path="matter">
<n-select
placeholder="请选择预约事项"
:options="matterList"
v-model:value="formValue.matter"
multiple
/>
</n-form-item>
<n-form-item label="性别" path="sex">
<n-radio-group v-model:value="formValue.sex" name="sex">
<n-space>
<n-radio :value="1"></n-radio>
<n-radio :value="2"></n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="预约备注" path="remark">
<n-input
v-model:value="formValue.remark"
type="textarea"
placeholder="请输入预约备注"
/>
</n-form-item>
<n-form-item label="图片" path="img">
<BasicUpload
:action="`${uploadUrl}/v1.0/files`"
:headers="uploadHeaders"
:data="{ type: 0 }"
name="files"
:width="100"
:height="100"
@uploadChange="uploadChange"
v-model:value="uploadList"
helpText="单个文件不超过20MB最多只能上传10个文件"
/>
</n-form-item>
<div style="margin-left: 80px">
<n-space>
<n-button type="primary" @click="formSubmit">提交预约</n-button>
<n-button @click="resetForm">重置</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, unref, reactive } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicUpload } from '@/components/Upload';
import { useGlobSetting } from '@/hooks/setting';
const globSetting = useGlobSetting();
const matterList = [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
];
const doctorList = [
{
label: '李医生',
value: 1,
},
{
label: '黄医生',
value: 2,
},
{
label: '张医生',
value: 3,
},
];
const rules = {
name: {
required: true,
message: '请输入预约姓名',
trigger: 'blur',
},
remark: {
required: true,
message: '请输入预约备注',
trigger: 'blur',
},
mobile: {
required: true,
message: '请输入预约电话号码',
trigger: ['input'],
},
datetime: {
required: true,
type: 'number',
message: '请选择预约时间',
trigger: ['blur', 'change'],
},
doctor: {
required: true,
type: 'number',
message: '请选择预约医生',
trigger: 'change',
},
};
const formRef: any = ref(null);
const message = useMessage();
const { uploadUrl } = globSetting;
const defaultValueRef = () => ({
name: '',
mobile: '',
remark: '',
sex: 1,
matter: null,
doctor: null,
datetime: [],
});
let formValue = reactive(defaultValueRef());
const uploadList = ref([
'https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png',
]);
const uploadHeaders = reactive({
platform: 'miniPrograms',
timestamp: new Date().getTime(),
token: 'Q6fFCuhc1vkKn5JNFWaCLf6gRAc5n0LQHd08dSnG4qo=',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
formRef.value.restoreValidation();
formValue = Object.assign(unref(formValue), defaultValueRef());
}
function uploadChange(list: string[]) {
console.log(list);
}
</script>

View File

@@ -0,0 +1,124 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="表单详情">
表单除了提交数据有时也用于显示只读信息
</n-card>
</div>
<n-card
:bordered="false"
title="基本信息"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item>
<template #label>收款人姓名</template>
啊俊
</n-descriptions-item>
<n-descriptions-item label="收款账户">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="付款类型">支付宝</n-descriptions-item>
<n-descriptions-item label="付款账户">NaiveUiAdmin@163.com</n-descriptions-item>
<n-descriptions-item label="转账金额">1980.00</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag type="success"> 已到账</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
title="其它信息"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item>
<template #label>城市</template>
深圳
</n-descriptions-item>
<n-descriptions-item label="性别"></n-descriptions-item>
<n-descriptions-item label="邮箱">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="地址">广东省深圳市南山区</n-descriptions-item>
<n-descriptions-item label="生日">1991-06-04</n-descriptions-item>
<n-descriptions-item label="认证">
<n-tag type="success"> 已认证</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
title="表格信息"
class="mt-4 proCard"
size="small"
:segmented="{ content: true }"
>
<n-table :bordered="false" :single-line="false">
<thead>
<tr>
<th>姓名</th>
<th>性别</th>
<th>城市</th>
<th>生日</th>
<th width="150">操作</th>
</tr>
</thead>
<tbody>
<tr>
<td>Ah jung</td>
<td></td>
<td>深圳</td>
<td>1993-11-09</td>
<td>
<n-space>
<n-button size="small" type="error">删除</n-button>
<n-button size="small" type="info">查看</n-button>
</n-space>
</td>
</tr>
<tr>
<td>西门飞雪</td>
<td></td>
<td>广州</td>
<td>1991-09-11</td>
<td>
<n-space>
<n-button size="small" type="error">删除</n-button>
<n-button size="small" type="info">查看</n-button>
</n-space>
</td>
</tr>
<tr>
<td>泰坦巨人</td>
<td></td>
<td>北京</td>
<td>1990-11-03</td>
<td>
<n-space>
<n-button size="small" type="error">删除</n-button>
<n-button size="small" type="info">查看</n-button>
</n-space>
</td>
</tr>
<tr>
<td>猎魔人</td>
<td></td>
<td>上海</td>
<td>1992-03-11</td>
<td>
<n-space>
<n-button size="small" type="error">删除</n-button>
<n-button size="small" type="info">查看</n-button>
</n-space>
</td>
</tr>
</tbody>
</n-table>
</n-card>
</div>
</template>
<script setup></script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,122 @@
<template>
<n-form
:label-width="90"
:model="formValue"
:rules="rules"
label-placement="left"
ref="form1Ref"
style="max-width: 500px; margin: 40px auto 0 80px"
>
<n-form-item label="付款账户" path="myAccount">
<n-select
placeholder="请选择付款账户"
:options="myAccountList"
v-model:value="formValue.myAccount"
/>
</n-form-item>
<n-form-item label="收款账户" path="account">
<n-input-group>
<n-select
placeholder="请选择"
:options="accountTypeList"
:style="{ width: '20%' }"
v-model:value="formValue.accountType"
/>
<n-input
placeholder="请输入收款账户"
:style="{ width: '80%' }"
v-model:value="formValue.account"
/>
</n-input-group>
</n-form-item>
<n-form-item label="收款人姓名" path="name">
<n-input placeholder="请输入收款人姓名" v-model:value="formValue.name" />
</n-form-item>
<n-form-item label="转账金额" path="money">
<n-input placeholder="请输入转账金额" v-model:value="formValue.money">
<template #prefix>
<span class="text-gray-400"></span>
</template>
</n-input>
</n-form-item>
<div style="margin-left: 80px">
<n-space>
<n-button type="primary" @click="formSubmit">下一步</n-button>
</n-space>
</div>
</n-form>
</template>
<script lang="ts" setup>
import { ref, defineEmits } from 'vue';
import { useMessage } from 'naive-ui';
const myAccountList = [
{
label: 'NaiveUiAdmin@163.com',
value: 1,
},
{
label: 'NaiveUiAdmin@qq.com',
value: 2,
},
];
const accountTypeList = [
{
label: '微信',
value: 1,
},
{
label: '支付宝',
value: 2,
},
];
const emit = defineEmits(['nextStep']);
const form1Ref: any = ref(null);
const message = useMessage();
const formValue = ref({
accountType: 1,
myAccount: null,
account: 'xioama@qq.com',
money: '1980',
name: 'Ah jung',
});
const rules = {
name: {
required: true,
message: '请输入收款人姓名',
trigger: 'blur',
},
account: {
required: true,
message: '请输入收款账户',
trigger: 'blur',
},
money: {
required: true,
message: '请输入转账金额',
trigger: 'blur',
},
myAccount: {
required: true,
type: 'number',
message: '请选择付款账户',
trigger: 'change',
},
};
function formSubmit() {
form1Ref.value.validate((errors) => {
if (!errors) {
emit('nextStep');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<n-form
:label-width="90"
:model="formValue"
:rules="rules"
label-placement="left"
ref="form2Ref"
style="max-width: 500px; margin: 40px auto 0 80px"
>
<n-form-item label="付款账户" path="myAccount">
<span>NaiveUiAdmin@163.com</span>
</n-form-item>
<n-form-item label="收款账户" path="account">
<span>NaiveUiAdmin@qq.com</span>
</n-form-item>
<n-form-item label="收款人姓名" path="name">
<span>Ah jung</span>
</n-form-item>
<n-form-item label="转账金额" path="money">
<span>1980</span>
</n-form-item>
<n-divider />
<n-form-item label="支付密码" path="password">
<n-input type="password" v-model:value="formValue.password" />
</n-form-item>
<div style="margin-left: 80px">
<n-space>
<n-button type="primary" :loading="loading" @click="formSubmit">提交</n-button>
<n-button @click="prevStep">上一步</n-button>
</n-space>
</div>
</n-form>
</template>
<script lang="ts" setup>
import { ref, defineEmits } from 'vue';
import { useMessage } from 'naive-ui';
const form2Ref: any = ref(null);
const message = useMessage();
const loading = ref(false);
const formValue = ref({
password: '086611',
});
const rules = {
password: {
required: true,
message: '请输入支付密码',
trigger: 'blur',
},
};
const emit = defineEmits(['prevStep', 'nextStep']);
function prevStep() {
emit('prevStep');
}
function formSubmit() {
loading.value = true;
form2Ref.value.validate((errors) => {
if (!errors) {
setTimeout(() => {
emit('nextStep');
}, 1500);
} else {
message.error('验证失败,请填写完整信息');
}
});
}
</script>

View File

@@ -0,0 +1,72 @@
<template>
<div>
<n-result status="success" title="操作成功" description="预计两小时内到账" class="step-result">
<template #default>
<div class="information">
<n-grid cols="2 s:2 m:3 l:3 xl:3 2xl:3" responsive="screen" class="my-1">
<n-gi>付款账户</n-gi>
<n-gi>NaiveUiAdmin@163.com</n-gi>
</n-grid>
<n-grid cols="2 s:2 m:3 l:3 xl:3 2xl:3" responsive="screen" class="my-1">
<n-gi>收款账户</n-gi>
<n-gi>xiaoma@qq.com</n-gi>
</n-grid>
<n-grid cols="2 s:2 m:3 l:3 xl:3 2xl:3" responsive="screen" class="my-1">
<n-gi>收款人姓名</n-gi>
<n-gi>啊俊</n-gi>
</n-grid>
<n-grid cols="2 s:2 m:3 l:3 xl:3 2xl:3" responsive="screen" class="my-1">
<n-gi>转账金额</n-gi>
<n-gi><span class="money">1980</span> </n-gi>
</n-grid>
</div>
</template>
<template #footer>
<div class="flex justify-center">
<n-button type="primary" @click="finish" class="mr-4">再转一笔</n-button>
<n-button @click="prevStep">查看账单</n-button>
</div>
</template>
</n-result>
</div>
</template>
<script lang="ts" setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['finish', 'prevStep']);
function prevStep() {
emit('prevStep');
}
function finish() {
emit('finish');
}
</script>
<style lang="less" scoped>
.step-result {
max-width: 560px;
margin: 40px auto 0;
::v-deep(.n-result-content) {
background-color: #fafafa;
padding: 24px 40px;
}
.information {
line-height: 22px;
.ant-row:not(:last-child) {
margin-bottom: 24px;
}
}
.money {
font-family: 'Helvetica Neue', sans-serif;
font-weight: 500;
font-size: 20px;
line-height: 14px;
}
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="分步表单">
将一个冗长或用户不熟悉的表单任务分成多个步骤指导用户完成
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<n-space vertical class="steps" justify="center">
<n-steps :current="currentTab" :status="currentStatus">
<n-step title="填写转账信息" description="确保填写正确" />
<n-step title="确认转账信息" description="确认转账信息" />
<n-step title="完成转账" description="恭喜您,转账成功" />
</n-steps>
<step1 v-if="currentTab === 1" @nextStep="nextStep" />
<step2 v-if="currentTab === 2" @nextStep="nextStep" @prevStep="prevStep" />
<step3 v-if="currentTab === 3" @prevStep="prevStep" @finish="finish" />
</n-space>
</n-card>
</div>
</template>
<script setup>
import { defineComponent, ref } from 'vue';
import step1 from './Step1.vue';
import step2 from './Step2.vue';
import step3 from './Step3.vue';
const currentTab = ref(1);
const currentStatus = ref('process');
function nextStep() {
if (currentTab.value < 3) {
currentTab.value += 1;
}
}
function prevStep() {
if (currentTab.value > 1) {
currentTab.value -= 1;
}
}
function finish() {
currentTab.value = 1;
}
</script>
<style lang="less" scoped>
.steps {
max-width: 750px;
margin: 16px auto;
}
</style>

View File

@@ -0,0 +1 @@
<template> 项目文档 </template>

View File

@@ -0,0 +1,61 @@
<template>
<n-spin :show="loading">
<div class="frame">
<iframe :src="frameSrc" class="frame-iframe" ref="frameRef"></iframe>
</div>
</n-spin>
</template>
<script lang="ts" setup>
import { ref, unref, onMounted, nextTick } from 'vue';
import { useRoute } from 'vue-router';
const currentRoute = useRoute();
const loading = ref(false);
const frameRef = ref<HTMLFrameElement | null>(null);
const frameSrc = ref<string>('');
if (unref(currentRoute.meta)?.frameSrc) {
frameSrc.value = unref(currentRoute.meta)?.frameSrc as string;
}
function hideLoading() {
loading.value = false;
}
function init() {
nextTick(() => {
const iframe = unref(frameRef);
if (!iframe) return;
const _frame = iframe as any;
if (_frame.attachEvent) {
_frame.attachEvent('onload', () => {
hideLoading();
});
} else {
iframe.onload = () => {
hideLoading();
};
}
});
}
onMounted(() => {
loading.value = true;
init();
});
</script>
<style lang="less" scoped>
.frame {
width: 100%;
height: 100vh;
&-iframe {
width: 100%;
height: 100%;
overflow: hidden;
border: 0;
box-sizing: border-box;
}
}
</style>

View File

@@ -0,0 +1,50 @@
import { h } from 'vue';
import { NAvatar } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
width: 100,
},
{
title: '名称',
key: 'name',
width: 100,
},
{
title: '头像',
key: 'avatar',
width: 100,
render(row) {
return h(NAvatar, {
size: 48,
src: row.avatar,
});
},
},
{
title: '地址',
key: 'address',
auth: ['basic_list'], // 同时根据权限控制是否显示
ifShow: (_column) => {
return true; // 根据业务控制是否显示
},
width: 150,
},
{
title: '开始日期',
key: 'beginTime',
width: 160,
},
{
title: '结束日期',
key: 'endTime',
width: 160,
},
{
title: '创建时间',
key: 'date',
width: 100,
},
];

View File

@@ -0,0 +1,347 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
新建
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="名称" path="name">
<n-input placeholder="请输入名称" v-model:value="formParams.name" />
</n-form-item>
<n-form-item label="地址" path="address">
<n-input type="textarea" placeholder="请输入地址" v-model:value="formParams.address" />
</n-form-item>
<n-form-item label="日期" path="date">
<n-date-picker type="datetime" placeholder="请选择日期" v-model:value="formParams.date" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getTableList } from '@/api/table/list';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { useRouter } from 'vue-router';
const rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
address: {
required: true,
trigger: ['blur', 'input'],
message: '请输入地址',
},
date: {
type: 'number',
required: true,
trigger: ['blur', 'change'],
message: '请选择日期',
},
};
const schemas: FormSchema[] = [
{
field: 'name',
labelMessage: '这是一个提示',
component: 'NInput',
label: '姓名',
componentProps: {
placeholder: '请输入姓名',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ required: true, message: '请输入姓名', trigger: ['blur'] }],
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'type',
component: 'NSelect',
label: '类型',
componentProps: {
placeholder: '请选择类型',
options: [
{
label: '舒适性',
value: 1,
},
{
label: '经济性',
value: 2,
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeDate',
component: 'NDatePicker',
label: '预约时间',
defaultValue: 1183135260000,
componentProps: {
type: 'date',
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeTime',
component: 'NTimePicker',
label: '停留时间',
componentProps: {
clearable: true,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
label: '状态',
//插槽
slot: 'statusSlot',
},
{
field: 'makeProject',
component: 'NCheckbox',
label: '预约项目',
componentProps: {
placeholder: '请选择预约项目',
options: [
{
label: '种牙',
value: 1,
},
{
label: '补牙',
value: 2,
},
{
label: '根管',
value: 3,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'makeSource',
component: 'NRadioGroup',
label: '来源',
componentProps: {
options: [
{
label: '网上',
value: 1,
},
{
label: '门店',
value: 2,
},
],
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const showModal = ref(false);
const formBtnLoading = ref(false);
const formParams = reactive({
name: '',
address: '',
date: null,
});
const params = ref({
pageSize: 5,
name: 'xiaoMa',
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '删除',
icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return true;
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
auth: ['basic_list'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return true;
},
auth: ['basic_list'],
},
],
dropDownActions: [
{
label: '启用',
key: 'enabled',
// 根据业务控制是否显示: 非enable状态的不显示启用按钮
ifShow: () => {
return true;
},
},
{
label: '禁用',
key: 'disabled',
ifShow: () => {
return true;
},
},
],
select: (key) => {
message.info(`您点击了,${key} 按钮`);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
}
const loadDataTable = async (res) => {
return await getTableList({ ...formParams, ...params.value, ...res });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
message.success('新建成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'basic-info', params: { id: record.id } });
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
message.info('点击了删除');
}
function handleSubmit(values: Recordable) {
console.log(values);
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,34 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="基础详情"> 基础详情有时也用于显示只读信息 </n-card>
</div>
<n-card :bordered="false" class="proCard mt-4" size="small" :segmented="{ content: true }">
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item>
<template #label>收款人姓名</template>
啊俊
</n-descriptions-item>
<n-descriptions-item label="收款账户">NaiveUiAdmin@qq.com</n-descriptions-item>
<n-descriptions-item label="付款类型">支付宝</n-descriptions-item>
<n-descriptions-item label="付款账户">NaiveUiAdmin@163.com</n-descriptions-item>
<n-descriptions-item label="转账金额">1980.00</n-descriptions-item>
<n-descriptions-item label="状态">
<n-tag type="success"> 已到账</n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
</div>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
setup() {
return {};
},
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,80 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,293 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">cron刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
const dialog = useDialog();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
componentProps: {
placeholder: '请输入IP地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
},
{
label: '-1 失败',
value: '-1',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'cron_log_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
console.log(values);
formParams.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,80 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,293 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">ems-log刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
const dialog = useDialog();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
componentProps: {
placeholder: '请输入IP地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
},
{
label: '-1 失败',
value: '-1',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'ems_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
console.log(values);
formParams.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,80 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,293 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
const dialog = useDialog();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
componentProps: {
placeholder: '请输入IP地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
},
{
label: '-1 失败',
value: '-1',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'log_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
console.log(values);
formParams.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,80 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,293 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">login-log刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
const dialog = useDialog();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
componentProps: {
placeholder: '请输入IP地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
},
{
label: '-1 失败',
value: '-1',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'login_log_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
console.log(values);
formParams.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,80 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,293 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">sms-log刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
const dialog = useDialog();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
componentProps: {
placeholder: '请输入IP地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
},
{
label: '-1 失败',
value: '-1',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'sms_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
console.log(values);
formParams.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,208 @@
<template>
<div class="view-account">
<div class="view-account-header"></div>
<div class="view-account-container">
<div class="view-account-top">
<div class="view-account-top-logo">
<img src="~@/assets/images/account-logo.png" alt="" />
</div>
<div class="view-account-top-desc">HotGo 后台管理系统</div>
</div>
<div class="view-account-form">
<n-form
ref="formRef"
label-placement="left"
size="large"
:model="formInline"
:rules="rules"
>
<n-form-item path="username">
<n-input v-model:value="formInline.username" placeholder="请输入用户名">
<template #prefix>
<n-icon size="18" color="#808695">
<PersonOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item path="password">
<n-input
v-model:value="formInline.password"
type="password"
showPasswordOn="click"
placeholder="请输入密码"
>
<template #prefix>
<n-icon size="18" color="#808695">
<LockClosedOutline />
</n-icon>
</template>
</n-input>
</n-form-item>
<n-form-item class="default-color">
<div class="flex justify-between">
<div class="flex-initial">
<n-checkbox v-model:checked="autoLogin">自动登录</n-checkbox>
</div>
<div class="flex-initial order-last">
<a href="javascript:">忘记密码</a>
</div>
</div>
</n-form-item>
<n-form-item>
<n-button type="primary" @click="handleSubmit" size="large" :loading="loading" block>
登录
</n-button>
</n-form-item>
<n-form-item class="default-color">
<div class="flex view-account-other">
<div class="flex-initial">
<span>其它登录方式</span>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoGithub />
</n-icon>
</a>
</div>
<div class="flex-initial mx-2">
<a href="javascript:">
<n-icon size="24" color="#2d8cf0">
<LogoFacebook />
</n-icon>
</a>
</div>
<div class="flex-initial" style="margin-left: auto">
<a href="javascript:">注册账号</a>
</div>
</div>
</n-form-item>
</n-form>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '@/store/modules/user';
import { useMessage } from 'naive-ui';
import { ResultEnum } from '@/enums/httpEnum';
import { PersonOutline, LockClosedOutline, LogoGithub, LogoFacebook } from '@vicons/ionicons5';
import { PageEnum } from '@/enums/pageEnum';
interface FormState {
username: string;
password: string;
}
const formRef = ref();
const message = useMessage();
const loading = ref(false);
const autoLogin = ref(true);
const LOGIN_NAME = PageEnum.BASE_LOGIN_NAME;
const formInline = reactive({
username: '',
password: '',
isCaptcha: true,
});
const rules = {
username: { required: true, message: '请输入用户名', trigger: 'blur' },
password: { required: true, message: '请输入密码', trigger: 'blur' },
};
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
const handleSubmit = (e) => {
e.preventDefault();
formRef.value.validate(async (errors) => {
if (!errors) {
const { username, password } = formInline;
message.loading('登录中...');
loading.value = true;
const params: FormState = {
username,
password,
};
try {
const { code, message: msg } = await userStore.login(params);
message.destroyAll();
if (code == ResultEnum.SUCCESS) {
const toPath = decodeURIComponent((route.query?.redirect || '/') as string);
message.success('登录成功,即将进入系统');
if (route.name === LOGIN_NAME) {
router.replace('/');
} else router.replace(toPath);
} else {
message.info(msg || '登录失败');
}
} finally {
loading.value = false;
}
} else {
message.error('请填写完整信息,并且进行验证码校验');
}
});
};
</script>
<style lang="less" scoped>
.view-account {
display: flex;
flex-direction: column;
height: 100vh;
overflow: auto;
&-container {
flex: 1;
padding: 32px 12px;
max-width: 384px;
min-width: 320px;
margin: 0 auto;
}
&-top {
padding: 32px 0;
text-align: center;
&-desc {
font-size: 14px;
color: #808695;
}
}
&-other {
width: 100%;
}
.default-color {
color: #515a6e;
.ant-checkbox-wrapper {
color: #515a6e;
}
}
}
@media (min-width: 768px) {
.view-account {
background-image: url('../../assets/images/login.svg');
background-repeat: no-repeat;
background-position: 50%;
background-size: 100%;
}
.page-account-container {
padding: 32px 0 24px 0;
}
}
</style>

View File

@@ -0,0 +1,117 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
import { timestampToTime, formatBefore, formatAfter } from '@/utils/dateUtil';
export const columns = [
{
title: '会话编号',
key: 'id',
width: 240,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'info',
bordered: false,
},
{
default: () => row.id,
}
);
},
},
{
title: '登录应用',
key: 'app',
width: 80,
render(row) {
return row.app;
},
},
// {
// title: '用户ID',
// key: 'userId',
// width: 100,
// },
{
title: '用户名',
key: 'username',
width: 100,
},
{
title: '头像',
key: 'avatar',
width: 80,
render(row) {
return h(NAvatar, {
size: 32,
src: row.avatar,
});
},
},
{
title: '登录地址',
key: 'addr',
width: 150,
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '浏览器',
key: 'browser',
width: 200,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'success',
bordered: false,
},
{
default: () => row.browser,
}
);
},
},
{
title: '操作系统',
key: 'os',
width: 150,
render(row) {
return row.os;
},
},
{
title: '授权过期',
key: 'expTime',
width: 80,
render: (rows, _) => {
return formatAfter(new Date(rows.expTime * 1000));
},
},
{
title: '最后活跃',
key: 'heartbeatTime',
width: 80,
render: (rows, _) => {
return formatBefore(new Date(rows.heartbeatTime * 1000));
},
},
{
title: '登录时间',
key: 'firstTime',
width: 220,
render: (rows, _) => {
return timestampToTime(rows.firstTime);
},
},
];

View File

@@ -0,0 +1,134 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
/>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { OnlineList, Offline } from '@/api/monitor/monitor';
import { columns } from './columns';
import { useRouter } from 'vue-router';
const dialog = useDialog();
const schemas: FormSchema[] = [
{
field: 'userId',
component: 'NInput',
label: '用户ID',
componentProps: {
placeholder: '请输入用户ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
// {
// label: '查看详情',
// onClick: handleEdit.bind(null, record),
// },
{
label: '强制退出',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要强制退出该用户?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Offline(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
const loadDataTable = async (res) => {
return await OnlineList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'serve_log_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
console.log(values);
formParams.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,80 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: '模块',
key: 'module',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.module == 'admin' ? 'info' : 'success',
bordered: false,
},
{
default: () => row.module,
}
);
},
},
{
title: '操作人',
key: 'member_name',
render(row) {
if (row.memberId === 0) {
return row.member_name;
}
return row.member_name + '(' + row.memberId + ')';
},
},
{
title: '请求方式',
key: 'method',
},
{
title: '请求路径',
key: 'url',
},
{
title: '访问IP',
key: 'ip',
},
// {
// title: 'IP地区',
// key: 'region',
// },
{
title: '状态码',
key: 'errorCode',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.errorCode == 0 ? 'success' : 'warning',
bordered: false,
},
{
default: () => row.errorMsg + '(' + row.errorCode + ')',
}
);
},
},
{
title: 'Goroutine耗时',
key: 'takeUpTime',
render(row) {
return row.takeUpTime + ' ms';
},
},
{
title: '访问时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,293 @@
<template>
<n-card :bordered="false" class="proCard">
<BasicForm @register="register" @submit="handleSubmit" @reset="handleReset">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
<template #toolbar>
<n-button type="primary" @click="reloadTable">系统刷新数据</n-button>
</template>
</BasicTable>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getLogList, Delete } from '@/api/log/log';
import { columns } from './columns';
import { useRouter } from 'vue-router';
import { DeleteOutlined } from '@vicons/antd';
const dialog = useDialog();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const schemas: FormSchema[] = [
{
field: 'member_id',
component: 'NInput',
label: '操作人员',
componentProps: {
placeholder: '请输入操作人员ID',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'url',
component: 'NInput',
label: '访问路径',
componentProps: {
placeholder: '请输入手机访问路径',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '访问IP',
componentProps: {
placeholder: '请输入IP地址',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'method',
component: 'NSelect',
label: '请求方式',
componentProps: {
placeholder: '请选择请求方式',
options: [
{
label: 'GET',
value: 'GET',
},
{
label: 'POST',
value: 'POST',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '访问时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'take_up_time',
component: 'NSelect',
label: '请求耗时',
componentProps: {
placeholder: '请选择请求耗时',
options: [
{
label: '50ms内',
value: '50',
},
{
label: '100ms内',
value: '100',
},
{
label: '200ms内',
value: '200',
},
{
label: '500ms内',
value: '500',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'error_code',
component: 'NSelect',
label: '状态码',
componentProps: {
placeholder: '请选择状态码',
options: [
{
label: '0 成功',
value: '0',
},
{
label: '-1 失败',
value: '-1',
},
],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const formParams = ref({});
const params = ref({
pageSize: 10,
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '查看详情',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
const loadDataTable = async (res) => {
return await getLogList({ ...formParams.value, ...params.value, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
router.push({ name: 'serve_log_view', params: { id: record.id } });
}
function handleSubmit(values: Recordable) {
console.log(values);
formParams.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
formParams.value = {};
reloadTable();
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,136 @@
<template>
<div>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
:title="data.id ? '日志详情 ID' + data.id : '日志详情'"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="请求方式">{{ data.method }}</n-descriptions-item>
<n-descriptions-item>
<template #label>请求地址</template>
{{ data.url }}
</n-descriptions-item>
<n-descriptions-item label="请求耗时">{{ data.takeUpTime }} ms</n-descriptions-item>
<n-descriptions-item label="访问IP">{{ data.ip }}</n-descriptions-item>
<n-descriptions-item label="IP归属地">河南 郑州</n-descriptions-item>
<n-descriptions-item label="链路ID">{{ data.reqId }}</n-descriptions-item>
<n-descriptions-item label="响应时间">{{
timestampToTime(data.timestamp)
}}</n-descriptions-item>
<n-descriptions-item label="创建时间">{{ data.createdAt }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="访问代理"
>
{{ data.userAgent }}
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="报错信息"
>
<n-descriptions label-placement="left" class="py-2">
<n-descriptions-item label="报错状态码"> {{ data.errorCode }} </n-descriptions-item>
<n-descriptions-item label="报错消息">
<n-tag type="success"> {{ data.errorMsg }} </n-tag>
</n-descriptions-item>
<n-descriptions-item label="报错日志">
<n-tag type="success"> {{ data.errorData }} </n-tag>
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="Header请求头"
>
<JsonViewer
:value="JSON.parse(data.headerData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="GET参数"
>
<JsonViewer
:value="JSON.parse(data.getData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="POST参数"
>
<JsonViewer
:value="JSON.parse(data.postData ?? '{}')"
:expand-depth="5"
copyable
boxed
sort
style="width: 100%; min-width: 3.125rem"
/>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/index.css';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/log/log';
import { timestampToTime } from '@/utils/dateUtil';
const message = useMessage();
const router = useRouter();
const logId = Number(router.currentRoute.value.params.id);
onMounted(async () => {
if (logId === undefined || logId < 1) {
message.error('ID不正确请检查');
return;
}
await getInfo();
});
const data = ref({});
const getInfo = async () => {
data.value = await View({ id: logId });
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,97 @@
<template>
<n-card :content-style="{ padding: '10px' }" :header-style="{ padding: '5px' }" :segmented="true">
<template #header>
<n-skeleton text v-if="loading" width="60%" />
<template v-else>
<div class="flex items-center justify-between">
<span class="text-sm text-bold">{{ dataModel.title }}</span>
<n-icon style="font-size: 26px">
<div v-if="dataModel.iconClass === 'HardwareChip'">
<HardwareChip />
</div>
<div v-else-if="dataModel.iconClass === 'AppsSharp'">
<AppsSharp />
</div>
<div v-else-if="dataModel.iconClass === 'Analytics'">
<Analytics />
</div>
<div v-else-if="dataModel.iconClass === 'PieChart'">
<PieChart />
</div>
<div v-else>
<Bookmark />
</div>
</n-icon>
</div>
</template>
</template>
<n-skeleton text v-if="loading" :repeat="6" />
<template v-else>
<div style="height: 130px" class="flex flex-col justify-between">
<div class="flex flex-col justify-center">
<span class="text-xxl">{{ dataModel.data }}</span>
</div>
<div class="flex flex-col justify-center flex-1">
<slot name="extra" :extra="dataModel.extra"></slot>
</div>
<div class="divide"></div>
<div class="flex items-center justify-between">
<span class="text-sm text-grey">{{ dataModel.bottomTitle }}</span>
<span class="text-sm text-grey">{{ dataModel.totalSum }}</span>
</div>
</div>
</template>
</n-card>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { HardwareChip, Bookmark, AppsSharp, PieChart, Analytics } from '@vicons/ionicons5';
export default defineComponent({
name: 'DataItem',
components: {
Bookmark,
HardwareChip,
AppsSharp,
PieChart,
Analytics,
},
props: {
dataModel: {
type: Object,
default: () => {
return {};
},
},
loading: {
type: Boolean,
default: () => {
return false;
},
},
},
setup() {
// const loading = ref(true);
// setTimeout(() => {
// loading.value = false;
// }, 1000);
return {
// loading,
Bookmark,
AppsSharp,
PieChart,
HardwareChip,
Analytics,
};
},
});
</script>
<style lang="less" scoped>
.divide {
margin: 10px 0;
border-bottom: 1px solid #f5f5f5;
}
</style>

View File

@@ -0,0 +1,259 @@
<template>
<n-card
:content-style="{ padding: '10px' }"
:header-style="{ padding: '10px' }"
:segmented="true"
>
<template #header>
<n-skeleton text style="width: 50%" v-if="loading" />
<template v-else>
<div class="text-sm"> 实时网卡流量全部网卡</div>
</template>
</template>
<div class="chart-item-container">
<n-skeleton text v-if="loading" :repeat="10" />
<template v-else>
<n-grid responsive="screen" cols="1 s:2 m:4 l:4 xl:4 2xl:4" x-gap="5" y-gap="5">
<n-grid-item class="item-wrapper">
<n-card
:bordered="false"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '5px' }"
:segmented="true"
>
<div class="text-number">{{ last.up }} KB</div>
<div class="title-text">
<n-badge :value="1" dot color="rgb(58, 104, 255)" />
上行
</div>
</n-card>
</n-grid-item>
<n-grid-item class="item-wrapper">
<n-card
:bordered="false"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '5px' }"
:segmented="true"
>
<div class="text-number"> {{ last.down ?? 0 }} KB</div>
<div class="title-text">
<n-badge :value="1" dot color="rgb(241, 136, 136)" />
下行
</div>
</n-card>
</n-grid-item>
<n-grid-item class="item-wrapper">
<n-card
:bordered="false"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '5px' }"
:segmented="true"
>
<div class="text-number">{{ last.bytesSent ?? 0 }}</div>
<div class="title-text"> 总发送</div>
</n-card>
</n-grid-item>
<n-grid-item class="item-wrapper">
<n-card
:bordered="false"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '5px' }"
:segmented="true"
>
<div class="text-number">{{ last.bytesRecv ?? 0 }}</div>
<div class="title-text"> 总接收</div>
</n-card>
</n-grid-item>
</n-grid>
<div ref="fullYearSalesChart" class="chart-item"></div>
</template>
</div>
</n-card>
</template>
<script lang="ts">
import useEcharts from '@/hooks/useEcharts';
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { dispose, graphic } from 'echarts';
export default defineComponent({
name: 'FullYearSalesChart',
props: {
dataModel: {
type: Array,
default: () => {
// eslint-disable-next-line vue/require-valid-default-prop
return {};
},
},
loading: {
type: Boolean,
default: () => {
return false;
},
},
},
setup(props) {
const last = ref({
bytesSent: '0B',
bytesRecv: '0B',
down: '0',
up: '0',
});
const s = ref([]);
const x = ref([]);
const sName = ref('上行宽带');
const xName = ref('下行宽带');
const months = ref([]);
const option = ref({
title: {
subtext: '单位KB',
},
color: ['rgb(58,104,255)', 'rgb(241,136,136)'],
grid: {
top: '10%',
left: '2%',
right: '2%',
bottom: '5%',
containLabel: true,
},
// legend: {
// data: [sName.value, xName.value],
// },
tooltip: {
trigger: 'axis',
},
xAxis: {
type: 'category',
data: months.value,
boundaryGap: false,
},
yAxis: {
type: 'value',
},
series: [
{
type: 'line',
showSymbol: false,
name: sName.value,
stack: '总量',
data: s.value,
smooth: true,
lineStyle: {
color: 'rgba(24, 160, 88, 0.5)',
},
label: {
show: true,
formatter(val: any) {
return val.data + 'KB';
},
},
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(85, 193, 250, 0.1)',
},
{
offset: 1,
color: 'rgba(156, 21, 214, 0.2)',
},
]),
},
},
{
type: 'line',
showSymbol: false,
name: xName.value,
stack: '总量',
data: x.value,
smooth: true,
lineStyle: {
color: 'rgba(24, 160, 88, 0.5)',
},
label: {
show: true,
formatter(val: any) {
return val.data + 'KB';
},
},
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(132, 248, 187, 0.1)',
},
{
offset: 1,
color: 'rgba(51, 209, 125, 0.2)',
},
]),
},
},
],
});
const fullYearSalesChart = ref<HTMLDivElement | null>(null);
watch(props, (_newVal, _oldVal) => {
last.value = _newVal.dataModel[_newVal.dataModel.length - 1];
// console.log('last _newVal:' + JSON.stringify(last.value));
if (months.value.length < 10) {
for (let i = 0; i < _newVal.dataModel?.length; i++) {
s.value.push(_newVal.dataModel[i].up);
x.value.push(_newVal.dataModel[i].down);
months.value.push(_newVal.dataModel[i].time);
}
} else {
s.value.shift();
s.value.push(last.value.up);
x.value.shift();
x.value.push(last.value.down);
months.value.shift();
months.value.push(last.value.time);
}
setTimeout(() => {
nextTick(() =>
useEcharts(fullYearSalesChart.value as HTMLDivElement).setOption(option.value)
);
}, 10);
});
const updateChart = () => {
useEcharts(fullYearSalesChart.value as HTMLDivElement).resize();
};
onBeforeUnmount(() => {
dispose(fullYearSalesChart.value as HTMLDivElement);
});
return {
fullYearSalesChart,
updateChart,
last,
};
},
});
</script>
<style lang="less" scoped>
.chart-item-container {
width: 100%;
.chart-item {
height: 345px;
}
}
.light-green {
height: 108px;
background-color: rgba(0, 128, 0, 0.12);
}
.green {
height: 108px;
background-color: rgba(0, 128, 0, 0.24);
}
</style>

View File

@@ -0,0 +1,122 @@
<template>
<div ref="orderChartWrapper" style="height: 100%"></div>
</template>
<script lang="ts">
import useEcharts from '@/hooks/useEcharts';
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { dispose, graphic } from 'echarts';
export default defineComponent({
name: 'LoadChart',
props: {
dataModel: {
type: Array,
default: () => {
// eslint-disable-next-line vue/require-valid-default-prop
return {};
},
},
},
setup(props) {
const data = ref([]);
const option = ref({
tooltip: {
trigger: 'item',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985',
},
},
},
grid: {
x: '-5%',
y: 0,
x2: '-5%',
y2: 0,
},
xAxis: {
type: 'category',
splitLine: { show: false },
},
yAxis: [
{
type: 'value',
splitLine: { show: false },
},
],
series: [
{
type: 'line',
smooth: true,
lineStyle: {
width: 0,
},
showSymbol: false,
areaStyle: {
opacity: 0.8,
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: 'rgba(128, 255, 165)',
},
{
offset: 1,
color: 'rgba(1, 191, 236)',
},
]),
},
data: [],
},
],
});
const loading = ref(true);
const orderChartWrapper = ref<HTMLDivElement | null>(null);
const init = () => {
for (let i = 0; i < props.dataModel?.length; i++) {
data.value.push({
name: 'CPU分钟负载比率',
value: [props.dataModel[i]?.time, props.dataModel[i]?.ratio],
});
}
// 基于准备好的dom初始化echarts实例
setTimeout(() => {
loading.value = false;
nextTick(() => {
useEcharts(orderChartWrapper.value as HTMLDivElement).setOption(option.value);
});
}, 100);
// 绘制图表
option.value.series.forEach((item) => {
item.data = data.value;
});
useEcharts(orderChartWrapper.value as HTMLDivElement).setOption(option.value);
};
const updateChart = () => {
useEcharts(orderChartWrapper.value as HTMLDivElement).resize();
};
onMounted(init);
onBeforeUnmount(() => {
dispose(orderChartWrapper.value as HTMLDivElement);
});
watch(props, (_newVal, _oldVal) => {
let last = _newVal.dataModel[_newVal.dataModel.length - 1];
data.value.shift();
data.value.push({
name: 'CPU分钟负载比率',
value: [last?.time, last?.ratio],
});
useEcharts(orderChartWrapper.value as HTMLDivElement).setOption(option.value);
});
return {
loading,
orderChartWrapper,
updateChart,
};
},
});
</script>

View File

@@ -0,0 +1,287 @@
<template>
<div class="main-container">
<n-spin :show="loading" description="正在从服务器查询信息...">
<n-grid responsive="screen" cols="1 s:2 m:4 l:4 xl:4 2xl:4" x-gap="5" y-gap="5">
<n-grid-item v-for="(item, index) of dataSource.head" :key="index" class="item-wrapper">
<DataItem :data-model="item" :loading="loading">
<template v-if="index === 0" #extra="{ extra }">
<div class="margin-top-lg">
<div> {{ extra.data }}</div>
<div class="margin-top-sm"> {{ extra.data1 }}</div>
</div>
</template>
<template v-else-if="index === 1" #extra="{ extra }">
<div class="margin-top" style="position: relative">
<div> 已用内存{{ extra.data }}</div>
<div class="margin-top-sm"> 剩余内存{{ extra.data1 }}</div>
<div class="stack-avatar-wrapper"></div>
</div>
</template>
<template v-else-if="index === 2" #extra="{ extra }">
<n-progress type="line" :percentage="extra.data" />
</template>
<template v-else-if="index === 3" #extra>
<LoadChart ref="mLoadChart" :data-model="dataSource.load" />
</template>
</DataItem>
</n-grid-item>
</n-grid>
<n-grid class="mt-2">
<n-grid-item :span="24">
<FullYearSalesChart
ref="fullYearSalesChart"
:data-model="dataSource.net"
:loading="loading"
/>
</n-grid-item>
</n-grid>
<n-space vertical style="padding-top: 10px">
<n-card title="服务器信息">
<n-descriptions
label-placement="top"
bordered
cols="1 s:1 m:2 l:3 xl:4 2xl:4"
:label-style="{ 'font-weight': 'bold', 'font-size': '16px' }"
>
<n-descriptions-item label="服务器名称">
{{ dataRunInfo.hostname }}
</n-descriptions-item>
<n-descriptions-item label="操作系统"> {{ dataRunInfo.os }}</n-descriptions-item>
<n-descriptions-item label="服务器IP">
{{ dataRunInfo.intranet_ip }} /
{{ dataRunInfo.public_ip }}
</n-descriptions-item>
<n-descriptions-item label="系统架构"> {{ dataRunInfo.arch }}</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card title="GO运行信息">
<n-descriptions
label-placement="top"
bordered
cols="1 s:1 m:2 l:3 xl:4 2xl:4"
:label-style="{ 'font-weight': 'bold', 'font-size': '16px' }"
>
<n-descriptions-item label="语言环境"> {{ dataRunInfo.goName }}</n-descriptions-item>
<n-descriptions-item label="版本号"> {{ dataRunInfo.version }}</n-descriptions-item>
<n-descriptions-item label="启动时间"> {{ dataRunInfo.startTime }}</n-descriptions-item>
<n-descriptions-item label="运行时长">
{{ formatBefore(new Date(dataRunInfo.startTime)) }}
</n-descriptions-item>
<n-descriptions-item label="运行路径"> {{ dataRunInfo.pwd }}</n-descriptions-item>
<n-descriptions-item label="goroutine数量">
{{ dataRunInfo.goroutine }}
</n-descriptions-item>
<n-descriptions-item label="运行内存"> {{ dataRunInfo.goMem }}</n-descriptions-item>
<n-descriptions-item label="磁盘占用"> {{ dataRunInfo.goSize }}</n-descriptions-item>
</n-descriptions>
</n-card>
</n-space>
</n-spin>
</div>
</template>
<script lang="ts">
import DataItem from './components/DataItem.vue';
import LoadChart from './components/chart/LoadChart.vue';
import FullYearSalesChart from './components/chart/FullYearSalesChart.vue';
import { defineComponent, inject, onMounted, ref, onUpdated } from 'vue';
import { SocketEnum } from '@/enums/socketEnum';
import { addOnMessage, sendMsg } from '@/utils/websocket';
import { formatBefore } from '@/utils/dateUtil';
import { useMessage } from 'naive-ui';
export default defineComponent({
name: 'Home',
components: {
DataItem,
LoadChart,
FullYearSalesChart,
},
setup() {
const dataRunInfo = ref({
arch: '',
goMem: '0MB',
goName: 'Golang',
goSize: '0MB',
goroutine: 0,
hostname: '',
intranet_ip: '127.0.0.1',
os: '',
public_ip: '0.0.0.0',
pwd: '/',
rootPath: '/',
runTime: 0,
startTime: '',
version: '',
});
const dataSource = ref({
head: [
{
title: 'CPU',
data: '0%',
bottomTitle: 'CPU数量',
totalSum: '',
iconClass: 'HardwareChip',
extra: {
data: '',
data1: '',
},
},
{
title: '内存',
data: '0%',
bottomTitle: '总内存',
totalSum: '0GB',
iconClass: 'AppsSharp',
extra: {
data: '0GB',
data1: '0GB',
},
},
{
title: '磁盘',
data: '已用 0GB',
bottomTitle: '总容量',
totalSum: '0GB',
iconClass: 'PieChart',
extra: {
data: 0,
},
},
{
title: '负载',
data: '0%',
bottomTitle: '总进程数',
totalSum: '0个',
iconClass: 'Analytics',
extra: {
data: 80,
},
},
],
load: {},
net: {},
});
const message = useMessage();
const loading = ref(true);
const onMessageList = inject('onMessageList');
// const onAdminMonitorTrends = (res) => {
//
// const data = JSON.parse(res.data);
// console.log('onAdminMonitorTrends...,'+ data.event)
// if (data.event !== SocketEnum.EventAdminMonitorTrends) {
// return;
// }
// console.log('onAdminMonitorTrends okokoko,' + JSON.stringify(data))
// loading.value = false;
// if (data.code == SocketEnum.CodeErr) {
// message.success('查询出错');
// return;
// }
// dataSource.value = data.data;
// console.log('dataSource.value.net:'+JSON.stringify(dataSource.value.net))
// };
//
// addOnMessage(onMessageList, onAdminMonitorTrends);
const onAdminMonitor= (res) => {
const data = JSON.parse(res.data);
if (data.event === SocketEnum.EventAdminMonitorRunInfo) {
loading.value = false;
if (data.code == SocketEnum.CodeErr) {
message.success('查询出错');
return;
}
dataRunInfo.value = data.data;
return;
}
if (data.event === SocketEnum.EventAdminMonitorTrends) {
loading.value = false;
if (data.code == SocketEnum.CodeErr) {
message.success('查询出错');
return;
}
dataSource.value = data.data;
return;
}
};
addOnMessage(onMessageList, onAdminMonitor);
onMounted(() => {
getInfo();
});
onUpdated(() => {
// 切换页面后直接出发一次
if (loading.value === false) {
sendMsg(SocketEnum.EventAdminMonitorTrends);
sendMsg(SocketEnum.EventAdminMonitorRunInfo);
}
});
function getInfo() {
loading.value = true;
sendMsg(SocketEnum.EventAdminMonitorTrends);
sendMsg(SocketEnum.EventAdminMonitorRunInfo);
setInterval(function () {
sendMsg(SocketEnum.EventAdminMonitorTrends);
}, 1000 * 2);
setInterval(function () {
sendMsg(SocketEnum.EventAdminMonitorRunInfo);
}, 1000 * 10);
}
const mLoadChart = ref<InstanceType<typeof LoadChart>>();
const fullYearSalesChart = ref<InstanceType<typeof FullYearSalesChart>>();
const onResize = () => {
setTimeout(() => {
mLoadChart.value?.updateChart();
// fullYearSalesChart.value?.updateChart();
}, 500);
};
const collapse = true;
onResize();
return {
loading,
collapse,
mLoadChart,
fullYearSalesChart,
dataSource,
dataRunInfo,
formatBefore,
};
},
});
</script>
<style lang="less" scoped>
@media screen and (max-width: 992px) {
.item-wrapper {
margin-bottom: 5px;
}
.map-margin-tb {
margin: 5px 0;
}
}
.light {
.chart-item {
background-color: #fff;
}
}
.stack-avatar-wrapper {
position: absolute;
right: -2%;
top: 10%;
}
</style>

View File

@@ -0,0 +1,417 @@
<template>
<div>
<n-card :bordered="false" title="部门管理">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="formRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<n-space vertical :size="12">
<n-space>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
新建
</n-button>
</n-space>
<n-data-table
:columns="columns"
:data="data"
:row-key="rowKey"
:loading="loading"
default-expand-all
/>
</n-space>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="上级部门" path="pid">
<n-tree-select
:options="options"
:default-value="optionsDefaultValue"
:default-expand-all="true"
@update:value="handleUpdateValue"
/>
</n-form-item>
<n-form-item label="部门名称" path="name">
<n-input placeholder="请输入名称" v-model:value="formParams.name" />
</n-form-item>
<n-form-item label="部门编码" path="code">
<n-input placeholder="请输入部门编码" v-model:value="formParams.code" />
</n-form-item>
<n-form-item label="负责人" path="leader">
<n-input placeholder="请输入负责人" v-model:value="formParams.leader" />
</n-form-item>
<n-form-item label="联系电话" path="phone">
<n-input placeholder="请输入联系电话" v-model:value="formParams.phone" />
</n-form-item>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入邮箱" v-model:value="formParams.email" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, onMounted, ref } from 'vue';
import {
DataTableColumns,
NButton,
NTag,
TreeSelectOption,
useDialog,
useMessage,
} from 'naive-ui';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { PlusOutlined } from '@vicons/antd';
import { TableAction } from '@/components/Table';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
import { Delete, Edit, getDeptList, Status } from '@/api/org/dept';
const rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
code: {
required: true,
trigger: ['blur', 'input'],
message: '请输入编码',
},
};
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '部门名称',
componentProps: {
placeholder: '请输入部门名称',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入部门名称', trigger: ['blur'] }],
},
{
field: 'code',
component: 'NInput',
label: '部门编码',
componentProps: {
placeholder: '请输入部门编码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
];
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loading = ref(false);
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
let resetFormParams = {
id: 0,
pid: 0,
name: '',
code: '',
type: '',
leader: '',
phone: '',
email: '',
sort: 0,
status: 1,
created_at: '',
updated_at: '',
};
let formParams = ref(resetFormParams);
const params = ref({
pageSize: 5,
name: 'xiaoMa',
});
type RowData = {
name: string;
index: string;
children?: RowData[];
};
const data = ref([]);
const columns: DataTableColumns<RowData> = [
{
type: 'selection',
},
{
title: '部门名称',
key: 'name',
width: 200,
},
{
title: '部门ID',
key: 'index',
width: 100,
},
{
title: '部门编码',
key: 'code',
width: 100,
},
{
title: '负责人',
key: 'leader',
width: 100,
},
{
title: '联系电话',
key: 'phone',
width: 150,
},
{
title: '邮箱',
key: 'email',
width: 150,
},
{
title: '状态',
key: 'status',
width: 80,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'info' : 'error',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '已禁用'),
}
);
},
},
{
title: '排序',
key: 'sort',
width: 80,
},
{
title: '创建时间',
key: 'created_at',
width: 200,
render: (rows, _) => {
return rows.created_at; //timestampToTime();
},
},
{
title: '操作',
key: 'actions',
width: 220,
// fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
// console.log('select record:' + JSON.stringify(record));
// message.info(`您点击了,${key} 按钮`);
updateStatus(record.id, key);
},
});
},
},
];
const rowKey = (row: RowData) => row.index;
function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
console.log('addTable formParams:' + JSON.stringify(formParams.value));
optionsDefaultValue.value = null;
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
formParams.value.children = null;
optionsDefaultValue.value = formParams.value.pid;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
loadDataTable({});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function updateStatus(id, status) {
Status({ id: id, status: status })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
loadDataTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
Edit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
loadDataTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
async function handleSubmit(values: Recordable) {
console.log('handleSubmit valuesL:' + JSON.stringify(values));
// reloadTable();
await loadDataTable(values);
}
function handleReset(values: Recordable) {
console.log(values);
}
const loadDataTable = async (res) => {
loading.value = true;
data.value = await getDeptList({ ...res, ...formRef.value?.formModel });
if (data.value === undefined || data.value === null) {
data.value = [];
}
options.value = [
{
index: 0,
key: 0,
label: '顶级部门',
children: data.value,
},
];
loading.value = false;
};
onMounted(async () => {
await loadDataTable({});
});
function handleUpdateValue(
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option);
formParams.value.pid = value;
}
const options = ref([]);
const optionsDefaultValue = ref(null);
</script>

View File

@@ -0,0 +1,58 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
key: 'id',
width: 100,
},
{
title: '岗位名称',
key: 'name',
width: 100,
},
{
title: '岗位名称',
key: 'name',
width: 100,
},
{
title: '岗位编码',
key: 'code',
width: 100,
},
{
title: '状态',
key: 'status',
width: 100,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'info' : 'error',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '已禁用'),
}
);
},
},
{
title: '排序',
key: 'sort',
width: 100,
},
{
title: '创建时间',
key: 'createdAt',
width: 100,
render: (rows, _) => {
return rows.createdAt;
},
},
];

View File

@@ -0,0 +1,345 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="岗位管理">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
新建
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" title="新建">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="岗位名称" path="name">
<n-input placeholder="请输入岗位名称" v-model:value="formParams.name" />
</n-form-item>
<n-form-item label="岗位编码" path="code">
<n-input placeholder="请输入岗位编码" v-model:value="formParams.code" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, getPostList, Status } from '@/api/org/post';
import { columns } from './columns';
import { DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
const params = ref({
pageSize: 10,
name: '',
code: '',
status: null,
});
const rules = {
name: {
// required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
};
const schemas: FormSchema[] = [
{
field: 'name',
component: 'NInput',
label: '岗位名称',
componentProps: {
placeholder: '请输入岗位名称',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入岗位名称', trigger: ['blur'] }],
},
{
field: 'code',
component: 'NInput',
label: '岗位编码',
componentProps: {
placeholder: '请输入岗位编码',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref({});
const formRef = ref({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const resetFormParams = {
id: 0,
pid: 0,
name: '',
code: '',
type: '',
leader: '',
phone: '',
email: '',
sort: 0,
status: 1,
created_at: '',
updated_at: '',
};
let formParams = ref(resetFormParams);
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
}
const loadDataTable = async (res) => {
return await getPostList({ ...params.value, ...res, ...searchFormRef.value.formModel });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
Edit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
reloadTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,119 @@
import { h } from 'vue';
import { NAvatar, NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
key: 'id',
width: 50,
},
{
title: '用户名',
key: 'username',
width: 100,
},
{
title: '姓名',
key: 'realname',
width: 100,
},
{
title: '头像',
key: 'avatar',
width: 50,
render(row) {
if (row.avatar !== '') {
return h(NAvatar, {
circle: true,
size: 'small',
src: row.avatar,
});
} else {
return h(
NAvatar,
{
circle: true,
size: 'small',
},
{
default: () =>
row.realname !== '' ? row.realname.substring(0, 1) : row.username.substring(0, 2),
}
);
}
},
},
{
title: '绑定角色',
key: 'role_name',
width: 100,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'success',
bordered: false,
},
{
default: () => row.role_name,
}
);
},
},
{
title: '所属部门',
key: 'dept_name',
width: 100,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'info',
bordered: false,
},
{
default: () => row.dept_name,
}
);
},
},
{
title: '状态',
key: 'status',
width: 50,
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'info' : 'error',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '已禁用'),
}
);
},
},
{
title: '访问次数',
key: 'visitCount',
width: 80,
},
{
title: '创建时间',
key: 'createdAt',
width: 100,
render: (rows, _) => {
return rows.createdAt;
},
},
];

View File

@@ -0,0 +1,509 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="后台用户">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
新建
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="新建"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="姓名" path="realname">
<n-input placeholder="请输入姓名" v-model:value="formParams.realname" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="登录用户名" path="username">
<n-input placeholder="请输入登录用户名" v-model:value="formParams.username" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="绑定角色" path="role">
<n-select
:default-value="formParams.role"
:options="roleList"
@update:value="handleUpdateRoleValue"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="所属部门" path="dept_id">
<n-tree-select
:options="deptList"
:default-value="formParams.dept_id"
:default-expand-all="true"
@update:value="handleUpdateDeptValue"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="绑定岗位" path="postIds">
<n-select
:default-value="formParams.postIds"
multiple
:options="postList"
@update:value="handleUpdatePostValue"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="密码" path="password">
<n-input
type="password"
:placeholder="formParams.id === 0 ? '请输入' : '不填则不修改'"
v-model:value="formParams.password"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-divider title-placement="left">填写更多信息(可选)</n-divider>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="手机号" path="mobile">
<n-input placeholder="请输入" v-model:value="formParams.mobile" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入" v-model:value="formParams.email" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="性别" path="sex">
<n-radio-group v-model:value="formParams.sex" name="sex">
<n-radio-button
v-for="status in sexOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { SelectOption, TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, List, Status } from '@/api/org/user';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { sexOptions, statusActions, statusOptions } from '@/enums/optionsiEnum';
import { getDeptList } from '@/api/org/dept';
import { getRoleList } from '@/api/system/role';
import { getPostList } from '@/api/org/post';
const params = ref({
pageSize: 10,
name: '',
code: '',
status: null,
});
const rules = {
name: {
// required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
};
const schemas: FormSchema[] = [
{
field: 'username',
component: 'NInput',
label: '用户名',
componentProps: {
placeholder: '请输入用户名',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入用户名', trigger: ['blur'] }],
},
{
field: 'realname',
component: 'NInput',
label: '姓名',
componentProps: {
placeholder: '请输入姓名',
showButton: false,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'mobile',
component: 'NInputNumber',
label: '手机',
componentProps: {
placeholder: '请输入手机号码',
showButton: false,
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'created_at',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
// defaultValue: [new Date() - 86400000 * 30, new Date()],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref({});
const formRef = ref({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const deptList = ref([]);
const roleList = ref([]);
const postList = ref([]);
const resetFormParams = {
id: 0,
role: null,
realname: '',
username: '',
password: '',
dept_id: null,
postIds: null,
mobile: '',
email: '',
sex: 1,
leader: '',
phone: '',
sort: 0,
status: 1,
created_at: '',
updated_at: '',
};
let formParams = ref(resetFormParams);
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
}
const loadDataTable = async (res) => {
mapWidth();
deptList.value = await getDeptList({});
if (deptList.value === undefined || deptList.value === null) {
deptList.value = [];
}
roleList.value = [];
let roleLists = await getRoleList();
if (roleLists.list === undefined || roleLists.list === null) {
roleLists = [];
} else {
roleLists = roleLists.list;
}
if (roleLists.length > 0) {
for (let i = 0; i < roleLists.length; i++) {
roleList.value[i] = {};
roleList.value[i].label = roleLists[i].name;
roleList.value[i].value = roleLists[i].id;
}
}
postList.value = [];
let postLists = await getPostList();
if (postLists.list === undefined || postLists.list === null) {
postLists = [];
} else {
postLists = postLists.list;
}
if (postLists.length > 0) {
for (let i = 0; i < postLists.length; i++) {
postList.value[i] = {};
postList.value[i].label = postLists[i].name;
postList.value[i].value = postLists[i].id;
}
}
console.log('post.value:' + JSON.stringify(postList.value));
return await List({ ...params.value, ...res, ...searchFormRef.value.formModel });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
Edit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((_e: Error) => {
// message.error(_e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
reloadTable();
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
const dialogWidth = ref('50%');
function mapWidth() {
let val = document.body.clientWidth;
const def = 720; // 默认宽度
if (val < def) {
dialogWidth.value = '100%';
} else {
dialogWidth.value = def + 'px';
}
return dialogWidth.value;
}
function handleUpdateDeptValue(
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option);
formParams.value.dept_id = value;
}
function handleUpdateRoleValue(
value: string | number | Array<string | number> | null,
option: SelectOption | null | Array<SelectOption | null>
) {
console.log(value, option);
formParams.value.role = value;
}
function handleUpdatePostValue(
value: string | number | Array<string | number> | null,
option: SelectOption | null | Array<SelectOption | null>
) {
console.log(value, option);
formParams.value.postIds = value;
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,427 @@
<template>
<n-drawer v-model:show="isDrawer" :width="width" :placement="placement">
<n-drawer-content :title="title" closable>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
>
<n-divider title-placement="left">基本设置</n-divider>
<n-form-item label="类型" path="type">
<n-radio-group v-model:value="formParams.type" name="type">
<n-radio-button
v-for="menuType in menuTypes"
:key="menuType.value"
:value="menuType.value"
:label="menuType.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item
:label="
formParams.type === 1 ? '上级目录' : formParams.type === 2 ? '上级菜单' : '上级按钮'
"
path="pid"
>
<n-tree-select
:options="optionTreeData"
default-value="0"
@update:value="handleUpdateValue"
/>
</n-form-item>
<n-form-item
:label="
formParams.type === 1 ? '目录名称' : formParams.type === 2 ? '菜单名称' : '按钮名称'
"
path="title"
>
<n-input
:placeholder="
formParams.type === 1 ? '目录名称' : formParams.type === 2 ? '菜单名称' : '按钮名称'
"
v-model:value="formParams.title"
/>
</n-form-item>
<n-form-item label="" path="icon">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请填写图标编码可以参考图标库也可以不填使用默认图标
</n-tooltip>
<span>&nbsp;&nbsp;图标 </span>
</span>
</div>
<n-input placeholder="图标映射路径" v-model:value="formParams.icon" />
</n-form-item>
<n-form-item label="" path="path">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
路由地址user
</n-tooltip>
<span>&nbsp;&nbsp;路由地址 </span>
</span>
</div>
<n-input placeholder="路由地址" v-model:value="formParams.path" />
</n-form-item>
<n-form-item label="" path="name">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
对应路由配置文件中 `name` 只能是唯一性配置 `http(s)://` 开头地址 则会新窗口打开
</n-tooltip>
<span>&nbsp;&nbsp;路由别名 </span>
</span>
</div>
<n-input placeholder="路由别名" v-model:value="formParams.name" />
</n-form-item>
<n-form-item label="" path="component">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
访问的组件路径`/system/menu/menu`默认在`views`目录下默认 `LAYOUT`
如果是多级菜单 `ParentLayout`
</n-tooltip>
<span>&nbsp;&nbsp;组件路径 </span>
</span>
</div>
<n-input placeholder="组件路径" v-model:value="formParams.component" />
</n-form-item>
<n-form-item label="" path="redirect" v-show="formParams.type === 1">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
默认跳转路由地址`/system/menu/menu` 多级路由情况下适用
</n-tooltip>
<span>&nbsp;&nbsp;默认跳转 </span>
</span>
</div>
<n-input placeholder="默认路由跳转地址" v-model:value="formParams.redirect" />
</n-form-item>
<n-divider title-placement="left">功能设置</n-divider>
<n-form-item label="API权限" path="permissions">
<n-input
placeholder="请输入API权限多个权限用,分割"
v-model:value="formParams.permissions"
/>
</n-form-item>
<!-- <n-form-item label="权限名称" path="permissionName">-->
<!-- <n-input placeholder="权限名称" v-model:value="formParams.permissionName" />-->
<!-- </n-form-item>-->
<n-form-item label="高亮路由" path="activeMenu">
<n-input placeholder="高亮路由" v-model:value="formParams.activeMenu" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="根路由" path="isRoot">
<n-radio-group v-model:value="formParams.isRoot" name="isRoot">
<n-radio-button
v-for="switchStatus in switchStatusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="简化路由" path="alwaysShow">
<n-radio-group v-model:value="formParams.alwaysShow" name="alwaysShow">
<n-radio-button
v-for="switchStatus in switchStatusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="缓存路由" path="keepAlive">
<n-radio-group v-model:value="formParams.keepAlive" name="keepAlive">
<n-radio-button
v-for="switchStatus in switchStatusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="是否隐藏" path="hidden">
<n-radio-group v-model:value="formParams.hidden" name="hidden">
<n-radio-button
v-for="hidden in hiddenMap"
:key="hidden.value"
:value="hidden.value"
:label="hidden.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="是否外链" path="isFrame">
<n-radio-group v-model:value="formParams.isFrame" name="isFrame">
<n-radio-button
v-for="switchStatus in switchStatusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusMap"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="外部地址" path="frameSrc" v-show="formParams.isFrame === true">
<n-input placeholder="内联外部地址" v-model:value="formParams.frameSrc" />
</n-form-item>
</n-gi>
<n-gi />
</n-grid>
</n-form>
<template #footer>
<n-space>
<n-button type="primary" :loading="subLoading" @click="formSubmit">提交</n-button>
<n-button @click="handleReset">重置</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
import { TreeSelectOption, useMessage } from 'naive-ui';
import { QuestionCircleOutlined } from '@vicons/antd';
import { EditMenu } from '@/api/system/menu';
const menuTypes = [
{
value: 1,
label: '目录',
},
{
value: 2,
label: '菜单',
},
{
value: 3,
label: '按钮',
},
].map((s) => {
return s;
});
const switchStatusMap = [
{
value: 0,
label: '关闭',
},
{
value: 1,
label: '开启',
},
].map((s) => {
return s;
});
const statusMap = [
{
value: 0,
label: '禁用',
},
{
value: 1,
label: '启用',
},
].map((s) => {
return s;
});
const hiddenMap = [
{
value: 0,
label: '否',
},
{
value: 1,
label: '是',
},
].map((s) => {
return s;
});
const rules = {
label: {
required: true,
message: '请输入标题',
trigger: 'blur',
},
path: {
required: true,
message: '请输入路径',
trigger: 'blur',
},
};
export default defineComponent({
name: 'CreateDrawer',
components: {},
props: {
title: {
type: String,
default: '添加顶级菜单',
},
optionTreeData: {
type: Object,
// eslint-disable-next-line vue/require-valid-default-prop
default: [],
},
},
emits: ['loadData'],
setup(_props, context) {
const message = useMessage();
const formRef: any = ref(null);
const defaultValueRef = () => ({
id: 0,
pid: 0,
title: '',
name: '',
path: '',
label: '',
icon: '',
type: 1,
redirect: '',
permissions: '',
permissionName: '',
component: '',
alwaysShow: 1,
activeMenu: '',
isRoot: 0,
isFrame: 0,
frameSrc: '',
keepAlive: 0,
hidden: 0,
affix: 0,
status: 1,
sort: 10,
});
const state = reactive({
width: 700,
isDrawer: false,
subLoading: false,
formParams: defaultValueRef(),
placement: 'right',
icon: '',
alertText:
'该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置,建议在生产环境关闭该布局预览功能。',
});
function openDrawer() {
if (document.body.clientWidth < 700) {
state.width = document.body.clientWidth;
}
state.isDrawer = true;
}
function closeDrawer() {
state.isDrawer = false;
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
console.log('state.formParams:' + JSON.stringify(state.formParams));
EditMenu({ ...state.formParams })
.then(async (_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
handleReset();
await context.emit('loadData');
closeDrawer();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
});
}
function handleReset() {
formRef.value.restoreValidation();
state.formParams = Object.assign(state.formParams, defaultValueRef());
}
// 处理选项更新
function handleUpdateValue(
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option);
state.formParams.pid = value;
}
return {
...toRefs(state),
formRef,
rules,
formSubmit,
handleReset,
openDrawer,
closeDrawer,
menuTypes,
switchStatusMap,
statusMap,
hiddenMap,
handleUpdateValue,
QuestionCircleOutlined,
};
},
});
</script>

View File

@@ -0,0 +1,619 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="菜单管理"> 在这里可以管理编辑系统下的所有菜单导航</n-card>
</div>
<n-grid class="mt-4" cols="1 s:1 m:1 l:3 xl:3 2xl:3" responsive="screen" :x-gap="12">
<n-gi span="1">
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
<n-space>
<!-- <n-dropdown trigger="hover" @select="selectAddMenu" :options="addMenuOptions">-->
<!-- <n-button type="info" ghost icon-placement="right">-->
<!-- 添加菜单-->
<!-- <template #icon>-->
<!-- <div class="flex items-center">-->
<!-- <n-icon size="14">-->
<!-- <DownOutlined />-->
<!-- </n-icon>-->
<!-- </div>-->
<!-- </template>-->
<!-- </n-button>-->
<!-- </n-dropdown>-->
<n-button type="info" ghost icon-placement="left" @click="openCreateDrawer">
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<PlusOutlined />
</n-icon>
</div>
</template>
添加菜单
</n-button>
<n-button type="info" ghost icon-placement="left" @click="packHandle">
全部{{ expandedKeys.length ? '收起' : '展开' }}
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<AlignLeftOutlined />
</n-icon>
</div>
</template>
</n-button>
</n-space>
</template>
<div class="w-full menu">
<n-input type="input" v-model:value="pattern" placeholder="输入菜单名称搜索">
<template #suffix>
<n-icon size="18" class="cursor-pointer">
<SearchOutlined />
</n-icon>
</template>
</n-input>
<div class="py-3 menu-list">
<template v-if="loading">
<div class="flex items-center justify-center py-4">
<n-spin size="medium" />
</div>
</template>
<template v-else>
<n-tree
block-line
cascade
checkable
:virtual-scroll="true"
:pattern="pattern"
:data="treeData"
:expandedKeys="expandedKeys"
style="max-height: 650px; overflow: hidden"
@update:selected-keys="selectedTree"
@update:expanded-keys="onExpandedKeys"
/>
</template>
</div>
</div>
</n-card>
</n-gi>
<n-gi span="2">
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
<n-space>
<n-icon size="18">
<FormOutlined />
</n-icon>
<span>编辑菜单{{ treeItemTitle ? `${treeItemTitle}` : '' }}</span>
<span style="font-size: 14px">{{
treeItemTitle ? '' : '从菜单列表选择一项后,进行编辑'
}}</span>
</n-space>
</template>
<!-- <n-alert type="info" closable> 从菜单列表选择一项后进行编辑</n-alert>-->
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
v-if="isEditMenu"
class="py-4"
>
<n-divider title-placement="left">基本设置</n-divider>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="类型" path="type">
<n-radio-group v-model:value="formParams.type" name="type">
<n-radio-button
v-for="menuType in menuTypes"
:key="menuType.value"
:value="menuType.value"
:label="menuType.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item
:label="
formParams.type === 1
? '上级目录'
: formParams.type === 2
? '上级菜单'
: '上级按钮'
"
path="pid"
>
<n-tree-select
:options="optionTreeData"
:value="formParams.pid"
@update:value="handleUpdateValue"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item
:label="
formParams.type === 1
? '目录名称'
: formParams.type === 2
? '菜单名称'
: '按钮名称'
"
path="title"
>
<n-input
:placeholder="
formParams.type === 1
? '目录名称'
: formParams.type === 2
? '菜单名称'
: '按钮名称'
"
v-model:value="formParams.title"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="" path="icon">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
请填写图标编码可以参考图标库也可以不填使用默认图标
</n-tooltip>
<span>&nbsp;&nbsp;图标 </span>
</span>
</div>
<n-input placeholder="图标映射路径" v-model:value="formParams.icon" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="" path="path">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
路由地址user
</n-tooltip>
<span>&nbsp;&nbsp;路由地址 </span>
</span>
</div>
<n-input placeholder="路由地址" v-model:value="formParams.path" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="" path="name">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
对应路由配置文件中 `name` 只能是唯一性配置 `http(s)://` 开头地址
则会新窗口打开
</n-tooltip>
<span>&nbsp;&nbsp;路由别名 </span>
</span>
</div>
<n-input placeholder="路由别名" v-model:value="formParams.name" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="" path="component">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
访问的组件路径`/system/menu/menu`默认在`views`目录下默认 `LAYOUT`
如果是多级菜单 `ParentLayout`
</n-tooltip>
<span>&nbsp;&nbsp;组件路径 </span>
</span>
</div>
<n-input placeholder="组件路径" v-model:value="formParams.component" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="" path="redirect">
<div style="width: 120px">
<span>
<n-tooltip trigger="hover">
<template #trigger>
<n-icon :component="QuestionCircleOutlined" :size="18" :depth="3" />
</template>
默认跳转路由地址`/system/menu/menu` 多级路由情况下适用
</n-tooltip>
<span>&nbsp;&nbsp;默认跳转 </span>
</span>
</div>
<n-input placeholder="默认路由跳转地址" v-model:value="formParams.redirect" />
</n-form-item>
</n-gi>
</n-grid>
<n-divider title-placement="left">功能设置</n-divider>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="API权限" path="permissions">
<n-input
placeholder="请输入API权限多个权限用,分割"
v-model:value="formParams.permissions"
/>
</n-form-item>
</n-gi>
<n-gi>
<!-- <n-form-item label="权限名称" path="permissionName">-->
<!-- <n-input placeholder="权限名称" v-model:value="formParams.permissionName" />-->
<!-- </n-form-item>-->
<n-form-item label="高亮路由" path="activeMenu">
<n-input placeholder="高亮路由" v-model:value="formParams.activeMenu" />
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="4">
<n-gi>
<n-form-item label="根路由" path="isRoot">
<n-radio-group v-model:value="formParams.isRoot" name="isRoot">
<n-radio-button
v-for="switchStatus in switchStatusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="简化路由" path="alwaysShow">
<n-radio-group v-model:value="formParams.alwaysShow" name="alwaysShow">
<n-radio-button
v-for="switchStatus in switchStatusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="缓存路由" path="keepAlive">
<n-radio-group v-model:value="formParams.keepAlive" name="keepAlive">
<n-radio-button
v-for="switchStatus in switchStatusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="是否隐藏" path="hidden">
<n-radio-group v-model:value="formParams.hidden" name="hidden">
<n-radio-button
v-for="hidden in hiddenMap"
:key="hidden.value"
:value="hidden.value"
:label="hidden.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="24" :cols="4">
<n-gi>
<n-form-item label="是否外链" path="isFrame">
<n-radio-group v-model:value="formParams.isFrame" name="isFrame">
<n-radio-button
v-for="switchStatus in switchStatusMap"
:key="switchStatus.value"
:value="switchStatus.value"
:label="switchStatus.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusMap"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="外部地址" path="frameSrc" v-show="formParams.isFrame === true">
<n-input placeholder="内联外部地址" v-model:value="formParams.frameSrc" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item path="auth" style="margin-left: 100px">
<n-space>
<n-button type="primary" :loading="subLoading" @click="formSubmit"
>保存修改
</n-button>
<n-button @click="handleReset">重置</n-button>
<n-button @click="handleDel">删除</n-button>
</n-space>
</n-form-item>
</n-form>
</n-card>
</n-gi>
</n-grid>
<CreateDrawer
ref="createDrawerRef"
:title="drawerTitle"
:optionTreeData="optionTreeData"
@loadData="loadData"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, reactive, ref, unref } from 'vue';
import { TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import {
AlignLeftOutlined,
FormOutlined,
PlusOutlined,
QuestionCircleOutlined,
SearchOutlined,
} from '@vicons/antd';
import { DeleteMenu, EditMenu, getMenuList } from '@/api/system/menu';
import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue';
const menuTypes = [
{
value: 1,
label: '目录',
},
{
value: 2,
label: '菜单',
},
{
value: 3,
label: '按钮',
},
].map((s) => {
return s;
});
const switchStatusMap = [
{
value: 0,
label: '关闭',
},
{
value: 1,
label: '开启',
},
].map((s) => {
return s;
});
const statusMap = [
{
value: 0,
label: '禁用',
},
{
value: 1,
label: '启用',
},
].map((s) => {
return s;
});
const hiddenMap = [
{
value: 0,
label: '否',
},
{
value: 1,
label: '是',
},
].map((s) => {
return s;
});
const rules = {
label: {
required: true,
message: '请输入标题',
trigger: 'blur',
},
path: {
required: true,
message: '请输入路径',
trigger: 'blur',
},
};
const formRef: any = ref(null);
const createDrawerRef = ref();
const message = useMessage();
const dialog = useDialog();
let treeItemKey = ref([]);
let expandedKeys = ref([]);
const treeData = ref([]);
const loading = ref(true);
const subLoading = ref(false);
const isEditMenu = ref(false);
const treeItemTitle = ref('');
const pattern = ref('');
const drawerTitle = ref('');
const optionTreeData = ref([
{
id: 0,
key: 0,
label: '根目录',
pid: 0,
title: '根目录',
type: 1,
},
]);
const formParams = reactive({
id: 0,
pid: 0,
title: '',
name: '',
path: '',
label: '',
icon: '',
type: 1,
redirect: '',
permissions: '',
permissionName: '',
component: '',
alwaysShow: 1,
activeMenu: '',
isRoot: 0,
isFrame: 0,
frameSrc: '',
keepAlive: 0,
hidden: 0,
affix: 0,
status: 1,
sort: 10,
});
function openCreateDrawer() {
drawerTitle.value = '添加菜单';
const { openDrawer } = createDrawerRef.value;
openDrawer();
}
function selectedTree(keys) {
if (keys.length) {
const treeItem = getTreeItem(unref(treeData), keys[0]);
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
Object.assign(formParams, treeItem);
isEditMenu.value = true;
} else {
isEditMenu.value = false;
treeItemKey.value = [];
treeItemTitle.value = '';
}
}
function handleDel() {
dialog.info({
title: '提示',
content: `您确定想删除此权限吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
console.log('DeleteMenu formParams:' + JSON.stringify(formParams));
DeleteMenu({ ...formParams })
.then(async (_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
// handleReset();
await loadData();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
message.error('已取消');
},
});
}
function handleReset() {
const treeItem = getTreeItem(unref(treeData), treeItemKey.value[0]);
Object.assign(formParams, treeItem);
}
function formSubmit() {
formRef.value.validate((errors: boolean) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams));
// message.error('抱歉,您没有该权限');
EditMenu({ ...formParams })
.then(async (_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
// handleReset();
await loadData();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
});
}
function packHandle() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = unref(treeData).map((item: any) => item.key as string) as [];
}
}
// 处理选项更新
function handleUpdateValue(
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
formParams.pid = value;
console.log(value, option);
}
onMounted(async () => {
await loadData();
});
async function loadData() {
const treeMenuList = await getMenuList();
const keys = treeMenuList.list.map((item) => item.key);
Object.assign(formParams, keys);
treeData.value = [];
optionTreeData.value = [];
treeData.value = treeMenuList.list;
optionTreeData.value = optionTreeData.value.concat(treeMenuList.list);
loading.value = false;
}
function onExpandedKeys(keys) {
expandedKeys.value = keys;
}
const editConfirm = (val) => {
console.log(val);
};
</script>

View File

@@ -0,0 +1,36 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
},
{
title: '角色名称',
key: 'name',
},
{
title: '说明',
key: 'remark',
},
{
title: '是否默认角色',
key: 'isDefault',
render(row) {
return h(
NTag,
{
type: row.id == 1 ? 'success' : 'error',
},
{
default: () => (row.id == 1 ? '是' : '否'),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,355 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="角色管理"> 在这里可以管理你权限下的角色权限</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加角色
</n-button>
</template>
<template #action>
<TableAction />
</template>
</BasicTable>
</n-card>
<n-modal v-model:show="showModal" :show-icon="false" preset="dialog" :title="editRoleTitle">
<div class="py-3 menu-list">
<n-tree
block-line
cascade
checkable
:virtual-scroll="true"
:data="treeData"
:expandedKeys="expandedKeys"
:checked-keys="checkedKeys"
style="max-height: 950px; overflow: hidden"
@update:checked-keys="checkedTree"
@update:expanded-keys="onExpandedKeys"
/>
</div>
<template #action>
<n-space>
<n-button type="info" ghost icon-placement="left" @click="packHandle">
全部{{ expandedKeys.length ? '收起' : '展开' }}
</n-button>
<n-button type="info" ghost icon-placement="left" @click="checkedAllHandle">
全部{{ checkedAll ? '取消' : '选择' }}
</n-button>
<n-button type="primary" :loading="formBtnLoading" @click="confirmForm">提交</n-button>
</n-space>
</template>
</n-modal>
<n-modal v-model:show="showModal2" :show-icon="false" preset="dialog" title="添加角色">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="角色名称" path="name">
<n-input placeholder="请输入名称" v-model:value="formParams.name" />
</n-form-item>
<n-form-item label="权限编码" path="key">
<n-input placeholder="请输入" v-model:value="formParams.key" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal2 = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading2" @click="confirmForm2">确定</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { h, onMounted, reactive, ref, unref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { Delete, Edit, GetPermissions, getRoleList, UpdatePermissions } from '@/api/system/role';
import { getMenuList } from '@/api/system/menu';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { getTreeAll } from '@/utils';
import { useRouter } from 'vue-router';
import { statusOptions } from '@/enums/optionsiEnum';
import { copyObj } from '@/utils/array';
const router = useRouter();
const formRef: any = ref(null);
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal2 = ref(false);
const showModal = ref(false);
const formBtnLoading = ref(false);
const formBtnLoading2 = ref(false);
const checkedAll = ref(false);
const editRoleTitle = ref('');
const treeData = ref([]);
const expandedKeys = ref([]);
const checkedKeys = ref([]);
const updatePermissionsParams = ref({});
const rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
address: {
required: true,
trigger: ['blur', 'input'],
message: '请输入地址',
},
date: {
type: 'number',
required: true,
trigger: ['blur', 'change'],
message: '请选择日期',
},
};
let formParams = reactive({
id: 0,
name: '',
key: '',
remark: null,
status: 1,
sort: 0,
dataScope: 0,
deptCheckStrictly: 0,
menuCheckStrictly: 0,
});
const params = reactive({
pageSize: 5,
name: 'xiaoMa',
});
const actionColumn = reactive({
width: 250,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction, {
style: 'button',
actions: [
{
label: '菜单权限',
onClick: handleMenuAuth.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
// console.log('ifShow record:'+JSON.stringify(record))
return record.key !== 'super';
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
// auth: ['basic_list'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
ifShow: () => {
return record.key !== 'super';
},
// auth: ['basic_list'],
},
{
label: '删除',
// icon: 'ic:outline-delete-outline',
onClick: handleDelete.bind(null, record),
// 根据业务控制是否显示 isShow 和 auth 是并且关系
ifShow: () => {
return record.key !== 'super';
},
// 根据权限控制是否显示: 有权限,会显示,支持多个
// auth: ['basic_list'],
},
],
});
},
});
const loadDataTable = async (res: any) => {
let _params = {
...unref(params),
...res,
};
return await getRoleList(_params);
};
function onCheckedRow(rowKeys: any[]) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e: any) {
console.log('checkedKeys.value:' + JSON.stringify(checkedKeys.value));
console.log('updatePermissionsParams.value:' + JSON.stringify(updatePermissionsParams.value));
e.preventDefault();
formBtnLoading.value = true;
UpdatePermissions({
...{
id: updatePermissionsParams.value.id,
menuIds:
checkedKeys.value === undefined || checkedKeys.value == null ? [] : checkedKeys.value,
},
})
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
showModal.value = false;
formBtnLoading.value = false;
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
function confirmForm2(e) {
e.preventDefault();
formBtnLoading2.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams));
Edit(formParams)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal2.value = false;
reloadTable();
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading2.value = false;
});
}
function addTable() {
showModal2.value = true;
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal2.value = true;
formParams = copyObj(formParams, record);
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
async function handleMenuAuth(record: Recordable) {
editRoleTitle.value = `分配 ${record.name} 的菜单权限`;
const data = await GetPermissions({ ...{ id: record.id } });
console.log('data:' + JSON.stringify(data));
checkedKeys.value = data.menuIds; //record.menu_keys;
updatePermissionsParams.value.id = record.id;
showModal.value = true;
}
function checkedTree(keys) {
checkedKeys.value = keys;
}
function onExpandedKeys(keys) {
expandedKeys.value = keys;
}
function packHandle() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = treeData.value.map((item: any) => item.key) as [];
}
}
function checkedAllHandle() {
if (!checkedAll.value) {
checkedKeys.value = getTreeAll(treeData.value);
checkedAll.value = true;
} else {
checkedKeys.value = [];
checkedAll.value = false;
}
}
onMounted(async () => {
const treeMenuList = await getMenuList();
expandedKeys.value = treeMenuList.list.map((item) => item.key);
treeData.value = treeMenuList.list;
});
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,22 @@
<script lang="tsx">
import { defineComponent, onBeforeMount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { NEmpty } from 'naive-ui';
export default defineComponent({
name: 'Redirect',
setup() {
const route = useRoute();
const router = useRouter();
onBeforeMount(() => {
const { params, query } = route;
const { path } = params;
router.replace({
path: '/' + (Array.isArray(path) ? path.join('/') : path),
query,
});
});
return () => <NEmpty />;
},
});
</script>

View File

@@ -0,0 +1,70 @@
<template>
<n-card :bordered="false" class="proCard">
<div class="result-box">
<n-result status="error" title="操作失败" description="请核对并修改以下信息后,再重新提交。">
<div class="result-box-extra">
<p>您提交的内容有如下错误</p>
<p class="mt-3">
<n-space align="center">
<n-icon size="20" color="#f0a020">
<InfoCircleOutlined />
</n-icon>
<span>认证照片不够清晰</span>
<n-button type="info" text>立即修改</n-button>
</n-space>
</p>
<p class="mt-3">
<n-space>
<n-icon size="20" color="#f0a020">
<InfoCircleOutlined />
</n-icon>
<span>备注包含敏感字符并且不能包含政治相关</span>
<n-button type="info" text>立即修改</n-button>
</n-space>
</p>
</div>
<template #footer>
<div class="flex justify-center mb-4">
<n-space align="center">
<n-button type="info" @click="goHome">回到首页</n-button>
<n-button>查看详情</n-button>
<n-button>打印</n-button>
</n-space>
</div>
</template>
</n-result>
</div>
</n-card>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useThemeVars } from 'naive-ui';
import { useRouter } from 'vue-router';
import { InfoCircleOutlined } from '@vicons/antd';
const router = useRouter();
const themeVars = useThemeVars();
const getTableHeaderColor = computed(() => {
return themeVars.value.tableHeaderColor;
});
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.result-box {
width: 72%;
margin: 0 auto;
text-align: center;
padding-top: 5px;
&-extra {
padding: 24px 40px;
text-align: left;
background: v-bind(getTableHeaderColor);
border-radius: 4px;
}
}
</style>

View File

@@ -0,0 +1,74 @@
<template>
<n-card :bordered="false" class="proCard">
<div class="result-box">
<n-result
status="info"
title="提示"
description="本次提交将在24小时候内自动转入对方账户如操作失误请及时撤回"
>
<div class="result-box-extra">
<p>您提交的内容如下</p>
<p class="mt-3">
<n-space align="center">
<n-icon size="20" color="#18a058">
<CheckCircleOutlined />
</n-icon>
<span>转入支付宝账户189****54261980</span>
<n-button type="info" text>立即撤回</n-button>
</n-space>
</p>
<p class="mt-3">
<n-space>
<n-icon size="20" color="#18a058">
<CheckCircleOutlined />
</n-icon>
<span>转入支付宝账户187****54262980</span>
<n-button type="info" text>立即撤回</n-button>
</n-space>
</p>
</div>
<template #footer>
<div class="flex justify-center mb-4">
<n-space align="center">
<n-button type="info" @click="goHome">回到首页</n-button>
<n-button>查看详情</n-button>
<n-button>全部撤回</n-button>
</n-space>
</div>
</template>
</n-result>
</div>
</n-card>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useThemeVars } from 'naive-ui';
import { useRouter } from 'vue-router';
import { CheckCircleOutlined } from '@vicons/antd';
const router = useRouter();
const themeVars = useThemeVars();
const getTableHeaderColor = computed(() => {
return themeVars.value.tableHeaderColor;
});
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.result-box {
width: 72%;
margin: 0 auto;
text-align: center;
padding-top: 5px;
&-extra {
padding: 24px 40px;
text-align: left;
background: v-bind(getTableHeaderColor);
border-radius: 4px;
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<n-card :bordered="false" class="proCard">
<div class="result-box">
<n-result
status="success"
title="操作成功"
description="提交结果页用于反馈一系列操作任务的处理结果,如果仅是简单操作,灰色区域可以显示一些补充的信息。"
>
<div class="result-box-extra">
<p>已提交申请等待财务部门审核</p>
</div>
<template #footer>
<div class="flex justify-center mb-4">
<n-space align="center">
<n-button type="info" @click="goHome">回到首页</n-button>
<n-button>查看详情</n-button>
<n-button>打印</n-button>
</n-space>
</div>
</template>
</n-result>
</div>
</n-card>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useThemeVars } from 'naive-ui';
const router = useRouter();
const themeVars = useThemeVars();
const getTableHeaderColor = computed(() => {
return themeVars.value.tableHeaderColor;
});
function goHome() {
router.push('/');
}
</script>
<style lang="less" scoped>
.result-box {
width: 72%;
margin: 0 auto;
text-align: center;
padding-top: 5px;
&-extra {
padding: 24px 40px;
text-align: left;
background: v-bind(getTableHeaderColor);
border-radius: 4px;
}
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="昵称" path="name">
<n-input v-model:value="formValue.name" placeholder="请输入昵称" />
</n-form-item>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入邮箱" v-model:value="formValue.email" />
</n-form-item>
<n-form-item label="联系电话" path="mobile">
<n-input placeholder="请输入联系电话" v-model:value="formValue.mobile" />
</n-form-item>
<n-form-item label="联系地址" path="address">
<n-input v-model:value="formValue.address" type="textarea" placeholder="请输入联系地址" />
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新基本信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
const rules = {
name: {
required: true,
message: '请输入昵称',
trigger: 'blur',
},
email: {
required: true,
message: '请输入邮箱',
trigger: 'blur',
},
mobile: {
required: true,
message: '请输入联系电话',
trigger: 'input',
},
};
const formRef: any = ref(null);
const message = useMessage();
const formValue = reactive({
name: '',
mobile: '',
email: '',
address: '',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
</script>

View File

@@ -0,0 +1,52 @@
<template>
<n-grid cols="1" responsive="screen" class="-mt-5">
<n-grid-item>
<n-list>
<n-list-item>
<template #suffix>
<n-button type="primary" text>修改</n-button>
</template>
<n-thing title="账户密码">
<template #description
><span class="text-gray-400">绑定手机和邮箱并设置密码帐号更安全</span></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text>修改</n-button>
</template>
<n-thing title="绑定手机">
<template #description
><span class="text-gray-400">已绑定手机号+86189****4877</span></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text>设置</n-button>
</template>
<n-thing title="密保问题">
<template #description
><span class="text-gray-400"
>未设置密保问题密保问题可有效保护账户安全</span
></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text>修改</n-button>
</template>
<n-thing title="个性域名">
<template #description
><span class="text-gray-400">已绑定域名https://hotgo.facms.cn</span></template
>
</n-thing>
</n-list-item>
</n-list>
</n-grid-item>
</n-grid>
</template>
<script lang="ts" setup></script>

View File

@@ -0,0 +1,76 @@
<template>
<div>
<n-grid :x-gap="24">
<n-grid-item span="6">
<n-card :bordered="false" size="small" class="proCard">
<n-thing
class="thing-cell"
v-for="item in typeTabList"
:key="item.key"
:class="{ 'thing-cell-on': type === item.key }"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
<template #description>{{ item.desc }}</template>
</n-thing>
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1" />
<SafetySetting v-if="type === 2" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import BasicSetting from './BasicSetting.vue';
import SafetySetting from './SafetySetting.vue';
const typeTabList = [
{
name: '基本设置',
desc: '个人账户信息设置',
key: 1,
},
{
name: '安全设置',
desc: '密码,邮箱等设置',
key: 2,
},
];
const type = ref(1);
const typeTitle = ref('基本设置');
function switchType(e) {
type.value = e.key;
typeTitle.value = e.name;
}
</script>
<style lang="less" scoped>
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
&:hover {
background: #f3f3f3;
cursor: pointer;
}
}
.thing-cell-on {
background: #f0faff;
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0;
}
&:hover {
background: #f0faff;
}
}
</style>

View File

@@ -0,0 +1,133 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="网站名称" path="name">
<n-input v-model:value="formValue.name" placeholder="请输入网站名称" />
</n-form-item>
<n-form-item label="备案编号" path="icpCode">
<n-input placeholder="请输入备案编号" v-model:value="formValue.icpCode" />
</n-form-item>
<n-form-item label="联系电话" path="mobile">
<n-input placeholder="请输入联系电话" v-model:value="formValue.mobile" />
</n-form-item>
<n-form-item label="联系地址" path="address">
<n-input v-model:value="formValue.address" type="textarea" placeholder="请输入联系地址" />
</n-form-item>
<n-form-item label="登录验证码" path="loginCode">
<n-radio-group v-model:value="formValue.loginCode" name="loginCode">
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="0">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="网站开启访问" path="systemOpen">
<n-switch
size="large"
v-model:value="formValue.systemOpen"
@update:value="systemOpenChange"
/>
</n-form-item>
<n-form-item label="网站关闭提示" path="closeText">
<n-input
v-model:value="formValue.closeText"
type="textarea"
placeholder="请输入网站关闭提示"
/>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新基本信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
const rules = {
name: {
required: true,
message: '请输入网站名称',
trigger: 'blur',
},
mobile: {
required: true,
message: '请输入联系电话',
trigger: 'input',
},
};
export default defineComponent({
setup() {
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();
const state = reactive({
formValue: {
name: '',
mobile: '',
icpCode: '',
address: '',
loginCode: 0,
closeText:
'网站维护中,暂时无法访问!本网站正在进行系统维护和技术升级,网站暂时无法访问,敬请谅解!',
systemOpen: true,
},
});
function systemOpenChange(value) {
if (!value) {
dialog.warning({
title: '提示',
content: '您确定要关闭系统访问吗?该操作立马生效,请慎重操作!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('操作成功');
},
onNegativeClick: () => {
state.formValue.systemOpen = true;
},
});
}
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
formRef.value.restoreValidation();
}
return {
formRef,
...toRefs(state),
rules,
formSubmit,
resetForm,
systemOpenChange,
};
},
});
</script>

View File

@@ -0,0 +1,79 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form :label-width="120" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="发件人邮箱" path="originator">
<n-input v-model:value="formValue.originator" placeholder="请输入发件人邮箱" />
</n-form-item>
<n-form-item label="SMTP服务器地址">
<n-input placeholder="请输入SMTP服务器地址" />
</n-form-item>
<n-form-item label="SMTP服务器端口">
<n-input placeholder="请输入SMTP服务器端口" />
</n-form-item>
<n-form-item label="SMTP用户名">
<n-input placeholder="请输入SMTP用户名" />
</n-form-item>
<n-form-item label="SMTP密码">
<n-input type="password" placeholder="请输入SMTP密码" />
</n-form-item>
<n-form-item label="邮件测试">
<n-button>邮件测试</n-button>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新邮件信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
import { useMessage } from 'naive-ui';
const rules = {
originator: {
required: true,
message: '请输入发件人邮箱',
trigger: 'blur',
},
};
export default defineComponent({
setup() {
const formRef: any = ref(null);
const message = useMessage();
const state = reactive({
formValue: {
originator: '',
},
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
return {
formRef,
...toRefs(state),
rules,
formSubmit,
};
},
});
</script>

View File

@@ -0,0 +1,218 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form :label-width="120" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="商品图片(大)">
<n-space align="center">
<span>宽度</span>
<n-input
v-model:value="formValue.bigWidth"
style="width: 80px"
placeholder="宽度像素"
/>
<span>高度</span>
<n-input
v-model:value="formValue.bigHeight"
style="width: 80px"
placeholder="高度像素"
/>
</n-space>
</n-form-item>
<n-form-item label="商品图片(小)">
<n-space align="center">
<span>宽度</span>
<n-input
v-model:value="formValue.smallWidth"
style="width: 80px"
placeholder="宽度像素"
/>
<span>高度</span>
<n-input
v-model:value="formValue.smallHeight"
style="width: 80px"
placeholder="高度像素"
/>
</n-space>
</n-form-item>
<n-form-item label="水印透明度" path="watermarkClarity">
<n-input-number
v-model:value="formValue.watermarkClarity"
:show-button="false"
placeholder="请输入水印透明度"
/>
</n-form-item>
<n-form-item label="水印图片" path="watermarkClarity">
<n-upload action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f">
<n-button>上传文件</n-button>
</n-upload>
</n-form-item>
<n-form-item label="水印位置" path="watermarkPlace">
<n-select
placeholder="请选择价格精确方式"
:options="watermarkPlaceList"
v-model:value="formValue.watermarkPlace"
/>
</n-form-item>
<n-form-item label="价格精确位数" path="pricePreciseNum">
<n-select
placeholder="请选择价格精确位数"
:options="pricePreciseNumList"
v-model:value="formValue.pricePreciseNum"
/>
</n-form-item>
<n-form-item label="价格精确方式" path="pricePrecise">
<n-select
placeholder="请选择价格精确方式"
:options="pricePreciseList"
v-model:value="formValue.pricePrecise"
/>
</n-form-item>
<n-form-item label="前台显示市场价" path="isMarketPrice">
<n-switch size="large" v-model:value="formValue.isMarketPrice" />
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新显示信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
const rules = {
name: {
required: true,
message: '请输入网站名称',
trigger: 'blur',
},
mobile: {
required: true,
message: '请输入联系电话',
trigger: 'input',
},
};
const watermarkPlaceList = [
{
label: '左上',
value: 1,
},
{
label: '右上',
value: 2,
},
{
label: '居中',
value: 3,
},
{
label: '右下',
value: 4,
},
];
const pricePreciseNumList = [
{
label: '2位',
value: 1,
},
{
label: '3位',
value: 2,
},
{
label: '4位',
value: 3,
},
];
const pricePreciseList = [
{
label: '四舍五入',
value: 1,
},
{
label: '向上取整',
value: 2,
},
{
label: '向下取整',
value: 3,
},
];
export default defineComponent({
setup() {
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();
const state = reactive({
formValue: {
bigWidth: '',
bigHeight: '',
smallWidth: '',
smallHeight: '',
watermarkClarity: null,
pricePrecise: 1,
isMarketPrice: true,
pricePreciseNum: null,
},
});
function systemOpenChange(value) {
if (!value) {
dialog.warning({
title: '提示',
content: '您确定要关闭系统访问吗?该操作立马生效,请慎重操作!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('操作成功');
},
onNegativeClick: () => {
state.formValue.systemOpen = true;
},
});
}
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
formRef.value.restoreValidation();
}
return {
formRef,
...toRefs(state),
pricePreciseList,
watermarkPlaceList,
pricePreciseNumList,
rules,
formSubmit,
resetForm,
systemOpenChange,
};
},
});
</script>

View File

@@ -0,0 +1,95 @@
<template>
<div>
<n-grid :x-gap="24">
<n-grid-item span="6">
<n-card :bordered="false" size="small" class="proCard">
<n-thing
class="thing-cell"
v-for="item in typeTabList"
:key="item.key"
:class="{ 'thing-cell-on': type === item.key }"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
<template #description>{{ item.desc }}</template>
</n-thing>
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1" />
<RevealSetting v-if="type === 2" />
<EmailSetting v-if="type === 3" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
import BasicSetting from './BasicSetting.vue';
import RevealSetting from './RevealSetting.vue';
import EmailSetting from './EmailSetting.vue';
const typeTabList = [
{
name: '基本设置',
desc: '系统常规设置',
key: 1,
},
{
name: '显示设置',
desc: '系统显示设置',
key: 2,
},
{
name: '邮件设置',
desc: '系统邮件设置',
key: 3,
},
];
export default defineComponent({
components: { BasicSetting, RevealSetting, EmailSetting },
setup() {
const state = reactive({
type: 1,
typeTitle: '基本设置',
});
function switchType(e) {
state.type = e.key;
state.typeTitle = e.name;
}
return {
...toRefs(state),
switchType,
typeTabList,
};
},
});
</script>
<style lang="less" scoped>
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
&:hover {
background: #f3f3f3;
cursor: pointer;
}
}
.thing-cell-on {
background: #f0faff;
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0;
}
&:hover {
background: #f0faff;
}
}
</style>

View File

@@ -0,0 +1,71 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="昵称" path="name">
<n-input v-model:value="formValue.name" placeholder="请输入昵称" />
</n-form-item>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入邮箱" v-model:value="formValue.email" />
</n-form-item>
<n-form-item label="联系电话" path="mobile">
<n-input placeholder="请输入联系电话" v-model:value="formValue.mobile" />
</n-form-item>
<n-form-item label="联系地址" path="address">
<n-input v-model:value="formValue.address" type="textarea" placeholder="请输入联系地址" />
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新基本信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts" setup>
import { reactive, ref } from 'vue';
import { useMessage } from 'naive-ui';
const rules = {
name: {
required: true,
message: '请输入昵称',
trigger: 'blur',
},
email: {
required: true,
message: '请输入邮箱',
trigger: 'blur',
},
mobile: {
required: true,
message: '请输入联系电话',
trigger: 'input',
},
};
const formRef: any = ref(null);
const message = useMessage();
const formValue = reactive({
name: '',
mobile: '',
email: '',
address: '',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
</script>

View File

@@ -0,0 +1,52 @@
<template>
<n-grid cols="1" responsive="screen" class="-mt-5">
<n-grid-item>
<n-list>
<n-list-item>
<template #suffix>
<n-button type="primary" text>修改</n-button>
</template>
<n-thing title="账户密码">
<template #description
><span class="text-gray-400">绑定手机和邮箱并设置密码帐号更安全</span></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text>修改</n-button>
</template>
<n-thing title="绑定手机">
<template #description
><span class="text-gray-400">已绑定手机号+86189****4877</span></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text>设置</n-button>
</template>
<n-thing title="密保问题">
<template #description
><span class="text-gray-400"
>未设置密保问题密保问题可有效保护账户安全</span
></template
>
</n-thing>
</n-list-item>
<n-list-item>
<template #suffix>
<n-button type="primary" text>修改</n-button>
</template>
<n-thing title="个性域名">
<template #description
><span class="text-gray-400">已绑定域名https://hotgo.facms.cn</span></template
>
</n-thing>
</n-list-item>
</n-list>
</n-grid-item>
</n-grid>
</template>
<script lang="ts" setup></script>

View File

@@ -0,0 +1,76 @@
<template>
<div>
<n-grid :x-gap="24">
<n-grid-item span="6">
<n-card :bordered="false" size="small" class="proCard">
<n-thing
class="thing-cell"
v-for="item in typeTabList"
:key="item.key"
:class="{ 'thing-cell-on': type === item.key }"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
<template #description>{{ item.desc }}</template>
</n-thing>
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1" />
<SafetySetting v-if="type === 2" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import BasicSetting from './BasicSetting.vue';
import SafetySetting from './SafetySetting.vue';
const typeTabList = [
{
name: '基本设置',
desc: '个人账户信息设置',
key: 1,
},
{
name: '安全设置',
desc: '密码,邮箱等设置',
key: 2,
},
];
const type = ref(1);
const typeTitle = ref('基本设置');
function switchType(e) {
type.value = e.key;
typeTitle.value = e.name;
}
</script>
<style lang="less" scoped>
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
&:hover {
background: #f3f3f3;
cursor: pointer;
}
}
.thing-cell-on {
background: #f0faff;
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0;
}
&:hover {
background: #f0faff;
}
}
</style>

View File

@@ -0,0 +1,41 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
key: 'id',
},
{
title: 'IP地址',
key: 'ip',
},
{
title: '备注',
key: 'remark',
},
{
title: '状态',
key: 'status',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.status == 1 ? '正常' : '隐藏'),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,331 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="访问黑名单">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
新建
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="新建"
style="width: 720px"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="IP地址" path="ip">
<n-input type="textarea" placeholder="请输入IP地址" v-model:value="formParams.ip" />
<template #feedback>
<p>支持添加IP如果添加多个IP请用","隔开</p>
<p>支持添加IP段,如192.168.0.0/24</p>
<p>支持添加IP范围,格式如192.168.1.xx-192.168.1.xx</p>
<br />
</template>
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, List, Status } from '@/api/sys/blacklist';
import { columns } from './columns';
import { DeleteOutlined, PlusOutlined } from '@vicons/antd';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
const params = ref({
pageSize: 10,
title: '',
content: '',
status: null,
});
const rules = {
title: {
// required: true,
trigger: ['blur', 'input'],
message: '请输入标题',
},
};
const schemas: FormSchema[] = [
{
field: 'ip',
component: 'NInput',
label: 'IP地址',
componentProps: {
placeholder: '请输入IP地址',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入IP地址', trigger: ['blur'] }],
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref({});
const formRef = ref({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const resetFormParams = {
id: 0,
ip: '',
remark: '',
sort: 0,
status: 1,
};
let formParams = ref(resetFormParams);
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
}
const loadDataTable = async (res) => {
return await List({ ...params.value, ...res, ...searchFormRef.value.formModel });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
Edit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
reloadTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,205 @@
<template>
<div>
<n-spin :show="show" description="正在获取配置...">
<n-grid cols="2 s:2 m:2 l:2 xl:2 2xl:2" responsive="screen">
<n-grid-item>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="网站名称" path="basicName">
<n-input v-model:value="formValue.basicName" placeholder="请输入网站名称" />
</n-form-item>
<n-form-item label="网站logo" path="basicLogo">
<BasicUpload
:action="`${uploadUrl}/admin/upload/image`"
:headers="uploadHeaders"
:data="{ type: 0 }"
name="file"
:width="100"
:height="100"
:maxNumber="1"
@uploadChange="uploadChange"
v-model:value="formValue.basicLogo"
:helpText="
'网站logo适用于客户端使用图片大小不超过' +
componentSetting.upload.maxSize +
'MB'
"
/>
</n-form-item>
<n-form-item label="用户是否可注册开关" path="basicRegisterSwitch">
<n-radio-group
v-model:value="formValue.basicRegisterSwitch"
name="basicRegisterSwitch"
>
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="0">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="验证码开关" path="basicCaptchaSwitch">
<n-radio-group v-model:value="formValue.basicCaptchaSwitch" name="basicCaptchaSwitch">
<n-space>
<n-radio :value="1">开启</n-radio>
<n-radio :value="0">关闭</n-radio>
</n-space>
</n-radio-group>
</n-form-item>
<n-form-item label="网站开启访问" path="basicSystemOpen">
<n-switch
size="large"
v-model:value="formValue.basicSystemOpen"
@update:value="systemOpenChange"
/>
</n-form-item>
<n-form-item label="网站关闭提示" path="basicCloseText">
<n-input
v-model:value="formValue.basicCloseText"
type="textarea"
placeholder="请输入网站关闭提示"
/>
</n-form-item>
<n-form-item label="备案编号" path="basicIcpCode">
<n-input placeholder="请输入备案编号" v-model:value="formValue.basicIcpCode" />
</n-form-item>
<n-form-item label="版权所有" path="basicCopyright">
<n-input placeholder="版权所有" v-model:value="formValue.basicCopyright" />
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref, unref, onMounted } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicUpload } from '@/components/Upload';
import { useGlobSetting } from '@/hooks/setting';
import { useUserStoreWidthOut } from '@/store/modules/user';
import componentSetting from '@/settings/componentSetting';
import { getConfig, updateConfig } from '@/api/sys/config';
const group = ref('basic');
const show = ref(false);
const useUserStore = useUserStoreWidthOut();
const globSetting = useGlobSetting();
const { uploadUrl } = globSetting;
const uploadHeaders = reactive({
Authorization: useUserStore.token,
});
const rules = {
basicName: {
required: true,
message: '请输入网站名称',
trigger: 'blur',
},
};
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();
const formValue = ref({
basicName: 'HotGo',
basicLogo: '',
basicIcpCode: '',
basicLoginCode: 0,
basicRegisterSwitch: 1,
basicCaptchaSwitch: 1,
basicCopyright: '© 2021 - 2023 HotGo All Rights Reserved.',
basicCloseText:
'网站维护中,暂时无法访问!本网站正在进行系统维护和技术升级,网站暂时无法访问,敬请谅解!',
basicSystemOpen: true,
});
function systemOpenChange(value) {
if (!value) {
dialog.warning({
title: '提示',
content: '您确定要关闭系统访问吗?该操作保存后立马生效,请慎重操作!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
// message.success('操作成功');
},
onNegativeClick: () => {
formValue.value.basicSystemOpen = true;
},
});
}
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
console.log('formValue.value:' + JSON.stringify(formValue.value));
updateConfig({ group: group.value, list: formValue.value })
.then((res) => {
console.log('res:' + JSON.stringify(res));
message.success('更新成功');
load();
})
.catch((error) => {
message.error(error.toString());
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
formRef.value.restoreValidation();
}
function uploadChange(list: string[]) {
// 单图模式,只需要第一个索引
if (list.length > 0) {
formValue.value.basicLogo = unref(list[0]);
} else {
formValue.value.basicLogo = unref('');
}
}
onMounted(() => {
load();
});
function load() {
show.value = true;
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
show.value = false;
// state.formValue.watermarkClarity = res;
formValue.value = res.list;
console.log('res:' + JSON.stringify(res));
})
.catch((error) => {
show.value = false;
message.error(error.toString());
});
});
}
</script>

View File

@@ -0,0 +1,179 @@
<template>
<div>
<n-spin :show="show" description="正在获取配置...">
<n-grid cols="2 s:2 m:2 l:2 xl:2 2xl:2" responsive="screen">
<n-grid-item>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="SMTP服务器" path="smtpHost">
<n-input v-model:value="formValue.smtpHost" placeholder="" />
<template #feedback> 错误的配置发送邮件会导致服务器超时</template>
</n-form-item>
<n-form-item label="SMTP端口" path="smtpPort">
<n-input v-model:value="formValue.smtpPort" placeholder="" />
<template #feedback> (不加密默认25,SSL默认465,TLS默认587)</template>
</n-form-item>
<n-form-item label="SMTP用户名" path="smtpUser">
<n-input v-model:value="formValue.smtpUser" placeholder="" />
<template #feedback>填写完整用户名</template>
</n-form-item>
<n-form-item label="SMTP密码" path="smtpPass">
<n-input v-model:value="formValue.smtpPass" placeholder="" />
<template #feedback>填写您的密码</template>
</n-form-item>
<n-form-item label="发件人名称" path="smtpSendName">
<n-input v-model:value="formValue.smtpSendName" placeholder="" />
</n-form-item>
<n-form-item label="管理员邮箱" path="smtpAdminMailbox">
<n-input v-model:value="formValue.smtpAdminMailbox" placeholder="" />
</n-form-item>
<n-form-item>
<n-button size="small" type="default" @click="sendTest">发送测试邮件</n-button>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-spin>
<n-modal
:block-scroll="false"
:mask-closable="false"
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="发送测试邮件"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="接收邮箱" path="to">
<n-input placeholder="多个用;隔开" v-model:value="formParams.to" :required="true" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">关闭</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">发送</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { getConfig, sendTestEmail, updateConfig } from '@/api/sys/config';
const group = ref('smtp');
const show = ref(false);
const showModal = ref(false);
const formBtnLoading = ref(false);
const formParams = ref({ to: '' });
const rules = {
smtpHost: {
required: true,
message: '请输入SMTP服务器',
trigger: 'blur',
},
};
const formRef: any = ref(null);
const message = useMessage();
const formValue = ref({
smtpHost: 'smtpdm.aliyun.com',
smtpPort: 25,
smtpUser: '',
smtpPass: '',
smtpSendName: 'HotGo',
smtpAdminMailbox: '',
});
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
showModal.value = false;
sendTestEmail(formParams.value)
.then((_res) => {
message.success('发送成功');
})
.catch((error) => {
// message.error(error.toString());
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function sendTest() {
showModal.value = true;
formBtnLoading.value = false;
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
console.log('formValue.value:' + JSON.stringify(formValue.value));
updateConfig({ group: group.value, list: formValue.value })
.then((res) => {
console.log('res:' + JSON.stringify(res));
message.success('更新成功');
load();
})
.catch((error) => {
message.error(error.toString());
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
onMounted(() => {
load();
});
function load() {
show.value = true;
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
show.value = false;
// state.formValue.watermarkClarity = res;
formValue.value = res.list;
console.log('res:' + JSON.stringify(res));
})
.catch((error) => {
show.value = false;
message.error(error.toString());
});
});
}
</script>

View File

@@ -0,0 +1,218 @@
<template>
<n-grid cols="2 s:2 m:2 l:3 xl:3 2xl:3" responsive="screen">
<n-grid-item>
<n-form :label-width="120" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="商品图片(大)">
<n-space align="center">
<span>宽度</span>
<n-input
v-model:value="formValue.bigWidth"
style="width: 80px"
placeholder="宽度像素"
/>
<span>高度</span>
<n-input
v-model:value="formValue.bigHeight"
style="width: 80px"
placeholder="高度像素"
/>
</n-space>
</n-form-item>
<n-form-item label="商品图片(小)">
<n-space align="center">
<span>宽度</span>
<n-input
v-model:value="formValue.smallWidth"
style="width: 80px"
placeholder="宽度像素"
/>
<span>高度</span>
<n-input
v-model:value="formValue.smallHeight"
style="width: 80px"
placeholder="高度像素"
/>
</n-space>
</n-form-item>
<n-form-item label="水印透明度" path="watermarkClarity">
<n-input-number
v-model:value="formValue.watermarkClarity"
:show-button="false"
placeholder="请输入水印透明度"
/>
</n-form-item>
<n-form-item label="水印图片" path="watermarkClarity">
<n-upload action="http://www.mocky.io/v2/5e4bafc63100007100d8b70f">
<n-button>上传文件</n-button>
</n-upload>
</n-form-item>
<n-form-item label="水印位置" path="watermarkPlace">
<n-select
placeholder="请选择价格精确方式"
:options="watermarkPlaceList"
v-model:value="formValue.watermarkPlace"
/>
</n-form-item>
<n-form-item label="价格精确位数" path="pricePreciseNum">
<n-select
placeholder="请选择价格精确位数"
:options="pricePreciseNumList"
v-model:value="formValue.pricePreciseNum"
/>
</n-form-item>
<n-form-item label="价格精确方式" path="pricePrecise">
<n-select
placeholder="请选择价格精确方式"
:options="pricePreciseList"
v-model:value="formValue.pricePrecise"
/>
</n-form-item>
<n-form-item label="前台显示市场价" path="isMarketPrice">
<n-switch size="large" v-model:value="formValue.isMarketPrice" />
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">更新显示信息</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</template>
<script lang="ts">
import { defineComponent, reactive, ref, toRefs } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
const rules = {
name: {
required: true,
message: '请输入网站名称',
trigger: 'blur',
},
mobile: {
required: true,
message: '请输入联系电话',
trigger: 'input',
},
};
const watermarkPlaceList = [
{
label: '左上',
value: 1,
},
{
label: '右上',
value: 2,
},
{
label: '居中',
value: 3,
},
{
label: '右下',
value: 4,
},
];
const pricePreciseNumList = [
{
label: '2位',
value: 1,
},
{
label: '3位',
value: 2,
},
{
label: '4位',
value: 3,
},
];
const pricePreciseList = [
{
label: '四舍五入',
value: 1,
},
{
label: '向上取整',
value: 2,
},
{
label: '向下取整',
value: 3,
},
];
export default defineComponent({
setup() {
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();
const state = reactive({
formValue: {
bigWidth: '',
bigHeight: '',
smallWidth: '',
smallHeight: '',
watermarkClarity: null,
pricePrecise: 1,
isMarketPrice: true,
pricePreciseNum: null,
},
});
function systemOpenChange(value) {
if (!value) {
dialog.warning({
title: '提示',
content: '您确定要关闭系统访问吗?该操作立马生效,请慎重操作!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
message.success('操作成功');
},
onNegativeClick: () => {
state.formValue.systemOpen = true;
},
});
}
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
message.success('验证成功');
} else {
message.error('验证失败,请填写完整信息');
}
});
}
function resetForm() {
formRef.value.restoreValidation();
}
return {
formRef,
...toRefs(state),
pricePreciseList,
watermarkPlaceList,
pricePreciseNumList,
rules,
formSubmit,
resetForm,
systemOpenChange,
};
},
});
</script>

View File

@@ -0,0 +1,98 @@
<template>
<div>
<n-spin :show="show" description="正在获取配置...">
<n-grid cols="2 s:2 m:2 l:2 xl:2 2xl:2" responsive="screen">
<n-grid-item>
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="默认主题" path="themeDarkTheme">
<n-input v-model:value="formValue.themeDarkTheme" placeholder="" />
<template #feedback> 可选'dark' 'light' </template>
</n-form-item>
<n-form-item label="默认系统主题" path="themeAppTheme">
<n-input v-model:value="formValue.themeAppTheme" placeholder="" />
<template #feedback> 默认#2d8cf0 </template>
</n-form-item>
<n-form-item label="默认侧边栏风格" path="themeNavTheme">
<n-input v-model:value="formValue.themeNavTheme" placeholder="" />
<template #feedback>可选'light' 'header-dark'</template>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
</n-space>
</div>
</n-form>
</n-grid-item>
</n-grid>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useMessage } from 'naive-ui';
import { getConfig, updateConfig } from '@/api/sys/config';
const group = ref('theme');
const show = ref(false);
const rules = {
themeDarkTheme: {
required: true,
message: '请输入默认主题',
trigger: 'blur',
},
};
const formRef: any = ref(null);
const message = useMessage();
const formValue = ref({
themeDarkTheme: 'dark',
themeAppTheme: '#2d8cf0',
themeNavTheme: 'dark',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
console.log('formValue.value:' + JSON.stringify(formValue.value));
updateConfig({ group: group.value, list: formValue.value })
.then((res) => {
console.log('res:' + JSON.stringify(res));
message.success('更新成功');
load();
})
.catch((error) => {
message.error(error.toString());
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
onMounted(() => {
load();
});
function load() {
show.value = true;
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
show.value = false;
// state.formValue.watermarkClarity = res;
formValue.value = res.list;
console.log('res:' + JSON.stringify(res));
})
.catch((error) => {
show.value = false;
message.error(error.toString());
});
});
}
</script>

View File

@@ -0,0 +1,102 @@
<template>
<div>
<n-grid :x-gap="24">
<n-grid-item span="6">
<n-card :bordered="false" size="small" class="proCard">
<n-thing
class="thing-cell"
v-for="item in typeTabList"
:key="item.key"
:class="{ 'thing-cell-on': type === item.key }"
@click="switchType(item)"
>
<template #header>{{ item.name }}</template>
<template #description>{{ item.desc }}</template>
</n-thing>
</n-card>
</n-grid-item>
<n-grid-item span="18">
<n-card :bordered="false" size="small" :title="typeTitle" class="proCard">
<BasicSetting v-if="type === 1" />
<ThemeSetting v-if="type === 2" />
<RevealSetting v-if="type === 3" />
<EmailSetting v-if="type === 4" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
import BasicSetting from './BasicSetting.vue';
import RevealSetting from './RevealSetting.vue';
import EmailSetting from './EmailSetting.vue';
import ThemeSetting from './ThemeSetting.vue';
const typeTabList = [
{
name: '基本设置',
desc: '系统常规设置',
key: 1,
},
{
name: '主题设置',
desc: '系统主题设置',
key: 2,
},
// {
// name: '显示设置',
// desc: '系统显示设置',
// key: 3,
// },
{
name: '邮件设置',
desc: '系统邮件设置',
key: 4,
},
];
export default defineComponent({
components: { BasicSetting, RevealSetting, EmailSetting, ThemeSetting },
setup() {
const state = reactive({
type: 1,
typeTitle: '基本设置',
});
function switchType(e) {
state.type = e.key;
state.typeTitle = e.name;
}
return {
...toRefs(state),
switchType,
typeTabList,
};
},
});
</script>
<style lang="less" scoped>
.thing-cell {
margin: 0 -16px 10px;
padding: 5px 16px;
&:hover {
background: #f3f3f3;
cursor: pointer;
}
}
.thing-cell-on {
background: #f0faff;
color: #2d8cf0;
::v-deep(.n-thing-main .n-thing-header .n-thing-header__title) {
color: #2d8cf0;
}
&:hover {
background: #f0faff;
}
}
</style>

View File

@@ -0,0 +1,81 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
const policyOptions = {
1: '并行策略',
2: '单例策略',
3: '单次策略',
4: '多次策略',
};
export const columns = [
{
title: 'ID',
key: 'id',
},
{
title: '任务分组',
key: 'groupName',
},
{
title: '任务名称',
key: 'name',
},
{
title: '执行参数',
key: 'params',
render(row) {
return row.params;
},
},
{
title: '执行策略',
key: 'policy',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'info',
bordered: false,
},
{
default: () => policyOptions[row.policy] ?? '未知',
}
);
},
},
{
title: '表达式',
key: 'pattern',
},
{
title: '执行次数',
key: 'count',
},
{
title: '状态',
key: 'status',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.status == 1 ? '运行中' : '已结束'),
}
);
},
},
{
title: '创建时间',
key: 'createdAt',
},
];

View File

@@ -0,0 +1,495 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="定时任务">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
@keyup.enter="handleSubmit"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
新建
</n-button>
&nbsp;
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
&nbsp;
<n-button type="info" @click="openGroupModal">
<template #icon>
<n-icon>
<GroupOutlined />
</n-icon>
</template>
任务分组
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="新建"
style="width: 720px"
>
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="任务分组" path="groupId">
<n-tree-select
:options="optionTreeData"
:default-value="formParams.groupId"
@update:value="handleUpdateValue"
/>
</n-form-item>
<n-form-item label="任务名称" path="name">
<n-input placeholder="请输入公告标题" v-model:value="formParams.name" />
<template #feedback> go函数名称</template>
</n-form-item>
<n-form-item label="执行参数" path="params">
<n-input
type="textarea"
placeholder="请输入执行参数如果函数需要多个参数请用,隔开"
v-model:value="formParams.params"
/>
</n-form-item>
<n-form-item label="执行策略" path="policy">
<n-radio-group v-model:value="formParams.policy" name="policy">
<n-radio-button
v-for="type in policyOptions"
:key="type.value"
:value="type.value"
:label="type.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="执行次数" path="count">
<n-input placeholder="请输入执行次数" v-model:value="formParams.count" />
<template #feedback> 仅在单次、多次策略时生效</template>
</n-form-item>
<n-form-item label="定时表达式" path="pattern">
<n-input placeholder="请输入表达式" v-model:value="formParams.pattern" />
<template #feedback>
表达式语法参考:<a
target="_blank"
href="https://goframe.org/pages/viewpage.action?pageId=30736411"
>https://goframe.org/pages/viewpage.action?pageId=30736411</a
>
</template>
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="formParams.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formParams.status" name="status">
<n-radio-button
v-for="status in statusOptions"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
<n-form-item label="备注" path="remark">
<n-input type="textarea" placeholder="请输入备注" v-model:value="formParams.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showModal = false)">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-card>
<GroupModal ref="GroupModalRef" />
</div>
</template>
<script lang="ts" setup>
import { h, onMounted, reactive, ref, onBeforeMount } from 'vue';
import { TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { Delete, Edit, getSelect, List, Status } from '@/api/sys/cron';
import { columns } from './columns';
import { DeleteOutlined, GroupOutlined, PlusOutlined } from '@vicons/antd';
import { statusActions } from '@/enums/optionsiEnum';
import GroupModal from './modal/modal.vue';
const optionTreeData = ref([]);
const defaultValueRef = () => ({
id: 0,
groupId: 0,
name: '',
params: '',
pattern: '',
policy: 1,
count: 1,
sort: 0,
remark: '',
status: 1,
});
const params = ref({
pageSize: 10,
title: '',
content: '',
status: null,
});
const rules = {
name: {
// required: true,
trigger: ['blur', 'input'],
message: '请输入任务名称',
},
};
const policyOptions = [
{
value: 1,
label: '并行策略',
},
{
value: 2,
label: '单例策略',
},
{
value: 3,
label: '单次策略',
},
{
value: 4,
label: '多次策略',
},
].map((s) => {
return s;
});
const statusOptions = [
{
value: 1,
label: '运行中',
},
{
value: 2,
label: '已结束',
},
].map((s) => {
return s;
});
const groupOptions = ref([]);
const schemas: FormSchema[] = [
{
field: 'groupId',
component: 'NSelect',
label: '任务分组',
defaultValue: null,
componentProps: {
placeholder: '请选择分组',
options: groupOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'name',
component: 'NInput',
label: '任务名称',
componentProps: {
placeholder: '请输入任务名称',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入任务名称', trigger: ['blur'] }],
},
{
field: 'policy',
component: 'NSelect',
label: '执行策略',
defaultValue: null,
componentProps: {
placeholder: '请选择策略',
options: policyOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '运行状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: statusOptions,
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const showModal = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref({});
const formRef = ref({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
let formParams = ref(defaultValueRef());
const actionColumn = reactive({
width: 320,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '在线执行',
onClick: handleExecute.bind(null, record),
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: statusActions,
select: (key) => {
updateStatus(record.id, key);
},
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = defaultValueRef();
}
const loadDataTable = async (res) => {
return await List({ ...params.value, ...res, ...searchFormRef.value.formModel });
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
if (rowKeys.length > 0) {
batchDeleteDisabled.value = false;
} else {
batchDeleteDisabled.value = true;
}
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams:' + JSON.stringify(formParams.value));
Edit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(defaultValueRef());
});
})
.catch((_e: Error) => {
// message.error(e.message ?? '操作失败');
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
}
function handleExecute(record: Recordable) {
console.log('点击了handleExecute', record);
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
Delete({ id: checkedIds.value })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
function updateStatus(id, status) {
Status({ id: id, status: status })
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
setTimeout(() => {
reloadTable({});
});
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
}
const GroupModalRef = ref();
function openGroupModal() {
const { openDrawer } = GroupModalRef.value;
openDrawer();
}
async function setDictSelect() {
optionTreeData.value = await getSelect({});
if (optionTreeData.value === undefined || optionTreeData.value === null) {
optionTreeData.value = [];
}
groupOptions.value = [];
for (let i = 0; i < optionTreeData.value?.length; i++) {
groupOptions.value.push({
value: optionTreeData.value[i].key,
label: optionTreeData.value[i].label,
});
}
}
onBeforeMount(async () => {
await setDictSelect();
});
// 处理选项更新
function handleUpdateValue(
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option);
formParams.value.groupId = value;
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,78 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 100,
},
{
title: '上级ID',
dataIndex: 'pid',
key: 'pid',
width: 100,
},
{
title: '分组名称',
dataIndex: 'name',
key: 'name',
width: 200,
},
{
title: '是否默认',
dataIndex: 'isDefault',
key: 'isDefault',
render(row) {
return row.is_default === 1 ? '是' : '否';
},
width: 100,
},
{
title: '排序',
dataIndex: 'sort',
key: 'sort',
width: 100,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status === 1 ? 'info' : 'error',
bordered: false,
},
{
default: () => (row.status === 1 ? '正常' : '禁用'),
}
);
},
width: 150,
},
{
title: '备注',
dataIndex: 'remark',
key: 'remark',
width: 150,
},
// {
// title: '更新时间',
// dataIndex: 'updatedAt',
// key: 'updatedAt',
// width: 160,
// },
{
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
},
];

Some files were not shown because too many files have changed in this diff Show More