feat: add web application
This commit is contained in:
@@ -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>
|
||||
@@ -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',
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header"><n-card :bordered="false" title="会员等级">配置会员等级、升级价格、赠送金额和佣金规则。</n-card></div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" />
|
||||
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="r=>r.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000"><template #tableTitle><n-button v-if="hasPermission(['/memberLevel/save'])" type="primary" @click="openEdit()">新增等级</n-button></template></BasicTable>
|
||||
</n-card>
|
||||
<n-modal v-model:show="showEdit" preset="card" :title="form.id?'编辑等级':'新增等级'" class="level-modal">
|
||||
<n-alert type="info" :show-icon="false">金额以元录入,服务端按分保存;佣金以百分比录入,服务端按万分比保存。</n-alert>
|
||||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="left" label-width="110">
|
||||
<n-grid :cols="2" :x-gap="16">
|
||||
<n-form-item-gi label="等级编码" path="levelCode" required><n-input v-model:value="form.levelCode" placeholder="如 VIP1" /></n-form-item-gi>
|
||||
<n-form-item-gi label="等级名称" path="name" required><n-input v-model:value="form.name" /></n-form-item-gi>
|
||||
<n-form-item-gi label="图标地址"><n-input v-model:value="form.iconUrl" /></n-form-item-gi>
|
||||
<n-form-item-gi label="状态"><n-select v-model:value="form.status" :options="dict.getOptionUnRef('sys_normal_disable')" /></n-form-item-gi>
|
||||
<n-form-item-gi label="升级价格(元)"><n-input-number v-model:value="form.upgradeYuan" :min="0" :precision="2" /></n-form-item-gi>
|
||||
<n-form-item-gi label="赠送金额(元)"><n-input-number v-model:value="form.giftYuan" :min="0" :precision="2" /></n-form-item-gi>
|
||||
<n-form-item-gi label="每日接单次数"><n-input-number v-model:value="form.dailyOrderLimit" :min="0" /></n-form-item-gi>
|
||||
<n-form-item-gi label="佣金比例(%)"><n-input-number v-model:value="form.commissionPercent" :min="0" :max="100" :precision="2" /></n-form-item-gi>
|
||||
<n-form-item-gi label="每日提现次数"><n-input-number v-model:value="form.dailyWithdrawLimit" :min="0" /></n-form-item-gi>
|
||||
<n-form-item-gi label="排序"><n-input-number v-model:value="form.sort" /></n-form-item-gi>
|
||||
<n-form-item-gi label="备注" :span="2"><n-input v-model:value="form.remark" type="textarea" /></n-form-item-gi>
|
||||
</n-grid>
|
||||
</n-form>
|
||||
<template #action><n-space justify="end"><n-button @click="showEdit=false">取消</n-button><n-button v-if="hasPermission(['/memberLevel/save'])" type="primary" :loading="saving" @click="saveLevel">保存</n-button></n-space></template>
|
||||
</n-modal>
|
||||
</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';import { usePermission } from '@/hooks/web/usePermission';import { useDictStore } from '@/store/modules/dict';import { adaTableScrollX } from '@/utils/hotgo';import { List,View,Save,Status,Delete } from '@/api/memberLevel';import { columns,schemas,loadOptions } from './model';
|
||||
const message=useMessage(),dialog=useDialog(),dict=useDictStore();const{hasPermission}=usePermission();const actionRef=ref(),searchFormRef=ref<any>({}),formRef=ref<any>(),showEdit=ref(false),saving=ref(false);const form=reactive<any>({});const rules={levelCode:{required:true,message:'请输入等级编码',trigger:['input','blur']},name:{required:true,message:'请输入等级名称',trigger:['input','blur']}};const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3'},labelWidth:80,schemas});const loadDataTable=res=>List({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();const reset=(o,v)=>{Object.keys(o).forEach(k=>delete o[k]);Object.assign(o,v)};
|
||||
const actionColumn=reactive({width:210,title:'操作',key:'action',fixed:'right',render:row=>h(TableAction as any,{style:'button',actions:[{label:'编辑',onClick:()=>openEdit(row),auth:['/memberLevel/view']},{label:row.status===1?'停用':'启用',onClick:()=>toggleStatus(row),auth:['/memberLevel/status']},{label:'删除',type:'error',onClick:()=>remove(row),auth:['/memberLevel/delete']}]})});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||
function emptyForm(){return{id:0,levelCode:'',name:'',iconAttachmentId:0,iconUrl:'',upgradeYuan:0,giftYuan:0,dailyOrderLimit:0,commissionPercent:0,dailyWithdrawLimit:0,sort:0,status:1,remark:''}}
|
||||
async function openEdit(row?){reset(form,emptyForm());showEdit.value=true;if(!row)return;const r:any=await View({id:row.id});const v=r.published||r.draft;Object.assign(form,{id:r.level.id,levelCode:r.level.levelCode,name:r.level.name,iconAttachmentId:r.level.iconAttachmentId,iconUrl:r.level.iconUrl,sort:r.level.sort,status:r.level.status,remark:r.level.remark||'',upgradeYuan:(v?.upgradeAmount||0)/100,giftYuan:(v?.giftAmount||0)/100,dailyOrderLimit:v?.dailyOrderLimit||0,commissionPercent:(v?.commissionRate||0)/100,dailyWithdrawLimit:v?.dailyWithdrawLimit||0})}
|
||||
async function saveLevel(){try{await formRef.value?.validate()}catch{return}saving.value=true;try{await Save({...form,upgradeAmount:Math.round(form.upgradeYuan*100),giftAmount:Math.round(form.giftYuan*100),commissionRate:Math.round(form.commissionPercent*100)});message.success('保存成功');showEdit.value=false;reloadTable();dict.loadOptions(['memberLevelOption'])}finally{saving.value=false}}
|
||||
async function toggleStatus(row){await Status({id:row.id,status:row.status===1?2:1});message.success('状态已更新');reloadTable()}
|
||||
function remove(row){dialog.warning({title:'删除等级',content:'仅无会员且未被任务大厅引用的等级可删除。',positiveText:'确认删除',negativeText:'取消',onPositiveClick:async()=>{await Delete({id:row.id,reason:'后台删除未使用等级'});message.success('删除成功');reloadTable()}})}
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
<style lang="less" scoped>:deep(.level-modal){width:min(980px,94vw)}</style>
|
||||
@@ -0,0 +1,18 @@
|
||||
import { computed,h,ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { renderImage } from '@/utils';
|
||||
const dict=useDictStore();
|
||||
export const schemas=ref<FormSchema[]>([
|
||||
{field:'name',component:'NInput',label:'等级名称',componentProps:{placeholder:'请输入等级名称'}},
|
||||
{field:'status',component:'NSelect',label:'状态',componentProps:{options:computed(()=>dict.getOptionUnRef('sys_normal_disable')),clearable:true}},
|
||||
]);
|
||||
const money=(v)=>`¥${((v||0)/100).toFixed(2)}`;const tag=(type,value)=>h(NTag,{type:dict.getType(type,value),bordered:false},{default:()=>dict.getLabel(type,value)||'-'});
|
||||
export const columns=[
|
||||
{title:'ID',key:'id',width:70},{title:'图标',key:'iconUrl',width:80,render:r=>renderImage(r.iconUrl)},{title:'等级编码',key:'levelCode',width:120},{title:'等级名称',key:'name',width:120},
|
||||
{title:'升级价格',key:'upgradeAmount',width:110,render:r=>money(r.upgradeAmount)},{title:'赠送金额',key:'giftAmount',width:110,render:r=>money(r.giftAmount)},
|
||||
{title:'接单次数/日',key:'dailyOrderLimit',width:110},{title:'佣金比例',key:'commissionRate',width:100,render:r=>`${((r.commissionRate||0)/100).toFixed(2)}%`},{title:'提现次数/日',key:'dailyWithdrawLimit',width:110},
|
||||
{title:'状态',key:'status',width:90,render:r=>tag('sys_normal_disable',r.status)},{title:'会员数',key:'memberCount',width:90},{title:'创建时间',key:'createdAt',width:180},
|
||||
];
|
||||
export const loadOptions=()=>dict.loadOptions(['sys_normal_disable']);
|
||||
@@ -0,0 +1,292 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header">
|
||||
<n-card :bordered="false" title="会员列表">会员资料默认脱敏展示;余额、禁用与奖励均通过审批流程处理。</n-card>
|
||||
</div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" @keyup.enter="reloadTable" />
|
||||
<div ref="memberTableWrapRef" class="member-table-wrap">
|
||||
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="(row) => row.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000">
|
||||
<template #tableTitle>
|
||||
<n-button v-if="hasPermission(['/appMember/edit'])" type="primary" @click="openEdit()">新增会员</n-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
<div class="action-column-scrollbar" aria-label="操作栏横向滚动条" @scroll="syncActionScroll">
|
||||
<div class="action-column-scrollbar-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</n-card>
|
||||
|
||||
<n-modal v-model:show="showEdit" preset="card" :title="editForm.id ? '编辑会员' : '新增会员'" class="edit-modal">
|
||||
<n-spin :show="editLoading">
|
||||
<n-form ref="editFormRef" :model="editForm" :rules="editRules" label-placement="left" label-width="100">
|
||||
<n-form-item label="标记">
|
||||
<n-checkbox :checked="editForm.isTestAccount === 1" @update:checked="editForm.isTestAccount = $event ? 1 : 2">标记</n-checkbox>
|
||||
</n-form-item>
|
||||
<n-form-item label="用户名称" path="username" required><n-input v-model:value="editForm.username" placeholder="请输入用户名称" /></n-form-item>
|
||||
<n-form-item label="手机号码" path="mobile" :required="!editForm.id"><n-input v-model:value="editForm.mobile" :placeholder="editForm.id ? editForm.mobileMask || '留空不修改' : '请输入手机号码'" /></n-form-item>
|
||||
<n-form-item label="账户余额"><n-input :value="money(editForm.availableAmount)" disabled /></n-form-item>
|
||||
<n-form-item label="冻结金额"><n-input :value="money(editForm.frozenAmount)" disabled /></n-form-item>
|
||||
<n-form-item label="会员等级"><n-select v-model:value="editForm.levelId" :options="memberLevelOptions" placeholder="请选择会员等级" /></n-form-item>
|
||||
<n-form-item label="交易状态"><n-select v-model:value="editForm.riskStatus" :options="dict.getOptionUnRef('appMemberRiskStatus')" /></n-form-item>
|
||||
<n-form-item label="登录密码" path="password" :required="!editForm.id"><n-input v-model:value="editForm.password" type="password" show-password-on="click" :placeholder="editForm.id ? '留空不修改' : '至少8位,含字母和数字'" /></n-form-item>
|
||||
<n-form-item label="交易密码" path="tradePassword"><n-input v-model:value="editForm.tradePassword" type="password" show-password-on="click" placeholder="留空不修改" /></n-form-item>
|
||||
<n-form-item label="上级ID"><n-input-number v-model:value="editForm.parentId" :min="0" class="form-number" /></n-form-item>
|
||||
<n-form-item label="信用分"><n-input-number v-model:value="editForm.creditScore" :min="0" :max="1000" class="form-number" /></n-form-item>
|
||||
</n-form>
|
||||
</n-spin>
|
||||
<template #action><n-space justify="end"><n-button type="primary" :loading="saving" @click="saveMember">确定</n-button><n-button @click="showEdit=false">取消</n-button></n-space></template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showDetail" preset="card" title="会员详情" class="member-modal">
|
||||
<n-spin :show="detailLoading">
|
||||
<n-descriptions v-if="detail.id" bordered :column="2" label-placement="left">
|
||||
<n-descriptions-item label="会员">{{ detail.username }}(#{{ detail.id }})</n-descriptions-item>
|
||||
<n-descriptions-item label="会员编号">{{ detail.memberNo }}</n-descriptions-item>
|
||||
<n-descriptions-item label="手机号">{{ detail.mobileMask }}</n-descriptions-item>
|
||||
<n-descriptions-item label="真实姓名">{{ detail.realNameMask || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="证件号">{{ detail.idNoMask || '-' }}</n-descriptions-item>
|
||||
<n-descriptions-item label="团队层级">{{ detail.level }}</n-descriptions-item>
|
||||
<n-descriptions-item label="可用余额">{{ money(detail.availableAmount) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="冻结余额">{{ money(detail.frozenAmount) }}</n-descriptions-item>
|
||||
<n-descriptions-item label="收款账户">{{ detail.paymentAccountCount }} 个</n-descriptions-item>
|
||||
<n-descriptions-item label="收货地址">{{ detail.addressCount }} 个</n-descriptions-item>
|
||||
<n-descriptions-item label="下级团队">{{ detail.teamCount }} 人</n-descriptions-item>
|
||||
<n-descriptions-item label="注册时间">{{ detail.createdAt }}</n-descriptions-item>
|
||||
</n-descriptions>
|
||||
<n-divider />
|
||||
<n-space>
|
||||
<n-button v-if="hasPermission(['/appMember/paymentList'])" @click="openRelated('payment')">收款账户</n-button>
|
||||
<n-button v-if="hasPermission(['/appMember/addressList'])" @click="openRelated('address')">收货地址</n-button>
|
||||
<n-button v-if="hasPermission(['/appMember/team'])" @click="openTeam">团队查询</n-button>
|
||||
<n-button v-if="hasPermission(['/appMember/sensitive'])" type="warning" @click="openSensitive('member', detail.id)">授权查看明文</n-button>
|
||||
</n-space>
|
||||
</n-spin>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showRelated" preset="card" :title="relatedType === 'payment' ? '编辑银行卡信息' : '编辑收货地址信息'" class="related-modal">
|
||||
<n-spin :show="relatedLoading">
|
||||
<n-form label-placement="left" label-width="110">
|
||||
<template v-if="relatedType === 'payment'">
|
||||
<n-form-item label="开户人"><n-input v-model:value="paymentForm.holder" placeholder="请输入开户人" /></n-form-item>
|
||||
<n-form-item label="开户银行"><n-input v-model:value="paymentForm.bankName" placeholder="请输入开户银行" /></n-form-item>
|
||||
<n-form-item label="开户电话"><n-input v-model:value="paymentForm.mobile" placeholder="请输入开户电话" /></n-form-item>
|
||||
<n-form-item label="银行卡号"><n-input v-model:value="paymentForm.bankAccount" placeholder="请输入银行卡号" /></n-form-item>
|
||||
<n-form-item label="数字钱包"><n-input v-model:value="paymentForm.digitalWallet" placeholder="请输入数字钱包" /></n-form-item>
|
||||
<n-form-item label="云闪付账号"><n-input v-model:value="paymentForm.unionPayAccount" placeholder="请输入云闪付账号" /></n-form-item>
|
||||
<n-form-item label="收款码">
|
||||
<n-grid :cols="2" :x-gap="16">
|
||||
<n-gi><div class="upload-title">支付宝收款码</div><UploadImage v-model:value="paymentForm.alipayQrCode" :max-number="1" /></n-gi>
|
||||
<n-gi><div class="upload-title">微信收款码</div><UploadImage v-model:value="paymentForm.wechatQrCode" :max-number="1" /></n-gi>
|
||||
</n-grid>
|
||||
</n-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-form-item label="真实姓名"><n-input v-model:value="addressForm.receiver" placeholder="请输入真实姓名" /></n-form-item>
|
||||
<n-form-item label="联系电话"><n-input v-model:value="addressForm.mobile" placeholder="请输入联系电话" /></n-form-item>
|
||||
<n-form-item label="地区"><n-input v-model:value="addressForm.region" placeholder="请输入地区" /></n-form-item>
|
||||
<n-form-item label="详细地址"><n-input v-model:value="addressForm.address" placeholder="请输入详细地址" /></n-form-item>
|
||||
</template>
|
||||
</n-form>
|
||||
</n-spin>
|
||||
<template #action><n-space justify="end"><n-button type="primary" :loading="relatedSaving" @click="saveRelated">确定</n-button><n-button @click="showRelated=false">取消</n-button></n-space></template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showTeam" preset="card" title="团队查询" class="member-modal">
|
||||
<n-space vertical>
|
||||
<n-space>
|
||||
<n-button type="error" @click="openStatus({id: detail.id, memberStatus: 1})">封禁</n-button>
|
||||
<n-button type="error" @click="submitTeamStatus(2)">封禁团队</n-button>
|
||||
<n-button type="success" @click="submitTeamStatus(1)">解封团队</n-button>
|
||||
</n-space>
|
||||
<n-space><span>查询层级</span><n-input-number v-model:value="teamLevel" :min="1" :max="10" /><n-button type="primary" @click="loadTeam">查询</n-button></n-space>
|
||||
<n-data-table :columns="teamColumns" :data="teamRows" :bordered="false" />
|
||||
</n-space>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showLedger" preset="card" :title="`账变记录 - ${ledgerMember?.username || ''}`" class="member-modal">
|
||||
<n-data-table :columns="ledgerColumns" :data="ledgerRows" :loading="ledgerLoading" :bordered="false" />
|
||||
<n-empty v-if="!ledgerLoading && ledgerRows.length === 0" description="暂无账变记录" />
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showSensitive" preset="dialog" title="授权查看敏感信息" :show-icon="false">
|
||||
<n-alert type="error">明文仅用于当前业务目的,请勿复制传播。本次操作将记录查看人、对象、用途和结果。</n-alert>
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="用途编码"><n-input v-model:value="sensitiveForm.purposeCode" placeholder="如 customer_service_verify" /></n-form-item>
|
||||
<n-form-item label="用途说明"><n-input v-model:value="sensitiveForm.purposeText" type="textarea" /></n-form-item>
|
||||
<n-checkbox v-model:checked="sensitiveForm.confirm">我确认业务需要并接受留痕</n-checkbox>
|
||||
</n-form>
|
||||
<n-code v-if="sensitiveResult" :code="sensitiveResult" language="json" word-wrap />
|
||||
<template #action><n-space><n-button @click="showSensitive=false">关闭</n-button><n-button type="error" @click="revealSensitive">确认查看</n-button></n-space></template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showStatus" preset="dialog" title="信息" :show-icon="false">
|
||||
<div>确定{{ statusForm.toStatus === 1 ? '启用' : '禁用' }}此用户吗?</div>
|
||||
<template #action><n-space><n-button type="primary" @click="submitStatus">确定</n-button><n-button @click="showStatus=false">取消</n-button></n-space></template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showWallet" preset="dialog" title="奖励金" :show-icon="false">
|
||||
<n-form label-placement="left" label-width="70"><n-form-item label="金额"><n-input-number v-model:value="walletYuan" :min="0.01" :precision="2" class="form-number" placeholder="请输入赠送的金额" /></n-form-item></n-form>
|
||||
<template #action><n-space><n-button type="primary" @click="submitWallet">确定</n-button><n-button @click="showWallet=false">取消</n-button></n-space></template>
|
||||
</n-modal>
|
||||
|
||||
<n-modal v-model:show="showNotice" preset="card" title="发送弹幕" class="related-modal">
|
||||
<n-form label-placement="left" label-width="100">
|
||||
<n-form-item label="用户ID"><n-input :value="String(noticeMember?.id || '')" disabled /></n-form-item>
|
||||
<n-form-item label="用户名"><n-input :value="noticeMember?.username || ''" disabled /></n-form-item>
|
||||
<n-form-item label="用户手机"><n-input :value="noticeMember?.mobileMask || ''" disabled /></n-form-item>
|
||||
<n-form-item label="发送内容"><n-input v-model:value="noticeForm.content" placeholder="请输入发送内容" /></n-form-item>
|
||||
</n-form>
|
||||
<template #action><n-space><n-button type="primary" @click="submitNotice">确定</n-button><n-button @click="showNotice=false">取消</n-button></n-space></template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, onUpdated, reactive, ref } from 'vue';
|
||||
import { NButton, NSpace, useDialog, useMessage } from 'naive-ui';
|
||||
import { DeleteOutlined, EditOutlined } from '@vicons/antd';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import UploadImage from '@/components/Upload/uploadImage.vue';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { List, View, Edit, Delete, AutoDispatch, Category, PaymentList, PaymentEdit, PaymentDelete, AddressList, AddressEdit, AddressDelete, Sensitive, Team, StatusRequest, LedgerList, WalletRequest, NoticeEdit } from '@/api/appMember';
|
||||
import { List as MemberLevelList } from '@/api/memberLevel';
|
||||
import { columns, schemas, loadOptions, MemberRecord } from './model';
|
||||
|
||||
const message=useMessage(), dialog=useDialog(), dict=useDictStore();
|
||||
const { hasPermission }=usePermission(); const actionRef=ref(), searchFormRef=ref<any>({}), editFormRef=ref<any>(), memberTableWrapRef=ref<HTMLElement|null>(null);
|
||||
const [register]=useForm({ gridProps:{cols:'1 s:1 m:2 l:3 xl:4 2xl:4'}, labelWidth:80, schemas });
|
||||
const showEdit=ref(false),showDetail=ref(false),showRelated=ref(false),showRelatedForm=ref(false),showTeam=ref(false),showLedger=ref(false),showSensitive=ref(false),showStatus=ref(false),showWallet=ref(false),showNotice=ref(false),saving=ref(false),detailLoading=ref(false),ledgerLoading=ref(false),editLoading=ref(false),relatedLoading=ref(false),relatedSaving=ref(false);
|
||||
const detail=ref<any>({}),relatedRows=ref<any[]>([]),teamRows=ref<any[]>([]),relatedType=ref<'payment'|'address'>('payment'),teamLevel=ref(3),sensitiveResult=ref(''),walletYuan=ref<number|null>(null),memberLevelOptions=ref<any[]>([]),noticeMember=ref<MemberRecord|null>(null);
|
||||
const editForm=reactive<any>({}),paymentForm=reactive<any>({}),addressForm=reactive<any>({}),sensitiveForm=reactive<any>({targetType:'member',targetId:0,purposeCode:'',purposeText:'',confirm:false}),statusForm=reactive<any>({memberId:0,toStatus:2,reasonCode:'',reasonText:''}),walletForm=reactive<any>({memberId:0,adjustType:1,direction:1,reasonCode:'',reasonText:''});
|
||||
const editRules={
|
||||
username:[{required:true,message:'请输入用户名称',trigger:['input','blur']},{min:3,max:64,message:'用户名长度为3到64个字符',trigger:['input','blur']}],
|
||||
mobile:{validator:(_rule,value)=>editForm.id||String(value||'').trim()?true:new Error('请输入手机号码'),trigger:['input','blur']},
|
||||
password:{validator:(_rule,value)=>{const text=String(value||'');if(editForm.id&&!text)return true;if(text.length<8||!/[A-Za-z]/.test(text)||!/\d/.test(text))return new Error('密码至少8位且必须同时包含字母和数字');return true},trigger:['input','blur']},
|
||||
tradePassword:{validator:(_rule,value)=>!value||String(value).length>=6?true:new Error('交易密码不能少于6位'),trigger:['input','blur']},
|
||||
};
|
||||
const noticeForm=reactive({memberId:0,title:'',content:'',sourceType:2});
|
||||
const noticeDateRange=ref<[string,string]>(['','']);
|
||||
const ledgerRows=ref<any[]>([]),ledgerMember=ref<MemberRecord|null>(null);
|
||||
const reset=(obj,value)=>{Object.keys(obj).forEach(k=>delete obj[k]);Object.assign(obj,value)};
|
||||
const loadDataTable=(res)=>List({...searchFormRef.value?.formModel,...res}); const reloadTable=()=>actionRef.value?.reload();
|
||||
const money=(v)=>`¥${((v||0)/100).toFixed(2)}`;
|
||||
const actionColumn=reactive({width:180,title:'操作',key:'action',fixed:'right',render(record:MemberRecord){return h('div',{class:'member-action-viewport'},[
|
||||
h('div',{class:'member-action-grid'},[
|
||||
h(TableAction as any,{style:'button',actions:[
|
||||
{label:'编辑',type:'primary',icon:EditOutlined,onClick:()=>openEdit(record),auth:['/appMember/edit']},
|
||||
{label:'删除',type:'error',icon:DeleteOutlined,onClick:()=>removeMember(record),auth:['/appMember/delete']},
|
||||
{label:'银行卡信息',type:'primary',onClick:()=>openRelated('payment',record.id),auth:['/appMember/paymentList']},
|
||||
{label:'地址信息',type:'primary',onClick:()=>openRelated('address',record.id),auth:['/appMember/addressList']},
|
||||
{label:'查看团队',type:'warning',onClick:()=>openTeam(record.id),auth:['/appMember/team']},
|
||||
]}),
|
||||
h(TableAction as any,{style:'button',actions:[
|
||||
{label:'账变',type:'warning',onClick:()=>openLedger(record),auth:['/appMember/walletRequest']},
|
||||
{label:'弹幕',color:'#009688',onClick:()=>openNotice(record),auth:['/appMember/noticeEdit']},
|
||||
{label:'奖励金',color:'#008f83',onClick:()=>openWallet(record,true),auth:['/appMember/walletRequest']},
|
||||
{label:record.memberCategory===2?'设为真人':'设为假人',type:'error',onClick:()=>toggleCategory(record),auth:['/appMember/edit']},
|
||||
{label:record.memberStatus===2?'启用':'禁用',type:record.memberStatus===2?'success':'error',onClick:()=>openStatus(record),auth:['/appMember/statusRequest']},
|
||||
]}),
|
||||
]),
|
||||
])}}); const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||
let actionScrollLeft=0;
|
||||
function syncActionViewports(left:number){memberTableWrapRef.value?.querySelectorAll<HTMLElement>('.member-action-viewport').forEach((el)=>{el.scrollLeft=left})}
|
||||
function syncActionScroll(event:Event){const el=event.currentTarget as HTMLElement;const externalMax=el.scrollWidth-el.clientWidth;const viewport=memberTableWrapRef.value?.querySelector<HTMLElement>('.member-action-viewport');const actionMax=viewport?viewport.scrollWidth-viewport.clientWidth:0;actionScrollLeft=externalMax>0?el.scrollLeft*actionMax/externalMax:0;syncActionViewports(actionScrollLeft)}
|
||||
async function openEdit(row?:MemberRecord){
|
||||
reset(editForm,{id:row?.id||0,username:row?.username||'',mobile:'',mobileMask:row?.mobileMask||'',password:'',tradePassword:'',parentId:row?.parentId||0,levelId:0,riskStatus:row?.riskStatus||1,creditScore:100,autoDispatch:row?.autoDispatch||2,memberCategory:row?.memberCategory||1,isTestAccount:row?.isTestAccount||2,availableAmount:row?.availableAmount||0,frozenAmount:row?.frozenAmount||0});
|
||||
showEdit.value=true;
|
||||
if(!row?.id)return;
|
||||
editLoading.value=true;
|
||||
try{
|
||||
const data:any=await View({id:row.id});
|
||||
Object.assign(editForm,{levelId:data.levelId||0,riskStatus:data.riskStatus||1,creditScore:data.creditScore??100,availableAmount:data.availableAmount||0,frozenAmount:data.frozenAmount||0,mobileMask:data.mobileMask||row.mobileMask});
|
||||
}finally{editLoading.value=false}
|
||||
}
|
||||
async function saveMember(){try{await editFormRef.value?.validate()}catch{return}saving.value=true;try{editForm.memberCategory=editForm.isTestAccount===1?2:1;await Edit(editForm);message.success('保存成功');showEdit.value=false;reloadTable()}finally{saving.value=false}}
|
||||
async function openDetail(id){showDetail.value=true;detailLoading.value=true;try{detail.value=await View({id})}finally{detailLoading.value=false}}
|
||||
async function toggleDispatch(row){await AutoDispatch({id:row.id,autoDispatch:row.autoDispatch===1?2:1});message.success('更新成功');reloadTable()}
|
||||
function openStatus(row){const action=row.memberStatus===1?'禁用':'启用';reset(statusForm,{memberId:row.id,toStatus:row.memberStatus===1?2:1,reasonCode:'manual_operation',reasonText:`后台管理员${action}会员`});showStatus.value=true}
|
||||
async function submitStatus(){await StatusRequest(statusForm);message.success('操作已提交');showStatus.value=false}
|
||||
function openWallet(row,rewardOnly=false){reset(walletForm,{memberId:row.id,adjustType:rewardOnly?1:2,direction:1,reasonCode:'member_reward',reasonText:'后台奖励金'});walletYuan.value=null;showWallet.value=true}
|
||||
async function submitWallet(){if(walletYuan.value===null||walletYuan.value<=0){message.warning('请输入大于0的金额');return}const params={...walletForm,amount:Math.round(walletYuan.value*100),idempotencyKey:`wallet-${walletForm.memberId}-${Date.now()}`};const r:any=await WalletRequest(params);message.success(`审批已提交:${r.approvalNo}`);showWallet.value=false}
|
||||
function removeMember(row){dialog.warning({title:'信息',content:'确定删除此记录吗?',positiveText:'确定',negativeText:'取消',onPositiveClick:async()=>{await Delete({id:row.id,reason:'后台管理员删除'});message.success('会员已删除');reloadTable()}})}
|
||||
async function openRelated(type:'payment'|'address',memberId=detail.value.id){
|
||||
detail.value={...detail.value,id:memberId};relatedType.value=type;showRelated.value=true;showRelatedForm.value=false;relatedLoading.value=true;
|
||||
try{
|
||||
await loadRelated();
|
||||
if(type==='payment'){
|
||||
const byType=(accountType:number)=>relatedRows.value.find(item=>item.accountType===accountType);
|
||||
const bank=byType(1);
|
||||
reset(paymentForm,{memberId,id:bank?.id||0,holder:'',bankName:bank?.bankName||'',mobile:'',bankAccount:'',digitalWallet:'',unionPayAccount:'',alipayQrCode:'',wechatQrCode:'',records:{1:bank,2:byType(2),3:byType(3),4:byType(4),5:byType(5)}});
|
||||
}else{
|
||||
const row=relatedRows.value.find(item=>item.isDefault===1)||relatedRows.value[0];
|
||||
const region=[row?.province,row?.city,row?.district].filter(Boolean).join('');
|
||||
reset(addressForm,{id:row?.id||0,memberId,receiver:'',mobile:'',region,originalRegion:region,province:row?.province||'',city:row?.city||'',district:row?.district||'',address:'',isDefault:1});
|
||||
}
|
||||
}finally{relatedLoading.value=false}
|
||||
}
|
||||
async function loadRelated(){const r:any=relatedType.value==='payment'?await PaymentList({memberId:detail.value.id}):await AddressList({memberId:detail.value.id});relatedRows.value=r.list||[]}
|
||||
function startRelatedAdd(){showRelatedForm.value=true;if(relatedType.value==='payment')reset(paymentForm,{id:0,memberId:detail.value.id,accountType:1,bankName:'',holder:'',mobile:'',accountNo:'',verifyStatus:1,isDefault:2});else reset(addressForm,{id:0,memberId:detail.value.id,receiver:'',mobile:'',province:'',city:'',district:'',address:'',isDefault:2})}
|
||||
async function saveRelated(){
|
||||
relatedSaving.value=true;
|
||||
try{
|
||||
if(relatedType.value==='payment'){
|
||||
const common={memberId:paymentForm.memberId,holder:paymentForm.holder,mobile:paymentForm.mobile,verifyStatus:1,isDefault:1};
|
||||
const accounts=[
|
||||
{type:1,value:paymentForm.bankAccount,bankName:paymentForm.bankName},
|
||||
{type:2,value:paymentForm.digitalWallet,bankName:''},
|
||||
{type:3,value:paymentForm.unionPayAccount,bankName:''},
|
||||
{type:4,value:paymentForm.alipayQrCode,bankName:''},
|
||||
{type:5,value:paymentForm.wechatQrCode,bankName:''},
|
||||
];
|
||||
for(const account of accounts){
|
||||
const old=paymentForm.records?.[account.type];
|
||||
if(account.value||old?.id)await PaymentEdit({...common,id:old?.id||0,accountType:account.type,accountNo:account.value,bankName:account.bankName,isDefault:account.type===1?1:2});
|
||||
}
|
||||
}else{
|
||||
const regionChanged=addressForm.region!==addressForm.originalRegion;
|
||||
await AddressEdit({...addressForm,province:regionChanged?addressForm.region:addressForm.province,city:regionChanged?'':addressForm.city,district:regionChanged?'':addressForm.district});
|
||||
}
|
||||
message.success('保存成功');showRelated.value=false;
|
||||
}finally{relatedSaving.value=false}
|
||||
}
|
||||
async function deleteRelated(id){if(relatedType.value==='payment')await PaymentDelete({id});else await AddressDelete({id});message.success('删除成功');await loadRelated()}
|
||||
function editRelated(row){showRelatedForm.value=true;if(relatedType.value==='payment')reset(paymentForm,{id:row.id,memberId:row.memberId,accountType:row.accountType,bankName:row.bankName,holder:'',mobile:'',accountNo:'',verifyStatus:row.verifyStatus,isDefault:row.isDefault});else reset(addressForm,{id:row.id,memberId:row.memberId,receiver:'',mobile:'',province:row.province,city:row.city,district:row.district,address:'',isDefault:row.isDefault})}
|
||||
function openSensitive(type,targetId){reset(sensitiveForm,{targetType:type,targetId,purposeCode:'',purposeText:'',confirm:false});sensitiveResult.value='';showSensitive.value=true}
|
||||
async function revealSensitive(){const r:any=await Sensitive(sensitiveForm);sensitiveResult.value=JSON.stringify({accessNo:r.accessNo,...r.fields},null,2)}
|
||||
async function openTeam(memberId=detail.value.id){detail.value={...detail.value,id:memberId};showTeam.value=true;await loadTeam()} async function loadTeam(){const r:any=await Team({memberId:detail.value.id,maxLevel:teamLevel.value,page:1,pageSize:100});teamRows.value=r.list||[]}
|
||||
function submitTeamStatus(toStatus:number){const action=toStatus===2?'封禁':'解封';const targets=teamRows.value.filter(row=>row.memberStatus!==toStatus);if(targets.length===0){message.warning(`没有可${action}的团队成员`);return}dialog.warning({title:`${action}团队`,content:`确定提交 ${targets.length} 名团队成员的${action}审批吗?`,positiveText:'确定提交',negativeText:'取消',onPositiveClick:async()=>{for(const row of targets){await StatusRequest({memberId:row.id,toStatus,reasonCode:'risk_control',reasonText:`后台批量${action}团队`})}message.success(`${action}团队审批已提交`);showTeam.value=false}})}
|
||||
async function openLedger(row:MemberRecord){ledgerMember.value=row;showLedger.value=true;ledgerLoading.value=true;try{const r:any=await LedgerList({memberId:row.id,page:1,pageSize:100});ledgerRows.value=r.list||[]}finally{ledgerLoading.value=false}}
|
||||
function toggleCategory(row:MemberRecord){const next=row.memberCategory===2?1:2;dialog.warning({title:'信息',content:`确定设为${next===2?'假人':'真人'}吗?`,positiveText:'确定',negativeText:'取消',onPositiveClick:async()=>{await Category({id:row.id,memberCategory:next});message.success('会员类别已更新');reloadTable()}})}
|
||||
function formatDate(value:Date){const pad=(n:number)=>String(n).padStart(2,'0');return `${value.getFullYear()}-${pad(value.getMonth()+1)}-${pad(value.getDate())} ${pad(value.getHours())}:${pad(value.getMinutes())}:${pad(value.getSeconds())}`}
|
||||
function openNotice(row){const start=new Date(),end=new Date(start.getTime()+30*24*60*60*1000);noticeMember.value=row;reset(noticeForm,{memberId:row.id,title:'会员弹幕',content:'',sourceType:2});noticeDateRange.value=[formatDate(start),formatDate(end)];showNotice.value=true}
|
||||
async function submitNotice(){if(!noticeForm.content.trim()){message.warning('请填写弹幕内容');return}await NoticeEdit({...noticeForm,startAt:noticeDateRange.value[0],endAt:noticeDateRange.value[1]});message.success('弹幕已发送');showNotice.value=false}
|
||||
const relatedColumns=computed(()=>relatedType.value==='payment'?[{title:'类型',key:'accountType',render:r=>dict.getLabel('paymentAccountType',r.accountType)},{title:'持有人',key:'holderMask'},{title:'账号',key:'accountNoMask'},{title:'银行',key:'bankName'},{title:'操作',key:'action',render:r=>h(NSpace,{},()=>[h(NButton,{size:'small',onClick:()=>editRelated(r)},()=> '编辑'),h(NButton,{size:'small',onClick:()=>openSensitive('payment',r.id)},()=> '查看明文'),h(NButton,{size:'small',type:'error',onClick:()=>deleteRelated(r.id)},()=> '删除')])}]:[{title:'收货人',key:'receiverMask'},{title:'手机号',key:'mobileMask'},{title:'地区',key:'province',render:r=>r.province+r.city+r.district},{title:'地址',key:'addressMask'},{title:'操作',key:'action',render:r=>h(NSpace,{},()=>[h(NButton,{size:'small',onClick:()=>editRelated(r)},()=> '编辑'),h(NButton,{size:'small',onClick:()=>openSensitive('address',r.id)},()=> '查看明文'),h(NButton,{size:'small',type:'error',onClick:()=>deleteRelated(r.id)},()=> '删除')])}]);
|
||||
const teamColumns=[{title:'层级',key:'relativeLevel'},{title:'会员ID',key:'id'},{title:'用户名',key:'username'},{title:'手机号',key:'mobileMask'},{title:'上级ID',key:'parentId'},{title:'可用余额',key:'availableAmount',render:r=>money(r.availableAmount)},{title:'注册时间',key:'createdAt'}];
|
||||
const ledgerColumns=[{title:'流水号',key:'ledgerNo',width:190},{title:'业务类型',key:'bizType',width:130},{title:'业务单号',key:'bizNo',width:170},{title:'方向',key:'direction',width:80,render:r=>dict.getLabel('walletDirection',r.direction)},{title:'发生金额',key:'amount',width:110,render:r=>money(r.amount)},{title:'变更前',key:'beforeAmount',width:110,render:r=>money(r.beforeAmount)},{title:'变更后',key:'afterAmount',width:110,render:r=>money(r.afterAmount)},{title:'说明',key:'remark',width:180},{title:'发生时间',key:'occurredAt',width:180}];
|
||||
onMounted(async()=>{loadOptions();const r:any=await MemberLevelList({page:1,pageSize:100,status:1});memberLevelOptions.value=(r.list||[]).map(item=>({label:item.name,value:item.id}))});
|
||||
onUpdated(()=>syncActionViewports(actionScrollLeft));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:global(.member-modal){width:980px;max-width:94vw}
|
||||
:global(.edit-modal){width:720px;max-width:92vw}
|
||||
:global(.related-modal){width:680px;max-width:92vw}
|
||||
.form-number{width:100%}
|
||||
.upload-title{margin-bottom:8px;color:var(--n-label-text-color)}
|
||||
.member-table-wrap{position:relative}
|
||||
:deep(.member-action-viewport){width:100%;overflow-x:hidden}
|
||||
:deep(.member-action-grid){width:max-content;display:flex;flex-direction:column;align-items:flex-start;gap:5px}
|
||||
:deep(.member-action-viewport .tableAction){width:max-content;min-width:0}
|
||||
:deep(.member-action-viewport .tableAction>.flex){justify-content:flex-start!important;flex-wrap:nowrap}
|
||||
:deep(.member-action-viewport .n-button){height:24px;padding:0 7px;font-size:12px}
|
||||
.action-column-scrollbar{position:absolute;right:0;bottom:52px;z-index:5;width:180px;height:10px;overflow-x:scroll;overflow-y:hidden;background:var(--card-color)}
|
||||
.action-column-scrollbar-content{width:420px;height:1px}
|
||||
.action-column-scrollbar::-webkit-scrollbar{height:6px}
|
||||
.action-column-scrollbar::-webkit-scrollbar-track{background:#eef0f4;border-radius:5px}
|
||||
.action-column-scrollbar::-webkit-scrollbar-thumb{background:#aeb4bf;border-radius:5px}
|
||||
.action-column-scrollbar::-webkit-scrollbar-thumb:hover{background:#8c94a1}
|
||||
</style>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { defRangeShortcuts } from '@/utils/dateUtil';
|
||||
|
||||
const dict = useDictStore();
|
||||
|
||||
export interface MemberRecord {
|
||||
id: number; memberNo: string; username: string; mobileMask: string; realNameMask: string;
|
||||
availableAmount: number; frozenAmount: number; parentId: number; parentUsername: string;
|
||||
inviteCode: string; memberStatus: number; riskStatus: number; autoDispatch: number; memberCategory: number;
|
||||
isTestAccount: number; lastLoginAt?: string; lastLoginIpMask: string; lastLoginRegion: string;
|
||||
todayTradeCount: number; createdAt: string;
|
||||
}
|
||||
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{ field: 'username', component: 'NInput', label: '用户名', componentProps: { placeholder: '请输入用户名' } },
|
||||
{ field: 'mobile', component: 'NInput', label: '手机号', componentProps: { placeholder: '完整手机号精确查询' } },
|
||||
{ field: 'memberStatus', component: 'NSelect', label: '状态', componentProps: { options: computed(() => dict.getOptionUnRef('appMemberStatus')), clearable: true } },
|
||||
{ field: 'autoDispatch', component: 'NSelect', label: '派单模式', componentProps: { options: computed(() => dict.getOptionUnRef('appMemberDispatchMode')), clearable: true } },
|
||||
{ field: 'memberCategory', component: 'NSelect', label: '类别', componentProps: { options: computed(() => dict.getOptionUnRef('appMemberCategory')), clearable: true } },
|
||||
{ field: 'createdAt', component: 'NDatePicker', label: '注册时间', componentProps: { type: 'datetimerange', clearable: true, shortcuts: defRangeShortcuts() } },
|
||||
]);
|
||||
|
||||
const money = (value: number) => `¥${((value || 0) / 100).toFixed(2)}`;
|
||||
const tag = (type: string, value: number) => h(NTag, { type: dict.getType(type, value), bordered: false }, { default: () => dict.getLabel(type, value) || '-' });
|
||||
|
||||
export const columns = [
|
||||
{ title: 'ID', key: 'id', width: 80 },
|
||||
{ title: '会员编号', key: 'memberNo', width: 170 },
|
||||
{ title: '账号', key: 'username', width: 130 },
|
||||
{ title: '手机号', key: 'mobileMask', width: 130 },
|
||||
{ title: '绑卡姓名', key: 'realNameMask', width: 100 },
|
||||
{ title: '可用余额', key: 'availableAmount', width: 120, render: (row: MemberRecord) => money(row.availableAmount) },
|
||||
{ title: '冻结余额', key: 'frozenAmount', width: 120, render: (row: MemberRecord) => money(row.frozenAmount) },
|
||||
{ title: '上级用户', key: 'parentUsername', width: 120, render: (row: MemberRecord) => row.parentUsername || '-' },
|
||||
{ title: '邀请码', key: 'inviteCode', width: 110 },
|
||||
{ title: '状态', key: 'memberStatus', width: 90, render: (row: MemberRecord) => tag('appMemberStatus', row.memberStatus) },
|
||||
{ title: '类别', key: 'memberCategory', width: 90, render: (row: MemberRecord) => tag('appMemberCategory', row.memberCategory) },
|
||||
{ title: '派单模式', key: 'autoDispatch', width: 100, render: (row: MemberRecord) => tag('appMemberDispatchMode', row.autoDispatch) },
|
||||
{ title: '今日交易', key: 'todayTradeCount', width: 90 },
|
||||
{ title: '最后登录IP', key: 'lastLoginIpMask', width: 130, render: (row: MemberRecord) => row.lastLoginIpMask || '-' },
|
||||
{ title: '最后登录地区', key: 'lastLoginRegion', width: 140, render: (row: MemberRecord) => row.lastLoginRegion || '-' },
|
||||
{ title: '注册时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
|
||||
export function loadOptions() {
|
||||
dict.loadOptions(['appMemberStatus', 'appMemberRiskStatus', 'appMemberDispatchMode', 'appMemberCategory', 'paymentAccountType', 'walletAdjustType', 'walletDirection', 'sys_switch', 'member_disable_reason', 'wallet_adjust_reason']);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header"><n-card :bordered="false" title="会员通知">真实通知与运营模拟使用独立标签;运营模拟仅允许在非生产环境创建。</n-card></div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" />
|
||||
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="(row)=>row.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000">
|
||||
<template #tableTitle><n-button v-if="hasPermission(['/appMember/noticeEdit'])" type="primary" @click="openCreate">创建通知</n-button></template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
<n-modal v-model:show="showCreate" preset="card" title="创建会员通知" class="notice-modal">
|
||||
<n-form label-placement="left" label-width="100">
|
||||
<n-form-item label="接收会员ID"><n-input-number v-model:value="form.memberId" :min="0" /><n-text depth="3">0 表示全体会员</n-text></n-form-item>
|
||||
<n-form-item label="标题"><n-input v-model:value="form.title" /></n-form-item>
|
||||
<n-form-item label="内容"><n-input v-model:value="form.content" type="textarea" /></n-form-item>
|
||||
<n-form-item label="来源"><n-select v-model:value="form.sourceType" :options="dict.getOptionUnRef('memberNoticeSource')" /></n-form-item>
|
||||
<n-form-item label="有效期"><n-date-picker v-model:formatted-value="dateRange" type="datetimerange" value-format="yyyy-MM-dd HH:mm:ss" clearable /></n-form-item>
|
||||
</n-form>
|
||||
<template #action><n-space justify="end"><n-button @click="showCreate=false">取消</n-button><n-button type="primary" @click="createNotice">创建</n-button></n-space></template>
|
||||
</n-modal>
|
||||
</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';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { NoticeList, NoticeEdit, NoticeRevoke } from '@/api/appMember';
|
||||
import { columns, schemas, loadOptions } from './model';
|
||||
const message=useMessage(),dialog=useDialog(),dict=useDictStore();const {hasPermission}=usePermission();const actionRef=ref(),searchFormRef=ref<any>({}),showCreate=ref(false),dateRange=ref<[string,string]|null>(null);const form=reactive({memberId:0,title:'',content:'',sourceType:1});
|
||||
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3'},labelWidth:70,schemas});const loadDataTable=(res)=>NoticeList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();
|
||||
const actionColumn=reactive({width:100,title:'操作',key:'action',fixed:'right',render:(row)=>h(TableAction as any,{style:'button',actions:[{label:'撤回',disabled:row.noticeStatus===2,onClick:()=>revoke(row),auth:['/appMember/noticeRevoke']}]})});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||
function openCreate(){Object.assign(form,{memberId:0,title:'',content:'',sourceType:1});dateRange.value=null;showCreate.value=true}
|
||||
async function createNotice(){if(!dateRange.value){message.error('请选择有效期');return}await NoticeEdit({...form,startAt:dateRange.value[0],endAt:dateRange.value[1]});message.success('创建成功');showCreate.value=false;reloadTable()}
|
||||
function revoke(row){dialog.warning({title:'撤回通知',content:'撤回后前台不再展示,历史记录仍保留。',positiveText:'确认撤回',negativeText:'取消',onPositiveClick:async()=>{await NoticeRevoke({id:row.id,reason:'后台管理员撤回'});message.success('已撤回');reloadTable()}})}
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
<style lang="less" scoped>:deep(.notice-modal){width:min(720px,92vw)}</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
const dict = useDictStore();
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{ field: 'title', component: 'NInput', label: '标题', componentProps: { placeholder: '请输入标题' } },
|
||||
{ field: 'sourceType', component: 'NSelect', label: '来源', componentProps: { options: computed(() => dict.getOptionUnRef('memberNoticeSource')), clearable: true } },
|
||||
{ field: 'noticeStatus', component: 'NSelect', label: '状态', componentProps: { options: computed(() => dict.getOptionUnRef('memberNoticeStatus')), clearable: true } },
|
||||
]);
|
||||
const tag = (type, value) => h(NTag, { type: dict.getType(type, value), bordered: false }, { default: () => dict.getLabel(type, value) || '-' });
|
||||
export const columns = [
|
||||
{ title: 'ID', key: 'id', width: 80 }, { title: '接收会员', key: 'username', width: 120 },
|
||||
{ title: '标题', key: 'title', width: 180 }, { title: '内容', key: 'content', width: 320, ellipsis: { tooltip: true } },
|
||||
{ title: '来源', key: 'sourceType', width: 110, render: (r) => tag('memberNoticeSource', r.sourceType) },
|
||||
{ title: '状态', key: 'noticeStatus', width: 90, render: (r) => tag('memberNoticeStatus', r.noticeStatus) },
|
||||
{ title: '开始时间', key: 'startAt', width: 180 }, { title: '结束时间', key: 'endAt', width: 180 },
|
||||
{ title: '创建时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
export const loadOptions = () => dict.loadOptions(['memberNoticeSource', 'memberNoticeStatus']);
|
||||
@@ -0,0 +1,247 @@
|
||||
<template>
|
||||
<div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicTable
|
||||
ref="tableRef"
|
||||
openChecked
|
||||
:columns="columns"
|
||||
:request="loadTable"
|
||||
:row-key="(row) => row.id"
|
||||
:action-column="actionColumn"
|
||||
:scroll-x="scrollX"
|
||||
:pagination="pagination"
|
||||
:checked-row-keys="checkedIds"
|
||||
@update:checked-row-keys="onCheckedRow"
|
||||
>
|
||||
<template #tableTitle>
|
||||
<n-button v-if="hasPermission(['/taskHall/delete'])" type="primary" @click="batchRemove">
|
||||
删除
|
||||
</n-button>
|
||||
<n-button
|
||||
v-if="hasPermission(['/taskHall/edit'])"
|
||||
type="primary"
|
||||
class="min-left-space"
|
||||
@click="openEdit()"
|
||||
>
|
||||
添加
|
||||
</n-button>
|
||||
</template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
|
||||
<n-modal
|
||||
v-model:show="showEdit"
|
||||
preset="card"
|
||||
:title="form.id ? '编辑大厅任务' : '添加大厅任务'"
|
||||
class="hall-modal"
|
||||
>
|
||||
<n-spin :show="loading">
|
||||
<n-form ref="formRef" :model="form" :rules="rules" label-placement="left" label-width="90">
|
||||
<n-form-item label="名称" path="name">
|
||||
<n-input v-model:value="form.name" placeholder="请输入名称" />
|
||||
</n-form-item>
|
||||
<n-form-item label="会员等级" path="levelId">
|
||||
<n-select
|
||||
v-model:value="form.levelId"
|
||||
:options="dict.getOptionUnRef('memberLevelOption')"
|
||||
placeholder="请选择会员等级"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item label="比例">
|
||||
<n-input value="以会员为主" readonly />
|
||||
</n-form-item>
|
||||
<n-form-item label="简介" path="summary">
|
||||
<n-input v-model:value="form.summary" placeholder="请输入简介" />
|
||||
</n-form-item>
|
||||
<n-form-item label="图标" path="iconUrl">
|
||||
<UploadImage v-model:value="form.iconUrl" :max-number="1" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-spin>
|
||||
<template #action>
|
||||
<n-space justify="end">
|
||||
<n-button @click="showEdit = false">取消</n-button>
|
||||
<n-button type="primary" :loading="saving" @click="saveHall">确定</n-button>
|
||||
</n-space>
|
||||
</template>
|
||||
</n-modal>
|
||||
</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 UploadImage from '@/components/Upload/uploadImage.vue';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { Delete, Edit, List, View } from '@/api/taskHall';
|
||||
import { columns, loadOptions } from './model';
|
||||
|
||||
const message = useMessage();
|
||||
const dialog = useDialog();
|
||||
const dict = useDictStore();
|
||||
const { hasPermission } = usePermission();
|
||||
const tableRef = ref();
|
||||
const formRef = ref();
|
||||
const checkedIds = ref<number[]>([]);
|
||||
const showEdit = ref(false);
|
||||
const loading = ref(false);
|
||||
const saving = ref(false);
|
||||
const form = reactive<any>({});
|
||||
const pagination = { pageSize: 30, pageSizes: [10, 20, 30, 40, 50, 60] };
|
||||
|
||||
const rules = {
|
||||
name: { required: true, message: '请输入名称', trigger: ['input', 'blur'] },
|
||||
levelId: { required: true, type: 'number', message: '请选择会员等级', trigger: 'change' },
|
||||
summary: { required: true, message: '请输入简介', trigger: ['input', 'blur'] },
|
||||
iconUrl: { required: true, message: '请上传图标', trigger: 'change' },
|
||||
};
|
||||
|
||||
const actionColumn = reactive({
|
||||
width: 180,
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
fixed: 'right',
|
||||
render: (row) =>
|
||||
h(TableAction as any, {
|
||||
style: 'button',
|
||||
actions: [
|
||||
{
|
||||
label: '编辑',
|
||||
type: 'primary',
|
||||
auth: ['/taskHall/view'],
|
||||
onClick: () => openEdit(row),
|
||||
},
|
||||
{
|
||||
label: '删除',
|
||||
type: 'error',
|
||||
auth: ['/taskHall/delete'],
|
||||
onClick: () => remove(row),
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
const scrollX = computed(() => adaTableScrollX(columns, actionColumn.width));
|
||||
|
||||
function emptyForm() {
|
||||
return {
|
||||
id: 0,
|
||||
version: 0,
|
||||
hallCode: '',
|
||||
name: '',
|
||||
levelId: null,
|
||||
iconAttachmentId: 0,
|
||||
iconUrl: '',
|
||||
summary: '',
|
||||
commissionRate: 0,
|
||||
clientRoute: '',
|
||||
sort: 0,
|
||||
remark: '',
|
||||
};
|
||||
}
|
||||
|
||||
function resetForm(value) {
|
||||
Object.keys(form).forEach((key) => delete form[key]);
|
||||
Object.assign(form, value);
|
||||
}
|
||||
|
||||
const loadTable = (params) => List(params);
|
||||
|
||||
function onCheckedRow(rowKeys: number[]) {
|
||||
checkedIds.value = rowKeys;
|
||||
}
|
||||
|
||||
function reload() {
|
||||
checkedIds.value = [];
|
||||
tableRef.value?.reload();
|
||||
}
|
||||
|
||||
async function openEdit(row?) {
|
||||
resetForm(emptyForm());
|
||||
showEdit.value = true;
|
||||
if (!row) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
const result: any = await View({ id: row.id });
|
||||
Object.assign(form, result, { levelId: result.levelIds?.[0] ?? null });
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveHall() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
saving.value = true;
|
||||
try {
|
||||
const hallCode = form.hallCode || `HALL_${Date.now()}`;
|
||||
await Edit({
|
||||
...form,
|
||||
hallCode,
|
||||
clientRoute: form.clientRoute || `/task-hall/${hallCode.toLowerCase()}`,
|
||||
levelIds: [form.levelId],
|
||||
});
|
||||
message.success(form.id ? '编辑成功' : '添加成功');
|
||||
showEdit.value = false;
|
||||
reload();
|
||||
dict.loadOptions(['taskHallOption']);
|
||||
} finally {
|
||||
saving.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function remove(row) {
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定删除此记录吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
await Delete({ id: row.id, reason: '后台删除任务大厅' });
|
||||
message.success('删除成功');
|
||||
reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function batchRemove() {
|
||||
if (checkedIds.value.length === 0) {
|
||||
message.warning('请选择数据');
|
||||
return;
|
||||
}
|
||||
dialog.warning({
|
||||
title: '提示',
|
||||
content: '确定删除吗?',
|
||||
positiveText: '确定',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
for (const id of checkedIds.value) {
|
||||
try {
|
||||
await Delete({ id, reason: '后台批量删除任务大厅' });
|
||||
successCount++;
|
||||
} catch {
|
||||
failedCount++;
|
||||
}
|
||||
}
|
||||
if (successCount > 0) message.success(`成功删除 ${successCount} 条数据`);
|
||||
if (failedCount > 0) message.warning(`${failedCount} 条数据删除失败`);
|
||||
reload();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.hall-modal) {
|
||||
width: min(750px, 94vw);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
import { renderImage } from '@/utils';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
|
||||
const dict = useDictStore();
|
||||
|
||||
export const columns = [
|
||||
{ title: 'ID', key: 'id', width: 90, sorter: true },
|
||||
{ title: '名称', key: 'name', width: 160 },
|
||||
{ title: '图标', key: 'iconUrl', width: 100, render: (row) => renderImage(row.iconUrl) },
|
||||
{
|
||||
title: '比例',
|
||||
key: 'commissionRate',
|
||||
width: 120,
|
||||
render: (row) => `${((row.commissionRate || 0) / 100).toFixed(2)}%`,
|
||||
},
|
||||
{ title: '简介', key: 'summary', width: 260, ellipsis: { tooltip: true } },
|
||||
{ title: '创建时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
|
||||
export const loadOptions = () => dict.loadOptions(['memberLevelOption']);
|
||||
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="n-layout-page-header"><n-card :bordered="false" title="资金调账">调账申请需双人审批;执行后生成不可变账变流水,普通会员编辑无法修改余额。</n-card></div>
|
||||
<n-card :bordered="false" class="proCard">
|
||||
<BasicForm ref="searchFormRef" @register="register" @submit="reloadTable" @reset="reloadTable" />
|
||||
<BasicTable ref="actionRef" :columns="columns" :request="loadDataTable" :row-key="(row)=>row.id" :action-column="actionColumn" :scroll-x="scrollX" :resize-height-offset="-10000">
|
||||
<template #tableTitle><n-button v-if="hasPermission(['/appMember/walletRequest'])" type="primary" @click="showCreate=true">发起调账</n-button></template>
|
||||
</BasicTable>
|
||||
</n-card>
|
||||
<n-modal v-model:show="showCreate" preset="dialog" title="发起资金调账" :show-icon="false">
|
||||
<n-alert type="warning" :show-icon="false">必须填写业务原因。金额以元录入,后端按分记账并校验幂等键。</n-alert>
|
||||
<n-form label-placement="top">
|
||||
<n-form-item label="会员ID"><n-input-number v-model:value="form.memberId" :min="1" /></n-form-item>
|
||||
<n-form-item label="调整类型"><n-select v-model:value="form.adjustType" :options="dict.getOptionUnRef('walletAdjustType')" /></n-form-item>
|
||||
<n-form-item label="资金方向"><n-select v-model:value="form.direction" :options="dict.getOptionUnRef('walletDirection')" /></n-form-item>
|
||||
<n-form-item label="金额(元)"><n-input-number v-model:value="amountYuan" :min="0.01" :precision="2" /></n-form-item>
|
||||
<n-form-item label="原因"><n-select v-model:value="form.reasonCode" :options="dict.getOptionUnRef('wallet_adjust_reason')" /></n-form-item>
|
||||
<n-form-item label="详细说明"><n-input v-model:value="form.reasonText" type="textarea" /></n-form-item>
|
||||
</n-form>
|
||||
<template #action><n-space><n-button @click="showCreate=false">取消</n-button><n-button type="primary" @click="createRequest">提交审批</n-button></n-space></template>
|
||||
</n-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { computed, h, onMounted, reactive, ref } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import { BasicTable, TableAction } from '@/components/Table';
|
||||
import { BasicForm, useForm } from '@/components/Form';
|
||||
import { usePermission } from '@/hooks/web/usePermission';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { adaTableScrollX } from '@/utils/hotgo';
|
||||
import { WalletList, WalletRequest, WalletExecute } from '@/api/appMember';
|
||||
import { columns, schemas, loadOptions } from './model';
|
||||
const message=useMessage(),dict=useDictStore();const {hasPermission}=usePermission();const actionRef=ref(),searchFormRef=ref<any>({}),showCreate=ref(false),amountYuan=ref(0);const form=reactive({memberId:0,adjustType:1,direction:1,reasonCode:'',reasonText:''});
|
||||
const [register]=useForm({gridProps:{cols:'1 s:1 m:2 l:3 xl:4'},labelWidth:80,schemas});const loadDataTable=(res)=>WalletList({...searchFormRef.value?.formModel,...res});const reloadTable=()=>actionRef.value?.reload();
|
||||
const actionColumn=reactive({width:120,title:'操作',key:'action',fixed:'right',render:(row)=>h(TableAction as any,{style:'button',actions:[{label:'执行',disabled:row.requestStatus===4,onClick:()=>execute(row),auth:['/appMember/walletExecute']}]})});const scrollX=computed(()=>adaTableScrollX(columns,actionColumn.width));
|
||||
async function createRequest(){const r:any=await WalletRequest({...form,amount:Math.round(amountYuan.value*100),idempotencyKey:`wallet-${form.memberId}-${Date.now()}`});message.success(`已提交审批:${r.approvalNo}`);showCreate.value=false;reloadTable()}
|
||||
async function execute(row){await WalletExecute({id:row.id});message.success('调账执行成功,账变流水已生成');reloadTable()}
|
||||
onMounted(loadOptions);
|
||||
</script>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { computed, h, ref } from 'vue';
|
||||
import { NTag } from 'naive-ui';
|
||||
import { FormSchema } from '@/components/Form';
|
||||
import { useDictStore } from '@/store/modules/dict';
|
||||
import { defRangeShortcuts } from '@/utils/dateUtil';
|
||||
const dict = useDictStore();
|
||||
export const schemas = ref<FormSchema[]>([
|
||||
{ field: 'memberId', component: 'NInputNumber', label: '会员ID', componentProps: { min: 1, clearable: true } },
|
||||
{ field: 'requestStatus', component: 'NSelect', label: '申请状态', componentProps: { options: computed(() => dict.getOptionUnRef('walletRequestStatus')), clearable: true } },
|
||||
{ field: 'createdAt', component: 'NDatePicker', label: '申请时间', componentProps: { type: 'datetimerange', clearable: true, shortcuts: defRangeShortcuts() } },
|
||||
]);
|
||||
const tag = (type, value) => h(NTag, { type: dict.getType(type, value), bordered: false }, { default: () => dict.getLabel(type, value) || '-' });
|
||||
export const columns = [
|
||||
{ title: '申请单号', key: 'requestNo', width: 210 }, { title: '会员ID', key: 'memberId', width: 90 },
|
||||
{ title: '用户名', key: 'username', width: 120 }, { title: '手机号', key: 'mobileMask', width: 130 },
|
||||
{ title: '类型', key: 'adjustType', width: 110, render: (r) => tag('walletAdjustType', r.adjustType) },
|
||||
{ title: '方向', key: 'direction', width: 90, render: (r) => tag('walletDirection', r.direction) },
|
||||
{ title: '金额', key: 'amount', width: 120, render: (r) => `¥${((r.amount || 0) / 100).toFixed(2)}` },
|
||||
{ title: '原因', key: 'reasonText', width: 220, ellipsis: { tooltip: true } },
|
||||
{ title: '状态', key: 'requestStatus', width: 100, render: (r) => tag('walletRequestStatus', r.requestStatus) },
|
||||
{ title: '审批ID', key: 'approvalRequestId', width: 100 }, { title: '账变ID', key: 'ledgerId', width: 90 },
|
||||
{ title: '申请时间', key: 'createdAt', width: 180 },
|
||||
];
|
||||
export const loadOptions = () => dict.loadOptions(['walletAdjustType', 'walletDirection', 'walletRequestStatus', 'wallet_adjust_reason']);
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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']);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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,48 @@
|
||||
<template>
|
||||
<div ref="chartRef" class="business-trend-chart"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { nextTick, ref, watch, Ref } from 'vue';
|
||||
import { useECharts } from '@/hooks/web/useECharts';
|
||||
|
||||
const props = defineProps<{ data: any[]; masked: boolean }>();
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>);
|
||||
|
||||
watch(
|
||||
() => [props.data, props.masked],
|
||||
async () => {
|
||||
await nextTick();
|
||||
const dates = props.data.map((item) => item.date.slice(5));
|
||||
const toYuan = (value: number) => Number((value / 100).toFixed(2));
|
||||
setOptions({
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
valueFormatter: (value) => (props.masked ? '无权限' : `¥${Number(value).toLocaleString()}`),
|
||||
},
|
||||
legend: { data: ['订单金额', '成功充值', '成功提现', '任务佣金'] },
|
||||
grid: { left: 24, right: 20, top: 48, bottom: 20, containLabel: true },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: dates },
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { formatter: (value) => (props.masked ? '-' : `¥${value}`) },
|
||||
},
|
||||
series: [
|
||||
{ name: '订单金额', type: 'line', smooth: true, data: props.data.map((item) => toYuan(item.orderAmount)) },
|
||||
{ name: '成功充值', type: 'line', smooth: true, data: props.data.map((item) => toYuan(item.rechargeAmount)) },
|
||||
{ name: '成功提现', type: 'line', smooth: true, data: props.data.map((item) => toYuan(item.withdrawalAmount)) },
|
||||
{ name: '任务佣金', type: 'line', smooth: true, data: props.data.map((item) => toYuan(item.commissionAmount)) },
|
||||
],
|
||||
});
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.business-trend-chart {
|
||||
width: 100%;
|
||||
height: 360px;
|
||||
}
|
||||
</style>
|
||||
@@ -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,760 @@
|
||||
<template>
|
||||
<div class="mall-dashboard">
|
||||
<n-card :bordered="false" class="dashboard-toolbar">
|
||||
<div class="toolbar-row">
|
||||
<div>
|
||||
<div class="toolbar-title-row">
|
||||
<h2 class="dashboard-title">商城统计</h2>
|
||||
<n-tag size="small" :type="summary.dataReady ? 'success' : 'warning'">
|
||||
{{ summary.dataReady ? '数据已更新' : '暂无汇总数据' }}
|
||||
</n-tag>
|
||||
</div>
|
||||
<n-space size="small" align="center" class="dashboard-meta">
|
||||
<span>数据截至:{{ summary.dataAsOf || '-' }}</span>
|
||||
<span>刷新时间:{{ summary.generatedAt || '-' }}</span>
|
||||
<span>延迟:{{ formatDelay(summary.dataDelaySeconds) }}</span>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-space align="center" class="toolbar-actions">
|
||||
<n-button-group>
|
||||
<n-button
|
||||
v-for="item in presets"
|
||||
:key="item.key"
|
||||
size="small"
|
||||
:type="activePreset === item.key ? 'primary' : 'default'"
|
||||
@click="applyPreset(item.key)"
|
||||
>
|
||||
{{ item.label }}
|
||||
</n-button>
|
||||
</n-button-group>
|
||||
<n-date-picker
|
||||
v-model:formatted-value="dateRange"
|
||||
type="daterange"
|
||||
value-format="yyyy-MM-dd"
|
||||
size="small"
|
||||
clearable
|
||||
class="range-picker"
|
||||
@update:formatted-value="handleCustomRange"
|
||||
/>
|
||||
<n-button size="small" type="primary" :loading="loading" @click="loadSummary">
|
||||
<template #icon>
|
||||
<n-icon><ReloadOutlined /></n-icon>
|
||||
</template>
|
||||
刷新
|
||||
</n-button>
|
||||
</n-space>
|
||||
</div>
|
||||
<n-alert v-if="summary.warning" type="warning" class="dashboard-warning" show-icon>
|
||||
{{ summary.warning }}
|
||||
</n-alert>
|
||||
</n-card>
|
||||
|
||||
<n-spin :show="loading" description="正在汇总商城数据...">
|
||||
<section class="dashboard-section">
|
||||
<div class="section-heading">
|
||||
<h3>商城统计</h3>
|
||||
<span>{{ selectedRangeText }}</span>
|
||||
</div>
|
||||
<div class="metric-grid">
|
||||
<article
|
||||
v-for="card in dashboardCards"
|
||||
:key="card.key"
|
||||
class="metric-card"
|
||||
:class="[card.theme, { 'metric-card-clickable': card.routeName }]"
|
||||
@click="openMetric(card)"
|
||||
>
|
||||
<div class="metric-content">
|
||||
<div class="metric-title">{{ card.title }}</div>
|
||||
<div class="metric-value">{{ card.value }}</div>
|
||||
<div class="metric-detail">
|
||||
<span>{{ card.primaryDetail }}</span>
|
||||
<span>{{ card.secondaryDetail }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<n-icon class="metric-icon" :component="card.icon" />
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="dashboard-section channel-section">
|
||||
<div class="section-heading">
|
||||
<h3>收付渠道</h3>
|
||||
<span>按渠道核对充值与提现数据</span>
|
||||
</div>
|
||||
<n-empty v-if="!summary.channels.length" description="暂无渠道汇总数据" />
|
||||
<n-card
|
||||
v-for="channel in orderedChannels"
|
||||
:key="channel.channel"
|
||||
:bordered="false"
|
||||
class="channel-card"
|
||||
>
|
||||
<template #header>
|
||||
<div class="channel-title">
|
||||
<n-icon :component="channelIcon(channel.channel)" />
|
||||
<span>{{ channelName(channel) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="channel-table-wrap">
|
||||
<n-table :single-line="false" size="small" class="channel-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>统计周期</th>
|
||||
<th>充值笔数</th>
|
||||
<th>充值金额(元)</th>
|
||||
<th>提现笔数</th>
|
||||
<th>提现金额(元)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ currentColumnTitle }}</td>
|
||||
<td>{{ formatCount(channel.rechargeCount) }}</td>
|
||||
<td>{{ formatChannelAmount(channel, 'rechargeAmount') }}</td>
|
||||
<td>{{ formatCount(channel.withdrawalCount) }}</td>
|
||||
<td>{{ formatChannelAmount(channel, 'withdrawalAmount') }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ previousColumnTitle }}</td>
|
||||
<td>{{ formatCount(channel.previousRechargeCount) }}</td>
|
||||
<td>{{ formatChannelAmount(channel, 'previousRechargeAmount') }}</td>
|
||||
<td>{{ formatCount(channel.previousWithdrawalCount) }}</td>
|
||||
<td>{{ formatChannelAmount(channel, 'previousWithdrawalAmount') }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</n-table>
|
||||
</div>
|
||||
</n-card>
|
||||
</section>
|
||||
</n-spin>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, reactive, ref, type Component } from 'vue';
|
||||
import { useMessage } from 'naive-ui';
|
||||
import {
|
||||
AccountBookOutlined,
|
||||
AlipayOutlined,
|
||||
BankOutlined,
|
||||
MoneyCollectOutlined,
|
||||
ProfileOutlined,
|
||||
ReloadOutlined,
|
||||
RiseOutlined,
|
||||
ShoppingOutlined,
|
||||
TeamOutlined,
|
||||
TrophyOutlined,
|
||||
UsergroupAddOutlined,
|
||||
WalletOutlined,
|
||||
WechatOutlined,
|
||||
} from '@vicons/antd';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { getDashboardSummary } from '@/api/dashboard/console';
|
||||
|
||||
interface DashboardMetric {
|
||||
code: string;
|
||||
title: string;
|
||||
value: number;
|
||||
previousValue: number;
|
||||
changeRate: number;
|
||||
valueType: 'count' | 'amount';
|
||||
masked: boolean;
|
||||
description: string;
|
||||
routeName: string;
|
||||
}
|
||||
|
||||
interface DashboardChannel {
|
||||
channel: string;
|
||||
label: string;
|
||||
rechargeCount: number;
|
||||
rechargeAmount: number;
|
||||
withdrawalCount: number;
|
||||
withdrawalAmount: number;
|
||||
previousRechargeCount: number;
|
||||
previousRechargeAmount: number;
|
||||
previousWithdrawalCount: number;
|
||||
previousWithdrawalAmount: number;
|
||||
masked: boolean;
|
||||
}
|
||||
|
||||
interface CardDefinition {
|
||||
key: string;
|
||||
code: string;
|
||||
valueType: 'count' | 'amount';
|
||||
title: string;
|
||||
theme: string;
|
||||
icon: Component;
|
||||
secondaryCode?: string;
|
||||
secondaryTitle?: string;
|
||||
}
|
||||
|
||||
interface DashboardCard extends CardDefinition {
|
||||
value: string;
|
||||
primaryDetail: string;
|
||||
secondaryDetail: string;
|
||||
routeName: string;
|
||||
}
|
||||
|
||||
type ChannelAmountKey =
|
||||
| 'rechargeAmount'
|
||||
| 'withdrawalAmount'
|
||||
| 'previousRechargeAmount'
|
||||
| 'previousWithdrawalAmount';
|
||||
|
||||
const router = useRouter();
|
||||
const message = useMessage();
|
||||
const loading = ref(false);
|
||||
const activePreset = ref('today');
|
||||
const dateRange = ref<[string, string] | null>(null);
|
||||
const presets = [
|
||||
{ key: 'today', label: '今日' },
|
||||
{ key: 'yesterday', label: '昨日' },
|
||||
{ key: 'last7', label: '近7日' },
|
||||
{ key: 'last30', label: '近30日' },
|
||||
];
|
||||
const summary = reactive<{
|
||||
metrics: DashboardMetric[];
|
||||
channels: DashboardChannel[];
|
||||
dataReady: boolean;
|
||||
dataDelaySeconds: number;
|
||||
dataAsOf?: string;
|
||||
generatedAt?: string;
|
||||
warning?: string;
|
||||
}>({
|
||||
metrics: [],
|
||||
channels: [],
|
||||
dataReady: false,
|
||||
dataDelaySeconds: 0,
|
||||
});
|
||||
|
||||
const cardDefinitions: CardDefinition[] = [
|
||||
{
|
||||
key: 'member-total',
|
||||
code: 'member_total',
|
||||
valueType: 'count',
|
||||
title: '用户总量',
|
||||
theme: 'theme-rose',
|
||||
icon: UsergroupAddOutlined,
|
||||
secondaryCode: 'member_new',
|
||||
secondaryTitle: '新增用户',
|
||||
},
|
||||
{
|
||||
key: 'recharge-member',
|
||||
code: 'recharge_member',
|
||||
valueType: 'count',
|
||||
title: '充值用户总量',
|
||||
theme: 'theme-coral',
|
||||
icon: TeamOutlined,
|
||||
},
|
||||
{
|
||||
key: 'product-total',
|
||||
code: 'product_total',
|
||||
valueType: 'count',
|
||||
title: '商品总量',
|
||||
theme: 'theme-sky',
|
||||
icon: ShoppingOutlined,
|
||||
},
|
||||
{
|
||||
key: 'trade-order-count',
|
||||
code: 'trade_order',
|
||||
valueType: 'count',
|
||||
title: '订单总量',
|
||||
theme: 'theme-purple',
|
||||
icon: ProfileOutlined,
|
||||
},
|
||||
{
|
||||
key: 'trade-order-amount',
|
||||
code: 'trade_order',
|
||||
valueType: 'amount',
|
||||
title: '订单总金额',
|
||||
theme: 'theme-indigo',
|
||||
icon: AccountBookOutlined,
|
||||
},
|
||||
{
|
||||
key: 'recharge-success',
|
||||
code: 'recharge_success',
|
||||
valueType: 'amount',
|
||||
title: '用户充值',
|
||||
theme: 'theme-gold',
|
||||
icon: MoneyCollectOutlined,
|
||||
},
|
||||
{
|
||||
key: 'withdrawal-success',
|
||||
code: 'withdrawal_success',
|
||||
valueType: 'amount',
|
||||
title: '用户提现',
|
||||
theme: 'theme-orange',
|
||||
icon: WalletOutlined,
|
||||
},
|
||||
{
|
||||
key: 'commission',
|
||||
code: 'commission',
|
||||
valueType: 'amount',
|
||||
title: '抢单佣金',
|
||||
theme: 'theme-wine',
|
||||
icon: TrophyOutlined,
|
||||
},
|
||||
{
|
||||
key: 'interest-transfer',
|
||||
code: 'interest_transfer',
|
||||
valueType: 'amount',
|
||||
title: '利息宝转入',
|
||||
theme: 'theme-green',
|
||||
icon: RiseOutlined,
|
||||
},
|
||||
{
|
||||
key: 'referral-commission',
|
||||
code: 'referral_commission',
|
||||
valueType: 'amount',
|
||||
title: '下级返佣',
|
||||
theme: 'theme-salmon',
|
||||
icon: TeamOutlined,
|
||||
},
|
||||
{
|
||||
key: 'wallet-available',
|
||||
code: 'wallet_available',
|
||||
valueType: 'amount',
|
||||
title: '用户总余额',
|
||||
theme: 'theme-teal',
|
||||
icon: BankOutlined,
|
||||
},
|
||||
];
|
||||
|
||||
const selectedRangeText = computed(() => {
|
||||
if (!dateRange.value) return '请选择统计周期';
|
||||
if (dateRange.value[0] === dateRange.value[1]) return dateRange.value[0];
|
||||
return `${dateRange.value[0]} 至 ${dateRange.value[1]}`;
|
||||
});
|
||||
|
||||
const currentColumnTitle = computed(() =>
|
||||
activePreset.value === 'today' ? '今日' : activePreset.value === 'yesterday' ? '昨日' : '本期'
|
||||
);
|
||||
|
||||
const previousColumnTitle = computed(() => {
|
||||
if (activePreset.value === 'today') return '昨日';
|
||||
if (activePreset.value === 'yesterday') return '前日';
|
||||
return '上期';
|
||||
});
|
||||
|
||||
const dashboardCards = computed<DashboardCard[]>(() =>
|
||||
cardDefinitions.map((definition) => {
|
||||
const metric = findMetric(definition.code, definition.valueType);
|
||||
const secondaryMetric = definition.secondaryCode
|
||||
? findMetric(definition.secondaryCode, 'count')
|
||||
: undefined;
|
||||
const masked = metric?.masked ?? false;
|
||||
const currentValue = metric?.value ?? 0;
|
||||
const previousValue = metric?.previousValue ?? 0;
|
||||
const rate = metric?.changeRate ?? 0;
|
||||
const ratePrefix = rate > 0 ? '+' : '';
|
||||
|
||||
return {
|
||||
...definition,
|
||||
value: formatMetricValue(currentValue, definition.valueType, masked),
|
||||
primaryDetail: secondaryMetric
|
||||
? `${definition.secondaryTitle} ${formatMetricValue(
|
||||
secondaryMetric.value,
|
||||
'count',
|
||||
secondaryMetric.masked
|
||||
)}`
|
||||
: `较上期 ${masked ? '••••' : `${ratePrefix}${rate.toFixed(2)}%`}`,
|
||||
secondaryDetail: `上期 ${formatMetricValue(previousValue, definition.valueType, masked)}`,
|
||||
routeName: metric?.routeName || '',
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const orderedChannels = computed(() => {
|
||||
const order = ['wechat', 'alipay', 'bank'];
|
||||
return [...summary.channels].sort((left, right) => {
|
||||
const leftIndex = order.indexOf(left.channel);
|
||||
const rightIndex = order.indexOf(right.channel);
|
||||
return (
|
||||
(leftIndex < 0 ? order.length : leftIndex) - (rightIndex < 0 ? order.length : rightIndex)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function findMetric(code: string, valueType: 'count' | 'amount') {
|
||||
return summary.metrics.find((item) => item.code === code && item.valueType === valueType);
|
||||
}
|
||||
|
||||
function formatDate(date: Date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function applyPreset(key: string) {
|
||||
activePreset.value = key;
|
||||
const today = new Date();
|
||||
const start = new Date(today);
|
||||
const end = new Date(today);
|
||||
if (key === 'yesterday') {
|
||||
start.setDate(start.getDate() - 1);
|
||||
end.setDate(end.getDate() - 1);
|
||||
}
|
||||
if (key === 'last7') start.setDate(start.getDate() - 6);
|
||||
if (key === 'last30') start.setDate(start.getDate() - 29);
|
||||
dateRange.value = [formatDate(start), formatDate(end)];
|
||||
loadSummary();
|
||||
}
|
||||
|
||||
function handleCustomRange(value: [string, string] | null) {
|
||||
if (!value) return;
|
||||
activePreset.value = 'custom';
|
||||
loadSummary();
|
||||
}
|
||||
|
||||
async function loadSummary() {
|
||||
if (loading.value || !dateRange.value) return;
|
||||
loading.value = true;
|
||||
try {
|
||||
Object.assign(
|
||||
summary,
|
||||
await getDashboardSummary({
|
||||
startDate: dateRange.value[0],
|
||||
endDate: dateRange.value[1],
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
message.error('商城统计数据加载失败,请稍后重试');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatMetricValue(value: number, valueType: 'count' | 'amount', masked = false) {
|
||||
if (masked) return '••••••';
|
||||
if (valueType === 'amount') {
|
||||
return `¥${(Number(value || 0) / 100).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
})}`;
|
||||
}
|
||||
return Number(value || 0).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatCount(value: number) {
|
||||
return Number(value || 0).toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatChannelAmount(channel: DashboardChannel, key: ChannelAmountKey) {
|
||||
if (channel.masked) return '无权限';
|
||||
return (Number(channel[key] || 0) / 100).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
});
|
||||
}
|
||||
|
||||
function formatDelay(seconds: number) {
|
||||
if (!summary.dataReady) return '-';
|
||||
if (seconds < 60) return `${seconds}秒`;
|
||||
if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟`;
|
||||
return `${Math.floor(seconds / 3600)}小时`;
|
||||
}
|
||||
|
||||
function openMetric(card: DashboardCard) {
|
||||
if (!card.routeName) return;
|
||||
router
|
||||
.push({
|
||||
name: card.routeName,
|
||||
query: { startDate: dateRange.value?.[0], endDate: dateRange.value?.[1] },
|
||||
})
|
||||
.catch(() => message.warning('对应明细模块尚未启用'));
|
||||
}
|
||||
|
||||
function channelName(channel: DashboardChannel) {
|
||||
const names: Record<string, string> = {
|
||||
wechat: '微信',
|
||||
alipay: '支付宝',
|
||||
bank: '银行卡',
|
||||
};
|
||||
return names[channel.channel] || channel.label || channel.channel;
|
||||
}
|
||||
|
||||
function channelIcon(channel: string) {
|
||||
const icons: Record<string, Component> = {
|
||||
wechat: WechatOutlined,
|
||||
alipay: AlipayOutlined,
|
||||
bank: BankOutlined,
|
||||
};
|
||||
return icons[channel] || BankOutlined;
|
||||
}
|
||||
|
||||
onMounted(() => applyPreset('today'));
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mall-dashboard {
|
||||
min-height: 100%;
|
||||
padding: 14px;
|
||||
background: #f4f5f7;
|
||||
}
|
||||
|
||||
.dashboard-toolbar {
|
||||
margin-bottom: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.toolbar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.toolbar-title-row,
|
||||
.channel-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.dashboard-title,
|
||||
.section-heading h3 {
|
||||
margin: 0;
|
||||
color: #262626;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.dashboard-meta {
|
||||
margin-top: 8px;
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.range-picker {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.dashboard-warning {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.dashboard-section {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 46px;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.section-heading h3 {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.section-heading span {
|
||||
color: #8c8c8c;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
position: relative;
|
||||
min-height: 150px;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 4px 12px rgba(34, 39, 52, 0.08);
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.metric-card-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.metric-card-clickable:hover {
|
||||
box-shadow: 0 8px 20px rgba(34, 39, 52, 0.18);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 23px 26px 19px;
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-size: 14px;
|
||||
opacity: 0.94;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
margin-top: 13px;
|
||||
overflow: hidden;
|
||||
font-size: 27px;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metric-detail {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 18px;
|
||||
margin-top: 18px;
|
||||
font-size: 12px;
|
||||
opacity: 0.86;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
bottom: 13px;
|
||||
z-index: 1;
|
||||
color: rgba(255, 255, 255, 0.2);
|
||||
font-size: 91px;
|
||||
}
|
||||
|
||||
.theme-rose {
|
||||
background: linear-gradient(112deg, #d83287, #ec706f);
|
||||
}
|
||||
|
||||
.theme-coral {
|
||||
background: linear-gradient(112deg, #d94b8d, #ed7470);
|
||||
}
|
||||
|
||||
.theme-sky {
|
||||
background: linear-gradient(112deg, #4a91d9, #69b8be);
|
||||
}
|
||||
|
||||
.theme-purple {
|
||||
background: linear-gradient(112deg, #7655c7, #a63ed0);
|
||||
}
|
||||
|
||||
.theme-indigo {
|
||||
background: linear-gradient(112deg, #2624d0, #7b79af);
|
||||
}
|
||||
|
||||
.theme-gold {
|
||||
background: linear-gradient(112deg, #dc961b, #f2c914);
|
||||
}
|
||||
|
||||
.theme-orange {
|
||||
background: linear-gradient(112deg, #dd7a17, #db5016);
|
||||
}
|
||||
|
||||
.theme-wine {
|
||||
background: linear-gradient(112deg, #9f395d, #4d1c32);
|
||||
}
|
||||
|
||||
.theme-green {
|
||||
background: linear-gradient(112deg, #409a67, #70db69);
|
||||
}
|
||||
|
||||
.theme-salmon {
|
||||
background: linear-gradient(112deg, #9d9e84, #dc766d);
|
||||
}
|
||||
|
||||
.theme-teal {
|
||||
background: linear-gradient(112deg, #278799, #42bea0);
|
||||
}
|
||||
|
||||
.channel-section {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.channel-card {
|
||||
margin-bottom: 14px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.channel-title {
|
||||
color: #333;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.channel-title :deep(.n-icon) {
|
||||
color: #666;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.channel-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.channel-table {
|
||||
min-width: 760px;
|
||||
}
|
||||
|
||||
.channel-table :deep(th) {
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.channel-table :deep(td),
|
||||
.channel-table :deep(th) {
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.toolbar-row {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.mall-dashboard {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.toolbar-actions {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.range-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
min-height: 140px;
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user