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,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,
},
];

View File

@@ -0,0 +1,248 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<BasicTable
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="1090"
:flex-height="false"
:pagination="{ pageSize: 10 }"
:resizeHeightOffset="-50000"
>
<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="modalTitle">
<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="optionTreeData"
:default-value="formParams.pid"
@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="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, onMounted } from 'vue';
import { SelectOption, TreeSelectOption, useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { GroupDelete, GroupEdit, GroupList, GroupStatus, getSelect } from '@/api/sys/cron';
import { statusActions, statusOptions } from '@/enums/optionsiEnum';
const optionTreeData = ref([]);
const message = useMessage();
const statusValue = ref(1);
const defaultValueRef = () => ({
id: 0,
pid: 0,
name: '',
sort: 0,
remark: '',
status: statusValue.value,
});
const modalTitle = ref('新建分组');
const showModal = ref(false);
const formBtnLoading = ref(false);
const rules = {
name: {
required: true,
trigger: ['blur', 'input'],
message: '请输入名称',
},
};
function addTable() {
showModal.value = true;
modalTitle.value = '新建分组';
formParams.value = defaultValueRef();
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
console.log('formParams.value:' + JSON.stringify(formParams.value));
GroupEdit(formParams.value)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
})
.catch((e: Error) => {
message.error(e.message ?? '操作失败');
});
setTimeout(() => {
showModal.value = false;
reloadTable();
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
const dialog = useDialog();
const actionRef = ref();
const formParams = ref(defaultValueRef);
const params = ref(defaultValueRef);
const formRef = ref({});
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),
},
],
});
},
});
function handleEdit(record: Recordable) {
console.log('handleEdit', record);
showModal.value = true;
modalTitle.value = '编辑分组 ID' + record.id;
formParams.value = {
id: record.id,
pid: record.pid,
name: record.name,
sort: record.sort,
remark: record.remark,
status: record.status,
};
}
function handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
GroupDelete(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((_e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
const dataSource = ref({
successful_order: 0,
transaction_money: 0,
});
const loadDataTable = async (res) => {
dataSource.value = await GroupList({ ...res });
return dataSource.value;
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
}
async function setDictSelect() {
optionTreeData.value = await getSelect({});
if (optionTreeData.value === undefined || optionTreeData.value === null) {
optionTreeData.value = [];
}
}
onMounted(async () => {
setDictSelect();
});
// 处理选项更新
function handleUpdateValue(
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option);
formParams.value.pid = value;
}
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,29 @@
<template>
<div>
<n-modal
v-model:show="showModal"
style="width: 80%; height: 700px"
:show-icon="false"
preset="dialog"
:title="title"
>
<Index />
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import Index from './index.vue';
const showModal = ref(false);
const title = ref('管理分组');
function openDrawer() {
showModal.value = true;
}
defineExpose({ openDrawer });
</script>
<style scoped></style>

View File

@@ -0,0 +1,182 @@
<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="pid">
<n-tree-select
:options="optionTreeData"
:default-value="formParams.pid"
@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="type">
<n-input placeholder="请输入类型编码" v-model:value="formParams.type" />
</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 statusMap"
:key="status.value"
:value="status.value"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</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 { EditDict } from '@/api/dict/dict';
const statusMap = [
{
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,
type: '',
name: '',
remark: '',
status: 1,
sort: 10,
});
const state = reactive({
width: 500,
isDrawer: false,
subLoading: false,
formParams: defaultValueRef(),
placement: 'right',
icon: '',
alertText:
'该功能主要实时预览各种布局效果,更多完整配置在 projectSetting.ts 中设置,建议在生产环境关闭该布局预览功能。',
});
function openDrawer(form) {
if (document.body.clientWidth < 500) {
state.width = document.body.clientWidth;
}
state.isDrawer = true;
console.log('form:' + JSON.stringify(form));
state.formParams = Object.assign(state.formParams, form);
}
function closeDrawer() {
state.isDrawer = false;
}
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
console.log('state.formParams:' + JSON.stringify(state.formParams));
EditDict({ ...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,
statusMap,
handleUpdateValue,
QuestionCircleOutlined,
};
},
});
</script>

