im 管理后台

This commit is contained in:
richjohnrambo
2026-08-11 23:13:51 +08:00
parent 0397fe5f22
commit 331a55751d
1407 changed files with 162069 additions and 2 deletions
+111
View File
@@ -0,0 +1,111 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="关于">
{{ name }} 是一个基于 goframe2,vue3,vite2,TypeScript,uniapp
的中后台解决方案,它可以帮助你快速搭建企业级中后台项目,相信不管是从新技术使用还是其他方面,都能帮助到你,持续更新中。
</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="HotGo版本">
<n-tag type="info"> {{ config?.version }}</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/tree/v2.0/docs/guide-zh-CN"
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>
import { ref } from 'vue';
import { useUserStoreWidthOut } from '@/store/modules/user';
const useUserStore = useUserStoreWidthOut();
const config = ref(useUserStore.config);
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,42 @@
<template>
<div class="n-layout-page-header">
<n-card :bordered="false" title="日历"> 一个普通的日历 </n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<n-calendar
v-model:value="value"
#="{ year, month, date }"
:is-date-disabled="isDateDisabled"
@update:value="handleUpdateValue"
>
{{ year }}-{{ month }}-{{ date }}
</n-calendar>
</n-card>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { isYesterday, addDays } from 'date-fns';
export default defineComponent({
setup() {
const message = useMessage();
return {
value: ref(addDays(Date.now(), 1).valueOf()),
handleUpdateValue(
_: number,
{ year, month, date }: { year: number; month: number; date: number }
) {
message.success(`${year}-${month}-${date}`);
},
isDateDisabled(timestamp: number) {
if (isYesterday(timestamp)) {
return true;
}
return false;
},
};
},
});
</script>
@@ -0,0 +1,89 @@
<template>
<n-space vertical>
<n-card>
<template #header>基础用法</template>
<n-descriptions label-placement="top" title="描述">
<n-descriptions-item>
<template #label> 早餐 </template>
苹果
</n-descriptions-item>
<n-descriptions-item label="早午餐"> 苹果 </n-descriptions-item>
<n-descriptions-item label="午餐"> 苹果 </n-descriptions-item>
<n-descriptions-item label="晚餐" :span="2">
两个<br />
苹果
</n-descriptions-item>
<n-descriptions-item label="夜宵"> 苹果 </n-descriptions-item>
</n-descriptions>
</n-card>
<n-card>
<template #header>每个描述项都可以设定跨度</template>
<n-descriptions label-placement="top" bordered :column="6">
<n-descriptions-item>
<template #label> 早餐 </template>
苹果
</n-descriptions-item>
<n-descriptions-item label="早午餐"> 苹果 </n-descriptions-item>
<n-descriptions-item label="午餐"> 苹果 </n-descriptions-item>
<n-descriptions-item label="晚餐"> 苹果 </n-descriptions-item>
<n-descriptions-item label="正餐"> 苹果 </n-descriptions-item>
<n-descriptions-item label="夜宵"> 苹果 </n-descriptions-item>
<n-descriptions-item label="苹果"> 苹果 </n-descriptions-item>
<n-descriptions-item label="苹果" :span="2"> 苹果 </n-descriptions-item>
<n-descriptions-item label="苹果" :span="3"> 苹果 </n-descriptions-item>
</n-descriptions>
</n-card>
<n-card>
<template #header>边框</template>
<template #header-extra> 如果有很多多行的信息,你可以把它设为 bordered </template>
<n-descriptions bordered>
<n-descriptions-item>
<template #label> 早餐 </template>
苹果
</n-descriptions-item>
<n-descriptions-item label="午餐"> 苹果 </n-descriptions-item>
<n-descriptions-item label="晚餐"> 苹果 </n-descriptions-item>
<n-descriptions-item label="为什么长">
为什么<br />长<br />长<br />长<br />长<br />长
</n-descriptions-item>
<n-descriptions-item label="为什么长">
为什么<br />长<br />长<br />长<br />长<br />长
</n-descriptions-item>
<n-descriptions-item label="为什么长">
为什么<br />长<br />长<br />长<br />长<br />长
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card>
<n-list hoverable clickable>
<n-list-item>
<n-thing title="相见恨晚" content-style="margin-top: 10px;">
<template #description>
<n-space size="small" style="margin-top: 4px">
<n-tag :bordered="false" type="info" size="small"> 暑夜 </n-tag>
<n-tag :bordered="false" type="info" size="small"> 晚春 </n-tag>
</n-space>
</template>
奋勇呀然后休息呀<br />
完成你伟大的人生
</n-thing>
</n-list-item>
<n-list-item>
<n-thing title="他在时间门外" content-style="margin-top: 10px;">
<template #description>
<n-space size="small" style="margin-top: 4px">
<n-tag :bordered="false" type="info" size="small"> 环形公路 </n-tag>
<n-tag :bordered="false" type="info" size="small"> 潜水艇司机 </n-tag>
</n-space>
</template>
最新的打印机<br />
复制着彩色傀儡<br />
早上好<br />
让他带你去被工厂敲击
</n-thing>
</n-list-item>
</n-list>
</n-card>
</n-space>
</template>
<script setup></script>
@@ -0,0 +1,58 @@
<template>
<div class="card content-box">
<n-card :bordered="false" title="复制指令">
<n-space>
<n-input placeholder="输入内容试试" v-model:value="data" style="width: 350px" />
<n-button v-copy="data" type="primary" @click="a">复 制</n-button>
</n-space>
</n-card>
<n-card :bordered="false" title="防抖指令" class="mt-3">
<n-button type="primary" v-debounce="b">防抖测试</n-button>
</n-card>
<n-card :bordered="false" title="节流指令" class="mt-3">
<n-button type="primary" v-throttle="c">节流测试</n-button>
</n-card>
<n-card :bordered="false" title="拖拽指令" class="mt-3"> 鼠标放到矩形上面拖拽试试 </n-card>
<div class="box" v-draggable> </div>
</div>
</template>
<script setup lang="ts" name="copyDirect">
import { ref } from 'vue';
import { useMessage } from 'naive-ui';
const data = ref<string>();
const message = useMessage();
const a = () => {
message.success('复制成功:' + data.value);
};
const b = () => {
message.success('防抖');
console.log(data.value);
};
const c = () => {
message.success('节流');
console.log(data.value);
};
</script>
<style scoped lang="less">
body {
width: 100px;
height: 100px;
background-color: #ccc;
position: relative;
}
.content-box {
height: 100vh;
.box {
width: 100px;
height: 100px;
background-color: #2d8cf0;
position: absolute;
z-index: 10000000;
border-radius: 10px;
margin: 20px 5px;
}
}
</style>
@@ -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>
@@ -0,0 +1,38 @@
<template>
<n-card :segmented="{ content: true, footer: true }" footer-style="padding:10px">
<template #header>
通过设备浏览器信息获取浏览器指纹的插件(官方宣称其识别精度达到99.5%)
</template>
<div>
指纹ID:
<n-text type="info">
{{ compData.murmur }}
</n-text>
</div>
</n-card>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import Fingerprint2 from 'fingerprintjs2';
const compData = reactive({
values: {},
murmur: '',
});
const createFingerprint = () => {
Fingerprint2.get((components) => {
compData.values = components.map((component) => component.value); // 配置的值的数组
compData.murmur = Fingerprint2.x64hash128(compData.values.join(''), 31).toUpperCase(); // 生成浏览器指纹
});
};
if (window.requestIdleCallback) {
requestIdleCallback(() => {
createFingerprint();
});
} else {
setTimeout(() => {
createFingerprint();
}, 600);
}
</script>
@@ -0,0 +1,175 @@
<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, FormSchema } from '@/components/Form/index';
import { useMessage } from 'naive-ui';
const schemas: FormSchema[] = [
{
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) {
if (!values) {
return message.error('请填写完整信息');
}
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>
@@ -0,0 +1,198 @@
<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) {
if (!values) {
return message.error('请填写完整信息');
}
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>
@@ -0,0 +1,23 @@
<template>
<div>
<n-spin :show="loading" description="请稍候..">
<Icons ref="iconsRef" />
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import Icons from './icons.vue';
const iconsRef = ref();
const loading = ref(true);
onMounted(() => {
setTimeout(() => {
iconsRef.value?.open('antd');
loading.value = false;
}, 10);
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,136 @@
<template>
<div>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
content-style=""
>
<template #header>
<n-grid y-gap="20" x-gap="10" cols="24" item-responsive responsive="screen">
<n-grid-item span="24 m:12 l:6">
<n-input-group>
<n-button>图标大小</n-button>
<n-input-number style="width: 100%" v-model:value="compData.size" />
</n-input-group>
</n-grid-item>
<n-grid-item span="24 m:12 l:6">
<n-color-picker
:modes="['hex']"
style="width: 100%"
v-model:value="compData.color"
:swatches="['#FFFFFF', '#18A058', '#2080F0', '#F0A020', '#D03050','#000000']"
>
<template #label>
<div style="color: white">图标颜色 {{ compData.color }}</div>
</template>
</n-color-picker>
</n-grid-item>
</n-grid>
</template>
<div class="icons">
<n-grid y-gap="20" x-gap="0" cols="24" item-responsive responsive="screen">
<n-grid-item v-for="(item, idx) in icons" :key="idx" span="12 m:4 l:3 xl:2">
<div class="icons-item">
<div class="icons-item_content">
<n-icon class="icon" :color="compData.color" :size="compData.size">
<component :is="item" />
</n-icon>
<span class="copy" v-copy="item.name" @click="handleCopy(item)">复制</span>
</div>
</div>
</n-grid-item>
</n-grid>
</div>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import * as AntdIcons from '@vicons/antd';
import * as Ionicons5 from '@vicons/ionicons5';
import { useMessage } from 'naive-ui';
import { useDesignSettingStore } from '@/store/modules/designSetting';
const designStore = useDesignSettingStore();
const message = useMessage();
const icons = ref<any>([]);
const compData = ref({
color: designStore.appTheme,
size: 32,
});
const color = computed(() => {
return compData.value.color;
});
function handleCopy(icon) {
message.success(`已复制,${icon.name}`);
}
function open(type: string) {
if (type === 'antd') {
icons.value = AntdIcons;
}
if (type === 'ionicons5') {
icons.value = Ionicons5;
}
}
defineExpose({
open,
});
</script>
<style lang="less" scoped>
:deep(.n-color-picker-trigger .n-color-picker-trigger__fill) {
top: 0;
bottom: 0;
left: 0;
right: 0;
overflow: hidden;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.icons {
&-item {
width: 100%;
text-align: center;
padding: 0 10px;
position: relative;
overflow: hidden;
cursor: pointer;
&_content {
padding: 20px 10px;
border: 1px solid rgb(240 240 245);
box-sizing: border-box;
}
.icon {
transition: top 0.3s;
position: relative;
top: 0;
}
&:hover {
.icon {
top: -10px;
}
.copy {
bottom: 0;
}
}
.copy {
position: absolute;
bottom: -30px;
height: 30px;
left: 10px;
right: 10px;
line-height: 30px;
background-color: v-bind(color);
transition: bottom 0.3s;
color: rgb(240 240 245);
}
}
}
</style>
@@ -0,0 +1,23 @@
<template>
<div>
<n-spin :show="loading" description="请稍候..">
<Icons ref="iconsRef" />
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import Icons from './icons.vue';
const iconsRef = ref();
const loading = ref(true);
onMounted(() => {
setTimeout(() => {
iconsRef.value?.open('ionicons5');
loading.value = false;
}, 10);
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,29 @@
<template>
<div>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="图标选择器">
图标选择器,用于表单选择图标,支持直接选定图标和输入图标名称来选择
</n-card>
</div>
<n-space>
<n-card :bordered="false" class="mt-4 proCard" title="Antd">
<IconSelector style="width: 100%" v-model:value="antdValue" option="antd" />
</n-card>
<n-card :bordered="false" class="mt-4 proCard" title="Ionicons5">
<IconSelector style="width: 100%" v-model:value="ionicons5Value" option="ionicons5" />
</n-card>
</n-space>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import IconSelector from '@/components/IconSelector/index.vue';
const antdValue = ref('');
const ionicons5Value = ref('');
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,68 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="导入excel">
将excel表格数据导入,可解析出完整的表格内容,包括所有的sheet和行列数据
</n-card>
</div>
<n-card :bordered="false" class="mt-4 proCard">
<n-upload
directory-dnd
:custom-request="handleUpload"
name="file"
type="file"
accept=".xlsx, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<DownloadOutlined />
</n-icon>
</div>
<n-text style="font-size: 16px"> 点击或者拖动.xlsx文件到该区域来上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0"> 单次上传数据最大不建议超过5000条</n-p>
</n-upload-dragger>
</n-upload>
</n-card>
<n-card
:bordered="false"
class="proCard mt-4"
size="small"
:segmented="{ content: true }"
title="表格数据"
>
<n-scrollbar style="max-height: 520px">
<JsonViewer :value="response" :expand-depth="5" copyable boxed sort class="json-width" />
</n-scrollbar>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { UploadCustomRequestOptions, useMessage } from 'naive-ui';
import { DownloadOutlined } from '@vicons/antd';
import { ImportExcel } from '@/api/addons/hgexample/comp';
import type { UploadFileParams } from '@/utils/http/axios/types';
import { JsonViewer } from 'vue3-json-viewer';
import 'vue3-json-viewer/dist/vue3-json-viewer.css';
const message = useMessage();
const response = ref<any>({});
function handleUpload(options: UploadCustomRequestOptions) {
message.loading('正在导入,请稍候...', { duration: 1200 });
const params: UploadFileParams = {
file: options.file.file as File,
};
ImportExcel(params).then((res) => {
response.value = res;
message.destroyAll();
message.success('解析成功');
});
}
</script>
<style lang="less"></style>
@@ -0,0 +1,369 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<n-p class="title">组件示例</n-p>
<n-space vertical>
<n-layout has-sider>
<n-scrollbar :style="scrollbarStyle">
<n-layout-sider
bordered
collapse-mode="width"
:collapsed-width="64"
:width="180"
:collapsed="collapsed"
show-trigger
@collapse="collapsed = true"
@expand="collapsed = false"
>
<n-menu
v-model:value="activeKey"
:collapsed="collapsed"
:collapsed-width="64"
:collapsed-icon-size="22"
:options="menuOptions"
/>
</n-layout-sider>
</n-scrollbar>
<n-layout>
<n-card :bordered="false" class="proCard">
<component :is="currentComponent" />
</n-card>
</n-layout>
</n-layout>
</n-space>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import type { MenuOption } from 'naive-ui';
import {
ProfileOutlined,
BorderOutlined,
NumberOutlined,
BorderOuterOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
FundProjectionScreenOutlined,
ReadOutlined,
StarOutlined,
PushpinOutlined,
LayoutOutlined,
PrinterOutlined,
DownloadOutlined,
} from '@vicons/antd';
import {
HourglassOutline,
NotificationsOutline,
LocationOutline,
SwapVertical,
EllipsisHorizontalCircleOutline,
AlbumsOutline,
LogoChrome,
} from '@vicons/ionicons5';
import { renderIcon } from '@/utils';
import FormBasic from './form/basic.vue';
import FormUseForm from './form/useForm.vue';
import Modal from './modal/index.vue';
import Drag from './drag/index.vue';
import Directive from './directive/index.vue';
import ResultSuccess from './result/success.vue';
import ResultFail from './result/fail.vue';
import ResultInfo from './result/info.vue';
import Exception403 from '../../../exception/403.vue';
import Exception404 from '../../../exception/404.vue';
import Exception500 from '../../../exception/500.vue';
import VisPPchart from './vis/ppchart.vue';
import VisEcharts from './vis/echarts.vue';
import VisMadeAPie from './vis/madeapie.vue';
import TextPinyin from './text/pinyin/index.vue';
import TextMint from './text/mint/index.vue';
import TextGradient from './text/gradient/index.vue';
import TextHigh from './text/high/index.vue';
import IconsAntd from './icons/antd.vue';
import IconsIonicons5 from './icons/ionicons5.vue';
import IconsSelector from './icons/selector.vue';
import Watermark from './watermark/index.vue';
import Des from './des/index.vue';
import Calendar from './calendar/index.vue';
import Timeline from './timeline/index.vue';
import Notice from './notice/index.vue';
import MapGaode from './map/gaode.vue';
import MapBaidu from './map/baidu.vue';
import Print from './print/index.vue';
import TagsView from './tagsView/index.vue';
import MoreComponents from './moreComponents/index.vue';
import Waterfall from './waterfall/index.vue';
import ImportExcel from './import/excel.vue';
import FingerPrintJs from './fingerprintjs/index.vue';
const components = {
formBasic: FormBasic,
formUseForm: FormUseForm,
modal: Modal,
drag: Drag,
directive: Directive,
resultSuccess: ResultSuccess,
resultFail: ResultFail,
resultInfo: ResultInfo,
exception403: Exception403,
exception404: Exception404,
exception500: Exception500,
visPPchart: VisPPchart,
visEcharts: VisEcharts,
visMadeAPie: VisMadeAPie,
textPinyin: TextPinyin,
textMint: TextMint,
textGradient: TextGradient,
textHigh: TextHigh,
iconsAntd: IconsAntd,
iconsIonicons5: IconsIonicons5,
iconsSelector: IconsSelector,
watermark: Watermark,
des: Des,
calendar: Calendar,
timeline: Timeline,
notice: Notice,
mapGaode: MapGaode,
mapBaidu: MapBaidu,
print: Print,
tagsView: TagsView,
moreComponents: MoreComponents,
waterfall: Waterfall,
importExcel: ImportExcel,
fingerPrintJs: FingerPrintJs,
};
const activeKey = ref<string>('formBasic');
const collapsed = ref(false);
const scrollbarStyle = computed(() => {
const height = '82vh';
const width = collapsed.value ? '74px' : '188px';
return {
height: height,
'max-height': height,
'min-width': width,
width: width,
};
});
const currentComponent = computed(() => {
return components[activeKey.value] || null;
});
const menuOptions: MenuOption[] = [
{
label: '表单',
key: 'form',
icon: renderIcon(ProfileOutlined),
children: [
{
label: '基础使用',
key: 'formBasic',
},
{
label: 'useForm',
key: 'formUseForm',
},
],
},
{
label: '图标',
key: 'icons',
icon: renderIcon(StarOutlined),
children: [
{
label: 'Antd',
key: 'iconsAntd',
},
{
label: 'IonIcons5',
key: 'iconsIonicons5',
},
{
label: '选择器',
key: 'iconsSelector',
},
],
},
{
label: '文字处理',
key: 'text',
icon: renderIcon(ReadOutlined),
children: [
{
label: '汉字拼音',
key: 'textPinyin',
},
{
label: '敏感词汇',
key: 'textMint',
},
{
label: '渐变文字',
key: 'textGradient',
},
{
label: '文字高亮',
key: 'textHigh',
},
],
},
{
label: '可视化',
key: 'vis',
icon: renderIcon(FundProjectionScreenOutlined),
children: [
{
label: 'PPChart',
key: 'visPPchart',
},
{
label: 'Echarts',
key: 'visEcharts',
},
{
label: 'MadeAPie',
key: 'visMadeAPie',
},
],
},
{
label: '异常页面',
key: 'exception',
icon: renderIcon(ExclamationCircleOutlined),
children: [
{
label: '403',
key: 'exception403',
},
{
label: '404',
key: 'exception404',
},
{
label: '500',
key: 'exception500',
},
],
},
{
label: '结果页面',
key: 'result',
icon: renderIcon(CheckCircleOutlined),
children: [
{
label: '成功页',
key: 'resultSuccess',
},
{
label: '失败页',
key: 'resultFail',
},
{
label: '信息页',
key: 'resultInfo',
},
],
},
{
label: '地图',
key: 'map',
icon: renderIcon(LocationOutline),
children: [
{
label: '高德地图',
key: 'mapGaode',
},
{
label: '百度地图',
key: 'mapBaidu',
},
],
},
{
label: '弹窗扩展',
key: 'modal',
icon: renderIcon(BorderOutlined),
},
{
label: '消息提示',
key: 'notice',
icon: renderIcon(NotificationsOutline),
},
{
label: '瀑布流',
key: 'waterfall',
icon: renderIcon(AlbumsOutline),
},
{
label: '拖拽',
key: 'drag',
icon: renderIcon(NumberOutlined),
},
{
label: '水印',
key: 'watermark',
icon: renderIcon(PushpinOutlined),
},
{
label: '日历',
key: 'calendar',
icon: renderIcon(ReadOutlined),
},
{
label: '打印',
key: 'print',
icon: renderIcon(PrinterOutlined),
},
{
label: '时间线',
key: 'timeline',
icon: renderIcon(HourglassOutline),
},
{
label: 'tagsView',
key: 'tagsView',
icon: renderIcon(SwapVertical),
},
{
label: '导入Excel',
key: 'importExcel',
icon: renderIcon(DownloadOutlined),
},
{
label: '卡片描述',
key: 'des',
icon: renderIcon(LayoutOutlined),
},
{
label: '指令示例',
key: 'directive',
icon: renderIcon(BorderOuterOutlined),
},
{
label: '浏览器指纹',
key: 'fingerPrintJs',
icon: renderIcon(LogoChrome),
},
{
label: '更多组件',
key: 'moreComponents',
icon: renderIcon(EllipsisHorizontalCircleOutline),
},
];
</script>
<style lang="less" scoped>
.title {
font-size: 18px;
transition: color 0.3s var(--n-bezier);
flex: 1;
min-width: 0;
color: var(--n-title-text-color);
}
</style>
@@ -0,0 +1,33 @@
<template>
<div class="container" ref="container"></div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import useCreateScript from '@/hooks/useCreateScript';
const SCRIPT_URL =
'http://api.map.baidu.com/getscript?v=3.0&ak=WxbQmaOc3bvSGSaKWcyeFSf8fnYCWpKd&services=&t=' +
new Date().getTime();
const container = ref<HTMLDivElement | null>(null);
const { createScriptPromise } = useCreateScript(SCRIPT_URL);
const initMap = () => {
createScriptPromise.then(() => {
const bMap = (window as any).BMap;
const map: any = new bMap.Map(container.value);
const point = new bMap.Point(116.404, 39.915);
map.centerAndZoom(point, 7);
map.enableScrollWheelZoom();
map.setMapStyleV2({ styleId: 'ea4652613f3629247d47666706ce7e89' });
});
};
onMounted(initMap);
</script>
<style lang="less">
.container {
width: 100%;
height: 720px;
margin-top: -32px;
}
</style>
@@ -0,0 +1,34 @@
<template>
<div class="container" ref="container"></div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import useCreateScript from '@/hooks/useCreateScript';
const SCRIPT_URL = 'https://webapi.amap.com/maps?v=1.4.15&key=9f2d3fcc4b12a7c915fded4b55902e21';
const container = ref<HTMLDivElement | null>(null);
const height = ref(0);
const { createScriptPromise } = useCreateScript(SCRIPT_URL);
const initMap = () => {
height.value = container.value?.parentElement?.getBoundingClientRect().height || 0;
createScriptPromise.then(() => {
const aMap: any = (window as any).AMap;
new aMap.Map(container.value, {
zoom: 22,
center: [116.397428, 39.90923],
viewMode: '3D',
pitch: 75,
});
});
};
onMounted(initMap);
</script>
<style lang="less">
.container {
width: 100%;
height: 720px;
margin-top: -32px;
}
</style>
@@ -0,0 +1,307 @@
<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="mt-4 proCard">
<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();
console.log('formRes', formRes);
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>
@@ -0,0 +1,15 @@
<template>
<Iframe ref="iframeRef" style="zoom: 0.8" />
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import Iframe from '../../../../iframe/index.vue';
const iframeRef = ref();
onMounted(() => {
iframeRef.value?.open('https://www.naiveui.com/zh-CN/os-theme/components/button');
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,322 @@
<template>
<div>
<n-grid :cols="2" :x-gap="10" :y-gap="10">
<n-gi>
<n-card
title="警告信息alert"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '10px' }"
>
<n-space vertical :size="12">
<n-alert title="Info 类型" type="info"> 万事开头难,只能一直努力!!! </n-alert>
<n-alert title="Success 类型" type="success">
成功就是眼前,过了黑夜就是白天,加油~~
</n-alert>
<n-alert title="Warning 类型" type="warning">
想想昨天,看看今天,望望明天;一切都会好起来
</n-alert>
<n-alert title="Error 类型" type="error"> 有人欺负人,请不犹豫,请还回去 </n-alert>
<n-alert title="可以关掉" type="info" closable> Gee it's good to be back home </n-alert>
<n-alert title="没有边框" type="info" :bordered="false">
Gee it's good to be back home
</n-alert>
<n-alert title="自定义图标">
<template #icon>
<n-icon>
<BalloonOutline />
</n-icon>
</template>
Well the Ukraine girls really knock me out<br />
They leave the West behind<br />
And Moscow girls make me sing and shout<br />
That Georgia's always on my mind<br />
Aw come on!
</n-alert>
<n-alert :show-icon="false" title="没有图标">
Yeah I'm back in the U.S.S.R.<br />
You don't know how lucky you are boys
</n-alert>
</n-space>
</n-card>
</n-gi>
<n-gi>
<n-card
title="对话框Dialog"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '10px' }"
>
<div class="padding">
<n-space>
<n-button size="small" type="primary" @click="openConfirm('warning')">警告</n-button>
<n-button size="small" type="warning" @click="openConfirm('success')">成功</n-button>
<n-button size="small" type="info" @click="openConfirm('error')">错误</n-button>
</n-space>
</div>
</n-card>
<n-card
class="mt-2"
title="信息Message"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '10px' }"
>
<n-space>
<n-button type="info" @click="openMessage('info')"> 信息 </n-button>
<n-button type="error" @click="openMessage('error')"> 错误 </n-button>
<n-button type="warning" @click="openMessage('warning')"> 警告 </n-button>
<n-button type="success" @click="openMessage('success')"> 成功 </n-button>
<n-button type="primary" @click="openMessage('loading')"> 加载中 </n-button>
</n-space>
</n-card>
<n-card
class="mt-2"
title="通知Notification"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '10px' }"
>
<n-space>
<n-button type="info" @click="openNotification('info')"> 信息 </n-button>
<n-button type="error" @click="openNotification('error')"> 错误 </n-button>
<n-button type="warning" @click="openNotification('warning')"> 警告 </n-button>
<n-button type="success" @click="openNotification('success')"> 成功 </n-button>
</n-space>
</n-card>
<n-card
class="mt-2"
title="弹出确认Popconfirm"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '10px' }"
>
<n-space>
<n-popconfirm
@positive-click="handlePositiveClick"
@negative-click="handleNegativeClick"
>
<template #trigger>
<n-button>引用</n-button>
</template>
一切都将一去杳然,任何人都无法将其捕获。
</n-popconfirm>
<n-popconfirm :negative-text="null" @positive-click="handlePositiveClick">
<template #trigger>
<n-button>只有确定</n-button>
</template>
一切都将一去杳然,任何人都无法将其捕获。
</n-popconfirm>
<n-popconfirm :positive-text="null" @negative-click="handleNegativeClick">
<template #trigger>
<n-button>只有取消</n-button>
</template>
一切都将一去杳然,任何人都无法将其捕获。
</n-popconfirm>
<n-popconfirm :positive-text="null" :negative-text="null">
<template #trigger>
<n-button>什么也没有</n-button>
</template>
一切都将一去杳然,任何人都无法将其捕获。
</n-popconfirm>
<n-popconfirm>
<template #trigger>
<n-button>自定义 action</n-button>
</template>
<template #action> 自定义 action </template>
一切都将一去杳然,任何人都无法将其捕获。
</n-popconfirm>
</n-space>
</n-card>
<n-card
class="mt-2"
title="弹出信息Popover"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '10px' }"
>
<n-space>
<n-popover trigger="hover">
<template #trigger>
<n-button>悬浮</n-button>
</template>
<span>I wish they all could be California girls</span>
</n-popover>
<n-popover trigger="hover" :keep-alive-on-hover="false">
<template #trigger>
<n-button>悬浮(忽略主体)</n-button>
</template>
<span>I wish they all could be California girls</span>
</n-popover>
<n-popover trigger="click">
<template #trigger>
<n-button>点击</n-button>
</template>
<span>I wish they all could be California girls</span>
</n-popover>
<n-popover trigger="focus">
<template #trigger>
<n-button>聚焦</n-button>
</template>
<span>I wish they all could be California girls</span>
</n-popover>
<n-popover trigger="manual" :show="showPopover">
<template #trigger>
<n-button @click="showPopover = !showPopover"> 手动 </n-button>
</template>
<span>I wish they all could be California girls</span>
</n-popover>
<n-popover trigger="hover" :delay="500" :duration="500">
<template #trigger>
<n-button>延迟 500ms, 持续 500ms</n-button>
</template>
<span>
Lately did you ever feel the pain In the morning rain as it soaks it to the bone
</span>
</n-popover>
</n-space>
</n-card>
<n-card
class="mt-2"
title="弹出提示Tooltip"
:content-style="{ padding: '10px' }"
:header-style="{ padding: '10px' }"
>
<n-space>
<n-tooltip trigger="hover">
<template #trigger>
<n-button> 鸭子 </n-button>
</template>
如果它长得像鸭子,走起来像鸭子,叫起来也像鸭子,那它一定是个鸭子。
</n-tooltip>
<n-tooltip placement="bottom" trigger="hover">
<template #trigger>
<n-button> 悬浮 </n-button>
</template>
<span> I wish they all could be California girls </span>
</n-tooltip>
<n-tooltip placement="bottom" trigger="click">
<template #trigger>
<n-button> 点击 </n-button>
</template>
<span> I wish they all could be California girls </span>
</n-tooltip>
<n-tooltip :show="showPopover" placement="bottom">
<template #trigger>
<n-button @click="showPopover = !showPopover"> 手动 </n-button>
</template>
<span> I wish they all could be California girls </span>
</n-tooltip>
<n-tooltip :show-arrow="false" trigger="hover">
<template #trigger>
<n-button>默认有箭头</n-button>
</template>
和 Popover 一样
</n-tooltip>
</n-space>
</n-card>
</n-gi>
</n-grid>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useDialog, useMessage, useNotification } from 'naive-ui';
import { BalloonOutline } from '@vicons/ionicons5';
const dialog = useDialog();
const message = useMessage();
const notification = useNotification();
const showPopover = ref(false);
function openConfirm(type: string) {
switch (type) {
case 'warning':
dialog.warning({
title: '警告',
content: '你确定要离开我?',
positiveText: '确定',
negativeText: '不确定',
onPositiveClick: () => {
message.success('确定');
},
onNegativeClick: () => {
message.error('不确定');
},
});
break;
case 'success':
dialog.success({
title: '成功',
content: '小哥哥你太棒了~~',
positiveText: '哇哦~',
});
break;
case 'error':
dialog.error({
title: '错误',
content: '知道错了吗,下次改了',
positiveText: '快哭了',
onPositiveClick: () => {
message.success('我知道了');
},
});
break;
}
}
function openMessage(type: string) {
switch (type) {
case 'info':
message.info('在心碎中认清遗憾 生命漫长也短暂跳动心脏长出藤蔓');
break;
case 'error':
message.error('愿为险而战 跌入灰暗坠入深渊');
break;
case 'warning':
message.warning('沾满泥土的脸 没有神的光环');
break;
case 'success':
message.success('握紧手中的平凡 此心自称无憾生命的火已点燃');
break;
case 'loading':
message.loading('有一天也许会走远 也许还能再相见');
break;
}
}
function openNotification(type: string) {
switch (type) {
case 'info':
notification.info({
content: '在心碎中认清遗憾 生命漫长也短暂跳动心脏长出藤蔓',
meta: '只要平凡',
});
break;
case 'error':
notification.error({
content: '愿为险而战 跌入灰暗坠入深渊',
meta: '只要平凡',
});
break;
case 'warning':
notification.warning({
content: '沾满泥土的脸 没有神的光环',
meta: '只要平凡',
});
break;
case 'success':
notification.success({
content: '握紧手中的平凡 此心自称无憾生命的火已点燃',
meta: '只要平凡',
});
break;
}
}
function handlePositiveClick() {
message.info('是的');
}
function handleNegativeClick() {
message.info('并不');
}
</script>
@@ -0,0 +1,109 @@
<template>
<div class="main-container">
<n-card title="打印图片" :header-style="{ padding: '5px' }" :content-style="{ padding: '0px' }">
<template #header-extra>
<n-button type="primary" size="small" @click="printImage">打印</n-button>
</template>
<div class="image-wrapper">
<img :src="imagePath" />
</div>
</n-card>
<n-card
title="打印HTML"
:header-style="{ padding: '5px' }"
:content-style="{ padding: '0px' }"
class="mt-4"
>
<template #header-extra>
<n-button type="primary" size="small" @click="printHtml">打印</n-button>
</template>
<div id="htmlWrapper" class="flex justify-center html-wrapper align-center flex-direction">
<n-table :data="dataList">
<thead>
<tr>
<th>姓名</th>
<th>年龄</th>
<th>性别</th>
<th>职业</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) of dataList" :key="index">
<td>{{ item.name }}</td>
<td>{{ item.age }}</td>
<td>{{ item.gender }}</td>
<td>{{ item.career }}</td>
</tr>
</tbody>
</n-table>
</div>
</n-card>
</div>
</template>
<script lang="ts">
import printJS from 'print-js';
import imagePath from '@/assets/images/logo.png';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Print',
setup() {
function printImage() {
printJS({
printable: imagePath,
type: 'image',
showModal: false,
});
}
function printHtml() {
printJS({
printable: 'htmlWrapper',
type: 'html',
targetStyles: ['*'],
});
}
return {
printImage,
printHtml,
imagePath,
dataList: [
{
name: '张三',
age: 30,
gender: '男',
career: '工程师',
},
{
name: '李四',
age: 20,
gender: '男',
career: '服务员',
},
{
name: '王五',
age: 40,
gender: '女',
career: '售货员',
},
],
};
},
});
</script>
<style lang="less" scoped>
.image-wrapper {
width: 30%;
margin: 0 auto;
& > img {
width: 100%;
}
}
.html-wrapper {
width: 80%;
margin: 0 auto;
& > h1 {
color: red;
}
}
</style>
@@ -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>
@@ -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****5426):¥1980元</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****5426):¥2980元</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>
@@ -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>
@@ -0,0 +1,26 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="tagsView"> 标签页操作项 </n-card>
</div>
<n-card>
<n-space>
<n-button type="info" @click="handleSignal('1')"> 刷新页面路由 </n-button>
<n-button type="error" @click="handleSignal('2')"> 关闭当前页面 </n-button>
<n-button type="warning" @click="handleSignal('3')"> 关闭其他页面 </n-button>
<n-button type="success" @click="handleSignal('4')"> 关闭全部页面 </n-button>
</n-space>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { useTabsViewStore } from '@/store/modules/tabsView';
const tabsViewStore = useTabsViewStore();
function handleSignal(signal: string) {
tabsViewStore.closeSignal(signal);
}
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,70 @@
<template>
<div>
<n-space vertical>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 渐变文字 </template>
<template #header-extra>
<n-gradient-text type="success"> 渐变文字成了 </n-gradient-text>
</template>
<n-space vertical>
<n-gradient-text style="white-space: normal" type="success">
<p
>苏轼,(1037年1月8日-1101年8月24日)字子瞻、和仲,号铁冠道人、东坡居士,世称苏东坡、苏仙,汉族,眉州眉山(四川省眉山市)人,祖籍河北栾城,北宋著名文学家、书法家、画家,历史治水名人。苏轼是北宋中期文坛领袖,在诗、词、散文、书、画等方面取得很高成就。文纵横恣肆;诗题材广阔,清新豪健,善用夸张比喻,独具风格,与黄庭坚并称“苏黄”;词开豪放一派,与辛弃疾同是豪放派代表,并称“苏辛”;散文著述宏富,豪放自如,与欧阳修并称“欧苏”,为“唐宋八大家”之一。苏轼善书,“宋四家”之一;擅长文人画,尤擅墨竹、怪石、枯木等。与韩愈、柳宗元和欧阳修合称“千古文章四大家”。作品有《东坡七集》《东坡易传》《东坡乐府》《潇湘竹石图卷》《古木怪石图卷》等</p
>
</n-gradient-text>
</n-space>
</n-card>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 渐变文字 </template>
<template #header-extra>
<n-gradient-text type="warning"> 渐变文字成了 </n-gradient-text>
</template>
<n-space vertical>
<n-gradient-text style="white-space: normal" type="warning">
<p
>白居易(772年-846年),字乐天,号香山居士,又号醉吟先生,祖籍太原,到其曾祖父时迁居下邽,生于河南新郑。是唐代伟大的现实主义诗人,唐代三大诗人之一。白居易与元稹共同倡导新乐府运动,世称“元白”,与刘禹锡并称“刘白”。白居易的诗歌题材广泛,形式多样,语言平易通俗,有“诗魔”和“诗王”之称。官至翰林学士、左赞善大夫。公元846年,白居易在洛阳逝世,葬于香山。有《白氏长庆集》传世,代表诗作有《长恨歌》《卖炭翁》《琵琶行》等。</p
>
</n-gradient-text>
</n-space>
</n-card>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 自定义颜色 </template>
<template #header-extra>
<n-gradient-text gradient="linear-gradient(90deg, red 0%, green 50%, blue 100%)">
渐变文字成了
</n-gradient-text>
</template>
<n-space vertical>
<n-gradient-text
gradient="linear-gradient(90deg, red 0%, green 50%, blue 100%)"
style="white-space: normal"
>
<p
>杜甫(712-770),字子美,自号少陵野老,世称“杜工部”、“杜少陵”等,汉族,河南府巩县(今河南省巩义市)人,唐代伟大的现实主义诗人,杜甫被世人尊为“诗圣”,其诗被称为“诗史”。杜甫与李白合称“李杜”,为了跟另外两位诗人李商隐与杜牧即“小李杜”区别开来,杜甫与李白又合称“大李杜”。他忧国忧民,人格高尚,他的约1400余首诗被保留了下来,诗艺精湛,在中国古典诗歌中备受推崇,影响深远。759-766年间曾居成都,后世有杜甫草堂纪念</p
>
</n-gradient-text>
</n-space>
</n-card>
</n-space>
</div>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped>
p {
font-size: 14px;
line-height: 2;
text-indent: 28px;
}
</style>
@@ -0,0 +1,94 @@
<template>
<div>
<n-card title="高亮" style="margin-bottom: 10px">
<n-text>
<p style="line-height: 2" v-html="compData1.text"></p>
</n-text>
</n-card>
<n-card title="自定义颜色" style="margin-bottom: 10px">
<n-text>
<p style="line-height: 2" v-html="compData2.text"></p>
</n-text>
</n-card>
<n-card title="自定义样式">
<n-text>
<p style="line-height: 2" v-html="compData3.text"></p>
</n-text>
</n-card>
</div>
</template>
<script lang="ts" setup>
import highHtml from '@/utils/highHtml';
import { reactive, onMounted } from 'vue';
const text =
'先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。\n' +
'\n' +
'  宫中府中,俱为一体;陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。\n' +
'\n' +
'  侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨补阙漏,有所广益。\n' +
'\n' +
'  将军向宠,性行淑均,晓畅军事,试用于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。\n' +
'\n' +
'  亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。\n' +
'\n' +
'  臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。\n' +
'\n' +
'  先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐托付不效,以伤先帝之明;故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。\n' +
'\n' +
'  愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏。臣不胜受恩感激。今当远离,临表涕零,不知所言';
const compData1 = reactive({
text: '',
keys: ['先帝', '先汉', '苟'],
});
const compData2 = reactive({
text: '',
keys: [
{
keyword: '先帝',
bgColor: 'red',
color: '#fff',
renderHighlightKeyword: (str) => {
return `<span class="high" style="padding:2px 5px;margin:0 5px;border-radius:3px;background-color: #3a5ccc;color: #ffffff">${str}</span>`;
},
},
{
keyword: '中',
bgColor: '#b4d5ff',
color: '#fff',
renderHighlightKeyword: (str) => {
return `<span class="high" style="padding:2px 5px;margin:0 5px;border-radius:3px;background-color: #e96656;color: #ffffff">${str}</span>`;
},
},
{ keyword: '苟', bgColor: '#b4d5ff', color: '#fff' },
],
});
const compData3 = reactive({
text: '',
keys: [
{
keyword: '之',
bgColor: 'red',
color: '#fff',
style: { padding: '2px 5px', margin: '0 5px', 'border-radius': '3px' },
},
{ keyword: '先帝', bgColor: '#b4d5ff', color: '#fff' },
{ keyword: '苟', bgColor: '#b4d5ff', color: '#fff' },
],
});
onMounted(() => {
compData1.text = highHtml(text, compData1.keys).highText;
compData2.text = highHtml(text, compData2.keys).highText;
compData3.text = highHtml(text, compData3.keys).highText;
});
</script>
<style lang="less">
.high {
&:hover {
background-color: seagreen !important;
cursor: pointer;
}
}
</style>
@@ -0,0 +1,50 @@
<template>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 敏感词汇验证 </template>
<n-space vertical>
<n-space>
<n-tag
:type="idx % 2 ? 'success' : 'warning'"
:key="idx"
v-for="(item, idx) in compData.keys"
>{{ item }}</n-tag
>
</n-space>
<n-input
placeholder="输入需要验证的词汇文本"
type="textarea"
size="small"
v-model:value="compData.text"
:autosize="{ minRows: 3, maxRows: 5 }"
@update:value="compData.handleUpdateText"
/>
<n-space>
<n-tag
:type="idx % 2 ? 'error' : 'info'"
:key="idx"
v-for="(item, idx) in compData.words"
>{{ item }}</n-tag
>
</n-space>
</n-space>
</n-card>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import Mint from 'mint-filter';
const compData = reactive({
keys: ['敏感词', '胡萝卜', '香蕉', '苹果'],
text: '',
words: [],
});
const mint = new Mint(compData.keys);
compData.handleUpdateText = (value: string | [string, string]) => {
const test = mint.filter(value);
compData.words = test.words;
};
</script>
@@ -0,0 +1,102 @@
<template>
<div>
<n-space vertical>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 字符拼音 </template>
<template #header-extra> 陋室铭,唐代:刘禹锡 </template>
<div class="pinyin">
<template v-for="item in compData.items1">
<div class="pinyin-item" :key="idx" v-for="(todo, idx) in item">
<span class="py">{{ todo.py }}</span>
<span class="hz">{{ todo.hz }}</span> </div
><br />
</template>
</div>
</n-card>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 字符拼音 </template>
<template #header-extra> 李贺小传,李商隐〔唐代〕 </template>
<div class="pinyin">
<template v-for="item in compData.items2">
<div class="pinyin-item" :key="idx" v-for="(todo, idx) in item">
<span class="py">{{ todo.py }}</span>
<span class="hz">{{ todo.hz }}</span> </div
><br />
</template>
</div>
</n-card>
</n-space>
</div>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { pinyin } from 'pinyin-pro';
interface WordInfo {
hz: string;
py: string;
}
const text1 = [
' 山不在高,有仙则名。水不在深,有龙则灵。斯是陋室,惟吾德馨。苔痕上阶绿,草色入帘青。谈笑有鸿儒,往来无白丁。可以调素琴,阅金经。无丝竹之乱耳,无案牍之劳形。南阳诸葛庐,西蜀子云亭。孔子云:何陋之有?',
];
const text2 = [
' 京兆杜牧为李长吉集序,状长吉之奇甚尽,世传之。长吉姊嫁王氏者,语长吉之事尤备。',
' 长吉细瘦,通眉,长指爪,能苦吟疾书。最先为昌黎韩愈所知。所与游者,王参元、杨敬之、权璩、崔植辈为密,每旦日出与诸公游,未尝得题然后为诗,如他人思量牵合,以及程限为意。恒从小奚奴,骑距驉,背一古破锦囊,遇有所得,即书投囊中。及暮归.太夫人使婢受囊出之,见所书多.辄曰:“是儿要当呕出心乃已尔。”上灯,与食。长吉从婢取书,研墨叠纸足成之,投他囊中。非大醉及吊丧日率如此,过亦不复省。王、杨辈时复来探取写去。长吉往往独骑往还京、洛,所至或时有著,随弃之,故沈子明家所余四卷而已。',
' 长吉将死时,忽昼见一绯衣人,驾赤虬,持一板,书若太古篆或霹雳石文者,云当召长吉。长吉了不能读,欻下榻叩头,言:“阿㜷老且病,贺不愿去。”绯衣人笑曰:“帝成白玉楼,立召君为记。天上差乐,不苦也。”长吉独泣,边人尽见之。少之,长吉气绝。常所居窗中,勃勃有烟气,闻行车嘒管之声。太夫人急止人哭,待之如炊五斗黍许时,长吉竟死。王氏姊非能造作谓长吉者,实所见如此。',
' 呜呼,天苍苍而高也,上果有帝耶?帝果有苑囿、宫室、观阁之玩耶?苟信然,则天之高邈,帝之尊严,亦宜有人物文采愈此世者,何独眷眷于长吉而使其不寿耶?噫,又岂世所谓才而奇者,不独地上少,即天上亦不多耶?长吉生二十七年,位不过奉礼太常,时人亦多排摈毁斥之,又岂才而奇者,帝独重之,而人反不重耶?又岂人见会胜帝耶?',
];
const createHzPy = (text: string[]): WordInfo[][] => {
const items: WordInfo[][] = [];
text.forEach((item) => {
const todo: WordInfo[] = [];
for (let i = 0; i < item.length; i++) {
const tg = item.charAt(i);
todo.push({ hz: tg, py: pinyin(tg)[0] });
}
items.push(todo);
});
return items;
};
const compData = reactive({
items1: createHzPy(text1),
items2: createHzPy(text2),
});
</script>
<style lang="less" scoped>
.pinyin {
&-item {
line-height: 100%;
width: 42px;
text-align: center;
display: inline-block;
.py {
clear: both;
font-size: 12px;
font-weight: normal;
float: left;
width: 42px;
}
.hz {
clear: both;
margin-bottom: 10px;
text-align: center;
float: left;
font-size: 16px;
height: 36px;
width: 42px;
line-height: 36px;
}
}
}
</style>
@@ -0,0 +1,97 @@
<template>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 时间线 </template>
<n-space>
<n-card title="竖向">
<n-timeline>
<n-timeline-item content="啊" />
<n-timeline-item type="success" title="成功" content="哪里成功" time="2018-04-03 20:46" />
<n-timeline-item type="error" content="哪里错误" time="2018-04-03 20:46" />
<n-timeline-item type="warning" title="警告" content="哪里警告" time="2018-04-03 20:46" />
<n-timeline-item
type="info"
title="信息"
content="是的"
time="2018-04-03 20:46"
line-type="dashed"
/>
<n-timeline-item content="啊" />
</n-timeline>
</n-card>
<n-card title="横向">
<div style="overflow: auto">
<n-timeline horizontal>
<n-timeline-item content="啊" />
<n-timeline-item
type="success"
title="成功"
content="哪里成功"
time="2018-04-03 20:46"
/>
<n-timeline-item type="error" content="哪里失败" time="2018-04-03 20:46" />
<n-timeline-item
type="warning"
title="警告"
content="哪里警告"
time="2018-04-03 20:46"
/>
<n-timeline-item type="info" title="信息" content="是的" time="2018-04-03 20:46" />
</n-timeline>
</div>
</n-card>
<n-card title="自定义">
<n-timeline :icon-size="20">
<n-timeline-item color="grey" content="啊">
<template #icon>
<n-icon>
<cash-icon />
</n-icon>
</template>
</n-timeline-item>
<n-timeline-item type="success" title="成功" content="哪里成功" time="2018-04-03 20:46">
<template #icon>
<n-icon>
<cash-icon />
</n-icon>
</template>
</n-timeline-item>
<n-timeline-item type="error" content="哪里错误" time="2018-04-03 20:46">
<template #icon>
<n-icon>
<cash-icon />
</n-icon>
</template>
</n-timeline-item>
<n-timeline-item type="warning" title="警告" content="哪里警告" time="2018-04-03 20:46">
<template #icon>
<n-icon>
<cash-icon />
</n-icon>
</template>
</n-timeline-item>
<n-timeline-item type="info" title="信息" content="是的" time="2018-04-03 20:46">
<template #icon>
<n-icon>
<cash-icon />
</n-icon>
</template>
</n-timeline-item>
</n-timeline>
</n-card>
</n-space>
</n-card>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { CashOutline as CashIcon } from '@vicons/ionicons5';
export default defineComponent({
components: {
CashIcon,
},
});
</script>
@@ -0,0 +1,15 @@
<template>
<Iframe ref="iframeRef" style="zoom: 0.8" />
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import Iframe from '../../../../iframe/index.vue';
const iframeRef = ref();
onMounted(() => {
iframeRef.value?.open('https://www.makeapie.cn/echarts');
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,15 @@
<template>
<Iframe ref="iframeRef" style="zoom: 0.9" />
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import Iframe from '../../../../iframe/index.vue';
const iframeRef = ref();
onMounted(() => {
iframeRef.value?.open('https://madeapie.com/');
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,15 @@
<template>
<Iframe ref="iframeRef" style="zoom: 0.9" />
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import Iframe from '../../../../iframe/index.vue';
const iframeRef = ref();
onMounted(() => {
iframeRef.value?.open('https://ppchart.com/#/');
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,106 @@
<template>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 瀑布流组件 </template>
<template #header-extra>
<n-tag type="success" @click="handleClick">vue-waterfall-plugin-next</n-tag>
</template>
<Waterfall
:row-key="options.rowKey"
:gutter="options.gutter"
:has-around-gutter="options.hasAroundGutter"
:width="options.width"
:breakpoints="options.breakpoints"
:img-selector="options.imgSelector"
:background-color="options.backgroundColor"
:animation-effect="options.animationEffect"
:animation-duration="options.animationDuration"
:animation-delay="options.animationDelay"
:lazyload="options.lazyload"
:list="compData.items"
>
<template #item="{ item }">
<div class="waterfall-item" :style="item.style">
<span>君问归期未有期,巴山夜雨涨秋池。何当共剪西窗烛,却话巴山夜雨时。</span>
</div>
</template>
</Waterfall>
</n-card>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import { Waterfall } from 'vue-waterfall-plugin-next';
import 'vue-waterfall-plugin-next/dist/style.css';
import { rdmLightRgbColor } from '@/utils/hotgo';
const options = reactive({
// 唯一key值
rowKey: 'id',
// 卡片之间的间隙
gutter: 20,
// 是否有周围的gutter
hasAroundGutter: true,
// 卡片在PC上的宽度
width: 250,
// 自定义行显示个数,主要用于对移动端的适配
breakpoints: {
1200: {
// 当屏幕宽度小于等于1200
rowPerView: 5,
},
800: {
// 当屏幕宽度小于等于800
rowPerView: 3,
},
500: {
// 当屏幕宽度小于等于500
rowPerView: 1,
},
},
// 动画效果
animationEffect: 'animate__fadeInUp',
// 动画时间
animationDuration: 1000,
// 动画延迟
animationDelay: 300,
// 背景色
backgroundColor: '',
// imgSelector
imgSelector: 'src.original',
// 是否懒加载
lazyload: true,
});
const items: any = [];
const genBetweenRight = (m, n) => Math.floor(Math.random() * (n - m) + 1) + m;
for (let i = 0; i < 90; i++) {
items.push({
style: {
height: genBetweenRight(100, 300) + 'px',
'background-color': rdmLightRgbColor(),
},
});
}
const compData = reactive({
items,
});
const handleClick = () => {
window.open('https://vue-waterfall.netlify.app/');
};
</script>
<style lang="less" scoped>
.waterfall-item {
border: 2px solid rgb(244, 244, 248);
height: 100px;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
border-radius: 4px;
}
</style>
@@ -0,0 +1,171 @@
<template>
<n-space vertical>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 文字水印 </template>
<template #header-extra> 核心机密 </template>
<n-watermark
content="核心机密"
cross
selectable
:font-size="16"
:line-height="16"
:width="192"
:height="128"
:x-offset="12"
:y-offset="28"
:rotate="-15"
>
<n-table :single-line="false">
<thead>
<tr>
<th>复盘</th>
<th>赋能</th>
<th>协同</th>
<th>...</th>
<th>串联</th>
</tr>
</thead>
<tbody>
<tr>
<td>拉通</td>
<td>打通</td>
<td>树立</td>
<td>...</td>
<td>履约</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</n-table>
</n-watermark>
</n-card>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 图片水印 </template>
<template #header-extra>
<n-image height="30" width="30" :src="compData.img" />
</template>
<n-watermark
:image="compData.img"
cross
:rotate="-15"
:font-size="16"
:line-height="16"
:width="192"
:height="128"
:x-offset="12"
:y-offset="30"
:image-width="34"
:image-opacity="0.24"
>
<n-table :single-line="false">
<thead>
<tr>
<th>复盘</th>
<th>赋能</th>
<th>协同</th>
<th>...</th>
<th>串联</th>
</tr>
</thead>
<tbody>
<tr>
<td>拉通</td>
<td>打通</td>
<td>树立</td>
<td>...</td>
<td>履约</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</n-table>
</n-watermark>
</n-card>
<n-card
:segmented="{ content: true, footer: true }"
header-style="padding:10px"
footer-style="padding:10px"
>
<template #header> 自定义水印 </template>
<template #header-extra>
<n-input-group>
<n-input v-model:value="compData.text" placeholder="输入自定义水印" />
<n-button type="primary" ghost>添加</n-button>
</n-input-group>
</template>
<n-watermark
v-model:content="compData.text"
cross
selectable
:font-size="16"
:line-height="16"
:width="192"
:height="128"
:x-offset="12"
:y-offset="28"
:rotate="-15"
>
<n-table :single-line="false">
<thead>
<tr>
<th>复盘</th>
<th>赋能</th>
<th>协同</th>
<th>...</th>
<th>串联</th>
</tr>
</thead>
<tbody>
<tr>
<td>拉通</td>
<td>打通</td>
<td>树立</td>
<td>...</td>
<td>履约</td>
</tr>
<tr>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
<td>...</td>
</tr>
</tbody>
</n-table>
</n-watermark>
</n-card>
</n-space>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue';
import logo from '@/assets/images/logo.png';
export default defineComponent({
setup() {
const compData = reactive({
img: logo,
text: '自定义水印',
});
return {
compData,
};
},
});
</script>
@@ -0,0 +1,74 @@
<template>
<div>
<n-spin :show="show" description="请稍候...">
<n-form :label-width="80" :model="formValue" :rules="rules" ref="formRef">
<n-form-item label="测试参数" path="basicTest">
<n-input v-model:value="formValue.basicTest" placeholder="请输入测试参数" />
<template #feedback>
这是一个测试参数,每个插件都可以有独立的配置项,可以按需添加</template
>
</n-form-item>
<div>
<n-space>
<n-button type="primary" @click="formSubmit">保存更新</n-button>
</n-space>
</div>
</n-form>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { useMessage } from 'naive-ui';
import { getConfig, updateConfig } from '@/api/addons/hgexample/config';
const group = ref('basic');
const show = ref(false);
const rules = {
basicTest: {
required: true,
message: '请输入测试参数',
trigger: 'blur',
},
};
const formRef: any = ref(null);
const message = useMessage();
const formValue = ref({
basicTest: 'HotGo',
});
function formSubmit() {
formRef.value.validate((errors) => {
if (!errors) {
updateConfig({ group: group.value, list: formValue.value }).then((_res) => {
message.success('更新成功');
load();
});
} else {
message.error('验证失败,请填写完整信息');
}
});
}
onMounted(() => {
load();
});
function load() {
show.value = true;
new Promise((_resolve, _reject) => {
getConfig({ group: group.value })
.then((res) => {
formValue.value = res.list;
})
.finally(() => {
show.value = false;
});
});
}
</script>
@@ -0,0 +1,82 @@
<template>
<div>
<n-grid cols="24 300:1 600:24" :x-gap="12">
<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" />
</n-card>
</n-grid-item>
</n-grid>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs } from 'vue';
import BasicSetting from './BasicSetting.vue';
const typeTabList = [
{
name: '基本设置',
desc: '系统常规设置',
key: 1,
},
];
export default defineComponent({
components: {
BasicSetting,
},
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>
@@ -0,0 +1,47 @@
<template>
<n-form class="py-4">
<n-form-item label="测试入口" path="index">
<n-input-group>
<n-input placeholder="请输入" :default-value="url" :disabled="true" />
<n-button v-copy="url" type="primary" @click="copy"> 复制链接 </n-button>
</n-input-group>
</n-form-item>
<n-form-item label="二维码" path="index" v-if="showQrcode">
<div class="text-center">
<qrcode-vue :value="url" :size="220" class="canvas" style="margin: 0 auto" />
</div>
</n-form-item>
</n-form>
</template>
<script lang="ts" setup>
import { useUserStoreWidthOut } from '@/store/modules/user';
import QrcodeVue from 'qrcode.vue';
import { useMessage } from 'naive-ui';
const message = useMessage();
interface Props {
path: string;
showQrcode?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
path: '',
showQrcode: true,
});
const copy = () => {
message.success('复制成功');
};
const useUserStore = useUserStoreWidthOut();
const url = useUserStore.config?.domain + props.path;
</script>
<style scoped>
::v-deep(.card-tabs .n-tabs-nav--bar-type) {
padding-left: 4px;
}
</style>
@@ -0,0 +1,35 @@
<template>
<div>
<n-card title="应用入口" class="proCard">
<n-tabs type="line" animated>
<n-tab-pane name="admin" tab="后台API">
<Form path="/admin/hgexample/index/test?name=HotGo" />
</n-tab-pane>
<n-tab-pane name="api" tab="前台API">
<Form path="/api/hgexample/index/test?name=HotGo" />
</n-tab-pane>
<n-tab-pane name="home" tab="前台页面">
<Form path="/home/hgexample/index/test?name=HotGo" />
</n-tab-pane>
<n-tab-pane name="static" tab="静态目录">
<Form path="/addons/hgexample/default" />
</n-tab-pane>
<n-tab-pane name="websocket" tab="Websocket">
<Form path="/socket/hgexample/index/test?name=HotGo" :showQrcode="false" />
<WebsocketTest />
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import Form from './form.vue';
import WebsocketTest from './websocketTest.vue';
</script>
<style scoped>
::v-deep(.card-tabs .n-tabs-nav--bar-type) {
padding-left: 4px;
}
</style>
@@ -0,0 +1,134 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="测试websocket">
尝试在下方输入框中输入任意文字消息内容,发送后websocket服务器收到会原样返回
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<n-space vertical>
<n-input-group style="width: 520px">
<n-input
@keyup.enter="sendMessage"
:style="{ width: '78%' }"
placeholder="请输入消息内容"
:on-focus="onFocus"
:on-blur="onBlur"
v-model:value="inputMessage"
/>
<n-button type="primary" @click="sendMessage"> 发送消息</n-button>
</n-input-group>
<div class="mt-5"></div>
<n-timeline :icon-size="20">
<n-timeline-item color="grey" content="输入中.." v-if="isInput">
<template #icon>
<n-icon>
<MessageOutlined />
</n-icon>
</template>
</n-timeline-item>
<n-timeline-item
v-for="item in messages"
:key="item"
:type="item.type == Enum.SendType ? 'success' : 'info'"
:title="item.type == Enum.SendType ? '发送消息' : '收到消息'"
:content="item.content"
:time="item.time"
>
<template #icon>
<n-icon>
<SendOutlined v-if="item.type == Enum.SendType" />
<SoundOutlined v-if="item.type == Enum.ReceiveType" />
</n-icon>
</template>
</n-timeline-item>
</n-timeline>
</n-space>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { MessageOutlined, SendOutlined, SoundOutlined } from '@vicons/antd';
import { format } from 'date-fns';
import { addOnMessage, removeOnMessage, sendMsg, WebSocketMessage } from '@/utils/websocket';
import { useMessage } from 'naive-ui';
interface Message {
type: Enum;
content: string;
time: string;
}
const message = useMessage();
const messages = ref<Message[]>([]);
const inputMessage = ref('你好,HotGo');
const isInput = ref(false);
const testMessageEvent = 'admin/addons/hgexample/testMessage';
enum Enum {
SendType = 1, // 发送类型
ReceiveType = 2, // 接受类型
}
function onFocus() {
isInput.value = true;
}
function onBlur() {
isInput.value = false;
}
function sendMessage() {
if (inputMessage.value == '') {
message.error('消息内容不能为空');
return;
}
// 发送消息
sendMsg(testMessageEvent, {
message: inputMessage.value,
});
const msg: Message = {
type: Enum.SendType,
content: inputMessage.value,
time: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
};
insertMessage(msg);
inputMessage.value = '';
}
// 存入本地记录
function insertMessage(msg: Message): void {
messages.value.unshift(msg); // 在头部插入消息
if (messages.value.length > 10) {
messages.value = messages.value.slice(0, 10); // 如果超过10个,则只保留最前面10个
}
}
const onMessage = (res: WebSocketMessage) => {
const msg: Message = {
type: Enum.ReceiveType,
content: res.data.message,
time: format(new Date(), 'yyyy-MM-dd HH:mm:ss'),
};
insertMessage(msg);
};
onMounted(() => {
// 在当前页面注册消息监听
addOnMessage(testMessageEvent, onMessage);
});
onBeforeUnmount(() => {
// 移除消息监听
removeOnMessage(testMessageEvent);
});
</script>
<style scoped></style>
@@ -0,0 +1,246 @@
<template>
<div>
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
:title="params?.id > 0 ? '编辑 #' + params?.id : '添加'"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="params"
: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="params.title" />
</n-form-item>
<n-form-item label="分类ID" path="categoryId">
<n-input-number placeholder="请输入分类ID" v-model:value="params.categoryId" />
</n-form-item>
<n-form-item label="标签" path="flag">
<n-checkbox-group v-model:value="params.flag">
<n-space>
<n-checkbox
v-for="item in dict.getOptionUnRef('sys_notice_type')"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</n-space>
</n-checkbox-group>
</n-form-item>
<n-form-item label="描述" path="description">
<n-input type="textarea" placeholder="描述" v-model:value="params.description" />
</n-form-item>
<n-form-item label="内容" path="content">
<Editor style="height: 450px" v-model:value="params.content" />
</n-form-item>
<n-form-item label="单图" path="image">
<UploadImage v-model:value="params.image" />
</n-form-item>
<n-form-item label="多图" path="images">
<FileChooser :maxNumber="10" v-model:value="params.images" fileType="image" />
</n-form-item>
<n-form-item label="单附件" path="attachfile">
<FileChooser v-model:value="params.attachfile" />
</n-form-item>
<n-form-item label="多附件" path="attachfiles">
<UploadFile :maxNumber="10" v-model:value="params.attachfiles" />
</n-form-item>
<n-form-item label="键值对" path="map">
<n-dynamic-input
v-model:value="params.map"
preset="pair"
key-placeholder="键名"
value-placeholder="键值"
/>
</n-form-item>
<n-form-item label="推荐星" path="star">
<n-rate allow-half :default-value="params.star" :on-update:value="updateStar" />
</n-form-item>
<n-form-item label="价格" path="price">
<n-input-number
placeholder="请输入价格"
clearable
v-model:value="params.price"
:precision="2"
/>
</n-form-item>
<n-form-item label="活动时间" path="activityAt">
<DatePicker v-model:formValue="params.activityAt" type="date" />
</n-form-item>
<n-form-item label="开放时间" path="startAt">
<DatePicker
v-model:startValue="params.startAt"
v-model:endValue="params.endAt"
type="datetimerange"
/>
</n-form-item>
<n-form-item label="用户渠道" path="channel">
<n-select
v-model:value="params.channel"
:options="dict.getOptionUnRef('sys_user_channel')"
/>
</n-form-item>
<n-form-item label="所在城市" path="cityId">
<CitySelector v-model:value="params.cityId" />
</n-form-item>
<n-form-item label="用户爱好" path="hobby">
<n-select
multiple
v-model:value="params.hobby"
:options="dict.getOptionUnRef('sys_user_hobby')"
/>
</n-form-item>
<n-form-item label="QQ" path="qq">
<n-input placeholder="请输入QQ号" v-model:value="params.qq" />
</n-form-item>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入邮箱地址" v-model:value="params.email" />
</n-form-item>
<n-form-item label="手机号" path="mobile">
<n-input placeholder="请输入手机号" v-model:value="params.mobile" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="params.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="params.status" name="status">
<n-radio-button
v-for="status in dict.getOptionUnRef('sys_normal_disable')"
:key="Number(status.value)"
:value="Number(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="params.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { rules, State, newState } from './model';
import { Edit, MaxSort } from '@/api/addons/hgexample/table';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import DatePicker from '@/components/DatePicker/datePicker.vue';
import Editor from '@/components/Editor/editor.vue';
import UploadImage from '@/components/Upload/uploadImage.vue';
import UploadFile from '@/components/Upload/uploadFile.vue';
import CitySelector from '@/components/CitySelector/citySelector.vue';
import FileChooser from '@/components/FileChooser/index.vue';
import { useDictStore } from '@/store/modules/dict';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const params = computed(() => {
return props.formParams;
});
const dict = useDictStore();
const message = useMessage();
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function updateStar(num) {
params.value.star = num;
}
function closeForm() {
isShowModal.value = false;
}
watch(
() => params.value,
(value) => {
if (value.id === 0) {
MaxSort().then((res) => {
params.value.sort = res.sort;
});
}
}
);
</script>
<style lang="less"></style>
@@ -0,0 +1,262 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="表格例子">
这里提供了一些常用的普通表格组件的用法和表单组件的例子,你可能会需要
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:checked-row-keys="checkedIds"
@update:checked-row-keys="onCheckedRow"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
size="small"
@update:sorter="handleUpdateSorter"
>
<template #tableTitle>
<n-button type="primary" @click="addTable" class="min-left-space">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加
</n-button>
<n-button
type="error"
@click="handleBatchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button type="primary" @click="handleExport" class="min-left-space">
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
<Edit
@reload-table="reloadTable"
@update-show-modal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { useSorter } from '@/hooks/common';
import { Delete, List, Status, Export } from '@/api/addons/hgexample/table';
import { State, columns, schemas, newState, loadOptions } from './model';
import { DeleteOutlined, PlusOutlined, ExportOutlined } from '@vicons/antd';
import { useRouter } from 'vue-router';
import { adaTableScrollX } from '@/utils/hotgo';
import Edit from './edit.vue';
import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();
const router = useRouter();
const dialog = useDialog();
const message = useMessage();
const searchFormRef = ref<any>();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const showModal = ref(false);
const formParams = ref<State>();
const actionRef = ref();
const { updateSorter: handleUpdateSorter, sortStatesRef: sortStatesRef } = useSorter(reloadTable);
const actionColumn = reactive({
width: 300,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '禁用',
onClick: handleStatus.bind(null, record, 2),
ifShow: () => {
return record.status === 1;
},
},
{
label: '启用',
onClick: handleStatus.bind(null, record, 1),
ifShow: () => {
return record.status === 2;
},
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
dropDownActions: [
{
label: '查看详情',
key: 'view',
},
{
label: '更多按钮1',
key: 'test1',
// 根据业务控制是否显示: 非enable状态的不显示启用按钮
ifShow: () => {
return true;
},
},
{
label: '更多按钮2',
key: 'test2',
ifShow: () => {
return true;
},
},
],
select: (key) => {
if (key === 'view') {
return handleView(record);
}
message.info(`您点击了,${key} 按钮`);
},
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loadDataTable = async (res) => {
return await List({
...searchFormRef.value?.formModel,
...{ sorters: sortStatesRef.value },
...res,
});
};
function addTable() {
showModal.value = true;
formParams.value = newState(null);
}
function updateShowModal(value) {
showModal.value = value;
}
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function handleView(record: Recordable) {
router.push({ name: 'table_view', params: { id: record.id } });
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = newState(record as State);
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
});
}
function handleBatchDelete() {
dialog.warning({
title: '警告',
content: '你确定要批量删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
batchDeleteDisabled.value = true;
checkedIds.value = [];
message.success('删除成功');
reloadTable();
});
},
});
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
function handleStatus(record: Recordable, status: number) {
Status({ id: record.id, status: status }).then((_res) => {
message.success('设为' + dict.getLabel('sys_normal_disable', status) + '成功');
setTimeout(() => {
reloadTable();
});
});
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,466 @@
import { h, ref } from 'vue';
import { NAvatar, NImage, NTag, NSwitch, NRate } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Switch } from '@/api/addons/hgexample/table';
import { isNullObject } from '@/utils/is';
import { getFileExt } from '@/utils/urlUtils';
import { defRangeShortcuts, defShortcuts, formatToDate } from '@/utils/dateUtil';
import { validate } from '@/utils/validateUtil';
import { fallbackSrc } from '@/utils/hotgo';
import { useDictStore } from '@/store/modules/dict';
import { renderOptionTag } from '@/utils';
import type { FormRules } from 'naive-ui/es/form/src/interface';
const dict = useDictStore();
const $message = window['$message'];
export interface State {
id: number;
memberId: number;
categoryId: number;
flag: number[] | null;
title: string;
content: string;
image: string;
images: string[] | null;
attachfile: string;
attachfiles: string[] | null;
map: unknown[] | null;
star: number;
description: string;
price: number;
views: number;
activityAt: string;
startAt: null;
endAt: null;
switch: number;
sort: number;
avatar: string;
sex: number;
qq: string;
email: string;
mobile: string;
channel: number;
cityId: number;
hobby: string[] | null;
pid: number;
level: number;
tree: string;
remark: string;
status: number;
createdBy: number;
createdAt: string;
updatedAt: string;
}
export const defaultState = {
id: 0,
memberId: 0,
categoryId: 0,
flag: [1],
title: '',
content: '',
image: '',
images: null,
attachfile: '',
attachfiles: null,
map: null,
star: 0,
description: '',
price: 0,
views: 0,
activityAt: '',
startAt: null,
endAt: null,
switch: 0,
sort: 0,
avatar: '',
sex: 0,
qq: '',
email: '',
mobile: '',
channel: 0,
cityId: 0,
hobby: null,
pid: 0,
level: 1,
tree: '',
remark: '',
status: 1,
createdBy: 0,
createdAt: '',
updatedAt: '',
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export const rules: FormRules = {
title: {
required: true,
trigger: ['blur', 'input'],
message: '请输入标题',
},
price: {
required: true,
trigger: ['blur', 'input'],
validator: validate.amount,
},
qq: {
required: false,
trigger: ['blur', 'input'],
validator: validate.qq,
},
email: {
required: true,
trigger: ['blur', 'input'],
validator: validate.email,
},
mobile: {
required: true,
trigger: ['blur', 'input'],
validator: validate.phone,
},
};
export const schemas = ref<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: 'price',
labelMessage: '我是自定义提示',
component: 'NInput',
label: '价格',
componentProps: {
pair: true,
separator: '-',
clearable: true,
placeholder: ['从', '到'],
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'activityAt',
component: 'NDatePicker',
label: '活动时间',
componentProps: {
type: 'date',
clearable: true,
shortcuts: defShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'flag',
component: 'NCheckbox',
label: '标签',
giProps: {
span: 1,
},
componentProps: {
placeholder: '请选择标签',
options: dict.getOption('sys_notice_type'),
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'switch',
component: 'NRadioGroup',
label: '开关',
giProps: {
//span: 24,
},
componentProps: {
options: dict.getOption('sys_switch'),
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'hobby',
component: 'NSelect',
label: '爱好',
defaultValue: null,
componentProps: {
multiple: true,
placeholder: '请选择爱好',
options: dict.getOption('sys_user_hobby'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: dict.getOption('sys_normal_disable'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: 'ID',
key: 'id',
sorter: true, // 单列排序
},
{
title: '标题',
key: 'title',
render(row) {
return row.title;
},
sorter: {
multiple: 1, // 为多列排序的优先级,越高优先级越高
},
},
{
title: '标签',
key: 'flag',
render(row) {
if (isNullObject(row.flag)) {
return ``;
}
return row.flag.map((tagKey) => {
return renderOptionTag('sys_notice_type', tagKey);
});
},
},
{
title: '单图',
key: 'image',
render(row) {
return h(NImage, {
width: 32,
height: 32,
src: row.image,
fallbackSrc: fallbackSrc(),
style: {
width: '32px',
height: '32px',
'max-width': '100%',
'max-height': '100%',
},
});
},
},
{
title: '多图',
key: 'images',
render(row) {
if (isNullObject(row.images)) {
return ``;
}
return row.images.map((image) => {
return h(NImage, {
width: 32,
height: 32,
src: image,
fallbackSrc: fallbackSrc(),
style: {
width: '32px',
height: '32px',
'max-width': '100%',
'max-height': '100%',
'margin-left': '2px',
},
});
});
},
},
{
title: '附件',
key: 'attachfile',
render(row) {
if (row.attachfile === '') {
return ``;
}
return h(
NAvatar,
{
size: 'small',
},
{
default: () => getFileExt(row.attachfile),
}
);
},
},
{
title: '多附件',
key: 'attachfiles',
render(row) {
if (isNullObject(row.attachfiles)) {
return ``;
}
return row.attachfiles.map((attachfile) => {
return h(
NAvatar,
{
size: 'small',
style: {
'margin-left': '2px',
},
},
{
default: () => getFileExt(attachfile),
}
);
});
},
},
{
title: '推荐星',
key: 'star',
// width: 180,
render(row) {
return h(NRate, {
allowHalf: true,
readonly: true,
defaultValue: row.star,
});
},
},
{
title: '描述',
key: 'description',
},
{
title: '价格',
key: 'price',
sorter: {
multiple: 2, // 为多列排序的优先级,越高优先级越高
},
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'success',
bordered: false,
},
{
default: () => row.price.toFixed(2),
}
);
},
},
{
title: '开关',
key: 'switch',
width: 100,
render(row) {
return h(NSwitch, {
value: row.switch === 1,
checked: '开启',
unchecked: '关闭',
onUpdateValue: function (e) {
row.switch = e ? 1 : 2;
Switch({ id: row.id, key: 'switch', value: row.switch }).then((_res) => {
$message.success('操作成功');
});
},
});
},
},
// {
// title: '排序',
// key: 'sort',
// },
{
title: '状态',
key: 'status',
render(row) {
return renderOptionTag('sys_normal_disable', row.status);
},
},
{
title: '爱好',
key: 'hobby',
render(row) {
if (isNullObject(row.hobby)) {
return ``;
}
return row.hobby.map((tagKey) => {
return renderOptionTag('sys_user_hobby', tagKey);
});
},
},
{
title: '活动时间',
key: 'activityAt',
render(row) {
return formatToDate(row.activityAt);
},
sorter: {
multiple: 3, // 为多列排序的优先级,越高优先级越高
},
},
];
export function loadOptions() {
dict.loadOptions([
'sys_normal_disable',
'sys_user_sex',
'sys_notice_type',
'sys_switch',
'sys_user_hobby',
'sys_user_channel',
]);
}
@@ -0,0 +1,143 @@
<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" :column="4">
<n-descriptions-item>
<template #label>分类ID</template>
{{ formValue.categoryId }}
</n-descriptions-item>
<n-descriptions-item label="标签">
<template v-for="(item, key) in formValue?.flag" :key="key">
<n-tag
:type="dict.getType('sys_notice_type', item)"
size="small"
class="min-left-space"
>{{ dict.getLabel('sys_notice_type', item) }}</n-tag
>
</template>
</n-descriptions-item>
<n-descriptions-item label="标题">{{ formValue.title }}</n-descriptions-item>
<n-descriptions-item label="描述">{{ formValue.description }}</n-descriptions-item>
<n-descriptions-item label="推荐星"
><n-rate readonly :default-value="formValue.star"
/></n-descriptions-item>
<n-descriptions-item label="价格">{{ formValue.price }}</n-descriptions-item>
<n-descriptions-item label="浏览次数">{{ formValue.views }}</n-descriptions-item>
<n-descriptions-item label="活动时间">{{ formValue.activityAt }}</n-descriptions-item>
<n-descriptions-item label="开关">
<n-switch v-model:value="formValue.switch" :unchecked-value="2" :checked-value="1"
/></n-descriptions-item>
<n-descriptions-item label="创建人ID">{{ formValue.createdBy }} </n-descriptions-item>
<n-descriptions-item label="创建时间">{{ formValue.createdAt }} </n-descriptions-item>
</n-descriptions>
</n-card>
<n-card :bordered="false" class="proCard mt-4" size="small" :segmented="{ content: true }">
<n-descriptions label-placement="top" title="内容" class="py-2" :column="1">
<n-descriptions-item><span v-html="formValue.content"></span></n-descriptions-item>
</n-descriptions>
</n-card>
<n-card :bordered="false" class="proCard mt-4" size="small" :segmented="{ content: true }">
<n-descriptions label-placement="top" title="单图" class="py-2" :column="1">
<n-descriptions-item>
<n-image style="margin-left: 10px; height: 100px; width: 100px" :src="formValue.image"
/></n-descriptions-item>
</n-descriptions>
<n-descriptions label-placement="top" title="多图" class="py-2" :column="1">
<n-descriptions-item>
<n-image-group>
<n-space>
<span v-for="(item, key) in formValue?.images" :key="key">
<n-image style="margin-left: 10px; height: 100px; width: 100px" :src="item" />
</span>
</n-space>
</n-image-group>
</n-descriptions-item>
</n-descriptions>
<n-descriptions label-placement="top" title="附件" class="py-2" :column="1">
<n-descriptions-item>
<div
class="upload-card"
v-show="formValue.attachfile !== ''"
@click="download(formValue.attachfile)"
>
<div class="upload-card-item" style="height: 100px; width: 100px">
<div class="upload-card-item-info">
<div class="img-box">
<n-avatar :style="fileAvatarCSS">{{ getFileExt(formValue.attachfile) }}</n-avatar>
</div>
</div>
</div>
</div>
</n-descriptions-item>
</n-descriptions>
<n-descriptions label-placement="top" title="多附件" class="py-2" :column="1">
<n-descriptions-item>
<div class="upload-card">
<n-space style="gap: 0px 0px">
<div
class="upload-card-item"
style="height: 100px; width: 100px"
v-for="(item, key) in formValue.attachfiles"
:key="key"
>
<div class="upload-card-item-info">
<div class="img-box">
<n-avatar :style="fileAvatarCSS" @click="download(item)">{{
getFileExt(item)
}}</n-avatar>
</div>
</div>
</div>
</n-space>
</div>
</n-descriptions-item>
</n-descriptions>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { useMessage } from 'naive-ui';
import { View } from '@/api/addons/hgexample/table';
import { newState } from './model';
import { getFileExt } from '@/utils/urlUtils';
import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();
const message = useMessage();
const router = useRouter();
const id = Number(router.currentRoute.value.params.id);
const formValue = ref(newState(null));
const fileAvatarCSS = computed(() => {
return {
'--n-merged-size': `var(--n-avatar-size-override, 80px)`,
'--n-font-size': `18px`,
};
});
//下载
function download(url: string) {
window.open(url);
}
onMounted(async () => {
if (id < 1) {
message.error('ID不正确,请检查!');
return;
}
formValue.value = await View({ id: id });
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,76 @@
<template>
<n-alert :show-icon="false" title="说明">
<n-p
>这里主要演示多租户业务下,不同用户身份如何在同一页面下展示不同的表格功能和字段数据,以及添加/编辑购买订单时服务端如何自动维护多租户关系</n-p
>
<n-p style="font-weight: 600">不同身份的测试账号</n-p>
<n-table :bordered="false" :single-line="false" size="small">
<thead>
<tr>
<th class="table-center">身份</th>
<th class="table-center">ID</th>
<th class="table-center">账号</th>
<th class="table-center">密码</th>
<th>身份描述</th>
</tr>
</thead>
<tbody>
<tr v-for="account in accounts" :key="account.id">
<td class="table-center">{{ account.type }}</td>
<td class="table-center">{{ account.id }}</td>
<td class="table-center">{{ account.username }}</td>
<td class="table-center">{{ account.password }}</td>
<td>{{ account.dc }}</td>
</tr>
</tbody>
</n-table>
</n-alert>
</template>
<script setup lang="ts">
interface Account {
type: string;
id: number;
username: string;
password: string;
dc: string;
}
const accounts: Account[] = [
{
type: '公司',
id: 1,
username: 'admin',
password: '123456',
dc: '可见全部数据。管理整个平台,包括商户和用户账户',
},
{
type: '租户',
id: 8,
username: 'ameng',
password: '123456',
dc: '可见自己下面的商户和用户数据。多租户系统中顶层实体,有自己的多个商户、用户、产品、订单等',
},
{
type: '商户',
id: 11,
username: 'abai',
password: '123456',
dc: '可见自己下面的用户数据。受租户的监管和管理,可以独立经营的实体,提供产品或服务,管理自己的业务,包括库存管理、订单处理、结算等',
},
{
type: '用户',
id: 12,
username: 'asong',
password: '123456',
dc: '只能看到自己数据。真正购买产品或享受服务的人,与商户互动,管理个人信息等个性化功能',
},
];
</script>
<style scoped lang="less">
.table-center {
text-align: center;
min-width: 80px;
}
</style>
@@ -0,0 +1,168 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
:title="formValue.id > 0 ? '编辑购买订单 #' + formValue.id : '添加购买订单'"
:style="{
width: dialogWidth,
}"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-spin :show="loading" description="请稍候...">
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
:label-placement="settingStore.isMobile ? 'top' : 'left'"
:label-width="100"
class="py-4"
>
<n-grid cols="1 s:1 m:1 l:1 xl:1 2xl:1" responsive="screen">
<n-gi span="1" v-if="userStore.isCompanyDept">
<n-form-item label="租户ID" path="tenantId">
<n-input placeholder="请输入租户ID" v-model:value="formValue.tenantId" />
</n-form-item>
</n-gi>
<n-gi span="1" v-if="userStore.isCompanyDept || userStore.isTenantDept">
<n-form-item label="商户ID" path="merchantId">
<n-input placeholder="请输入商户ID" v-model:value="formValue.merchantId" />
</n-form-item>
</n-gi>
<n-gi
span="1"
v-if="userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept"
>
<n-form-item label="用户ID" path="userId">
<n-input placeholder="请输入用户ID" v-model:value="formValue.userId" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="购买产品" path="productName">
<n-input placeholder="请输入购买产品" v-model:value="formValue.productName" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="关联订单号" path="orderSn">
<n-input placeholder="请输入关联订单号" v-model:value="formValue.orderSn" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="充值金额" path="money">
<n-input-group>
<n-input-number
:min="1"
:show-button="false"
style="width: 100%"
placeholder="请输入充值金额"
v-model:value="formValue.money"
/>
<n-input-group-label>元</n-input-group-label>
</n-input-group>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="备注" path="remark">
<n-input placeholder="请输入备注" v-model:value="formValue.remark" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="支付状态" path="status">
<n-select
v-model:value="formValue.status"
:options="dict.getOptionUnRef('payStatus')"
/>
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-spin>
</n-scrollbar>
<template #action>
<n-space>
<n-button @click="closeForm"> 取消 </n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm"> 确定 </n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { Edit, View } from '@/api/addons/hgexample/tenantOrder';
import { State, newState, rules } from './model';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { useUserStore } from '@/store/modules/user';
import { useDictStore } from '@/store/modules/dict';
const emit = defineEmits(['reloadTable']);
const dict = useDictStore();
const message = useMessage();
const settingStore = useProjectSettingStore();
const userStore = useUserStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth(840);
});
function openModal(state: State) {
showModal.value = true;
// 新增
if (!state || state.id < 1) {
formValue.value = newState(state);
return;
}
// 编辑
loading.value = true;
View({ id: state.id })
.then((res) => {
formValue.value = res;
})
.finally(() => {
loading.value = false;
});
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(formValue.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
closeForm();
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
showModal.value = false;
loading.value = false;
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>
@@ -0,0 +1,146 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="多租户功能演示">
<Alert />
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
ref="searchFormRef"
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
ref="actionRef"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
:actionColumn="actionColumn"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="primary"
@click="addTable"
class="min-left-space"
v-if="hasPermission(['/hgexample/tenantOrder/edit'])"
>
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加购买订单
</n-button>
</template>
</BasicTable>
</n-card>
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref, computed, onMounted } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Delete } from '@/api/addons/hgexample/tenantOrder';
import { PlusOutlined } from '@vicons/antd';
import { columns, schemas, loadOptions } from './model';
import { adaTableScrollX } from '@/utils/hotgo';
import Edit from './edit.vue';
import Alert from './alert.vue';
const dialog = useDialog();
const message = useMessage();
const { hasPermission } = usePermission();
const actionRef = ref();
const searchFormRef = ref<any>({});
const editRef = ref();
const actionColumn = reactive({
width: 144,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/hgexample/tenantOrder/edit'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/hgexample/tenantOrder/delete'],
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载表格数据
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
// 重新加载表格数据
function reloadTable() {
actionRef.value?.reload();
}
// 添加数据
function addTable() {
editRef.value.openModal(null);
}
// 编辑数据
function handleEdit(record: Recordable) {
editRef.value.openModal(record);
}
// 单个删除
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
});
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,212 @@
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useUserStore } from '@/store/modules/user';
import { useDictStore } from '@/store/modules/dict';
import { renderOptionTag } from '@/utils';
import type { FormRules } from 'naive-ui/es/form/src/interface';
const dict = useDictStore();
const userStore = useUserStore();
export class State {
public id = 0; // 主键
public tenantId = null; // 租户ID
public merchantId = null; // 商户ID
public userId = null; // 用户ID
public productName = ''; // 购买产品
public orderSn = ''; // 关联订单号
public money = null; // 充值金额
public remark = ''; // 备注
public status = 1; // 订单状态
public createdAt = ''; // 创建时间
public updatedAt = ''; // 修改时间
constructor(state?: Partial<State>) {
if (state) {
Object.assign(this, state);
}
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) {
return cloneDeep(state);
}
return new State(state);
}
return new State();
}
// 表单验证规则
export const rules: FormRules = {
money: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入充值金额',
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'tenantId',
component: 'NInput',
label: '租户ID',
componentProps: {
placeholder: '请输入租户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept;
},
},
{
field: 'merchantId',
component: 'NInput',
label: '商户ID',
componentProps: {
placeholder: '请输入商户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept;
},
},
{
field: 'userId',
component: 'NInput',
label: '用户ID',
componentProps: {
placeholder: '请输入用户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept;
},
},
{
field: 'orderSn',
component: 'NInput',
label: '订单号',
componentProps: {
placeholder: '请输入订单号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '订单状态',
defaultValue: null,
componentProps: {
placeholder: '请选择订单状态',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
// 表格列
export const columns = [
{
title: '订单ID',
key: 'id',
align: 'left',
width: 100,
},
{
title: '租户ID',
key: 'tenantId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept;
},
},
{
title: '商户ID',
key: 'merchantId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept;
},
},
{
title: '用户ID',
key: 'userId',
align: 'left',
width: 100,
ifShow: () => {
return userStore.isCompanyDept || userStore.isTenantDept || userStore.isMerchantDept;
},
},
{
title: '购买产品',
key: 'productName',
align: 'left',
width: 150,
},
{
title: '订单号',
key: 'orderSn',
align: 'left',
width: 200,
},
{
title: '充值金额',
key: 'money',
align: 'left',
width: 100,
render(row) {
return row.money + ' 元';
},
},
{
title: '订单状态',
key: 'status',
align: 'left',
width: 100,
render(row) {
return renderOptionTag('payStatus', row.status);
},
},
{
title: '创建时间',
key: 'createdAt',
align: 'left',
width: 180,
},
];
// 加载字典数据选项
export function loadOptions() {
dict.loadOptions(['payStatus']);
}
@@ -0,0 +1,256 @@
<template>
<div>
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
:title="params?.id > 0 ? '编辑 #' + params?.id : '添加'"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="params"
: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="params.pid"
v-model:value="params.pid"
/>
</n-form-item>
<n-form-item label="标题" path="title">
<n-input placeholder="请输入标题" v-model:value="params.title" />
</n-form-item>
<n-form-item label="分类ID" path="categoryId">
<n-input-number placeholder="请输入分类ID" v-model:value="params.categoryId" />
</n-form-item>
<n-form-item label="标签" path="flag">
<n-checkbox-group v-model:value="params.flag">
<n-space>
<n-checkbox
v-for="item in dict.getOptionUnRef('sys_notice_type')"
:key="item.value"
:value="item.value"
:label="item.label"
/>
</n-space>
</n-checkbox-group>
</n-form-item>
<n-form-item label="描述" path="description">
<n-input type="textarea" placeholder="描述" v-model:value="params.description" />
</n-form-item>
<n-form-item label="内容" path="content">
<Editor style="height: 450px" v-model:value="params.content" />
</n-form-item>
<n-form-item label="单图" path="image">
<UploadImage v-model:value="params.image" />
</n-form-item>
<n-form-item label="多图" path="images">
<FileChooser :maxNumber="10" v-model:value="params.images" fileType="image" />
</n-form-item>
<n-form-item label="单附件" path="attachfile">
<FileChooser v-model:value="params.attachfile" />
</n-form-item>
<n-form-item label="多附件" path="attachfiles">
<UploadFile :maxNumber="10" v-model:value="params.attachfiles" />
</n-form-item>
<n-form-item label="键值对" path="map">
<n-dynamic-input
v-model:value="params.map"
preset="pair"
key-placeholder="键名"
value-placeholder="键值"
/>
</n-form-item>
<n-form-item label="推荐星" path="star">
<n-rate allow-half :default-value="params.star" :on-update:value="updateStar" />
</n-form-item>
<n-form-item label="价格" path="price">
<n-input-number
placeholder="请输入价格"
clearable
v-model:value="params.price"
:precision="2"
/>
</n-form-item>
<n-form-item label="活动时间" path="activityAt">
<DatePicker v-model:formValue="params.activityAt" type="date" />
</n-form-item>
<n-form-item label="开放时间" path="startAt">
<DatePicker
v-model:startValue="params.startAt"
v-model:endValue="params.endAt"
type="datetimerange"
/>
</n-form-item>
<n-form-item label="用户渠道" path="channel">
<n-select
v-model:value="params.channel"
:options="dict.getOptionUnRef('sys_user_channel')"
/>
</n-form-item>
<n-form-item label="所在城市" path="cityId">
<CitySelector v-model:value="params.cityId" />
</n-form-item>
<n-form-item label="用户爱好" path="hobby">
<n-select
multiple
v-model:value="params.hobby"
:options="dict.getOptionUnRef('sys_user_hobby')"
/>
</n-form-item>
<n-form-item label="QQ" path="qq">
<n-input placeholder="请输入QQ号" v-model:value="params.qq" />
</n-form-item>
<n-form-item label="邮箱" path="email">
<n-input placeholder="请输入邮箱地址" v-model:value="params.email" />
</n-form-item>
<n-form-item label="手机号" path="mobile">
<n-input placeholder="请输入手机号" v-model:value="params.mobile" />
</n-form-item>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="params.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="params.status" name="status">
<n-radio-button
v-for="status in dict.getOptionUnRef('sys_normal_disable')"
:key="Number(status.value)"
:value="Number(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="params.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { rules, State, newState } from './model';
import { Edit, MaxSort } from '@/api/addons/hgexample/treeTable';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import DatePicker from '@/components/DatePicker/datePicker.vue';
import Editor from '@/components/Editor/editor.vue';
import UploadImage from '@/components/Upload/uploadImage.vue';
import UploadFile from '@/components/Upload/uploadFile.vue';
import CitySelector from '@/components/CitySelector/citySelector.vue';
import FileChooser from '@/components/FileChooser/index.vue';
import { useDictStore } from '@/store/modules/dict';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
optionTreeData: any;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
optionTreeData: [],
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const params = computed(() => {
return props.formParams;
});
const dict = useDictStore();
const message = useMessage();
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function updateStar(num) {
params.value.star = num;
}
function closeForm() {
isShowModal.value = false;
}
watch(
() => params.value,
(value) => {
if (value.id === 0) {
MaxSort().then((res) => {
params.value.sort = res.sort;
});
}
}
);
</script>
<style lang="less"></style>
@@ -0,0 +1,235 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="表格例子"> 一个树形表格例子 </n-card>
</div>
<n-grid 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" class="proCard">
<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 || formParams?.id < 1"
>
<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 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="height: 75vh"
@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" class="proCard">
<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"
:optionTreeData="optionTreeData"
@reload-table="reloadTable"
/>
</n-card>
</n-gi>
</n-grid>
<Edit
@reload-table="reloadTable"
@update-show-modal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
:optionTreeData="optionTreeData"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, unref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import {
AlignLeftOutlined,
FormOutlined,
PlusOutlined,
EditOutlined,
SearchOutlined,
DeleteOutlined,
} from '@vicons/antd';
import List from './list.vue';
import { Delete, Select } from '@/api/addons/hgexample/treeTable';
import Edit from './edit.vue';
import { newState, loadOptions } from './model';
const showModal = ref(false);
const message = useMessage();
const dialog = useDialog();
let treeItemKey = ref([]);
let expandedKeys = ref([]);
const treeData = ref([]);
const loading = ref(true);
const treeItemTitle = ref('');
const checkedId = ref(0);
const pattern = ref('');
const optionTreeData = ref<any>([]);
const formParams = ref(newState(null));
function openCreateDrawer() {
showModal.value = true;
formParams.value = newState(null);
}
function openEditDrawer() {
showModal.value = true;
}
function selectedTree(keys, opts) {
if (keys.length) {
const treeItem = opts[0];
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
formParams.value = newState(treeItem);
checkedId.value = treeItem.id;
} else {
treeItemKey.value = [];
treeItemTitle.value = '';
}
}
function handleDel() {
dialog.info({
title: '提示',
content: `您确定想删除吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ ...formParams.value }).then(async (_res) => {
message.success('操作成功');
await reloadTable();
});
},
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 reloadTable();
});
async function reloadTable() {
const treeMenuList = await Select();
Object.assign(
formParams,
treeMenuList.list.map((item) => item.key)
);
treeData.value = [];
optionTreeData.value = [
{
id: 0,
key: 0,
label: '根目录',
pid: 0,
title: '根目录',
},
];
treeData.value = treeMenuList.list;
optionTreeData.value = optionTreeData.value.concat(treeMenuList.list);
loading.value = false;
}
function onExpandedKeys(keys) {
expandedKeys.value = keys;
}
function updateShowModal(value) {
showModal.value = value;
}
onMounted(() => {
loadOptions();
});
</script>
@@ -0,0 +1,256 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<n-result
v-show="checkedId <= 0"
status="info"
title="提示"
description="请选择一个想要编辑的树"
/>
<div v-show="checkedId > 0">
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
:checked-row-keys="checkedIds"
@update:checked-row-keys="onCheckedRow"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
size="small"
@update:sorter="handleUpdateSorter"
>
<template #tableTitle>
<n-button type="primary" @click="addTable" class="min-left-space">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加
</n-button>
<n-button
type="error"
@click="handleBatchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button type="primary" @click="handleExport" class="min-left-space">
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</div>
</n-card>
<Edit
@reload-table="reloadTable"
@update-show-modal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
:optionTreeData="optionTreeData"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, h, reactive, ref, watch } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { useSorter } from '@/hooks/common';
import { Delete, List, Status, Export } from '@/api/addons/hgexample/treeTable';
import { State, columns, schemas, newState } from './model';
import { DeleteOutlined, PlusOutlined, ExportOutlined } from '@vicons/antd';
import { adaTableScrollX } from '@/utils/hotgo';
import Edit from './edit.vue';
import { useDictStore } from '@/store/modules/dict';
interface Props {
checkedId?: number;
optionTreeData: any;
}
const props = withDefaults(defineProps<Props>(), { checkedId: 0, optionTreeData: [] });
const emit = defineEmits(['reloadTable']);
const dict = useDictStore();
const dialog = useDialog();
const message = useMessage();
const searchFormRef = ref<any>();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const showModal = ref(false);
const formParams = ref<State>();
const actionRef = ref();
const { updateSorter: handleUpdateSorter, sortStatesRef: sortStatesRef } = useSorter(reloadTable);
const pid = ref(0);
const actionColumn = reactive({
width: 200,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
},
{
label: '禁用',
onClick: handleStatus.bind(null, record, 2),
ifShow: () => {
return record.status === 1;
},
},
{
label: '启用',
onClick: handleStatus.bind(null, record, 1),
ifShow: () => {
return record.status === 2;
},
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loadDataTable = async (res) => {
if (pid.value <= 0) {
return [];
}
return await List({
...searchFormRef.value?.formModel,
...{ sorters: sortStatesRef.value },
...{ pid: pid.value },
...res,
});
};
function addTable() {
showModal.value = true;
formParams.value = newState(null);
formParams.value.pid = pid.value;
}
function updateShowModal(value) {
showModal.value = value;
}
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
emit('reloadTable');
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = newState(record as State);
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
});
}
function handleBatchDelete() {
dialog.warning({
title: '警告',
content: '你确定要批量删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
batchDeleteDisabled.value = true;
checkedIds.value = [];
message.success('删除成功');
reloadTable();
});
},
});
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
function handleStatus(record: Recordable, status: number) {
Status({ id: record.id, status: status }).then((_res) => {
message.success('设为' + dict.getLabel('sys_normal_disable', status) + '成功');
setTimeout(() => {
reloadTable();
});
});
}
watch(props, (_newVal, _oldVal) => {
if (pid.value === _newVal.checkedId) {
return;
}
pid.value = Number(_newVal.checkedId);
if (_newVal.checkedId > 0) {
reloadTable();
}
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,460 @@
import { h, ref } from 'vue';
import { NAvatar, NImage, NTag, NSwitch, NRate } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { Switch } from '@/api/addons/hgexample/table';
import { isNullObject } from '@/utils/is';
import { getFileExt } from '@/utils/urlUtils';
import { defRangeShortcuts, defShortcuts, formatToDate } from '@/utils/dateUtil';
import { validate } from '@/utils/validateUtil';
import { fallbackSrc } from '@/utils/hotgo';
import { useDictStore } from '@/store/modules/dict';
import { renderOptionTag } from '@/utils';
const dict = useDictStore();
const $message = window['$message'];
export interface State {
id: number;
memberId: number;
categoryId: number;
flag: number[] | null;
title: string;
content: string;
image: string;
images: string[] | null;
attachfile: string;
attachfiles: string[] | null;
map: any | null;
star: number;
description: string;
price: number;
views: number;
activityAt: string;
startAt: null;
endAt: null;
switch: number;
sort: number;
avatar: string;
sex: number;
qq: string;
email: string;
mobile: string;
channel: number;
cityId: number;
hobby: string[] | null;
pid: number;
level: number;
tree: string;
remark: string;
status: number;
createdBy: number;
createdAt: string;
updatedAt: string;
}
export const defaultState = {
id: 0,
memberId: 0,
categoryId: 0,
flag: [1],
title: '',
content: '',
image: '',
images: null,
attachfile: '',
attachfiles: null,
map: null,
star: 0,
description: '',
price: 0,
views: 0,
activityAt: '',
startAt: null,
endAt: null,
switch: 0,
sort: 0,
avatar: '',
sex: 0,
qq: '',
email: '',
mobile: '',
channel: 0,
cityId: 0,
hobby: null,
pid: 0,
level: 1,
tree: '',
remark: '',
status: 1,
createdBy: 0,
createdAt: '',
updatedAt: '',
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export const rules = {
title: {
required: true,
trigger: ['blur', 'input'],
message: '请输入标题',
},
price: {
required: true,
trigger: ['blur', 'input'],
validator: validate.amount,
},
qq: {
required: false,
trigger: ['blur', 'input'],
validator: validate.qq,
},
email: {
required: true,
trigger: ['blur', 'input'],
validator: validate.email,
},
mobile: {
required: true,
trigger: ['blur', 'input'],
validator: validate.phone,
},
};
export const schemas = ref<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: 'price',
labelMessage: '我是自定义提示',
component: 'NInput',
label: '价格',
componentProps: {
pair: true,
separator: '-',
clearable: true,
placeholder: ['从', '到'],
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'activityAt',
component: 'NDatePicker',
label: '活动时间',
componentProps: {
type: 'date',
clearable: true,
shortcuts: defShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'flag',
component: 'NCheckbox',
label: '标签',
giProps: {
span: 1,
},
componentProps: {
placeholder: '请选择标签',
options: dict.getOption('sys_notice_type'),
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'switch',
component: 'NRadioGroup',
label: '开关',
giProps: {
//span: 24,
},
componentProps: {
options: dict.getOption('sys_switch'),
onUpdateChecked: (e: any) => {
console.log(e);
},
},
},
{
field: 'hobby',
component: 'NSelect',
label: '爱好',
defaultValue: null,
componentProps: {
multiple: true,
placeholder: '请选择爱好',
options: dict.getOption('sys_user_hobby'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择类型',
options: dict.getOption('sys_normal_disable'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: 'ID',
key: 'id',
sorter: true, // 单列排序
},
{
title: '标题',
key: 'title',
render(row) {
return row.title;
},
sorter: {
multiple: 1, // 为多列排序的优先级,越高优先级越高
},
},
{
title: '标签',
key: 'flag',
render(row) {
if (isNullObject(row.flag)) {
return ``;
}
return row.flag.map((tagKey) => {
return renderOptionTag('sys_notice_type', tagKey);
});
},
},
{
title: '单图',
key: 'image',
render(row) {
return h(NImage, {
width: 32,
height: 32,
src: row.image,
fallbackSrc: fallbackSrc(),
style: {
width: '32px',
height: '32px',
'max-width': '100%',
'max-height': '100%',
},
});
},
},
{
title: '多图',
key: 'images',
render(row) {
if (isNullObject(row.images)) {
return ``;
}
return row.images.map((image) => {
return h(NImage, {
width: 32,
height: 32,
src: image,
fallbackSrc: fallbackSrc(),
style: {
width: '32px',
height: '32px',
'max-width': '100%',
'max-height': '100%',
'margin-left': '2px',
},
});
});
},
},
{
title: '附件',
key: 'attachfile',
render(row) {
if (row.attachfile === '') {
return ``;
}
return h(
NAvatar,
{
size: 'small',
},
{
default: () => getFileExt(row.attachfile),
}
);
},
},
{
title: '多附件',
key: 'attachfiles',
render(row) {
if (isNullObject(row.attachfiles)) {
return ``;
}
return row.attachfiles.map((attachfile) => {
return h(
NAvatar,
{
size: 'small',
style: {
'margin-left': '2px',
},
},
{
default: () => getFileExt(attachfile),
}
);
});
},
},
{
title: '推荐星',
key: 'star',
// width: 180,
render(row) {
return h(NRate, {
allowHalf: true,
readonly: true,
defaultValue: row.star,
});
},
},
{
title: '描述',
key: 'description',
},
{
title: '价格',
key: 'price',
sorter: {
multiple: 2, // 为多列排序的优先级,越高优先级越高
},
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'success',
bordered: false,
},
{
default: () => row.price.toFixed(2),
}
);
},
},
{
title: '开关',
key: 'switch',
width: 100,
render(row) {
return h(NSwitch, {
value: row.switch === 1,
checked: '开启',
unchecked: '关闭',
onUpdateValue: function (e) {
row.switch = e ? 1 : 2;
Switch({ id: row.id, key: 'switch', value: row.switch }).then((_res) => {
$message.success('操作成功');
});
},
});
},
},
// {
// title: '排序',
// key: 'sort',
// },
{
title: '状态',
key: 'status',
render(row) {
return renderOptionTag('sys_normal_disable', row.status);
},
},
{
title: '爱好',
key: 'hobby',
render(row) {
return renderOptionTag('sys_user_hobby', row.hobby);
},
},
{
title: '活动时间',
key: 'activityAt',
render(row) {
return formatToDate(row.activityAt);
},
sorter: {
multiple: 3, // 为多列排序的优先级,越高优先级越高
},
},
];
export function loadOptions() {
dict.loadOptions([
'sys_normal_disable',
'sys_user_sex',
'sys_notice_type',
'sys_switch',
'sys_user_hobby',
'sys_user_channel',
]);
}
+171
View File
@@ -0,0 +1,171 @@
import { h, ref } from 'vue';
import { NAvatar, NImage } from 'naive-ui';
import { getFileExt } from '@/utils/urlUtils';
import { FormSchema } from '@/components/Form';
import { fallbackSrc } from '@/utils/hotgo';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { renderOptionTag } from '@/utils';
const dict = useDictStore();
export const schemas = ref<FormSchema[]>([
{
field: 'drive',
component: 'NSelect',
label: '上传驱动',
defaultValue: null,
componentProps: {
placeholder: '请选择上传驱动',
options: dict.getOption('config_upload_drive'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'kind',
component: 'NSelect',
label: '上传类型',
defaultValue: null,
componentProps: {
placeholder: '请选择上传类型',
options: dict.getOption('AttachmentKindOption'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'name',
component: 'NInput',
label: '文件名称',
labelMessage: '支持模糊查询',
componentProps: {
placeholder: '请输入文件名称',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入文件名称', trigger: ['blur'] }],
},
{
field: 'updatedAt',
component: 'NDatePicker',
label: '上传时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'member_id',
component: 'NInput',
label: '用户ID',
componentProps: {
placeholder: '请输入用户ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入用户ID', trigger: ['blur'] }],
},
]);
export const columns = [
{
title: '附件ID',
key: 'id',
width: 80,
},
{
title: '应用',
key: 'appId',
width: 100,
},
{
title: '驱动',
key: 'drive',
render(row) {
return row.drive;
},
width: 100,
},
{
title: '上传类型',
key: 'kind',
render(row) {
return renderOptionTag('AttachmentKindOption', row.kind);
},
width: 120,
},
{
title: '文件名称',
key: 'name',
width: 150,
},
{
title: '文件',
key: 'fileUrl',
width: 80,
render(row) {
if (row.fileUrl === '') {
return ``;
}
if (row.kind !== 'image') {
return h(
NAvatar,
{
width: '40px',
height: '40px',
'max-width': '100%',
'max-height': '100%',
},
{
default: () => getFileExt(row.fileUrl),
}
);
}
return h(NImage, {
width: 40,
height: 40,
src: row.fileUrl,
fallbackSrc: fallbackSrc(),
style: {
width: '40px',
height: '40px',
'max-width': '100%',
'max-height': '100%',
},
});
},
},
{
title: '文件大小',
key: 'sizeFormat',
width: 100,
},
{
title: '扩展名',
key: 'ext',
width: 80,
},
{
title: '扩展类型',
key: 'mimeType',
width: 200,
},
{
title: '上传时间',
key: 'updatedAt',
width: 180,
},
];
export function loadOptions() {
dict.loadOptions(['AttachmentKindOption', 'config_upload_drive']);
}
+239
View File
@@ -0,0 +1,239 @@
<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
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="scrollX"
:resizeHeightOffset="-20000"
>
<template #tableTitle>
<n-button type="primary" @click="handleUpload" class="ml-2">
<template #icon>
<n-icon>
<UploadOutlined />
</n-icon>
</template>
上传文件
</n-button>
<n-button type="success" @click="handleMultipartUpload" class="ml-2">
<template #icon>
<n-icon>
<FileAddOutlined />
</n-icon>
</template>
上传大文件
</n-button>
<n-button type="primary" @click="handleUploadImage" class="ml-2">
<template #icon>
<n-icon>
<FileImageOutlined />
</n-icon>
</template>
上传图片
</n-button>
<n-button type="primary" @click="handleUploadDoc" class="ml-2">
<template #icon>
<n-icon>
<FileWordOutlined />
</n-icon>
</template>
上传文档
</n-button>
<n-button type="success" @click="showUrlModal" class="ml-2">
<template #icon>
<n-icon>
<FileImageOutlined />
</n-icon>
</template>
链接转图片
</n-button>
<n-button type="error" @click="batchDelete" :disabled="batchDeleteDisabled" class="ml-2">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
</n-card>
<FileUpload ref="fileUploadRef" :finish-call="handleFinishCall" />
<FileUpload ref="imageUploadRef" :finish-call="handleFinishCall" upload-type="image" />
<FileUpload ref="docUploadRef" :finish-call="handleFinishCall" upload-type="doc" />
<MultipartUpload ref="multipartUploadRef" @on-finish="handleFinishCall" />
<UrlModal ref="urlModalRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { Delete, List } from '@/api/apply/attachment';
import { columns, schemas, loadOptions } from './columns';
import {
DeleteOutlined,
UploadOutlined,
FileWordOutlined,
FileImageOutlined,
FileAddOutlined,
} from '@vicons/antd';
import FileUpload from '@/components/FileChooser/src/Upload.vue';
import MultipartUpload from '@/components/Upload/multipartUpload.vue';
import { Attachment } from '@/components/FileChooser/src/model';
import UrlModal from './urlModal.vue';
import { adaTableScrollX } from '@/utils/hotgo';
const message = useMessage();
const actionRef = ref();
const dialog = useDialog();
const searchFormRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const fileUploadRef = ref();
const imageUploadRef = ref();
const docUploadRef = ref();
const multipartUploadRef = ref();
const urlModalRef =ref();
const actionColumn = reactive({
width: 132,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '下载',
onClick: handleDown.bind(null, record),
type: 'default',
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 100,
schemas,
});
function handleUpload() {
fileUploadRef.value.openModal();
}
function handleUploadImage() {
imageUploadRef.value.openModal();
}
function handleUploadDoc() {
docUploadRef.value.openModal();
}
function handleMultipartUpload() {
multipartUploadRef.value.openModal();
}
const loadDataTable = async (res) => {
return await List({ ...res, ...searchFormRef.value?.formModel });
};
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function handleDown(record: Recordable) {
window.open(record.fileUrl);
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleSubmit(_values: Recordable) {
reloadTable();
}
function handleReset(_values: Recordable) {
reloadTable();
}
function handleFinishCall(result: Attachment, success: boolean) {
if (success) {
reloadTable();
}
}
function showUrlModal() {
urlModalRef.value?.showModal();
}
onMounted(async () => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,90 @@
<template>
<div>
<n-modal
v-model:show="isShowModal"
:style="{
width: dialogWidth,
}"
:show-icon="false"
preset="dialog"
title="链接转图片"
>
<n-alert type="info"> 将外部图片链接下载并转存至平台的存储驱动中 </n-alert>
<n-form
:model="formParams"
ref="formPacketRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-spin :show="loading" description="请稍候...">
<n-form-item label="图片链接" path="url">
<n-input v-model:value="formParams.url" />
</n-form-item>
<n-form-item label="预览图片" v-if="formParams.url != ''">
<n-image width="150" :src="formParams.url" />
</n-form-item>
</n-spin>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">关闭</n-button>
<n-button type="primary" :loading="formBtnLoading" @click="confirmForm">上传 </n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { adaModalWidth } from '@/utils/hotgo';
import { useMessage } from 'naive-ui';
import { ImageTransferStorage } from '@/api/apply/attachment';
const emit = defineEmits(['reloadTable']);
const loading = ref(false);
const isShowModal = ref(false);
const dialogWidth = ref(adaModalWidth(640));
const formBtnLoading = ref(false);
const formPacketRef = ref();
const message = useMessage();
const formParams = ref({
url: '',
});
function reloadTable() {
emit('reloadTable');
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
ImageTransferStorage(formParams.value)
.then((_res) => {
message.success('操作成功');
setTimeout(() => {
reloadTable();
closeForm();
});
})
.finally(() => {
formBtnLoading.value = false;
});
}
function closeForm() {
isShowModal.value = false;
}
function showModal() {
isShowModal.value = true;
formParams.value.url = '';
}
defineExpose({ showModal });
</script>
<style lang="less"></style>
+106
View File
@@ -0,0 +1,106 @@
import { h } from 'vue';
import { NAvatar, NAvatarGroup, NTooltip } from 'naive-ui';
import { renderOptionTag } from '@/utils';
export const columns = [
{
title: 'ID',
key: 'id',
width: 80,
},
{
title: '消息标题',
key: 'title',
render(row) {
return h('p', { id: 'app' }, [
h('div', {
innerHTML: '<div style="white-space: pre-wrap">' + row.title + '</div>',
}),
]);
},
width: 280,
},
{
title: '消息类型',
key: 'type',
render(row) {
return renderOptionTag('noticeTypeOptions', row.type);
},
width: 100,
},
{
title: '标签',
key: 'tag',
render(row) {
return renderOptionTag('noticeTagOptions', row.tag);
},
width: 100,
},
{
title: '接收人',
key: 'receiver',
render(row) {
if (row.type === 1 || row.type === 2) {
return '所有人';
}
return h(
NAvatarGroup,
{
max: 4,
size: 40,
options: row.receiverGroup,
},
{
avatar: (column) =>
h(NTooltip, null, {
trigger: () =>
column.option.src !== ''
? h(NAvatar, {
src: column.option.src,
round: true,
size: 32,
style: {
marginRight: '4px',
},
})
: h(
NAvatar,
{
round: true,
size: 32,
style: {
marginRight: '4px',
},
},
{
default: () => column.option.name?.substring(0, 1) as string,
}
),
default: () => column.option.name,
}),
}
);
},
width: 180,
},
{
title: '阅读量',
key: 'readCount',
width: 80,
},
{
title: '排序',
key: 'sort',
width: 80,
},
{
title: '备注',
key: 'remark',
width: 150,
},
{
title: '发送时间',
key: 'createdAt',
width: 180,
},
];
+215
View File
@@ -0,0 +1,215 @@
<template>
<div>
<n-drawer v-model:show="showModal" :width="dialogWidth">
<n-drawer-content closable>
<template #header>
{{
formValue.id > 0
? '编辑' + dict.getLabel('noticeTypeOptions', formValue.type) + ' #' + formValue.id
: '发送' + dict.getLabel('noticeTypeOptions', formValue.type)
}}
</template>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-spin :show="loading" description="请稍候...">
<n-alert :show-icon="false" type="info">
消息发送成功后如果接收人在线会立即收到一条消息通知,编辑已发送的消息不会再次通知
</n-alert>
<n-form
:model="formValue"
:rules="rules"
ref="formRef"
:label-placement="settingStore.isMobile ? 'top' : 'left'"
:label-width="80"
class="py-4"
>
<n-form-item label="消息标题" path="title">
<n-input placeholder="请输入消息标题" v-model:value="formValue.title" />
</n-form-item>
<n-form-item label="接收人" path="receiver" v-if="formValue.type === 3">
<n-select
multiple
:options="options"
:render-label="renderLabel"
:render-tag="renderMultipleSelectTag"
v-model:value="formValue.receiver"
filterable
/>
</n-form-item>
<n-form-item label="消息内容" path="content">
<template v-if="formValue.type === 1">
<n-input
type="textarea"
:autosize="{ minRows: 3, maxRows: 10 }"
placeholder="请输入通知内容"
v-model:value="formValue.content"
/>
</template>
<template v-else>
<Editor style="height: 320px" v-model:value="formValue.content" />
</template>
</n-form-item>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="标签" path="tag">
<n-select
clearable
placeholder="可以不填"
:render-tag="renderTag"
v-model:value="formValue.tag"
:options="dict.getOptionUnRef('noticeTagOptions')"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="排序" path="sort">
<n-input-number style="width: 100%" v-model:value="formValue.sort" clearable />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="formValue.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="formValue.remark"
/>
</n-form-item>
</n-form>
</n-spin>
</n-scrollbar>
<template #footer>
<n-space>
<n-button @click="closeForm"> 取消 </n-button>
<n-button type="primary" :loading="formBtnLoading" @click="confirmForm">
立即发送
</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useDictStore } from '@/store/modules/dict';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { personOption, renderLabel, renderMultipleSelectTag } from '@/enums/systemMessageEnum';
import Editor from '@/components/Editor/editor.vue';
import { statusOptions } from '@/enums/optionsiEnum';
import { GetMemberOption } from '@/api/org/user';
import { MaxSort, EditLetter, EditNotice, EditNotify } from '@/api/apply/notice';
import { renderTag } from '@/utils';
import { adaModalWidth } from '@/utils/hotgo';
import { State, newState, rules } from './model';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const dict = useDictStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const options = ref<personOption[]>();
const dialogWidth = ref(adaModalWidth(840));
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
switch (formValue.value.type) {
case 1:
EditNotify(formValue.value).then((_res) => {
confirmComplete();
});
break;
case 2:
EditNotice(formValue.value).then((_res) => {
confirmComplete();
});
break;
case 3:
EditLetter(formValue.value).then((_res) => {
confirmComplete();
});
break;
default:
message.error('公告类型不支持');
}
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function confirmComplete() {
message.success('操作成功');
setTimeout(() => {
closeForm();
emit('reloadTable');
});
}
function getMemberOption() {
GetMemberOption().then((res) => {
options.value = res;
});
}
// 关闭抽屉
function closeForm() {
showModal.value = false;
loading.value = false;
}
// 打开抽屉
function openModal(state: State, type: number) {
showModal.value = true;
dialogWidth.value = adaModalWidth(840);
getMemberOption();
// 新增
if (!state || state.id < 1) {
formValue.value = newState(state);
formValue.value.type = type;
loading.value = true;
MaxSort()
.then((res) => {
formValue.value.sort = res.sort;
})
.finally(() => {
loading.value = false;
});
return;
}
// 编辑
formValue.value = newState(state);
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>
+244
View File
@@ -0,0 +1,244 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="通知公告">
在这里,您可以向平台中的用户发送通知、公告和私信消息
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<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
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="scrollX"
:resizeHeightOffset="-20000"
>
<template #tableTitle>
<n-button
type="warning"
@click="addTable(1)"
class="min-left-space"
v-if="hasPermission(['/notice/editNotify'])"
>
<template #icon>
<n-icon>
<NotificationOutlined />
</n-icon>
</template>
发通知
</n-button>
<n-button
type="error"
@click="addTable(2)"
class="min-left-space"
v-if="hasPermission(['/notice/editNotice'])"
>
<template #icon>
<n-icon>
<BellOutlined />
</n-icon>
</template>
发公告
</n-button>
<n-button
type="info"
@click="addTable(3)"
class="min-left-space"
v-if="hasPermission(['/notice/editLetter'])"
>
<template #icon>
<n-icon>
<SendOutlined />
</n-icon>
</template>
发私信
</n-button>
<n-button
type="error"
@click="batchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
v-if="hasPermission(['/notice/delete'])"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
</template>
</BasicTable>
</n-card>
<Edit ref="editRef" @reload-table="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { Delete, List, Status } from '@/api/apply/notice';
import { BellOutlined, DeleteOutlined, NotificationOutlined, SendOutlined } from '@vicons/antd';
import { adaTableScrollX } from '@/utils/hotgo';
import { usePermission } from '@/hooks/web/usePermission';
import { useDictStore } from '@/store/modules/dict';
import { columns } from './columns';
import { schemas, loadOptions } from './model';
import Edit from './edit.vue';
const { hasPermission } = usePermission();
const message = useMessage();
const dict = useDictStore();
const actionRef = ref();
const dialog = useDialog();
const searchFormRef = ref<any>({});
const editRef = ref();
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const actionColumn = reactive({
width: 200,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '已启用',
onClick: handleStatus.bind(null, record, 2),
ifShow: () => {
return record.status === 1;
},
auth: ['/notice/status'],
},
{
label: '已禁用',
onClick: handleStatus.bind(null, record, 1),
ifShow: () => {
return record.status === 2;
},
auth: ['/notice/status'],
},
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/notice/edit'],
type: 'primary',
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/notice/delete'],
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function addTable(type) {
editRef.value.openModal(null, type);
}
const loadDataTable = async (res) => {
return await List({ ...res, ...searchFormRef.value?.formModel });
};
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function reloadTable() {
actionRef.value.reload();
}
function handleEdit(record: Recordable) {
editRef.value.openModal(record, record.type);
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function batchDelete() {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleSubmit(_values: Recordable) {
reloadTable();
}
function handleReset(_values: Recordable) {
reloadTable();
}
function handleStatus(record: Recordable, status: number) {
Status({ id: record.id, status: status }).then((_res) => {
message.success('操作成功');
setTimeout(() => {
reloadTable();
});
});
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
+113
View File
@@ -0,0 +1,113 @@
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();
export class State {
id: number;
title: string;
type: number;
tag: number = 1;
content: string;
receiver: number[];
remark: string;
sort: number;
status: number = 1;
createdBy: number;
updatedBy: number;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
readCount: number;
receiverGroup: Receiver[];
constructor(state?: Partial<State>) {
if (state) {
Object.assign(this, state);
}
}
}
export class Receiver {
name: string;
src: string;
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) {
return cloneDeep(state);
}
return new State(state);
}
return new State();
}
// 表单验证规则
export const rules = {
title: {
required: true,
trigger: ['blur', 'input'],
message: '请输入消息标题',
},
};
// 表格搜索表单
export const schemas: FormSchema[] = [
{
field: 'type',
component: 'NSelect',
label: '消息类型',
defaultValue: null,
componentProps: {
placeholder: '请选择消息类型',
options: dict.getOption('noticeTypeOptions'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
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: dict.getOption('sys_normal_disable'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
// 加载字典数据选项
export function loadOptions() {
dict.loadOptions(['sys_normal_disable']);
}
+191
View File
@@ -0,0 +1,191 @@
<template>
<div>
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
:title="isUpdate ? '编辑 #' + params?.id : '添加'"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="params"
: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="params.pid" />
</n-form-item>
<n-form-item label="地区ID" path="id">
<n-input-number
style="width: 100%"
placeholder="请输入地区ID"
v-model:value="params.id"
:disabled="isUpdate"
path="handleChangeId"
/>
</n-form-item>
<n-form-item label="地区名称" path="title">
<n-input placeholder="请输入地区名称" v-model:value="params.title" />
</n-form-item>
<n-form-item label="拼音" path="pinyin">
<n-input placeholder="请输入拼音" v-model:value="params.pinyin" />
</n-form-item>
<n-grid x-gap="24" :cols="2">
<n-gi>
<n-form-item label="经度" path="lng">
<n-input placeholder="经度" v-model:value="params.lng" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="纬度" path="lat">
<n-input placeholder="纬度" v-model:value="params.lat" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="排序" path="sort">
<n-input-number v-model:value="params.sort" clearable />
</n-form-item>
<n-form-item label="状态" path="status">
<n-radio-group v-model:value="params.status" name="status">
<n-radio-button
v-for="status in dict.getOptionUnRef('sys_normal_disable')"
:key="Number(status.value)"
:value="Number(status.value)"
:label="status.label"
/>
</n-radio-group>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { State, newState } from './model';
import { Edit, MaxSort, CheckProvincesUniqueId } from '@/api/apply/provinces';
import { FormItemRule, useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { useDictStore } from '@/store/modules/dict';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
optionTreeData: any;
isUpdate: boolean;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
optionTreeData: [],
isUpdate: false,
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const params = computed(() => {
return props.formParams;
});
const rules = {
id: {
required: true,
async validator(rule: FormItemRule, value: string, callback: Function) {
if (!value) {
callback(new Error('请填写地区ID'));
} else if (!/^\d*$/.test(value)) {
callback(new Error('地区ID应该为整数'));
} else if (!(await isUniqueId(value))) {
callback(new Error('地区ID已存在'));
} else {
callback();
}
},
trigger: ['input', 'blur'],
},
title: {
required: true,
trigger: ['blur', 'input'],
message: '请输入地区名称',
},
};
const dict = useDictStore();
const message = useMessage();
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Edit(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
isShowModal.value = false;
}
watch(
() => params.value,
(value) => {
params.value.oldId = Number(value.id);
if (value.id === 0 || value.id === null) {
MaxSort().then((res) => {
params.value.sort = res.sort;
});
}
}
);
async function isUniqueId(newId: any) {
const res = await CheckProvincesUniqueId({ oldId: params.value.oldId, newId: newId });
return res.unique;
}
</script>
<style lang="less"></style>
+232
View File
@@ -0,0 +1,232 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="省市区"> 中国省市区编码对照表 </n-card>
</div>
<n-grid 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" class="proCard">
<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 === null || 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 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="height: 75vh"
@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" class="proCard">
<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" :optionTreeData="optionTreeData" @reload-table="loadData" />
</n-card>
</n-gi>
</n-grid>
<Edit
@reload-table="loadData"
@update-show-modal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
:optionTreeData="optionTreeData"
:isUpdate="isUpdate"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, unref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import {
AlignLeftOutlined,
FormOutlined,
PlusOutlined,
EditOutlined,
SearchOutlined,
DeleteOutlined,
} from '@vicons/antd';
import List from './list.vue';
import { getProvincesTree, Delete } from '@/api/apply/provinces';
import Edit from './edit.vue';
import { newState, loadOptions } from './model';
const isUpdate = ref(false);
const showModal = ref(false);
const message = useMessage();
const dialog = useDialog();
let treeItemKey = ref([]);
let expandedKeys = ref([]);
const treeData = ref([]);
const loading = ref(true);
const treeItemTitle = ref('');
const checkedId = ref(0);
const pattern = ref('');
const optionTreeData = ref<any>([]);
const formParams = ref(newState(null));
function openCreateDrawer() {
showModal.value = true;
formParams.value = newState(null);
isUpdate.value = false;
}
function openEditDrawer() {
showModal.value = true;
isUpdate.value = true;
}
function selectedTree(keys, opts) {
if (keys.length) {
const treeItem = opts[0];
treeItemKey.value = keys;
treeItemTitle.value = treeItem.label;
formParams.value = newState(treeItem);
checkedId.value = treeItem.id;
} else {
treeItemKey.value = [];
treeItemTitle.value = '';
}
}
function handleDel() {
dialog.info({
title: '提示',
content: `您确定想删除吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ ...formParams.value }).then(async (_res) => {
message.success('操作成功');
await loadData();
});
},
onNegativeClick: () => {
message.error('已取消');
},
});
}
function packHandle() {
if (expandedKeys.value.length) {
expandedKeys.value = [];
} else {
expandedKeys.value = unref(treeData).map((item: any) => item.key as string) as [];
}
}
async function loadData() {
const treeMenuList = await getProvincesTree();
Object.assign(
formParams,
treeMenuList.list.map((item) => item.key)
);
treeData.value = [];
optionTreeData.value = [
{
id: 0,
key: 0,
label: '顶级地区',
pid: 0,
title: '顶级地区',
},
];
treeData.value = treeMenuList.list;
optionTreeData.value = optionTreeData.value.concat(treeMenuList.list);
loading.value = false;
}
function onExpandedKeys(keys) {
expandedKeys.value = keys;
}
function updateShowModal(value) {
showModal.value = value;
}
onMounted(async () => {
loadOptions();
await loadData();
});
</script>
+220
View File
@@ -0,0 +1,220 @@
<template>
<n-card :bordered="false" class="proCard">
<n-result
v-show="checkedId <= 0"
status="info"
title="提示"
description="请选择一个想要编辑的省市区"
/>
<div v-show="checkedId > 0">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:columns="listColumns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加数据
</n-button>
</template>
</BasicTable>
</div>
<Edit
@reloadTable="reloadTable"
@updateShowModal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
:optionTreeData="optionTreeData"
:isUpdate="isUpdate"
/>
</n-card>
</template>
<script lang="ts" setup>
import { h, reactive, ref, watch, computed } from 'vue';
import { useMessage, useDialog } from 'naive-ui';
import { BasicColumn, BasicTable, TableAction } from '@/components/Table';
import { BasicForm, FormSchema, useForm } from '@/components/Form/index';
import { listColumns, newState, State } from './model';
import { PlusOutlined } from '@vicons/antd';
import { getProvincesChildrenList, Delete } from '@/api/apply/provinces';
import Edit from './edit.vue';
import { adaTableScrollX } from '@/utils/hotgo';
const emit = defineEmits(['reloadTable']);
interface Props {
checkedId?: number;
optionTreeData: any;
}
const props = withDefaults(defineProps<Props>(), { checkedId: 0, optionTreeData: [] });
const searchFormRef = ref<any>({});
const message = useMessage();
const dialog = useDialog();
const actionRef = ref();
const isUpdate = ref(false);
const showModal = ref(false);
const formParams = ref<State>(newState(null));
const params = ref({
pageSize: 10,
pid: props.checkedId,
label: '',
});
const schemas: FormSchema[] = [
{
field: 'id',
component: 'NInput',
label: '地区ID',
componentProps: {
placeholder: '请输入地区ID',
onInput: (e: any) => {
console.log(e);
params.value.label = e;
},
},
},
{
field: 'title',
component: 'NInput',
label: '地区名称',
componentProps: {
placeholder: '请输入地区名称',
onInput: (e: any) => {
console.log(e);
params.value.label = e;
},
},
},
];
const actionColumn = reactive<BasicColumn>({
width: 150,
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 scrollX = computed(() => {
return adaTableScrollX(listColumns, actionColumn.width);
});
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 = newState(null);
formParams.value.pid = props.checkedId;
isUpdate.value = false;
}
const loadDataTable = async (res) => {
if (props.checkedId <= 0) {
return [];
}
return await getProvincesChildrenList({
...{ pid: props.checkedId },
...searchFormRef.value?.formModel,
...res,
});
};
function onCheckedRow(rowKeys) {
console.log(rowKeys);
}
function reloadTable() {
actionRef.value.reload();
emit('reloadTable');
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '您确定想删除吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleEdit(record: Recordable) {
showModal.value = true;
formParams.value = newState(record as State);
isUpdate.value = true;
}
function handleSubmit(_values: Recordable) {
reloadTable();
}
function handleReset(_values: Recordable) {
params.value.label = '';
reloadTable();
}
watch(props, (_newVal, _oldVal) => {
if (params.value.pid === _newVal.checkedId) {
return;
}
params.value.pid = _newVal.checkedId;
formParams.value.pid = Number(_newVal.checkedId);
if (_newVal.checkedId > 0) {
reloadTable();
}
});
function updateShowModal(value) {
showModal.value = value;
}
</script>
<style lang="less" scoped></style>
+87
View File
@@ -0,0 +1,87 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { useDictStore } from '@/store/modules/dict';
import { renderOptionTag } from '@/utils';
const dict = useDictStore();
export const listColumns = [
{
title: '地区ID',
key: 'id',
},
{
title: '地区名称',
key: 'title',
},
{
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 renderOptionTag('sys_normal_disable', row.status);
},
},
];
export interface State {
id: number | null;
title: string;
pinyin: string;
lng: string;
lat: string;
pid: number;
sort: number;
status: number;
oldId: number;
}
export const defaultState = {
id: null,
title: '',
pinyin: '',
lng: '',
lat: '',
pid: 0,
sort: 0,
status: 1,
oldId: 0,
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
export function loadOptions() {
dict.loadOptions(['sys_normal_disable']);
}
+99
View File
@@ -0,0 +1,99 @@
import { h } from 'vue';
import { NTag } from 'naive-ui';
import { renderPopoverMemberSumma } from '@/utils';
const msgMap = {
1: '处理中',
2: '提现成功',
3: ' 提现异常',
};
export const statusOptions = [
{
value: 1,
label: '处理中',
},
{
value: 2,
label: '提现成功',
},
{
value: 3,
label: '提现异常',
},
];
export const columns = [
{
title: '提现ID',
key: 'id',
width: 100,
},
{
title: '申请人',
key: 'memberId',
width: 100,
render(row) {
return renderPopoverMemberSumma(row.memberBySumma);
},
},
{
title: '提现金额',
key: 'money',
render(row) {
return row.money.toFixed(2);
},
width: 100,
},
{
title: '手续费',
key: 'fee',
render(row) {
return row.fee.toFixed(2);
},
width: 100,
},
{
title: '最终到账',
key: 'lastMoney',
render(row) {
return row.lastMoney.toFixed(2);
},
width: 100,
},
{
title: '处理结果',
key: 'msg',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: row.status == 1 ? 'info' : row.status == 2 ? 'success' : 'warning',
bordered: false,
},
{
default: () => (row.msg == '' ? msgMap[row.status] : row.msg),
}
);
},
width: 200,
},
{
title: '申请IP',
key: 'ip',
width: 180,
},
{
title: '处理时间',
key: 'handleAt',
width: 180,
},
{
title: '申请时间',
key: 'createdAt',
width: 180,
},
];
+37
View File
@@ -0,0 +1,37 @@
<template>
<div>
<n-card :bordered="false" class="proCard" title="提现管理">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane name="" tab="全部"> <List :type="defaultTab" /></n-tab-pane>
<n-tab-pane name="1" tab="处理中"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="2" tab="提现成功"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="3" tab="提现异常"> <List :type="defaultTab" /> </n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const defaultTab = ref('');
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
});
function handleBeforeLeave(tabName: string) {
defaultTab.value = tabName;
}
</script>
+408
View File
@@ -0,0 +1,408 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<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"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button type="primary" @click="addTable" class="min-left-space">
<template #icon>
<n-icon>
<MoneyCollectOutlined />
</n-icon>
</template>
申请提现
</n-button>
<n-button type="default" @click="setCash" class="min-left-space">
<template #icon>
<n-icon>
<EditOutlined />
</n-icon>
</template>
设置提现账户
</n-button>
</template>
</BasicTable>
<n-modal
v-model:show="showModal"
:show-icon="false"
preset="dialog"
title="申请提现"
:style="{
width: dialogWidth,
}"
>
<n-alert type="info">
<div v-html="config.cashTips"></div>
</n-alert>
<n-form
:model="formParams"
ref="formRef"
label-placement="left"
:label-width="100"
class="py-4"
>
<n-form-item label="可提现金额">
<n-input v-model:value="newUserInfo.balance" disabled />
<template #feedback
><p>{{ estimated }}</p></template
>
</n-form-item>
<br />
<n-form-item label="提现金额" path="money">
<n-input-number v-model:value="formParams.money" :min="1" :max="newUserInfo.balance">
<template #minus-icon>
<n-icon :component="ArrowDownCircleOutline" />
</template>
<template #add-icon>
<n-icon :component="ArrowUpCircleOutline" />
</template>
</n-input-number>
</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-modal
v-model:show="showPaymentModal"
:show-icon="false"
preset="dialog"
title="处理打款"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="paymentParams"
ref="PaymentRef"
label-placement="left"
:label-width="100"
class="py-4"
>
<n-form-item label="最终到账金额">
<n-input v-model:value="paymentParams.lastMoney" disabled />
</n-form-item>
<n-form-item label="收款信息">
<n-input v-model:value="paymentParams.accountInfo" disabled />
</n-form-item>
<n-form-item label="收款码">
<n-carousel draggable>
<img style="width: 200px" class="carousel-img" :src="paymentParams.payeeCode" />
</n-carousel>
</n-form-item>
<n-form-item label="变更提现状态" path="status">
<n-radio-group v-model:value="paymentParams.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="处理结果">
<n-input v-model:value="paymentParams.msg" />
<template #feedback>不填默认显示提现状态</template>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showPaymentModal = false)">取消</n-button>
<n-button type="info" :loading="PaymentBtnLoading" @click="confirmPayment"
>确定</n-button
>
</n-space>
</template>
</n-modal>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { computed, 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 { Apply, List, Payment, View } from '@/api/cash';
import { columns, statusOptions } from './columns';
import { ArrowDownCircleOutline, ArrowUpCircleOutline } from '@vicons/ionicons5';
import { MoneyCollectOutlined, EditOutlined } from '@vicons/antd';
import { defRangeShortcuts, timestampToTime } from '@/utils/dateUtil';
import { useRouter } from 'vue-router';
import { getUserInfo } from '@/api/system/user';
import { getCashConfig } from '@/api/sys/config';
import { adaTableScrollX } from '@/utils/hotgo';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '',
});
const router = useRouter();
const params = ref<any>({
pageSize: 10,
title: '',
content: '',
status: null,
});
const estimated = ref(
'本次提现预计将在 ' +
timestampToTime(new Date().setTime(new Date().getTime() + 86400 * 4 * 1000) / 1000) +
' 前到账 (1-3个工作日,双休日和法定节假日顺延)'
);
const schemas: FormSchema[] = [
{
field: 'complexMemberId',
component: 'ComplexMemberPicker',
label: '申请人',
componentProps: {
placeholder: '请选择申请人',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '申请IP',
componentProps: {
placeholder: '请输入申请IP',
onUpdateValue: (e: any) => {
console.log(e);
},
},
rules: [{ message: '请输入申请IP', trigger: ['blur'] }],
},
{
field: 'created_at',
component: 'NDatePicker',
label: '申请时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
];
const newUserInfo = ref({ balance: 0 });
const message = useMessage();
const actionRef = ref();
const showModal = ref(false);
const showPaymentModal = ref(false);
const PaymentRef = ref<any>({});
const PaymentBtnLoading = ref(false);
const formBtnLoading = ref(false);
const searchFormRef = ref<any>({});
const formRef = ref<any>({});
const config = ref<any>({
cashMinFee: 3,
cashMinFeeRatio: '0.03',
cashMinMoney: 0,
cashSwitch: false,
cashTips: '',
});
const resetFormParams = {
money: null,
accountInfo: null,
};
let formParams = ref<any>(resetFormParams);
const resetPaymentParams = {
id: null,
money: null,
};
let paymentParams = ref<any>(resetPaymentParams);
const actionColumn = reactive({
auth: ['/cash/payment'],
width: 100,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '处理打款',
onClick: handleEdit.bind(null, record),
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
function setCash() {
router.push({
name: 'home_account',
query: {
type: 3,
},
});
}
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
async function addTable() {
showModal.value = true;
formParams.value = resetFormParams;
newUserInfo.value = await getUserInfo();
if (newUserInfo.value.balance < config.value.cashMinMoney) {
message.error('当前余额不满足提现条件,至少需要:' + config.value.cashMinMoney + '元');
}
}
const loadDataTable = async (res) => {
mapWidth();
config.value = await getCashConfig();
config.value = config.value.list;
return await List({
...params.value,
...res,
...searchFormRef.value.formModel,
...{ status: props.type },
});
};
function reloadTable() {
actionRef.value.reload();
}
/**
* 申请提现
* @param e
*/
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
Apply({ money: formParams.value.money }).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showModal.value = false;
reloadTable();
formParams.value = ref(resetFormParams);
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
/**
* 处理打款
* @param e
*/
function confirmPayment(e) {
e.preventDefault();
PaymentBtnLoading.value = true;
PaymentRef.value.validate((errors) => {
if (!errors) {
Payment({
id: PaymentRef.value.model.id,
status: PaymentRef.value.model.status,
msg: PaymentRef.value.model.msg,
}).then((_res) => {
message.success('操作成功');
setTimeout(() => {
showPaymentModal.value = false;
reloadTable();
PaymentRef.value = ref(resetPaymentParams);
});
});
} else {
message.error('请填写完整信息');
}
PaymentBtnLoading.value = false;
});
}
async function handleEdit(record: Recordable) {
showPaymentModal.value = true;
paymentParams.value = record;
paymentParams.value = await View({ id: record.id });
paymentParams.value.lastMoney = paymentParams.value.lastMoney.toFixed(2);
paymentParams.value.accountInfo =
paymentParams.value.name + ' - ' + paymentParams.value.account;
}
function handleSubmit(values: Recordable) {
console.log(values);
params.value = values;
reloadTable();
}
function handleReset(values: Recordable) {
params.value = values;
reloadTable();
}
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;
}
</script>
<style lang="less" scoped></style>
+46
View File
@@ -0,0 +1,46 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="资金变动">
你和下级在平台中余额、积分的变动明细都可以在这里进行查看
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane name="" tab="全部"> <List :type="defaultTab" /></n-tab-pane>
<n-tab-pane name="balance" tab="余额明细"> <List :type="defaultTab" /> </n-tab-pane>
<n-tab-pane name="integral" tab="积分明细"> <List :type="defaultTab" /> </n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
import { loadOptions } from './model';
const router = useRouter();
const defaultTab = ref('');
function handleBeforeLeave(tabName: string) {
defaultTab.value = tabName;
}
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
loadOptions();
});
</script>
<style lang="less" scoped></style>
+98
View File
@@ -0,0 +1,98 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="false"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
size="small"
>
<template #tableTitle>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/creditsLog/export'])"
>
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Export } from '@/api/creditsLog';
import { columns, schemas } from './model';
import { ExportOutlined } from '@vicons/antd';
import { adaTableScrollX } from '@/utils/hotgo';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '',
});
const scrollX = computed(() => {
return adaTableScrollX(columns, 0);
});
const { hasPermission } = usePermission();
const actionRef = ref();
const message = useMessage();
const searchFormRef = ref<any>({});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loadDataTable = async (res) => {
return await List({
...searchFormRef.value?.formModel,
...res,
...{ creditType: props.type },
});
};
function reloadTable() {
actionRef.value.reload();
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
</script>
<style lang="less" scoped></style>
+166
View File
@@ -0,0 +1,166 @@
import { ref } from 'vue';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { renderOptionTag, renderPopoverMemberSumma } from '@/utils';
const dict = useDictStore();
export const schemas = ref<FormSchema[]>([
{
field: 'complexMemberId',
component: 'ComplexMemberPicker',
label: '选择用户',
componentProps: {
placeholder: '请选择用户',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'creditGroup',
component: 'NSelect',
label: '变动组别',
defaultValue: null,
componentProps: {
placeholder: '请选择变动组别',
options: dict.getOption('creditGroup'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'remark',
component: 'NInput',
label: '备注',
componentProps: {
placeholder: '请输入备注',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '操作人IP',
componentProps: {
placeholder: '请输入操作人IP',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'id',
component: 'NInput',
label: '变动ID',
componentProps: {
placeholder: '请输入变动ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '变动时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: '变动ID',
key: 'id',
width: 100,
},
{
title: '用户',
key: 'memberId',
width: 100,
render(row) {
return renderPopoverMemberSumma(row.memberBySumma);
},
},
{
title: '变动类型',
key: 'creditType',
render(row) {
return renderOptionTag('creditType', row.creditType);
},
width: 150,
},
{
title: '变动组别',
key: 'creditGroup',
render(row) {
return renderOptionTag('creditGroup', row.creditGroup);
},
width: 150,
},
{
title: '变动前',
key: 'beforeNum',
width: 100,
render(row) {
return Number(row.beforeNum).toFixed(2);
},
},
{
title: '变动数量',
key: 'num',
width: 100,
render(row) {
return Number(row.num).toFixed(2);
},
},
{
title: '变动后',
key: 'afterNum',
width: 100,
render(row) {
return Number(row.afterNum).toFixed(2);
},
},
{
title: '备注',
key: 'remark',
width: 200,
},
{
title: '操作人IP',
key: 'ip',
width: 150,
},
{
title: '关联ID',
key: 'mapId',
width: 100,
render(row) {
if (row.mapId === 0) {
return '-';
}
return row.mapId;
},
},
{
title: '变动时间',
key: 'createdAt',
width: 180,
},
];
export function loadOptions() {
dict.loadOptions(['creditType', 'creditGroup']);
}
+89
View File
@@ -0,0 +1,89 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<div class="n-layout-page-header">
<n-card :bordered="false" title="交易退款">
<!-- 这是系统自动生成的CURD表格,你可以将此行注释改为表格的描述 -->
</n-card>
</div>
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="false"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:scroll-x="1090"
:resizeHeightOffset="-10000"
size="small"
>
<template #tableTitle>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/creditsLog/export'])"
>
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { useMessage } from 'naive-ui';
import { BasicTable } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Export } from '@/api/pay/refund';
import { columns, schemas, loadOptions } from './model';
import { ExportOutlined } from '@vicons/antd';
const { hasPermission } = usePermission();
const actionRef = ref();
const message = useMessage();
const searchFormRef = ref<any>({});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
function reloadTable() {
actionRef.value.reload();
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
onMounted(async () => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
+181
View File
@@ -0,0 +1,181 @@
import { ref } from 'vue';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { renderOptionTag } from '@/utils';
const dict = useDictStore();
export const schemas = ref<FormSchema[]>([
{
field: 'id',
component: 'NInputNumber',
label: '变动ID',
componentProps: {
placeholder: '请输入变动ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'memberId',
component: 'NInputNumber',
label: '管理员ID',
componentProps: {
placeholder: '请输入管理员ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'appId',
component: 'NInput',
label: '应用id',
componentProps: {
placeholder: '请输入应用id',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'creditType',
component: 'NSelect',
label: '变动类型',
defaultValue: null,
componentProps: {
placeholder: '请选择变动类型',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'creditGroup',
component: 'NSelect',
label: '变动的组别',
defaultValue: null,
componentProps: {
placeholder: '请选择变动的组别',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'remark',
component: 'NInput',
label: '备注',
componentProps: {
placeholder: '请输入备注',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'ip',
component: 'NInput',
label: '操作人IP',
componentProps: {
placeholder: '请输入操作人IP',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '状态',
defaultValue: null,
componentProps: {
placeholder: '请选择状态',
options: [],
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: '变动ID',
key: 'id',
},
{
title: '管理员ID',
key: 'memberId',
},
{
title: '应用id',
key: 'appId',
},
{
title: '插件名称',
key: 'addonsName',
},
{
title: '变动前',
key: 'beforeNum',
},
{
title: '变动数据',
key: 'num',
},
{
title: '变动后',
key: 'afterNum',
},
{
title: '备注',
key: 'remark',
},
{
title: '操作人IP',
key: 'ip',
},
{
title: '关联ID',
key: 'mapId',
},
{
title: '状态',
key: 'status',
render(row) {
return renderOptionTag('sys_normal_disable', row.status);
},
},
{
title: '创建时间',
key: 'createdAt',
},
{
title: '修改时间',
key: 'updatedAt',
},
];
export function loadOptions() {
dict.loadOptions(['sys_normal_disable']);
}
loadOptions();
+287
View File
@@ -0,0 +1,287 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="在线充值"> 余额可用于购买付费产品或商城消费 </n-card>
</div>
<n-spin :show="loading">
<n-grid cols="1 s:1 m:1 l:4 xl:4 2xl:4" responsive="screen" :x-gap="12">
<n-gi span="4">
<n-card :bordered="false" class="proCard">
<n-thing>
<template #description> <span class="title">充值金额</span> </template>
<n-space>
<n-button
type="primary"
ghost
v-for="item in amounts"
:key="item"
@click="SetAmount(item)"
>
¥{{ item }}
<n-icon
class="check-icon"
:size="18"
:component="CheckOutlined"
v-if="amount === item && amountType === 1"
/>
</n-button>
<n-input-number v-model:value="amount" v-if="amountType === 2">
<template #prefix> ¥ </template>
</n-input-number>
</n-space>
<template #footer> <span class="title">支付方式 </span></template>
<template #action>
<n-space>
<n-button
strong
secondary
:color="item.color"
v-for="item in payTypes"
:key="item"
@click="SetPayType(item.value)"
>
<template #icon>
<n-icon :component="item.icon" />
</template>
{{ item.label }}
<n-icon
class="check-icon"
:size="18"
:component="CheckOutlined"
v-if="payType === item.value"
/>
</n-button>
</n-space>
<n-button
type="success"
class="create-order-button"
size="large"
@click="CreateOrder"
>
立即充值
</n-button>
</template>
</n-thing>
</n-card>
</n-gi>
</n-grid>
</n-spin>
<n-modal v-model:show="showQrModal" :show-icon="false" preset="dialog" :title="qrParams.name">
<n-form class="py-4">
<div class="text-center">
<qrcode-vue :value="qrParams.qrUrl" :size="220" class="canvas" style="margin: 0 auto" />
</div>
</n-form>
<template #action>
<n-space>
<n-button @click="() => (showQrModal = false)">关闭</n-button>
</n-space>
</template>
</n-modal>
<RechargeLog class="mt-6" />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, inject } from 'vue';
import wx from 'weixin-js-sdk';
import { WechatOutlined, AlipayOutlined, QqOutlined, CheckOutlined } from '@vicons/antd';
import { useMessage } from 'naive-ui';
import { Create } from '@/api/order';
import QrcodeVue from 'qrcode.vue';
import RechargeLog from '../rechargeLog/index.vue';
import { SocketEnum } from '@/enums/socketEnum';
import { addOnMessage } from '@/utils/websocket';
const showQrModal = ref(false);
const qrParams = ref({
name: '',
qrUrl: '',
});
const loading = ref(false);
const message = useMessage();
const amountType = ref(1);
const amounts = ref([0.01, 10, 20, 30, 50, 100, '其他金额']);
const payTypes = ref([
{ value: 'wxpay', label: '微信支付', icon: WechatOutlined, color: '#18a058' },
{ value: 'alipay', label: '支付宝', icon: AlipayOutlined, color: '#2d8cf0' },
{ value: 'qqpay', label: 'QQ支付', icon: QqOutlined, color: '#2d8cf0' },
]);
const amount = ref<any>(null);
const payType = ref<any>(null);
onMounted(() => {});
function SetPayType(type: string) {
payType.value = type;
}
function SetAmount(a: number | string) {
amount.value = a;
if (a === '其他金额') {
amountType.value = 2;
amount.value = null;
} else {
amountType.value = 1;
}
}
function CreateOrder() {
if (amount.value === null || amount.value <= 0) {
message.error('请选择充值金额');
return;
}
if (payType.value === null || payType.value === '') {
message.error('请选择支付方式');
return;
}
loading.value = true;
Create({
orderType: 'balance',
payType: payType.value,
money: amount.value,
returnUrl: window.location.href,
})
.then((res) => {
if (res.order?.tradeType === undefined || res.order?.tradeType === '') {
message.error('创建支付订单失败,没找到交易方式,请联系管理处理!');
return;
}
if (res.order?.tradeType !== 'mp') {
if (res.order?.payURL === undefined || res.order?.payURL === '') {
message.error('创建支付订单失败,没找到支付地址,请联系管理处理!');
return;
}
}
switch (res.order?.tradeType) {
case 'scan':
showQr(res.order?.payURL, '打开微信【扫一扫】完成支付');
break;
case 'mp':
if (res.order.jsApi === undefined) {
message.error('支付失败请选择其他支付方式:JSAPI支付参数无效');
return;
}
const jsApi = res.order.jsApi;
// 配置微信JS SDK
wx.config({
// debug: true,
appId: jsApi.config.app_id,
timestamp: jsApi.config.timestamp,
nonceStr: jsApi.config.nonce_str,
signature: jsApi.config.signature,
jsApiList: ['chooseWXPay'],
});
// 配置完成后返回一个resolve
wx.ready(() => {
wxJSPay({
timestamp: jsApi.params.timeStamp,
nonceStr: jsApi.params.nonceStr,
package: jsApi.params.package,
signType: jsApi.params.signType,
paySign: jsApi.params.paySign,
})
.then((_res) => {
// ...
})
.catch((err) => {
message.success('支付失败:', err.message);
});
});
break;
case 'qqweb':
showQr(res.order?.payURL, '打开QQ【扫一扫】完成支付');
break;
default:
window.open(res.order?.payURL, '_blank');
}
})
.finally(() => {
loading.value = false;
});
}
// 发起微信公众号支付
function wxJSPay(params) {
return new Promise((resolve, reject) => {
// 调用微信支付
wx.chooseWXPay({
timestamp: params.timestamp,
nonceStr: params.nonceStr,
package: params.package,
signType: params.signType,
paySign: params.paySign,
success: (res) => {
// 支付成功时返回resolve
resolve(res);
},
fail: (err) => {
// 支付失败时返回reject
reject(err);
},
});
});
}
function showQr(url: string, name: string) {
qrParams.value.qrUrl = url;
qrParams.value.name = name;
showQrModal.value = true;
}
const onMessageList = inject('onMessageList');
const handleMessageList = (res) => {
const data = JSON.parse(res.data);
if (data.event === SocketEnum.EventAdminOrderNotify) {
if (data.code == SocketEnum.CodeErr) {
message.error('查询出错:' + data.event);
return;
}
showQrModal.value = false;
message.success('支付成功');
location.reload();
return;
}
};
addOnMessage(onMessageList, handleMessageList);
</script>
<style lang="less" scoped>
::v-deep(.n-thing .n-thing-main .n-thing-main__footer:not(:first-child)) {
margin-top: 36px;
margin-bottom: 10px;
}
::v-deep(.title) {
font-weight: var(--n-title-font-weight);
transition: color 0.3s var(--n-bezier);
flex: 1;
min-width: 0;
color: var(--n-title-text-color);
font-size: 18px;
}
::v-deep(.check-icon) {
margin-left: 3px;
}
::v-deep(.create-order-button) {
margin-top: 28px;
}
</style>
@@ -0,0 +1,152 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
title="受理退款申请"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="业务单号" path="orderSn">
<n-input v-model:value="params.orderSn" :disabled="true" />
</n-form-item>
<n-form-item label="订单金额" path="money">
<n-input placeholder="请输入标题" v-model:value="params.money" :disabled="true" />
</n-form-item>
<n-form-item label="退款原因" path="refundReason">
<n-input
type="textarea"
placeholder="请填写退款原因"
v-model:value="params.refundReason"
:disabled="true"
/>
</n-form-item>
<n-form-item label="更新状态" path="status">
<n-select
v-model:value="params.status"
:options="dict.getOptionUnRef('acceptRefundStatus')"
/>
</n-form-item>
<n-form-item label="拒绝原因" path="rejectRefundReason" v-if="params.status === 9">
<n-input
type="textarea"
placeholder="请填拒绝退款原因"
v-model:value="params.rejectRefundReason"
/>
</n-form-item>
<n-form-item label="退款备注" path="remark" v-if="params.status === 8">
<n-input type="textarea" placeholder="请填退款备注" v-model:value="params.remark" />
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { rules, State, newState } from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { AcceptRefund, View } from '@/api/order';
import { useDictStore } from '@/store/modules/dict';
const emit = defineEmits(['reloadTable', 'updateShowModal']);
interface Props {
showModal: boolean;
formParams?: State;
}
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const loading = ref(false);
const params = ref<State>(props.formParams);
const dict = useDictStore();
const message = useMessage();
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
AcceptRefund(params.value).then((_res) => {
message.success('操作成功');
setTimeout(() => {
isShowModal.value = false;
emit('reloadTable');
});
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function closeForm() {
isShowModal.value = false;
}
function loadForm(value) {
loading.value = true;
// 编辑
View({ id: value.id })
.then((res) => {
params.value = res;
})
.finally(() => {
loading.value = false;
});
}
watch(
() => props.formParams,
(value) => {
loadForm(value);
}
);
</script>
<style lang="less"></style>
@@ -0,0 +1,131 @@
<template>
<div>
<n-spin :show="loading" description="请稍候...">
<n-modal
v-model:show="isShowModal"
:show-icon="false"
preset="dialog"
title="退款申请"
:style="{
width: dialogWidth,
}"
>
<n-form
:model="params"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="业务单号" path="orderSn">
<n-input v-model:value="params.orderSn" :disabled="true" />
</n-form-item>
<n-form-item label="订单金额" path="money">
<n-input placeholder="请输入标题" v-model:value="params.money" :disabled="true" />
</n-form-item>
<n-form-item label="退款原因" path="refundReason">
<n-input
type="textarea"
placeholder="请填写退款原因"
v-model:value="params.refundReason"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" :loading="formBtnLoading" @click="confirmForm">确定</n-button>
</n-space>
</template>
</n-modal>
</n-spin>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { rules, State, newState } from './model';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
import { ApplyRefund, View } from '@/api/order';
interface Props {
showModal: boolean;
formParams?: State;
}
const emit = defineEmits(['reloadTable', 'updateShowModal']);
const props = withDefaults(defineProps<Props>(), {
showModal: false,
formParams: () => {
return newState(null);
},
});
const isShowModal = computed({
get: () => {
return props.showModal;
},
set: (value) => {
emit('updateShowModal', value);
},
});
const loading = ref(false);
const params = ref<State>(props.formParams);
const message = useMessage();
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth();
});
function confirmForm(e) {
e.preventDefault();
formRef.value.validate((errors) => {
if (!errors) {
formBtnLoading.value = true;
ApplyRefund(params.value)
.then((_res) => {
message.success('操作成功');
isShowModal.value = false;
emit('reloadTable');
})
.finally(() => {
formBtnLoading.value = false;
});
} else {
message.error('请填写完整信息');
}
});
}
function closeForm() {
isShowModal.value = false;
}
function loadForm(value) {
loading.value = true;
// 编辑
View({ id: value.id })
.then((res) => {
params.value = res;
})
.finally(() => {
loading.value = false;
});
}
watch(
() => props.formParams,
(value) => {
loadForm(value);
}
);
</script>
<style lang="less"></style>
+45
View File
@@ -0,0 +1,45 @@
<template>
<div>
<n-card :bordered="false" title="充值记录" class="proCard">
<n-tabs
type="card"
class="card-tabs"
:value="defaultTab"
animated
@before-leave="handleBeforeLeave"
>
<n-tab-pane
:name="item.key.toString()"
:tab="item.label"
v-for="item in dict.getOptionUnRef('orderStatus')"
:key="item.key"
>
<List :type="defaultTab" />
</n-tab-pane>
</n-tabs>
</n-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import List from './list.vue';
import { useRouter } from 'vue-router';
import { loadOptions } from './model';
import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();
const router = useRouter();
const defaultTab = ref('-1');
function handleBeforeLeave(tabName: string) {
defaultTab.value = tabName;
}
onMounted(() => {
if (router.currentRoute.value.query?.type) {
defaultTab.value = router.currentRoute.value.query.type as string;
}
loadOptions();
});
</script>
+231
View File
@@ -0,0 +1,231 @@
<template>
<div>
<n-card :bordered="false" class="proCard">
<BasicForm
@register="register"
@submit="reloadTable"
@reset="reloadTable"
@keyup.enter="reloadTable"
ref="searchFormRef"
>
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable
:openChecked="true"
:columns="columns"
:request="loadDataTable"
:row-key="(row) => row.id"
ref="actionRef"
:actionColumn="actionColumn"
@update:checked-row-keys="onCheckedRow"
:scroll-x="scrollX"
:resizeHeightOffset="-10000"
>
<template #tableTitle>
<n-button
type="error"
@click="handleBatchDelete"
:disabled="batchDeleteDisabled"
class="min-left-space"
v-if="hasPermission(['/order/delete'])"
>
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button
type="primary"
@click="handleExport"
class="min-left-space"
v-if="hasPermission(['/order/export'])"
>
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
<ApplyRefund
@reload-table="reloadTable"
@update-show-modal="updateShowModal"
:showModal="showModal"
:formParams="formParams"
/>
<AcceptRefund
@reload-table="reloadTable"
@update-show-modal="updateAcceptShowModal"
:showModal="showAcceptModal"
:formParams="formParams"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, h, reactive, ref } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { List, Export, Delete } from '@/api/order';
import { State, columns, schemas, newState } from './model';
import { ExportOutlined, DeleteOutlined } from '@vicons/antd';
import ApplyRefund from './applyRefund.vue';
import AcceptRefund from './acceptRefund.vue';
import { adaTableScrollX } from '@/utils/hotgo';
interface Props {
type?: string;
}
const props = withDefaults(defineProps<Props>(), {
type: '-1',
});
const { hasPermission } = usePermission();
const actionRef = ref();
const dialog = useDialog();
const message = useMessage();
const searchFormRef = ref<any>({});
const batchDeleteDisabled = ref(true);
const checkedIds = ref([]);
const showModal = ref(false);
const showAcceptModal = ref(false);
const formParams = ref<State>();
const actionColumn = reactive({
width: 120,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
type: 'warning',
label: '受理退款',
onClick: handleAcceptRefund.bind(null, record),
auth: ['/order/acceptRefund'],
ifShow: () => {
return record.status == 6;
},
},
{
type: 'default',
label: '申请退款',
onClick: handleApplyRefund.bind(null, record),
auth: ['/order/applyRefund'],
ifShow: () => {
return record.status == 4;
},
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/order/delete'],
ifShow: () => {
return record.status == 5;
},
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res, ...{ status: props.type } });
};
function reloadTable() {
actionRef.value.reload();
}
function onCheckedRow(rowKeys) {
batchDeleteDisabled.value = rowKeys.length <= 0;
checkedIds.value = rowKeys;
}
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
});
}
function handleBatchDelete() {
dialog.warning({
title: '警告',
content: '你确定要批量删除?只有已关闭的订单才能被删除',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
onNegativeClick: () => {
// message.error('取消');
},
});
}
function updateShowModal(value) {
showModal.value = value;
}
function handleApplyRefund(record: Recordable) {
showModal.value = true;
formParams.value = newState(record as State);
}
function updateAcceptShowModal(value) {
showAcceptModal.value = value;
}
function handleAcceptRefund(record: Recordable) {
showAcceptModal.value = true;
formParams.value = newState(record as State);
}
defineExpose({
reloadTable,
});
</script>
<style lang="less" scoped></style>
+154
View File
@@ -0,0 +1,154 @@
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { useDictStore } from '@/store/modules/dict';
import { MemberSumma, renderOptionTag, renderPopoverMemberSumma } from '@/utils';
export interface State {
id: number;
memberId: number;
orderType: string;
productId: number;
orderSn: string;
money: number;
remark: string;
payLogOutTradeNo: string;
status: number;
createdAt: string;
updatedAt: string;
refundReason: string;
rejectRefundReason: string;
payLogPayType: string;
memberBySumma?: null | MemberSumma;
}
export const defaultState = {
id: 0,
memberId: 0,
orderType: '',
productId: 0,
orderSn: '',
money: 0,
payLogOutTradeNo: '',
remark: '',
status: 1,
createdAt: '',
updatedAt: '',
refundReason: '',
rejectRefundReason: '',
payLogPayType: '',
};
export function newState(state: State | null): State {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(defaultState);
}
const dict = useDictStore();
export const rules = {};
export const schemas = ref<FormSchema[]>([
{
field: 'complexMemberId',
component: 'ComplexMemberPicker',
label: '下单用户',
componentProps: {
placeholder: '请选择下单用户',
onInput: (e: any) => {
console.log(e);
},
},
},
{
field: 'orderSn',
component: 'NInput',
label: '业务单号',
componentProps: {
placeholder: '请输入业务订单号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'payLogOutTradeNo',
component: 'NInput',
label: '商户单号',
componentProps: {
placeholder: '请输入商户订单号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export const columns = [
{
title: '业务订单号',
key: 'orderSn',
width: 220,
},
{
title: '下单用户',
key: 'memberId',
width: 100,
render(row: State) {
return renderPopoverMemberSumma(row.memberBySumma);
},
},
{
title: '支付方式',
key: 'payLogPayType',
render(row) {
return renderOptionTag('payType', row.payLogPayType);
},
width: 150,
},
{
title: '充值金额',
key: 'money',
width: 100,
render(row) {
return '¥' + Number(row.money).toFixed(2);
},
},
{
title: '订单状态',
key: 'status',
render(row) {
return renderOptionTag('orderStatus', row.status);
},
width: 150,
},
{
title: '商户订单号',
key: 'payLogOutTradeNo',
width: 220,
},
{
title: '创建时间',
key: 'createdAt',
width: 180,
},
];
export function loadOptions() {
dict.loadOptions(['payType', 'orderStatus', 'acceptRefundStatus']);
}
+173
View File
@@ -0,0 +1,173 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
:title="formValue.id > 0 ? '编辑CURD列表 #' + formValue.id : '添加CURD列表'"
:style="{
width: dialogWidth,
}"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-spin :show="loading" description="请稍候...">
<n-form
ref="formRef"
:model="formValue"
:rules="rules"
:label-placement="settingStore.isMobile ? 'top' : 'left'"
:label-width="100"
class="py-4"
>
<n-grid cols="1 s:1 m:2 l:2 xl:2 2xl:2" responsive="screen">
<n-gi span="1">
<n-form-item label="标题" path="title">
<n-input placeholder="请输入标题" v-model:value="formValue.title" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="描述" path="description">
<n-input type="textarea" placeholder="描述" v-model:value="formValue.description" />
</n-form-item>
</n-gi>
<n-gi span="2">
<n-form-item label="内容" path="content">
<Editor style="height: 450px" id="content" v-model:value="formValue.content" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="单图" path="image">
<UploadImage :maxNumber="1" v-model:value="formValue.image" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="附件" path="attachfile">
<UploadFile :maxNumber="1" v-model:value="formValue.attachfile" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="所在城市" path="cityId">
<CitySelector v-model:value="formValue.cityId" />
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="显示开关" path="switch">
<n-switch :unchecked-value="2" :checked-value="1" v-model:value="formValue.switch"
/>
</n-form-item>
</n-gi>
<n-gi span="1">
<n-form-item label="排序" path="sort">
<n-input-number placeholder="请输入排序" v-model:value="formValue.sort" />
</n-form-item>
</n-gi>
</n-grid>
</n-form>
</n-spin>
</n-scrollbar>
<template #action>
<n-space>
<n-button @click="closeForm">
取消
</n-button>
<n-button type="info" :loading="formBtnLoading" :disabled="!isFormValid" @click="confirmForm">
确定
</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue';
import { useDictStore } from '@/store/modules/dict';
import { Edit, View, MaxSort } from '@/api/curdDemo';
import { State, newState, rules } from './model';
import Editor from '@/components/Editor/editor.vue';
import UploadImage from '@/components/Upload/uploadImage.vue';
import UploadFile from '@/components/Upload/uploadFile.vue';
import CitySelector from '@/components/CitySelector/citySelector.vue';
import { useProjectSettingStore } from '@/store/modules/projectSetting';
import { useMessage } from 'naive-ui';
import { adaModalWidth } from '@/utils/hotgo';
const emit = defineEmits(['reloadTable']);
const message = useMessage();
const settingStore = useProjectSettingStore();
const dict = useDictStore();
const loading = ref(false);
const showModal = ref(false);
const formValue = ref<State>(newState(null));
const formRef = ref<any>({});
const formBtnLoading = ref(false);
const dialogWidth = computed(() => {
return adaModalWidth(840);
});
const isFormValid = ref(true);
// 提交表单
function confirmForm(e) {
e.preventDefault();
formRef.value.validate((errors) => {
if (!errors) {
formBtnLoading.value = true;
Edit(formValue.value)
.then((_res) => {
message.success('操作成功');
closeForm();
emit('reloadTable');
})
.finally(() => {
formBtnLoading.value = false;
});
} else {
message.error('请填写完整信息');
}
});
}
// 关闭表单
function closeForm() {
showModal.value = false;
loading.value = false;
}
// 打开模态框
function openModal(state: State) {
showModal.value = true;
// 新增
if (!state || state.id < 1) {
formValue.value = newState(state);
loading.value = true;
MaxSort()
.then((res) => {
formValue.value.sort = res.sort;
})
.finally(() => {
loading.value = false;
});
return;
}
// 编辑
loading.value = true;
View({ id: state.id })
.then((res) => {
formValue.value = res;
})
.finally(() => {
loading.value = false;
});
}
defineExpose({
openModal,
});
</script>
<style lang="less"></style>
+207
View File
@@ -0,0 +1,207 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="CURD列表">
<!-- 这是由系统生成的CURD表格,你可以将此行注释改为表格的描述 -->
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" @keyup.enter="reloadTable">
<template #statusSlot="{ model, field }">
<n-input v-model:value="model[field]" />
</template>
</BasicForm>
<BasicTable ref="actionRef" openChecked :columns="columns" :request="loadDataTable" :row-key="(row) => row.id" :actionColumn="actionColumn" :scroll-x="scrollX" :resizeHeightOffset="-10000" :checked-row-keys="checkedIds" @update:checked-row-keys="handleOnCheckedRow">
<template #tableTitle>
<n-button type="primary" @click="addTable" class="min-left-space" v-if="hasPermission(['/curdDemo/edit'])">
<template #icon>
<n-icon>
<PlusOutlined />
</n-icon>
</template>
添加
</n-button>
<n-button type="error" @click="handleBatchDelete" class="min-left-space" v-if="hasPermission(['/curdDemo/delete'])">
<template #icon>
<n-icon>
<DeleteOutlined />
</n-icon>
</template>
批量删除
</n-button>
<n-button type="primary" @click="handleExport" class="min-left-space" v-if="hasPermission(['/curdDemo/export'])">
<template #icon>
<n-icon>
<ExportOutlined />
</n-icon>
</template>
导出
</n-button>
</template>
</BasicTable>
</n-card>
<Edit ref="editRef" @reloadTable="reloadTable" />
</div>
</template>
<script lang="ts" setup>
import { h, reactive, ref, computed, onMounted } from 'vue';
import { useDialog, useMessage } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { usePermission } from '@/hooks/web/usePermission';
import { useDictStore } from '@/store/modules/dict';
import { List, Export, Delete, Status } from '@/api/curdDemo';
import { PlusOutlined, ExportOutlined, DeleteOutlined } from '@vicons/antd';
import { columns, schemas, loadOptions } from './model';
import { adaTableScrollX } from '@/utils/hotgo';
import Edit from './edit.vue';
const dict = useDictStore();
const dialog = useDialog();
const message = useMessage();
const { hasPermission } = usePermission();
const actionRef = ref();
const searchFormRef = ref<any>({});
const editRef = ref();
const checkedIds = ref([]);
const actionColumn = reactive({
width: 216,
title: '操作',
key: 'action',
fixed: 'right',
render(record: State) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: ['/curdDemo/edit'],
},
{
label: '禁用',
onClick: handleStatus.bind(null, record, 2),
ifShow: () => {
return record.status === 1;
},
auth: ['/curdDemo/status'],
},
{
label: '启用',
onClick: handleStatus.bind(null, record, 1),
ifShow: () => {
return record.status === 2;
},
auth: ['/curdDemo/status'],
},
{
label: '删除',
onClick: handleDelete.bind(null, record),
auth: ['/curdDemo/delete'],
},
],
});
},
});
const scrollX = computed(() => {
return adaTableScrollX(columns, actionColumn.width);
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
// 加载表格数据
const loadDataTable = async (res) => {
return await List({ ...searchFormRef.value?.formModel, ...res });
};
// 更新选中的行
function handleOnCheckedRow(rowKeys) {
checkedIds.value = rowKeys;
}
// 重新加载表格数据
function reloadTable() {
actionRef.value?.reload();
}
// 添加数据
function addTable() {
editRef.value.openModal(null);
}
// 编辑数据
function handleEdit(record: Recordable) {
editRef.value.openModal(record);
}
// 单个删除
function handleDelete(record: Recordable) {
dialog.warning({
title: '警告',
content: '你确定要删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete(record).then((_res) => {
message.success('删除成功');
reloadTable();
});
},
});
}
// 批量删除
function handleBatchDelete() {
if (checkedIds.value.length < 1){
message.error('请至少选择一项要删除的数据');
return;
}
dialog.warning({
title: '警告',
content: '你确定要批量删除?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Delete({ id: checkedIds.value }).then((_res) => {
checkedIds.value = [];
message.success('删除成功');
reloadTable();
});
},
});
}
// 导出
function handleExport() {
message.loading('正在导出列表...', { duration: 1200 });
Export(searchFormRef.value?.formModel);
}
// 修改状态
function handleStatus(record: Recordable, status: number) {
Status({ id: record.id, status: status }).then((_res) => {
message.success('设为' + dict.getLabel('sys_normal_disable', status) + '成功');
setTimeout(() => {
reloadTable();
});
});
}
onMounted(() => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
+280
View File
@@ -0,0 +1,280 @@
import { h, ref } from 'vue';
import { NSwitch } from 'naive-ui';
import { cloneDeep } from 'lodash-es';
import { FormSchema } from '@/components/Form';
import { defRangeShortcuts } from '@/utils/dateUtil';
import { renderImage, renderFile, renderPopoverMemberSumma, MemberSumma } from '@/utils';
import { Switch } from '@/api/curdDemo';
import { useDictStore } from '@/store/modules/dict';
import { usePermission } from '@/hooks/web/usePermission';
const dict = useDictStore();
const { hasPermission } = usePermission();
const $message = window['$message'];
export class State {
public id = 0;//ID
public categoryId = null;//测试分类
public title = '';//标题
public description = '';//描述
public content = '';//内容
public image = '';//单图
public attachfile = '';//附件
public cityId = null;//所在城市
public switch = 2;//显示开关
public sort = 0;//排序
public status = 1;//状态
public createdBy = 0;//创建者
public createdBySumma?: null | MemberSumma = null;//创建者摘要信息
public updatedBy = 0;//更新者
public updatedBySumma?: null | MemberSumma = null;//更新者摘要信息
public deletedBy = 0;//删除者
public deletedBySumma?: null | MemberSumma = null;//删除者摘要信息
public createdAt = '';//创建时间
public updatedAt = '';//修改时间
public deletedAt = '';//删除时间
constructor(state?: Partial<State>) {
if (state) {
Object.assign(this, state);
}
}
}
export function newState(state: State | Record<string, any> | null): State {
if (state !== null) {
if (state instanceof State) {
return cloneDeep(state);
}
return new State(state);
}
return new State();
}
// 表单验证规则
export const rules = {
title: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入标题',
},
description: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入描述',
},
content: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '请输入内容',
},
sort: {
required: true,
trigger: ['blur', 'input'],
type: 'number',
message: '请输入排序',
},
};
// 表格搜索表单
export const schemas = ref<FormSchema[]>([
{
field: 'id',
component: 'NInput',
label: 'ID',
componentProps: {
placeholder: '请输入ID',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'title',
component: 'NInput',
label: '标题',
componentProps: {
placeholder: '请输入标题',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'description',
component: 'NInput',
label: '描述',
componentProps: {
placeholder: '请输入描述',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdBy',
component: 'NInput',
label: '创建者',
componentProps: {
placeholder: '请输入ID|用户名|姓名|手机号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'deletedBy',
component: 'NInput',
label: '删除者',
componentProps: {
placeholder: '请输入ID|用户名|姓名|手机号',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'createdAt',
component: 'NDatePicker',
label: '创建时间',
componentProps: {
type: 'datetimerange',
clearable: true,
shortcuts: defRangeShortcuts(),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'testCategoryName',
component: 'NInput',
label: '关联分类',
componentProps: {
placeholder: '请输入关联分类',
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
// 表格列
export const columns = [
{
title: 'ID',
key: 'id',
align: 'left',
width: 50,
},
{
title: '标题',
key: 'title',
align: 'left',
width: 150,
},
{
title: '描述',
key: 'description',
align: 'left',
width: 300,
},
{
title: '单图',
key: 'image',
align: 'left',
width: 100,
render(row: State) {
return renderImage(row.image);
},
},
{
title: '附件',
key: 'attachfile',
align: 'left',
width: 100,
render(row: State) {
return renderFile(row.attachfile);
},
},
{
title: '显示开关',
key: 'switch',
align: 'left',
width: 150,
render(row: State) {
return h(NSwitch, {
value: row.switch === 1,
checked: '开启',
unchecked: '关闭',
disabled: !hasPermission(['/curdDemo/switch']),
onUpdateValue: function (e) {
console.log('onUpdateValue e:' + JSON.stringify(e));
row.switch = e ? 1 : 2;
Switch({ id: row.id, key: 'switch', value: row.switch }).then((_res) => {
$message.success('操作成功');
});
},
});
},
},
{
title: '排序',
key: 'sort',
align: 'left',
width: 100,
},
{
title: '创建者',
key: 'createdBy',
align: 'left',
width: 150,
render(row: State) {
return renderPopoverMemberSumma(row.createdBySumma);
},
},
{
title: '更新者',
key: 'updatedBy',
align: 'left',
width: 150,
render(row: State) {
return renderPopoverMemberSumma(row.updatedBySumma);
},
},
{
title: '删除者',
key: 'deletedBy',
align: 'left',
width: -1,
render(row: State) {
return renderPopoverMemberSumma(row.deletedBySumma);
},
},
{
title: '创建时间',
key: 'createdAt',
align: 'left',
width: 180,
},
{
title: '修改时间',
key: 'updatedAt',
align: 'left',
width: 180,
},
{
title: '关联分类',
key: 'testCategoryName',
align: 'left',
width: 100,
},
];
// 加载字典数据选项
export function loadOptions() {
dict.loadOptions(['sys_normal_disable', 'testCategoryOption']);
}
@@ -0,0 +1,109 @@
<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>
@@ -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,
};
@@ -0,0 +1,36 @@
<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;"
default-value="FluxTrend"
>
<n-tab-pane name="FluxTrend" :tab="t('流量消耗趋势')">
<FluxTrend />
</n-tab-pane>
<n-tab-pane name="VisitAmount" :tab="t('客户端访问量')">
<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>
@@ -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>
@@ -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',
},
};
@@ -0,0 +1,24 @@
<template>
<div class="mt-2">
<Company v-if="userStore.isCompanyDept" />
<Tenant v-else-if="userStore.isTenantDept" />
<Merchant v-else-if="userStore.isMerchantDept" />
<User v-else-if="userStore.isUserDept" />
<template v-else>
<div class="n-layout-page-header">
<n-card :bordered="false" title="默认首页">
部门类型未配置首页:{{ userStore.info?.deptType }}
</n-card>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import Company from './console_company.vue';
import Merchant from './console_merchant.vue';
import User from './console_user.vue';
import Tenant from './console_tenant.vue';
import { useUserStore } from '@/store/modules/user';
const userStore = useUserStore();
</script>
@@ -0,0 +1,343 @@
<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="t('卡板量')"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="success">{{ t('日') }}</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>
{{ t('日同比') }}
<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>
{{ t('周同比') }}
<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"> {{ t('总卡板量:') }} </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="visits.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
:title="t('激活卡板')"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="info">{{ t('周') }}</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"> {{ t('总激活卡板:') }} </div>
<div class="text-sn">
<CountTo :startVal="1" :endVal="saleroom.amount" />
</div>
</template>
</div>
</template>
</NCard>
</n-grid-item>
<n-grid-item>
<NCard
:title="t('代理商')"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="warning">{{ t('周') }}</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>
{{ t('日同比') }}
<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>
{{ t('周同比') }}
<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"> {{ t('总代理商量:') }} </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="t('提现佣金')"
:segmented="{ content: true, footer: true }"
size="small"
:bordered="false"
>
<template #header-extra>
<n-tag type="error">{{ t('月') }}</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>
{{ t('月同比') }}
<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>
{{ t('月同比') }}
<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"> {{ t('总提现额:') }} </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" @click="item.eventObject || {}">
<NCard
content-style="padding-top: 0;"
size="small"
:bordered="false"
v-on="item.eventObject || {}"
>
<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" />
</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 { getConsoleInfo } from '@/api/dashboard/console';
import VisiTab from './components/VisiTab.vue';
import { CountTo } from '@/components/CountTo';
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 router = useRouter();
// 图标列表
const iconList = computed(() => {
return [
{
icon: UsergroupAddOutlined,
size: '32',
title: t('用户'),
color: '#69c0ff',
eventObject: {
click: () => router.push({ name: 'user' }),
},
},
{
icon: BarChartOutlined,
size: '32',
title: t('分析'),
color: '#69c0ff',
eventObject: {
click: () => {},
},
},
{
icon: ShoppingCartOutlined,
size: '32',
title: t('商品'),
color: '#ff9c6e',
eventObject: {
click: () => {},
},
},
{
icon: AccountBookOutlined,
size: '32',
title: t('订单'),
color: '#b37feb',
eventObject: {
click: () => {},
},
},
{
icon: CreditCardOutlined,
size: '32',
title: t('票据'),
color: '#ffd666',
eventObject: {
click: () => {},
},
},
{
icon: MailOutlined,
size: '32',
title: t('消息'),
color: '#5cdbd3',
eventObject: {
click: () => {},
},
},
{
icon: TagsOutlined,
size: '32',
title: t('标签'),
color: '#ff85c0',
eventObject: {
click: () => {},
},
},
{
icon: SettingOutlined,
size: '32',
title: t('配置'),
color: '#ffc069',
eventObject: {
click: () => {},
},
},
];
});
onMounted(async () => {
const data = await getConsoleInfo();
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>
@@ -0,0 +1,11 @@
<script setup lang="ts"></script>
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="商户首页"> 这是商户的首页,如果需要,你可以定制TA </n-card>
</div>
</div>
</template>
<style scoped lang="less"></style>
@@ -0,0 +1,11 @@
<script setup lang="ts"></script>
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="租户首页"> 这是租户的首页,如果需要,你可以定制TA </n-card>
</div>
</div>
</template>
<style scoped lang="less"></style>
@@ -0,0 +1,13 @@
<script setup lang="ts"></script>
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="用户首页">
这是普通用户的首页,如果需要,你可以定制TA
</n-card>
</div>
</div>
</template>
<style scoped lang="less"></style>
@@ -0,0 +1,328 @@
<template>
<div>
<div class="mt-2">
<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>
+326
View File
@@ -0,0 +1,326 @@
<template>
<div>
<div class="n-layout-page-header">
<n-card :bordered="false" title="插件管理">
在这里可以管理和创建插件模板,开发独立、临时性、工具类型的功能时推荐使用插件化开发。
</n-card>
</div>
<n-card :bordered="false" class="proCard">
<BasicForm
@register="register"
@submit="handleSubmit"
@reset="handleReset"
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"
:mask-closable="false"
:show-icon="false"
preset="dialog"
title="创建新插件"
:style="{
width: dialogWidth,
}"
>
<n-alert :show-icon="false" type="info">
注意:插件创建成功后会在服务端对应的项目目录中生成插件模块文件,并自动注册到当前项目中。
</n-alert>
<n-form
:model="formParams"
ref="formRef"
label-placement="left"
:label-width="80"
class="py-4"
>
<n-form-item label="插件标签" path="label">
<n-input placeholder="请输入" v-model:value="formParams.label" />
<template #feedback>显示在插件列表中的模块名称. 不要超过20个字符</template>
</n-form-item>
<n-form-item label="插件包名" path="name">
<n-input placeholder="请输入" v-model:value="formParams.name" />
<template #feedback>
系统按照此名称创建和查找插件定义, 只能英文和下划线组成,建议全部小写,例如:hgexample
</template>
</n-form-item>
<n-form-item label="功能分组" path="group">
<n-select
placeholder="请选择"
:options="dict.getOptionUnRef('addonsGroupOptions')"
v-model:value="formParams.group"
:on-update:value="onUpdateValueGroup"
/>
</n-form-item>
<n-form-item label="扩展功能" path="extend">
<n-checkbox-group v-model:value="formParams.extend">
<n-space item-style="display: flex;">
<n-checkbox
:value="option.value"
:label="option.label"
v-for="option in dict.getOptionUnRef('addonsExtend')"
:key="option"
/>
</n-space>
</n-checkbox-group>
</n-form-item>
<n-form-item label="版本" path="version">
<n-input placeholder="请输入" v-model:value="formParams.version" />
<template #feedback>此版本号用于插件的版本更新</template>
</n-form-item>
<n-form-item label="简介" path="brief">
<n-input placeholder="请输入" v-model:value="formParams.brief" />
</n-form-item>
<n-form-item label="使用说明" path="description">
<n-input
type="textarea"
placeholder="使用说明"
v-model:value="formParams.description"
/>
<template #feedback>详细介绍插件的功能和使用方法</template>
</n-form-item>
<n-form-item label="作者" path="author">
<n-input placeholder="请输入" v-model:value="formParams.author" />
</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 { computed, h, onMounted, reactive, ref } from 'vue';
import { NIcon, useMessage, useDialog, useNotification } from 'naive-ui';
import { BasicTable, TableAction } from '@/components/Table';
import { BasicForm, useForm } from '@/components/Form/index';
import { List, Build, UnInstall, Install, Upgrade } from '@/api/develop/addons';
import { PlusOutlined } from '@vicons/antd';
import { newState, schemas, columns, loadOptions } from './model';
import { adaModalWidth } from '@/utils/hotgo';
import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();
const dialog = useDialog();
const message = useMessage();
const notification = useNotification();
const showModal = ref(false);
const formBtnLoading = ref(false);
const formRef: any = ref(null);
const actionRef = ref();
const formParams = ref<any>();
const checkedIds = ref([]);
const searchFormRef = ref<any>();
const dialogWidth = computed(() => {
return adaModalWidth();
});
const actionColumn = reactive({
width: 220,
title: '操作',
key: 'action',
fixed: 'right',
render(record) {
return h(TableAction as any, {
style: 'button',
actions: [
{
label: '安装模块',
onClick: handleInstall.bind(null, record),
ifShow: () => {
return record.installStatus !== 1;
},
},
{
type: 'warning',
label: '更新配置',
onClick: handleUpgrade.bind(null, record),
ifShow: () => {
return record.installStatus === 1;
},
},
{
type: 'error',
label: '卸载模块',
onClick: handleUnInstall.bind(null, record),
ifShow: () => {
return record.installStatus === 1;
},
},
],
});
},
});
const [register, {}] = useForm({
gridProps: { cols: '1 s:1 m:2 l:3 xl:4 2xl:4' },
labelWidth: 80,
schemas,
});
function onCheckedRow(rowKeys) {
checkedIds.value = rowKeys;
}
const loadDataTable = async (res) => {
return await List({ ...res, ...searchFormRef.value?.formModel });
};
function reloadTable() {
actionRef.value.reload();
}
function handleInstall(record: Recordable) {
dialog.info({
title: '提示',
content: '你确定要安装 【' + record.label + '】 模块?安装成功后需要重启服务才能生效!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Install(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleUpgrade(record: Recordable) {
dialog.warning({
title: '提示',
content: '你确定要更新 【' + record.label + '】 模块?更新成功后需要重启服务才能生效!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Upgrade(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleUnInstall(record: Recordable) {
dialog.error({
title: '提示',
content: '你确定要卸载 【' + record.label + '】 模块?卸载成功后需要重启服务才能生效!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
UnInstall(record).then((_res) => {
message.success('操作成功');
reloadTable();
});
},
});
}
function handleSubmit(_values: Recordable) {
reloadTable();
}
function handleReset(_values: Recordable) {
reloadTable();
}
function addTable() {
showModal.value = true;
formParams.value = newState(null);
}
function confirmForm(e) {
e.preventDefault();
formBtnLoading.value = true;
formRef.value.validate((errors) => {
if (!errors) {
dialog.info({
title: '提示',
content:
'你确定要提交生成吗?热编译生成后如果列表自动刷新没有出现新插件请等几秒刷新即可。否则请手动重启服务后刷新页面!',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: () => {
Build(formParams.value).then((_res) => {
showModal.value = false;
buildSuccessNotify();
});
},
});
} else {
message.error('请填写完整信息');
}
formBtnLoading.value = false;
});
}
function onUpdateValueGroup(value) {
formParams.value.group = value;
}
function buildSuccessNotify() {
let count = 10;
const n = notification.success({
title: '插件构建成功',
content: `如果你使用的热编译,页面将在 ${count} 秒后自动刷新插件列表即可生效。否则请手动重启服务后刷新页面!`,
duration: 10000,
closable: false,
onAfterEnter: () => {
const minusCount = () => {
count--;
n.content = `如果你使用的热编译,页面将在 ${count} 秒后自动刷新插件列表即可生效。否则请手动重启服务后刷新页面!`;
if (count > 0) {
window.setTimeout(minusCount, 1000);
}
};
window.setTimeout(minusCount, 1000);
},
onAfterLeave: () => {
location.reload();
},
});
}
onMounted(async () => {
loadOptions();
});
</script>
<style lang="less" scoped></style>
+179
View File
@@ -0,0 +1,179 @@
import { cloneDeep } from 'lodash-es';
import { h, ref } from 'vue';
import { fallbackSrc } from '@/utils/hotgo';
import { isUrl } from '@/utils/is';
import { NIcon, NIconWrapper, NImage, NTag } from 'naive-ui';
import { getIconComponent } from '@/utils/icons';
import { FormSchema } from '@/components/Form';
import { useDictStore } from '@/store/modules/dict';
const dict = useDictStore();
export const genInfoObj = {
label: '',
name: '',
group: 1,
extend: ['resourcePublic', 'resourceTemplate'],
version: 'v1.0.0',
brief: '',
description: '',
author: '',
};
export function newState(state) {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(genInfoObj);
}
export const columns = [
{
title: '图标',
key: 'logo',
width: 80,
render(row) {
if (isUrl(row.logo)) {
return h(NImage, {
width: 48,
height: 48,
src: row.logo,
fallbackSrc: fallbackSrc(),
style: {
width: '48px',
height: '48px',
'max-width': '100%',
'max-height': '100%',
},
});
} else {
return h(
NIconWrapper,
{
size: 48,
borderRadius: 8,
},
{
default: () =>
h(
NIcon,
{
size: 36,
style: {
marginTop: '-8px',
},
},
{
default: () => h(getIconComponent(row.logo)),
}
),
}
);
}
},
},
{
title: '模块名称',
key: 'name',
width: 120,
render(row) {
return h('div', {
innerHTML:
'<div >' + row.label + '<br><span style="opacity: 0.8;">' + row.name + '</span></div>',
});
},
},
{
title: '作者',
key: 'author',
width: 100,
},
{
title: '分组',
key: 'groupName',
render(row) {
return h(
NTag,
{
style: {
marginRight: '6px',
},
type: 'info',
bordered: false,
},
{
default: () => row.groupName,
}
);
},
width: 120,
},
{
title: '简介',
key: 'brief',
render(row) {
return row.brief;
},
width: 180,
},
{
title: '详细描述',
key: 'description',
width: 300,
render(row) {
return h('p', { id: 'app' }, [
h('div', {
innerHTML: '<div style="white-space: pre-wrap">' + row.description + '</div>',
}),
]);
},
},
{
title: '版本',
key: 'version',
width: 100,
},
];
export const schemas = ref<FormSchema[]>([
{
field: 'name',
component: 'NInput',
label: '模块名称',
componentProps: {
placeholder: '请输入模块名称或标签',
onInput: (e: any) => {
console.log(e);
},
},
rules: [{ trigger: ['blur'] }],
},
{
field: 'group',
component: 'NSelect',
label: '分组',
componentProps: {
placeholder: '请选择分组',
options: dict.getOption('addonsGroupOptions'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
{
field: 'status',
component: 'NSelect',
label: '安装状态',
componentProps: {
placeholder: '请选择状态',
options: dict.getOption('addonsInstallStatus'),
onUpdateValue: (e: any) => {
console.log(e);
},
},
},
]);
export function loadOptions() {
dict.loadOptions(['addonsGroupOptions', 'addonsInstallStatus', 'addonsExtend']);
}
@@ -0,0 +1,614 @@
<template>
<div>
<n-spin :show="bodyShow" description="请稍候...">
<n-card
:bordered="true"
title="基本设置"
class="proCard mt-2"
size="small"
:segmented="{ content: true }"
>
<n-form ref="formRef" :model="formValue">
<n-row :gutter="24">
<n-col :span="6" style="min-width: 200px">
<n-form-item label="生成类型" path="title">
<n-select
placeholder="请选择"
:options="selectList.genType"
v-model:value="formValue.genType"
/>
</n-form-item>
</n-col>
<n-col :span="6" style="min-width: 200px">
<n-form-item label="实体命名" path="varName">
<n-input placeholder="请输入" v-model:value="formValue.varName" />
</n-form-item>
</n-col>
<n-col :span="6" style="min-width: 200px">
<n-form-item
label="数据库"
path="dbName"
v-show="formValue.genType >= 10 && formValue.genType < 20"
>
<n-select
placeholder="请选择"
:options="selectList.db"
v-model:value="formValue.dbName"
@update:value="handleDbUpdateValue"
/>
</n-form-item>
</n-col>
<n-col :span="6" style="min-width: 200px">
<n-form-item
label="数据库表"
path="tableName"
v-show="formValue.genType >= 10 && formValue.genType < 20"
>
<n-select
filterable
tag
:loading="tablesLoading"
placeholder="请选择"
:options="tablesOption"
v-model:value="formValue.tableName"
@update:value="handleTableUpdateValue"
:disabled="formValue.dbName === ''"
/>
</n-form-item>
</n-col>
<!-- 树表-->
<template v-if="formValue.genType == 11">
<n-col :span="6" style="min-width: 200px">
<n-form-item path="title">
<template #label>
<div class="flex flex-row items-end"
>树名称字段
<n-tooltip trigger="hover">
<template #trigger>
<n-button strong text>
<template #icon>
<n-icon size="15" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
</n-button>
</template>
树节点的显示名称字段名, 如:`title`
</n-tooltip>
</div>
</template>
<n-select
filterable
tag
:loading="columnsLoading"
placeholder="请选择"
:options="columnsOption"
v-model:value="formValue.options.tree.titleColumn"
/>
</n-form-item>
</n-col>
<n-col :span="6" style="min-width: 200px">
<n-form-item label="树表格样式" path="styleType">
<n-radio-group v-model:value="formValue.options.tree.styleType" name="styleType">
<n-radio
v-for="status in selectList.treeStyleType"
:value="status.value"
:label="status.label"
>{{ status.label }}</n-radio
>
</n-radio-group>
</n-form-item>
</n-col>
</template>
<!-- 树表-->
<n-col :span="18">
<n-form-item
label="表格头部按钮组"
path="tableName"
v-show="formValue.genType >= 10 && formValue.genType < 20"
>
<n-checkbox-group v-model:value="formValue.options.headOps">
<n-space item-style="display: flex;">
<n-checkbox value="add" label="新增表单按钮" />
<n-checkbox value="batchDel" label="批量删除按钮" />
<n-checkbox value="export" label="导出按钮" />
</n-space>
</n-checkbox-group>
</n-form-item>
</n-col>
<n-col :span="24">
<n-form-item
label="表格列操作"
path="columnOps"
v-show="formValue.genType >= 10 && formValue.genType < 20"
>
<n-checkbox-group v-model:value="formValue.options.columnOps">
<n-space item-style="display: flex;">
<n-checkbox value="edit" label="编辑" />
<n-checkbox value="status" label="状态修改" />
<n-popover trigger="hover">
<template #trigger>
<n-icon size="15" class="tips-help-icon" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
<span>主表中存在`status`字段时才会生效</span>
</n-popover>
<n-checkbox value="del" label="删除" />
<n-checkbox value="view" label="详情页" />
<n-checkbox value="check" label="开启勾选列" />
<n-checkbox value="notFilterAuth" label="不过滤权限" />
<n-popover trigger="hover">
<template #trigger>
<n-icon size="15" class="tips-help-icon" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
<span>系统默认会过滤权限,勾选后强制不开启权限验证</span>
</n-popover>
</n-space>
</n-checkbox-group>
</n-form-item>
</n-col>
<n-col :span="24">
<n-form-item
label="高级设置"
path="autoOps"
v-show="formValue.genType >= 10 && formValue.genType < 20"
>
<n-checkbox-group v-model:value="formValue.options.autoOps">
<n-space item-style="display: flex;">
<n-checkbox value="genMenuPermissions" label="生成菜单权限" />
<n-checkbox value="runDao" label="生成前运行 [gf gen dao]" />
<n-popover trigger="hover">
<template #trigger>
<n-icon size="15" class="tips-help-icon" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
<span>如果你选择的表已经生成过dao相关代码,取消勾选可减少生成时间</span>
</n-popover>
<n-checkbox value="runService" label="生成后运行 [gf gen service]" />
<n-popover trigger="hover">
<template #trigger>
<n-icon size="15" class="tips-help-icon" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
<span>如果是插件模块,勾选后也会自动在对应插件下运行service相关代码生成</span>
</n-popover>
<n-checkbox
value="genFuncDict"
label="生成字典选项"
@click="handleCheckboxGenFuncDict"
/>
<n-popover trigger="hover">
<template #trigger>
<n-icon size="15" class="tips-help-icon" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
<span>将表数据生成为数据选项,并注册到内置的方法字典</span>
</n-popover>
<n-checkbox value="forcedCover" label="强制覆盖" />
<n-popover trigger="hover">
<template #trigger>
<n-icon size="15" class="tips-help-icon" color="#2d8cf0">
<QuestionCircleOutlined />
</n-icon>
</template>
<span>强制覆盖所有需要生成的代码文件,但不包含菜单SQL文件</span>
</n-popover>
</n-space>
</n-checkbox-group>
</n-form-item>
</n-col>
<n-col
:span="6"
style="min-width: 200px"
v-show="formValue.options?.autoOps?.includes('genMenuPermissions')"
>
<n-form-item path="pid">
<template #label>
<span>上级菜单</span>
<n-button class="ml-2" text type="primary" strong @click="handleAddMenu"
>菜单管理</n-button
>
</template>
<n-tree-select
:options="optionMenuTree"
:value="formValue.options.menu.pid"
@update:value="handleUpdateMenuPid"
/>
</n-form-item>
</n-col>
<n-col
:span="6"
style="min-width: 200px"
v-show="formValue.options?.autoOps?.includes('genMenuPermissions')"
>
<n-form-item label="菜单名称" path="tableComment">
<n-input placeholder="请输入" v-model:value="formValue.tableComment" />
</n-form-item>
</n-col>
<n-col
:span="6"
style="min-width: 200px"
v-show="formValue.options?.autoOps?.includes('genMenuPermissions')"
>
<n-form-item label="菜单图标" path="menuIcon">
<IconSelector style="width: 100%" v-model:value="formValue.options.menu.icon" />
</n-form-item>
</n-col>
<n-col
:span="6"
style="min-width: 200px"
v-show="formValue.options?.autoOps?.includes('genMenuPermissions')"
>
<n-form-item label="菜单排序" path="menuIcon">
<n-input-number
style="width: 100%"
placeholder="请输入"
v-model:value="formValue.options.menu.sort"
clearable
/>
</n-form-item>
</n-col>
</n-row>
</n-form>
</n-card>
<n-card
:bordered="true"
title="关联表设置"
class="proCard mt-2"
size="small"
:segmented="{ content: true }"
v-show="formValue.genType >= 10 && formValue.genType < 20"
>
<template #header-extra>
<n-space>
<n-button
type="primary"
@click="addJoin"
:disabled="formValue.options?.join?.length >= 20"
>新增关联表</n-button
>
</n-space>
</template>
<n-form ref="formRef" :model="formValue">
<n-alert type="warning" :show-icon="false" v-if="formValue.options?.join?.length > 3">关联表数量建议在三个以下</n-alert>
<div class="mt-4"></div>
<n-row :gutter="6" v-for="(join, index) in formValue.options.join" :key="index">
<n-col :span="6" style="min-width: 200px">
<n-form-item label="关联表" path="join.linkTable">
<n-select
filterable
tag
:loading="tablesLoading"
placeholder="请选择"
:options="linkTablesOption"
v-model:value="join.linkTable"
@update:value="handleLinkTableUpdateValue(join)"
:disabled="formValue.dbName === ''"
/>
</n-form-item>
</n-col>
<n-col :span="3" style="min-width: 100px">
<n-form-item
label="别名"
path="join.alias"
v-show="formValue.genType >= 10 && formValue.genType < 20"
>
<n-input
placeholder="请输入"
v-model:value="join.alias"
@update:value="updateJoinAlias"
/>
<template #feedback> {{ joinAliasFeedback }}</template>
</n-form-item>
</n-col>
<n-col :span="3" style="min-width: 100px">
<n-form-item label="关联方式" path="join.linkMode">
<n-select
placeholder="请选择"
:options="selectList.linkMode"
v-model:value="join.linkMode"
/>
</n-form-item>
</n-col>
<n-col :span="5" style="min-width: 180px">
<n-form-item label="关联字段" path="join.field">
<n-select
filterable
tag
:loading="linkColumnsLoading"
placeholder="请选择"
:options="linkColumnsOption[join.uuid]"
v-model:value="join.field"
/>
</n-form-item>
</n-col>
<n-col :span="5" style="min-width: 180px">
<n-form-item label="主表关联字段" path="join.masterField">
<n-select
filterable
tag
:loading="columnsLoading"
placeholder="请选择"
:options="columnsOption"
v-model:value="join.masterField"
/>
</n-form-item>
</n-col>
<n-col :span="2" style="min-width: 50px">
<n-space>
<n-form-item label="操作" path="title">
<n-button @click="delJoin(join, index)" size="small" strong secondary type="error"
>移除</n-button
>
</n-form-item>
</n-space>
</n-col>
</n-row>
</n-form>
</n-card>
</n-spin>
<MenuModal ref="menuModalRef" @reloadTable="loadMenuTreeOption" />
<SetFuncDict
ref="setFuncDictRef"
@update="handleUpdateFuncDict"
:columnsOption="columnsOption"
/>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, computed, watch } from 'vue';
import { FormInst, useDialog, useMessage } from 'naive-ui';
import { newState, selectListObj } from './model';
import { TableSelect, ColumnSelect } from '@/api/develop/code';
import { getRandomString } from '@/utils/charset';
import IconSelector from '@/components/IconSelector/index.vue';
import { QuestionCircleOutlined } from '@vicons/antd';
import { getMenuList } from '@/api/system/menu';
import { cloneDeep } from 'lodash-es';
import { isLetterBegin } from '@/utils/is';
import MenuModal from '@/views/permission/menu/menuModal.vue';
import SetFuncDict from './SetFuncDict.vue';
const message = useMessage();
const dialog = useDialog();
const timer = ref();
const formRef = ref<FormInst | null>(null);
const bodyShow = ref(true);
const tablesLoading = ref(false);
const columnsLoading = ref(false);
const linkColumnsLoading = ref(false);
const tablesOption = ref<any>([]); // 数据库表选项
const columnsOption = ref<any>([]); // 主表字段选项
const linkTablesOption = ref<any>([]); // 关联表选项
const linkColumnsOption = ref<any>([]); // 关联表字段选项
const menuModalRef = ref();
const setFuncDictRef = ref();
const optionMenuTree = ref([
{
id: 0,
key: 0,
label: '根目录',
pid: 0,
title: '根目录',
type: 1,
},
]);
const emit = defineEmits(['update:value']);
interface Props {
value?: any;
selectList: any;
}
const props = withDefaults(defineProps<Props>(), {
value: newState(null),
selectList: selectListObj,
});
watch(props, async (newVal, oldVal) => {
if (newVal.value.dbName != oldVal.value.dbName) {
await instLoad();
}
});
const formValue = computed({
get() {
return props.value;
},
set(value) {
emit('update:value', value);
},
});
onMounted(() => {
timer.value = setInterval(async () => {
if (props.value.id > 0) {
clearTimeout(timer.value);
await instLoad();
// 切换tab时会导致选项被清空,这里重新进行加载
await loadLinkColumnsOption();
await loadMenuTreeOption();
bodyShow.value = false;
}
}, 30);
});
const loadMenuTreeOption = async () => {
const options = await getMenuList();
optionMenuTree.value = [
{
id: 0,
key: 0,
label: '根目录',
pid: 0,
title: '根目录',
type: 1,
},
];
optionMenuTree.value = optionMenuTree.value.concat(options.list);
};
const loadSelect = async () => {
columnsOption.value = await loadColumnSelect(formValue.value.tableName);
};
async function instLoad() {
columnsLoading.value = true;
tablesLoading.value = true;
await loadSelect();
await loadTableSelect(formValue.value.dbName);
tablesLoading.value = false;
columnsLoading.value = false;
}
async function loadLinkColumnsOption() {
if (formValue.value.options.join === undefined) {
return;
}
for (let i = 0; i < formValue.value.options.join.length; i++) {
linkColumnsLoading.value = true;
linkColumnsOption.value[formValue.value.options.join[i].uuid] = await loadColumnSelect(
formValue.value.options.join[i].linkTable
);
linkColumnsLoading.value = false;
}
}
// 处理选项更新
async function handleDbUpdateValue(value, _option) {
tablesLoading.value = true;
await loadTableSelect(value);
tablesLoading.value = false;
}
async function loadTableSelect(value) {
const options = await TableSelect({ name: value });
tablesOption.value = cloneDeep(options);
linkTablesOption.value = cloneDeep(options);
}
async function loadColumnSelect(value) {
return await ColumnSelect({ name: formValue.value.dbName, table: value });
}
function handleTableUpdateValue(value, option) {
formValue.value.varName = option?.defVarName as string;
formValue.value.daoName = option?.daoName as string;
formValue.value.tableComment = option?.defTableComment as string;
}
function addJoin() {
if (formValue.value.options.join === undefined) {
formValue.value.options.join = [];
}
let uuid = getRandomString(16, true);
formValue.value.options.join.push({
uuid: uuid,
linkTable: '',
alias: '',
linkMode: 1,
field: '',
masterField: '',
daoName: '',
columns: [],
});
linkColumnsOption.value[uuid] = [];
}
function delJoin(join, index) {
formValue.value.options.join.splice(index, 1);
delete linkColumnsOption.value[join.uuid];
let i = linkTablesOption.value.findIndex((res) => res.value === join.linkTable);
if (i > -1) {
linkTablesOption.value[i].disabled = false;
}
}
async function handleLinkTableUpdateValue(join) {
let i = linkTablesOption.value.findIndex((res) => res.value === join.linkTable);
if (i > -1) {
join.alias = linkTablesOption.value[i].defAlias;
join.daoName = linkTablesOption.value[i].daoName;
linkTablesOption.value[i].disabled = true;
}
linkColumnsLoading.value = true;
linkColumnsOption.value[join.uuid] = await loadColumnSelect(join.linkTable);
// 清空更新前的字段
join.field = '';
linkColumnsLoading.value = false;
}
const joinAliasFeedback = ref('');
function updateJoinAlias(value: string) {
if (value.length < 3) {
joinAliasFeedback.value = '别名不能小于3位';
return;
}
if (!isLetterBegin(value)) {
joinAliasFeedback.value = '别名必须以字母开头';
return;
}
joinAliasFeedback.value = '';
}
function handleUpdateMenuPid(value: string | number | Array<string | number> | null) {
formValue.value.options.menu.pid = value;
}
function handleAddMenu() {
menuModalRef.value.openModal();
}
function handleCheckboxGenFuncDict() {
if (formValue.value.options.autoOps.includes('genFuncDict')) {
setFuncDictRef.value.openModal(formValue.value.options.funcDict);
}
}
function handleUpdateFuncDict(value) {
formValue.value.options.funcDict = value;
}
</script>
<style lang="less" scoped>
::v-deep(.default_text_value) {
color: var(--n-tab-text-color-active);
}
::v-deep(.tips-help-icon) {
margin-left: -16px;
margin-top: 5px;
display: block;
}
</style>
@@ -0,0 +1,651 @@
<template>
<n-card :bordered="false" class="proCard">
<n-spin :show="show" description="加载中...">
<BasicTable
size="small"
striped
resizable
canResize
virtual-scroll
:single-line="false"
:showTopRight="false"
:pagination="false"
:columns="columns"
:dataSource="dataSource"
:row-key="(row) => row.id"
:resizeHeightOffset="-20000"
:scroll-x="columnCollapse ? 1400 : 2400"
:scroll-y="720"
:scrollbar-props="{ trigger: 'none' }"
>
<template #tableTitle>
<n-space>
<n-popconfirm @positive-click="reloadColumns">
<template #trigger>
<n-button type="primary" class="min-left-space">
<template #icon>
<n-icon>
<Reload />
</n-icon>
</template>
重置字段
</n-button>
</template>
重置后将从数据库重新加载表,不保留当前字段配置,确定要重置吗?
</n-popconfirm>
<n-popconfirm @positive-click="syncColumns">
<template #trigger>
<n-button type="primary" class="min-left-space">
<template #icon>
<n-icon>
<Sync />
</n-icon>
</template>
同步字段
</n-button>
</template>
同步是从数据库重新加载表,保留当前有效的字段配置,确定要同步吗?
</n-popconfirm>
<n-button type="default" class="min-left-space" @click="handleMove">
<template #icon>
<n-icon>
<MoveOutline />
</n-icon>
</template>
移动字段
</n-button>
</n-space>
</template>
</BasicTable>
</n-spin>
</n-card>
<Move ref="moveRef" v-model:columns="dataSource" />
</template>
<script lang="ts" setup>
import { computed, h, onMounted, ref } from 'vue';
import { BasicTable } from '@/components/Table';
import {
formatColumns,
formGridColsOptions,
formGridSpanOptions,
genInfoObj,
selectListObj,
} from '@/views/develop/code/components/model';
import { ColumnList } from '@/api/develop/code';
import { NInputNumber, NSpace, NButton, NCheckbox, NInput, NSelect, NCascader } from 'naive-ui';
import { HelpCircleOutline, Reload, Sync, MoveOutline } from '@vicons/ionicons5';
import { cloneDeep } from 'lodash-es';
import { renderIcon, renderTooltip } from '@/utils';
import Move from './Move.vue';
const emit = defineEmits(['update:value']);
interface Props {
value?: any;
selectList: any;
}
const props = withDefaults(defineProps<Props>(), {
value: genInfoObj,
selectList: selectListObj,
});
const formValue = computed({
get() {
return props.value;
},
set(value) {
emit('update:value', value);
},
});
const show = ref(false);
const dataSource = ref([]);
const moveRef = ref();
const columnCollapse = ref(true);
const columnsCollapseData = computed(() => {
return columnCollapse.value
? [
{
title: '字段列名',
key: 'name',
width: 100,
},
{
title: '字段描述',
key: 'dc',
width: 100,
render(row) {
return h(NInput, {
value: row.dc,
onUpdateValue: function (e) {
row.dc = e;
},
});
},
},
]
: [
{
title: '字段列名',
key: 'name',
width: 100,
},
{
title: '物理类型',
key: 'sqlType',
width: 80,
},
{
title: 'Go属性',
key: 'goName',
width: 100,
},
{
title: 'Go类型',
key: 'goType',
width: 80,
},
{
title: 'Ts属性',
key: 'tsName',
width: 100,
},
{
title: 'Ts类型',
key: 'tsType',
width: 80,
},
{
title: '字段描述',
key: 'dc',
width: 100,
render(row) {
return h(NInput, {
value: row.dc,
onUpdateValue: function (e) {
row.dc = e;
},
});
},
},
];
});
const columns = computed(() => {
return [
{
title: '',
key: 'id',
width: 30,
render(row, index) {
return index + 1;
},
},
{
title(_column) {
return h('div', null, [
renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '字段', icon: renderIcon(HelpCircleOutline) }
),
'Go类型和属性定义取决于你在/hack/config.yaml中的配置参数'
),
h(
NButton,
{
strong: true,
size: 'small',
text: true,
type: 'primary',
style: { 'margin-left': '20px' },
onClick: () => (columnCollapse.value = !columnCollapse.value),
},
{ default: () => (columnCollapse.value ? '展开 >>' : '折叠 <<') }
),
]);
},
key: 'field',
align: 'center',
width: 800,
children: columnsCollapseData.value,
},
{
width: 800,
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '新增/编辑表单', icon: renderIcon(HelpCircleOutline) }
),
'勾选编辑以后会在新增、编辑表单中显示该字段;当同时勾选列表查询时,会优先使用配置的表单组件'
);
},
key: 'edit',
align: 'center',
children: [
{
align: 'center',
title: '编辑',
key: 'isEdit',
width: 30,
render(row) {
const disabled = isEditDisabled(row);
const checkbox = h(NCheckbox, {
defaultChecked: row.isEdit,
disabled: disabled,
onUpdateChecked: function (e) {
row.isEdit = e;
},
});
if (!disabled) {
return checkbox;
}
return renderTooltip(checkbox, '该字段属性由系统维护,无需单独配置!');
},
},
{
title: '必填',
key: 'required',
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.required,
disabled: row.name === 'id',
onUpdateChecked: function (e) {
row.required = e;
},
});
},
},
{
title: '唯一',
key: 'unique',
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.unique,
disabled: row.name === 'id',
onUpdateChecked: function (e) {
row.unique = e;
},
});
},
},
{
title: '表单组件',
key: 'formMode',
width: 100,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.formMode,
options: getFormModeOptions(row.tsType),
onUpdateValue: function (e) {
row.formMode = e;
},
});
},
},
{
title: '绑定字典',
key: 'dictType',
width: 100,
render(row) {
if (row.dictType == 0) {
row.dictType = null;
}
return h(NCascader, {
placeholder: ' ',
filterable: true,
clearable: true,
showPath: false,
checkStrategy: 'child',
disabled: row.name === 'id',
value: row.dictType,
options: props.selectList?.dictMode ?? [],
onUpdateValue: function (e) {
row.dictType = e;
},
});
},
},
{
title: '验证规则',
key: 'formRole',
width: 100,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.formRole,
disabled: row.name === 'id',
options: props.selectList?.formRole ?? [],
onUpdateValue: function (e) {
row.formRole = e;
},
});
},
},
{
title(_column) {
return h(NSpace, { inline: true }, [
renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '栅格', icon: renderIcon(HelpCircleOutline) }
),
'表单每行摆放组件的个数。响应式栅格,小屏幕自动转为每行摆放一个组件。参考文档:https://www.naiveui.com/zh-CN/os-theme/components/grid#responsive-item.vue'
),
h(NSelect, {
style: { width: '100px' },
size: 'small',
consistentMenuWidth: false,
value: formValue.value.options.presetStep.formGridCols,
options: formGridColsOptions,
onUpdateValue: function (e) {
formValue.value.options.presetStep.formGridCols = e;
},
}),
]);
},
key: 'formGridSpan',
width: 120,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
disabled: row.name === 'id',
value: row.formGridSpan,
options: getFormGridSpanOptions(formValue.value.options.presetStep.formGridCols),
onUpdateValue: function (e) {
row.formGridSpan = e;
},
});
},
},
],
},
{
width: 800,
title: '列表',
key: 'list',
align: 'center',
children: [
{
title: '列表',
key: 'isList',
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isList,
onUpdateChecked: function (e) {
row.isList = e;
},
});
},
},
{
title: '导出',
key: 'isExport',
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isExport,
onUpdateChecked: function (e) {
row.isExport = e;
},
});
},
},
{
title: '查询',
key: 'isQuery',
width: 30,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isQuery,
onUpdateChecked: function (e) {
row.isQuery = e;
},
});
},
},
{
title: '查询条件',
key: 'queryWhere',
width: 90,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.queryWhere,
disabled: row.name === 'id',
options: props.selectList?.whereMode ?? [],
onUpdateValue: function (e) {
row.queryWhere = e;
},
});
},
},
{
title: '排列方式',
key: 'align',
width: 80,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.align,
options: props.selectList?.tableAlign ?? [],
onUpdateValue: function (e) {
row.align = e;
},
});
},
},
{
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '列宽', icon: renderIcon(HelpCircleOutline) }
),
'选填。设定固定值时表格生成自动计算scroll-x,未设定默认每列按100计算'
);
},
key: 'width',
width: 50,
render(row) {
return h(NInputNumber, {
value: row.width,
placeholder: ' ',
min: -1,
max: 2000,
showButton: false,
onUpdateValue: function (e) {
row.width = e;
},
});
},
},
],
},
];
});
// 同步字段
function syncColumns() {
show.value = true;
dataSource.value = [];
const params = {
name: formValue.value.dbName,
table: formValue.value.tableName,
};
ColumnList(params)
.then((res) => {
const columns = formatColumns(res);
for (let i = 0; i < columns.length; i++) {
// 相同字段名称和类型,保留原字段属性
const index = formValue.value.masterColumns.findIndex(
(item) => item.name == columns[i].name && item.dataType == columns[i].dataType
);
if (index !== -1) {
columns[i] = formValue.value.masterColumns[index];
}
}
formValue.value.masterColumns = columns;
dataSource.value = formValue.value.masterColumns;
})
.finally(() => {
show.value = false;
});
}
// 重载字段属性
function reloadColumns() {
show.value = true;
dataSource.value = [];
const params = {
name: formValue.value.dbName,
table: formValue.value.tableName,
};
ColumnList(params)
.then((res) => {
formValue.value.masterColumns = formatColumns(res);
dataSource.value = formValue.value.masterColumns;
})
.finally(() => {
show.value = false;
});
}
function getFormModeOptions(type: string) {
const options = cloneDeep(props.selectList?.formMode ?? []);
if (options.length === 0) {
return [];
}
switch (type) {
case 'number':
for (let i = 0; i < options.length; i++) {
const allows = [
'Input',
'InputNumber',
'Radio',
'Select',
'Switch',
'Rate',
'TreeSelect',
'Cascader',
];
if (!allows.includes(options[i].value)) {
options[i].disabled = true;
}
}
break;
default:
}
options.sort((a, b) => (a.disabled === b.disabled ? 0 : a.disabled ? 1 : -1));
return options;
}
function getFormGridSpanOptions(cols: number) {
if (cols < 1) {
cols = 1;
}
if (cols > 4) {
cols = 4;
}
for (let i = 0; i < formValue.value.masterColumns.length; i++) {
if (!formValue.value.masterColumns[i].formGridSpan) {
formValue.value.masterColumns[i].formGridSpan = 1;
}
if (formValue.value.masterColumns[i].formGridSpan > cols) {
formValue.value.masterColumns[i].formGridSpan = cols;
}
}
return formGridSpanOptions.slice(0, Math.min(cols, formGridSpanOptions.length));
}
// 禁止编辑的字段,由系统维护
function isEditDisabled(row) {
const disabledNames = [
'id',
'created_by',
'updated_by',
'deleted_by',
'created_at',
'updated_at',
'deleted_at',
];
if (disabledNames.includes(row.name)) {
return true;
}
if (formValue.value.genType == 11) {
const disabledTreeNames = ['pid', 'level', 'tree'];
if (disabledTreeNames.includes(row.name)) {
return true;
}
}
return false;
}
function handleMove() {
moveRef.value.openModal();
}
onMounted(() => {
if (formValue.value.masterColumns.length === 0) {
reloadColumns();
} else {
show.value = true;
setTimeout(function () {
dataSource.value = formValue.value.masterColumns;
show.value = false;
}, 100);
}
});
</script>
<style lang="less" scoped>
.tree-tips {
margin-left: 12px;
color: #18a058;
font-weight: 600;
}
</style>
@@ -0,0 +1,430 @@
<template>
<n-spin :show="show" description="加载中...">
<n-card :bordered="false" class="proCard">
<BasicTable
size="small"
striped
resizable
canResize
:single-line="false"
:showTopRight="false"
:pagination="false"
:columns="columns"
:dataSource="dataSource"
:row-key="(row) => row.id"
:resizeHeightOffset="-20000"
:scroll-x="columnCollapse ? 880 : 1880"
:scroll-y="720"
:scrollbar-props="{ trigger: 'none' }"
>
<template #tableTitle>
<n-space>
<n-popconfirm @positive-click="reloadColumns(getIndex())">
<template #trigger>
<n-button type="primary" class="min-left-space">
<template #icon>
<n-icon>
<Reload />
</n-icon>
</template>
重置字段
</n-button>
</template>
重置后将从数据库重新加载该表的默认配置,确定要重置吗?
</n-popconfirm>
<n-popconfirm @positive-click="syncColumns(getIndex())">
<template #trigger>
<n-button type="primary" class="min-left-space">
<template #icon>
<n-icon>
<Sync />
</n-icon>
</template>
同步字段
</n-button>
</template>
同步是从数据库重新加载表,保留当前有效的字段配置,确定要同步吗?
</n-popconfirm>
<n-button type="default" class="min-left-space" @click="handleMove">
<template #icon>
<n-icon>
<MoveOutline />
</n-icon>
</template>
移动字段
</n-button>
</n-space>
</template>
</BasicTable>
</n-card>
<Move ref="moveRef" v-model:columns="dataSource" />
</n-spin>
</template>
<script lang="ts" setup>
import { computed, h, onMounted, ref } from 'vue';
import { BasicTable } from '@/components/Table';
import { formatColumns, genInfoObj, selectListObj } from '@/views/develop/code/components/model';
import { ColumnList } from '@/api/develop/code';
import {
NButton,
NCheckbox,
NIcon,
NInput,
NInputNumber,
NSelect,
NSpace,
NTooltip,
} from 'naive-ui';
import { HelpCircleOutline, MoveOutline, Reload, Sync, WarningOutline } from '@vicons/ionicons5';
import { renderIcon, renderTooltip } from '@/utils';
import Move from './Move.vue';
const emit = defineEmits(['update:value']);
interface Props {
value?: any;
selectList: any;
uuid: string;
}
const props = withDefaults(defineProps<Props>(), {
value: genInfoObj,
selectList: selectListObj,
uuid: '',
});
const formValue = computed({
get() {
return props.value;
},
set(value) {
emit('update:value', value);
},
});
const show = ref(false);
const dataSource = ref([]);
const moveRef = ref();
const columnCollapse = ref(true);
const columnsCollapseData = computed(() => {
return columnCollapse.value
? [
{
title: '字段列名',
key: 'name',
width: 150,
},
{
title: '字段描述',
key: 'dc',
width: 150,
render(row) {
return h(NInput, {
value: row.dc,
onUpdateValue: function (e) {
row.dc = e;
// await saveProductCustom(row.id, 'frontShow', e);
},
});
},
},
]
: [
{
title: '字段列名',
key: 'name',
width: 100,
},
{
title: '物理类型',
key: 'sqlType',
width: 80,
},
{
title: 'Go属性',
key: 'goName',
width: 150,
},
{
title: 'Go类型',
key: 'goType',
width: 100,
},
{
title: 'Ts属性',
key: 'tsName',
width: 150,
},
{
title: 'Ts类型',
key: 'tsType',
width: 100,
},
{
title: '字段描述',
key: 'dc',
width: 100,
render(row) {
return h(NInput, {
value: row.dc,
onUpdateValue: function (e) {
row.dc = e;
},
});
},
},
];
});
const columns = computed(() => {
return [
{
title: '',
key: 'id',
width: 50,
render(row, index) {
return index + 1;
},
},
{
title(_column) {
return h('div', null, [
renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '字段', icon: renderIcon(HelpCircleOutline) }
),
'Go类型和属性定义取决于你在/hack/config.yaml中的配置参数'
),
h(
NButton,
{
strong: true,
size: 'small',
text: true,
type: 'primary',
style: { 'margin-left': '20px' },
onClick: () => (columnCollapse.value = !columnCollapse.value),
},
{ default: () => (columnCollapse.value ? '展开 >>' : '折叠 <<') }
),
]);
},
key: 'field',
align: 'center',
width: 800,
children: columnsCollapseData.value,
},
{
width: 800,
title: '列表',
key: 'list',
align: 'center',
children: [
{
title: '列表',
key: 'isList',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isList,
onUpdateChecked: function (e) {
row.isList = e;
},
});
},
},
{
title: '导出',
key: 'isExport',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isExport,
onUpdateChecked: function (e) {
row.isExport = e;
},
});
},
},
{
title: '查询',
key: 'isQuery',
width: 50,
align: 'center',
render(row) {
return h(NCheckbox, {
defaultChecked: row.isQuery,
onUpdateChecked: function (e) {
row.isQuery = e;
},
});
},
},
{
title: '查询条件',
key: 'queryWhere',
width: 100,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.queryWhere,
disabled: row.name === 'id',
options: props.selectList?.whereMode ?? [],
onUpdateValue: function (e) {
row.queryWhere = e;
},
});
},
},
{
title: '排列方式',
key: 'align',
width: 100,
render(row) {
return h(NSelect, {
consistentMenuWidth: false,
value: row.align,
options: props.selectList?.tableAlign ?? [],
onUpdateValue: function (e) {
row.align = e;
},
});
},
},
{
title(_column) {
return renderTooltip(
h(
NButton,
{
strong: true,
size: 'small',
text: true,
iconPlacement: 'right',
},
{ default: () => '列宽', icon: renderIcon(HelpCircleOutline) }
),
'选填。设定固定值时表格生成自动计算scroll-x,未设定默认每列按100计算'
);
},
key: 'width',
width: 50,
render(row) {
return h(NInputNumber, {
value: row.width,
placeholder: ' ',
min: 0,
max: 2000,
showButton: false,
onUpdateValue: function (e) {
row.width = e;
},
});
},
},
],
},
];
});
function handleMove() {
moveRef.value.openModal();
}
function getIndex() {
if (formValue.value.options.join.length === 0) {
return -1;
}
for (let i = 0; i < formValue.value.options.join.length; i++) {
if (formValue.value.options.join[i].uuid === props.uuid) {
return i;
}
}
return -1;
}
// 同步字段属性
function syncColumns(index: number) {
show.value = true;
dataSource.value = [];
const join = formValue.value.options.join[index];
const params = {
name: formValue.value.dbName,
table: join.linkTable,
isLink: 1,
alias: join.alias,
};
ColumnList(params)
.then((res) => {
const columns = formatColumns(res);
for (let i = 0; i < columns.length; i++) {
// 相同字段名称和类型,保留原字段属性
const index2 = join.columns.findIndex(
(item) => item.name == columns[i].name && item.dataType == columns[i].dataType
);
if (index2 !== -1) {
columns[i] = join.columns[index2];
}
}
join.columns = columns;
dataSource.value = join.columns;
})
.finally(() => {
show.value = false;
});
}
// 重载字段属性
function reloadColumns(index: number) {
show.value = true;
dataSource.value = [];
const join = formValue.value.options.join[index];
const params = {
name: formValue.value.dbName,
table: join.linkTable,
isLink: 1,
alias: join.alias,
};
ColumnList(params)
.then((res) => {
join.columns = formatColumns(res);
dataSource.value = join.columns;
})
.finally(() => {
show.value = false;
});
}
onMounted(() => {
show.value = true;
setTimeout(() => {
const index = getIndex();
// 已存在直接加载
if (formValue.value.options.join[index].columns.length > 0) {
dataSource.value = formValue.value.options.join[index].columns;
show.value = false;
return;
}
reloadColumns(index);
}, 100);
});
</script>
<style lang="less" scoped></style>
@@ -0,0 +1,82 @@
<template>
<div>
<n-modal
v-model:show="showModal"
:mask-closable="false"
:show-icon="false"
preset="dialog"
transform-origin="center"
title="移动字段"
:style="{
width: dialogWidth,
}"
>
<n-scrollbar style="max-height: 87vh" class="pr-5">
<n-card
:bordered="false"
:content-style="{ padding: '0px' }"
:header-style="{ padding: 'px' }"
:segmented="true"
>
请通过拖拽来移动字段的位置。
<div class="mt-8"></div>
<Draggable
class="draggable-ul"
animation="300"
:list="columns"
group="people"
itemKey="name"
>
<template #item="{ element }">
<div class="cursor-move draggable-li">
<n-tag type="default" size="small" style="font-weight: 800">{{
element.name
}}</n-tag
><span class="ml-2">{{ element.dc }}</span>
</div>
</template>
</Draggable>
</n-card>
</n-scrollbar>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import Draggable from 'vuedraggable';
import { adaModalWidth } from '@/utils/hotgo';
const showModal = ref(false);
const columns = defineModel<[]>('columns');
const dialogWidth = computed(() => {
return adaModalWidth(360);
});
function openModal() {
showModal.value = true;
}
defineExpose({
openModal,
});
</script>
<style lang="less" scoped>
.draggable-ul {
width: 100%;
overflow: hidden;
margin-top: -8px;
.draggable-li {
width: 100%;
padding: 8px 4px;
color: #333;
border-bottom: 1px solid #efeff5;
}
.draggable-li:hover {
background-color: rgba(229, 231, 235, var(--tw-border-opacity));
}
}
</style>
@@ -0,0 +1,161 @@
<template>
<div>
<textarea id="copy-code" :value="content"></textarea>
<n-tabs type="line" animated>
<n-tab-pane v-for="(view, index) in views" :key="index" :name="view.name" :tab="view.name">
<n-space justify="space-between">
<n-tag :type="view.tag.type" class="tag-margin">
{{ view.tag.label }}
<template #icon>
<n-icon :component="view.tag.icon" />
</template>
{{ view.path }}
</n-tag>
<n-button type="primary" size="small" class="tag-margin" @click="handleCopy(view.content)"
>复制本页代码</n-button
>
</n-space>
<n-scrollbar class="code-scrollbar" trigger="none">
<n-code
:class="'code-' + getFileExtension(view.path)"
:code="view.content"
:hljs="hljs"
:language="getFileExtension(view.path)"
show-line-numbers
/>
</n-scrollbar>
</n-tab-pane>
</n-tabs>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import hljs from 'highlight.js/lib/core';
import go from 'highlight.js/lib/languages/go';
import typescript from 'highlight.js/lib/languages/typescript';
import xml from 'highlight.js/lib/languages/xml';
import sql from 'highlight.js/lib/languages/sql';
import { cloneDeep } from 'lodash-es';
import {
CheckmarkCircle,
CheckmarkDoneCircle,
CloseCircleOutline,
HelpCircleOutline,
RemoveCircleOutline,
} from '@vicons/ionicons5';
import { useMessage } from 'naive-ui';
hljs.registerLanguage('go', go);
hljs.registerLanguage('ts', typescript);
hljs.registerLanguage('sql', sql);
hljs.registerLanguage('vue', xml);
interface Props {
previewModel: any;
showModal: boolean;
}
const props = withDefaults(defineProps<Props>(), {
previewModel: cloneDeep({ views: {} }),
showModal: false,
});
const message = useMessage();
const content = ref('');
const views = computed(() => {
let tmpViews: any = [];
let i = 0;
for (const [k, v] of Object.entries(props.previewModel.views)) {
let item = v as any;
item.name = k;
switch (item.meth) {
case 1:
item.tag = { type: 'success', label: '创建文件', icon: CheckmarkCircle };
break;
case 2:
item.tag = { type: 'warning', label: '覆盖文件', icon: CheckmarkDoneCircle };
break;
case 3:
item.tag = { type: 'info', label: '已存在跳过', icon: CloseCircleOutline };
break;
case 4:
item.tag = { type: 'error', label: '不生成', icon: RemoveCircleOutline };
break;
default:
item.tag = { type: 'error', label: '未知状态', icon: HelpCircleOutline };
}
tmpViews[i] = item;
i++;
}
return tmpViews;
});
function getFileExtension(path: string): string {
const parts = path.split('.');
if (parts.length > 1) {
return parts[parts.length - 1];
}
return '';
}
function handleCopy(code: string) {
content.value = code;
setTimeout(function () {
const copyVal = document.getElementById('copy-code');
copyVal.select();
document.execCommand('copy');
message.success('已复制');
}, 20);
}
</script>
<style lang="less" scoped>
::v-deep(.alert-margin) {
margin-bottom: 20px;
}
::v-deep(.tag-margin) {
margin-bottom: 10px;
}
::v-deep(.code-scrollbar) {
height: calc(100vh - 300px);
background: #282b2e;
color: #e0e2e4;
padding: 10px;
}
::v-deep(.code-vue .hljs-tag) {
color: rgb(242, 197, 92);
}
::v-deep(.code-vue .hljs-name) {
color: rgb(242, 197, 92);
}
::v-deep(.code-vue .hljs-attr) {
color: rgb(49, 104, 213);
}
::v-deep(.code-go .hljs-params) {
color: rgb(49, 104, 213);
}
::v-deep(.code-ts .hljs-params) {
color: rgb(49, 104, 213);
}
::v-deep(.code-ts .hljs-property) {
color: rgb(49, 104, 213);
}
::v-deep(.code-ts .hljs-function) {
color: rgb(49, 104, 213);
}
#copy-code {
position: fixed;
top: -100px;
left: -100px;
}
</style>
@@ -0,0 +1,133 @@
<template>
<div>
<n-modal
title="设置选项字段"
v-model:show="showModal"
:block-scroll="false"
:mask-closable="false"
:show-icon="false"
preset="dialog"
>
<n-form
:model="formValue"
:rules="rules"
ref="formRef"
label-placement="left"
:label-width="100"
class="py-4"
>
<n-form-item label="选项值" path="valueColumn">
<n-select
filterable
tag
placeholder="请选择"
:options="columnsOption"
v-model:value="formValue.valueColumn"
/>
</n-form-item>
<n-form-item label="选项名称" path="labelColumn">
<n-select
filterable
tag
placeholder="请选择"
:options="columnsOption"
v-model:value="formValue.labelColumn"
/>
</n-form-item>
</n-form>
<template #action>
<n-space>
<n-button @click="closeForm">取消</n-button>
<n-button type="info" @click="confirmForm">保存</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { rules } from '@/views/addons/stock/itemBrand/model';
import { cloneDeep } from 'lodash-es';
import { useMessage } from 'naive-ui';
import { Edit } from '@/api/addons/stock/itemClass';
interface Props {
columnsOption: any;
}
const props = withDefaults(defineProps<Props>(), {
columnsOption: [],
});
const rules = {
valueColumn: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '选项值不能为空',
},
labelColumn: {
required: true,
trigger: ['blur', 'input'],
type: 'string',
message: '选项名称不能为空',
},
};
const message = useMessage();
const emit = defineEmits(['update']);
const showModal = ref(false);
const formRef = ref();
const formValue = ref({
valueColumn: null,
labelColumn: null,
});
function openModal(state) {
showModal.value = true;
if (!state) {
state = {
valueColumn: null,
labelColumn: null,
};
}
if (!state.valueColumn) {
const item = props.columnsOption.find((item) => item.value === 'id');
if (item) {
state.valueColumn = item.value;
}
}
if (!state.labelColumn) {
const item = props.columnsOption.find((item) => item.value === 'title' || item.value === 'name');
if (item) {
state.labelColumn = item.value;
}
}
formValue.value = cloneDeep(state);
emit('update', formValue.value);
}
function closeForm() {
showModal.value = false;
}
function confirmForm(e) {
e.preventDefault();
formRef.value.validate((errors) => {
if (!errors) {
emit('update', formValue.value);
closeForm();
} else {
message.error('请填写完整信息');
}
});
}
defineExpose({
openModal,
});
</script>
@@ -0,0 +1,142 @@
import { cloneDeep } from 'lodash-es';
import { isJsonString } from '@/utils/is';
export const genFileObj = {
meth: 1,
content: '',
path: '',
required: true,
};
export interface joinAttr {
uuid: string;
linkTable: string;
alias: string;
linkMode: number;
field: string;
masterField: string;
columns: any;
}
export const genInfoObj = {
id: 0,
genType: 10,
genTemplate: null,
varName: '',
options: {
headOps: ['add', 'batchDel', 'export'],
columnOps: ['edit', 'del', 'view', 'status', 'check'],
autoOps: ['genMenuPermissions', 'runDao', 'runService'],
join: [],
menu: {
pid: 0,
icon: 'MenuOutlined',
sort: 0,
},
tree: {
titleColumn: null,
styleType: 1,
},
funcDict: {
valueColumn: null,
labelColumn: null,
},
presetStep: {
formGridCols: 1,
},
},
dbName: '',
tableName: '',
tableComment: '',
daoName: '',
masterColumns: [],
addonName: null,
status: 2,
createdAt: '',
updatedAt: '',
};
export const selectListObj = {
db: [],
genType: [],
status: [],
tables: [],
formMode: [],
formRole: [],
dictMode: [],
whereMode: [],
buildMeth: [],
tableAlign: [],
treeStyleType: [],
};
export function newState(state) {
if (state !== null) {
return cloneDeep(state);
}
return cloneDeep(genInfoObj);
}
export const formGridColsOptions = [
{
value: 1,
label: '一行一列',
},
{
value: 2,
label: '一行两列',
},
{
value: 3,
label: '一行三列',
},
{
value: 4,
label: '一行四列',
},
];
export const formGridSpanOptions = [
{
value: 1,
label: '占一列位置',
},
{
value: 2,
label: '占两列位置',
},
{
value: 3,
label: '占三列位置',
},
{
value: 4,
label: '占四列位置',
},
];
// 格式化列字段
export function formatColumns(columns: any) {
if (columns === undefined || columns.length === 0) {
columns = [];
}
if (isJsonString(columns)) {
columns = JSON.parse(columns);
}
if (columns.length > 0) {
for (let i = 0; i < columns.length; i++) {
if (!columns[i].formGridSpan) {
columns[i].formGridSpan = 1;
}
if (!columns[i].align) {
columns[i].align = 'left';
}
if (!columns[i].width || columns[i].width < 1) {
columns[i].width = null;
}
}
}
return columns;
}

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