View File

@@ -0,0 +1,50 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
export const columns = [
{
title: 'id',
key: 'id',
},
{
title: '字典类型',
key: 'type',
},
{
title: '字典标签',
key: 'label',
},
{
title: '字典键值',
key: 'value',
},
// {
// 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',
// width: 100,
// },
];

View File

@@ -0,0 +1,247 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="字典管理">
可代替后台管理系统设置的大量枚举值和配置统一标准化管理随时修改或增加
</n-card>
</div>
<n-grid class="mt-6" cols="1 s:1 m:1 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12">
<n-gi span="1">
<n-card :segmented="{ content: true }" :bordered="false" size="small">
<template #header>
<n-space>
<n-button type="info" 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"
icon-placement="left"
@click="openEditDrawer"
:disabled="formParams.id === 0"
>
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<EditOutlined />
</n-icon>
</div>
</template>
编辑
</n-button>
<n-button type="error" icon-placement="left" @click="handleDel">
<template #icon>
<div class="flex items-center">
<n-icon size="14">
<DeleteOutlined />
</n-icon>
</div>
</template>
删除
</n-button>
<n-button type="info" 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: 95%; overflow: hidden"
@update:selected-keys="selectedTree"
@update:expanded-keys="onExpandedKeys"
/>
</template>
</div>
</div>
</n-card>
</n-gi>
<n-gi span="3">
<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>
<List :checkedId="checkedId" />
</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 { useDialog, useMessage } from 'naive-ui';
import {
AlignLeftOutlined,
FormOutlined,
PlusOutlined,
EditOutlined,
SearchOutlined,
DeleteOutlined,
} from '@vicons/antd';
import { getTreeItem } from '@/utils';
import CreateDrawer from './CreateDrawer.vue';
import List from './list.vue';
import { DeleteDict, getDictTree } from '@/api/dict/dict';
const createDrawerRef = ref();
const message = useMessage();
const dialog = useDialog();
let treeItemKey = ref([]);
let expandedKeys = ref([]);
const treeData = ref([]);
const loading = ref(true);
const isEditMenu = ref(false);
const treeItemTitle = ref('');
const checkedId = ref(0);
const pattern = ref('');
const drawerTitle = ref('');
const optionTreeData = ref([
{
id: 0,
key: 0,
label: '根目录',
pid: 0,
title: '根目录',
type: 1,
},
]);
const defaultValueRef = () => ({
id: 0,
pid: 0,
type: '',
name: '',
remark: '',
status: 1,
sort: 10,
});
const formParams = reactive(defaultValueRef());
function openCreateDrawer() {
drawerTitle.value = '添加字典类型';
const { openDrawer } = createDrawerRef.value;
openDrawer(defaultValueRef());
}
function openEditDrawer() {
drawerTitle.value = '编辑字典类型';
const { openDrawer } = createDrawerRef.value;
openDrawer(formParams);
}
function selectedTree(keys) {
if (keys.length) {
const treeItem = getTreeItem(unref(treeData), keys[0]);
// console.log('选择treeItem:' + JSON.stringify(treeItem));
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
Object.assign(formParams, treeItem);
isEditMenu.value = true;
checkedId.value = treeItem.id;
} else {
isEditMenu.value = false;
treeItemKey.value = [];
treeItemTitle.value = '';
}
}
function handleDel() {
dialog.info({
title: '提示',
content: `您确定想删除此类型吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
console.log('DeleteMenu formParams:' + JSON.stringify(formParams));
DeleteDict({ ...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 packHandle() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = unref(treeData).map((item: any) => item.key as string) as [];
}
}
onMounted(async () => {
await loadData();
});
async function loadData() {
const treeMenuList = await getDictTree();
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;
}
</script>

View File

@@ -0,0 +1,298 @@
<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"
:resizeHeightOffset="-10000"
>
<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="新建">
<n-form
:model="formParams"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="字典类型" path="typeId">
<n-tree-select
:options="typeList"
:default-value="formParams.typeId"
:default-expand-all="true"
@update:value="handleUpdateTypeIdValue"
/>
</n-form-item>
<n-form-item label="标签" path="label">
<n-input placeholder="请输入标签名称" v-model:value="formParams.label" />
</n-form-item>
<n-form-item label="字典键值" path="value">
<n-input placeholder="请输入键值" v-model:value="formParams.value" />
</n-form-item>
<n-form-item label="表格回显" path="listClass">
<n-input placeholder="请输入表格回显样式" v-model:value="formParams.listClass" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input placeholder="请输入" v-model:value="formParams.sort" />
</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>
</template>
<script lang="ts" setup>
import { h, reactive, ref, watch, onMounted } from 'vue';
import { TreeSelectOption, useMessage, useDialog } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { getDataList, getDictSelect, EditData, DeleteData } from '@/api/dict/dict';
import { columns } from './columns';
import { PlusOutlined } from '@vicons/antd';
import { statusOptions } from '@/enums/optionsiEnum';
interface Props {
checkedId?: number;
}
const props = withDefaults(defineProps<Props>(), { checkedId: 0 });
const typeList = ref([]);
const rules = {
label: {
required: true,
trigger: ['blur', 'input'],
message: '请输入标签名称',
},
value: {
required: true,
trigger: ['blur', 'input'],
message: '请输入键值',
},
};
const schemas: FormSchema[] = [
{
field: 'label',
// labelMessage: '请输入字典标签名称',
component: 'NInput',
label: '标签',
componentProps: {
placeholder: '请输入标签名称',
onInput: (e: any) => {
console.log(e);
params.value.label = e;
},
},
rules: [{ message: '请输入字典标签名称', trigger: ['blur'] }],
},
];
const formRef: any = ref(null);
const message = useMessage();
const dialog = useDialog();
const actionRef = ref();
const showModal = ref(false);
const formBtnLoading = ref(false);
const resetFormParams = {
typeId: props.checkedId,
label: '',
value: '',
listClass: '',
sort: 0,
status: 1,
remark: '',
};
const formParams = ref(resetFormParams);
const params = ref({
pageSize: 10,
typeId: props.checkedId,
label: '',
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:1 l:2 xl:2 2xl:2' },
labelWidth: 80,
schemas,
});
function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
}
const loadDataTable = async (res) => {
return await getDataList({ ...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) {
console.log('formParams:' + JSON.stringify(formParams.value));
EditData(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 handleDelete(record: Recordable) {
console.log('点击了删除', record);
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
DeleteData(record)
.then((_res) => {
console.log('_res:' + JSON.stringify(_res));
message.success('操作成功');
reloadTable();
})
.catch((e: Error) => {
// message.error(e.message ?? '操作失败');
});
},
onNegativeClick: () => {
// message.error('不确定');
},
});
}
function handleEdit(record: Recordable) {
console.log('点击了编辑', record);
showModal.value = true;
formParams.value = record;
}
function handleSubmit(values: Recordable) {
console.log(values);
reloadTable();
}
function handleReset(values: Recordable) {
console.log(values);
params.value.label = '';
reloadTable();
}
watch(props, (_newVal, _oldVal) => {
console.log('_newVal:' + JSON.stringify(_newVal));
params.value.typeId = _newVal.checkedId;
formParams.value.typeId = _newVal.checkedId;
actionRef.value.reload();
setDictSelect();
});
async function setDictSelect() {
typeList.value = await getDictSelect({});
if (typeList.value === undefined || typeList.value === null) {
typeList.value = [];
}
}
function handleUpdateTypeIdValue(
value: string | number | Array<string | number> | null,
option: TreeSelectOption | null | Array<TreeSelectOption | null>
) {
console.log(value, option);
formParams.value.typeId = value;
}
onMounted(() => {
setDictSelect();
});
</script>
<style lang="less" scoped></style>